From f13fad4d95461a86833e6274cb3cd07b1282eeac Mon Sep 17 00:00:00 2001 From: Mehryar Mansoor <5124019+mmansoor-magento@users.noreply.github.com> Date: Wed, 16 Oct 2019 09:51:06 -0700 Subject: [PATCH 001/157] Initial commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..5eb2b62c --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# magento-cloud-patches \ No newline at end of file From ddf9889b23104da96479945b3c8b6f40984cb1c8 Mon Sep 17 00:00:00 2001 From: Yevhenii Pyltiai Date: Thu, 17 Oct 2019 15:43:39 -0500 Subject: [PATCH 002/157] Add License & Update Readme (#1) - Added LICENSE.TXT - Added COPYING.TXT - Updated README.md --- COPYING.txt | 4 ++++ LICENSE.txt | 47 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 28 +++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 COPYING.txt create mode 100644 LICENSE.txt diff --git a/COPYING.txt b/COPYING.txt new file mode 100644 index 00000000..ad4ea641 --- /dev/null +++ b/COPYING.txt @@ -0,0 +1,4 @@ +Copyright © 2013-present Magento, Inc. + +Each Magento source file included in this distribution is licensed under OSL-3.0 license. +See the `LICENSE.txt` file for the full text of the OSL-3.0 license or visit http://opensource.org/licenses/osl-3.0.php diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..1a7fab51 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,47 @@ +Open Software License v. 3.0 (OSL-3.0) + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + +1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + a) to reproduce the Original Work in copies, either alone or as part of a collective work; + + b) to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + c) to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + d) to perform the Original Work publicly; and + + e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + +5) External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + +6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + +9) Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + +10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + +12) Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + +13) Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + +16) Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/README.md b/README.md index 5eb2b62c..9a27b215 100644 --- a/README.md +++ b/README.md @@ -1 +1,27 @@ -# magento-cloud-patches \ No newline at end of file +# Magento Cloud Patches + +## Welcome +The Magento Cloud Patches package is a set of patches, previously used within the ece-tools package to improve the integration of all Magento versions with Cloud environments and to deliver critical fixes quickly. Beginning with ece-tools version `2002.0.22` and `2002.1.0`, the Magento Cloud Patches package is required so that patches can be applied with same workflow used in ece-tools `2002.0.21` and earlier versions. + +## Contributing to Magento Cloud Patches Code Base +You can submit pull requests to fix potential bugs. If you find a bug or have a new suggestion, let us know by creating a Github issue. + +**Note:** This repository is not an official support channel. To receive project-specific help, submit a support ticket using the [Magento Support Portal](https://support.magento.com). Any support-related issues opened in this repository will be closed with a request to open a support ticket. + +# Magento Cloud Suite +The Magento Cloud Suite includes a set of packages designed to deploy and manage Magento Commerce installations on the Cloud platform. +- The [ece-tools package](https://github.com/magento/ece-tools) - A set of scripts and tools designed to manage and deploy Cloud projects +- [Magento Cloud Components](https://github.com/magento/magento-cloud-components) package - Extended Magento Commerce core functionality for sites deployed on the Cloud platform +- [Magento Cloud Docker](https://github.com/magento/magento-cloud-docker) package - Functionality and Docker images to deploy Magento Commerce to a local Cloud environment +- [Magento Cloud Patches](https://github.com/magento/magento-cloud-patches) package - A set of patches which improve the integration of all Magento versions with Cloud environments + +## Useful Resources +- [Release Notes](https://github.com/magento/magento-cloud-patches/releases) +- [Magento Cloud Guide DevDocs](https://devdocs.magento.com/guides/v2.3/cloud/bk-cloud.html) +- [Cloud Knowledge Base and Support](https://support.magento.com) +- [Cloud Slack Channel](https://magentocommeng.slack.com) (join #cloud and #cloud-docker) + +## License +Each Magento source file included in this distribution is licensed under OSL-3.0 license. + +See [LICENSE.txt](https://github.com/magento/magento-cloud-patches/blob/develop/LICENSE.txt) for the full text of the [Open Software License v. 3.0 (OSL-3.0)](http://opensource.org/licenses/osl-3.0.php) From 173ee29e4eac7787e8b3ee03cfc75e29fbee8359 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak Date: Tue, 29 Oct 2019 09:14:23 -0500 Subject: [PATCH 003/157] MAGECLOUD-4458: De-compose All Patches from ECE-Tools (#2) --- .github/ISSUE_TEMPLATE.md | 35 + .github/PULL_REQUEST_TEMPLATE.md | 37 + .gitignore | 6 + .travis.yml | 18 + autoload.php | 19 + bin/.gitignore | 1 + bin/ece-patches | 10 + bootstrap.php | 10 + composer.json | 46 + config/services.xml | 17 + patches.json | 230 ++++ ..._import_during_ece_tools_dump__2.2.2.patch | 15 + ..._fix_session_manager_locking__2.1.10.patch | 24 + ...__fix_session_manager_locking__2.2.0.patch | 24 + ...igure_scd_on_demand_for_cloud__2.1.4.patch | 149 +++ ...igure_scd_on_demand_for_cloud__2.2.0.patch | 204 +++ ...601__trim_static_content_path__2.1.4.patch | 11 + ...overhaul_cron_implementation__2.1.13.patch | 907 +++++++++++++ ...overhaul_cron_implementation__2.1.14.patch | 902 +++++++++++++ ..._overhaul_cron_implementation__2.1.4.patch | 924 +++++++++++++ ..._overhaul_cron_implementation__2.1.5.patch | 924 +++++++++++++ ..._overhaul_cron_implementation__2.2.0.patch | 616 +++++++++ ..._overhaul_cron_implementation__2.2.2.patch | 595 +++++++++ ..._overhaul_cron_implementation__2.2.4.patch | 601 +++++++++ ...respect_minification_override__2.1.4.patch | 23 + ...respect_minification_override__2.2.0.patch | 20 + ...xml_and_robotstxt_generation__2.1.11.patch | 161 +++ ...pxml_and_robotstxt_generation__2.1.4.patch | 165 +++ ...event_deadlock_during_db_dump__2.2.0.patch | 13 + ...le_editing_when_scd_on_demand__2.2.0.patch | 345 +++++ ...rsion_error_during_deployment__2.2.0.patch | 16 + ...ating_factories_in_extensions__2.2.0.patch | 159 +++ ...7__resolve_issues_with_cron_schedule.patch | 28 + ..._run_cron_when_it_is_disabled__2.1.4.patch | 62 + ...mer_runners_on_cloud_clusters__2.2.0.patch | 39 + ...mer_runners_on_cloud_clusters__2.2.4.patch | 13 + ...check_for_console_application__2.2.0.patch | 61 + ...check_for_console_application__2.2.6.patch | 53 + ...OUD-2521__zendframework1_use_TLS_1.2.patch | 58 + ...lation_without_admin_creation__2.1.4.patch | 152 +++ ...lation_without_admin_creation__2.2.2.patch | 198 +++ ...ix_timezone_parsing_for_cron__2.1.13.patch | 122 ++ ...fix_timezone_parsing_for_cron__2.1.4.patch | 170 +++ ...fix_timezone_parsing_for_cron__2.1.5.patch | 170 +++ ...793__fix_monolog_slack_handler_2.1.x.patch | 164 +++ ...olated_connections_mechanism__2.1.13.patch | 76 ++ ...solated_connections_mechanism__2.1.4.patch | 79 ++ ...solated_connections_mechanism__2.1.5.patch | 79 ++ ...solated_connections_mechanism__2.2.0.patch | 75 ++ ...D-2822__configure_max_execution_time.patch | 107 ++ ...__configure_max_execution_time_2.3.1.patch | 89 ++ ...850_fix_amazon_payment_module__2.2.6.patch | 177 +++ ...ix_redis_slave_configuration__2.1.16.patch | 40 + ...fix_redis_slave_configuration__2.2.3.patch | 40 + ...fix_redis_slave_configuration__2.3.0.patch | 40 + ...add_zookeeper_and_flock_locks__2.2.5.patch | 1068 +++++++++++++++ ...add_zookeeper_and_flock_locks__2.3.0.patch | 1055 +++++++++++++++ ...ECLOUD-3611__multi_thread_scd__2.2.0.patch | 63 + ...ECLOUD-3611__multi_thread_scd__2.2.4.patch | 245 ++++ ...ECLOUD-3611__multi_thread_scd__2.3.0.patch | 63 + ...ECLOUD-3611__multi_thread_scd__2.3.2.patch | 71 + ...or_code_fix_for_setup_upgrade__2.2.0.patch | 32 + ...or_code_fix_for_setup_upgrade__2.2.1.patch | 33 + ...or_code_fix_for_setup_upgrade__2.3.0.patch | 15 + ...mer_runners_on_cloud_clusters__2.2.5.patch | 573 ++++++++ ...mer_runners_on_cloud_clusters__2.2.6.patch | 572 ++++++++ ...mer_runners_on_cloud_clusters__2.2.7.patch | 548 ++++++++ ...mer_runners_on_cloud_clusters__2.2.8.patch | 496 +++++++ ...mer_runners_on_cloud_clusters__2.3.0.patch | 511 +++++++ ...mer_runners_on_cloud_clusters__2.3.1.patch | 447 +++++++ ...nsumers_if_the_queue_is_empty__2.2.0.patch | 170 +++ ...nsumers_if_the_queue_is_empty__2.3.2.patch | 181 +++ ...unnecessary_permission_checks__2.1.4.patch | 43 + ...unnecessary_permission_checks__2.2.0.patch | 43 + ...89__load_appropriate_js_files__2.1.4.patch | 26 + ..._avoid_nonexistent_setup_area__2.1.4.patch | 15 + ..._fix_enterprise_payment_codes__2.1.8.patch | 93 ++ ...ont_skip_setup_scoped_plugins__2.1.4.patch | 20 + ...__move_vendor_path_autoloader__2.1.4.patch | 37 + ...atic_assets_without_rewrites__2.1.17.patch | 58 + ...tatic_assets_without_rewrites__2.1.4.patch | 58 + ...ent_excessive_js_optimization__2.1.4.patch | 521 ++++++++ ...x_scd_with_multiple_languages__2.1.4.patch | 13 + ...essary_write_permission_check__2.1.4.patch | 20 + ...7097__fix_credis_pipeline_bug__2.1.4.patch | 12 + ..._image_resizing_after_upgrade__2.1.6.patch | 187 +++ ...ort_credis_forking_during_scd__2.1.4.patch | 169 +++ ...ort_credis_forking_during_scd__2.2.0.patch | 169 +++ ...2__reload_js_translation_data__2.2.0.patch | 11 + ...-84444__fix_mview_on_staging__2.1.10.patch | 1190 +++++++++++++++++ ...O-84444__fix_mview_on_staging__2.1.4.patch | 1172 ++++++++++++++++ ...O-84444__fix_mview_on_staging__2.1.5.patch | 1190 +++++++++++++++++ ...ix_complex_folder_js_bundling__2.2.0.patch | 13 + ...ix_complex_folder_js_bundling__2.1.4.patch | 13 + ...heck_of_directory_permissions__2.1.4.patch | 20 + ...8833__turn_off_google_chart_api__2.x.patch | 141 ++ patches/MC-5964__preauth_sql__2.1.4.patch | 12 + patches/MC-5964__preauth_sql__2.2.0.patch | 92 ++ patches/MC-5964__preauth_sql__2.3.0.patch | 123 ++ ..._asset_locking_race_condition__2.1.4.patch | 109 ++ ..._asset_locking_race_condition__2.2.0.patch | 80 ++ ...y_encode_characters_in_emails__2.1.4.patch | 12 + ...xer_fails_with_large_catalogs__1.0.3.patch | 88 ++ ...ault_stock_view_in_storefront__1.0.3.patch | 82 ++ ..._configurable-product-indexer__1.0.3.patch | 102 ++ ...exer__grouped-product-indexer__1.0.3.patch | 106 ++ ..._source_item_indexer__indexer__1.0.3.patch | 99 ++ ...ce_item_indexer__reservations__1.0.3.patch | 12 + ...x_oom_during_customer_import__2.1.11.patch | 104 ++ ...ix_oom_during_customer_import__2.1.4.patch | 110 ++ ...ix_oom_during_customer_import__2.2.0.patch | 110 ++ ...ix_oom_during_customer_import__2.2.4.patch | 102 ++ src/App/Container.php | 96 ++ src/App/GenericException.php | 26 + src/Application.php | 46 + src/Command/Apply.php | 71 + src/Command/Patch/Manager.php | 163 +++ src/Command/Patch/ManagerException.php | 17 + src/Filesystem/DirectoryList.php | 58 + src/Filesystem/FileList.php | 35 + src/Filesystem/FileNotFoundException.php | 17 + src/Filesystem/Filesystem.php | 66 + src/Patch/Applier.php | 170 +++ src/Patch/ApplierException.php | 17 + src/Shell/ProcessFactory.php | 44 + src/Test/Unit/Command/ApplyTest.php | 64 + src/Test/Unit/Command/Patch/ManagerTest.php | 209 +++ .../Patch/_files/m2-hotfixes/patch1.patch | 0 .../Patch/_files/m2-hotfixes/patch2.patch | 0 .../Patch/_files/m2-hotfixes/readme.md | 0 .../Unit/Filesystem/DirectoryListTest.php | 67 + src/Test/Unit/Filesystem/FileListTest.php | 51 + src/Test/Unit/Filesystem/_files/.gitignore | 0 src/Test/Unit/Patch/ApplierTest.php | 283 ++++ .../Sniffs/Directives/StrictTypesSniff.php | 120 ++ .../Whitespace/MultipleEmptyLinesSniff.php | 51 + tests/static/phpcs-ruleset.xml | 30 + tests/static/phpmd-ruleset.xml | 48 + tests/unit/.gitignore | 1 + tests/unit/phpunit.xml.dist | 27 + 140 files changed, 23816 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 autoload.php create mode 100644 bin/.gitignore create mode 100755 bin/ece-patches create mode 100644 bootstrap.php create mode 100644 composer.json create mode 100644 config/services.xml create mode 100644 patches.json create mode 100644 patches/MAGECLOUD-1567__fix_import_during_ece_tools_dump__2.2.2.patch create mode 100644 patches/MAGECLOUD-1582__fix_session_manager_locking__2.1.10.patch create mode 100644 patches/MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch create mode 100644 patches/MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.1.4.patch create mode 100644 patches/MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.2.0.patch create mode 100644 patches/MAGECLOUD-1601__trim_static_content_path__2.1.4.patch create mode 100644 patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch create mode 100644 patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch create mode 100644 patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch create mode 100644 patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch create mode 100644 patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch create mode 100644 patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch create mode 100644 patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.4.patch create mode 100644 patches/MAGECLOUD-1736__respect_minification_override__2.1.4.patch create mode 100644 patches/MAGECLOUD-1736__respect_minification_override__2.2.0.patch create mode 100644 patches/MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.11.patch create mode 100644 patches/MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.4.patch create mode 100644 patches/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch create mode 100644 patches/MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch create mode 100644 patches/MAGECLOUD-2173__the_recursion_error_during_deployment__2.2.0.patch create mode 100644 patches/MAGECLOUD-2209__write_logs_for_failed_process_of_generating_factories_in_extensions__2.2.0.patch create mode 100644 patches/MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch create mode 100644 patches/MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch create mode 100644 patches/MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.0.patch create mode 100644 patches/MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.4.patch create mode 100644 patches/MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.0.patch create mode 100644 patches/MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.6.patch create mode 100644 patches/MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch create mode 100644 patches/MAGECLOUD-2573__installation_without_admin_creation__2.1.4.patch create mode 100644 patches/MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch create mode 100644 patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.13.patch create mode 100644 patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.4.patch create mode 100644 patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.5.patch create mode 100644 patches/MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch create mode 100644 patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.13.patch create mode 100644 patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.4.patch create mode 100644 patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.5.patch create mode 100644 patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.2.0.patch create mode 100644 patches/MAGECLOUD-2822__configure_max_execution_time.patch create mode 100644 patches/MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch create mode 100644 patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch create mode 100644 patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.1.16.patch create mode 100644 patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.2.3.patch create mode 100644 patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.3.0.patch create mode 100644 patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.2.5.patch create mode 100644 patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch create mode 100644 patches/MAGECLOUD-3611__multi_thread_scd__2.2.0.patch create mode 100644 patches/MAGECLOUD-3611__multi_thread_scd__2.2.4.patch create mode 100644 patches/MAGECLOUD-3611__multi_thread_scd__2.3.0.patch create mode 100644 patches/MAGECLOUD-3611__multi_thread_scd__2.3.2.patch create mode 100644 patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.0.patch create mode 100644 patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.1.patch create mode 100644 patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.3.0.patch create mode 100644 patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.5.patch create mode 100644 patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.6.patch create mode 100644 patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.7.patch create mode 100644 patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.8.patch create mode 100644 patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch create mode 100644 patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch create mode 100644 patches/MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.2.0.patch create mode 100644 patches/MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.3.2.patch create mode 100644 patches/MAGECLOUD-414__remove_unnecessary_permission_checks__2.1.4.patch create mode 100644 patches/MAGECLOUD-414__remove_unnecessary_permission_checks__2.2.0.patch create mode 100644 patches/MAGECLOUD-589__load_appropriate_js_files__2.1.4.patch create mode 100644 patches/MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch create mode 100644 patches/MAGETWO-53941__fix_enterprise_payment_codes__2.1.8.patch create mode 100644 patches/MAGETWO-56675__dont_skip_setup_scoped_plugins__2.1.4.patch create mode 100644 patches/MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch create mode 100644 patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch create mode 100644 patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch create mode 100644 patches/MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch create mode 100644 patches/MAGETWO-63020__fix_scd_with_multiple_languages__2.1.4.patch create mode 100644 patches/MAGETWO-63032__skip_unnecessary_write_permission_check__2.1.4.patch create mode 100644 patches/MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch create mode 100644 patches/MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch create mode 100644 patches/MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch create mode 100644 patches/MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch create mode 100644 patches/MAGETWO-82752__reload_js_translation_data__2.2.0.patch create mode 100644 patches/MAGETWO-84444__fix_mview_on_staging__2.1.10.patch create mode 100644 patches/MAGETWO-84444__fix_mview_on_staging__2.1.4.patch create mode 100644 patches/MAGETWO-84444__fix_mview_on_staging__2.1.5.patch create mode 100644 patches/MAGETWO-84507__fix_complex_folder_js_bundling__2.2.0.patch create mode 100644 patches/MAGETWO-88336__fix_complex_folder_js_bundling__2.1.4.patch create mode 100644 patches/MAGETWO-93265__fix_depth_of_recursive_check_of_directory_permissions__2.1.4.patch create mode 100644 patches/MAGETWO-98833__turn_off_google_chart_api__2.x.patch create mode 100644 patches/MC-5964__preauth_sql__2.1.4.patch create mode 100644 patches/MC-5964__preauth_sql__2.2.0.patch create mode 100644 patches/MC-5964__preauth_sql__2.3.0.patch create mode 100644 patches/MDVA-2470__fix_asset_locking_race_condition__2.1.4.patch create mode 100644 patches/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch create mode 100644 patches/MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch create mode 100644 patches/MSI-2210__price_indexer_fails_with_large_catalogs__1.0.3.patch create mode 100644 patches/MSI-GH-2350__avoid_quering_inventory_default_stock_view_in_storefront__1.0.3.patch create mode 100644 patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__configurable-product-indexer__1.0.3.patch create mode 100644 patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__grouped-product-indexer__1.0.3.patch create mode 100644 patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__indexer__1.0.3.patch create mode 100644 patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__reservations__1.0.3.patch create mode 100644 patches/SET-36__fix_oom_during_customer_import__2.1.11.patch create mode 100644 patches/SET-36__fix_oom_during_customer_import__2.1.4.patch create mode 100644 patches/SET-36__fix_oom_during_customer_import__2.2.0.patch create mode 100644 patches/SET-36__fix_oom_during_customer_import__2.2.4.patch create mode 100644 src/App/Container.php create mode 100644 src/App/GenericException.php create mode 100644 src/Application.php create mode 100644 src/Command/Apply.php create mode 100644 src/Command/Patch/Manager.php create mode 100644 src/Command/Patch/ManagerException.php create mode 100644 src/Filesystem/DirectoryList.php create mode 100644 src/Filesystem/FileList.php create mode 100644 src/Filesystem/FileNotFoundException.php create mode 100644 src/Filesystem/Filesystem.php create mode 100644 src/Patch/Applier.php create mode 100644 src/Patch/ApplierException.php create mode 100644 src/Shell/ProcessFactory.php create mode 100644 src/Test/Unit/Command/ApplyTest.php create mode 100644 src/Test/Unit/Command/Patch/ManagerTest.php create mode 100644 src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch1.patch create mode 100644 src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch2.patch create mode 100644 src/Test/Unit/Command/Patch/_files/m2-hotfixes/readme.md create mode 100644 src/Test/Unit/Filesystem/DirectoryListTest.php create mode 100644 src/Test/Unit/Filesystem/FileListTest.php create mode 100644 src/Test/Unit/Filesystem/_files/.gitignore create mode 100644 src/Test/Unit/Patch/ApplierTest.php create mode 100644 tests/static/Sniffs/Directives/StrictTypesSniff.php create mode 100644 tests/static/Sniffs/Whitespace/MultipleEmptyLinesSniff.php create mode 100644 tests/static/phpcs-ruleset.xml create mode 100644 tests/static/phpmd-ruleset.xml create mode 100644 tests/unit/.gitignore create mode 100644 tests/unit/phpunit.xml.dist diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..12ad4e45 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,35 @@ + + +### Preconditions + +1. +2. + +### Steps to reproduce + +1. +2. +3. + +### Expected result + +1. [Screenshots, logs or description] + +### Actual result + +1. [Screenshots, logs or description] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..af5ee0bd --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ + + + + +### Description + + +### Fixed Issues (if relevant) + +1. magento/magento-cloud-patches#: Issue title +2. ... + +### Manual testing scenarios + +1. ... +2. ... + +### Contribution checklist + - [ ] Pull request has a meaningful description of its purpose + - [ ] All commits are accompanied by meaningful commit messages + - [ ] All new or changed code is covered with unit/integration tests (if applicable) + - [ ] All automated tests passed successfully (all builds on Travis CI are green) diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a3b63ba8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/.idea +/vendor +/composer.phar +/composer.lock +/auth.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..1104c7c3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +dist: xenial + +git: + depth: false + +language: php +php: + - '7.0' + - '7.1' + - '7.2' + - '7.3' + +install: composer update + +script: + - ./vendor/bin/phpcs ./src --standard=./tests/static/phpcs-ruleset.xml -p -n + - ./vendor/bin/phpmd ./src xml ./tests/static/phpmd-ruleset.xml + - ./vendor/bin/phpunit --configuration ./tests/unit diff --git a/autoload.php b/autoload.php new file mode 100644 index 00000000..8cff6975 --- /dev/null +++ b/autoload.php @@ -0,0 +1,19 @@ +run(); diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 00000000..f6081520 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,10 @@ + + + + + + + + + + + + + + + + + diff --git a/patches.json b/patches.json new file mode 100644 index 00000000..0a5ec42a --- /dev/null +++ b/patches.json @@ -0,0 +1,230 @@ +{ + "magento/magento2-base": { + "Fix asset locker race condition when using Redis": { + "2.1.4 - 2.1.14": "MDVA-2470__fix_asset_locking_race_condition__2.1.4.patch", + "2.2.0 - 2.2.5": "MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch" + }, + "Prevent redundant permissions check during build": { + "2.1.4 - 2.1.14": "MAGECLOUD-414__remove_unnecessary_permission_checks__2.1.4.patch", + "2.2.0 - 2.2.5": "MAGECLOUD-414__remove_unnecessary_permission_checks__2.2.0.patch" + }, + "Fix Redis issues with session manager locking": { + "2.1.10 - 2.1.13" : "MAGECLOUD-1582__fix_session_manager_locking__2.1.10.patch", + "2.2.0 - 2.2.1": "MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch" + }, + "Workaround app/etc not being available before the deploy phase": { + "~2.1.4": "MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch" + }, + "Allow static assets to be loaded without URL rewrites": { + "2.1.4 - 2.1.16": "MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch", + "~2.1.17": "MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch" + }, + "Don't attempt to use non-existent setup areas": { + "~2.1.4": "MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch" + }, + "Skip checking var/generation for write permissions when it doesn't affect the build process": { + "2.1.4 - 2.1.14": "MAGETWO-63032__skip_unnecessary_write_permission_check__2.1.4.patch" + }, + "Fix loading multiple plugins in the setup scope": { + "~2.1.4": "MAGETWO-56675__dont_skip_setup_scoped_plugins__2.1.4.patch" + }, + "Support SCD forking in the credis connector": { + "~2.1.4": "MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch", + "2.2.0 - 2.2.5" : "MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch" + }, + "Allow multiple languages to be specified for the SCD command": { + "2.1.4 - 2.1.7": "MAGETWO-63020__fix_scd_with_multiple_languages__2.1.4.patch" + }, + "Continue to load javascript assets in the admin panel when using particular build parameters": { + "2.1.4 - 2.1.7": "MAGECLOUD-589__load_appropriate_js_files__2.1.4.patch" + }, + "Handle special characters in email headers": { + "~2.1.4": "MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch", + "2.2.0": "MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch" + }, + "Enable SCD on demand in production": { + ">=2.1.4": "MAGECLOUD-1601__trim_static_content_path__2.1.4.patch", + "~2.1.4": "MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.1.4.patch", + "2.2.0 - 2.2.3": "MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.2.0.patch" + }, + "Respect user-specified minification settings": { + "~2.1.4": "MAGECLOUD-1736__respect_minification_override__2.1.4.patch", + "~2.2.0": "MAGECLOUD-1736__respect_minification_override__2.2.0.patch" + }, + "Process the application cron queue more reliably": { + "2.1.4": "MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch", + "2.1.5 - 2.1.12": "MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch", + "2.1.13": "MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch", + "~2.1.14": "MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch", + "2.2.0 - 2.2.1": "MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch", + "2.2.2 - 2.2.3": "MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch", + "2.2.4": "MAGECLOUD-1607__overhaul_cron_implementation__2.2.4.patch" + }, + "Add Zookeeper and flock locks": { + "2.2.5 - 2.2.8": "MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.2.5.patch", + "2.3.0 - 2.3.1": "MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch" + }, + "Reduce memory usage when importing customers and addresses": { + "2.1.4 - 2.1.10": "SET-36__fix_oom_during_customer_import__2.1.4.patch", + "2.1.11 - 2.1.12": "SET-36__fix_oom_during_customer_import__2.1.11.patch", + "2.2.0 - 2.2.3": "SET-36__fix_oom_during_customer_import__2.2.0.patch", + "2.2.4": "SET-36__fix_oom_during_customer_import__2.2.4.patch" + }, + "Add PayPal and Braintree TPV codes on checkout": { + "~2.1.8": "MAGETWO-53941__fix_enterprise_payment_codes__2.1.8.patch" + }, + "Fix Mview on staging environments": { + "2.1.4": "MAGETWO-84444__fix_mview_on_staging__2.1.4.patch", + "2.1.5 - 2.1.9": "MAGETWO-84444__fix_mview_on_staging__2.1.5.patch", + "2.1.10": "MAGETWO-84444__fix_mview_on_staging__2.1.10.patch" + }, + "Resize images properly after upgrading to 2.1.6": { + "2.1.6": "MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch" + }, + "Bundle javascript files even when other files are present": { + "2.1.4 - 2.1.12": "MAGETWO-88336__fix_complex_folder_js_bundling__2.1.4.patch", + "2.2.0 - 2.2.3": "MAGETWO-84507__fix_complex_folder_js_bundling__2.2.0.patch" + }, + "Allow ece-tools dumps to complete by fixing app:config:import": { + "2.2.2": "MAGECLOUD-1567__fix_import_during_ece_tools_dump__2.2.2.patch" + }, + "Unify robots.txt and sitemap.xml generation": { + "2.1.4 - 2.1.10": "MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.4.patch", + "~2.1.11": "MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.11.patch" + }, + "Fix javascript localization issues": { + "~2.1.4": "MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch", + "2.2.0 - 2.2.1": "MAGETWO-82752__reload_js_translation_data__2.2.0.patch" + }, + "Unlock locale editing when SCD on demand is enabled": { + "2.2.0 - 2.2.5": "MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch" + }, + "Allow DB dumps done with the support module to complete": { + "2.2.0 - 2.2.5": "MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch" + }, + "Write Logs for Failed Process of Generating Factories in Extensions": { + "2.2.0 - 2.2.5": "MAGECLOUD-2209__write_logs_for_failed_process_of_generating_factories_in_extensions__2.2.0.patch" + }, + "Fix Problems with Consumer Runners on Cloud Clusters": { + "2.2.0 - 2.2.3": "MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.0.patch", + "2.2.4": "MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.4.patch", + "2.2.5": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.5.patch", + "2.2.6": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.6.patch", + "2.2.7": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.7.patch", + "2.2.8 - 2.2.9": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.8.patch", + "2.3.0": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch", + "2.3.1 - 2.3.2": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch" + }, + "Resolve Issues with Cron Schedule": { + "2.1.10 - 2.1.14 || 2.2.2 - 2.2.5": "MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch" + }, + "Fix timezone parsing for Cron": { + "2.1.4": "MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.4.patch", + "2.1.5 - 2.1.10": "MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.5.patch", + "2.1.13 - 2.1.14": "MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.13.patch" + }, + "Change the depth of a recursive check of directory write permissions": { + "2.1.4 - 2.1.14": "MAGETWO-93265__fix_depth_of_recursive_check_of_directory_permissions__2.1.4.patch" + }, + "Google chart API used by Magento dashboard scheduled to be turned off": { + "2.1.4 - 2.1.17": "MAGETWO-98833__turn_off_google_chart_api__2.x.patch", + "2.2.0 - 2.2.8": "MAGETWO-98833__turn_off_google_chart_api__2.x.patch", + "2.3.0 - 2.3.1": "MAGETWO-98833__turn_off_google_chart_api__2.x.patch" + }, + "Do not run cron when it is disabled": { + "2.1.4 - 2.2.5": "MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch" + }, + "Zendframework1 should use TLS 1.2": { + ">=2.1.4 <2.3": "MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch" + }, + "The recursion detected error during deployment": { + "2.2.0 - 2.2.6": "MAGECLOUD-2173__the_recursion_error_during_deployment__2.2.0.patch" + }, + "Remove the permission check for the console application": { + "2.2.0 - 2.2.5": "MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.0.patch", + "2.2.6": "MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.6.patch" + }, + "Fix for DI compilation with Amazon_Payment module": { + "2.2.6": "MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch" + }, + "Add the possibility to install Magento without admin creation" : { + "2.1.4 - 2.2.1": "MAGECLOUD-2573__installation_without_admin_creation__2.1.4.patch", + "2.2.2 - 2.2.7": "MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch" + }, + "Add the possibility to configure max execution time during static content deployment": { + "2.2.0 - 2.2.8 || 2.3.0": "MAGECLOUD-2822__configure_max_execution_time.patch", + "2.3.1": "MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch" + }, + "Suppress PDO warnings to work around PHP bugs #63812, #74401": { + "2.1.4": "MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.4.patch", + "2.1.5 - 2.1.12": "MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.5.patch", + "2.1.13 - 2.1.17": "MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.13.patch", + "2.2.0 - 2.2.8 || 2.3.0 - 2.3.1": "MAGECLOUD-2820__implement_isolated_connections_mechanism__2.2.0.patch" + }, + "Pre-auth SQL": { + "2.1.4 - 2.1.17": "MC-5964__preauth_sql__2.1.4.patch", + "2.2.0 - 2.2.7": "MC-5964__preauth_sql__2.2.0.patch", + "2.3.0": "MC-5964__preauth_sql__2.3.0.patch" + }, + "Multi-thread SCD": { + "2.2.0 - 2.2.3": "MAGECLOUD-3611__multi_thread_scd__2.2.0.patch", + "2.2.4 - 2.2.9": "MAGECLOUD-3611__multi_thread_scd__2.2.4.patch", + "2.3.0 - 2.3.1": "MAGECLOUD-3611__multi_thread_scd__2.3.0.patch", + "2.3.2": "MAGECLOUD-3611__multi_thread_scd__2.3.2.patch" + }, + "setup:upgrade returns error code if app:config:import failed": { + "2.2.0": "MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.0.patch", + "2.2.1 - 2.2.9": "MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.1.patch", + "2.3.0 - 2.3.2": "MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.3.0.patch" + }, + "Re-work consumers to terminate as soon as there is nothing left to process": { + "2.2.0 - 2.3.1": "MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.2.0.patch", + "2.3.2 - 2.3.3": "MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.3.2.patch" + } + }, + "monolog/monolog": { + "Fix monolog Slack Handler bug for magento 2.1.x": { + "1.16.0": "MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch" + } + }, + "colinmollenhour/cache-backend-redis": { + "The ability to read from the master Redis instance if the slave Redis is unavailable:": { + "1.10.2": "MAGECLOUD-2899__fix_redis_slave_configuration__2.1.16.patch", + "1.10.4": "MAGECLOUD-2899__fix_redis_slave_configuration__2.2.3.patch", + "1.10.5": "MAGECLOUD-2899__fix_redis_slave_configuration__2.3.0.patch" + } + }, + "colinmollenhour/credis": { + "Fix credis pipeline issue": { + "1.6": "MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch" + } + }, + "magento/module-inventory-catalog": { + "Price indexer fails with large catalogs": { + ">=1.0.3 <1.0.6": "MSI-2210__price_indexer_fails_with_large_catalogs__1.0.3.patch" + } + }, + "magento/module-inventory-indexer": { + "Avoid quering inventory default stock view in storefront": { + ">=1.0.3 <1.0.6": "MSI-GH-2350__avoid_quering_inventory_default_stock_view_in_storefront__1.0.3.patch" + }, + "Avoid group concat from source item indexer": { + ">=1.0.3 <1.0.6": "MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__indexer__1.0.3.patch" + } + }, + "magento/module-inventory-reservations": { + "Avoid group concat from source item indexer": { + ">=1.0.3 <1.0.6": "MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__reservations__1.0.3.patch" + } + }, + "magento/module-inventory-configurable-product-indexer": { + "Avoid group concat from source item indexer": { + ">=1.0.3 <1.0.5": "MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__configurable-product-indexer__1.0.3.patch" + } + }, + "magento/module-inventory-grouped-product-indexer": { + "Avoid group concat from source item indexer": { + ">=1.0.3 <1.0.5": "MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__grouped-product-indexer__1.0.3.patch" + } + } +} diff --git a/patches/MAGECLOUD-1567__fix_import_during_ece_tools_dump__2.2.2.patch b/patches/MAGECLOUD-1567__fix_import_during_ece_tools_dump__2.2.2.patch new file mode 100644 index 00000000..ad1532e6 --- /dev/null +++ b/patches/MAGECLOUD-1567__fix_import_during_ece_tools_dump__2.2.2.patch @@ -0,0 +1,15 @@ +diff -Nuar a/vendor/magento/module-config/Model/Config/Importer.php b/vendor/magento/module-config/Model/Config/Importer.php +--- a/vendor/magento/module-config/Model/Config/Importer.php ++++ b/vendor/magento/module-config/Model/Config/Importer.php +@@ -129,8 +129,10 @@ class Importer implements ImporterInterface + + // Invoke saving of new values. + $this->saveProcessor->process($changedData); +- $this->flagManager->saveFlag(static::FLAG_CODE, $data); + }); ++ ++ $this->scope->setCurrentScope($currentScope); ++ $this->flagManager->saveFlag(static::FLAG_CODE, $data); + } catch (\Exception $e) { + throw new InvalidTransitionException(__('%1', $e->getMessage()), $e); + } finally { diff --git a/patches/MAGECLOUD-1582__fix_session_manager_locking__2.1.10.patch b/patches/MAGECLOUD-1582__fix_session_manager_locking__2.1.10.patch new file mode 100644 index 00000000..0fbdef83 --- /dev/null +++ b/patches/MAGECLOUD-1582__fix_session_manager_locking__2.1.10.patch @@ -0,0 +1,24 @@ +diff -Nuar a/vendor/magento/framework/Session/SessionManager.php b/vendor/magento/framework/Session/SessionManager.php +--- a/vendor/magento/framework/Session/SessionManager.php ++++ b/vendor/magento/framework/Session/SessionManager.php +@@ -470,18 +470,9 @@ class SessionManager implements SessionManagerInterface + if (headers_sent()) { + return $this; + } +- //@see http://php.net/manual/en/function.session-regenerate-id.php#53480 workaround ++ + if ($this->isSessionExists()) { +- $oldSessionId = session_id(); +- session_regenerate_id(); +- $newSessionId = session_id(); +- session_id($oldSessionId); +- session_destroy(); +- +- $oldSession = $_SESSION; +- session_id($newSessionId); +- session_start(); +- $_SESSION = $oldSession; ++ session_regenerate_id(true); + } else { + session_start(); + } diff --git a/patches/MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch b/patches/MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch new file mode 100644 index 00000000..33635d7f --- /dev/null +++ b/patches/MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch @@ -0,0 +1,24 @@ +diff -Nuar a/vendor/magento/framework/Session/SessionManager.php b/vendor/magento/framework/Session/SessionManager.php +index 2cea02f..272d3d9 100644 +--- a/vendor/magento/framework/Session/SessionManager.php ++++ b/vendor/magento/framework/Session/SessionManager.php +@@ -504,18 +504,8 @@ class SessionManager implements SessionManagerInterface + return $this; + } + +- //@see http://php.net/manual/en/function.session-regenerate-id.php#53480 workaround + if ($this->isSessionExists()) { +- $oldSessionId = session_id(); +- session_regenerate_id(); +- $newSessionId = session_id(); +- session_id($oldSessionId); +- session_destroy(); +- +- $oldSession = $_SESSION; +- session_id($newSessionId); +- session_start(); +- $_SESSION = $oldSession; ++ session_regenerate_id(true); + } else { + session_start(); + } diff --git a/patches/MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.1.4.patch b/patches/MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.1.4.patch new file mode 100644 index 00000000..fe8b750b --- /dev/null +++ b/patches/MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.1.4.patch @@ -0,0 +1,149 @@ +diff -Nuar a/vendor/magento/framework/Config/ConfigOptionsListConstants.php b/vendor/magento/framework/Config/ConfigOptionsListConstants.php +--- a/vendor/magento/framework/Config/ConfigOptionsListConstants.php ++++ b/vendor/magento/framework/Config/ConfigOptionsListConstants.php +@@ -11,6 +11,8 @@ namespace Magento\Framework\Config; + */ + class ConfigOptionsListConstants + { ++ const CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION = 'static_content_on_demand_in_production'; ++ + /**#@+ + * Path to the values in the deployment config + */ + +diff -Nuar a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +--- a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php ++++ b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +@@ -10,6 +10,9 @@ use Magento\Framework\App\State; + use Magento\Framework\View\Asset\ConfigInterface; + use Magento\Framework\View\Design\ThemeInterface; + use Magento\Framework\View\Template\Html\MinifierInterface; ++use Magento\Framework\App\DeploymentConfig; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Config\ConfigOptionsListConstants; + + /** + * Provider of template view files +@@ -32,20 +35,28 @@ class TemplateFile extends File + protected $assetConfig; + + /** ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ ++ /** + * @param ResolverInterface $resolver + * @param MinifierInterface $templateMinifier + * @param State $appState + * @param ConfigInterface $assetConfig ++ * @param DeploymentConfig $deploymentConfig + */ + public function __construct( + ResolverInterface $resolver, + MinifierInterface $templateMinifier, + State $appState, +- ConfigInterface $assetConfig ++ ConfigInterface $assetConfig, ++ DeploymentConfig $deploymentConfig = null + ) { + $this->appState = $appState; + $this->templateMinifier = $templateMinifier; + $this->assetConfig = $assetConfig; ++ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); + parent::__construct($resolver); + } + +@@ -73,7 +84,7 @@ class TemplateFile extends File + if ($template && $this->assetConfig->isMinifyHtml()) { + switch ($this->appState->getMode()) { + case State::MODE_PRODUCTION: +- return $this->templateMinifier->getPathToMinified($template); ++ return $this->getMinifiedTemplateInProduction($template); + case State::MODE_DEFAULT: + return $this->templateMinifier->getMinified($template); + case State::MODE_DEVELOPER: +@@ -83,4 +94,24 @@ class TemplateFile extends File + } + return $template; + } ++ ++ /** ++ * Returns path to minified template file ++ * ++ * If SCD on demand in production is disabled - returns the path to minified template file. ++ * Otherwise returns the path to minified template file, ++ * or minify if file not exist and returns path. ++ * ++ * @param string $template ++ * @return string ++ */ ++ private function getMinifiedTemplateInProduction($template) ++ { ++ if ($this->deploymentConfig->getConfigData( ++ ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION ++ )) { ++ return $this->templateMinifier->getMinified($template); ++ } ++ return $this->templateMinifier->getPathToMinified($template); ++ } + } + +diff -Nuar a/vendor/magento/framework/App/View/Deployment/Version.php b/vendor/magento/framework/App/View/Deployment/Version.php +--- a/vendor/magento/framework/App/View/Deployment/Version.php ++++ b/vendor/magento/framework/App/View/Deployment/Version.php +@@ -6,6 +6,10 @@ + + namespace Magento\Framework\App\View\Deployment; + ++use Magento\Framework\App\DeploymentConfig; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Config\ConfigOptionsListConstants; ++ + /** + * Deployment version of static files + */ +@@ -27,15 +31,23 @@ class Version + private $cachedValue; + + /** ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ ++ /** + * @param \Magento\Framework\App\State $appState + * @param Version\StorageInterface $versionStorage ++ * @param DeploymentConfig|null $deploymentConfig + */ + public function __construct( + \Magento\Framework\App\State $appState, +- \Magento\Framework\App\View\Deployment\Version\StorageInterface $versionStorage ++ \Magento\Framework\App\View\Deployment\Version\StorageInterface $versionStorage, ++ DeploymentConfig $deploymentConfig = null + ) { + $this->appState = $appState; + $this->versionStorage = $versionStorage; ++ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); + } + + /** +@@ -74,7 +86,17 @@ class Version + break; + + default: +- $result = $this->versionStorage->load(); ++ try { ++ $result = $this->versionStorage->load(); ++ } catch (\UnexpectedValueException $e) { ++ if (!$this->deploymentConfig->getConfigData( ++ ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION ++ )) { ++ throw $e; ++ } ++ $result = (new \DateTime())->getTimestamp(); ++ $this->versionStorage->save($result); ++ } + } + return $result; + } diff --git a/patches/MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.2.0.patch b/patches/MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.2.0.patch new file mode 100644 index 00000000..f9d7a30c --- /dev/null +++ b/patches/MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.2.0.patch @@ -0,0 +1,204 @@ +diff -Nuar a/vendor/magento/framework/App/StaticResource.php b/vendor/magento/framework/App/StaticResource.php +--- a/vendor/magento/framework/App/StaticResource.php ++++ b/vendor/magento/framework/App/StaticResource.php +@@ -8,6 +8,7 @@ namespace Magento\Framework\App; + use Magento\Framework\App\Filesystem\DirectoryList; + use Magento\Framework\ObjectManager\ConfigLoaderInterface; + use Magento\Framework\Filesystem; ++use Magento\Framework\Config\ConfigOptionsListConstants; + use Psr\Log\LoggerInterface; + + /** +@@ -63,6 +64,11 @@ class StaticResource implements \Magento\Framework\AppInterface + private $filesystem; + + /** ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ ++ /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; +@@ -76,6 +82,7 @@ class StaticResource implements \Magento\Framework\AppInterface + * @param \Magento\Framework\Module\ModuleList $moduleList + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param ConfigLoaderInterface $configLoader ++ * @param DeploymentConfig|null $deploymentConfig + */ + public function __construct( + State $state, +@@ -85,7 +92,8 @@ class StaticResource implements \Magento\Framework\AppInterface + \Magento\Framework\View\Asset\Repository $assetRepo, + \Magento\Framework\Module\ModuleList $moduleList, + \Magento\Framework\ObjectManagerInterface $objectManager, +- ConfigLoaderInterface $configLoader ++ ConfigLoaderInterface $configLoader, ++ DeploymentConfig $deploymentConfig = null + ) { + $this->state = $state; + $this->response = $response; +@@ -95,6 +103,7 @@ class StaticResource implements \Magento\Framework\AppInterface + $this->moduleList = $moduleList; + $this->objectManager = $objectManager; + $this->configLoader = $configLoader; ++ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); + } + + /** +@@ -108,7 +117,11 @@ class StaticResource implements \Magento\Framework\AppInterface + // disabling profiling when retrieving static resource + \Magento\Framework\Profiler::reset(); + $appMode = $this->state->getMode(); +- if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION) { ++ if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION ++ && !$this->deploymentConfig->getConfigData( ++ ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION ++ ) ++ ) { + $this->response->setHttpResponseCode(404); + } else { + $path = $this->request->get('resource'); + +diff -Nuar a/vendor/magento/framework/App/View/Deployment/Version.php b/vendor/magento/framework/App/View/Deployment/Version.php +--- a/vendor/magento/framework/App/View/Deployment/Version.php ++++ b/vendor/magento/framework/App/View/Deployment/Version.php +@@ -6,6 +6,9 @@ + + namespace Magento\Framework\App\View\Deployment; + ++use Magento\Framework\App\DeploymentConfig; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Config\ConfigOptionsListConstants; + use Psr\Log\LoggerInterface; + + /** +@@ -34,15 +37,23 @@ class Version + private $logger; + + /** ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ ++ /** + * @param \Magento\Framework\App\State $appState + * @param Version\StorageInterface $versionStorage ++ * @param DeploymentConfig|null $deploymentConfig + */ + public function __construct( + \Magento\Framework\App\State $appState, +- \Magento\Framework\App\View\Deployment\Version\StorageInterface $versionStorage ++ \Magento\Framework\App\View\Deployment\Version\StorageInterface $versionStorage, ++ DeploymentConfig $deploymentConfig = null + ) { + $this->appState = $appState; + $this->versionStorage = $versionStorage; ++ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); + } + + /** +@@ -68,7 +79,11 @@ class Version + { + $result = $this->versionStorage->load(); + if (!$result) { +- if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION) { ++ if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION ++ && !$this->deploymentConfig->getConfigData( ++ ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION ++ ) ++ ) { + $this->getLogger()->critical('Can not load static content version.'); + throw new \UnexpectedValueException( + "Unable to retrieve deployment version of static files from the file system." + +diff -Nuar a/vendor/magento/framework/Config/ConfigOptionsListConstants.php b/vendor/magento/framework/Config/ConfigOptionsListConstants.php +--- a/vendor/magento/framework/Config/ConfigOptionsListConstants.php ++++ b/vendor/magento/framework/Config/ConfigOptionsListConstants.php +@@ -36,6 +36,7 @@ class ConfigOptionsListConstants + const CONFIG_PATH_DB_LOGGER_LOG_EVERYTHING = 'db_logger/log_everything'; + const CONFIG_PATH_DB_LOGGER_QUERY_TIME_THRESHOLD = 'db_logger/query_time_threshold'; + const CONFIG_PATH_DB_LOGGER_INCLUDE_STACKTRACE = 'db_logger/include_stacktrace'; ++ const CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION = 'static_content_on_demand_in_production'; + /**#@-*/ + + /**#@+ + +diff -Nuar a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +--- a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php ++++ b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +@@ -10,6 +10,9 @@ use Magento\Framework\App\State; + use Magento\Framework\View\Asset\ConfigInterface; + use Magento\Framework\View\Design\ThemeInterface; + use Magento\Framework\View\Template\Html\MinifierInterface; ++use Magento\Framework\App\DeploymentConfig; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Config\ConfigOptionsListConstants; + + /** + * Provider of template view files +@@ -32,20 +35,28 @@ class TemplateFile extends File + protected $assetConfig; + + /** ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ ++ /** + * @param ResolverInterface $resolver + * @param MinifierInterface $templateMinifier + * @param State $appState + * @param ConfigInterface $assetConfig ++ * @param DeploymentConfig $deploymentConfig + */ + public function __construct( + ResolverInterface $resolver, + MinifierInterface $templateMinifier, + State $appState, +- ConfigInterface $assetConfig ++ ConfigInterface $assetConfig, ++ DeploymentConfig $deploymentConfig = null + ) { + $this->appState = $appState; + $this->templateMinifier = $templateMinifier; + $this->assetConfig = $assetConfig; ++ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); + parent::__construct($resolver); + } + +@@ -73,7 +84,7 @@ class TemplateFile extends File + if ($template && $this->assetConfig->isMinifyHtml()) { + switch ($this->appState->getMode()) { + case State::MODE_PRODUCTION: +- return $this->templateMinifier->getPathToMinified($template); ++ return $this->getMinifiedTemplateInProduction($template); + case State::MODE_DEFAULT: + return $this->templateMinifier->getMinified($template); + case State::MODE_DEVELOPER: +@@ -83,4 +94,24 @@ class TemplateFile extends File + } + return $template; + } ++ ++ /** ++ * Returns path to minified template file ++ * ++ * If SCD on demand in production is disabled - returns the path to minified template file. ++ * Otherwise returns the path to minified template file, ++ * or minify if file not exist and returns path. ++ * ++ * @param string $template ++ * @return string ++ */ ++ private function getMinifiedTemplateInProduction($template) ++ { ++ if ($this->deploymentConfig->getConfigData( ++ ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION ++ )) { ++ return $this->templateMinifier->getMinified($template); ++ } ++ return $this->templateMinifier->getPathToMinified($template); ++ } + } diff --git a/patches/MAGECLOUD-1601__trim_static_content_path__2.1.4.patch b/patches/MAGECLOUD-1601__trim_static_content_path__2.1.4.patch new file mode 100644 index 00000000..759df9e8 --- /dev/null +++ b/patches/MAGECLOUD-1601__trim_static_content_path__2.1.4.patch @@ -0,0 +1,11 @@ +diff -Naur a/pub/front-static.php b/pub/front-static.php +--- a/pub/front-static.php ++++ b/pub/front-static.php +@@ -6,6 +6,7 @@ + * See COPYING.txt for license details. + */ + ++$_GET['resource'] = preg_replace('/^(\/static\/)(version(\d+)?\/)?|(\?.*)/', '', $_SERVER['REQUEST_URI'] ?: ''); + require realpath(__DIR__) . '/../app/bootstrap.php'; + $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); + /** @var \Magento\Framework\App\StaticResource $app */ diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch new file mode 100644 index 00000000..48905b15 --- /dev/null +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch @@ -0,0 +1,907 @@ +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -9,9 +9,12 @@ + */ + namespace Magento\Cron\Observer; + +-use Magento\Framework\Console\CLI; ++use Magento\Framework\App\State; ++use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; + use \Magento\Cron\Model\Schedule; ++use Magento\Framework\Profiler\Driver\Standard\Stat; ++use Magento\Framework\Profiler\Driver\Standard\StatFactory; + + /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) +@@ -96,9 +99,9 @@ class ProcessCronQueueObserver implements ObserverInterface + protected $_shell; + + /** +- * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface ++ * @var \Magento\Framework\Stdlib\DateTime\DateTime + */ +- protected $timezone; ++ protected $dateTime; + + /** + * @var \Symfony\Component\Process\PhpExecutableFinder +@@ -106,15 +109,44 @@ class ProcessCronQueueObserver implements ObserverInterface + protected $phpExecutableFinder; + + /** ++ * @var \Psr\Log\LoggerInterface ++ */ ++ private $logger; ++ ++ /** ++ * @var \Magento\Framework\App\State ++ */ ++ private $state; ++ ++ /** ++ * @var array ++ */ ++ private $invalid = []; ++ ++ /** ++ * @var array ++ */ ++ private $jobs; ++ ++ /** ++ * @var Stat ++ */ ++ private $statProfiler; ++ ++ /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager +- * @param ScheduleFactory $scheduleFactory ++ * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory + * @param \Magento\Framework\App\CacheInterface $cache +- * @param ConfigInterface $config ++ * @param \Magento\Cron\Model\ConfigInterface $config + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Framework\App\Console\Request $request + * @param \Magento\Framework\ShellInterface $shell +- * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone ++ * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime + * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory ++ * @param \Psr\Log\LoggerInterface $logger ++ * @param \Magento\Framework\App\State $state ++ * @param StatFactory $statFactory ++ * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\ObjectManagerInterface $objectManager, +@@ -124,8 +156,11 @@ class ProcessCronQueueObserver implements ObserverInterface + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Framework\App\Console\Request $request, + \Magento\Framework\ShellInterface $shell, +- \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone, +- \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory ++ \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, ++ \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, ++ \Psr\Log\LoggerInterface $logger, ++ \Magento\Framework\App\State $state, ++ StatFactory $statFactory + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -134,8 +169,11 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->_scopeConfig = $scopeConfig; + $this->_request = $request; + $this->_shell = $shell; +- $this->timezone = $timezone; ++ $this->dateTime = $dateTime; + $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); ++ $this->logger = $logger; ++ $this->state = $state; ++ $this->statProfiler = $statFactory->create(); + } + + /** +@@ -151,26 +189,29 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- $pendingJobs = $this->_getPendingSchedules(); +- $currentTime = $this->timezone->scopeTimeStamp(); ++ ++ $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); ++ // sort jobs groups to start from used in separated process ++ uksort( ++ $jobGroupsRoot, ++ function ($a, $b) { ++ return $this->getCronGroupConfigurationValue($b, 'use_separate_process') ++ - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); ++ } ++ ); + + $phpPath = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($jobGroupsRoot as $groupId => $jobsRoot) { +- if ($this->_request->getParam('group') !== null +- && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' +- && $this->_request->getParam('group') !== $groupId) { ++ if (!$this->isGroupInFilter($groupId)) { + continue; + } +- if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( +- $this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/use_separate_process', +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ) == 1 +- )) { ++ if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' ++ && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 ++ ) { + $this->_shell->execute( +- $phpPath . ' %s cron:run --group=' . $groupId . ' --' . CLI::INPUT_KEY_BOOTSTRAP . '=' ++ $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' + . self::STANDALONE_PROCESS_STARTED . '=1', + [ + BP . '/bin/magento' +@@ -179,29 +220,9 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- foreach ($pendingJobs as $schedule) { +- $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; +- if (!$jobConfig) { +- continue; +- } +- +- $scheduledTime = strtotime($schedule->getScheduledAt()); +- if ($scheduledTime > $currentTime) { +- continue; +- } +- +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $schedule->setMessages($e->getMessage()); +- } +- $schedule->save(); +- } +- +- $this->_generate($groupId); +- $this->_cleanup($groupId); ++ $this->cleanupJobs($groupId, $currentTime); ++ $this->generateSchedules($groupId); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -218,58 +239,105 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) + { +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $jobCode = $schedule->getJobCode(); ++ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ $this->logger->info(sprintf('Cron Job %s is missed', $jobCode)); + throw new \Exception('Too late for the schedule'); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception('No callbacks found'); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + ++ $this->startProfiling(); + try { ++ $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + call_user_func_array($callback, [$schedule]); + } catch (\Exception $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf( ++ 'Cron Job %s has an error. Statistics: %s %s', ++ $jobCode, ++ $this->getProfilingStat(), $e->getMessage() ++ )); + throw $e; ++ } finally { ++ $this->stopProfiling(); + } + + $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( + '%Y-%m-%d %H:%M:%S', +- $this->timezone->scopeTimeStamp() ++ $this->dateTime->gmtTimestamp() ++ )); ++ ++ $this->logger->info(sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() + )); + } + + /** ++ * Starts profiling ++ * ++ * @return void ++ */ ++ private function startProfiling() ++ { ++ $this->statProfiler->clear(); ++ $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Stops profiling ++ * ++ * @return void ++ */ ++ private function stopProfiling() ++ { ++ $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Retrieves statistics in the JSON format ++ * ++ * @return string ++ */ ++ private function getProfilingStat() ++ { ++ $stat = $this->statProfiler->get('job'); ++ unset($stat[Stat::START]); ++ return json_encode($stat); ++ } ++ ++ /** + * Return job collection from data base with status 'pending' + * + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ +- protected function _getPendingSchedules() ++ private function getPendingSchedules($groupId) + { +- if (!$this->_pendingSchedules) { +- $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- Schedule::STATUS_PENDING +- )->load(); +- } +- return $this->_pendingSchedules; ++ $jobs = $this->getJobs(); ++ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); ++ $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); ++ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); ++ return $pendingJobs; + } + + /** +@@ -278,22 +346,32 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param string $groupId + * @return $this + */ +- protected function _generate($groupId) ++ private function generateSchedules($groupId) + { + /** + * check if schedule generation is needed + */ + $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); +- $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( ++ $groupId, ++ self::XML_PATH_SCHEDULE_GENERATE_EVERY + ); + $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; +- if ($lastRun > $this->timezone->scopeTimeStamp() - $schedulePeriod) { ++ if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { + return $this; + } + +- $schedules = $this->_getPendingSchedules(); ++ /** ++ * save time schedules generation was ran with no expiration ++ */ ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, ++ ['crontab'], ++ null ++ ); ++ ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -303,18 +381,10 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * generate global crontab jobs + */ +- $jobs = $this->_config->getJobs(); ++ $jobs = $this->getJobs(); ++ $this->invalid = []; + $this->_generateJobs($jobs[$groupId], $exists, $groupId); +- +- /** +- * save time schedules generation was ran with no expiration +- */ +- $this->_cache->save( +- $this->timezone->scopeTimeStamp(), +- self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, +- ['crontab'], +- null +- ); ++ $this->cleanupScheduleMismatches(); + + return $this; + } +@@ -325,22 +395,12 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param array $jobs + * @param array $exists + * @param string $groupId +- * @return $this ++ * @return void + */ + protected function _generateJobs($jobs, $exists, $groupId) + { + foreach ($jobs as $jobCode => $jobConfig) { +- $cronExpression = null; +- if (isset($jobConfig['config_path'])) { +- $cronExpression = $this->getConfigSchedule($jobConfig) ?: null; +- } +- +- if (!$cronExpression) { +- if (isset($jobConfig['schedule'])) { +- $cronExpression = $jobConfig['schedule']; +- } +- } +- ++ $cronExpression = $this->getCronExpression($jobConfig); + if (!$cronExpression) { + continue; + } +@@ -348,75 +408,60 @@ class ProcessCronQueueObserver implements ObserverInterface + $timeInterval = $this->getScheduleTimeInterval($groupId); + $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); + } +- return $this; + } + + /** +- * Clean existed jobs ++ * Clean expired jobs + * +- * @param string $groupId +- * @return $this ++ * @param $groupId ++ * @param $currentTime ++ * @return void + */ +- protected function _cleanup($groupId) ++ private function cleanupJobs($groupId, $currentTime) + { + // check if history cleanup is needed + $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); +- $historyCleanUp = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- if ($lastCleanup > $this->timezone->scopeTimeStamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { ++ $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); ++ if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { + return $this; + } +- +- // check how long the record should stay unprocessed before marked as MISSED +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ // save time history cleanup was ran with no expiration ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, ++ ['crontab'], ++ null + ); +- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + +- /** +- * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history +- */ +- $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] +- )->load(); ++ $this->cleanupDisabledJobs($groupId); + +- $historySuccess = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- $historyFailure = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); ++ $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); + $historyLifetimes = [ + Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, + Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, ++ Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, + ]; + +- $now = $this->timezone->scopeTimeStamp(); +- /** @var Schedule $record */ +- foreach ($history as $record) { +- $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : +- strtotime($record->getScheduledAt()) + $scheduleLifetime; +- if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { +- $record->delete(); +- } ++ $jobs = $this->getJobs()[$groupId]; ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $connection = $scheduleResource->getConnection(); ++ $count = 0; ++ foreach ($historyLifetimes as $status => $time) { ++ $count += $connection->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => $status, ++ 'job_code in (?)' => array_keys($jobs), ++ 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ ] ++ ); + } + +- // save time history cleanup was ran with no expiration +- $this->_cache->save( +- $this->timezone->scopeTimeStamp(), +- self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, +- ['crontab'], +- null +- ); +- +- return $this; ++ if ($count) { ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -442,19 +487,25 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exists) + { +- $currentTime = $this->timezone->scopeTimeStamp(); ++ $currentTime = $this->dateTime->gmtTimestamp(); + $timeAhead = $currentTime + $timeInterval; + for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { +- $ts = strftime('%Y-%m-%d %H:%M:00', $time); +- if (!empty($exists[$jobCode . '/' . $ts])) { +- // already scheduled ++ $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); ++ $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); ++ $schedule = $this->createSchedule($jobCode, $cronExpression, $time); ++ $valid = $schedule->trySchedule(); ++ if (!$valid) { ++ if ($alreadyScheduled) { ++ if (!isset($this->invalid[$jobCode])) { ++ $this->invalid[$jobCode] = []; ++ } ++ $this->invalid[$jobCode][] = $scheduledAt; ++ } + continue; + } +- $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); +- if ($schedule->trySchedule()) { ++ if (!$alreadyScheduled) { + // time matches cron expression + $schedule->save(); +- return; + } + } + } +@@ -465,13 +516,13 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param int $time + * @return Schedule + */ +- protected function generateSchedule($jobCode, $cronExpression, $time) ++ protected function createSchedule($jobCode, $cronExpression, $time) + { + $schedule = $this->_scheduleFactory->create() + ->setCronExpr($cronExpression) + ->setJobCode($jobCode) + ->setStatus(Schedule::STATUS_PENDING) +- ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp())) ++ ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())) + ->setScheduledAt(strftime('%Y-%m-%d %H:%M', $time)); + + return $schedule; +@@ -483,12 +534,174 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function getScheduleTimeInterval($groupId) + { +- $scheduleAheadFor = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); + $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; + + return $scheduleAheadFor; + } ++ ++ /** ++ * Clean up scheduled jobs that are disabled in the configuration ++ * This can happen when you turn off a cron job in the config and flush the cache ++ * ++ * @param string $groupId ++ * @return void ++ */ ++ private function cleanupDisabledJobs($groupId) ++ { ++ $jobs = $this->getJobs(); ++ $jobsToCleanup = []; ++ foreach ($jobs[$groupId] as $jobCode => $jobConfig) { ++ if (!$this->getCronExpression($jobConfig)) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $jobsToCleanup[] = $jobCode; ++ } ++ } ++ ++ if (count($jobsToCleanup) > 0) { ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $count = $scheduleResource->getConnection()->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code in (?)' => $jobsToCleanup, ++ ] ++ ); ++ ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } ++ } ++ ++ /** ++ * @param array $jobConfig ++ * @return null|string ++ */ ++ private function getCronExpression($jobConfig) ++ { ++ $cronExpression = null; ++ if (isset($jobConfig['config_path'])) { ++ $cronExpression = $this->getConfigSchedule($jobConfig) ?: null; ++ } ++ ++ if (!$cronExpression) { ++ if (isset($jobConfig['schedule'])) { ++ $cronExpression = $jobConfig['schedule']; ++ } ++ } ++ return $cronExpression; ++ } ++ ++ /** ++ * Clean up scheduled jobs that do not match their cron expression anymore ++ * This can happen when you change the cron expression and flush the cache ++ * ++ * @return $this ++ */ ++ private function cleanupScheduleMismatches() ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ foreach ($this->invalid as $jobCode => $scheduledAtList) { ++ $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ]); ++ } ++ return $this; ++ } ++ ++ /** ++ * @return array ++ */ ++ private function getJobs() ++ { ++ if ($this->jobs === null) { ++ $this->jobs = $this->_config->getJobs(); ++ } ++ return $this->jobs; ++ } ++ ++ /** ++ * Get CronGroup Configuration Value ++ * ++ * @param $groupId ++ * @return int ++ */ ++ private function getCronGroupConfigurationValue($groupId, $path) ++ { ++ return $this->_scopeConfig->getValue( ++ 'system/cron/' . $groupId . '/' . $path, ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ return $scheduleLifetime; ++ } ++ ++ /** ++ * Is Group In Filter ++ * ++ * @param $groupId ++ * @return bool ++ */ ++ private function isGroupInFilter($groupId): bool ++ { ++ return !($this->_request->getParam('group') !== null ++ && trim($this->_request->getParam('group'), "'") !== $groupId); ++ } ++ ++ /** ++ * Process pending jobs ++ * ++ * @param $groupId ++ * @param $jobsRoot ++ * @param $currentTime ++ */ ++ private function processPendingJobs($groupId, $jobsRoot, $currentTime) ++ { ++ $procesedJobs = []; ++ $pendingJobs = $this->getPendingSchedules($groupId); ++ /** @var \Magento\Cron\Model\Schedule $schedule */ ++ foreach ($pendingJobs as $schedule) { ++ if (isset($procesedJobs[$schedule->getJobCode()])) { ++ // process only on job per run ++ continue; ++ } ++ $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; ++ if (!$jobConfig) { ++ continue; ++ } ++ ++ $scheduledTime = strtotime($schedule->getScheduledAt()); ++ if ($scheduledTime > $currentTime) { ++ continue; ++ } ++ ++ try { ++ if ($schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ } ++ } catch (\Exception $e) { ++ $schedule->setMessages($e->getMessage()); ++ if ($schedule->getStatus() === Schedule::STATUS_ERROR) { ++ $this->logger->critical($e); ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_MISSED ++ && $this->state->getMode() === State::MODE_DEVELOPER ++ ) { ++ $this->logger->error( ++ sprintf( ++ "%s Schedule Id: %s Job Code: %s", ++ $schedule->getMessages(), ++ $schedule->getScheduleId(), ++ $schedule->getJobCode() ++ ) ++ ); ++ } ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { ++ $procesedJobs[$schedule->getJobCode()] = true; ++ } ++ $schedule->save(); ++ } ++ } + } + +diff -Naur a/vendor/magento/module-cron/Model/Schedule.php b/vendor/magento/module-cron/Model/Schedule.php +--- a/vendor/magento/module-cron/Model/Schedule.php ++++ b/vendor/magento/module-cron/Model/Schedule.php +@@ -1,18 +1,18 @@ + ++ * @api ++ * @since 100.0.2 + */ + class Schedule extends \Magento\Framework\Model\AbstractModel + { +@@ -45,20 +46,28 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + const STATUS_ERROR = 'error'; + + /** ++ * @var TimezoneInterface ++ */ ++ private $timezoneConverter; ++ ++ /** + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource + * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param array $data ++ * @param TimezoneInterface $timezoneConverter + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, +- array $data = [] ++ array $data = [], ++ TimezoneInterface $timezoneConverter = null + ) { + parent::__construct($context, $registry, $resource, $resourceCollection, $data); ++ $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + } + + /** +@@ -66,7 +75,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + */ + public function _construct() + { +- $this->_init('Magento\Cron\Model\ResourceModel\Schedule'); ++ $this->_init(\Magento\Cron\Model\ResourceModel\Schedule::class); + } + + /** +@@ -101,6 +110,9 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + return false; + } + if (!is_numeric($time)) { ++ //convert time from UTC to admin store timezone ++ //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone ++ $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); + $time = strtotime($time); + } + $match = $this->matchCronExpression($e[0], strftime('%M', $time)) +@@ -221,16 +233,17 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. +- * Returns true if status was changed and false otherwise. ++ * Lock the cron job so no other scheduled instances run simultaneously. + * +- * This is used to implement locking for cron jobs. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING ++ * and no other jobs of the same code are currently in STATUS_RUNNING. ++ * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobStatusAtomic( ++ if ($this->_getResource()->trySetJobUniqueStatusAtomic( + $this->getId(), + self::STATUS_RUNNING, + self::STATUS_PENDING + +diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +index dca4e22..25dd02c 100644 +--- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php ++++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +@@ -8,7 +8,8 @@ namespace Magento\Cron\Model\ResourceModel; + /** + * Schedule resource + * +- * @author Magento Core Team ++ * @api ++ * @since 100.0.2 + */ + class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + { +@@ -23,9 +24,10 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + + /** +- * If job is currently in $currentStatus, set it to $newStatus +- * and return true. Otherwise, return false and do not change the job. +- * This method is used to implement locking for cron jobs. ++ * Sets new schedule status only if it's in the expected current status. ++ * ++ * If schedule is currently in $currentStatus, set it to $newStatus and ++ * return true. Otherwise, return false. + * + * @param string $scheduleId + * @param string $newStatus +@@ -45,4 +47,49 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + return false; + } ++ ++ /** ++ * Sets schedule status only if no existing schedules with the same job code ++ * have that status. This is used to implement locking for cron jobs. ++ * ++ * If the schedule is currently in $currentStatus and there are no existing ++ * schedules with the same job code and $newStatus, set the schedule to ++ * $newStatus and return true. Otherwise, return false. ++ * ++ * @param string $scheduleId ++ * @param string $newStatus ++ * @param string $currentStatus ++ * @return bool ++ * @since 100.2.0 ++ */ ++ public function trySetJobUniqueStatusAtomic($scheduleId, $newStatus, $currentStatus) ++ { ++ $connection = $this->getConnection(); ++ ++ // this condition added to avoid cron jobs locking after incorrect termination of running job ++ $match = $connection->quoteInto( ++ 'existing.job_code = current.job_code ' . ++ 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL) ' . ++ 'AND existing.status = ?', ++ $newStatus ++ ); ++ ++ $selectIfUnlocked = $connection->select() ++ ->joinLeft( ++ ['existing' => $this->getTable('cron_schedule')], ++ $match, ++ ['status' => new \Zend_Db_Expr($connection->quote($newStatus))] ++ ) ++ ->where('current.schedule_id = ?', $scheduleId) ++ ->where('current.status = ?', $currentStatus) ++ ->where('existing.schedule_id IS NULL'); ++ ++ $update = $connection->updateFromSelect($selectIfUnlocked, ['current' => $this->getTable('cron_schedule')]); ++ $result = $connection->query($update)->rowCount(); ++ ++ if ($result == 1) { ++ return true; ++ } ++ return false; ++ } + } diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch new file mode 100644 index 00000000..117d4d74 --- /dev/null +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch @@ -0,0 +1,902 @@ +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -9,9 +9,12 @@ + */ + namespace Magento\Cron\Observer; + ++use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; + use \Magento\Cron\Model\Schedule; ++use Magento\Framework\Profiler\Driver\Standard\Stat; ++use Magento\Framework\Profiler\Driver\Standard\StatFactory; + + /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) +@@ -96,25 +99,54 @@ class ProcessCronQueueObserver implements ObserverInterface + protected $_shell; + + /** +- * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface ++ * @var \Magento\Framework\Stdlib\DateTime\DateTime + */ +- protected $timezone; ++ protected $dateTime; + + /** + * @var \Symfony\Component\Process\PhpExecutableFinder + */ + protected $phpExecutableFinder; + ++ /** ++ * @var \Psr\Log\LoggerInterface ++ */ ++ private $logger; ++ ++ /** ++ * @var \Magento\Framework\App\State ++ */ ++ private $state; ++ ++ /** ++ * @var array ++ */ ++ private $invalid = []; ++ ++ /** ++ * @var array ++ */ ++ private $jobs; ++ ++ /** ++ * @var Stat ++ */ ++ private $statProfiler; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager +- * @param ScheduleFactory $scheduleFactory ++ * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory + * @param \Magento\Framework\App\CacheInterface $cache +- * @param ConfigInterface $config ++ * @param \Magento\Cron\Model\ConfigInterface $config + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Framework\App\Console\Request $request + * @param \Magento\Framework\ShellInterface $shell +- * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone ++ * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime + * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory ++ * @param \Psr\Log\LoggerInterface $logger ++ * @param \Magento\Framework\App\State $state ++ * @param StatFactory $statFactory ++ * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\ObjectManagerInterface $objectManager, +@@ -124,8 +156,11 @@ class ProcessCronQueueObserver implements ObserverInterface + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Framework\App\Console\Request $request, + \Magento\Framework\ShellInterface $shell, +- \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone, +- \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory ++ \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, ++ \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, ++ \Psr\Log\LoggerInterface $logger, ++ \Magento\Framework\App\State $state, ++ StatFactory $statFactory + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -134,8 +169,11 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->_scopeConfig = $scopeConfig; + $this->_request = $request; + $this->_shell = $shell; +- $this->timezone = $timezone; ++ $this->dateTime = $dateTime; + $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); ++ $this->logger = $logger; ++ $this->state = $state; ++ $this->statProfiler = $statFactory->create(); + } + + /** +@@ -151,26 +189,29 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- $pendingJobs = $this->_getPendingSchedules(); +- $currentTime = $this->timezone->scopeTimeStamp(); ++ ++ $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); ++ // sort jobs groups to start from used in separated process ++ uksort( ++ $jobGroupsRoot, ++ function ($a, $b) { ++ return $this->getCronGroupConfigurationValue($b, 'use_separate_process') ++ - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); ++ } ++ ); + + $phpPath = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($jobGroupsRoot as $groupId => $jobsRoot) { +- if ($this->_request->getParam('group') !== null +- && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' +- && $this->_request->getParam('group') !== $groupId) { ++ if (!$this->isGroupInFilter($groupId)) { + continue; + } +- if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( +- $this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/use_separate_process', +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ) == 1 +- )) { ++ if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' ++ && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 ++ ) { + $this->_shell->execute( + $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' + . self::STANDALONE_PROCESS_STARTED . '=1', + [ + BP . '/bin/magento' +@@ -179,29 +220,9 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- foreach ($pendingJobs as $schedule) { +- $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; +- if (!$jobConfig) { +- continue; +- } +- +- $scheduledTime = strtotime($schedule->getScheduledAt()); +- if ($scheduledTime > $currentTime) { +- continue; +- } +- +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $schedule->setMessages($e->getMessage()); +- } +- $schedule->save(); +- } +- +- $this->_generate($groupId); +- $this->_cleanup($groupId); ++ $this->cleanupJobs($groupId, $currentTime); ++ $this->generateSchedules($groupId); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -218,58 +239,105 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) + { +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $jobCode = $schedule->getJobCode(); ++ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ $this->logger->info(sprintf('Cron Job %s is missed', $jobCode)); + throw new \Exception('Too late for the schedule'); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception('No callbacks found'); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + ++ $this->startProfiling(); + try { ++ $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + call_user_func_array($callback, [$schedule]); + } catch (\Exception $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf( ++ 'Cron Job %s has an error. Statistics: %s %s', ++ $jobCode, ++ $this->getProfilingStat(), $e->getMessage() ++ )); + throw $e; ++ } finally { ++ $this->stopProfiling(); + } + + $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( + '%Y-%m-%d %H:%M:%S', +- $this->timezone->scopeTimeStamp() ++ $this->dateTime->gmtTimestamp() ++ )); ++ ++ $this->logger->info(sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() + )); + } + ++ /** ++ * Starts profiling ++ * ++ * @return void ++ */ ++ private function startProfiling() ++ { ++ $this->statProfiler->clear(); ++ $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Stops profiling ++ * ++ * @return void ++ */ ++ private function stopProfiling() ++ { ++ $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Retrieves statistics in the JSON format ++ * ++ * @return string ++ */ ++ private function getProfilingStat() ++ { ++ $stat = $this->statProfiler->get('job'); ++ unset($stat[Stat::START]); ++ return json_encode($stat); ++ } ++ + /** + * Return job collection from data base with status 'pending' + * + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ +- protected function _getPendingSchedules() ++ private function getPendingSchedules($groupId) + { +- if (!$this->_pendingSchedules) { +- $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- Schedule::STATUS_PENDING +- )->load(); +- } +- return $this->_pendingSchedules; ++ $jobs = $this->getJobs(); ++ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); ++ $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); ++ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); ++ return $pendingJobs; + } + + /** +@@ -278,22 +346,32 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param string $groupId + * @return $this + */ +- protected function _generate($groupId) ++ private function generateSchedules($groupId) + { + /** + * check if schedule generation is needed + */ + $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); +- $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( ++ $groupId, ++ self::XML_PATH_SCHEDULE_GENERATE_EVERY + ); + $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; +- if ($lastRun > $this->timezone->scopeTimeStamp() - $schedulePeriod) { ++ if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { + return $this; + } + +- $schedules = $this->_getPendingSchedules(); ++ /** ++ * save time schedules generation was ran with no expiration ++ */ ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, ++ ['crontab'], ++ null ++ ); ++ ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -303,18 +381,10 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * generate global crontab jobs + */ +- $jobs = $this->_config->getJobs(); ++ $jobs = $this->getJobs(); ++ $this->invalid = []; + $this->_generateJobs($jobs[$groupId], $exists, $groupId); +- +- /** +- * save time schedules generation was ran with no expiration +- */ +- $this->_cache->save( +- $this->timezone->scopeTimeStamp(), +- self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, +- ['crontab'], +- null +- ); ++ $this->cleanupScheduleMismatches(); + + return $this; + } +@@ -325,22 +395,12 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param array $jobs + * @param array $exists + * @param string $groupId +- * @return $this ++ * @return void + */ + protected function _generateJobs($jobs, $exists, $groupId) + { + foreach ($jobs as $jobCode => $jobConfig) { +- $cronExpression = null; +- if (isset($jobConfig['config_path'])) { +- $cronExpression = $this->getConfigSchedule($jobConfig) ?: null; +- } +- +- if (!$cronExpression) { +- if (isset($jobConfig['schedule'])) { +- $cronExpression = $jobConfig['schedule']; +- } +- } +- ++ $cronExpression = $this->getCronExpression($jobConfig); + if (!$cronExpression) { + continue; + } +@@ -348,75 +408,60 @@ class ProcessCronQueueObserver implements ObserverInterface + $timeInterval = $this->getScheduleTimeInterval($groupId); + $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); + } +- return $this; + } + + /** +- * Clean existed jobs ++ * Clean expired jobs + * +- * @param string $groupId +- * @return $this ++ * @param $groupId ++ * @param $currentTime ++ * @return void + */ +- protected function _cleanup($groupId) ++ private function cleanupJobs($groupId, $currentTime) + { + // check if history cleanup is needed + $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); +- $historyCleanUp = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- if ($lastCleanup > $this->timezone->scopeTimeStamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { ++ $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); ++ if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { + return $this; + } +- +- // check how long the record should stay unprocessed before marked as MISSED +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ // save time history cleanup was ran with no expiration ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, ++ ['crontab'], ++ null + ); +- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + +- /** +- * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history +- */ +- $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] +- )->load(); ++ $this->cleanupDisabledJobs($groupId); + +- $historySuccess = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- $historyFailure = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); ++ $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); + $historyLifetimes = [ + Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, + Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, ++ Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, + ]; + +- $now = $this->timezone->scopeTimeStamp(); +- /** @var Schedule $record */ +- foreach ($history as $record) { +- $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : +- strtotime($record->getScheduledAt()) + $scheduleLifetime; +- if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { +- $record->delete(); +- } ++ $jobs = $this->getJobs()[$groupId]; ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $connection = $scheduleResource->getConnection(); ++ $count = 0; ++ foreach ($historyLifetimes as $status => $time) { ++ $count += $connection->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => $status, ++ 'job_code in (?)' => array_keys($jobs), ++ 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ ] ++ ); + } + +- // save time history cleanup was ran with no expiration +- $this->_cache->save( +- $this->timezone->scopeTimeStamp(), +- self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, +- ['crontab'], +- null +- ); +- +- return $this; ++ if ($count) { ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -442,16 +487,23 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exists) + { +- $currentTime = $this->timezone->scopeTimeStamp(); ++ $currentTime = $this->dateTime->gmtTimestamp(); + $timeAhead = $currentTime + $timeInterval; + for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { +- $ts = strftime('%Y-%m-%d %H:%M:00', $time); +- if (!empty($exists[$jobCode . '/' . $ts])) { +- // already scheduled ++ $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); ++ $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); ++ $schedule = $this->createSchedule($jobCode, $cronExpression, $time); ++ $valid = $schedule->trySchedule(); ++ if (!$valid) { ++ if ($alreadyScheduled) { ++ if (!isset($this->invalid[$jobCode])) { ++ $this->invalid[$jobCode] = []; ++ } ++ $this->invalid[$jobCode][] = $scheduledAt; ++ } + continue; + } +- $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); +- if ($schedule->trySchedule()) { ++ if (!$alreadyScheduled) { + // time matches cron expression + $schedule->save(); + } +@@ -464,13 +516,13 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param int $time + * @return Schedule + */ +- protected function generateSchedule($jobCode, $cronExpression, $time) ++ protected function createSchedule($jobCode, $cronExpression, $time) + { + $schedule = $this->_scheduleFactory->create() + ->setCronExpr($cronExpression) + ->setJobCode($jobCode) + ->setStatus(Schedule::STATUS_PENDING) +- ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp())) ++ ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())) + ->setScheduledAt(strftime('%Y-%m-%d %H:%M', $time)); + + return $schedule; +@@ -482,12 +534,174 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function getScheduleTimeInterval($groupId) + { +- $scheduleAheadFor = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); + $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; + + return $scheduleAheadFor; + } ++ ++ /** ++ * Clean up scheduled jobs that are disabled in the configuration ++ * This can happen when you turn off a cron job in the config and flush the cache ++ * ++ * @param string $groupId ++ * @return void ++ */ ++ private function cleanupDisabledJobs($groupId) ++ { ++ $jobs = $this->getJobs(); ++ $jobsToCleanup = []; ++ foreach ($jobs[$groupId] as $jobCode => $jobConfig) { ++ if (!$this->getCronExpression($jobConfig)) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $jobsToCleanup[] = $jobCode; ++ } ++ } ++ ++ if (count($jobsToCleanup) > 0) { ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $count = $scheduleResource->getConnection()->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code in (?)' => $jobsToCleanup, ++ ] ++ ); ++ ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } ++ } ++ ++ /** ++ * @param array $jobConfig ++ * @return null|string ++ */ ++ private function getCronExpression($jobConfig) ++ { ++ $cronExpression = null; ++ if (isset($jobConfig['config_path'])) { ++ $cronExpression = $this->getConfigSchedule($jobConfig) ?: null; ++ } ++ ++ if (!$cronExpression) { ++ if (isset($jobConfig['schedule'])) { ++ $cronExpression = $jobConfig['schedule']; ++ } ++ } ++ return $cronExpression; ++ } ++ ++ /** ++ * Clean up scheduled jobs that do not match their cron expression anymore ++ * This can happen when you change the cron expression and flush the cache ++ * ++ * @return $this ++ */ ++ private function cleanupScheduleMismatches() ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ foreach ($this->invalid as $jobCode => $scheduledAtList) { ++ $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ]); ++ } ++ return $this; ++ } ++ ++ /** ++ * @return array ++ */ ++ private function getJobs() ++ { ++ if ($this->jobs === null) { ++ $this->jobs = $this->_config->getJobs(); ++ } ++ return $this->jobs; ++ } ++ ++ /** ++ * Get CronGroup Configuration Value ++ * ++ * @param $groupId ++ * @return int ++ */ ++ private function getCronGroupConfigurationValue($groupId, $path) ++ { ++ return $this->_scopeConfig->getValue( ++ 'system/cron/' . $groupId . '/' . $path, ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ return $scheduleLifetime; ++ } ++ ++ /** ++ * Is Group In Filter ++ * ++ * @param $groupId ++ * @return bool ++ */ ++ private function isGroupInFilter($groupId): bool ++ { ++ return !($this->_request->getParam('group') !== null ++ && trim($this->_request->getParam('group'), "'") !== $groupId); ++ } ++ ++ /** ++ * Process pending jobs ++ * ++ * @param $groupId ++ * @param $jobsRoot ++ * @param $currentTime ++ */ ++ private function processPendingJobs($groupId, $jobsRoot, $currentTime) ++ { ++ $procesedJobs = []; ++ $pendingJobs = $this->getPendingSchedules($groupId); ++ /** @var \Magento\Cron\Model\Schedule $schedule */ ++ foreach ($pendingJobs as $schedule) { ++ if (isset($procesedJobs[$schedule->getJobCode()])) { ++ // process only on job per run ++ continue; ++ } ++ $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; ++ if (!$jobConfig) { ++ continue; ++ } ++ ++ $scheduledTime = strtotime($schedule->getScheduledAt()); ++ if ($scheduledTime > $currentTime) { ++ continue; ++ } ++ ++ try { ++ if ($schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ } ++ } catch (\Exception $e) { ++ $schedule->setMessages($e->getMessage()); ++ if ($schedule->getStatus() === Schedule::STATUS_ERROR) { ++ $this->logger->critical($e); ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_MISSED ++ && $this->state->getMode() === State::MODE_DEVELOPER ++ ) { ++ $this->logger->error( ++ sprintf( ++ "%s Schedule Id: %s Job Code: %s", ++ $schedule->getMessages(), ++ $schedule->getScheduleId(), ++ $schedule->getJobCode() ++ ) ++ ); ++ } ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { ++ $procesedJobs[$schedule->getJobCode()] = true; ++ } ++ $schedule->save(); ++ } ++ } + } + +diff -Naur a/vendor/magento/module-cron/Model/Schedule.php b/vendor/magento/module-cron/Model/Schedule.php +--- a/vendor/magento/module-cron/Model/Schedule.php ++++ b/vendor/magento/module-cron/Model/Schedule.php +@@ -1,18 +1,18 @@ + ++ * @api ++ * @since 100.0.2 + */ + class Schedule extends \Magento\Framework\Model\AbstractModel + { +@@ -45,20 +46,28 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + const STATUS_ERROR = 'error'; + + /** ++ * @var TimezoneInterface ++ */ ++ private $timezoneConverter; ++ ++ /** + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource + * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param array $data ++ * @param TimezoneInterface $timezoneConverter + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, +- array $data = [] ++ array $data = [], ++ TimezoneInterface $timezoneConverter = null + ) { + parent::__construct($context, $registry, $resource, $resourceCollection, $data); ++ $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + } + + /** +@@ -66,7 +75,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + */ + public function _construct() + { +- $this->_init('Magento\Cron\Model\ResourceModel\Schedule'); ++ $this->_init(\Magento\Cron\Model\ResourceModel\Schedule::class); + } + + /** +@@ -101,6 +110,9 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + return false; + } + if (!is_numeric($time)) { ++ //convert time from UTC to admin store timezone ++ //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone ++ $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); + $time = strtotime($time); + } + $match = $this->matchCronExpression($e[0], strftime('%M', $time)) +@@ -221,16 +233,17 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. +- * Returns true if status was changed and false otherwise. ++ * Lock the cron job so no other scheduled instances run simultaneously. + * +- * This is used to implement locking for cron jobs. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING ++ * and no other jobs of the same code are currently in STATUS_RUNNING. ++ * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobStatusAtomic( ++ if ($this->_getResource()->trySetJobUniqueStatusAtomic( + $this->getId(), + self::STATUS_RUNNING, + self::STATUS_PENDING + +diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +index dca4e22..25dd02c 100644 +--- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php ++++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +@@ -8,7 +8,8 @@ namespace Magento\Cron\Model\ResourceModel; + /** + * Schedule resource + * +- * @author Magento Core Team ++ * @api ++ * @since 100.0.2 + */ + class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + { +@@ -23,9 +24,10 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + + /** +- * If job is currently in $currentStatus, set it to $newStatus +- * and return true. Otherwise, return false and do not change the job. +- * This method is used to implement locking for cron jobs. ++ * Sets new schedule status only if it's in the expected current status. ++ * ++ * If schedule is currently in $currentStatus, set it to $newStatus and ++ * return true. Otherwise, return false. + * + * @param string $scheduleId + * @param string $newStatus +@@ -45,4 +47,49 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + return false; + } ++ ++ /** ++ * Sets schedule status only if no existing schedules with the same job code ++ * have that status. This is used to implement locking for cron jobs. ++ * ++ * If the schedule is currently in $currentStatus and there are no existing ++ * schedules with the same job code and $newStatus, set the schedule to ++ * $newStatus and return true. Otherwise, return false. ++ * ++ * @param string $scheduleId ++ * @param string $newStatus ++ * @param string $currentStatus ++ * @return bool ++ * @since 100.2.0 ++ */ ++ public function trySetJobUniqueStatusAtomic($scheduleId, $newStatus, $currentStatus) ++ { ++ $connection = $this->getConnection(); ++ ++ // this condition added to avoid cron jobs locking after incorrect termination of running job ++ $match = $connection->quoteInto( ++ 'existing.job_code = current.job_code ' . ++ 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL) ' . ++ 'AND existing.status = ?', ++ $newStatus ++ ); ++ ++ $selectIfUnlocked = $connection->select() ++ ->joinLeft( ++ ['existing' => $this->getTable('cron_schedule')], ++ $match, ++ ['status' => new \Zend_Db_Expr($connection->quote($newStatus))] ++ ) ++ ->where('current.schedule_id = ?', $scheduleId) ++ ->where('current.status = ?', $currentStatus) ++ ->where('existing.schedule_id IS NULL'); ++ ++ $update = $connection->updateFromSelect($selectIfUnlocked, ['current' => $this->getTable('cron_schedule')]); ++ $result = $connection->query($update)->rowCount(); ++ ++ if ($result == 1) { ++ return true; ++ } ++ return false; ++ } + } diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch new file mode 100644 index 00000000..be3173c7 --- /dev/null +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch @@ -0,0 +1,924 @@ +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -1,6 +1,6 @@ + _objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -134,8 +169,11 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->_scopeConfig = $scopeConfig; + $this->_request = $request; + $this->_shell = $shell; +- $this->timezone = $timezone; ++ $this->dateTime = $dateTime; + $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); ++ $this->logger = $logger; ++ $this->state = $state; ++ $this->statProfiler = $statFactory->create(); + } + + /** +@@ -151,26 +189,29 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- $pendingJobs = $this->_getPendingSchedules(); +- $currentTime = $this->timezone->scopeTimeStamp(); ++ ++ $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); ++ // sort jobs groups to start from used in separated process ++ uksort( ++ $jobGroupsRoot, ++ function ($a, $b) { ++ return $this->getCronGroupConfigurationValue($b, 'use_separate_process') ++ - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); ++ } ++ ); + + $phpPath = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($jobGroupsRoot as $groupId => $jobsRoot) { +- if ($this->_request->getParam('group') !== null +- && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' +- && $this->_request->getParam('group') !== $groupId) { ++ if (!$this->isGroupInFilter($groupId)) { + continue; + } +- if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( +- $this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/use_separate_process', +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ) == 1 +- )) { ++ if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' ++ && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 ++ ) { + $this->_shell->execute( +- $phpPath . ' %s cron:run --group=' . $groupId . ' --' . CLI::INPUT_KEY_BOOTSTRAP . '=' ++ $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' + . self::STANDALONE_PROCESS_STARTED . '=1', + [ + BP . '/bin/magento' +@@ -179,29 +220,9 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- foreach ($pendingJobs as $schedule) { +- $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; +- if (!$jobConfig) { +- continue; +- } +- +- $scheduledTime = strtotime($schedule->getScheduledAt()); +- if ($scheduledTime > $currentTime) { +- continue; +- } +- +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $schedule->setMessages($e->getMessage()); +- } +- $schedule->save(); +- } +- +- $this->_generate($groupId); +- $this->_cleanup($groupId); ++ $this->cleanupJobs($groupId, $currentTime); ++ $this->generateSchedules($groupId); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -218,58 +239,105 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) + { +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $jobCode = $schedule->getJobCode(); ++ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ $this->logger->info(sprintf('Cron Job %s is missed', $jobCode)); + throw new \Exception('Too late for the schedule'); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception('No callbacks found'); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + ++ $this->startProfiling(); + try { ++ $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + call_user_func_array($callback, [$schedule]); + } catch (\Exception $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf( ++ 'Cron Job %s has an error. Statistics: %s %s', ++ $jobCode, ++ $this->getProfilingStat(), $e->getMessage() ++ )); + throw $e; ++ } finally { ++ $this->stopProfiling(); + } + + $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( + '%Y-%m-%d %H:%M:%S', +- $this->timezone->scopeTimeStamp() ++ $this->dateTime->gmtTimestamp() ++ )); ++ ++ $this->logger->info(sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() + )); + } + + /** ++ * Starts profiling ++ * ++ * @return void ++ */ ++ private function startProfiling() ++ { ++ $this->statProfiler->clear(); ++ $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Stops profiling ++ * ++ * @return void ++ */ ++ private function stopProfiling() ++ { ++ $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Retrieves statistics in the JSON format ++ * ++ * @return string ++ */ ++ private function getProfilingStat() ++ { ++ $stat = $this->statProfiler->get('job'); ++ unset($stat[Stat::START]); ++ return json_encode($stat); ++ } ++ ++ /** + * Return job collection from data base with status 'pending' + * + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ +- protected function _getPendingSchedules() ++ private function getPendingSchedules($groupId) + { +- if (!$this->_pendingSchedules) { +- $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- Schedule::STATUS_PENDING +- )->load(); +- } +- return $this->_pendingSchedules; ++ $jobs = $this->getJobs(); ++ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); ++ $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); ++ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); ++ return $pendingJobs; + } + + /** +@@ -278,22 +346,32 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param string $groupId + * @return $this + */ +- protected function _generate($groupId) ++ private function generateSchedules($groupId) + { + /** + * check if schedule generation is needed + */ + $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); +- $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( ++ $groupId, ++ self::XML_PATH_SCHEDULE_GENERATE_EVERY + ); + $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; +- if ($lastRun > $this->timezone->scopeTimeStamp() - $schedulePeriod) { ++ if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { + return $this; + } + +- $schedules = $this->_getPendingSchedules(); ++ /** ++ * save time schedules generation was ran with no expiration ++ */ ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, ++ ['crontab'], ++ null ++ ); ++ ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -303,18 +381,10 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * generate global crontab jobs + */ +- $jobs = $this->_config->getJobs(); ++ $jobs = $this->getJobs(); ++ $this->invalid = []; + $this->_generateJobs($jobs[$groupId], $exists, $groupId); +- +- /** +- * save time schedules generation was ran with no expiration +- */ +- $this->_cache->save( +- $this->timezone->scopeTimeStamp(), +- self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, +- ['crontab'], +- null +- ); ++ $this->cleanupScheduleMismatches(); + + return $this; + } +@@ -325,22 +395,12 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param array $jobs + * @param array $exists + * @param string $groupId +- * @return $this ++ * @return void + */ + protected function _generateJobs($jobs, $exists, $groupId) + { + foreach ($jobs as $jobCode => $jobConfig) { +- $cronExpression = null; +- if (isset($jobConfig['config_path'])) { +- $cronExpression = $this->getConfigSchedule($jobConfig) ?: null; +- } +- +- if (!$cronExpression) { +- if (isset($jobConfig['schedule'])) { +- $cronExpression = $jobConfig['schedule']; +- } +- } +- ++ $cronExpression = $this->getCronExpression($jobConfig); + if (!$cronExpression) { + continue; + } +@@ -348,75 +408,60 @@ class ProcessCronQueueObserver implements ObserverInterface + $timeInterval = $this->getScheduleTimeInterval($groupId); + $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); + } +- return $this; + } + + /** +- * Clean existed jobs ++ * Clean expired jobs + * +- * @param string $groupId +- * @return $this ++ * @param $groupId ++ * @param $currentTime ++ * @return void + */ +- protected function _cleanup($groupId) ++ private function cleanupJobs($groupId, $currentTime) + { + // check if history cleanup is needed + $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); +- $historyCleanUp = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- if ($lastCleanup > $this->timezone->scopeTimeStamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { ++ $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); ++ if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { + return $this; + } +- +- // check how long the record should stay unprocessed before marked as MISSED +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ // save time history cleanup was ran with no expiration ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, ++ ['crontab'], ++ null + ); +- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + +- /** +- * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history +- */ +- $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] +- )->load(); ++ $this->cleanupDisabledJobs($groupId); + +- $historySuccess = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- $historyFailure = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); ++ $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); + $historyLifetimes = [ + Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, + Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, ++ Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, + ]; + +- $now = $this->timezone->scopeTimeStamp(); +- /** @var Schedule $record */ +- foreach ($history as $record) { +- $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : +- strtotime($record->getScheduledAt()) + $scheduleLifetime; +- if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { +- $record->delete(); +- } ++ $jobs = $this->getJobs()[$groupId]; ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $connection = $scheduleResource->getConnection(); ++ $count = 0; ++ foreach ($historyLifetimes as $status => $time) { ++ $count += $connection->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => $status, ++ 'job_code in (?)' => array_keys($jobs), ++ 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ ] ++ ); + } + +- // save time history cleanup was ran with no expiration +- $this->_cache->save( +- $this->timezone->scopeTimeStamp(), +- self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, +- ['crontab'], +- null +- ); +- +- return $this; ++ if ($count) { ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -442,19 +487,25 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exists) + { +- $currentTime = $this->timezone->scopeTimeStamp(); ++ $currentTime = $this->dateTime->gmtTimestamp(); + $timeAhead = $currentTime + $timeInterval; + for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { +- $ts = strftime('%Y-%m-%d %H:%M:00', $time); +- if (!empty($exists[$jobCode . '/' . $ts])) { +- // already scheduled ++ $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); ++ $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); ++ $schedule = $this->createSchedule($jobCode, $cronExpression, $time); ++ $valid = $schedule->trySchedule(); ++ if (!$valid) { ++ if ($alreadyScheduled) { ++ if (!isset($this->invalid[$jobCode])) { ++ $this->invalid[$jobCode] = []; ++ } ++ $this->invalid[$jobCode][] = $scheduledAt; ++ } + continue; + } +- $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); +- if ($schedule->trySchedule()) { ++ if (!$alreadyScheduled) { + // time matches cron expression + $schedule->save(); +- return; + } + } + } +@@ -465,13 +516,13 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param int $time + * @return Schedule + */ +- protected function generateSchedule($jobCode, $cronExpression, $time) ++ protected function createSchedule($jobCode, $cronExpression, $time) + { + $schedule = $this->_scheduleFactory->create() + ->setCronExpr($cronExpression) + ->setJobCode($jobCode) + ->setStatus(Schedule::STATUS_PENDING) +- ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp())) ++ ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())) + ->setScheduledAt(strftime('%Y-%m-%d %H:%M', $time)); + + return $schedule; +@@ -483,12 +534,174 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function getScheduleTimeInterval($groupId) + { +- $scheduleAheadFor = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); + $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; + + return $scheduleAheadFor; + } ++ ++ /** ++ * Clean up scheduled jobs that are disabled in the configuration ++ * This can happen when you turn off a cron job in the config and flush the cache ++ * ++ * @param string $groupId ++ * @return void ++ */ ++ private function cleanupDisabledJobs($groupId) ++ { ++ $jobs = $this->getJobs(); ++ $jobsToCleanup = []; ++ foreach ($jobs[$groupId] as $jobCode => $jobConfig) { ++ if (!$this->getCronExpression($jobConfig)) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $jobsToCleanup[] = $jobCode; ++ } ++ } ++ ++ if (count($jobsToCleanup) > 0) { ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $count = $scheduleResource->getConnection()->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code in (?)' => $jobsToCleanup, ++ ] ++ ); ++ ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } ++ } ++ ++ /** ++ * @param array $jobConfig ++ * @return null|string ++ */ ++ private function getCronExpression($jobConfig) ++ { ++ $cronExpression = null; ++ if (isset($jobConfig['config_path'])) { ++ $cronExpression = $this->getConfigSchedule($jobConfig) ?: null; ++ } ++ ++ if (!$cronExpression) { ++ if (isset($jobConfig['schedule'])) { ++ $cronExpression = $jobConfig['schedule']; ++ } ++ } ++ return $cronExpression; ++ } ++ ++ /** ++ * Clean up scheduled jobs that do not match their cron expression anymore ++ * This can happen when you change the cron expression and flush the cache ++ * ++ * @return $this ++ */ ++ private function cleanupScheduleMismatches() ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ foreach ($this->invalid as $jobCode => $scheduledAtList) { ++ $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ]); ++ } ++ return $this; ++ } ++ ++ /** ++ * @return array ++ */ ++ private function getJobs() ++ { ++ if ($this->jobs === null) { ++ $this->jobs = $this->_config->getJobs(); ++ } ++ return $this->jobs; ++ } ++ ++ /** ++ * Get CronGroup Configuration Value ++ * ++ * @param $groupId ++ * @return int ++ */ ++ private function getCronGroupConfigurationValue($groupId, $path) ++ { ++ return $this->_scopeConfig->getValue( ++ 'system/cron/' . $groupId . '/' . $path, ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ return $scheduleLifetime; ++ } ++ ++ /** ++ * Is Group In Filter ++ * ++ * @param $groupId ++ * @return bool ++ */ ++ private function isGroupInFilter($groupId): bool ++ { ++ return !($this->_request->getParam('group') !== null ++ && trim($this->_request->getParam('group'), "'") !== $groupId); ++ } ++ ++ /** ++ * Process pending jobs ++ * ++ * @param $groupId ++ * @param $jobsRoot ++ * @param $currentTime ++ */ ++ private function processPendingJobs($groupId, $jobsRoot, $currentTime) ++ { ++ $procesedJobs = []; ++ $pendingJobs = $this->getPendingSchedules($groupId); ++ /** @var \Magento\Cron\Model\Schedule $schedule */ ++ foreach ($pendingJobs as $schedule) { ++ if (isset($procesedJobs[$schedule->getJobCode()])) { ++ // process only on job per run ++ continue; ++ } ++ $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; ++ if (!$jobConfig) { ++ continue; ++ } ++ ++ $scheduledTime = strtotime($schedule->getScheduledAt()); ++ if ($scheduledTime > $currentTime) { ++ continue; ++ } ++ ++ try { ++ if ($schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ } ++ } catch (\Exception $e) { ++ $schedule->setMessages($e->getMessage()); ++ if ($schedule->getStatus() === Schedule::STATUS_ERROR) { ++ $this->logger->critical($e); ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_MISSED ++ && $this->state->getMode() === State::MODE_DEVELOPER ++ ) { ++ $this->logger->error( ++ sprintf( ++ "%s Schedule Id: %s Job Code: %s", ++ $schedule->getMessages(), ++ $schedule->getScheduleId(), ++ $schedule->getJobCode() ++ ) ++ ); ++ } ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { ++ $procesedJobs[$schedule->getJobCode()] = true; ++ } ++ $schedule->save(); ++ } ++ } + } + +diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +index dc401e3..25dd02c 100644 +--- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php ++++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +@@ -1,6 +1,6 @@ + ++ * @api ++ * @since 100.0.2 + */ + class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + { +@@ -23,9 +24,10 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + + /** +- * If job is currently in $currentStatus, set it to $newStatus +- * and return true. Otherwise, return false and do not change the job. +- * This method is used to implement locking for cron jobs. ++ * Sets new schedule status only if it's in the expected current status. ++ * ++ * If schedule is currently in $currentStatus, set it to $newStatus and ++ * return true. Otherwise, return false. + * + * @param string $scheduleId + * @param string $newStatus +@@ -45,4 +47,49 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + return false; + } ++ ++ /** ++ * Sets schedule status only if no existing schedules with the same job code ++ * have that status. This is used to implement locking for cron jobs. ++ * ++ * If the schedule is currently in $currentStatus and there are no existing ++ * schedules with the same job code and $newStatus, set the schedule to ++ * $newStatus and return true. Otherwise, return false. ++ * ++ * @param string $scheduleId ++ * @param string $newStatus ++ * @param string $currentStatus ++ * @return bool ++ * @since 100.2.0 ++ */ ++ public function trySetJobUniqueStatusAtomic($scheduleId, $newStatus, $currentStatus) ++ { ++ $connection = $this->getConnection(); ++ ++ // this condition added to avoid cron jobs locking after incorrect termination of running job ++ $match = $connection->quoteInto( ++ 'existing.job_code = current.job_code ' . ++ 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL) ' . ++ 'AND existing.status = ?', ++ $newStatus ++ ); ++ ++ $selectIfUnlocked = $connection->select() ++ ->joinLeft( ++ ['existing' => $this->getTable('cron_schedule')], ++ $match, ++ ['status' => new \Zend_Db_Expr($connection->quote($newStatus))] ++ ) ++ ->where('current.schedule_id = ?', $scheduleId) ++ ->where('current.status = ?', $currentStatus) ++ ->where('existing.schedule_id IS NULL'); ++ ++ $update = $connection->updateFromSelect($selectIfUnlocked, ['current' => $this->getTable('cron_schedule')]); ++ $result = $connection->query($update)->rowCount(); ++ ++ if ($result == 1) { ++ return true; ++ } ++ return false; ++ } + } + +diff -Naur a/vendor/magento/module-cron/Model/Schedule.php b/vendor/magento/module-cron/Model/Schedule.php +--- a/vendor/magento/module-cron/Model/Schedule.php ++++ b/vendor/magento/module-cron/Model/Schedule.php +@@ -1,18 +1,18 @@ + ++ * @api ++ * @since 100.0.2 + */ + class Schedule extends \Magento\Framework\Model\AbstractModel + { +@@ -45,20 +46,28 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + const STATUS_ERROR = 'error'; + + /** ++ * @var TimezoneInterface ++ */ ++ private $timezoneConverter; ++ ++ /** + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource + * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param array $data ++ * @param TimezoneInterface $timezoneConverter + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, +- array $data = [] ++ array $data = [], ++ TimezoneInterface $timezoneConverter = null + ) { + parent::__construct($context, $registry, $resource, $resourceCollection, $data); ++ $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + } + + /** +@@ -66,7 +75,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + */ + public function _construct() + { +- $this->_init('Magento\Cron\Model\ResourceModel\Schedule'); ++ $this->_init(\Magento\Cron\Model\ResourceModel\Schedule::class); + } + + /** +@@ -101,6 +110,9 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + return false; + } + if (!is_numeric($time)) { ++ //convert time from UTC to admin store timezone ++ //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone ++ $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); + $time = strtotime($time); + } + $match = $this->matchCronExpression($e[0], strftime('%M', $time)) +@@ -221,16 +233,17 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. +- * Returns true if status was changed and false otherwise. ++ * Lock the cron job so no other scheduled instances run simultaneously. + * +- * This is used to implement locking for cron jobs. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING ++ * and no other jobs of the same code are currently in STATUS_RUNNING. ++ * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobStatusAtomic( ++ if ($this->_getResource()->trySetJobUniqueStatusAtomic( + $this->getId(), + self::STATUS_RUNNING, + self::STATUS_PENDING diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch new file mode 100644 index 00000000..363704e7 --- /dev/null +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch @@ -0,0 +1,924 @@ +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -1,6 +1,6 @@ + _objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -134,8 +169,11 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->_scopeConfig = $scopeConfig; + $this->_request = $request; + $this->_shell = $shell; +- $this->timezone = $timezone; ++ $this->dateTime = $dateTime; + $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); ++ $this->logger = $logger; ++ $this->state = $state; ++ $this->statProfiler = $statFactory->create(); + } + + /** +@@ -151,26 +189,29 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- $pendingJobs = $this->_getPendingSchedules(); +- $currentTime = $this->timezone->scopeTimeStamp(); ++ ++ $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); ++ // sort jobs groups to start from used in separated process ++ uksort( ++ $jobGroupsRoot, ++ function ($a, $b) { ++ return $this->getCronGroupConfigurationValue($b, 'use_separate_process') ++ - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); ++ } ++ ); + + $phpPath = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($jobGroupsRoot as $groupId => $jobsRoot) { +- if ($this->_request->getParam('group') !== null +- && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' +- && $this->_request->getParam('group') !== $groupId) { ++ if (!$this->isGroupInFilter($groupId)) { + continue; + } +- if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( +- $this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/use_separate_process', +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ) == 1 +- )) { ++ if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' ++ && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 ++ ) { + $this->_shell->execute( +- $phpPath . ' %s cron:run --group=' . $groupId . ' --' . CLI::INPUT_KEY_BOOTSTRAP . '=' ++ $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' + . self::STANDALONE_PROCESS_STARTED . '=1', + [ + BP . '/bin/magento' +@@ -179,29 +220,9 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- foreach ($pendingJobs as $schedule) { +- $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; +- if (!$jobConfig) { +- continue; +- } +- +- $scheduledTime = strtotime($schedule->getScheduledAt()); +- if ($scheduledTime > $currentTime) { +- continue; +- } +- +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $schedule->setMessages($e->getMessage()); +- } +- $schedule->save(); +- } +- +- $this->_generate($groupId); +- $this->_cleanup($groupId); ++ $this->cleanupJobs($groupId, $currentTime); ++ $this->generateSchedules($groupId); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -218,58 +239,105 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) + { +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $jobCode = $schedule->getJobCode(); ++ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ $this->logger->info(sprintf('Cron Job %s is missed', $jobCode)); + throw new \Exception('Too late for the schedule'); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception('No callbacks found'); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + ++ $this->startProfiling(); + try { ++ $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + call_user_func_array($callback, [$schedule]); + } catch (\Exception $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf( ++ 'Cron Job %s has an error. Statistics: %s %s', ++ $jobCode, ++ $this->getProfilingStat(), $e->getMessage() ++ )); + throw $e; ++ } finally { ++ $this->stopProfiling(); + } + + $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( + '%Y-%m-%d %H:%M:%S', +- $this->timezone->scopeTimeStamp() ++ $this->dateTime->gmtTimestamp() ++ )); ++ ++ $this->logger->info(sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() + )); + } + + /** ++ * Starts profiling ++ * ++ * @return void ++ */ ++ private function startProfiling() ++ { ++ $this->statProfiler->clear(); ++ $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Stops profiling ++ * ++ * @return void ++ */ ++ private function stopProfiling() ++ { ++ $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Retrieves statistics in the JSON format ++ * ++ * @return string ++ */ ++ private function getProfilingStat() ++ { ++ $stat = $this->statProfiler->get('job'); ++ unset($stat[Stat::START]); ++ return json_encode($stat); ++ } ++ ++ /** + * Return job collection from data base with status 'pending' + * + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ +- protected function _getPendingSchedules() ++ private function getPendingSchedules($groupId) + { +- if (!$this->_pendingSchedules) { +- $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- Schedule::STATUS_PENDING +- )->load(); +- } +- return $this->_pendingSchedules; ++ $jobs = $this->getJobs(); ++ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); ++ $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); ++ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); ++ return $pendingJobs; + } + + /** +@@ -278,22 +346,32 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param string $groupId + * @return $this + */ +- protected function _generate($groupId) ++ private function generateSchedules($groupId) + { + /** + * check if schedule generation is needed + */ + $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); +- $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( ++ $groupId, ++ self::XML_PATH_SCHEDULE_GENERATE_EVERY + ); + $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; +- if ($lastRun > $this->timezone->scopeTimeStamp() - $schedulePeriod) { ++ if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { + return $this; + } + +- $schedules = $this->_getPendingSchedules(); ++ /** ++ * save time schedules generation was ran with no expiration ++ */ ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, ++ ['crontab'], ++ null ++ ); ++ ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -303,18 +381,10 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * generate global crontab jobs + */ +- $jobs = $this->_config->getJobs(); ++ $jobs = $this->getJobs(); ++ $this->invalid = []; + $this->_generateJobs($jobs[$groupId], $exists, $groupId); +- +- /** +- * save time schedules generation was ran with no expiration +- */ +- $this->_cache->save( +- $this->timezone->scopeTimeStamp(), +- self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, +- ['crontab'], +- null +- ); ++ $this->cleanupScheduleMismatches(); + + return $this; + } +@@ -325,22 +395,12 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param array $jobs + * @param array $exists + * @param string $groupId +- * @return $this ++ * @return void + */ + protected function _generateJobs($jobs, $exists, $groupId) + { + foreach ($jobs as $jobCode => $jobConfig) { +- $cronExpression = null; +- if (isset($jobConfig['config_path'])) { +- $cronExpression = $this->getConfigSchedule($jobConfig) ?: null; +- } +- +- if (!$cronExpression) { +- if (isset($jobConfig['schedule'])) { +- $cronExpression = $jobConfig['schedule']; +- } +- } +- ++ $cronExpression = $this->getCronExpression($jobConfig); + if (!$cronExpression) { + continue; + } +@@ -348,75 +408,60 @@ class ProcessCronQueueObserver implements ObserverInterface + $timeInterval = $this->getScheduleTimeInterval($groupId); + $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); + } +- return $this; + } + + /** +- * Clean existed jobs ++ * Clean expired jobs + * +- * @param string $groupId +- * @return $this ++ * @param $groupId ++ * @param $currentTime ++ * @return void + */ +- protected function _cleanup($groupId) ++ private function cleanupJobs($groupId, $currentTime) + { + // check if history cleanup is needed + $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); +- $historyCleanUp = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- if ($lastCleanup > $this->timezone->scopeTimeStamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { ++ $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); ++ if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { + return $this; + } +- +- // check how long the record should stay unprocessed before marked as MISSED +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ // save time history cleanup was ran with no expiration ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, ++ ['crontab'], ++ null + ); +- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + +- /** +- * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history +- */ +- $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] +- )->load(); ++ $this->cleanupDisabledJobs($groupId); + +- $historySuccess = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- $historyFailure = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); ++ $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); + $historyLifetimes = [ + Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, + Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, ++ Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, + ]; + +- $now = $this->timezone->scopeTimeStamp(); +- /** @var Schedule $record */ +- foreach ($history as $record) { +- $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : +- strtotime($record->getScheduledAt()) + $scheduleLifetime; +- if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { +- $record->delete(); +- } ++ $jobs = $this->getJobs()[$groupId]; ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $connection = $scheduleResource->getConnection(); ++ $count = 0; ++ foreach ($historyLifetimes as $status => $time) { ++ $count += $connection->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => $status, ++ 'job_code in (?)' => array_keys($jobs), ++ 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ ] ++ ); + } + +- // save time history cleanup was ran with no expiration +- $this->_cache->save( +- $this->timezone->scopeTimeStamp(), +- self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, +- ['crontab'], +- null +- ); +- +- return $this; ++ if ($count) { ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -442,19 +487,25 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exists) + { +- $currentTime = $this->timezone->scopeTimeStamp(); ++ $currentTime = $this->dateTime->gmtTimestamp(); + $timeAhead = $currentTime + $timeInterval; + for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { +- $ts = strftime('%Y-%m-%d %H:%M:00', $time); +- if (!empty($exists[$jobCode . '/' . $ts])) { +- // already scheduled ++ $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); ++ $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); ++ $schedule = $this->createSchedule($jobCode, $cronExpression, $time); ++ $valid = $schedule->trySchedule(); ++ if (!$valid) { ++ if ($alreadyScheduled) { ++ if (!isset($this->invalid[$jobCode])) { ++ $this->invalid[$jobCode] = []; ++ } ++ $this->invalid[$jobCode][] = $scheduledAt; ++ } + continue; + } +- $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); +- if ($schedule->trySchedule()) { ++ if (!$alreadyScheduled) { + // time matches cron expression + $schedule->save(); +- return; + } + } + } +@@ -465,13 +516,13 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param int $time + * @return Schedule + */ +- protected function generateSchedule($jobCode, $cronExpression, $time) ++ protected function createSchedule($jobCode, $cronExpression, $time) + { + $schedule = $this->_scheduleFactory->create() + ->setCronExpr($cronExpression) + ->setJobCode($jobCode) + ->setStatus(Schedule::STATUS_PENDING) +- ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp())) ++ ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())) + ->setScheduledAt(strftime('%Y-%m-%d %H:%M', $time)); + + return $schedule; +@@ -483,12 +534,174 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function getScheduleTimeInterval($groupId) + { +- $scheduleAheadFor = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); + $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; + + return $scheduleAheadFor; + } ++ ++ /** ++ * Clean up scheduled jobs that are disabled in the configuration ++ * This can happen when you turn off a cron job in the config and flush the cache ++ * ++ * @param string $groupId ++ * @return void ++ */ ++ private function cleanupDisabledJobs($groupId) ++ { ++ $jobs = $this->getJobs(); ++ $jobsToCleanup = []; ++ foreach ($jobs[$groupId] as $jobCode => $jobConfig) { ++ if (!$this->getCronExpression($jobConfig)) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $jobsToCleanup[] = $jobCode; ++ } ++ } ++ ++ if (count($jobsToCleanup) > 0) { ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $count = $scheduleResource->getConnection()->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code in (?)' => $jobsToCleanup, ++ ] ++ ); ++ ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } ++ } ++ ++ /** ++ * @param array $jobConfig ++ * @return null|string ++ */ ++ private function getCronExpression($jobConfig) ++ { ++ $cronExpression = null; ++ if (isset($jobConfig['config_path'])) { ++ $cronExpression = $this->getConfigSchedule($jobConfig) ?: null; ++ } ++ ++ if (!$cronExpression) { ++ if (isset($jobConfig['schedule'])) { ++ $cronExpression = $jobConfig['schedule']; ++ } ++ } ++ return $cronExpression; ++ } ++ ++ /** ++ * Clean up scheduled jobs that do not match their cron expression anymore ++ * This can happen when you change the cron expression and flush the cache ++ * ++ * @return $this ++ */ ++ private function cleanupScheduleMismatches() ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ foreach ($this->invalid as $jobCode => $scheduledAtList) { ++ $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ]); ++ } ++ return $this; ++ } ++ ++ /** ++ * @return array ++ */ ++ private function getJobs() ++ { ++ if ($this->jobs === null) { ++ $this->jobs = $this->_config->getJobs(); ++ } ++ return $this->jobs; ++ } ++ ++ /** ++ * Get CronGroup Configuration Value ++ * ++ * @param $groupId ++ * @return int ++ */ ++ private function getCronGroupConfigurationValue($groupId, $path) ++ { ++ return $this->_scopeConfig->getValue( ++ 'system/cron/' . $groupId . '/' . $path, ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ return $scheduleLifetime; ++ } ++ ++ /** ++ * Is Group In Filter ++ * ++ * @param $groupId ++ * @return bool ++ */ ++ private function isGroupInFilter($groupId): bool ++ { ++ return !($this->_request->getParam('group') !== null ++ && trim($this->_request->getParam('group'), "'") !== $groupId); ++ } ++ ++ /** ++ * Process pending jobs ++ * ++ * @param $groupId ++ * @param $jobsRoot ++ * @param $currentTime ++ */ ++ private function processPendingJobs($groupId, $jobsRoot, $currentTime) ++ { ++ $procesedJobs = []; ++ $pendingJobs = $this->getPendingSchedules($groupId); ++ /** @var \Magento\Cron\Model\Schedule $schedule */ ++ foreach ($pendingJobs as $schedule) { ++ if (isset($procesedJobs[$schedule->getJobCode()])) { ++ // process only on job per run ++ continue; ++ } ++ $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; ++ if (!$jobConfig) { ++ continue; ++ } ++ ++ $scheduledTime = strtotime($schedule->getScheduledAt()); ++ if ($scheduledTime > $currentTime) { ++ continue; ++ } ++ ++ try { ++ if ($schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ } ++ } catch (\Exception $e) { ++ $schedule->setMessages($e->getMessage()); ++ if ($schedule->getStatus() === Schedule::STATUS_ERROR) { ++ $this->logger->critical($e); ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_MISSED ++ && $this->state->getMode() === State::MODE_DEVELOPER ++ ) { ++ $this->logger->error( ++ sprintf( ++ "%s Schedule Id: %s Job Code: %s", ++ $schedule->getMessages(), ++ $schedule->getScheduleId(), ++ $schedule->getJobCode() ++ ) ++ ); ++ } ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { ++ $procesedJobs[$schedule->getJobCode()] = true; ++ } ++ $schedule->save(); ++ } ++ } + } + +diff -Naur a/vendor/magento/module-cron/Model/Schedule.php b/vendor/magento/module-cron/Model/Schedule.php +--- a/vendor/magento/module-cron/Model/Schedule.php ++++ b/vendor/magento/module-cron/Model/Schedule.php +@@ -1,18 +1,18 @@ + ++ * @api ++ * @since 100.0.2 + */ + class Schedule extends \Magento\Framework\Model\AbstractModel + { +@@ -45,20 +46,28 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + const STATUS_ERROR = 'error'; + + /** ++ * @var TimezoneInterface ++ */ ++ private $timezoneConverter; ++ ++ /** + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource + * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param array $data ++ * @param TimezoneInterface $timezoneConverter + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, +- array $data = [] ++ array $data = [], ++ TimezoneInterface $timezoneConverter = null + ) { + parent::__construct($context, $registry, $resource, $resourceCollection, $data); ++ $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + } + + /** +@@ -66,7 +75,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + */ + public function _construct() + { +- $this->_init('Magento\Cron\Model\ResourceModel\Schedule'); ++ $this->_init(\Magento\Cron\Model\ResourceModel\Schedule::class); + } + + /** +@@ -101,6 +110,9 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + return false; + } + if (!is_numeric($time)) { ++ //convert time from UTC to admin store timezone ++ //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone ++ $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); + $time = strtotime($time); + } + $match = $this->matchCronExpression($e[0], strftime('%M', $time)) +@@ -221,16 +233,17 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. +- * Returns true if status was changed and false otherwise. ++ * Lock the cron job so no other scheduled instances run simultaneously. + * +- * This is used to implement locking for cron jobs. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING ++ * and no other jobs of the same code are currently in STATUS_RUNNING. ++ * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobStatusAtomic( ++ if ($this->_getResource()->trySetJobUniqueStatusAtomic( + $this->getId(), + self::STATUS_RUNNING, + self::STATUS_PENDING + +diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +index dca4e22..25dd02c 100644 +--- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php ++++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +@@ -1,6 +1,6 @@ + ++ * @api ++ * @since 100.0.2 + */ + class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + { +@@ -23,9 +24,10 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + + /** +- * If job is currently in $currentStatus, set it to $newStatus +- * and return true. Otherwise, return false and do not change the job. +- * This method is used to implement locking for cron jobs. ++ * Sets new schedule status only if it's in the expected current status. ++ * ++ * If schedule is currently in $currentStatus, set it to $newStatus and ++ * return true. Otherwise, return false. + * + * @param string $scheduleId + * @param string $newStatus +@@ -45,4 +47,49 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + return false; + } ++ ++ /** ++ * Sets schedule status only if no existing schedules with the same job code ++ * have that status. This is used to implement locking for cron jobs. ++ * ++ * If the schedule is currently in $currentStatus and there are no existing ++ * schedules with the same job code and $newStatus, set the schedule to ++ * $newStatus and return true. Otherwise, return false. ++ * ++ * @param string $scheduleId ++ * @param string $newStatus ++ * @param string $currentStatus ++ * @return bool ++ * @since 100.2.0 ++ */ ++ public function trySetJobUniqueStatusAtomic($scheduleId, $newStatus, $currentStatus) ++ { ++ $connection = $this->getConnection(); ++ ++ // this condition added to avoid cron jobs locking after incorrect termination of running job ++ $match = $connection->quoteInto( ++ 'existing.job_code = current.job_code ' . ++ 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL) ' . ++ 'AND existing.status = ?', ++ $newStatus ++ ); ++ ++ $selectIfUnlocked = $connection->select() ++ ->joinLeft( ++ ['existing' => $this->getTable('cron_schedule')], ++ $match, ++ ['status' => new \Zend_Db_Expr($connection->quote($newStatus))] ++ ) ++ ->where('current.schedule_id = ?', $scheduleId) ++ ->where('current.status = ?', $currentStatus) ++ ->where('existing.schedule_id IS NULL'); ++ ++ $update = $connection->updateFromSelect($selectIfUnlocked, ['current' => $this->getTable('cron_schedule')]); ++ $result = $connection->query($update)->rowCount(); ++ ++ if ($result == 1) { ++ return true; ++ } ++ return false; ++ } + } diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch new file mode 100644 index 00000000..17cdb82c --- /dev/null +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch @@ -0,0 +1,616 @@ +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +index f772a6c..d760e92 100644 +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -13,6 +13,8 @@ use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; + use \Magento\Cron\Model\Schedule; ++use Magento\Framework\Profiler\Driver\Standard\Stat; ++use Magento\Framework\Profiler\Driver\Standard\StatFactory; + + /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) +@@ -127,6 +129,11 @@ class ProcessCronQueueObserver implements ObserverInterface + private $jobs; + + /** ++ * @var Stat ++ */ ++ private $statProfiler; ++ ++ /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory + * @param \Magento\Framework\App\CacheInterface $cache +@@ -138,6 +145,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\App\State $state ++ * @param StatFactory $statFactory + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -151,7 +159,8 @@ class ProcessCronQueueObserver implements ObserverInterface + \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, + \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, + \Psr\Log\LoggerInterface $logger, +- \Magento\Framework\App\State $state ++ \Magento\Framework\App\State $state, ++ StatFactory $statFactory + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -164,6 +173,7 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); + $this->logger = $logger; + $this->state = $state; ++ $this->statProfiler = $statFactory->create(); + } + + /** +@@ -179,27 +189,26 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- $pendingJobs = $this->_getPendingSchedules(); ++ + $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); ++ // sort jobs groups to start from used in separated process ++ uksort( ++ $jobGroupsRoot, ++ function ($a, $b) { ++ return $this->getCronGroupConfigurationValue($b, 'use_separate_process') ++ - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); ++ } ++ ); + + $phpPath = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($jobGroupsRoot as $groupId => $jobsRoot) { +- $this->_cleanup($groupId); +- $this->_generate($groupId); +- if ($this->_request->getParam('group') !== null +- && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' +- && $this->_request->getParam('group') !== $groupId +- ) { ++ if (!$this->isGroupInFilter($groupId)) { + continue; + } +- if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( +- $this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/use_separate_process', +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ) == 1 +- ) ++ if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' ++ && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 + ) { + $this->_shell->execute( + $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' +@@ -211,42 +220,9 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- /** @var \Magento\Cron\Model\Schedule $schedule */ +- foreach ($pendingJobs as $schedule) { +- $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; +- if (!$jobConfig) { +- continue; +- } +- +- $scheduledTime = strtotime($schedule->getScheduledAt()); +- if ($scheduledTime > $currentTime) { +- continue; +- } +- +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $schedule->setMessages($e->getMessage()); +- if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +- $this->logger->critical($e); +- } +- if ($schedule->getStatus() === Schedule::STATUS_MISSED +- && $this->state->getMode() === State::MODE_DEVELOPER +- ) { +- $this->logger->info( +- sprintf( +- "%s Schedule Id: %s Job Code: %s", +- $schedule->getMessages(), +- $schedule->getScheduleId(), +- $schedule->getJobCode() +- ) +- ); +- } +- } +- $schedule->save(); +- } ++ $this->cleanupJobs($groupId, $currentTime); ++ $this->generateSchedules($groupId); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -263,24 +239,25 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) + { +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $jobCode = $schedule->getJobCode(); ++ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ $this->logger->info(sprintf('Cron Job %s is missed', $jobCode)); + throw new \Exception('Too late for the schedule'); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception('No callbacks found'); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); +@@ -288,17 +265,65 @@ class ProcessCronQueueObserver implements ObserverInterface + + $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + ++ $this->startProfiling(); + try { ++ $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + call_user_func_array($callback, [$schedule]); + } catch (\Exception $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf( ++ 'Cron Job %s has an error. Statistics: %s %s', ++ $jobCode, ++ $this->getProfilingStat(), $e->getMessage() ++ )); + throw $e; ++ } finally { ++ $this->stopProfiling(); + } + + $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( + '%Y-%m-%d %H:%M:%S', + $this->dateTime->gmtTimestamp() + )); ++ ++ $this->logger->info(sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() ++ )); ++ } ++ ++ /** ++ * Starts profiling ++ * ++ * @return void ++ */ ++ private function startProfiling() ++ { ++ $this->statProfiler->clear(); ++ $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Stops profiling ++ * ++ * @return void ++ */ ++ private function stopProfiling() ++ { ++ $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Retrieves statistics in the JSON format ++ * ++ * @return string ++ */ ++ private function getProfilingStat() ++ { ++ $stat = $this->statProfiler->get('job'); ++ unset($stat[Stat::START]); ++ return json_encode($stat); + } + + /** +@@ -306,15 +331,13 @@ class ProcessCronQueueObserver implements ObserverInterface + * + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ +- protected function _getPendingSchedules() ++ private function getPendingSchedules($groupId) + { +- if (!$this->_pendingSchedules) { +- $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- Schedule::STATUS_PENDING +- )->load(); +- } +- return $this->_pendingSchedules; ++ $jobs = $this->getJobs(); ++ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); ++ $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); ++ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); ++ return $pendingJobs; + } + + /** +@@ -323,22 +346,32 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param string $groupId + * @return $this + */ +- protected function _generate($groupId) ++ private function generateSchedules($groupId) + { + /** + * check if schedule generation is needed + */ + $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); +- $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( ++ $groupId, ++ self::XML_PATH_SCHEDULE_GENERATE_EVERY + ); + $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; + if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { + return $this; + } + +- $schedules = $this->_getPendingSchedules(); ++ /** ++ * save time schedules generation was ran with no expiration ++ */ ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, ++ ['crontab'], ++ null ++ ); ++ ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -353,16 +386,6 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->_generateJobs($jobs[$groupId], $exists, $groupId); + $this->cleanupScheduleMismatches(); + +- /** +- * save time schedules generation was ran with no expiration +- */ +- $this->_cache->save( +- $this->dateTime->gmtTimestamp(), +- self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, +- ['crontab'], +- null +- ); +- + return $this; + } + +@@ -372,7 +395,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param array $jobs + * @param array $exists + * @param string $groupId +- * @return $this ++ * @return void + */ + protected function _generateJobs($jobs, $exists, $groupId) + { +@@ -385,77 +408,60 @@ class ProcessCronQueueObserver implements ObserverInterface + $timeInterval = $this->getScheduleTimeInterval($groupId); + $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); + } +- return $this; + } + + /** + * Clean expired jobs + * +- * @param string $groupId +- * @return $this ++ * @param $groupId ++ * @param $currentTime ++ * @return void + */ +- protected function _cleanup($groupId) ++ private function cleanupJobs($groupId, $currentTime) + { +- $this->cleanupDisabledJobs($groupId); +- + // check if history cleanup is needed + $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); +- $historyCleanUp = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); + if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { + return $this; + } +- +- // check how long the record should stay unprocessed before marked as MISSED +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ // save time history cleanup was ran with no expiration ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, ++ ['crontab'], ++ null + ); +- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + +- /** +- * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history +- */ +- $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] +- )->load(); ++ $this->cleanupDisabledJobs($groupId); + +- $historySuccess = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- $historyFailure = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); ++ $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); + $historyLifetimes = [ + Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, + Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, ++ Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, + ]; + +- $now = $this->dateTime->gmtTimestamp(); +- /** @var Schedule $record */ +- foreach ($history as $record) { +- $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : +- strtotime($record->getScheduledAt()) + $scheduleLifetime; +- if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { +- $record->delete(); +- } ++ $jobs = $this->getJobs()[$groupId]; ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $connection = $scheduleResource->getConnection(); ++ $count = 0; ++ foreach ($historyLifetimes as $status => $time) { ++ $count += $connection->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => $status, ++ 'job_code in (?)' => array_keys($jobs), ++ 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ ] ++ ); + } + +- // save time history cleanup was ran with no expiration +- $this->_cache->save( +- $this->dateTime->gmtTimestamp(), +- self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, +- ['crontab'], +- null +- ); +- +- return $this; ++ if ($count) { ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -486,7 +492,7 @@ class ProcessCronQueueObserver implements ObserverInterface + for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { + $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); + $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); +- $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); ++ $schedule = $this->createSchedule($jobCode, $cronExpression, $time); + $valid = $schedule->trySchedule(); + if (!$valid) { + if ($alreadyScheduled) { +@@ -510,7 +516,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param int $time + * @return Schedule + */ +- protected function generateSchedule($jobCode, $cronExpression, $time) ++ protected function createSchedule($jobCode, $cronExpression, $time) + { + $schedule = $this->_scheduleFactory->create() + ->setCronExpr($cronExpression) +@@ -528,10 +534,7 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function getScheduleTimeInterval($groupId) + { +- $scheduleAheadFor = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); + $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; + + return $scheduleAheadFor; +@@ -547,16 +550,26 @@ class ProcessCronQueueObserver implements ObserverInterface + private function cleanupDisabledJobs($groupId) + { + $jobs = $this->getJobs(); ++ $jobsToCleanup = []; + foreach ($jobs[$groupId] as $jobCode => $jobConfig) { + if (!$this->getCronExpression($jobConfig)) { + /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status=?' => Schedule::STATUS_PENDING, +- 'job_code=?' => $jobCode, +- ]); ++ $jobsToCleanup[] = $jobCode; + } + } ++ ++ if (count($jobsToCleanup) > 0) { ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $count = $scheduleResource->getConnection()->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code in (?)' => $jobsToCleanup, ++ ] ++ ); ++ ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -586,12 +599,12 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function cleanupScheduleMismatches() + { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status=?' => Schedule::STATUS_PENDING, +- 'job_code=?' => $jobCode, ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, + 'scheduled_at in (?)' => $scheduledAtList, + ]); + } +@@ -608,4 +621,87 @@ class ProcessCronQueueObserver implements ObserverInterface + } + return $this->jobs; + } ++ ++ /** ++ * Get CronGroup Configuration Value ++ * ++ * @param $groupId ++ * @return int ++ */ ++ private function getCronGroupConfigurationValue($groupId, $path) ++ { ++ return $this->_scopeConfig->getValue( ++ 'system/cron/' . $groupId . '/' . $path, ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ return $scheduleLifetime; ++ } ++ ++ /** ++ * Is Group In Filter ++ * ++ * @param $groupId ++ * @return bool ++ */ ++ private function isGroupInFilter($groupId): bool ++ { ++ return !($this->_request->getParam('group') !== null ++ && trim($this->_request->getParam('group'), "'") !== $groupId); ++ } ++ ++ /** ++ * Process pending jobs ++ * ++ * @param $groupId ++ * @param $jobsRoot ++ * @param $currentTime ++ */ ++ private function processPendingJobs($groupId, $jobsRoot, $currentTime) ++ { ++ $procesedJobs = []; ++ $pendingJobs = $this->getPendingSchedules($groupId); ++ /** @var \Magento\Cron\Model\Schedule $schedule */ ++ foreach ($pendingJobs as $schedule) { ++ if (isset($procesedJobs[$schedule->getJobCode()])) { ++ // process only on job per run ++ continue; ++ } ++ $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; ++ if (!$jobConfig) { ++ continue; ++ } ++ ++ $scheduledTime = strtotime($schedule->getScheduledAt()); ++ if ($scheduledTime > $currentTime) { ++ continue; ++ } ++ ++ try { ++ if ($schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ } ++ } catch (\Exception $e) { ++ $schedule->setMessages($e->getMessage()); ++ if ($schedule->getStatus() === Schedule::STATUS_ERROR) { ++ $this->logger->critical($e); ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_MISSED ++ && $this->state->getMode() === State::MODE_DEVELOPER ++ ) { ++ $this->logger->info( ++ sprintf( ++ "%s Schedule Id: %s Job Code: %s", ++ $schedule->getMessages(), ++ $schedule->getScheduleId(), ++ $schedule->getJobCode() ++ ) ++ ); ++ } ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { ++ $procesedJobs[$schedule->getJobCode()] = true; ++ } ++ $schedule->save(); ++ } ++ } + } + +diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +index a47227b..25dd02c 100644 +--- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php ++++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +@@ -66,7 +66,14 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + { + $connection = $this->getConnection(); + +- $match = $connection->quoteInto('existing.job_code = current.job_code AND existing.status = ?', $newStatus); ++ // this condition added to avoid cron jobs locking after incorrect termination of running job ++ $match = $connection->quoteInto( ++ 'existing.job_code = current.job_code ' . ++ 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL) ' . ++ 'AND existing.status = ?', ++ $newStatus ++ ); ++ + $selectIfUnlocked = $connection->select() + ->joinLeft( + ['existing' => $this->getTable('cron_schedule')], diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch new file mode 100644 index 00000000..94b780ba --- /dev/null +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch @@ -0,0 +1,595 @@ +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +index f772a6c..d760e92 100644 +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -13,6 +13,8 @@ use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; + use \Magento\Cron\Model\Schedule; ++use Magento\Framework\Profiler\Driver\Standard\Stat; ++use Magento\Framework\Profiler\Driver\Standard\StatFactory; + + /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) +@@ -127,6 +129,11 @@ class ProcessCronQueueObserver implements ObserverInterface + private $jobs; + + /** ++ * @var Stat ++ */ ++ private $statProfiler; ++ ++ /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory + * @param \Magento\Framework\App\CacheInterface $cache +@@ -138,6 +145,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\App\State $state ++ * @param StatFactory $statFactory + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -151,7 +159,8 @@ class ProcessCronQueueObserver implements ObserverInterface + \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, + \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, + \Psr\Log\LoggerInterface $logger, +- \Magento\Framework\App\State $state ++ \Magento\Framework\App\State $state, ++ StatFactory $statFactory + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -164,6 +173,7 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); + $this->logger = $logger; + $this->state = $state; ++ $this->statProfiler = $statFactory->create(); + } + + /** +@@ -179,27 +189,26 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- $pendingJobs = $this->_getPendingSchedules(); ++ + $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); ++ // sort jobs groups to start from used in separated process ++ uksort( ++ $jobGroupsRoot, ++ function ($a, $b) { ++ return $this->getCronGroupConfigurationValue($b, 'use_separate_process') ++ - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); ++ } ++ ); + + $phpPath = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($jobGroupsRoot as $groupId => $jobsRoot) { +- $this->_cleanup($groupId); +- $this->_generate($groupId); +- if ($this->_request->getParam('group') !== null +- && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' +- && $this->_request->getParam('group') !== $groupId +- ) { ++ if (!$this->isGroupInFilter($groupId)) { + continue; + } +- if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( +- $this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/use_separate_process', +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ) == 1 +- ) ++ if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' ++ && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 + ) { + $this->_shell->execute( + $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' +@@ -211,42 +220,9 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- /** @var \Magento\Cron\Model\Schedule $schedule */ +- foreach ($pendingJobs as $schedule) { +- $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; +- if (!$jobConfig) { +- continue; +- } +- +- $scheduledTime = strtotime($schedule->getScheduledAt()); +- if ($scheduledTime > $currentTime) { +- continue; +- } +- +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $schedule->setMessages($e->getMessage()); +- if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +- $this->logger->critical($e); +- } +- if ($schedule->getStatus() === Schedule::STATUS_MISSED +- && $this->state->getMode() === State::MODE_DEVELOPER +- ) { +- $this->logger->info( +- sprintf( +- "%s Schedule Id: %s Job Code: %s", +- $schedule->getMessages(), +- $schedule->getScheduleId(), +- $schedule->getJobCode() +- ) +- ); +- } +- } +- $schedule->save(); +- } ++ $this->cleanupJobs($groupId, $currentTime); ++ $this->generateSchedules($groupId); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -263,24 +239,25 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) + { +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $jobCode = $schedule->getJobCode(); ++ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ $this->logger->info(sprintf('Cron Job %s is missed', $jobCode)); + throw new \Exception('Too late for the schedule'); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception('No callbacks found'); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); +@@ -288,17 +265,65 @@ class ProcessCronQueueObserver implements ObserverInterface + + $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + ++ $this->startProfiling(); + try { ++ $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + call_user_func_array($callback, [$schedule]); + } catch (\Exception $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf( ++ 'Cron Job %s has an error. Statistics: %s %s', ++ $jobCode, ++ $this->getProfilingStat(), $e->getMessage() ++ )); + throw $e; ++ } finally { ++ $this->stopProfiling(); + } + + $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( + '%Y-%m-%d %H:%M:%S', + $this->dateTime->gmtTimestamp() + )); ++ ++ $this->logger->info(sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() ++ )); ++ } ++ ++ /** ++ * Starts profiling ++ * ++ * @return void ++ */ ++ private function startProfiling() ++ { ++ $this->statProfiler->clear(); ++ $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Stops profiling ++ * ++ * @return void ++ */ ++ private function stopProfiling() ++ { ++ $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Retrieves statistics in the JSON format ++ * ++ * @return string ++ */ ++ private function getProfilingStat() ++ { ++ $stat = $this->statProfiler->get('job'); ++ unset($stat[Stat::START]); ++ return json_encode($stat); + } + + /** +@@ -306,15 +331,13 @@ class ProcessCronQueueObserver implements ObserverInterface + * + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ +- protected function _getPendingSchedules() ++ private function getPendingSchedules($groupId) + { +- if (!$this->_pendingSchedules) { +- $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- Schedule::STATUS_PENDING +- )->load(); +- } +- return $this->_pendingSchedules; ++ $jobs = $this->getJobs(); ++ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); ++ $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); ++ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); ++ return $pendingJobs; + } + + /** +@@ -323,22 +346,32 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param string $groupId + * @return $this + */ +- protected function _generate($groupId) ++ private function generateSchedules($groupId) + { + /** + * check if schedule generation is needed + */ + $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); +- $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( ++ $groupId, ++ self::XML_PATH_SCHEDULE_GENERATE_EVERY + ); + $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; + if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { + return $this; + } + +- $schedules = $this->_getPendingSchedules(); ++ /** ++ * save time schedules generation was ran with no expiration ++ */ ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, ++ ['crontab'], ++ null ++ ); ++ ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -353,16 +386,6 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->_generateJobs($jobs[$groupId], $exists, $groupId); + $this->cleanupScheduleMismatches(); + +- /** +- * save time schedules generation was ran with no expiration +- */ +- $this->_cache->save( +- $this->dateTime->gmtTimestamp(), +- self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, +- ['crontab'], +- null +- ); +- + return $this; + } + +@@ -372,7 +395,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param array $jobs + * @param array $exists + * @param string $groupId +- * @return $this ++ * @return void + */ + protected function _generateJobs($jobs, $exists, $groupId) + { +@@ -385,77 +408,60 @@ class ProcessCronQueueObserver implements ObserverInterface + $timeInterval = $this->getScheduleTimeInterval($groupId); + $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); + } +- return $this; + } + + /** + * Clean expired jobs + * +- * @param string $groupId +- * @return $this ++ * @param $groupId ++ * @param $currentTime ++ * @return void + */ +- protected function _cleanup($groupId) ++ private function cleanupJobs($groupId, $currentTime) + { +- $this->cleanupDisabledJobs($groupId); +- + // check if history cleanup is needed + $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); +- $historyCleanUp = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); + if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { + return $this; + } +- +- // check how long the record should stay unprocessed before marked as MISSED +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ // save time history cleanup was ran with no expiration ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, ++ ['crontab'], ++ null + ); +- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + +- /** +- * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history +- */ +- $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] +- )->load(); ++ $this->cleanupDisabledJobs($groupId); + +- $historySuccess = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- $historyFailure = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); ++ $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); + $historyLifetimes = [ + Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, + Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, ++ Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, + ]; + +- $now = $this->dateTime->gmtTimestamp(); +- /** @var Schedule $record */ +- foreach ($history as $record) { +- $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : +- strtotime($record->getScheduledAt()) + $scheduleLifetime; +- if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { +- $record->delete(); +- } ++ $jobs = $this->getJobs()[$groupId]; ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $connection = $scheduleResource->getConnection(); ++ $count = 0; ++ foreach ($historyLifetimes as $status => $time) { ++ $count += $connection->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => $status, ++ 'job_code in (?)' => array_keys($jobs), ++ 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ ] ++ ); + } + +- // save time history cleanup was ran with no expiration +- $this->_cache->save( +- $this->dateTime->gmtTimestamp(), +- self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, +- ['crontab'], +- null +- ); +- +- return $this; ++ if ($count) { ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -486,7 +492,7 @@ class ProcessCronQueueObserver implements ObserverInterface + for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { + $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); + $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); +- $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); ++ $schedule = $this->createSchedule($jobCode, $cronExpression, $time); + $valid = $schedule->trySchedule(); + if (!$valid) { + if ($alreadyScheduled) { +@@ -510,7 +516,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param int $time + * @return Schedule + */ +- protected function generateSchedule($jobCode, $cronExpression, $time) ++ protected function createSchedule($jobCode, $cronExpression, $time) + { + $schedule = $this->_scheduleFactory->create() + ->setCronExpr($cronExpression) +@@ -528,10 +534,7 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function getScheduleTimeInterval($groupId) + { +- $scheduleAheadFor = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); + $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; + + return $scheduleAheadFor; +@@ -547,16 +550,26 @@ class ProcessCronQueueObserver implements ObserverInterface + private function cleanupDisabledJobs($groupId) + { + $jobs = $this->getJobs(); ++ $jobsToCleanup = []; + foreach ($jobs[$groupId] as $jobCode => $jobConfig) { + if (!$this->getCronExpression($jobConfig)) { + /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status=?' => Schedule::STATUS_PENDING, +- 'job_code=?' => $jobCode, +- ]); ++ $jobsToCleanup[] = $jobCode; + } + } ++ ++ if (count($jobsToCleanup) > 0) { ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $count = $scheduleResource->getConnection()->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code in (?)' => $jobsToCleanup, ++ ] ++ ); ++ ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -586,12 +599,12 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function cleanupScheduleMismatches() + { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status=?' => Schedule::STATUS_PENDING, +- 'job_code=?' => $jobCode, ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, + 'scheduled_at in (?)' => $scheduledAtList, + ]); + } +@@ -608,4 +621,87 @@ class ProcessCronQueueObserver implements ObserverInterface + } + return $this->jobs; + } ++ ++ /** ++ * Get CronGroup Configuration Value ++ * ++ * @param $groupId ++ * @return int ++ */ ++ private function getCronGroupConfigurationValue($groupId, $path) ++ { ++ return $this->_scopeConfig->getValue( ++ 'system/cron/' . $groupId . '/' . $path, ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ return $scheduleLifetime; ++ } ++ ++ /** ++ * Is Group In Filter ++ * ++ * @param $groupId ++ * @return bool ++ */ ++ private function isGroupInFilter($groupId): bool ++ { ++ return !($this->_request->getParam('group') !== null ++ && trim($this->_request->getParam('group'), "'") !== $groupId); ++ } ++ ++ /** ++ * Process pending jobs ++ * ++ * @param $groupId ++ * @param $jobsRoot ++ * @param $currentTime ++ */ ++ private function processPendingJobs($groupId, $jobsRoot, $currentTime) ++ { ++ $procesedJobs = []; ++ $pendingJobs = $this->getPendingSchedules($groupId); ++ /** @var \Magento\Cron\Model\Schedule $schedule */ ++ foreach ($pendingJobs as $schedule) { ++ if (isset($procesedJobs[$schedule->getJobCode()])) { ++ // process only on job per run ++ continue; ++ } ++ $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; ++ if (!$jobConfig) { ++ continue; ++ } ++ ++ $scheduledTime = strtotime($schedule->getScheduledAt()); ++ if ($scheduledTime > $currentTime) { ++ continue; ++ } ++ ++ try { ++ if ($schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ } ++ } catch (\Exception $e) { ++ $schedule->setMessages($e->getMessage()); ++ if ($schedule->getStatus() === Schedule::STATUS_ERROR) { ++ $this->logger->critical($e); ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_MISSED ++ && $this->state->getMode() === State::MODE_DEVELOPER ++ ) { ++ $this->logger->info( ++ sprintf( ++ "%s Schedule Id: %s Job Code: %s", ++ $schedule->getMessages(), ++ $schedule->getScheduleId(), ++ $schedule->getJobCode() ++ ) ++ ); ++ } ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { ++ $procesedJobs[$schedule->getJobCode()] = true; ++ } ++ $schedule->save(); ++ } ++ } + } diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.4.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.4.patch new file mode 100644 index 00000000..95adfa92 --- /dev/null +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.4.patch @@ -0,0 +1,601 @@ +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -13,6 +13,8 @@ use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; + use \Magento\Cron\Model\Schedule; ++use Magento\Framework\Profiler\Driver\Standard\Stat; ++use Magento\Framework\Profiler\Driver\Standard\StatFactory; + + /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) +@@ -126,6 +128,11 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private $jobs; + ++ /** ++ * @var Stat ++ */ ++ private $statProfiler; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory +@@ -138,6 +145,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\App\State $state ++ * @param StatFactory $statFactory + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -151,7 +159,8 @@ class ProcessCronQueueObserver implements ObserverInterface + \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, + \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, + \Psr\Log\LoggerInterface $logger, +- \Magento\Framework\App\State $state ++ \Magento\Framework\App\State $state, ++ StatFactory $statFactory + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -164,6 +173,7 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); + $this->logger = $logger; + $this->state = $state; ++ $this->statProfiler = $statFactory->create(); + } + + /** +@@ -179,27 +189,26 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- $pendingJobs = $this->_getPendingSchedules(); ++ + $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); ++ // sort jobs groups to start from used in separated process ++ uksort( ++ $jobGroupsRoot, ++ function ($a, $b) { ++ return $this->getCronGroupConfigurationValue($b, 'use_separate_process') ++ - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); ++ } ++ ); + + $phpPath = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($jobGroupsRoot as $groupId => $jobsRoot) { +- $this->_cleanup($groupId); +- $this->_generate($groupId); +- if ($this->_request->getParam('group') !== null +- && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' +- && $this->_request->getParam('group') !== $groupId +- ) { ++ if (!$this->isGroupInFilter($groupId)) { + continue; + } +- if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( +- $this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/use_separate_process', +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ) == 1 +- ) ++ if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' ++ && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 + ) { + $this->_shell->execute( + $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' +@@ -211,42 +220,9 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- /** @var \Magento\Cron\Model\Schedule $schedule */ +- foreach ($pendingJobs as $schedule) { +- $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; +- if (!$jobConfig) { +- continue; +- } +- +- $scheduledTime = strtotime($schedule->getScheduledAt()); +- if ($scheduledTime > $currentTime) { +- continue; +- } +- +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $schedule->setMessages($e->getMessage()); +- if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +- $this->logger->critical($e); +- } +- if ($schedule->getStatus() === Schedule::STATUS_MISSED +- && $this->state->getMode() === State::MODE_DEVELOPER +- ) { +- $this->logger->info( +- sprintf( +- "%s Schedule Id: %s Job Code: %s", +- $schedule->getMessages(), +- $schedule->getScheduleId(), +- $schedule->getJobCode() +- ) +- ); +- } +- } +- $schedule->save(); +- } ++ $this->cleanupJobs($groupId, $currentTime); ++ $this->generateSchedules($groupId); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -263,24 +239,25 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) + { +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $jobCode = $schedule->getJobCode(); ++ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ $this->logger->info(sprintf('Cron Job %s is missed', $jobCode)); + throw new \Exception('Too late for the schedule'); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception('No callbacks found'); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ $this->logger->error(sprintf('Cron Job %s has an error', $jobCode)); + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); +@@ -288,8 +265,12 @@ class ProcessCronQueueObserver implements ObserverInterface + + $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + ++ $this->startProfiling(); + try { ++ $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + call_user_func_array($callback, [$schedule]); ++ ++ + } catch (\Throwable $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); + if (!$e instanceof \Exception) { +@@ -299,13 +280,59 @@ class ProcessCronQueueObserver implements ObserverInterface + $e + ); + } ++ $this->logger->error(sprintf( ++ 'Cron Job %s has an error. Statistics: %s %s', ++ $jobCode, ++ $this->getProfilingStat(), $e->getMessage() ++ )); + throw $e; ++ } finally { ++ $this->stopProfiling(); + } + + $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( + '%Y-%m-%d %H:%M:%S', + $this->dateTime->gmtTimestamp() + )); ++ ++ $this->logger->info(sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() ++ )); ++ } ++ ++ /** ++ * Starts profiling ++ * ++ * @return void ++ */ ++ private function startProfiling() ++ { ++ $this->statProfiler->clear(); ++ $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Stops profiling ++ * ++ * @return void ++ */ ++ private function stopProfiling() ++ { ++ $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); ++ } ++ ++ /** ++ * Retrieves statistics in the JSON format ++ * ++ * @return string ++ */ ++ private function getProfilingStat() ++ { ++ $stat = $this->statProfiler->get('job'); ++ unset($stat[Stat::START]); ++ return json_encode($stat); + } + + /** +@@ -313,15 +340,13 @@ class ProcessCronQueueObserver implements ObserverInterface + * + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ +- protected function _getPendingSchedules() ++ private function getPendingSchedules($groupId) + { +- if (!$this->_pendingSchedules) { +- $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- Schedule::STATUS_PENDING +- )->load(); +- } +- return $this->_pendingSchedules; ++ $jobs = $this->getJobs(); ++ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); ++ $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); ++ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); ++ return $pendingJobs; + } + + /** +@@ -330,22 +355,32 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param string $groupId + * @return $this + */ +- protected function _generate($groupId) ++ private function generateSchedules($groupId) + { + /** + * check if schedule generation is needed + */ + $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); +- $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( ++ $groupId, ++ self::XML_PATH_SCHEDULE_GENERATE_EVERY + ); + $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; + if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { + return $this; + } + +- $schedules = $this->_getPendingSchedules(); ++ /** ++ * save time schedules generation was ran with no expiration ++ */ ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, ++ ['crontab'], ++ null ++ ); ++ ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -360,16 +395,6 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->_generateJobs($jobs[$groupId], $exists, $groupId); + $this->cleanupScheduleMismatches(); + +- /** +- * save time schedules generation was ran with no expiration +- */ +- $this->_cache->save( +- $this->dateTime->gmtTimestamp(), +- self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, +- ['crontab'], +- null +- ); +- + return $this; + } + +@@ -379,7 +404,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param array $jobs + * @param array $exists + * @param string $groupId +- * @return $this ++ * @return void + */ + protected function _generateJobs($jobs, $exists, $groupId) + { +@@ -392,77 +417,60 @@ class ProcessCronQueueObserver implements ObserverInterface + $timeInterval = $this->getScheduleTimeInterval($groupId); + $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); + } +- return $this; + } + + /** + * Clean expired jobs + * +- * @param string $groupId +- * @return $this ++ * @param $groupId ++ * @param $currentTime ++ * @return void + */ +- protected function _cleanup($groupId) ++ private function cleanupJobs($groupId, $currentTime) + { +- $this->cleanupDisabledJobs($groupId); +- + // check if history cleanup is needed + $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); +- $historyCleanUp = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); + if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { + return $this; + } +- +- // check how long the record should stay unprocessed before marked as MISSED +- $scheduleLifetime = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ // save time history cleanup was ran with no expiration ++ $this->_cache->save( ++ $this->dateTime->gmtTimestamp(), ++ self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, ++ ['crontab'], ++ null + ); +- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + +- /** +- * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history +- */ +- $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( +- 'status', +- ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] +- )->load(); ++ $this->cleanupDisabledJobs($groupId); + +- $historySuccess = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); +- $historyFailure = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); ++ $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); + $historyLifetimes = [ + Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, + Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, ++ Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, + ]; + +- $now = $this->dateTime->gmtTimestamp(); +- /** @var Schedule $record */ +- foreach ($history as $record) { +- $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : +- strtotime($record->getScheduledAt()) + $scheduleLifetime; +- if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { +- $record->delete(); +- } ++ $jobs = $this->getJobs()[$groupId]; ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $connection = $scheduleResource->getConnection(); ++ $count = 0; ++ foreach ($historyLifetimes as $status => $time) { ++ $count += $connection->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => $status, ++ 'job_code in (?)' => array_keys($jobs), ++ 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ ] ++ ); + } + +- // save time history cleanup was ran with no expiration +- $this->_cache->save( +- $this->dateTime->gmtTimestamp(), +- self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, +- ['crontab'], +- null +- ); +- +- return $this; ++ if ($count) { ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -493,7 +501,7 @@ class ProcessCronQueueObserver implements ObserverInterface + for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { + $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); + $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); +- $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); ++ $schedule = $this->createSchedule($jobCode, $cronExpression, $time); + $valid = $schedule->trySchedule(); + if (!$valid) { + if ($alreadyScheduled) { +@@ -517,7 +525,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param int $time + * @return Schedule + */ +- protected function generateSchedule($jobCode, $cronExpression, $time) ++ protected function createSchedule($jobCode, $cronExpression, $time) + { + $schedule = $this->_scheduleFactory->create() + ->setCronExpr($cronExpression) +@@ -535,10 +543,7 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + protected function getScheduleTimeInterval($groupId) + { +- $scheduleAheadFor = (int)$this->_scopeConfig->getValue( +- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, +- \Magento\Store\Model\ScopeInterface::SCOPE_STORE +- ); ++ $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); + $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; + + return $scheduleAheadFor; +@@ -554,16 +559,26 @@ class ProcessCronQueueObserver implements ObserverInterface + private function cleanupDisabledJobs($groupId) + { + $jobs = $this->getJobs(); ++ $jobsToCleanup = []; + foreach ($jobs[$groupId] as $jobCode => $jobConfig) { + if (!$this->getCronExpression($jobConfig)) { + /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status=?' => Schedule::STATUS_PENDING, +- 'job_code=?' => $jobCode, +- ]); ++ $jobsToCleanup[] = $jobCode; + } + } ++ ++ if (count($jobsToCleanup) > 0) { ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ $count = $scheduleResource->getConnection()->delete( ++ $scheduleResource->getMainTable(), ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code in (?)' => $jobsToCleanup, ++ ] ++ ); ++ ++ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); ++ } + } + + /** +@@ -593,12 +608,12 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function cleanupScheduleMismatches() + { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status=?' => Schedule::STATUS_PENDING, +- 'job_code=?' => $jobCode, ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, + 'scheduled_at in (?)' => $scheduledAtList, + ]); + } +@@ -615,4 +630,87 @@ class ProcessCronQueueObserver implements ObserverInterface + } + return $this->jobs; + } ++ ++ /** ++ * Get CronGroup Configuration Value ++ * ++ * @param $groupId ++ * @return int ++ */ ++ private function getCronGroupConfigurationValue($groupId, $path) ++ { ++ return $this->_scopeConfig->getValue( ++ 'system/cron/' . $groupId . '/' . $path, ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ return $scheduleLifetime; ++ } ++ ++ /** ++ * Is Group In Filter ++ * ++ * @param $groupId ++ * @return bool ++ */ ++ private function isGroupInFilter($groupId): bool ++ { ++ return !($this->_request->getParam('group') !== null ++ && trim($this->_request->getParam('group'), "'") !== $groupId); ++ } ++ ++ /** ++ * Process pending jobs ++ * ++ * @param $groupId ++ * @param $jobsRoot ++ * @param $currentTime ++ */ ++ private function processPendingJobs($groupId, $jobsRoot, $currentTime) ++ { ++ $procesedJobs = []; ++ $pendingJobs = $this->getPendingSchedules($groupId); ++ /** @var \Magento\Cron\Model\Schedule $schedule */ ++ foreach ($pendingJobs as $schedule) { ++ if (isset($procesedJobs[$schedule->getJobCode()])) { ++ // process only on job per run ++ continue; ++ } ++ $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; ++ if (!$jobConfig) { ++ continue; ++ } ++ ++ $scheduledTime = strtotime($schedule->getScheduledAt()); ++ if ($scheduledTime > $currentTime) { ++ continue; ++ } ++ ++ try { ++ if ($schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ } ++ } catch (\Exception $e) { ++ $schedule->setMessages($e->getMessage()); ++ if ($schedule->getStatus() === Schedule::STATUS_ERROR) { ++ $this->logger->critical($e); ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_MISSED ++ && $this->state->getMode() === State::MODE_DEVELOPER ++ ) { ++ $this->logger->info( ++ sprintf( ++ "%s Schedule Id: %s Job Code: %s", ++ $schedule->getMessages(), ++ $schedule->getScheduleId(), ++ $schedule->getJobCode() ++ ) ++ ); ++ } ++ } ++ if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { ++ $procesedJobs[$schedule->getJobCode()] = true; ++ } ++ $schedule->save(); ++ } ++ } + } diff --git a/patches/MAGECLOUD-1736__respect_minification_override__2.1.4.patch b/patches/MAGECLOUD-1736__respect_minification_override__2.1.4.patch new file mode 100644 index 00000000..3d43d061 --- /dev/null +++ b/patches/MAGECLOUD-1736__respect_minification_override__2.1.4.patch @@ -0,0 +1,23 @@ +diff --git a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +index 82cb8ac51..7997d7cb6 100644 +--- a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php ++++ b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +@@ -107,11 +107,15 @@ class TemplateFile extends File + */ + private function getMinifiedTemplateInProduction($template) + { +- if ($this->deploymentConfig->getConfigData( +- ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION +- )) { ++ $forceMinified = $this->deploymentConfig->getConfigData( ++ ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION ++ ) ++ || $this->deploymentConfig->getConfigData('force_html_minification'); ++ ++ if ($forceMinified) { + return $this->templateMinifier->getMinified($template); + } ++ + return $this->templateMinifier->getPathToMinified($template); + } + } diff --git a/patches/MAGECLOUD-1736__respect_minification_override__2.2.0.patch b/patches/MAGECLOUD-1736__respect_minification_override__2.2.0.patch new file mode 100644 index 00000000..ee97f396 --- /dev/null +++ b/patches/MAGECLOUD-1736__respect_minification_override__2.2.0.patch @@ -0,0 +1,20 @@ +diff --git a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +index 09f87d878..5ef71afcc 100644 +--- a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php ++++ b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +@@ -107,9 +107,12 @@ class TemplateFile extends File + */ + private function getMinifiedTemplateInProduction($template) + { +- if ($this->deploymentConfig->getConfigData( +- ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION +- )) { ++ $forceMinified = $this->deploymentConfig->getConfigData( ++ ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION ++ ) ++ || $this->deploymentConfig->getConfigData('force_html_minification'); ++ ++ if ($forceMinified) { + return $this->templateMinifier->getMinified($template); + } + return $this->templateMinifier->getPathToMinified($template); diff --git a/patches/MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.11.patch b/patches/MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.11.patch new file mode 100644 index 00000000..3b584641 --- /dev/null +++ b/patches/MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.11.patch @@ -0,0 +1,161 @@ +diff -Naur a/vendor/magento/module-config/Model/Config/Reader/Source/Deployed/DocumentRoot.php b/vendor/magento/module-config/Model/Config/Reader/Source/Deployed/DocumentRoot.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-config/Model/Config/Reader/Source/Deployed/DocumentRoot.php +@@ -0,0 +1,55 @@ ++config = $config; ++ } ++ ++ /** ++ * A shortcut to load the document root path from the DirectoryList based on the ++ * deployment configuration. ++ * ++ * @return string ++ */ ++ public function getPath() ++ { ++ return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; ++ } ++ ++ /** ++ * Returns whether the deployment configuration specifies that the document root is ++ * in the pub/ folder. This affects ares such as sitemaps and robots.txt (and will ++ * likely be extended to control other areas). ++ * ++ * @return bool ++ */ ++ public function isPub() ++ { ++ return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); ++ } ++} +diff -Naur a/vendor/magento/module-sitemap/Block/Adminhtml/Grid/Renderer/Link.php b/vendor/magento/module-sitemap/Block/Adminhtml/Grid/Renderer/Link.php +--- a/vendor/magento/module-sitemap/Block/Adminhtml/Grid/Renderer/Link.php ++++ b/vendor/magento/module-sitemap/Block/Adminhtml/Grid/Renderer/Link.php +@@ -11,6 +11,8 @@ + namespace Magento\Sitemap\Block\Adminhtml\Grid\Renderer; + + use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; ++use Magento\Framework\App\ObjectManager; + + class Link extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer + { +@@ -25,19 +27,28 @@ class Link extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRe + protected $_sitemapFactory; + + /** ++ * @var DocumentRoot ++ */ ++ protected $documentRoot; ++ ++ /** + * @param \Magento\Backend\Block\Context $context + * @param \Magento\Sitemap\Model\SitemapFactory $sitemapFactory + * @param \Magento\Framework\Filesystem $filesystem + * @param array $data ++ * @param DocumentRoot $documentRoot + */ + public function __construct( + \Magento\Backend\Block\Context $context, + \Magento\Sitemap\Model\SitemapFactory $sitemapFactory, + \Magento\Framework\Filesystem $filesystem, +- array $data = [] ++ array $data = [], ++ DocumentRoot $documentRoot = null + ) { + $this->_sitemapFactory = $sitemapFactory; + $this->_filesystem = $filesystem; ++ $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); ++ + parent::__construct($context, $data); + } + +@@ -54,7 +65,8 @@ class Link extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRe + $url = $this->escapeHtml($sitemap->getSitemapUrl($row->getSitemapPath(), $row->getSitemapFilename())); + + $fileName = preg_replace('/^\//', '', $row->getSitemapPath() . $row->getSitemapFilename()); +- $directory = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT); ++ $documentRootPath = $this->documentRoot->getPath(); ++ $directory = $this->_filesystem->getDirectoryRead($documentRootPath); + if ($directory->isFile($fileName)) { + return sprintf('%1$s', $url); + } +diff -Naur a/vendor/magento/module-sitemap/Model/Sitemap.php b/vendor/magento/module-sitemap/Model/Sitemap.php +--- a/vendor/magento/module-sitemap/Model/Sitemap.php ++++ b/vendor/magento/module-sitemap/Model/Sitemap.php +@@ -8,7 +8,8 @@ + + namespace Magento\Sitemap\Model; + +-use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; ++use Magento\Framework\App\ObjectManager; + use Magento\Robots\Model\Config\Value; + + /** +@@ -170,6 +171,7 @@ + * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource + * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param array $data ++ * @param DocumentRoot|null $documentRoot + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -187,11 +189,13 @@ + \Magento\Framework\Stdlib\DateTime $dateTime, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, +- array $data = [] ++ array $data = [], ++ \Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot $documentRoot = null + ) { + $this->_escaper = $escaper; + $this->_sitemapData = $sitemapData; +- $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); ++ $documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); ++ $this->_directory = $filesystem->getDirectoryWrite($documentRoot->getPath()); + $this->_categoryFactory = $categoryFactory; + $this->_productFactory = $productFactory; + $this->_cmsFactory = $cmsFactory; +diff -Naur a/vendor/magento/framework/Config/ConfigOptionsListConstants.php b/vendor/magento/framework/Config/ConfigOptionsListConstants.php +--- a/vendor/magento/framework/Config/ConfigOptionsListConstants.php ++++ b/vendor/magento/framework/Config/ConfigOptionsListConstants.php +@@ -30,6 +30,8 @@ class ConfigOptionsListConstants + const CONFIG_PATH_DB = 'db'; + const CONFIG_PATH_RESOURCE = 'resource'; + const CONFIG_PATH_CACHE_TYPES = 'cache_types'; ++ const CONFIG_PATH_DOCUMENT_ROOT_IS_PUB = 'directories/document_root_is_pub'; ++ + /**#@-*/ + + /**#@+ diff --git a/patches/MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.4.patch b/patches/MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.4.patch new file mode 100644 index 00000000..06bc09e6 --- /dev/null +++ b/patches/MAGECLOUD-1998__unify_sitemapxml_and_robotstxt_generation__2.1.4.patch @@ -0,0 +1,165 @@ +diff -Naur a/vendor/magento/module-config/Model/Config/Backend/Admin/Robots.php b/vendor/magento/module-config/Model/Config/Backend/Admin/Robots.php +--- a/vendor/magento/module-config/Model/Config/Backend/Admin/Robots.php ++++ b/vendor/magento/module-config/Model/Config/Backend/Admin/Robots.php +@@ -44,7 +44,7 @@ + array $data = [] + ) { + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); +- $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); ++ $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->_file = 'robots.txt'; + } + +diff -Naur a/vendor/magento/module-config/Model/Config/Reader/Source/Deployed/DocumentRoot.php b/vendor/magento/module-config/Model/Config/Reader/Source/Deployed/DocumentRoot.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-config/Model/Config/Reader/Source/Deployed/DocumentRoot.php +@@ -0,0 +1,55 @@ ++config = $config; ++ } ++ ++ /** ++ * A shortcut to load the document root path from the DirectoryList based on the ++ * deployment configuration. ++ * ++ * @return string ++ */ ++ public function getPath() ++ { ++ return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; ++ } ++ ++ /** ++ * Returns whether the deployment configuration specifies that the document root is ++ * in the pub/ folder. This affects ares such as sitemaps and robots.txt (and will ++ * likely be extended to control other areas). ++ * ++ * @return bool ++ */ ++ public function isPub() ++ { ++ return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); ++ } ++} +diff -Naur a/vendor/magento/module-sitemap/Block/Adminhtml/Grid/Renderer/Link.php b/vendor/magento/module-sitemap/Block/Adminhtml/Grid/Renderer/Link.php +--- a/vendor/magento/module-sitemap/Block/Adminhtml/Grid/Renderer/Link.php ++++ b/vendor/magento/module-sitemap/Block/Adminhtml/Grid/Renderer/Link.php +@@ -11,6 +11,8 @@ + namespace Magento\Sitemap\Block\Adminhtml\Grid\Renderer; + + use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; ++use Magento\Framework\App\ObjectManager; + + class Link extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer + { +@@ -25,19 +27,28 @@ class Link extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRe + protected $_sitemapFactory; + + /** ++ * @var DocumentRoot ++ */ ++ protected $documentRoot; ++ ++ /** + * @param \Magento\Backend\Block\Context $context + * @param \Magento\Sitemap\Model\SitemapFactory $sitemapFactory + * @param \Magento\Framework\Filesystem $filesystem + * @param array $data ++ * @param DocumentRoot $documentRoot + */ + public function __construct( + \Magento\Backend\Block\Context $context, + \Magento\Sitemap\Model\SitemapFactory $sitemapFactory, + \Magento\Framework\Filesystem $filesystem, +- array $data = [] ++ array $data = [], ++ DocumentRoot $documentRoot = null + ) { + $this->_sitemapFactory = $sitemapFactory; + $this->_filesystem = $filesystem; ++ $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); ++ + parent::__construct($context, $data); + } + +@@ -54,7 +65,8 @@ class Link extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRe + $url = $this->escapeHtml($sitemap->getSitemapUrl($row->getSitemapPath(), $row->getSitemapFilename())); + + $fileName = preg_replace('/^\//', '', $row->getSitemapPath() . $row->getSitemapFilename()); +- $directory = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT); ++ $documentRootPath = $this->documentRoot->getPath(); ++ $directory = $this->_filesystem->getDirectoryRead($documentRootPath); + if ($directory->isFile($fileName)) { + return sprintf('%1$s', $url); + } +diff -Naur a/vendor/magento/module-sitemap/Model/Sitemap.php b/vendor/magento/module-sitemap/Model/Sitemap.php +--- a/vendor/magento/module-sitemap/Model/Sitemap.php ++++ b/vendor/magento/module-sitemap/Model/Sitemap.php +@@ -8,7 +8,8 @@ + + namespace Magento\Sitemap\Model; + +-use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; ++use Magento\Framework\App\ObjectManager; + + /** + * Sitemap model +@@ -179,11 +180,13 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel + \Magento\Framework\Stdlib\DateTime $dateTime, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, +- array $data = [] ++ array $data = [], ++ DocumentRoot $documentRoot = null + ) { + $this->_escaper = $escaper; + $this->_sitemapData = $sitemapData; +- $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); ++ $documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); ++ $this->_directory = $filesystem->getDirectoryWrite($documentRoot->getPath()); + $this->_categoryFactory = $categoryFactory; + $this->_productFactory = $productFactory; + $this->_cmsFactory = $cmsFactory; +diff -Naur a/vendor/magento/framework/Config/ConfigOptionsListConstants.php b/vendor/magento/framework/Config/ConfigOptionsListConstants.php +--- a/vendor/magento/framework/Config/ConfigOptionsListConstants.php ++++ b/vendor/magento/framework/Config/ConfigOptionsListConstants.php +@@ -30,6 +30,8 @@ class ConfigOptionsListConstants + const CONFIG_PATH_DB = 'db'; + const CONFIG_PATH_RESOURCE = 'resource'; + const CONFIG_PATH_CACHE_TYPES = 'cache_types'; ++ const CONFIG_PATH_DOCUMENT_ROOT_IS_PUB = 'directories/document_root_is_pub'; ++ + /**#@-*/ + + /**#@+ diff --git a/patches/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch b/patches/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch new file mode 100644 index 00000000..43027043 --- /dev/null +++ b/patches/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch @@ -0,0 +1,13 @@ +diff --git a/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php b/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php +index 673d65ec1b2..aa198e265b0 100644 +--- a/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php ++++ b/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php +@@ -181,7 +181,7 @@ class AbstractBackupDumpCommand extends AbstractBackupCommand + : $this->getParam(ConfigOptionsListConstants::KEY_HOST); + + $this->dbConnectionParams = sprintf( +- '-u%s -h%s %s %s %s', ++ '-u%s -h%s %s %s %s %s', + $this->getParam(ConfigOptionsListConstants::KEY_USER), + $host, + $port, diff --git a/patches/MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch b/patches/MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch new file mode 100644 index 00000000..7d62c396 --- /dev/null +++ b/patches/MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch @@ -0,0 +1,345 @@ +diff -Nuar a/vendor/magento/module-backend/etc/adminhtml/di.xml b/vendor/magento/module-backend/etc/adminhtml/di.xml +--- a/vendor/magento/module-backend/etc/adminhtml/di.xml ++++ b/vendor/magento/module-backend/etc/adminhtml/di.xml +@@ -139,10 +139,16 @@ + + + +- ++ + + + Magento\Config\Model\Config\Structure\ElementVisibilityInterface::HIDDEN ++ ++ ++ ++ ++ ++ + Magento\Config\Model\Config\Structure\ElementVisibilityInterface::DISABLED + + + +diff -Nuar a/vendor/magento/module-config/etc/adminhtml/di.xml b/vendor/magento/module-config/etc/adminhtml/di.xml +--- a/vendor/magento/module-config/etc/adminhtml/di.xml ++++ b/vendor/magento/module-config/etc/adminhtml/di.xml +@@ -15,6 +15,8 @@ + + + Magento\Config\Model\Config\Structure\ConcealInProductionConfigList ++ Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction ++ Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProductionWithoutScdOnDemand + + + + +diff -Nuar a/vendor/magento/framework/Locale/Deployed/Options.php b/vendor/magento/framework/Locale/Deployed/Options.php +--- a/vendor/magento/framework/Locale/Deployed/Options.php ++++ b/vendor/magento/framework/Locale/Deployed/Options.php +@@ -3,9 +3,14 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++declare(strict_types=1); ++ + namespace Magento\Framework\Locale\Deployed; + ++use Magento\Framework\App\DeploymentConfig; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\App\State; ++use Magento\Framework\Config\ConfigOptionsListConstants as Constants; + use Magento\Framework\Exception\LocalizedException; + use Magento\Framework\Locale\AvailableLocalesInterface; + use Magento\Framework\Locale\ListsInterface; +@@ -45,28 +50,36 @@ class Options implements OptionInterface + */ + private $localeLists; + ++ /** ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ + /** + * @param ListsInterface $localeLists locales list + * @param State $state application state class + * @param AvailableLocalesInterface $availableLocales operates with available locales + * @param DesignInterface $design operates with magento design settings ++ * @param DeploymentConfig $deploymentConfig + */ + public function __construct( + ListsInterface $localeLists, + State $state, + AvailableLocalesInterface $availableLocales, +- DesignInterface $design ++ DesignInterface $design, ++ DeploymentConfig $deploymentConfig = null + ) { + $this->localeLists = $localeLists; + $this->state = $state; + $this->availableLocales = $availableLocales; + $this->design = $design; ++ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); + } + + /** + * {@inheritdoc} + */ +- public function getOptionLocales() ++ public function getOptionLocales(): array + { + return $this->filterLocales($this->localeLists->getOptionLocales()); + } +@@ -74,7 +87,7 @@ class Options implements OptionInterface + /** + * {@inheritdoc} + */ +- public function getTranslatedOptionLocales() ++ public function getTranslatedOptionLocales(): array + { + return $this->filterLocales($this->localeLists->getTranslatedOptionLocales()); + } +@@ -82,7 +95,7 @@ class Options implements OptionInterface + /** + * Filter list of locales by available locales for current theme and depends on running application mode. + * +- * Applies filters only in production mode. ++ * Applies filters only in production mode when flag 'static_content_on_demand_in_production' is not enabled. + * For example, if the current design theme has only one generated locale en_GB then for given array of locales: + * ```php + * $locales = [ +@@ -113,9 +126,10 @@ class Options implements OptionInterface + * @param array $locales list of locales for filtering + * @return array of filtered locales + */ +- private function filterLocales(array $locales) ++ private function filterLocales(array $locales): array + { +- if ($this->state->getMode() != State::MODE_PRODUCTION) { ++ if ($this->state->getMode() != State::MODE_PRODUCTION ++ || $this->deploymentConfig->getConfigData(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION)) { + return $locales; + } + + +diff -Nuar a/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php b/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php +@@ -0,0 +1,138 @@ ++ Settings > Configuration page ++ * in Admin Panel in Production mode. ++ * @api ++ */ ++class ConcealInProduction implements ElementVisibilityInterface ++{ ++ /** ++ * The list of form element paths with concrete visibility status. ++ * ++ * E.g. ++ * ++ * ```php ++ * [ ++ * 'general/locale/code' => ElementVisibilityInterface::DISABLED, ++ * 'general/country' => ElementVisibilityInterface::HIDDEN, ++ * ]; ++ * ``` ++ * ++ * It means that: ++ * - field Locale (in group Locale Options in section General) will be disabled ++ * - group Country Options (in section General) will be hidden ++ * ++ * @var array ++ */ ++ private $configs = []; ++ ++ /** ++ * The object that has information about the state of the system. ++ * ++ * @var State ++ */ ++ private $state; ++ ++ /** ++ * ++ * The list of form element paths which ignore visibility status. ++ * ++ * E.g. ++ * ++ * ```php ++ * [ ++ * 'general/country/default' => '', ++ * ]; ++ * ``` ++ * ++ * It means that: ++ * - field 'default' in group Country Options (in section General) will be showed, even if all group(section) ++ * will be hidden. ++ * ++ * @var array ++ */ ++ private $exemptions = []; ++ ++ /** ++ * @param State $state The object that has information about the state of the system ++ * @param array $configs The list of form element paths with concrete visibility status. ++ * @param array $exemptions The list of form element paths which ignore visibility status. ++ */ ++ public function __construct(State $state, array $configs = [], array $exemptions = []) ++ { ++ $this->state = $state; ++ $this->configs = $configs; ++ $this->exemptions = $exemptions; ++ } ++ ++ /** ++ * @inheritdoc ++ * @since 100.2.0 ++ */ ++ public function isHidden($path) ++ { ++ $path = $this->normalizePath($path); ++ if ($this->state->getMode() === State::MODE_PRODUCTION ++ && preg_match('/(?(?
.*?)\/.*?)\/.*?/', $path, $match)) { ++ $group = $match['group']; ++ $section = $match['section']; ++ $exemptions = array_keys($this->exemptions); ++ $checkedItems = []; ++ foreach ([$path, $group, $section] as $itemPath) { ++ $checkedItems[] = $itemPath; ++ if (!empty($this->configs[$itemPath])) { ++ return $this->configs[$itemPath] === static::HIDDEN ++ && empty(array_intersect($checkedItems, $exemptions)); ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ /** ++ * @inheritdoc ++ * @since 100.2.0 ++ */ ++ public function isDisabled($path) ++ { ++ $path = $this->normalizePath($path); ++ if ($this->state->getMode() === State::MODE_PRODUCTION) { ++ while (true) { ++ if (!empty($this->configs[$path])) { ++ return $this->configs[$path] === static::DISABLED; ++ } ++ ++ $position = strripos($path, '/'); ++ if ($position === false) { ++ break; ++ } ++ $path = substr($path, 0, $position); ++ } ++ } ++ ++ return false; ++ } ++ ++ /** ++ * Returns normalized path. ++ * ++ * @param string $path The path to be normalized ++ * @return string The normalized path ++ */ ++ private function normalizePath($path) ++ { ++ return trim($path, '/'); ++ } ++} + +diff --git a/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php b/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php +@@ -0,0 +1,72 @@ ++ Settings > Configuration page ++ * when Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION is enabled ++ * otherwise rule from Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction is used ++ * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction ++ * ++ * @api ++ */ ++class ConcealInProductionWithoutScdOnDemand implements ElementVisibilityInterface ++{ ++ /** ++ * @var ConcealInProduction Element visibility rules in the Production mode ++ */ ++ private $concealInProduction; ++ ++ /** ++ * @var DeploymentConfig The application deployment configuration ++ */ ++ private $deploymentConfig; ++ ++ /** ++ * @param ConcealInProductionFactory $concealInProductionFactory ++ * @param DeploymentConfig $deploymentConfig Deployment configuration reader ++ * @param array $configs The list of form element paths with concrete visibility status. ++ * @param array $exemptions The list of form element paths which ignore visibility status. ++ */ ++ public function __construct( ++ ConcealInProductionFactory $concealInProductionFactory, ++ DeploymentConfig $deploymentConfig, ++ array $configs = [], ++ array $exemptions = [] ++ ) { ++ $this->concealInProduction = $concealInProductionFactory ++ ->create(['configs' => $configs, 'exemptions' => $exemptions]); ++ $this->deploymentConfig = $deploymentConfig; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function isHidden($path): bool ++ { ++ if (!$this->deploymentConfig->getConfigData(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION)) { ++ return $this->concealInProduction->isHidden($path); ++ } ++ return false; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function isDisabled($path): bool ++ { ++ if (!$this->deploymentConfig->getConfigData(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION)) { ++ return $this->concealInProduction->isDisabled($path); ++ } ++ return false; ++ } ++} diff --git a/patches/MAGECLOUD-2173__the_recursion_error_during_deployment__2.2.0.patch b/patches/MAGECLOUD-2173__the_recursion_error_during_deployment__2.2.0.patch new file mode 100644 index 00000000..775982fe --- /dev/null +++ b/patches/MAGECLOUD-2173__the_recursion_error_during_deployment__2.2.0.patch @@ -0,0 +1,16 @@ +diff -Nuar a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php +--- a/vendor/magento/module-config/App/Config/Type/System.php ++++ b/vendor/magento/module-config/App/Config/Type/System.php +@@ -155,7 +155,11 @@ class System implements ConfigTypeInterface + } + $scopeId = array_shift($pathParts); + if (!isset($this->data[$scopeType][$scopeId])) { +- $this->data = array_replace_recursive($this->loadScopeData($scopeType, $scopeId), $this->data); ++ $scopeData = $this->loadScopeData($scopeType, $scopeId); ++ /* Starting from 2.2.0 $this->data can be already loaded with $this->loadScopeData */ ++ if (!isset($this->data[$scopeType][$scopeId])) { ++ $this->data = array_replace_recursive($scopeData, $this->data); ++ } + } + return isset($this->data[$scopeType][$scopeId]) + ? $this->getDataByPathParts($this->data[$scopeType][$scopeId], $pathParts) diff --git a/patches/MAGECLOUD-2209__write_logs_for_failed_process_of_generating_factories_in_extensions__2.2.0.patch b/patches/MAGECLOUD-2209__write_logs_for_failed_process_of_generating_factories_in_extensions__2.2.0.patch new file mode 100644 index 00000000..9228a647 --- /dev/null +++ b/patches/MAGECLOUD-2209__write_logs_for_failed_process_of_generating_factories_in_extensions__2.2.0.patch @@ -0,0 +1,159 @@ +diff -Naur a/vendor/magento/framework/Code/Generator.php b/vendor/magento/framework/Code/Generator.php + +--- a/vendor/magento/framework/Code/Generator.php ++++ b/vendor/magento/framework/Code/Generator.php +@@ -7,6 +7,11 @@ namespace Magento\Framework\Code; + + use Magento\Framework\Code\Generator\DefinedClasses; + use Magento\Framework\Code\Generator\EntityAbstract; ++use Magento\Framework\Code\Generator\Io; ++use Magento\Framework\ObjectManagerInterface; ++use Magento\Framework\Phrase; ++use Magento\Framework\Filesystem\Driver\File; ++use Psr\Log\LoggerInterface; + + class Generator + { +@@ -17,7 +22,7 @@ class Generator + const GENERATION_SKIP = 'skip'; + + /** +- * @var \Magento\Framework\Code\Generator\Io ++ * @var Io + */ + protected $_ioObject; + +@@ -32,26 +37,33 @@ class Generator + protected $definedClasses; + + /** +- * @var \Magento\Framework\ObjectManagerInterface ++ * @var ObjectManagerInterface + */ + protected $objectManager; + + /** +- * @param Generator\Io $ioObject +- * @param array $generatedEntities ++ * Logger instance ++ * ++ * @var LoggerInterface ++ */ ++ private $logger; ++ ++ /** ++ * @param Generator\Io $ioObject ++ * @param array $generatedEntities + * @param DefinedClasses $definedClasses ++ * @param LoggerInterface $logger + */ + public function __construct( +- \Magento\Framework\Code\Generator\Io $ioObject = null, ++ Io $ioObject = null, + array $generatedEntities = [], +- DefinedClasses $definedClasses = null ++ DefinedClasses $definedClasses = null, ++ LoggerInterface $logger = null + ) { +- $this->_ioObject = $ioObject +- ?: new \Magento\Framework\Code\Generator\Io( +- new \Magento\Framework\Filesystem\Driver\File() +- ); ++ $this->_ioObject = $ioObject ?: new Io(new File()); + $this->definedClasses = $definedClasses ?: new DefinedClasses(); + $this->_generatedEntities = $generatedEntities; ++ $this->logger = $logger; + } + + /** +@@ -111,8 +123,16 @@ class Generator + if ($generator !== null) { + $this->tryToLoadSourceClass($className, $generator); + if (!($file = $generator->generate())) { ++ /** @var $logger LoggerInterface */ + $errors = $generator->getErrors(); +- throw new \RuntimeException(implode(' ', $errors) . ' in [' . $className . ']'); ++ $errors[] = 'Class ' . $className . ' generation error: The requested class did not generate properly, ' ++ . 'because the \'generated\' directory permission is read-only. ' ++ . 'If --- after running the \'bin/magento setup:di:compile\' CLI command when the \'generated\' ' ++ . 'directory permission is set to write --- the requested class did not generate properly, then ' ++ . 'you must add the generated class object to the signature of the related construct method, only.'; ++ $message = implode(PHP_EOL, $errors); ++ $this->getLogger()->critical($message); ++ throw new \RuntimeException($message); + } + if (!$this->definedClasses->isClassLoadableFromMemory($className)) { + $this->_ioObject->includeFile($file); +@@ -121,13 +141,26 @@ class Generator + } + } + ++ /** ++ * Retrieve logger ++ * ++ * @return LoggerInterface ++ */ ++ private function getLogger() ++ { ++ if (!$this->logger) { ++ $this->logger = $this->getObjectManager()->get(LoggerInterface::class); ++ } ++ return $this->logger; ++ } ++ + /** + * Create entity generator + * + * @param string $generatorClass + * @param string $entityName + * @param string $className +- * @return \Magento\Framework\Code\Generator\EntityAbstract ++ * @return EntityAbstract + */ + protected function createGeneratorInstance($generatorClass, $entityName, $className) + { +@@ -140,10 +173,10 @@ class Generator + /** + * Set object manager instance. + * +- * @param \Magento\Framework\ObjectManagerInterface $objectManager ++ * @param ObjectManagerInterface $objectManager + * @return $this + */ +- public function setObjectManager(\Magento\Framework\ObjectManagerInterface $objectManager) ++ public function setObjectManager(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + return $this; +@@ -152,11 +185,11 @@ class Generator + /** + * Get object manager instance. + * +- * @return \Magento\Framework\ObjectManagerInterface ++ * @return ObjectManagerInterface + */ + public function getObjectManager() + { +- if (!($this->objectManager instanceof \Magento\Framework\ObjectManagerInterface)) { ++ if (!($this->objectManager instanceof ObjectManagerInterface)) { + throw new \LogicException( + "Object manager was expected to be set using setObjectManger() " + . "before getObjectManager() invocation." +@@ -169,7 +202,7 @@ class Generator + * Try to load/generate source class to check if it is valid or not. + * + * @param string $className +- * @param \Magento\Framework\Code\Generator\EntityAbstract $generator ++ * @param EntityAbstract $generator + * @return void + * @throws \RuntimeException + */ +@@ -178,7 +211,7 @@ class Generator + $sourceClassName = $generator->getSourceClassName(); + if (!$this->definedClasses->isClassLoadable($sourceClassName)) { + if ($this->generateClass($sourceClassName) !== self::GENERATION_SUCCESS) { +- $phrase = new \Magento\Framework\Phrase( ++ $phrase = new Phrase( + 'Source class "%1" for "%2" generation does not exist.', + [$sourceClassName, $className] + ); diff --git a/patches/MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch b/patches/MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch new file mode 100644 index 00000000..b2899201 --- /dev/null +++ b/patches/MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch @@ -0,0 +1,28 @@ +diff -Nuar a/vendor/magento/module-cron/etc/cron_groups.xml b/vendor/magento/module-cron/etc/cron_groups.xml +--- a/vendor/magento/module-cron/etc/cron_groups.xml ++++ b/vendor/magento/module-cron/etc/cron_groups.xml +@@ -11,8 +11,8 @@ + 20 + 15 + 10 +- 10080 +- 10080 ++ 60 ++ 4320 + 0 + + +diff -Nuar a/vendor/magento/module-indexer/etc/cron_groups.xml b/vendor/magento/module-indexer/etc/cron_groups.xml +--- a/vendor/magento/module-indexer/etc/cron_groups.xml ++++ b/vendor/magento/module-indexer/etc/cron_groups.xml +@@ -11,8 +11,8 @@ + 4 + 2 + 10 +- 10080 +- 10080 ++ 60 ++ 4320 + 1 + + diff --git a/patches/MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch b/patches/MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch new file mode 100644 index 00000000..89817a0f --- /dev/null +++ b/patches/MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch @@ -0,0 +1,62 @@ +diff -Nuar a/vendor/magento/module-cron/Console/Command/CronCommand.php b/vendor/magento/module-cron/Console/Command/CronCommand.php +index 6a9686c514e..4df6888f461 100644 +--- a/vendor/magento/module-cron/Console/Command/CronCommand.php ++++ b/vendor/magento/module-cron/Console/Command/CronCommand.php +@@ -9,10 +9,12 @@ use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + use Symfony\Component\Console\Input\InputOption; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\App\ObjectManagerFactory; + use Magento\Store\Model\Store; + use Magento\Store\Model\StoreManager; + use Magento\Cron\Observer\ProcessCronQueueObserver; ++use Magento\Framework\App\DeploymentConfig; + use Magento\Framework\Console\Cli; + use Magento\Framework\Shell\ComplexParameter; + +@@ -34,13 +36,24 @@ class CronCommand extends Command + private $objectManagerFactory; + + /** +- * Constructor ++ * Application deployment configuration + * ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ ++ /** + * @param ObjectManagerFactory $objectManagerFactory ++ * @param DeploymentConfig $deploymentConfig Application deployment configuration + */ +- public function __construct(ObjectManagerFactory $objectManagerFactory) +- { ++ public function __construct( ++ ObjectManagerFactory $objectManagerFactory, ++ DeploymentConfig $deploymentConfig = null ++ ) { + $this->objectManagerFactory = $objectManagerFactory; ++ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get( ++ DeploymentConfig::class ++ ); + parent::__construct(); + } + +@@ -70,10 +83,16 @@ class CronCommand extends Command + } + + /** ++ * Runs cron jobs if cron is not disabled in Magento configurations ++ * + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { ++ if (!$this->deploymentConfig->get('cron/enabled', 1)) { ++ $output->writeln('' . 'Cron is disabled. Jobs were not run.' . ''); ++ return; ++ } + $omParams = $_SERVER; + $omParams[StoreManager::PARAM_RUN_CODE] = 'admin'; + $omParams[Store::CUSTOM_ENTRY_POINT_PARAM] = true; diff --git a/patches/MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.0.patch b/patches/MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.0.patch new file mode 100644 index 00000000..49319093 --- /dev/null +++ b/patches/MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.0.patch @@ -0,0 +1,39 @@ +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php ++++ b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +@@ -119,7 +119,7 @@ class PidConsumerManager + */ + public function getPid($consumerName) + { +- $pidFile = $consumerName . static::PID_FILE_EXT; ++ $pidFile = $this->getPidFileName($consumerName); + /** @var WriteInterface $directory */ + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + +@@ -138,7 +138,7 @@ class PidConsumerManager + */ + public function getPidFilePath($consumerName) + { +- return $this->directoryList->getPath(DirectoryList::VAR_DIR) . '/' . $consumerName . static::PID_FILE_EXT; ++ return $this->directoryList->getPath(DirectoryList::VAR_DIR) . '/' . $this->getPidFileName($consumerName); + } + + /** +@@ -152,4 +152,17 @@ class PidConsumerManager + $file->write(function_exists('posix_getpid') ? posix_getpid() : getmypid()); + $file->close(); + } ++ ++ /** ++ * Returns default file name with PID by consumers name ++ * ++ * @param string $consumerName The consumers name ++ * @return string The file name with PID ++ */ ++ private function getPidFileName($consumerName) ++ { ++ $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); ++ ++ return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; ++ } + } diff --git a/patches/MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.4.patch b/patches/MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.4.patch new file mode 100644 index 00000000..67d350f9 --- /dev/null +++ b/patches/MAGECLOUD-2464__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.4.patch @@ -0,0 +1,13 @@ +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php ++++ b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +@@ -139,6 +139,8 @@ class ConsumersRunner + */ + private function getPidFilePath($consumerName) + { +- return $consumerName . static::PID_FILE_EXT; ++ $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); ++ ++ return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; + } + } diff --git a/patches/MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.0.patch b/patches/MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.0.patch new file mode 100644 index 00000000..54c6aefb --- /dev/null +++ b/patches/MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.0.patch @@ -0,0 +1,61 @@ +diff -Naur a/vendor/magento/framework/Console/Cli.php b/vendor/magento/framework/Console/Cli.php +--- a/vendor/magento/framework/Console/Cli.php ++++ b/vendor/magento/framework/Console/Cli.php +@@ -9,7 +9,6 @@ use Magento\Framework\App\Bootstrap; + use Magento\Framework\App\DeploymentConfig; + use Magento\Framework\App\Filesystem\DirectoryList; + use Magento\Framework\App\ProductMetadata; +-use Magento\Framework\App\State; + use Magento\Framework\Composer\ComposerJsonFinder; + use Magento\Framework\Console\Exception\GenerationDirectoryAccessException; + use Magento\Framework\Filesystem\Driver\File; +@@ -19,7 +18,6 @@ use Magento\Setup\Application; + use Magento\Setup\Console\CompilerPreparation; + use Magento\Setup\Model\ObjectManagerProvider; + use Symfony\Component\Console; +-use Zend\ServiceManager\ServiceManager; + + /** + * Magento 2 CLI Application. +@@ -74,7 +72,6 @@ class Cli extends Console\Application + + $this->assertCompilerPreparation(); + $this->initObjectManager(); +- $this->assertGenerationPermissions(); + } catch (\Exception $exception) { + $output = new \Symfony\Component\Console\Output\ConsoleOutput(); + $output->writeln( +@@ -166,33 +163,6 @@ class Cli extends Console\Application + $omProvider->setObjectManager($this->objectManager); + } + +- /** +- * Checks whether generation directory is read-only. +- * Depends on the current mode: +- * production - application will proceed +- * default - application will be terminated +- * developer - application will be terminated +- * +- * @return void +- * @throws GenerationDirectoryAccessException If generation directory is read-only in developer mode +- */ +- private function assertGenerationPermissions() +- { +- /** @var GenerationDirectoryAccess $generationDirectoryAccess */ +- $generationDirectoryAccess = $this->objectManager->create( +- GenerationDirectoryAccess::class, +- ['serviceManager' => $this->serviceManager] +- ); +- /** @var State $state */ +- $state = $this->objectManager->get(State::class); +- +- if ($state->getMode() !== State::MODE_PRODUCTION +- && !$generationDirectoryAccess->check() +- ) { +- throw new GenerationDirectoryAccessException(); +- } +- } +- + /** + * Checks whether compiler is being prepared. + * diff --git a/patches/MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.6.patch b/patches/MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.6.patch new file mode 100644 index 00000000..8c176af2 --- /dev/null +++ b/patches/MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.6.patch @@ -0,0 +1,53 @@ +diff -Naur a/vendor/magento/framework/Console/Cli.php b/vendor/magento/framework/Console/Cli.php +--- a/vendor/magento/framework/Console/Cli.php ++++ b/vendor/magento/framework/Console/Cli.php +@@ -9,7 +9,6 @@ use Magento\Framework\App\Bootstrap; + use Magento\Framework\App\DeploymentConfig; + use Magento\Framework\App\Filesystem\DirectoryList; + use Magento\Framework\App\ProductMetadata; +-use Magento\Framework\App\State; + use Magento\Framework\Composer\ComposerJsonFinder; + use Magento\Framework\Console\Exception\GenerationDirectoryAccessException; + use Magento\Framework\Filesystem\Driver\File; +@@ -74,7 +73,6 @@ class Cli extends Console\Application + + $this->assertCompilerPreparation(); + $this->initObjectManager(); +- $this->assertGenerationPermissions(); + } catch (\Exception $exception) { + $output = new \Symfony\Component\Console\Output\ConsoleOutput(); + $output->writeln( +@@ -167,33 +165,6 @@ class Cli extends Console\Application + $omProvider->setObjectManager($this->objectManager); + } + +- /** +- * Checks whether generation directory is read-only. +- * Depends on the current mode: +- * production - application will proceed +- * default - application will be terminated +- * developer - application will be terminated +- * +- * @return void +- * @throws GenerationDirectoryAccessException If generation directory is read-only in developer mode +- */ +- private function assertGenerationPermissions() +- { +- /** @var GenerationDirectoryAccess $generationDirectoryAccess */ +- $generationDirectoryAccess = $this->objectManager->create( +- GenerationDirectoryAccess::class, +- ['serviceManager' => $this->serviceManager] +- ); +- /** @var State $state */ +- $state = $this->objectManager->get(State::class); +- +- if ($state->getMode() !== State::MODE_PRODUCTION +- && !$generationDirectoryAccess->check() +- ) { +- throw new GenerationDirectoryAccessException(); +- } +- } +- + /** + * Checks whether compiler is being prepared. + * diff --git a/patches/MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch b/patches/MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch new file mode 100644 index 00000000..a901d459 --- /dev/null +++ b/patches/MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch @@ -0,0 +1,58 @@ +diff -Nuar a/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Imap.php b/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Imap.php +--- a/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Imap.php ++++ b/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Imap.php +@@ -111,7 +111,8 @@ class Zend_Mail_Protocol_Imap + + if ($ssl === 'TLS') { + $result = $this->requestAndResponse('STARTTLS'); +- $result = $result && stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); ++ // TODO: Add STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT in the future when it is supported by PHP ++ $result = $result && stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); + if (!$result) { + /** + * @see Zend_Mail_Protocol_Exception + +diff -Nuar a/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Pop3.php b/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Pop3.php +--- a/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Pop3.php ++++ b/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Pop3.php +@@ -122,7 +122,8 @@ class Zend_Mail_Protocol_Pop3 + + if ($ssl === 'TLS') { + $this->request('STLS'); +- $result = stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); ++ // TODO: Add STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT in the future when it is supported by PHP ++ $result = stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); + if (!$result) { + /** + * @see Zend_Mail_Protocol_Exception + +diff -Nuar a/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Smtp.php b/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Smtp.php +--- a/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Smtp.php ++++ b/vendor/magento/zendframework1/library/Zend/Mail/Protocol/Smtp.php +@@ -203,7 +203,8 @@ class Zend_Mail_Protocol_Smtp extends Ze + if ($this->_secure == 'tls') { + $this->_send('STARTTLS'); + $this->_expect(220, 180); +- if (!stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { ++ // TODO: Add STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT in the future when it is supported by PHP ++ if (!stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + +diff -Nuar a/vendor/magento/zendframework1/library/Zend/Http/Client/Adapter/Proxy.php b/vendor/magento/zendframework1/library/Zend/Http/Client/Adapter/Proxy.php +--- a/vendor/magento/zendframework1/library/Zend/Http/Client/Adapter/Proxy.php ++++ b/vendor/magento/zendframework1/library/Zend/Http/Client/Adapter/Proxy.php +@@ -297,10 +297,8 @@ class Zend_Http_Client_Adapter_Proxy ext + // If all is good, switch socket to secure mode. We have to fall back + // through the different modes + $modes = array( +- STREAM_CRYPTO_METHOD_TLS_CLIENT, +- STREAM_CRYPTO_METHOD_SSLv3_CLIENT, +- STREAM_CRYPTO_METHOD_SSLv23_CLIENT, +- STREAM_CRYPTO_METHOD_SSLv2_CLIENT ++ // TODO: Add STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT in the future when it is supported by PHP ++ STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT + ); + + $success = false; diff --git a/patches/MAGECLOUD-2573__installation_without_admin_creation__2.1.4.patch b/patches/MAGECLOUD-2573__installation_without_admin_creation__2.1.4.patch new file mode 100644 index 00000000..abb12a79 --- /dev/null +++ b/patches/MAGECLOUD-2573__installation_without_admin_creation__2.1.4.patch @@ -0,0 +1,152 @@ +diff -Naur a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php +--- a/setup/src/Magento/Setup/Model/Installer.php ++++ b/setup/src/Magento/Setup/Model/Installer.php +@@ -316,7 +316,9 @@ class Installer + [$request[InstallCommand::INPUT_KEY_SALES_ORDER_INCREMENT_PREFIX]], + ]; + } +- $script[] = ['Installing admin user...', 'installAdminUser', [$request]]; ++ if ($this->isAdminDataSet($request)) { ++ $script[] = ['Installing admin user...', 'installAdminUser', [$request]]; ++ } + $script[] = ['Caches clearing:', 'cleanCaches', []]; + $script[] = ['Disabling Maintenance Mode:', 'setMaintenanceMode', [0]]; + $script[] = ['Post installation file permissions check...', 'checkApplicationFilePermissions', []]; +@@ -1318,4 +1320,27 @@ class Installer + $this->log->log($message); + } + } ++ ++ /** ++ * Checks that admin data is not empty in request array ++ * ++ * @param \ArrayObject|array $request ++ * @return bool ++ */ ++ private function isAdminDataSet($request) ++ { ++ $adminData = array_filter($request, function ($value, $key) { ++ return in_array( ++ $key, ++ [ ++ AdminAccount::KEY_EMAIL, ++ AdminAccount::KEY_FIRST_NAME, ++ AdminAccount::KEY_LAST_NAME, ++ AdminAccount::KEY_USER, ++ AdminAccount::KEY_PASSWORD, ++ ] ++ ) && $value !== null; ++ }, ARRAY_FILTER_USE_BOTH); ++ return !empty($adminData); ++ } + } + +diff -Naur a/setup/src/Magento/Setup/Console/Command/InstallCommand.php b/setup/src/Magento/Setup/Console/Command/InstallCommand.php +--- a/setup/src/Magento/Setup/Console/Command/InstallCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/InstallCommand.php +@@ -13,6 +13,7 @@ use Magento\Setup\Model\InstallerFactory; + use Magento\Framework\Setup\ConsoleLogger; + use Symfony\Component\Console\Input\InputOption; + use Magento\Setup\Model\ConfigModel; ++use Magento\Setup\Model\AdminAccount; + + /** + * Command to install Magento application +@@ -90,7 +91,7 @@ class InstallCommand extends AbstractSetupCommand + { + $inputOptions = $this->configModel->getAvailableOptions(); + $inputOptions = array_merge($inputOptions, $this->userConfig->getOptionsList()); +- $inputOptions = array_merge($inputOptions, $this->adminUser->getOptionsList()); ++ $inputOptions = array_merge($inputOptions, $this->adminUser->getOptionsList(InputOption::VALUE_OPTIONAL)); + $inputOptions = array_merge($inputOptions, [ + new InputOption( + self::INPUT_KEY_CLEANUP_DB, +@@ -146,7 +147,7 @@ class InstallCommand extends AbstractSetupCommand + } + } + $errors = $this->configModel->validate($configOptionsToValidate); +- $errors = array_merge($errors, $this->adminUser->validate($input)); ++ $errors = array_merge($errors, $this->validateAdmin($input)); + $errors = array_merge($errors, $this->validate($input)); + $errors = array_merge($errors, $this->userConfig->validate($input)); + +@@ -177,4 +178,23 @@ class InstallCommand extends AbstractSetupCommand + } + return $errors; + } ++ ++ /** ++ * Performs validation of admin options if at least one of them was set. ++ * ++ * @param InputInterface $input ++ * @return array ++ */ ++ private function validateAdmin(InputInterface $input): array ++ { ++ if ($input->getOption(AdminAccount::KEY_FIRST_NAME) ++ || $input->getOption(AdminAccount::KEY_LAST_NAME) ++ || $input->getOption(AdminAccount::KEY_EMAIL) ++ || $input->getOption(AdminAccount::KEY_USER) ++ || $input->getOption(AdminAccount::KEY_PASSWORD) ++ ) { ++ return $this->adminUser->validate($input); ++ } ++ return []; ++ } + } + +diff -Naur a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php +--- a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php +@@ -71,25 +71,43 @@ class AdminUserCreateCommand extends AbstractSetupCommand + /** + * Get list of arguments for the command + * ++ * @param int $mode The mode of options. + * @return InputOption[] + */ +- public function getOptionsList() ++ public function getOptionsList($mode = InputOption::VALUE_REQUIRED) + { ++ $requiredStr = ($mode === InputOption::VALUE_REQUIRED ? '(Required) ' : ''); ++ + return [ +- new InputOption(AdminAccount::KEY_USER, null, InputOption::VALUE_REQUIRED, '(Required) Admin user'), +- new InputOption(AdminAccount::KEY_PASSWORD, null, InputOption::VALUE_REQUIRED, '(Required) Admin password'), +- new InputOption(AdminAccount::KEY_EMAIL, null, InputOption::VALUE_REQUIRED, '(Required) Admin email'), ++ new InputOption( ++ AdminAccount::KEY_USER, ++ null, ++ $mode, ++ $requiredStr . 'Admin user' ++ ), ++ new InputOption( ++ AdminAccount::KEY_PASSWORD, ++ null, ++ $mode, ++ $requiredStr . 'Admin password' ++ ), ++ new InputOption( ++ AdminAccount::KEY_EMAIL, ++ null, ++ $mode, ++ $requiredStr . 'Admin email' ++ ), + new InputOption( + AdminAccount::KEY_FIRST_NAME, + null, +- InputOption::VALUE_REQUIRED, +- '(Required) Admin first name' ++ $mode, ++ $requiredStr . 'Admin first name' + ), + new InputOption( + AdminAccount::KEY_LAST_NAME, + null, +- InputOption::VALUE_REQUIRED, +- '(Required) Admin last name' ++ $mode, ++ $requiredStr . 'Admin last name' + ), + ]; + } diff --git a/patches/MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch b/patches/MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch new file mode 100644 index 00000000..e88214b7 --- /dev/null +++ b/patches/MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch @@ -0,0 +1,198 @@ +diff -Nuar a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php +--- a/setup/src/Magento/Setup/Model/Installer.php ++++ b/setup/src/Magento/Setup/Model/Installer.php +@@ -316,7 +316,9 @@ class Installer + [$request[InstallCommand::INPUT_KEY_SALES_ORDER_INCREMENT_PREFIX]], + ]; + } +- $script[] = ['Installing admin user...', 'installAdminUser', [$request]]; ++ if ($this->isAdminDataSet($request)) { ++ $script[] = ['Installing admin user...', 'installAdminUser', [$request]]; ++ } + $script[] = ['Caches clearing:', 'cleanCaches', []]; + $script[] = ['Disabling Maintenance Mode:', 'setMaintenanceMode', [0]]; + $script[] = ['Post installation file permissions check...', 'checkApplicationFilePermissions', []]; +@@ -1318,4 +1320,27 @@ class Installer + $this->log->log($message); + } + } ++ ++ /** ++ * Checks that admin data is not empty in request array ++ * ++ * @param \ArrayObject|array $request ++ * @return bool ++ */ ++ private function isAdminDataSet($request) ++ { ++ $adminData = array_filter($request, function ($value, $key) { ++ return in_array( ++ $key, ++ [ ++ AdminAccount::KEY_EMAIL, ++ AdminAccount::KEY_FIRST_NAME, ++ AdminAccount::KEY_LAST_NAME, ++ AdminAccount::KEY_USER, ++ AdminAccount::KEY_PASSWORD, ++ ] ++ ) && $value !== null; ++ }, ARRAY_FILTER_USE_BOTH); ++ return !empty($adminData); ++ } + } + +diff -Nuar a/setup/src/Magento/Setup/Console/Command/InstallCommand.php b/setup/src/Magento/Setup/Console/Command/InstallCommand.php +--- a/setup/src/Magento/Setup/Console/Command/InstallCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/InstallCommand.php +@@ -11,6 +11,7 @@ use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Setup\Model\InstallerFactory; + use Magento\Framework\Setup\ConsoleLogger; ++use Magento\Setup\Model\AdminAccount; + use Symfony\Component\Console\Input\InputOption; + use Magento\Setup\Model\ConfigModel; + use Symfony\Component\Console\Question\Question; +@@ -103,7 +104,7 @@ class InstallCommand extends AbstractSetupCommand + { + $inputOptions = $this->configModel->getAvailableOptions(); + $inputOptions = array_merge($inputOptions, $this->userConfig->getOptionsList()); +- $inputOptions = array_merge($inputOptions, $this->adminUser->getOptionsList()); ++ $inputOptions = array_merge($inputOptions, $this->adminUser->getOptionsList(InputOption::VALUE_OPTIONAL)); + $inputOptions = array_merge($inputOptions, [ + new InputOption( + self::INPUT_KEY_CLEANUP_DB, +@@ -178,7 +179,7 @@ class InstallCommand extends AbstractSetupCommand + } + + $errors = $this->configModel->validate($configOptionsToValidate); +- $errors = array_merge($errors, $this->adminUser->validate($input)); ++ $errors = array_merge($errors, $this->validateAdmin($input)); + $errors = array_merge($errors, $this->validate($input)); + $errors = array_merge($errors, $this->userConfig->validate($input)); + +@@ -247,7 +248,7 @@ class InstallCommand extends AbstractSetupCommand + + $output->writeln(""); + +- foreach ($this->adminUser->getOptionsList() as $option) { ++ foreach ($this->adminUser->getOptionsList(InputOption::VALUE_OPTIONAL) as $option) { + $configOptionsToValidate[$option->getName()] = $this->askQuestion( + $input, + $output, +@@ -338,4 +339,23 @@ class InstallCommand extends AbstractSetupCommand + + return $value; + } ++ ++ /** ++ * Performs validation of admin options if at least one of them was set. ++ * ++ * @param InputInterface $input ++ * @return array ++ */ ++ private function validateAdmin(InputInterface $input): array ++ { ++ if ($input->getOption(AdminAccount::KEY_FIRST_NAME) ++ || $input->getOption(AdminAccount::KEY_LAST_NAME) ++ || $input->getOption(AdminAccount::KEY_EMAIL) ++ || $input->getOption(AdminAccount::KEY_USER) ++ || $input->getOption(AdminAccount::KEY_PASSWORD) ++ ) { ++ return $this->adminUser->validate($input); ++ } ++ return []; ++ } + } + +diff --git a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php +--- a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php +@@ -15,6 +15,9 @@ use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Output\OutputInterface; + use Symfony\Component\Console\Question\Question; + ++/** ++ * Command to create an admin user. ++ */ + class AdminUserCreateCommand extends AbstractSetupCommand + { + /** +@@ -52,6 +55,8 @@ class AdminUserCreateCommand extends AbstractSetupCommand + } + + /** ++ * Creation admin user in interaction mode. ++ * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * +@@ -129,6 +134,8 @@ class AdminUserCreateCommand extends AbstractSetupCommand + } + + /** ++ * Add not empty validator. ++ * + * @param \Symfony\Component\Console\Question\Question $question + * @return void + */ +@@ -144,7 +151,7 @@ class AdminUserCreateCommand extends AbstractSetupCommand + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { +@@ -165,25 +172,43 @@ class AdminUserCreateCommand extends AbstractSetupCommand + /** + * Get list of arguments for the command + * ++ * @param int $mode The mode of options. + * @return InputOption[] + */ +- public function getOptionsList() ++ public function getOptionsList($mode = InputOption::VALUE_REQUIRED) + { ++ $requiredStr = ($mode === InputOption::VALUE_REQUIRED ? '(Required) ' : ''); ++ + return [ +- new InputOption(AdminAccount::KEY_USER, null, InputOption::VALUE_REQUIRED, '(Required) Admin user'), +- new InputOption(AdminAccount::KEY_PASSWORD, null, InputOption::VALUE_REQUIRED, '(Required) Admin password'), +- new InputOption(AdminAccount::KEY_EMAIL, null, InputOption::VALUE_REQUIRED, '(Required) Admin email'), ++ new InputOption( ++ AdminAccount::KEY_USER, ++ null, ++ $mode, ++ $requiredStr . 'Admin user' ++ ), ++ new InputOption( ++ AdminAccount::KEY_PASSWORD, ++ null, ++ $mode, ++ $requiredStr . 'Admin password' ++ ), ++ new InputOption( ++ AdminAccount::KEY_EMAIL, ++ null, ++ $mode, ++ $requiredStr . 'Admin email' ++ ), + new InputOption( + AdminAccount::KEY_FIRST_NAME, + null, +- InputOption::VALUE_REQUIRED, +- '(Required) Admin first name' ++ $mode, ++ $requiredStr . 'Admin first name' + ), + new InputOption( + AdminAccount::KEY_LAST_NAME, + null, +- InputOption::VALUE_REQUIRED, +- '(Required) Admin last name' ++ $mode, ++ $requiredStr . 'Admin last name' + ), + ]; + } diff --git a/patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.13.patch b/patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.13.patch new file mode 100644 index 00000000..ebaf1f89 --- /dev/null +++ b/patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.13.patch @@ -0,0 +1,122 @@ +diff -Nuar a/vendor/magento/module-catalog/Block/Product/View/Options/Type/Date.php b/vendor/magento/module-catalog/Block/Product/View/Options/Type/Date.php +--- a/vendor/magento/module-catalog/Block/Product/View/Options/Type/Date.php ++++ b/vendor/magento/module-catalog/Block/Product/View/Options/Type/Date.php +@@ -83,7 +83,7 @@ class Date extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions + $yearEnd = $this->_catalogProductOptionTypeDate->getYearEnd(); + + $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); +- /** Escape invisible characters which are present in some locales and may corrupt formatting */ ++ /** Escape RTL characters which are present in some locales and corrupt formatting */ + $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); + $calendar = $this->getLayout()->createBlock( + 'Magento\Framework\View\Element\Html\Date' +diff -Nuar a/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php b/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php +--- a/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php ++++ b/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php +@@ -142,7 +142,7 @@ class Date extends \Magento\Catalog\Model\Product\Option\Type\DefaultType + + if ($this->_dateExists()) { + if ($this->useCalendar()) { +- $timestamp += $this->_localeDate->date($value['date'], null, true)->getTimestamp(); ++ $timestamp += $this->_localeDate->date($value['date'], null, true, false)->getTimestamp(); + } else { + $timestamp += mktime(0, 0, 0, $value['month'], $value['day'], $value['year']); + } +diff -Nuar a/vendor/magento/framework/Stdlib/DateTime/Timezone.php b/vendor/magento/framework/Stdlib/DateTime/Timezone.php +--- a/vendor/magento/framework/Stdlib/DateTime/Timezone.php ++++ b/vendor/magento/framework/Stdlib/DateTime/Timezone.php +@@ -151,27 +151,33 @@ class Timezone implements TimezoneInterface + + /** + * {@inheritdoc} +- * @SuppressWarnings(PHPMD.NPathComplexity) + */ +- public function date($date = null, $locale = null, $useTimezone = true) ++ public function date($date = null, $locale = null, $useTimezone = true, $includeTime = true) + { + $locale = $locale ?: $this->_localeResolver->getLocale(); + $timezone = $useTimezone + ? $this->getConfigTimezone() + : date_default_timezone_get(); + +- if (empty($date)) { +- return new \DateTime('now', new \DateTimeZone($timezone)); +- } elseif ($date instanceof \DateTime) { +- return $date->setTimezone(new \DateTimeZone($timezone)); +- } elseif (!is_numeric($date)) { +- $formatter = new \IntlDateFormatter( +- $locale, +- \IntlDateFormatter::SHORT, +- \IntlDateFormatter::NONE +- ); +- $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); ++ switch (true) { ++ case (empty($date)): ++ return new \DateTime('now', new \DateTimeZone($timezone)); ++ case ($date instanceof \DateTime): ++ return $date->setTimezone(new \DateTimeZone($timezone)); ++ case ($date instanceof \DateTimeImmutable): ++ return new \DateTime($date->format('Y-m-d H:i:s'), $date->getTimezone()); ++ case (!is_numeric($date)): ++ $timeType = $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE; ++ $formatter = new \IntlDateFormatter( ++ $locale, ++ \IntlDateFormatter::SHORT, ++ $timeType, ++ new \DateTimeZone($timezone) ++ ); ++ $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); ++ break; + } ++ + return (new \DateTime(null, new \DateTimeZone($timezone)))->setTimestamp($date); + } + +@@ -195,7 +201,7 @@ class Timezone implements TimezoneInterface + { + $formatTime = $showTime ? $format : \IntlDateFormatter::NONE; + +- if (!($date instanceof \DateTime)) { ++ if (!($date instanceof \DateTimeInterface)) { + $date = new \DateTime($date); + } + +@@ -258,7 +264,7 @@ class Timezone implements TimezoneInterface + $timezone = null, + $pattern = null + ) { +- if (!($date instanceof \DateTime)) { ++ if (!($date instanceof \DateTimeInterface)) { + $date = new \DateTime($date); + } + +@@ -294,8 +300,12 @@ class Timezone implements TimezoneInterface + */ + public function convertConfigTimeToUtc($date, $format = 'Y-m-d H:i:s') + { +- if (!($date instanceof \DateTime)) { +- $date = new \DateTime($date, new \DateTimeZone($this->getConfigTimezone())); ++ if (!($date instanceof \DateTimeInterface)) { ++ if ($date instanceof \DateTimeImmutable) { ++ $date = new \DateTime($date->format('Y-m-d H:i:s'), new \DateTimeZone($this->getConfigTimezone())); ++ } else { ++ $date = new \DateTime($date, new \DateTimeZone($this->getConfigTimezone())); ++ } + } else { + if ($date->getTimezone()->getName() !== $this->getConfigTimezone()) { + throw new LocalizedException( +diff -Nuar a/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php b/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php +--- a/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php ++++ b/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php +@@ -65,9 +65,10 @@ interface TimezoneInterface + * @param mixed $date + * @param string $locale + * @param bool $useTimezone ++ * @param bool $includeTime + * @return \DateTime + */ +- public function date($date = null, $locale = null, $useTimezone = true); ++ public function date($date = null, $locale = null, $useTimezone = true, $includeTime = true); + + /** + * Create \DateTime object with date converted to scope timezone and scope Locale diff --git a/patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.4.patch b/patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.4.patch new file mode 100644 index 00000000..f2f668ba --- /dev/null +++ b/patches/MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.4.patch @@ -0,0 +1,170 @@ +diff -Naur a/vendor/magento/module-catalog/Block/Product/View/Options/Type/Date.php b/vendor/magento/module-catalog/Block/Product/View/Options/Type/Date.php +--- a/vendor/magento/module-catalog/Block/Product/View/Options/Type/Date.php ++++ b/vendor/magento/module-catalog/Block/Product/View/Options/Type/Date.php +@@ -1,6 +1,6 @@ + _catalogProductOptionTypeDate->getYearStart(); + $yearEnd = $this->_catalogProductOptionTypeDate->getYearEnd(); + ++ $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); ++ /** Escape RTL characters which are present in some locales and corrupt formatting */ ++ $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); + $calendar = $this->getLayout()->createBlock( + 'Magento\Framework\View\Element\Html\Date' + )->setId( +@@ -93,7 +96,7 @@ class Date extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions + )->setImage( + $this->getViewFileUrl('Magento_Theme::calendar.png') + )->setDateFormat( +- $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT) ++ $escapedDateFormat + )->setValue( + $value + )->setYearsRange( +diff -Naur a/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php b/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php +--- a/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php ++++ b/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php +@@ -1,6 +1,6 @@ + _dateExists()) { + if ($this->useCalendar()) { +- $timestamp += (new \DateTime($value['date']))->getTimestamp(); ++ $timestamp += $this->_localeDate->date($value['date'], null, true, false)->getTimestamp(); + } else { + $timestamp += mktime(0, 0, 0, $value['month'], $value['day'], $value['year']); + } +diff -Naur a/vendor/magento/framework/Stdlib/DateTime/Timezone.php b/vendor/magento/framework/Stdlib/DateTime/Timezone.php +--- a/vendor/magento/framework/Stdlib/DateTime/Timezone.php ++++ b/vendor/magento/framework/Stdlib/DateTime/Timezone.php +@@ -1,6 +1,6 @@ + _localeResolver->getLocale(); + $timezone = $useTimezone + ? $this->getConfigTimezone() + : date_default_timezone_get(); + +- if (empty($date)) { +- return new \DateTime('now', new \DateTimeZone($timezone)); +- } elseif ($date instanceof \DateTime) { +- return $date->setTimezone(new \DateTimeZone($timezone)); +- } elseif (!is_numeric($date)) { +- $formatter = new \IntlDateFormatter( +- $locale, +- \IntlDateFormatter::SHORT, +- \IntlDateFormatter::SHORT, +- new \DateTimeZone($timezone) +- ); +- $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); ++ switch (true) { ++ case (empty($date)): ++ return new \DateTime('now', new \DateTimeZone($timezone)); ++ case ($date instanceof \DateTime): ++ return $date->setTimezone(new \DateTimeZone($timezone)); ++ case ($date instanceof \DateTimeImmutable): ++ return new \DateTime($date->format('Y-m-d H:i:s'), $date->getTimezone()); ++ case (!is_numeric($date)): ++ $timeType = $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE; ++ $formatter = new \IntlDateFormatter( ++ $locale, ++ \IntlDateFormatter::SHORT, ++ $timeType, ++ new \DateTimeZone($timezone) ++ ); ++ $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); ++ break; + } ++ + return (new \DateTime(null, new \DateTimeZone($timezone)))->setTimestamp($date); + } + +@@ -196,7 +201,7 @@ class Timezone implements TimezoneInterface + { + $formatTime = $showTime ? $format : \IntlDateFormatter::NONE; + +- if (!($date instanceof \DateTime)) { ++ if (!($date instanceof \DateTimeInterface)) { + $date = new \DateTime($date); + } + +@@ -259,7 +264,7 @@ class Timezone implements TimezoneInterface + $timezone = null, + $pattern = null + ) { +- if (!($date instanceof \DateTime)) { ++ if (!($date instanceof \DateTimeInterface)) { + $date = new \DateTime($date); + } + +@@ -295,8 +300,12 @@ class Timezone implements TimezoneInterface + */ + public function convertConfigTimeToUtc($date, $format = 'Y-m-d H:i:s') + { +- if (!($date instanceof \DateTime)) { +- $date = new \DateTime($date, new \DateTimeZone($this->getConfigTimezone())); ++ if (!($date instanceof \DateTimeInterface)) { ++ if ($date instanceof \DateTimeImmutable) { ++ $date = new \DateTime($date->format('Y-m-d H:i:s'), new \DateTimeZone($this->getConfigTimezone())); ++ } else { ++ $date = new \DateTime($date, new \DateTimeZone($this->getConfigTimezone())); ++ } + } else { + if ($date->getTimezone()->getName() !== $this->getConfigTimezone()) { + throw new LocalizedException( +diff -Nuar a/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php b/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php +--- a/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php ++++ b/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php +@@ -1,6 +1,6 @@ + _catalogProductOptionTypeDate->getYearStart(); + $yearEnd = $this->_catalogProductOptionTypeDate->getYearEnd(); + ++ $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); ++ /** Escape RTL characters which are present in some locales and corrupt formatting */ ++ $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); + $calendar = $this->getLayout()->createBlock( + 'Magento\Framework\View\Element\Html\Date' + )->setId( +@@ -93,7 +96,7 @@ class Date extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions + )->setImage( + $this->getViewFileUrl('Magento_Theme::calendar.png') + )->setDateFormat( +- $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT) ++ $escapedDateFormat + )->setValue( + $value + )->setYearsRange( +diff -Nuar a/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php b/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php +--- a/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php ++++ b/vendor/magento/module-catalog/Model/Product/Option/Type/Date.php +@@ -1,6 +1,6 @@ + _dateExists()) { + if ($this->useCalendar()) { +- $timestamp += (new \DateTime($value['date']))->getTimestamp(); ++ $timestamp += $this->_localeDate->date($value['date'], null, true, false)->getTimestamp(); + } else { + $timestamp += mktime(0, 0, 0, $value['month'], $value['day'], $value['year']); + } +diff -Nuar a/vendor/magento/framework/Stdlib/DateTime/Timezone.php b/vendor/magento/framework/Stdlib/DateTime/Timezone.php +--- a/vendor/magento/framework/Stdlib/DateTime/Timezone.php ++++ b/vendor/magento/framework/Stdlib/DateTime/Timezone.php +@@ -1,6 +1,6 @@ + _localeResolver->getLocale(); + $timezone = $useTimezone + ? $this->getConfigTimezone() + : date_default_timezone_get(); + +- if (empty($date)) { +- return new \DateTime('now', new \DateTimeZone($timezone)); +- } elseif ($date instanceof \DateTime) { +- return $date->setTimezone(new \DateTimeZone($timezone)); +- } elseif (!is_numeric($date)) { +- $formatter = new \IntlDateFormatter( +- $locale, +- \IntlDateFormatter::SHORT, +- \IntlDateFormatter::SHORT, +- new \DateTimeZone($timezone) +- ); +- $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); ++ switch (true) { ++ case (empty($date)): ++ return new \DateTime('now', new \DateTimeZone($timezone)); ++ case ($date instanceof \DateTime): ++ return $date->setTimezone(new \DateTimeZone($timezone)); ++ case ($date instanceof \DateTimeImmutable): ++ return new \DateTime($date->format('Y-m-d H:i:s'), $date->getTimezone()); ++ case (!is_numeric($date)): ++ $timeType = $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE; ++ $formatter = new \IntlDateFormatter( ++ $locale, ++ \IntlDateFormatter::SHORT, ++ $timeType, ++ new \DateTimeZone($timezone) ++ ); ++ $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); ++ break; + } ++ + return (new \DateTime(null, new \DateTimeZone($timezone)))->setTimestamp($date); + } + +@@ -196,7 +201,7 @@ class Timezone implements TimezoneInterface + { + $formatTime = $showTime ? $format : \IntlDateFormatter::NONE; + +- if (!($date instanceof \DateTime)) { ++ if (!($date instanceof \DateTimeInterface)) { + $date = new \DateTime($date); + } + +@@ -259,7 +264,7 @@ class Timezone implements TimezoneInterface + $timezone = null, + $pattern = null + ) { +- if (!($date instanceof \DateTime)) { ++ if (!($date instanceof \DateTimeInterface)) { + $date = new \DateTime($date); + } + +@@ -295,8 +300,12 @@ class Timezone implements TimezoneInterface + */ + public function convertConfigTimeToUtc($date, $format = 'Y-m-d H:i:s') + { +- if (!($date instanceof \DateTime)) { +- $date = new \DateTime($date, new \DateTimeZone($this->getConfigTimezone())); ++ if (!($date instanceof \DateTimeInterface)) { ++ if ($date instanceof \DateTimeImmutable) { ++ $date = new \DateTime($date->format('Y-m-d H:i:s'), new \DateTimeZone($this->getConfigTimezone())); ++ } else { ++ $date = new \DateTime($date, new \DateTimeZone($this->getConfigTimezone())); ++ } + } else { + if ($date->getTimezone()->getName() !== $this->getConfigTimezone()) { + throw new LocalizedException( +diff -Nuar a/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php b/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php +--- a/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php ++++ b/vendor/magento/framework/Stdlib/DateTime/TimezoneInterface.php +@@ -1,6 +1,6 @@ + useAttachment = $useAttachment; + $this->useShortAttachment = $useShortAttachment; + $this->includeContextAndExtra = $includeContextAndExtra; +- if ($this->includeContextAndExtra) { ++ ++ if ($this->includeContextAndExtra && $this->useShortAttachment) { + $this->lineFormatter = new LineFormatter; + } + } +@@ -139,35 +141,26 @@ class SlackHandler extends SocketHandler + 'channel' => $this->channel, + 'username' => $this->username, + 'text' => '', +- 'attachments' => array() ++ 'attachments' => array(), + ); + + if ($this->useAttachment) { + $attachment = array( + 'fallback' => $record['message'], +- 'color' => $this->getAttachmentColor($record['level']) ++ 'color' => $this->getAttachmentColor($record['level']), ++ 'fields' => array(), + ); + + if ($this->useShortAttachment) { +- $attachment['fields'] = array( +- array( +- 'title' => $record['level_name'], +- 'value' => $record['message'], +- 'short' => false +- ) +- ); ++ $attachment['title'] = $record['level_name']; ++ $attachment['text'] = $record['message']; + } else { +- $attachment['fields'] = array( +- array( +- 'title' => 'Message', +- 'value' => $record['message'], +- 'short' => false +- ), +- array( +- 'title' => 'Level', +- 'value' => $record['level_name'], +- 'short' => true +- ) ++ $attachment['title'] = 'Message'; ++ $attachment['text'] = $record['message']; ++ $attachment['fields'][] = array( ++ 'title' => 'Level', ++ 'value' => $record['level_name'], ++ 'short' => true, + ); + } + +@@ -177,7 +170,7 @@ class SlackHandler extends SocketHandler + $attachment['fields'][] = array( + 'title' => "Extra", + 'value' => $this->stringify($record['extra']), +- 'short' => $this->useShortAttachment ++ 'short' => $this->useShortAttachment, + ); + } else { + // Add all extra fields as individual fields in attachment +@@ -185,7 +178,7 @@ class SlackHandler extends SocketHandler + $attachment['fields'][] = array( + 'title' => $var, + 'value' => $val, +- 'short' => $this->useShortAttachment ++ 'short' => $this->useShortAttachment, + ); + } + } +@@ -196,7 +189,7 @@ class SlackHandler extends SocketHandler + $attachment['fields'][] = array( + 'title' => "Context", + 'value' => $this->stringify($record['context']), +- 'short' => $this->useShortAttachment ++ 'short' => $this->useShortAttachment, + ); + } else { + // Add all context fields as individual fields in attachment +@@ -204,7 +197,7 @@ class SlackHandler extends SocketHandler + $attachment['fields'][] = array( + 'title' => $var, + 'value' => $val, +- 'short' => $this->useShortAttachment ++ 'short' => $this->useShortAttachment, + ); + } + } +@@ -248,6 +241,10 @@ class SlackHandler extends SocketHandler + protected function write(array $record) + { + parent::write($record); ++ $res = $this->getResource(); ++ if (is_resource($res)) { ++ @fread($res, 2048); ++ } + $this->closeSocket(); + } + +@@ -275,8 +272,7 @@ class SlackHandler extends SocketHandler + /** + * Stringifies an array of key/value pairs to be used in attachment fields + * +- * @param array $fields +- * @access protected ++ * @param array $fields + * @return string + */ + protected function stringify($fields) +diff -Nuar a/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php +index a3e7252e..e4c3c37f 100644 +--- a/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php ++++ b/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php +@@ -41,6 +41,15 @@ class SocketHandler extends AbstractProcessingHandler + $this->connectionTimeout = (float) ini_get('default_socket_timeout'); + } + ++ /** ++ * @return resource|null ++ */ ++ protected function getResource() ++ { ++ return $this->resource; ++ } ++ ++ + /** + * Connect (if necessary) and write to the socket + * diff --git a/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.13.patch b/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.13.patch new file mode 100644 index 00000000..9e9b4785 --- /dev/null +++ b/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.13.patch @@ -0,0 +1,76 @@ +diff -Nuar a/vendor/magento/framework/DB/Statement/Pdo/Mysql.php b/vendor/magento/framework/DB/Statement/Pdo/Mysql.php +--- a/vendor/magento/framework/DB/Statement/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Statement/Pdo/Mysql.php +@@ -3,23 +3,20 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++namespace Magento\Framework\DB\Statement\Pdo; + +-// @codingStandardsIgnoreFile ++use Magento\Framework\DB\Statement\Parameter; + + /** + * Mysql DB Statement + * + * @author Magento Core Team + */ +-namespace Magento\Framework\DB\Statement\Pdo; +- +-use Magento\Framework\DB\Statement\Parameter; +- + class Mysql extends \Zend_Db_Statement_Pdo + { ++ + /** +- * Executes statement with binding values to it. +- * Allows transferring specific options to DB driver. ++ * Executes statement with binding values to it. Allows transferring specific options to DB driver. + * + * @param array $params Array of values to bind to parameter placeholders. + * @return bool +@@ -63,11 +60,9 @@ class Mysql extends \Zend_Db_Statement_Pdo + $statement->bindParam($paramName, $bindValues[$name], $dataType, $length, $driverOptions); + } + +- try { ++ return $this->tryExecute(function () use ($statement) { + return $statement->execute(); +- } catch (\PDOException $e) { +- throw new \Zend_Db_Statement_Exception($e->getMessage(), (int)$e->getCode(), $e); +- } ++ }); + } + + /** +@@ -92,7 +87,29 @@ class Mysql extends \Zend_Db_Statement_Pdo + if ($specialExecute) { + return $this->_executeWithBinding($params); + } else { +- return parent::_execute($params); ++ return $this->tryExecute(function () use ($params) { ++ return $params !== null ? $this->_stmt->execute($params) : $this->_stmt->execute(); ++ }); ++ } ++ } ++ ++ /** ++ * Executes query and avoid warnings. ++ * ++ * @param callable $callback ++ * @return bool ++ * @throws \Zend_Db_Statement_Exception ++ */ ++ private function tryExecute($callback) ++ { ++ $previousLevel = error_reporting(\E_ERROR); // disable warnings for PDO bugs #63812, #74401 ++ try { ++ return $callback(); ++ } catch (\PDOException $e) { ++ $message = sprintf('%s, query was: %s', $e->getMessage(), $this->_stmt->queryString); ++ throw new \Zend_Db_Statement_Exception($message, (int)$e->getCode(), $e); ++ } finally { ++ error_reporting($previousLevel); + } + } + } diff --git a/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.4.patch b/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.4.patch new file mode 100644 index 00000000..b2b704d6 --- /dev/null +++ b/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.4.patch @@ -0,0 +1,79 @@ +diff -Nuar a/vendor/magento/framework/DB/Statement/Pdo/Mysql.php b/vendor/magento/framework/DB/Statement/Pdo/Mysql.php +--- a/vendor/magento/framework/DB/Statement/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Statement/Pdo/Mysql.php +@@ -1,25 +1,22 @@ + + */ +-namespace Magento\Framework\DB\Statement\Pdo; +- +-use Magento\Framework\DB\Statement\Parameter; +- + class Mysql extends \Zend_Db_Statement_Pdo + { ++ + /** +- * Executes statement with binding values to it. +- * Allows transferring specific options to DB driver. ++ * Executes statement with binding values to it. Allows transferring specific options to DB driver. + * + * @param array $params Array of values to bind to parameter placeholders. + * @return bool +@@ -63,11 +60,9 @@ class Mysql extends \Zend_Db_Statement_Pdo + $statement->bindParam($paramName, $bindValues[$name], $dataType, $length, $driverOptions); + } + +- try { ++ return $this->tryExecute(function () use ($statement) { + return $statement->execute(); +- } catch (\PDOException $e) { +- throw new \Zend_Db_Statement_Exception($e->getMessage(), (int)$e->getCode(), $e); +- } ++ }); + } + + /** +@@ -92,7 +87,29 @@ class Mysql extends \Zend_Db_Statement_Pdo + if ($specialExecute) { + return $this->_executeWithBinding($params); + } else { +- return parent::_execute($params); ++ return $this->tryExecute(function () use ($params) { ++ return $params !== null ? $this->_stmt->execute($params) : $this->_stmt->execute(); ++ }); ++ } ++ } ++ ++ /** ++ * Executes query and avoid warnings. ++ * ++ * @param callable $callback ++ * @return bool ++ * @throws \Zend_Db_Statement_Exception ++ */ ++ private function tryExecute($callback) ++ { ++ $previousLevel = error_reporting(\E_ERROR); // disable warnings for PDO bugs #63812, #74401 ++ try { ++ return $callback(); ++ } catch (\PDOException $e) { ++ $message = sprintf('%s, query was: %s', $e->getMessage(), $this->_stmt->queryString); ++ throw new \Zend_Db_Statement_Exception($message, (int)$e->getCode(), $e); ++ } finally { ++ error_reporting($previousLevel); + } + } + } diff --git a/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.5.patch b/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.5.patch new file mode 100644 index 00000000..641edcea --- /dev/null +++ b/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.5.patch @@ -0,0 +1,79 @@ +diff -Nuar a/vendor/magento/framework/DB/Statement/Pdo/Mysql.php b/vendor/magento/framework/DB/Statement/Pdo/Mysql.php +--- a/vendor/magento/framework/DB/Statement/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Statement/Pdo/Mysql.php +@@ -1,25 +1,22 @@ + + */ +-namespace Magento\Framework\DB\Statement\Pdo; +- +-use Magento\Framework\DB\Statement\Parameter; +- + class Mysql extends \Zend_Db_Statement_Pdo + { ++ + /** +- * Executes statement with binding values to it. +- * Allows transferring specific options to DB driver. ++ * Executes statement with binding values to it. Allows transferring specific options to DB driver. + * + * @param array $params Array of values to bind to parameter placeholders. + * @return bool +@@ -63,11 +60,9 @@ class Mysql extends \Zend_Db_Statement_Pdo + $statement->bindParam($paramName, $bindValues[$name], $dataType, $length, $driverOptions); + } + +- try { ++ return $this->tryExecute(function () use ($statement) { + return $statement->execute(); +- } catch (\PDOException $e) { +- throw new \Zend_Db_Statement_Exception($e->getMessage(), (int)$e->getCode(), $e); +- } ++ }); + } + + /** +@@ -92,7 +87,29 @@ class Mysql extends \Zend_Db_Statement_Pdo + if ($specialExecute) { + return $this->_executeWithBinding($params); + } else { +- return parent::_execute($params); ++ return $this->tryExecute(function () use ($params) { ++ return $params !== null ? $this->_stmt->execute($params) : $this->_stmt->execute(); ++ }); ++ } ++ } ++ ++ /** ++ * Executes query and avoid warnings. ++ * ++ * @param callable $callback ++ * @return bool ++ * @throws \Zend_Db_Statement_Exception ++ */ ++ private function tryExecute($callback) ++ { ++ $previousLevel = error_reporting(\E_ERROR); // disable warnings for PDO bugs #63812, #74401 ++ try { ++ return $callback(); ++ } catch (\PDOException $e) { ++ $message = sprintf('%s, query was: %s', $e->getMessage(), $this->_stmt->queryString); ++ throw new \Zend_Db_Statement_Exception($message, (int)$e->getCode(), $e); ++ } finally { ++ error_reporting($previousLevel); + } + } + } diff --git a/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.2.0.patch b/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.2.0.patch new file mode 100644 index 00000000..d6ed4350 --- /dev/null +++ b/patches/MAGECLOUD-2820__implement_isolated_connections_mechanism__2.2.0.patch @@ -0,0 +1,75 @@ +diff -Nuar a/vendor/magento/framework/DB/Statement/Pdo/Mysql.php b/vendor/magento/framework/DB/Statement/Pdo/Mysql.php +--- a/vendor/magento/framework/DB/Statement/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Statement/Pdo/Mysql.php +@@ -3,21 +3,20 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++namespace Magento\Framework\DB\Statement\Pdo; ++ ++use Magento\Framework\DB\Statement\Parameter; + + /** + * Mysql DB Statement + * + * @author Magento Core Team + */ +-namespace Magento\Framework\DB\Statement\Pdo; +- +-use Magento\Framework\DB\Statement\Parameter; +- + class Mysql extends \Zend_Db_Statement_Pdo + { ++ + /** +- * Executes statement with binding values to it. +- * Allows transferring specific options to DB driver. ++ * Executes statement with binding values to it. Allows transferring specific options to DB driver. + * + * @param array $params Array of values to bind to parameter placeholders. + * @return bool +@@ -61,11 +60,9 @@ class Mysql extends \Zend_Db_Statement_Pdo + $statement->bindParam($paramName, $bindValues[$name], $dataType, $length, $driverOptions); + } + +- try { ++ return $this->tryExecute(function () use ($statement) { + return $statement->execute(); +- } catch (\PDOException $e) { +- throw new \Zend_Db_Statement_Exception($e->getMessage(), (int)$e->getCode(), $e); +- } ++ }); + } + + /** +@@ -90,7 +87,29 @@ class Mysql extends \Zend_Db_Statement_Pdo + if ($specialExecute) { + return $this->_executeWithBinding($params); + } else { +- return parent::_execute($params); ++ return $this->tryExecute(function () use ($params) { ++ return $params !== null ? $this->_stmt->execute($params) : $this->_stmt->execute(); ++ }); ++ } ++ } ++ ++ /** ++ * Executes query and avoid warnings. ++ * ++ * @param callable $callback ++ * @return bool ++ * @throws \Zend_Db_Statement_Exception ++ */ ++ private function tryExecute($callback) ++ { ++ $previousLevel = error_reporting(\E_ERROR); // disable warnings for PDO bugs #63812, #74401 ++ try { ++ return $callback(); ++ } catch (\PDOException $e) { ++ $message = sprintf('%s, query was: %s', $e->getMessage(), $this->_stmt->queryString); ++ throw new \Zend_Db_Statement_Exception($message, (int)$e->getCode(), $e); ++ } finally { ++ error_reporting($previousLevel); + } + } + } diff --git a/patches/MAGECLOUD-2822__configure_max_execution_time.patch b/patches/MAGECLOUD-2822__configure_max_execution_time.patch new file mode 100644 index 00000000..1fd810cc --- /dev/null +++ b/patches/MAGECLOUD-2822__configure_max_execution_time.patch @@ -0,0 +1,107 @@ +diff -Nuar a/vendor/magento/module-deploy/Console/DeployStaticOptions.php b/vendor/magento/module-deploy/Console/DeployStaticOptions.php +--- a/vendor/magento/module-deploy/Console/DeployStaticOptions.php ++++ b/vendor/magento/module-deploy/Console/DeployStaticOptions.php +@@ -6,6 +6,7 @@ + + namespace Magento\Deploy\Console; + ++use Magento\Deploy\Process\Queue; + use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Input\InputArgument; + +@@ -57,6 +58,11 @@ class DeployStaticOptions + */ + const JOBS_AMOUNT = 'jobs'; + ++ /** ++ * Key for max execution time option ++ */ ++ const MAX_EXECUTION_TIME = 'max-execution-time'; ++ + /** + * Force run of static deploy + */ +@@ -150,6 +156,7 @@ public function getOptionsList() + * Basic options + * + * @return array ++ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function getBasicOptions() + { +@@ -216,6 +223,13 @@ private function getBasicOptions() + 'Enable parallel processing using the specified number of jobs.', + self::DEFAULT_JOBS_AMOUNT + ), ++ new InputOption( ++ self::MAX_EXECUTION_TIME, ++ null, ++ InputOption::VALUE_OPTIONAL, ++ 'The maximum expected execution time of deployment static process (in seconds).', ++ Queue::DEFAULT_MAX_EXEC_TIME ++ ), + new InputOption( + self::SYMLINK_LOCALE, + null, +diff -Nuar a/vendor/magento/module-deploy/Service/DeployStaticContent.php b/vendor/magento/module-deploy/Service/DeployStaticContent.php +--- a/vendor/magento/module-deploy/Service/DeployStaticContent.php ++++ b/vendor/magento/module-deploy/Service/DeployStaticContent.php +@@ -85,24 +85,26 @@ public function deploy(array $options) + return; + } + +- $queue = $this->queueFactory->create( +- [ +- 'logger' => $this->logger, +- 'options' => $options, +- 'maxProcesses' => $this->getProcessesAmount($options), +- 'deployPackageService' => $this->objectManager->create( +- \Magento\Deploy\Service\DeployPackage::class, +- [ +- 'logger' => $this->logger +- ] +- ) +- ] +- ); ++ $queueOptions = [ ++ 'logger' => $this->logger, ++ 'options' => $options, ++ 'maxProcesses' => $this->getProcessesAmount($options), ++ 'deployPackageService' => $this->objectManager->create( ++ \Magento\Deploy\Service\DeployPackage::class, ++ [ ++ 'logger' => $this->logger ++ ] ++ ) ++ ]; ++ ++ if (isset($options[Options::MAX_EXECUTION_TIME])) { ++ $queueOptions['maxExecTime'] = (int)$options[Options::MAX_EXECUTION_TIME]; ++ } + + $deployStrategy = $this->deployStrategyFactory->create( + $options[Options::STRATEGY], + [ +- 'queue' => $queue ++ 'queue' => $this->queueFactory->create($queueOptions) + ] + ); + +@@ -133,6 +135,8 @@ public function deploy(array $options) + } + + /** ++ * Returns amount of parallel processes, returns zero if option wasn't set. ++ * + * @param array $options + * @return int + */ +@@ -142,6 +146,8 @@ private function getProcessesAmount(array $options) + } + + /** ++ * Checks if need to refresh only version. ++ * + * @param array $options + * @return bool + */ diff --git a/patches/MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch b/patches/MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch new file mode 100644 index 00000000..9f987132 --- /dev/null +++ b/patches/MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch @@ -0,0 +1,89 @@ +diff -Nuar a/vendor/magento/module-deploy/Console/DeployStaticOptions.php b/vendor/magento/module-deploy/Console/DeployStaticOptions.php +--- a/vendor/magento/module-deploy/Console/DeployStaticOptions.php ++++ b/vendor/magento/module-deploy/Console/DeployStaticOptions.php +@@ -6,6 +6,7 @@ + + namespace Magento\Deploy\Console; + ++use Magento\Deploy\Process\Queue; + use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Input\InputArgument; + +@@ -57,6 +58,11 @@ class DeployStaticOptions + */ + const JOBS_AMOUNT = 'jobs'; + ++ /** ++ * Key for max execution time option ++ */ ++ const MAX_EXECUTION_TIME = 'max-execution-time'; ++ + /** + * Force run of static deploy + */ +@@ -150,6 +156,7 @@ public function getOptionsList() + * Basic options + * + * @return array ++ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function getBasicOptions() + { +@@ -216,6 +223,13 @@ private function getBasicOptions() + 'Enable parallel processing using the specified number of jobs.', + self::DEFAULT_JOBS_AMOUNT + ), ++ new InputOption( ++ self::MAX_EXECUTION_TIME, ++ null, ++ InputOption::VALUE_OPTIONAL, ++ 'The maximum expected execution time of deployment static process (in seconds).', ++ Queue::DEFAULT_MAX_EXEC_TIME ++ ), + new InputOption( + self::SYMLINK_LOCALE, + null, +diff -Nuar a/vendor/magento/module-deploy/Service/DeployStaticContent.php b/vendor/magento/module-deploy/Service/DeployStaticContent.php +--- a/vendor/magento/module-deploy/Service/DeployStaticContent.php ++++ b/vendor/magento/module-deploy/Service/DeployStaticContent.php +@@ -88,24 +88,26 @@ class DeployStaticContent + return; + } + +- $queue = $this->queueFactory->create( +- [ +- 'logger' => $this->logger, +- 'options' => $options, +- 'maxProcesses' => $this->getProcessesAmount($options), +- 'deployPackageService' => $this->objectManager->create( +- \Magento\Deploy\Service\DeployPackage::class, +- [ +- 'logger' => $this->logger +- ] +- ) +- ] +- ); ++ $queueOptions = [ ++ 'logger' => $this->logger, ++ 'options' => $options, ++ 'maxProcesses' => $this->getProcessesAmount($options), ++ 'deployPackageService' => $this->objectManager->create( ++ \Magento\Deploy\Service\DeployPackage::class, ++ [ ++ 'logger' => $this->logger ++ ] ++ ) ++ ]; ++ ++ if (isset($options[Options::MAX_EXECUTION_TIME])) { ++ $queueOptions['maxExecTime'] = (int)$options[Options::MAX_EXECUTION_TIME]; ++ } + + $deployStrategy = $this->deployStrategyFactory->create( + $options[Options::STRATEGY], + [ +- 'queue' => $queue ++ 'queue' => $this->queueFactory->create($queueOptions) + ] + ); + diff --git a/patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch b/patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch new file mode 100644 index 00000000..df9b29e1 --- /dev/null +++ b/patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch @@ -0,0 +1,177 @@ +diff -Nuar a/vendor/amzn/amazon-pay-module/etc/di.xml b/vendor/amzn/amazon-pay-module/etc/di.xml +index c954f48..e585eae 100644 +--- a/vendor/amzn/amazon-pay-module/etc/di.xml ++++ b/vendor/amzn/amazon-pay-module/etc/di.xml +@@ -39,24 +39,20 @@ + + + +- ++ + + amazon_error_mapping.xml + + +- ++ + + Amazon\Payment\Gateway\ErrorMapper\VirtualConfigReader + amazon_error_mapper + + +- ++ + +- Amazon\Payment\Gateway\ErrorMapper\VirtualMappingData +- ++ Amazon\Payment\Gateway\ErrorMapper\VirtualMappingData + + + +@@ -120,15 +116,12 @@ + + + +- Amazon\Payment\Gateway\Request\AuthorizationRequest +- ++ Amazon\Payment\Gateway\Request\AuthorizationRequest + Amazon\Payment\Gateway\Response\CompleteAuthHandler + Amazon\Payment\Gateway\Http\TransferFactory + AmazonAuthorizationValidators + Amazon\Payment\Gateway\Http\Client\AuthorizeClient +- Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper +- ++ Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper + + + +@@ -141,30 +134,24 @@ + + + +- Amazon\Payment\Gateway\Request\AuthorizationRequest +- ++ Amazon\Payment\Gateway\Request\AuthorizationRequest + Amazon\Payment\Gateway\Response\CompleteSaleHandler + Amazon\Payment\Gateway\Http\TransferFactory + AmazonAuthorizationValidators + Amazon\Payment\Gateway\Http\Client\CaptureClient +- Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper +- ++ Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper + + + + + + +- Amazon\Payment\Gateway\Request\SettlementRequest +- ++ Amazon\Payment\Gateway\Request\SettlementRequest + Amazon\Payment\Gateway\Response\SettlementHandler + Amazon\Payment\Gateway\Http\TransferFactory + AmazonAuthorizationValidators + Amazon\Payment\Gateway\Http\Client\SettlementClient +- Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper +- ++ Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper + + + +@@ -183,12 +170,9 @@ + Amazon\Payment\Gateway\Request\RefundRequest + Amazon\Payment\Gateway\Response\RefundHandler + Amazon\Payment\Gateway\Http\TransferFactory +- Amazon\Payment\Gateway\Validator\AuthorizationValidator +- ++ Amazon\Payment\Gateway\Validator\AuthorizationValidator + Amazon\Payment\Gateway\Http\Client\RefundClient +- Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper +- ++ Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper + + + +@@ -198,12 +182,9 @@ + Amazon\Payment\Gateway\Request\VoidRequest + Amazon\Payment\Gateway\Response\VoidHandler + Amazon\Payment\Gateway\Http\TransferFactory +- Amazon\Payment\Gateway\Validator\AuthorizationValidator +- ++ Amazon\Payment\Gateway\Validator\AuthorizationValidator + Amazon\Payment\Gateway\Http\Client\VoidClient +- Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper +- ++ Amazon\Payment\Gateway\ErrorMapper\VirtualErrorMessageMapper + + + +@@ -237,26 +218,22 @@ + + + +- ++ + + + + + +- ++ + + + +- ++ + + + + +- ++ + + + +@@ -280,17 +257,14 @@ + + + +- Magento\Framework\Notification\NotifierInterface\Proxy +- ++ Magento\Framework\Notification\NotifierInterface\Proxy + + + + + + Amazon\Payment\Model\Ipn\CaptureProcessor\Proxy +- Amazon\Payment\Model\Ipn\AuthorizationProcessor\Proxy +- ++ Amazon\Payment\Model\Ipn\AuthorizationProcessor\Proxy + Amazon\Payment\Model\Ipn\OrderProcessor\Proxy + Amazon\Payment\Model\Ipn\RefundProcessor\Proxy + +@@ -310,8 +284,7 @@ + + + +- ++ + + + diff --git a/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.1.16.patch b/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.1.16.patch new file mode 100644 index 00000000..7d4df148 --- /dev/null +++ b/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.1.16.patch @@ -0,0 +1,40 @@ +diff -Nuar a/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php b/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php +--- a/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php ++++ b/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php +@@ -111,6 +111,13 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + */ + protected $_luaMaxCStack = 5000; + ++ /** ++ * If 'retry_reads_on_master' is truthy then reads will be retried against master when slave returns "(nil)" value ++ * ++ * @var boolean ++ */ ++ protected $_retryReadsOnMaster = false; ++ + /** + * @var stdClass + */ +@@ -316,6 +323,10 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + $this->_luaMaxCStack = (int) $options['lua_max_c_stack']; + } + ++ if (isset($options['retry_reads_on_master'])) { ++ $this->_retryReadsOnMaster = (bool) $options['retry_reads_on_master']; ++ } ++ + if (isset($options['auto_expire_lifetime'])) { + $this->_autoExpireLifetime = (int) $options['auto_expire_lifetime']; + } +@@ -371,6 +382,11 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + { + if ($this->_slave) { + $data = $this->_slave->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); ++ ++ // Prevent compounded effect of cache flood on asynchronously replicating master/slave setup ++ if ($this->_retryReadsOnMaster && $data === false) { ++ $data = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); ++ } + } else { + $data = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); + } diff --git a/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.2.3.patch b/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.2.3.patch new file mode 100644 index 00000000..c64650ee --- /dev/null +++ b/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.2.3.patch @@ -0,0 +1,40 @@ +diff -Nuar a/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php b/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php +--- a/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php ++++ b/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php +@@ -111,6 +111,13 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + */ + protected $_luaMaxCStack = 5000; + ++ /** ++ * If 'retry_reads_on_master' is truthy then reads will be retried against master when slave returns "(nil)" value ++ * ++ * @var boolean ++ */ ++ protected $_retryReadsOnMaster = false; ++ + /** + * @var stdClass + */ +@@ -326,6 +333,10 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + $this->_luaMaxCStack = (int) $options['lua_max_c_stack']; + } + ++ if (isset($options['retry_reads_on_master'])) { ++ $this->_retryReadsOnMaster = (bool) $options['retry_reads_on_master']; ++ } ++ + if (isset($options['auto_expire_lifetime'])) { + $this->_autoExpireLifetime = (int) $options['auto_expire_lifetime']; + } +@@ -428,6 +439,11 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + { + if ($this->_slave) { + $data = $this->_slave->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); ++ ++ // Prevent compounded effect of cache flood on asynchronously replicating master/slave setup ++ if ($this->_retryReadsOnMaster && $data === false) { ++ $data = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); ++ } + } else { + $data = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); + } diff --git a/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.3.0.patch b/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.3.0.patch new file mode 100644 index 00000000..568c2a1a --- /dev/null +++ b/patches/MAGECLOUD-2899__fix_redis_slave_configuration__2.3.0.patch @@ -0,0 +1,40 @@ +diff -Nuar a/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php b/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php +--- a/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php ++++ b/vendor/colinmollenhour/cache-backend-redis/Cm/Cache/Backend/Redis.php +@@ -111,6 +111,13 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + */ + protected $_luaMaxCStack = 5000; + ++ /** ++ * If 'retry_reads_on_master' is truthy then reads will be retried against master when slave returns "(nil)" value ++ * ++ * @var boolean ++ */ ++ protected $_retryReadsOnMaster = false; ++ + /** + * @var stdClass + */ +@@ -339,6 +346,10 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + $this->_luaMaxCStack = (int) $options['lua_max_c_stack']; + } + ++ if (isset($options['retry_reads_on_master'])) { ++ $this->_retryReadsOnMaster = (bool) $options['retry_reads_on_master']; ++ } ++ + if (isset($options['auto_expire_lifetime'])) { + $this->_autoExpireLifetime = (int) $options['auto_expire_lifetime']; + } +@@ -441,6 +452,11 @@ class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Ba + { + if ($this->_slave) { + $data = $this->_slave->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); ++ ++ // Prevent compounded effect of cache flood on asynchronously replicating master/slave setup ++ if ($this->_retryReadsOnMaster && $data === false) { ++ $data = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); ++ } + } else { + $data = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA); + } diff --git a/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.2.5.patch b/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.2.5.patch new file mode 100644 index 00000000..8df8c6d4 --- /dev/null +++ b/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.2.5.patch @@ -0,0 +1,1068 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -37,7 +37,7 @@ + + + +- ++ + + + +diff -Naur a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/Backend/FileLock.php +@@ -0,0 +1,194 @@ ++fileDriver = $fileDriver; ++ $this->path = rtrim($path, '/') . '/'; ++ ++ try { ++ if (!$this->fileDriver->isExists($this->path)) { ++ $this->fileDriver->createDirectory($this->path); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException( ++ new Phrase('Cannot create the directory for locks: %1', [$this->path]), ++ $exception ++ ); ++ } ++ } ++ ++ /** ++ * Acquires a lock by name ++ * ++ * @param string $name The lock name ++ * @param int $timeout Timeout in seconds. A negative timeout value means infinite timeout ++ * @return bool Returns true if the lock is acquired, otherwise returns false ++ * @throws RuntimeException Throws RuntimeException if cannot acquires the lock because FS problems ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ try { ++ $lockFile = $this->getLockPath($name); ++ $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); ++ $skipDeadline = $timeout < 0; ++ $deadline = microtime(true) + $timeout; ++ ++ while (!$this->tryToLock($fileResource)) { ++ if (!$skipDeadline && $deadline <= microtime(true)) { ++ $this->fileDriver->fileClose($fileResource); ++ return false; ++ } ++ usleep($this->sleepCycle); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException(new Phrase('Cannot acquire a lock.'), $exception); ++ } ++ ++ $this->locks[$lockFile] = $fileResource; ++ return true; ++ } ++ ++ /** ++ * Checks if a lock exists by name ++ * ++ * @param string $name The lock name ++ * @return bool Returns true if the lock exists, otherwise returns false ++ * @throws RuntimeException Throws RuntimeException if cannot check that the lock exists ++ */ ++ public function isLocked(string $name): bool ++ { ++ $lockFile = $this->getLockPath($name); ++ $result = false; ++ ++ try { ++ if ($this->fileDriver->isExists($lockFile)) { ++ $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); ++ if ($this->tryToLock($fileResource)) { ++ $result = false; ++ } else { ++ $result = true; ++ } ++ $this->fileDriver->fileClose($fileResource); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); ++ } ++ ++ return $result; ++ } ++ ++ /** ++ * Remove the lock by name ++ * ++ * @param string $name The lock name ++ * @return bool If the lock is removed returns true, otherwise returns false ++ */ ++ public function unlock(string $name): bool ++ { ++ $lockFile = $this->getLockPath($name); ++ ++ if (isset($this->locks[$lockFile]) && $this->tryToUnlock($this->locks[$lockFile])) { ++ unset($this->locks[$lockFile]); ++ return true; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * Returns the full path to the lock file by name ++ * ++ * @param string $name The lock name ++ * @return string The path to the lock file ++ */ ++ private function getLockPath(string $name): string ++ { ++ return $this->path . $name; ++ } ++ ++ /** ++ * Tries to lock a file resource ++ * ++ * @param resource $resource The file resource ++ * @return bool If the lock is acquired returns true, otherwise returns false ++ */ ++ private function tryToLock($resource): bool ++ { ++ try { ++ return $this->fileDriver->fileLock($resource, LOCK_EX | LOCK_NB); ++ } catch (FileSystemException $exception) { ++ return false; ++ } ++ } ++ ++ /** ++ * Tries to unlock a file resource ++ * ++ * @param resource $resource The file resource ++ * @return bool If the lock is removed returns true, otherwise returns false ++ */ ++ private function tryToUnlock($resource): bool ++ { ++ try { ++ return $this->fileDriver->fileLock($resource, LOCK_UN | LOCK_NB); ++ } catch (FileSystemException $exception) { ++ return false; ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/Lock/Backend/Zookeeper.php b/vendor/magento/framework/Lock/Backend/Zookeeper.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/Backend/Zookeeper.php +@@ -0,0 +1,280 @@ ++\Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']]; ++ ++ /** ++ * The mapping list of the lock name with the full lock path ++ * ++ * @var array ++ */ ++ private $locks = []; ++ ++ /** ++ * The default path to storage locks ++ */ ++ const DEFAULT_PATH = '/magento/locks'; ++ ++ /** ++ * @param string $host The host to connect to Zookeeper ++ * @param string $path The base path to locks in Zookeeper ++ * @throws RuntimeException ++ */ ++ public function __construct(string $host, string $path = self::DEFAULT_PATH) ++ { ++ if (!$path) { ++ throw new RuntimeException( ++ new Phrase('The path needs to be a non-empty string.') ++ ); ++ } ++ ++ if (!$host) { ++ throw new RuntimeException( ++ new Phrase('The host needs to be a non-empty string.') ++ ); ++ } ++ ++ $this->host = $host; ++ $this->path = rtrim($path, '/') . '/'; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * You can see the lock algorithm by the link ++ * @link https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks ++ * ++ * @throws RuntimeException ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ $skipDeadline = $timeout < 0; ++ $lockPath = $this->getFullPathToLock($name); ++ $deadline = microtime(true) + $timeout; ++ ++ if (!$this->checkAndCreateParentNode($lockPath)) { ++ throw new RuntimeException(new Phrase('Failed creating the path %1', [$lockPath])); ++ } ++ ++ $lockKey = $this->getProvider() ++ ->create($lockPath, '1', $this->acl, \Zookeeper::EPHEMERAL | \Zookeeper::SEQUENCE); ++ ++ if (!$lockKey) { ++ throw new RuntimeException(new Phrase('Failed creating lock %1', [$lockPath])); ++ } ++ ++ while ($this->isAnyLock($lockKey, $this->getIndex($lockKey))) { ++ if (!$skipDeadline && $deadline <= microtime(true)) { ++ $this->getProvider()->delete($lockKey); ++ return false; ++ } ++ ++ usleep($this->sleepCycle); ++ } ++ ++ $this->locks[$name] = $lockKey; ++ ++ return true; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function unlock(string $name): bool ++ { ++ if (!isset($this->locks[$name])) { ++ return false; ++ } ++ ++ return $this->getProvider()->delete($this->locks[$name]); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function isLocked(string $name): bool ++ { ++ return $this->isAnyLock($this->getFullPathToLock($name)); ++ } ++ ++ /** ++ * Gets full path to lock by its name ++ * ++ * @param string $name ++ * @return string ++ */ ++ private function getFullPathToLock(string $name): string ++ { ++ return $this->path . $name . '/' . $this->lockName; ++ } ++ ++ /** ++ * Initiolizes and returns Zookeeper provider ++ * ++ * @return \Zookeeper ++ * @throws RuntimeException ++ */ ++ private function getProvider(): \Zookeeper ++ { ++ if (!$this->zookeeper) { ++ $this->zookeeper = new \Zookeeper($this->host); ++ } ++ ++ $deadline = microtime(true) + $this->connectionTimeout; ++ while ($this->zookeeper->getState() != \Zookeeper::CONNECTED_STATE) { ++ if ($deadline <= microtime(true)) { ++ throw new RuntimeException(new Phrase('Zookeeper connection timed out!')); ++ } ++ usleep($this->sleepCycle); ++ } ++ ++ return $this->zookeeper; ++ } ++ ++ /** ++ * Checks and creates base path recursively ++ * ++ * @param string $path ++ * @return bool ++ * @throws RuntimeException ++ */ ++ private function checkAndCreateParentNode(string $path): bool ++ { ++ $path = dirname($path); ++ if ($this->getProvider()->exists($path)) { ++ return true; ++ } ++ ++ if (!$this->checkAndCreateParentNode($path)) { ++ return false; ++ } ++ ++ if ($this->getProvider()->create($path, '1', $this->acl)) { ++ return true; ++ } ++ ++ return $this->getProvider()->exists($path); ++ } ++ ++ /** ++ * Gets int increment of lock key ++ * ++ * @param string $key ++ * @return int|null ++ */ ++ private function getIndex(string $key) ++ { ++ if (!preg_match('/' . $this->lockName . '([0-9]+)$/', $key, $matches)) { ++ return null; ++ } ++ ++ return intval($matches[1]); ++ } ++ ++ /** ++ * Checks if there is any sequence node under parent of $fullKey. ++ * ++ * At first checks that the $fullKey node is present, if not - returns false. ++ * If $indexKey is non-null and there is a smaller index than $indexKey then returns true, ++ * otherwise returns false. ++ * ++ * @param string $fullKey The full path without any sequence info ++ * @param int|null $indexKey The index to compare ++ * @return bool ++ * @throws RuntimeException ++ */ ++ private function isAnyLock(string $fullKey, int $indexKey = null): bool ++ { ++ $parent = dirname($fullKey); ++ ++ if (!$this->getProvider()->exists($parent)) { ++ return false; ++ } ++ ++ $children = $this->getProvider()->getChildren($parent); ++ ++ if (null === $indexKey && !empty($children)) { ++ return true; ++ } ++ ++ foreach ($children as $childKey) { ++ $childIndex = $this->getIndex($childKey); ++ ++ if (null === $childIndex) { ++ continue; ++ } ++ ++ if ($childIndex < $indexKey) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++} +diff -Naur a/vendor/magento/framework/Lock/LockBackendFactory.php b/vendor/magento/framework/Lock/LockBackendFactory.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/LockBackendFactory.php +@@ -0,0 +1,111 @@ ++ DatabaseLock::class, ++ self::LOCK_ZOOKEEPER => ZookeeperLock::class, ++ self::LOCK_CACHE => CacheLock::class, ++ self::LOCK_FILE => FileLock::class, ++ ]; ++ ++ /** ++ * @param ObjectManagerInterface $objectManager The Object Manager instance ++ * @param DeploymentConfig $deploymentConfig The Application deployment configuration ++ */ ++ public function __construct( ++ ObjectManagerInterface $objectManager, ++ DeploymentConfig $deploymentConfig ++ ) { ++ $this->objectManager = $objectManager; ++ $this->deploymentConfig = $deploymentConfig; ++ } ++ ++ /** ++ * Creates an instance of LockManagerInterface using information from deployment config ++ * ++ * @return LockManagerInterface ++ * @throws RuntimeException ++ */ ++ public function create(): LockManagerInterface ++ { ++ $provider = $this->deploymentConfig->get('lock/provider', self::LOCK_DB); ++ $config = $this->deploymentConfig->get('lock/config', []); ++ ++ if (!isset($this->lockers[$provider])) { ++ throw new RuntimeException(new Phrase('Unknown locks provider: %1', [$provider])); ++ } ++ ++ if (self::LOCK_ZOOKEEPER === $provider && !extension_loaded(self::LOCK_ZOOKEEPER)) { ++ throw new RuntimeException(new Phrase('php extension Zookeeper is not installed.')); ++ } ++ ++ return $this->objectManager->create($this->lockers[$provider], $config); ++ } ++} +diff -Naur a/vendor/magento/framework/Lock/LockManagerInterface.php b/vendor/magento/framework/Lock/LockManagerInterface.php +--- a/vendor/magento/framework/Lock/LockManagerInterface.php ++++ b/vendor/magento/framework/Lock/LockManagerInterface.php +@@ -3,8 +3,8 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +- + declare(strict_types=1); ++ + namespace Magento\Framework\Lock; + + /** +diff -Naur a/vendor/magento/framework/Lock/Proxy.php b/vendor/magento/framework/Lock/Proxy.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/Proxy.php +@@ -0,0 +1,83 @@ ++factory = $factory; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function isLocked(string $name): bool ++ { ++ return $this->getLocker()->isLocked($name); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ return $this->getLocker()->lock($name, $timeout); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function unlock(string $name): bool ++ { ++ return $this->getLocker()->unlock($name); ++ } ++ ++ /** ++ * Gets LockManagerInterface implementation using Factory ++ * ++ * @return LockManagerInterface ++ * @throws RuntimeException ++ */ ++ private function getLocker(): LockManagerInterface ++ { ++ if (!$this->locker) { ++ $this->locker = $this->factory->create(); ++ } ++ ++ return $this->locker; ++ } ++} +diff -Naur a/setup/src/Magento/Setup/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList.php +@@ -44,7 +44,8 @@ class ConfigOptionsList implements ConfigOptionsListInterface + private $configOptionsListClasses = [ + \Magento\Setup\Model\ConfigOptionsList\Session::class, + \Magento\Setup\Model\ConfigOptionsList\Cache::class, +- \Magento\Setup\Model\ConfigOptionsList\PageCache::class ++ \Magento\Setup\Model\ConfigOptionsList\PageCache::class, ++ \Magento\Setup\Model\ConfigOptionsList\Lock::class, + ]; + + /** +diff -Naur a/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php +--- /dev/null ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php +@@ -0,0 +1,342 @@ ++ [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_DB_PREFIX => self::CONFIG_PATH_LOCK_DB_PREFIX, ++ ], ++ LockBackendFactory::LOCK_ZOOKEEPER => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_HOST => self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ ], ++ LockBackendFactory::LOCK_CACHE => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ ], ++ LockBackendFactory::LOCK_FILE => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_FILE_PATH => self::CONFIG_PATH_LOCK_FILE_PATH, ++ ], ++ ]; ++ ++ /** ++ * The list of default values ++ * ++ * @var array ++ */ ++ private $defaultConfigValues = [ ++ self::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, ++ self::INPUT_KEY_LOCK_DB_PREFIX => null, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => ZookeeperLock::DEFAULT_PATH, ++ ]; ++ ++ /** ++ * @inheritdoc ++ */ ++ public function getOptions() ++ { ++ return [ ++ new SelectConfigOption( ++ self::INPUT_KEY_LOCK_PROVIDER, ++ SelectConfigOption::FRONTEND_WIZARD_SELECT, ++ $this->validLockProviders, ++ self::CONFIG_PATH_LOCK_PROVIDER, ++ 'Lock provider name', ++ LockBackendFactory::LOCK_DB ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_DB_PREFIX, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_DB_PREFIX, ++ 'Installation specific lock prefix to avoid lock conflicts' ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_ZOOKEEPER_HOST, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ 'Host and port to connect to Zookeeper cluster. For example: 127.0.0.1:2181' ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ 'The path where Zookeeper will save locks. The default path is: ' . ZookeeperLock::DEFAULT_PATH ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_FILE_PATH, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_FILE_PATH, ++ 'The path where file locks will be saved.' ++ ), ++ ]; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function createConfig(array $options, DeploymentConfig $deploymentConfig) ++ { ++ $configData = new ConfigData(ConfigFilePool::APP_ENV); ++ $configData->setOverrideWhenSave(true); ++ $lockProvider = $this->getLockProvider($options, $deploymentConfig); ++ ++ $this->setDefaultConfiguration($configData, $deploymentConfig, $lockProvider); ++ ++ foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { ++ if (isset($options[$input])) { ++ $configData->set($path, $options[$input]); ++ } ++ } ++ ++ return $configData; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function validate(array $options, DeploymentConfig $deploymentConfig) ++ { ++ $lockProvider = $this->getLockProvider($options, $deploymentConfig); ++ switch ($lockProvider) { ++ case LockBackendFactory::LOCK_ZOOKEEPER: ++ $errors = $this->validateZookeeperConfig($options, $deploymentConfig); ++ break; ++ case LockBackendFactory::LOCK_FILE: ++ $errors = $this->validateFileConfig($options, $deploymentConfig); ++ break; ++ case LockBackendFactory::LOCK_CACHE: ++ case LockBackendFactory::LOCK_DB: ++ $errors = []; ++ break; ++ default: ++ $errors[] = 'The lock provider ' . $lockProvider . ' does not exist.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Validates File locks configuration ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return array ++ */ ++ private function validateFileConfig(array $options, DeploymentConfig $deploymentConfig): array ++ { ++ $errors = []; ++ ++ $path = $options[self::INPUT_KEY_LOCK_FILE_PATH] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_FILE_PATH, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_FILE_PATH) ++ ); ++ ++ if (!$path) { ++ $errors[] = 'The path needs to be a non-empty string.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Validates Zookeeper configuration ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return array ++ */ ++ private function validateZookeeperConfig(array $options, DeploymentConfig $deploymentConfig): array ++ { ++ $errors = []; ++ ++ if (!extension_loaded(LockBackendFactory::LOCK_ZOOKEEPER)) { ++ $errors[] = 'php extension Zookeeper is not installed.'; ++ } ++ ++ $host = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_HOST] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_HOST) ++ ); ++ $path = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_PATH] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_PATH) ++ ); ++ ++ if (!$path) { ++ $errors[] = 'Zookeeper path needs to be a non-empty string.'; ++ } ++ ++ if (!$host) { ++ $errors[] = 'Zookeeper host is should be set.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Returns the name of lock provider ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return string ++ */ ++ private function getLockProvider(array $options, DeploymentConfig $deploymentConfig): string ++ { ++ if (!isset($options[self::INPUT_KEY_LOCK_PROVIDER])) { ++ return (string) $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_PROVIDER, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_PROVIDER) ++ ); ++ } ++ ++ return (string) $options[self::INPUT_KEY_LOCK_PROVIDER]; ++ } ++ ++ /** ++ * Sets default configuration for locks ++ * ++ * @param ConfigData $configData ++ * @param DeploymentConfig $deploymentConfig ++ * @param string $lockProvider ++ * @return ConfigData ++ */ ++ private function setDefaultConfiguration( ++ ConfigData $configData, ++ DeploymentConfig $deploymentConfig, ++ string $lockProvider ++ ) { ++ foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { ++ $configData->set($path, $deploymentConfig->get($path, $this->getDefaultValue($input))); ++ } ++ ++ return $configData; ++ } ++ ++ /** ++ * Returns default value by input key ++ * ++ * If default value is not set returns null ++ * ++ * @param string $inputKey ++ * @return mixed|null ++ */ ++ private function getDefaultValue(string $inputKey) ++ { ++ if (isset($this->defaultConfigValues[$inputKey])) { ++ return $this->defaultConfigValues[$inputKey]; ++ } else { ++ return null; ++ } ++ } ++} diff --git a/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch b/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch new file mode 100644 index 00000000..f364ba3c --- /dev/null +++ b/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch @@ -0,0 +1,1055 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -38,7 +38,7 @@ + + + +- ++ + + + +diff -Naur a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/Backend/FileLock.php +@@ -0,0 +1,194 @@ ++fileDriver = $fileDriver; ++ $this->path = rtrim($path, '/') . '/'; ++ ++ try { ++ if (!$this->fileDriver->isExists($this->path)) { ++ $this->fileDriver->createDirectory($this->path); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException( ++ new Phrase('Cannot create the directory for locks: %1', [$this->path]), ++ $exception ++ ); ++ } ++ } ++ ++ /** ++ * Acquires a lock by name ++ * ++ * @param string $name The lock name ++ * @param int $timeout Timeout in seconds. A negative timeout value means infinite timeout ++ * @return bool Returns true if the lock is acquired, otherwise returns false ++ * @throws RuntimeException Throws RuntimeException if cannot acquires the lock because FS problems ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ try { ++ $lockFile = $this->getLockPath($name); ++ $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); ++ $skipDeadline = $timeout < 0; ++ $deadline = microtime(true) + $timeout; ++ ++ while (!$this->tryToLock($fileResource)) { ++ if (!$skipDeadline && $deadline <= microtime(true)) { ++ $this->fileDriver->fileClose($fileResource); ++ return false; ++ } ++ usleep($this->sleepCycle); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException(new Phrase('Cannot acquire a lock.'), $exception); ++ } ++ ++ $this->locks[$lockFile] = $fileResource; ++ return true; ++ } ++ ++ /** ++ * Checks if a lock exists by name ++ * ++ * @param string $name The lock name ++ * @return bool Returns true if the lock exists, otherwise returns false ++ * @throws RuntimeException Throws RuntimeException if cannot check that the lock exists ++ */ ++ public function isLocked(string $name): bool ++ { ++ $lockFile = $this->getLockPath($name); ++ $result = false; ++ ++ try { ++ if ($this->fileDriver->isExists($lockFile)) { ++ $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); ++ if ($this->tryToLock($fileResource)) { ++ $result = false; ++ } else { ++ $result = true; ++ } ++ $this->fileDriver->fileClose($fileResource); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); ++ } ++ ++ return $result; ++ } ++ ++ /** ++ * Remove the lock by name ++ * ++ * @param string $name The lock name ++ * @return bool If the lock is removed returns true, otherwise returns false ++ */ ++ public function unlock(string $name): bool ++ { ++ $lockFile = $this->getLockPath($name); ++ ++ if (isset($this->locks[$lockFile]) && $this->tryToUnlock($this->locks[$lockFile])) { ++ unset($this->locks[$lockFile]); ++ return true; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * Returns the full path to the lock file by name ++ * ++ * @param string $name The lock name ++ * @return string The path to the lock file ++ */ ++ private function getLockPath(string $name): string ++ { ++ return $this->path . $name; ++ } ++ ++ /** ++ * Tries to lock a file resource ++ * ++ * @param resource $resource The file resource ++ * @return bool If the lock is acquired returns true, otherwise returns false ++ */ ++ private function tryToLock($resource): bool ++ { ++ try { ++ return $this->fileDriver->fileLock($resource, LOCK_EX | LOCK_NB); ++ } catch (FileSystemException $exception) { ++ return false; ++ } ++ } ++ ++ /** ++ * Tries to unlock a file resource ++ * ++ * @param resource $resource The file resource ++ * @return bool If the lock is removed returns true, otherwise returns false ++ */ ++ private function tryToUnlock($resource): bool ++ { ++ try { ++ return $this->fileDriver->fileLock($resource, LOCK_UN | LOCK_NB); ++ } catch (FileSystemException $exception) { ++ return false; ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/Lock/Backend/Zookeeper.php b/vendor/magento/framework/Lock/Backend/Zookeeper.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/Backend/Zookeeper.php +@@ -0,0 +1,280 @@ ++\Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']]; ++ ++ /** ++ * The mapping list of the lock name with the full lock path ++ * ++ * @var array ++ */ ++ private $locks = []; ++ ++ /** ++ * The default path to storage locks ++ */ ++ const DEFAULT_PATH = '/magento/locks'; ++ ++ /** ++ * @param string $host The host to connect to Zookeeper ++ * @param string $path The base path to locks in Zookeeper ++ * @throws RuntimeException ++ */ ++ public function __construct(string $host, string $path = self::DEFAULT_PATH) ++ { ++ if (!$path) { ++ throw new RuntimeException( ++ new Phrase('The path needs to be a non-empty string.') ++ ); ++ } ++ ++ if (!$host) { ++ throw new RuntimeException( ++ new Phrase('The host needs to be a non-empty string.') ++ ); ++ } ++ ++ $this->host = $host; ++ $this->path = rtrim($path, '/') . '/'; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * You can see the lock algorithm by the link ++ * @link https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks ++ * ++ * @throws RuntimeException ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ $skipDeadline = $timeout < 0; ++ $lockPath = $this->getFullPathToLock($name); ++ $deadline = microtime(true) + $timeout; ++ ++ if (!$this->checkAndCreateParentNode($lockPath)) { ++ throw new RuntimeException(new Phrase('Failed creating the path %1', [$lockPath])); ++ } ++ ++ $lockKey = $this->getProvider() ++ ->create($lockPath, '1', $this->acl, \Zookeeper::EPHEMERAL | \Zookeeper::SEQUENCE); ++ ++ if (!$lockKey) { ++ throw new RuntimeException(new Phrase('Failed creating lock %1', [$lockPath])); ++ } ++ ++ while ($this->isAnyLock($lockKey, $this->getIndex($lockKey))) { ++ if (!$skipDeadline && $deadline <= microtime(true)) { ++ $this->getProvider()->delete($lockKey); ++ return false; ++ } ++ ++ usleep($this->sleepCycle); ++ } ++ ++ $this->locks[$name] = $lockKey; ++ ++ return true; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function unlock(string $name): bool ++ { ++ if (!isset($this->locks[$name])) { ++ return false; ++ } ++ ++ return $this->getProvider()->delete($this->locks[$name]); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function isLocked(string $name): bool ++ { ++ return $this->isAnyLock($this->getFullPathToLock($name)); ++ } ++ ++ /** ++ * Gets full path to lock by its name ++ * ++ * @param string $name ++ * @return string ++ */ ++ private function getFullPathToLock(string $name): string ++ { ++ return $this->path . $name . '/' . $this->lockName; ++ } ++ ++ /** ++ * Initiolizes and returns Zookeeper provider ++ * ++ * @return \Zookeeper ++ * @throws RuntimeException ++ */ ++ private function getProvider(): \Zookeeper ++ { ++ if (!$this->zookeeper) { ++ $this->zookeeper = new \Zookeeper($this->host); ++ } ++ ++ $deadline = microtime(true) + $this->connectionTimeout; ++ while ($this->zookeeper->getState() != \Zookeeper::CONNECTED_STATE) { ++ if ($deadline <= microtime(true)) { ++ throw new RuntimeException(new Phrase('Zookeeper connection timed out!')); ++ } ++ usleep($this->sleepCycle); ++ } ++ ++ return $this->zookeeper; ++ } ++ ++ /** ++ * Checks and creates base path recursively ++ * ++ * @param string $path ++ * @return bool ++ * @throws RuntimeException ++ */ ++ private function checkAndCreateParentNode(string $path): bool ++ { ++ $path = dirname($path); ++ if ($this->getProvider()->exists($path)) { ++ return true; ++ } ++ ++ if (!$this->checkAndCreateParentNode($path)) { ++ return false; ++ } ++ ++ if ($this->getProvider()->create($path, '1', $this->acl)) { ++ return true; ++ } ++ ++ return $this->getProvider()->exists($path); ++ } ++ ++ /** ++ * Gets int increment of lock key ++ * ++ * @param string $key ++ * @return int|null ++ */ ++ private function getIndex(string $key) ++ { ++ if (!preg_match('/' . $this->lockName . '([0-9]+)$/', $key, $matches)) { ++ return null; ++ } ++ ++ return intval($matches[1]); ++ } ++ ++ /** ++ * Checks if there is any sequence node under parent of $fullKey. ++ * ++ * At first checks that the $fullKey node is present, if not - returns false. ++ * If $indexKey is non-null and there is a smaller index than $indexKey then returns true, ++ * otherwise returns false. ++ * ++ * @param string $fullKey The full path without any sequence info ++ * @param int|null $indexKey The index to compare ++ * @return bool ++ * @throws RuntimeException ++ */ ++ private function isAnyLock(string $fullKey, int $indexKey = null): bool ++ { ++ $parent = dirname($fullKey); ++ ++ if (!$this->getProvider()->exists($parent)) { ++ return false; ++ } ++ ++ $children = $this->getProvider()->getChildren($parent); ++ ++ if (null === $indexKey && !empty($children)) { ++ return true; ++ } ++ ++ foreach ($children as $childKey) { ++ $childIndex = $this->getIndex($childKey); ++ ++ if (null === $childIndex) { ++ continue; ++ } ++ ++ if ($childIndex < $indexKey) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++} +diff -Naur a/vendor/magento/framework/Lock/LockBackendFactory.php b/vendor/magento/framework/Lock/LockBackendFactory.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/LockBackendFactory.php +@@ -0,0 +1,111 @@ ++ DatabaseLock::class, ++ self::LOCK_ZOOKEEPER => ZookeeperLock::class, ++ self::LOCK_CACHE => CacheLock::class, ++ self::LOCK_FILE => FileLock::class, ++ ]; ++ ++ /** ++ * @param ObjectManagerInterface $objectManager The Object Manager instance ++ * @param DeploymentConfig $deploymentConfig The Application deployment configuration ++ */ ++ public function __construct( ++ ObjectManagerInterface $objectManager, ++ DeploymentConfig $deploymentConfig ++ ) { ++ $this->objectManager = $objectManager; ++ $this->deploymentConfig = $deploymentConfig; ++ } ++ ++ /** ++ * Creates an instance of LockManagerInterface using information from deployment config ++ * ++ * @return LockManagerInterface ++ * @throws RuntimeException ++ */ ++ public function create(): LockManagerInterface ++ { ++ $provider = $this->deploymentConfig->get('lock/provider', self::LOCK_DB); ++ $config = $this->deploymentConfig->get('lock/config', []); ++ ++ if (!isset($this->lockers[$provider])) { ++ throw new RuntimeException(new Phrase('Unknown locks provider: %1', [$provider])); ++ } ++ ++ if (self::LOCK_ZOOKEEPER === $provider && !extension_loaded(self::LOCK_ZOOKEEPER)) { ++ throw new RuntimeException(new Phrase('php extension Zookeeper is not installed.')); ++ } ++ ++ return $this->objectManager->create($this->lockers[$provider], $config); ++ } ++} +diff -Naur a/vendor/magento/framework/Lock/Proxy.php b/vendor/magento/framework/Lock/Proxy.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/Proxy.php +@@ -0,0 +1,83 @@ ++factory = $factory; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function isLocked(string $name): bool ++ { ++ return $this->getLocker()->isLocked($name); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ return $this->getLocker()->lock($name, $timeout); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function unlock(string $name): bool ++ { ++ return $this->getLocker()->unlock($name); ++ } ++ ++ /** ++ * Gets LockManagerInterface implementation using Factory ++ * ++ * @return LockManagerInterface ++ * @throws RuntimeException ++ */ ++ private function getLocker(): LockManagerInterface ++ { ++ if (!$this->locker) { ++ $this->locker = $this->factory->create(); ++ } ++ ++ return $this->locker; ++ } ++} +diff -Naur a/setup/src/Magento/Setup/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList.php +@@ -50,7 +50,8 @@ class ConfigOptionsList implements ConfigOptionsListInterface + private $configOptionsListClasses = [ + \Magento\Setup\Model\ConfigOptionsList\Session::class, + \Magento\Setup\Model\ConfigOptionsList\Cache::class, +- \Magento\Setup\Model\ConfigOptionsList\PageCache::class ++ \Magento\Setup\Model\ConfigOptionsList\PageCache::class, ++ \Magento\Setup\Model\ConfigOptionsList\Lock::class, + ]; + + /** +diff -Naur a/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php +--- /dev/null ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php +@@ -0,0 +1,342 @@ ++ [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_DB_PREFIX => self::CONFIG_PATH_LOCK_DB_PREFIX, ++ ], ++ LockBackendFactory::LOCK_ZOOKEEPER => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_HOST => self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ ], ++ LockBackendFactory::LOCK_CACHE => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ ], ++ LockBackendFactory::LOCK_FILE => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_FILE_PATH => self::CONFIG_PATH_LOCK_FILE_PATH, ++ ], ++ ]; ++ ++ /** ++ * The list of default values ++ * ++ * @var array ++ */ ++ private $defaultConfigValues = [ ++ self::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, ++ self::INPUT_KEY_LOCK_DB_PREFIX => null, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => ZookeeperLock::DEFAULT_PATH, ++ ]; ++ ++ /** ++ * @inheritdoc ++ */ ++ public function getOptions() ++ { ++ return [ ++ new SelectConfigOption( ++ self::INPUT_KEY_LOCK_PROVIDER, ++ SelectConfigOption::FRONTEND_WIZARD_SELECT, ++ $this->validLockProviders, ++ self::CONFIG_PATH_LOCK_PROVIDER, ++ 'Lock provider name', ++ LockBackendFactory::LOCK_DB ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_DB_PREFIX, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_DB_PREFIX, ++ 'Installation specific lock prefix to avoid lock conflicts' ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_ZOOKEEPER_HOST, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ 'Host and port to connect to Zookeeper cluster. For example: 127.0.0.1:2181' ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ 'The path where Zookeeper will save locks. The default path is: ' . ZookeeperLock::DEFAULT_PATH ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_FILE_PATH, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_FILE_PATH, ++ 'The path where file locks will be saved.' ++ ), ++ ]; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function createConfig(array $options, DeploymentConfig $deploymentConfig) ++ { ++ $configData = new ConfigData(ConfigFilePool::APP_ENV); ++ $configData->setOverrideWhenSave(true); ++ $lockProvider = $this->getLockProvider($options, $deploymentConfig); ++ ++ $this->setDefaultConfiguration($configData, $deploymentConfig, $lockProvider); ++ ++ foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { ++ if (isset($options[$input])) { ++ $configData->set($path, $options[$input]); ++ } ++ } ++ ++ return $configData; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function validate(array $options, DeploymentConfig $deploymentConfig) ++ { ++ $lockProvider = $this->getLockProvider($options, $deploymentConfig); ++ switch ($lockProvider) { ++ case LockBackendFactory::LOCK_ZOOKEEPER: ++ $errors = $this->validateZookeeperConfig($options, $deploymentConfig); ++ break; ++ case LockBackendFactory::LOCK_FILE: ++ $errors = $this->validateFileConfig($options, $deploymentConfig); ++ break; ++ case LockBackendFactory::LOCK_CACHE: ++ case LockBackendFactory::LOCK_DB: ++ $errors = []; ++ break; ++ default: ++ $errors[] = 'The lock provider ' . $lockProvider . ' does not exist.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Validates File locks configuration ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return array ++ */ ++ private function validateFileConfig(array $options, DeploymentConfig $deploymentConfig): array ++ { ++ $errors = []; ++ ++ $path = $options[self::INPUT_KEY_LOCK_FILE_PATH] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_FILE_PATH, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_FILE_PATH) ++ ); ++ ++ if (!$path) { ++ $errors[] = 'The path needs to be a non-empty string.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Validates Zookeeper configuration ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return array ++ */ ++ private function validateZookeeperConfig(array $options, DeploymentConfig $deploymentConfig): array ++ { ++ $errors = []; ++ ++ if (!extension_loaded(LockBackendFactory::LOCK_ZOOKEEPER)) { ++ $errors[] = 'php extension Zookeeper is not installed.'; ++ } ++ ++ $host = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_HOST] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_HOST) ++ ); ++ $path = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_PATH] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_PATH) ++ ); ++ ++ if (!$path) { ++ $errors[] = 'Zookeeper path needs to be a non-empty string.'; ++ } ++ ++ if (!$host) { ++ $errors[] = 'Zookeeper host is should be set.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Returns the name of lock provider ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return string ++ */ ++ private function getLockProvider(array $options, DeploymentConfig $deploymentConfig): string ++ { ++ if (!isset($options[self::INPUT_KEY_LOCK_PROVIDER])) { ++ return (string) $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_PROVIDER, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_PROVIDER) ++ ); ++ } ++ ++ return (string) $options[self::INPUT_KEY_LOCK_PROVIDER]; ++ } ++ ++ /** ++ * Sets default configuration for locks ++ * ++ * @param ConfigData $configData ++ * @param DeploymentConfig $deploymentConfig ++ * @param string $lockProvider ++ * @return ConfigData ++ */ ++ private function setDefaultConfiguration( ++ ConfigData $configData, ++ DeploymentConfig $deploymentConfig, ++ string $lockProvider ++ ) { ++ foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { ++ $configData->set($path, $deploymentConfig->get($path, $this->getDefaultValue($input))); ++ } ++ ++ return $configData; ++ } ++ ++ /** ++ * Returns default value by input key ++ * ++ * If default value is not set returns null ++ * ++ * @param string $inputKey ++ * @return mixed|null ++ */ ++ private function getDefaultValue(string $inputKey) ++ { ++ if (isset($this->defaultConfigValues[$inputKey])) { ++ return $this->defaultConfigValues[$inputKey]; ++ } else { ++ return null; ++ } ++ } ++} diff --git a/patches/MAGECLOUD-3611__multi_thread_scd__2.2.0.patch b/patches/MAGECLOUD-3611__multi_thread_scd__2.2.0.patch new file mode 100644 index 00000000..171e72d9 --- /dev/null +++ b/patches/MAGECLOUD-3611__multi_thread_scd__2.2.0.patch @@ -0,0 +1,63 @@ +diff -Naur a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php +index e5e10c8f54a..85ef6514432 100644 +--- a/vendor/magento/module-deploy/Process/Queue.php ++++ b/vendor/magento/module-deploy/Process/Queue.php +@@ -291,12 +291,30 @@ class Queue + { + if ($this->isCanBeParalleled()) { + if ($package->getState() === null) { +- $pid = pcntl_waitpid($this->getPid($package), $status, WNOHANG); +- if ($pid === $this->getPid($package)) { ++ $pid = $this->getPid($package); ++ if ($pid === null) { ++ return false; ++ } ++ $result = pcntl_waitpid($pid, $status, WNOHANG); ++ if ($result === $pid) { + $package->setState(Package::STATE_COMPLETED); ++ $exitStatus = pcntl_wexitstatus($status); ++ $this->logger->info( ++ "Exited: " . $package->getPath() . "(status: $exitStatus)", ++ [ ++ 'process' => $package->getPath(), ++ 'status' => $exitStatus, ++ ] ++ ); + + unset($this->inProgress[$package->getPath()]); + return pcntl_wexitstatus($status) === 0; ++ } elseif ($result === -1) { ++ $errno = pcntl_errno(); ++ $strerror = pcntl_strerror($errno); ++ throw new \RuntimeException( ++ "Error encountered checking child process status (PID: $pid): $strerror (errno: $errno)" ++ ); + } + return false; + } +@@ -333,11 +351,23 @@ class Queue + public function __destruct() + { + foreach ($this->inProgress as $package) { +- if (pcntl_waitpid($this->getPid($package), $status) === -1) { ++ $pid = $this->getPid($package); ++ $this->logger->info( ++ "Reaping child process: {$package->getPath()} (PID: $pid)", ++ [ ++ 'process' => $package->getPath(), ++ 'pid' => $pid, ++ ] ++ ); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ if (pcntl_waitpid($pid, $status) === -1) { ++ $errno = pcntl_errno(); ++ $strerror = pcntl_strerror($errno); + throw new \RuntimeException( +- 'Error while waiting for package deployed: ' . $this->getPid($package) . '; Status: ' . $status ++ "Error encountered waiting for child process (PID: $pid): $strerror (errno: $errno)" + ); + } ++ + } + } + } diff --git a/patches/MAGECLOUD-3611__multi_thread_scd__2.2.4.patch b/patches/MAGECLOUD-3611__multi_thread_scd__2.2.4.patch new file mode 100644 index 00000000..5425daa4 --- /dev/null +++ b/patches/MAGECLOUD-3611__multi_thread_scd__2.2.4.patch @@ -0,0 +1,245 @@ +diff -Naur a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php +index d8089457ce5b..ca75bf1acb73 100644 +--- a/vendor/magento/module-deploy/Process/Queue.php ++++ b/vendor/magento/module-deploy/Process/Queue.php +@@ -3,14 +3,16 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++declare(strict_types=1); ++ + namespace Magento\Deploy\Process; + + use Magento\Deploy\Package\Package; + use Magento\Deploy\Service\DeployPackage; + use Magento\Framework\App\ResourceConnection; +-use Psr\Log\LoggerInterface; + use Magento\Framework\App\State as AppState; + use Magento\Framework\Locale\ResolverInterface as LocaleResolver; ++use Psr\Log\LoggerInterface; + + /** + * Deployment Queue +@@ -125,6 +127,8 @@ public function __construct( + } + + /** ++ * Adds deployment package. ++ * + * @param Package $package + * @param Package[] $dependencies + * @return bool true on success +@@ -140,6 +144,8 @@ public function add(Package $package, array $dependencies = []) + } + + /** ++ * Returns packages array. ++ * + * @return Package[] + */ + public function getPackages() +@@ -159,9 +165,11 @@ public function process() + $packages = $this->packages; + while (count($packages) && $this->checkTimeout()) { + foreach ($packages as $name => $packageJob) { ++ // Unsets each member of $packages array (passed by reference) as each is executed + $this->assertAndExecute($name, $packages, $packageJob); + } + $this->logger->info('.'); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction + sleep(3); + foreach ($this->inProgress as $name => $package) { + if ($this->isDeployed($package)) { +@@ -182,8 +190,6 @@ public function process() + * @param array $packages + * @param array $packageJob + * @return void +- * +- * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function assertAndExecute($name, array & $packages, array $packageJob) + { +@@ -207,13 +213,23 @@ private function assertAndExecute($name, array & $packages, array $packageJob) + } + } + } ++ $this->executePackage($package, $name, $packages, $dependenciesNotFinished); ++ } + ++ /** ++ * Executes deployment package. ++ * ++ * @param Package $package ++ * @param string $name ++ * @param array $packages ++ * @param bool $dependenciesNotFinished ++ * @return void ++ */ ++ private function executePackage(Package $package, string $name, array &$packages, bool $dependenciesNotFinished) ++ { + if (!$dependenciesNotFinished + && !$this->isDeployed($package) +- && ( +- $this->maxProcesses < 2 +- || (count($this->inProgress) < $this->maxProcesses) +- ) ++ && ($this->maxProcesses < 2 || (count($this->inProgress) < $this->maxProcesses)) + ) { + unset($packages[$name]); + $this->execute($package); +@@ -234,6 +250,7 @@ private function awaitForAllProcesses() + } + } + $this->logger->info('.'); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction + sleep(5); + } + if ($this->isCanBeParalleled()) { +@@ -243,6 +260,8 @@ private function awaitForAllProcesses() + } + + /** ++ * Checks if can be parallel. ++ * + * @return bool + */ + private function isCanBeParalleled() +@@ -251,9 +270,12 @@ private function isCanBeParalleled() + } + + /** ++ * Executes the process. ++ * + * @param Package $package + * @return bool true on success for main process and exit for child process + * @SuppressWarnings(PHPMD.ExitExpression) ++ * @throws \RuntimeException + */ + private function execute(Package $package) + { +@@ -281,6 +303,7 @@ function () use ($package) { + ); + + if ($this->isCanBeParalleled()) { ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction + $pid = pcntl_fork(); + if ($pid === -1) { + throw new \RuntimeException('Unable to fork a new process'); +@@ -295,6 +318,7 @@ function () use ($package) { + // process child process + $this->inProgress = []; + $this->deployPackageService->deploy($package, $this->options, true); ++ // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage + exit(0); + } else { + $this->deployPackageService->deploy($package, $this->options); +@@ -303,6 +327,8 @@ function () use ($package) { + } + + /** ++ * Checks if package is deployed. ++ * + * @param Package $package + * @return bool + */ +@@ -310,12 +336,41 @@ private function isDeployed(Package $package) + { + if ($this->isCanBeParalleled()) { + if ($package->getState() === null) { +- $pid = pcntl_waitpid($this->getPid($package), $status, WNOHANG); +- if ($pid === $this->getPid($package)) { ++ $pid = $this->getPid($package); ++ ++ // When $pid comes back as null the child process for this package has not yet started; prevents both ++ // hanging until timeout expires (which was behaviour in 2.2.x) and the type error from strict_types ++ if ($pid === null) { ++ return false; ++ } ++ ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $result = pcntl_waitpid($pid, $status, WNOHANG); ++ if ($result === $pid) { + $package->setState(Package::STATE_COMPLETED); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $exitStatus = pcntl_wexitstatus($status); ++ ++ $this->logger->info( ++ "Exited: " . $package->getPath() . "(status: $exitStatus)", ++ [ ++ 'process' => $package->getPath(), ++ 'status' => $exitStatus, ++ ] ++ ); + + unset($this->inProgress[$package->getPath()]); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction + return pcntl_wexitstatus($status) === 0; ++ } elseif ($result === -1) { ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $errno = pcntl_errno(); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $strerror = pcntl_strerror($errno); ++ ++ throw new \RuntimeException( ++ "Error encountered checking child process status (PID: $pid): $strerror (errno: $errno)" ++ ); + } + return false; + } +@@ -324,17 +379,19 @@ private function isDeployed(Package $package) + } + + /** ++ * Returns process ID or null if not found. ++ * + * @param Package $package + * @return int|null + */ + private function getPid(Package $package) + { +- return isset($this->processIds[$package->getPath()]) +- ? $this->processIds[$package->getPath()] +- : null; ++ return $this->processIds[$package->getPath()] ?? null; + } + + /** ++ * Checks timeout. ++ * + * @return bool + */ + private function checkTimeout() +@@ -347,14 +404,31 @@ private function checkTimeout() + * + * Protect against zombie process + * ++ * @throws \RuntimeException ++ * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @return void + */ + public function __destruct() + { + foreach ($this->inProgress as $package) { +- if (pcntl_waitpid($this->getPid($package), $status) === -1) { ++ $pid = $this->getPid($package); ++ $this->logger->info( ++ "Reaping child process: {$package->getPath()} (PID: $pid)", ++ [ ++ 'process' => $package->getPath(), ++ 'pid' => $pid, ++ ] ++ ); ++ ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ if (pcntl_waitpid($pid, $status) === -1) { ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $errno = pcntl_errno(); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $strerror = pcntl_strerror($errno); ++ + throw new \RuntimeException( +- 'Error while waiting for package deployed: ' . $this->getPid($package) . '; Status: ' . $status ++ "Error encountered waiting for child process (PID: $pid): $strerror (errno: $errno)" + ); + } + } diff --git a/patches/MAGECLOUD-3611__multi_thread_scd__2.3.0.patch b/patches/MAGECLOUD-3611__multi_thread_scd__2.3.0.patch new file mode 100644 index 00000000..3ba98d4b --- /dev/null +++ b/patches/MAGECLOUD-3611__multi_thread_scd__2.3.0.patch @@ -0,0 +1,63 @@ +diff -Naur a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php +index e5e10c8f54a..85ef6514432 100644 +--- a/vendor/magento/module-deploy/Process/Queue.php ++++ b/vendor/magento/module-deploy/Process/Queue.php +@@ -291,12 +291,30 @@ class Queue + { + if ($this->isCanBeParalleled()) { + if ($package->getState() === null) { +- $pid = pcntl_waitpid($this->getPid($package), $status, WNOHANG); +- if ($pid === $this->getPid($package)) { ++ $pid = $this->getPid($package); ++ if ($pid === null) { ++ return false; ++ } ++ $result = pcntl_waitpid($pid, $status, WNOHANG); ++ if ($result === $pid) { + $package->setState(Package::STATE_COMPLETED); ++ $exitStatus = pcntl_wexitstatus($status); ++ $this->logger->info( ++ "Exited: " . $package->getPath() . "(status: $exitStatus)", ++ [ ++ 'process' => $package->getPath(), ++ 'status' => $exitStatus, ++ ] ++ ); + + unset($this->inProgress[$package->getPath()]); + return pcntl_wexitstatus($status) === 0; ++ } elseif ($result === -1) { ++ $errno = pcntl_errno(); ++ $strerror = pcntl_strerror($errno); ++ throw new \RuntimeException( ++ "Error encountered checking child process status (PID: $pid): $strerror (errno: $errno)" ++ ); + } + return false; + } +@@ -333,11 +351,23 @@ class Queue + public function __destruct() + { + foreach ($this->inProgress as $package) { +- if (pcntl_waitpid($this->getPid($package), $status) === -1) { ++ $pid = $this->getPid($package); ++ $this->logger->info( ++ "Reaping child process: {$package->getPath()} (PID: $pid)", ++ [ ++ 'process' => $package->getPath(), ++ 'pid' => $pid, ++ ] ++ ); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ if (pcntl_waitpid($pid, $status) === -1) { ++ $errno = pcntl_errno(); ++ $strerror = pcntl_strerror($errno); + throw new \RuntimeException( +- 'Error while waiting for package deployed: ' . $this->getPid($package) . '; Status: ' . $status ++ "Error encountered waiting for child process (PID: $pid): $strerror (errno: $errno)" + ); + } ++ + } + } + } diff --git a/patches/MAGECLOUD-3611__multi_thread_scd__2.3.2.patch b/patches/MAGECLOUD-3611__multi_thread_scd__2.3.2.patch new file mode 100644 index 00000000..4bb7aa18 --- /dev/null +++ b/patches/MAGECLOUD-3611__multi_thread_scd__2.3.2.patch @@ -0,0 +1,71 @@ +diff -Naur a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php +index d7bb816e61c..c7fe02a4b02 100644 +--- a/vendor/magento/module-deploy/Process/Queue.php ++++ b/vendor/magento/module-deploy/Process/Queue.php +@@ -338,14 +338,37 @@ class Queue + { + if ($this->isCanBeParalleled()) { + if ($package->getState() === null) { ++ $pid = $this->getPid($package); ++ // When $pid comes back as null the child process for this package has not yet started; prevents both ++ // hanging until timeout expires (which was behaviour in 2.2.x) and the type error from strict_types ++ if ($pid === null) { ++ return false; ++ } + // phpcs:ignore Magento2.Functions.DiscouragedFunction +- $pid = pcntl_waitpid($this->getPid($package) ?? 0, $status, WNOHANG); +- if ($pid === $this->getPid($package)) { ++ $result = pcntl_waitpid($pid, $status, WNOHANG); ++ if ($result === $pid) { + $package->setState(Package::STATE_COMPLETED); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $exitStatus = pcntl_wexitstatus($status); ++ $this->logger->info( ++ "Exited: " . $package->getPath() . "(status: $exitStatus)", ++ [ ++ 'process' => $package->getPath(), ++ 'status' => $exitStatus, ++ ] ++ ); + + unset($this->inProgress[$package->getPath()]); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return pcntl_wexitstatus($status) === 0; ++ } elseif ($result === -1) { ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $errno = pcntl_errno(); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $strerror = pcntl_strerror($errno); ++ throw new \RuntimeException( ++ "Error encountered checking child process status (PID: $pid): $strerror (errno: $errno)" ++ ); + } + return false; + } +@@ -385,10 +408,24 @@ class Queue + public function __destruct() + { + foreach ($this->inProgress as $package) { ++ $pid = $this->getPid($package); ++ $this->logger->info( ++ "Reaping child process: {$package->getPath()} (PID: $pid)", ++ [ ++ 'process' => $package->getPath(), ++ 'pid' => $pid, ++ ] ++ ); ++ + // phpcs:ignore Magento2.Functions.DiscouragedFunction +- if (pcntl_waitpid($this->getPid($package), $status) === -1) { ++ if (pcntl_waitpid($pid, $status) === -1) { ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $errno = pcntl_errno(); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $strerror = pcntl_strerror($errno); ++ + throw new \RuntimeException( +- 'Error while waiting for package deployed: ' . $this->getPid($package) . '; Status: ' . $status ++ "Error encountered waiting for child process (PID: $pid): $strerror (errno: $errno)" + ); + } + } diff --git a/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.0.patch b/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.0.patch new file mode 100644 index 00000000..e8af178d --- /dev/null +++ b/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.0.patch @@ -0,0 +1,32 @@ +diff -Nuar a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +--- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +@@ -77,6 +77,7 @@ class UpgradeCommand extends AbstractSetupCommand + protected function execute(InputInterface $input, OutputInterface $output) + { + try { ++ $resultCode = \Magento\Framework\Console\Cli::RETURN_SUCCESS; + $keepGenerated = $input->getOption(self::INPUT_KEY_KEEP_GENERATED); + $installer = $this->installerFactory->create(new ConsoleLogger($output)); + $installer->updateModulesSequence($keepGenerated); +@@ -87,10 +88,10 @@ class UpgradeCommand extends AbstractSetupCommand + $importConfigCommand = $this->getApplication()->find(ConfigImportCommand::COMMAND_NAME); + $arrayInput = new ArrayInput([]); + $arrayInput->setInteractive($input->isInteractive()); +- $importConfigCommand->run($arrayInput, $output); ++ $resultCode = $importConfigCommand->run($arrayInput, $output); + } + +- if (!$keepGenerated) { ++ if ($resultCode !== \Magento\Framework\Console\Cli::RETURN_FAILURE && !$keepGenerated) { + $output->writeln( + 'Please re-run Magento compile command. Use the command "setup:di:compile"' + ); +@@ -100,6 +101,6 @@ class UpgradeCommand extends AbstractSetupCommand + return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + +- return \Magento\Framework\Console\Cli::RETURN_SUCCESS; ++ return $resultCode; + } + } diff --git a/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.1.patch b/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.1.patch new file mode 100644 index 00000000..78d77d84 --- /dev/null +++ b/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.1.patch @@ -0,0 +1,33 @@ +diff -Nuar a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +--- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +@@ -87,6 +87,7 @@ protected function configure() + protected function execute(InputInterface $input, OutputInterface $output) + { + try { ++ $resultCode = \Magento\Framework\Console\Cli::RETURN_SUCCESS; + $keepGenerated = $input->getOption(self::INPUT_KEY_KEEP_GENERATED); + $installer = $this->installerFactory->create(new ConsoleLogger($output)); + $installer->updateModulesSequence($keepGenerated); +@@ -97,10 +98,11 @@ protected function execute(InputInterface $input, OutputInterface $output) + $importConfigCommand = $this->getApplication()->find(ConfigImportCommand::COMMAND_NAME); + $arrayInput = new ArrayInput([]); + $arrayInput->setInteractive($input->isInteractive()); +- $importConfigCommand->run($arrayInput, $output); ++ $resultCode = $importConfigCommand->run($arrayInput, $output); + } + +- if (!$keepGenerated && $this->appState->getMode() === AppState::MODE_PRODUCTION) { ++ if ($resultCode !== \Magento\Framework\Console\Cli::RETURN_FAILURE ++ && !$keepGenerated && $this->appState->getMode() === AppState::MODE_PRODUCTION) { + $output->writeln( + 'Please re-run Magento compile command. Use the command "setup:di:compile"' + ); +@@ -110,6 +112,6 @@ protected function execute(InputInterface $input, OutputInterface $output) + return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + +- return \Magento\Framework\Console\Cli::RETURN_SUCCESS; ++ return $resultCode; + } + } diff --git a/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.3.0.patch b/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.3.0.patch new file mode 100644 index 00000000..e3b06dd3 --- /dev/null +++ b/patches/MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.3.0.patch @@ -0,0 +1,15 @@ +diff -Nuar a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +--- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +@@ -126,7 +126,10 @@ protected function execute(InputInterface $input, OutputInterface $output) + $importConfigCommand = $this->getApplication()->find(ConfigImportCommand::COMMAND_NAME); + $arrayInput = new ArrayInput([]); + $arrayInput->setInteractive($input->isInteractive()); +- $importConfigCommand->run($arrayInput, $output); ++ $result = $importConfigCommand->run($arrayInput, $output); ++ if ($result === \Magento\Framework\Console\Cli::RETURN_FAILURE) { ++ throw new \Magento\Framework\Exception\RuntimeException(__('%1 failed. See previous output.', ConfigImportCommand::COMMAND_NAME)); ++ } + } + + if (!$keepGenerated && $this->appState->getMode() === AppState::MODE_PRODUCTION) { diff --git a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.5.patch b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.5.patch new file mode 100644 index 00000000..a10280b6 --- /dev/null +++ b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.5.patch @@ -0,0 +1,573 @@ +diff -Nuar a/vendor/magento/framework/Lock/Backend/Database.php b/vendor/magento/framework/Lock/Backend/Database.php +--- a/vendor/magento/framework/Lock/Backend/Database.php ++++ b/vendor/magento/framework/Lock/Backend/Database.php +@@ -3,8 +3,8 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +- + declare(strict_types=1); ++ + namespace Magento\Framework\Lock\Backend; + + use Magento\Framework\App\DeploymentConfig; +@@ -14,20 +14,44 @@ use Magento\Framework\Exception\AlreadyExistsException; + use Magento\Framework\Exception\InputException; + use Magento\Framework\Phrase; + ++/** ++ * Implementation of the lock manager on the basis of MySQL. ++ */ + class Database implements \Magento\Framework\Lock\LockManagerInterface + { +- /** @var ResourceConnection */ ++ /** ++ * Max time for lock is 1 week ++ * ++ * MariaDB does not support negative timeout value to get infinite timeout, ++ * so we set 1 week for lock timeout ++ */ ++ const MAX_LOCK_TIME = 604800; ++ ++ /** ++ * @var ResourceConnection ++ */ + private $resource; + +- /** @var DeploymentConfig */ ++ /** ++ * @var DeploymentConfig ++ */ + private $deploymentConfig; + +- /** @var string Lock prefix */ ++ /** ++ * @var string Lock prefix ++ */ + private $prefix; + +- /** @var string|false Holds current lock name if set, otherwise false */ ++ /** ++ * @var string|false Holds current lock name if set, otherwise false ++ */ + private $currentLock = false; + ++ /** ++ * @param ResourceConnection $resource ++ * @param DeploymentConfig $deploymentConfig ++ * @param string|null $prefix ++ */ + public function __construct( + ResourceConnection $resource, + DeploymentConfig $deploymentConfig, +@@ -46,9 +70,13 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @return bool + * @throws InputException + * @throws AlreadyExistsException ++ * @throws \Zend_Db_Statement_Exception + */ + public function lock(string $name, int $timeout = -1): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return true; ++ }; + $name = $this->addPrefix($name); + + /** +@@ -59,7 +87,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + if ($this->currentLock) { + throw new AlreadyExistsException( + new Phrase( +- 'Current connection is already holding lock for $1, only single lock allowed', ++ 'Current connection is already holding lock for %1, only single lock allowed', + [$this->currentLock] + ) + ); +@@ -67,7 +95,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + + $result = (bool)$this->resource->getConnection()->query( + "SELECT GET_LOCK(?, ?);", +- [(string)$name, (int)$timeout] ++ [$name, $timeout < 0 ? self::MAX_LOCK_TIME : $timeout] + )->fetchColumn(); + + if ($result === true) { +@@ -83,9 +111,14 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @param string $name lock name + * @return bool + * @throws InputException ++ * @throws \Zend_Db_Statement_Exception + */ + public function unlock(string $name): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return true; ++ }; ++ + $name = $this->addPrefix($name); + + $result = (bool)$this->resource->getConnection()->query( +@@ -106,14 +139,19 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @param string $name lock name + * @return bool + * @throws InputException ++ * @throws \Zend_Db_Statement_Exception + */ + public function isLocked(string $name): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return false; ++ }; ++ + $name = $this->addPrefix($name); + + return (bool)$this->resource->getConnection()->query( + "SELECT IS_USED_LOCK(?);", +- [(string)$name] ++ [$name] + )->fetchColumn(); + } + +@@ -123,7 +161,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * Limited to 64 characters in MySQL. + * + * @param string $name +- * @return string $name ++ * @return string + * @throws InputException + */ + private function addPrefix(string $name): string +diff -Nuar a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +--- a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php ++++ b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +@@ -11,7 +11,7 @@ use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Framework\MessageQueue\ConsumerFactory; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Command for starting MessageQueue consumers. +@@ -22,6 +22,7 @@ class StartConsumerCommand extends Command + const OPTION_NUMBER_OF_MESSAGES = 'max-messages'; + const OPTION_BATCH_SIZE = 'batch-size'; + const OPTION_AREACODE = 'area-code'; ++ const OPTION_SINGLE_THREAD = 'single-thread'; + const PID_FILE_PATH = 'pid-file-path'; + const COMMAND_QUEUE_CONSUMERS_START = 'queue:consumers:start'; + +@@ -36,9 +37,9 @@ class StartConsumerCommand extends Command + private $appState; + + /** +- * @var PidConsumerManager ++ * @var LockManagerInterface + */ +- private $pidConsumerManager; ++ private $lockManager; + + /** + * StartConsumerCommand constructor. +@@ -47,23 +48,23 @@ class StartConsumerCommand extends Command + * @param \Magento\Framework\App\State $appState + * @param ConsumerFactory $consumerFactory + * @param string $name +- * @param PidConsumerManager $pidConsumerManager ++ * @param LockManagerInterface $lockManager + */ + public function __construct( + \Magento\Framework\App\State $appState, + ConsumerFactory $consumerFactory, + $name = null, +- PidConsumerManager $pidConsumerManager = null ++ LockManagerInterface $lockManager = null + ) { + $this->appState = $appState; + $this->consumerFactory = $consumerFactory; +- $this->pidConsumerManager = $pidConsumerManager ?: \Magento\Framework\App\ObjectManager::getInstance() +- ->get(PidConsumerManager::class); ++ $this->lockManager = $lockManager ?: \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(LockManagerInterface::class); + parent::__construct($name); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { +@@ -71,30 +72,36 @@ class StartConsumerCommand extends Command + $numberOfMessages = $input->getOption(self::OPTION_NUMBER_OF_MESSAGES); + $batchSize = (int)$input->getOption(self::OPTION_BATCH_SIZE); + $areaCode = $input->getOption(self::OPTION_AREACODE); +- $pidFilePath = $input->getOption(self::PID_FILE_PATH); + +- if ($pidFilePath && $this->pidConsumerManager->isRun($pidFilePath)) { +- $output->writeln('Consumer with the same PID is running'); +- return \Magento\Framework\Console\Cli::RETURN_FAILURE; ++ if ($input->getOption(self::PID_FILE_PATH)) { ++ $input->setOption(self::OPTION_SINGLE_THREAD, true); + } + +- if ($pidFilePath) { +- $this->pidConsumerManager->savePid($pidFilePath); ++ $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); ++ ++ if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore ++ $output->writeln('Consumer with the same name is running'); ++ return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + +- if ($areaCode !== null) { +- $this->appState->setAreaCode($areaCode); +- } else { +- $this->appState->setAreaCode('global'); ++ if ($singleThread) { ++ $this->lockManager->lock(md5($consumerName)); //phpcs:ignore + } + ++ $this->appState->setAreaCode($areaCode ?? 'global'); ++ + $consumer = $this->consumerFactory->get($consumerName, $batchSize); + $consumer->process($numberOfMessages); ++ ++ if ($singleThread) { ++ $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore ++ } ++ + return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function configure() + { +@@ -125,11 +132,17 @@ class StartConsumerCommand extends Command + 'The preferred area (global, adminhtml, etc...) ' + . 'default is global.' + ); ++ $this->addOption( ++ self::OPTION_SINGLE_THREAD, ++ null, ++ InputOption::VALUE_NONE, ++ 'This option prevents running multiple copies of one consumer simultaneously.' ++ ); + $this->addOption( + self::PID_FILE_PATH, + null, + InputOption::VALUE_REQUIRED, +- 'The file path for saving PID' ++ 'The file path for saving PID (This option is deprecated, use --single-thread instead)' + ); + $this->setHelp( + <<%command.full_name% someConsumer --area-code='adminhtml' ++ ++To do not run multiple copies of one consumer simultaneously: ++ ++ %command.full_name% someConsumer --single-thread' + +-To save PID enter path: ++To save PID enter path (This option is deprecated, use --single-thread instead): + + %command.full_name% someConsumer --pid-file-path='/var/someConsumer.pid' + HELP +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php ++++ b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +@@ -5,22 +5,21 @@ + */ + namespace Magento\MessageQueue\Model\Cron; + ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\MessageQueue\ConnectionTypeResolver; ++use Magento\Framework\MessageQueue\Consumer\Config\ConsumerConfigItemInterface; + use Magento\Framework\ShellInterface; + use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; + use Magento\Framework\App\DeploymentConfig; ++use Psr\Log\LoggerInterface; + use Symfony\Component\Process\PhpExecutableFinder; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Class for running consumers processes by cron + */ + class ConsumersRunner + { +- /** +- * Extension of PID file +- */ +- const PID_FILE_EXT = '.pid'; +- + /** + * Shell command line wrapper for executing command in background + * +@@ -50,11 +49,21 @@ class ConsumersRunner + private $phpExecutableFinder; + + /** +- * The class for checking status of process by PID ++ * @var ConnectionTypeResolver ++ */ ++ private $mqConnectionTypeResolver; ++ ++ /** ++ * @var LoggerInterface ++ */ ++ private $logger; ++ ++ /** ++ * Lock Manager + * +- * @var PidConsumerManager ++ * @var LockManagerInterface + */ +- private $pidConsumerManager; ++ private $lockManager; + + /** + * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed +@@ -62,20 +71,28 @@ class ConsumersRunner + * @param ConsumerConfigInterface $consumerConfig The consumer config provider + * @param DeploymentConfig $deploymentConfig The application deployment configuration + * @param ShellInterface $shellBackground The shell command line wrapper for executing command in background +- * @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID ++ * @param LockManagerInterface $lockManager The lock manager ++ * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver ++ * @param LoggerInterface $logger Logger + */ + public function __construct( + PhpExecutableFinder $phpExecutableFinder, + ConsumerConfigInterface $consumerConfig, + DeploymentConfig $deploymentConfig, + ShellInterface $shellBackground, +- PidConsumerManager $pidConsumerManager ++ LockManagerInterface $lockManager, ++ ConnectionTypeResolver $mqConnectionTypeResolver = null, ++ LoggerInterface $logger = null + ) { + $this->phpExecutableFinder = $phpExecutableFinder; + $this->consumerConfig = $consumerConfig; + $this->deploymentConfig = $deploymentConfig; + $this->shellBackground = $shellBackground; +- $this->pidConsumerManager = $pidConsumerManager; ++ $this->lockManager = $lockManager; ++ $this->mqConnectionTypeResolver = $mqConnectionTypeResolver ++ ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); ++ $this->logger = $logger ++ ?: ObjectManager::getInstance()->get(LoggerInterface::class); + } + + /** +@@ -94,15 +111,13 @@ class ConsumersRunner + $php = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($this->consumerConfig->getConsumers() as $consumer) { +- $consumerName = $consumer->getName(); +- +- if (!$this->canBeRun($consumerName, $allowedConsumers)) { ++ if (!$this->canBeRun($consumer, $allowedConsumers)) { + continue; + } + + $arguments = [ +- $consumerName, +- '--pid-file-path=' . $this->getPidFilePath($consumerName), ++ $consumer->getName(), ++ '--single-thread' + ]; + + if ($maxMessages) { +@@ -119,26 +134,38 @@ class ConsumersRunner + /** + * Checks that the consumer can be run + * +- * @param string $consumerName The consumer name ++ * @param ConsumerConfigItemInterface $consumerConfig The consumer config + * @param array $allowedConsumers The list of allowed consumers + * If $allowedConsumers is empty it means that all consumers are allowed + * @return bool Returns true if the consumer can be run ++ * @throws \Magento\Framework\Exception\FileSystemException + */ +- private function canBeRun($consumerName, array $allowedConsumers = []) ++ private function canBeRun(ConsumerConfigItemInterface $consumerConfig, array $allowedConsumers = []): bool + { +- $allowed = empty($allowedConsumers) ?: in_array($consumerName, $allowedConsumers); ++ $consumerName = $consumerConfig->getName(); ++ if (!empty($allowedConsumers) && !in_array($consumerName, $allowedConsumers)) { ++ return false; ++ } + +- return $allowed && !$this->pidConsumerManager->isRun($this->getPidFilePath($consumerName)); +- } ++ if ($this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore ++ return false; ++ } + +- /** +- * Returns default path to file with PID by consumers name +- * +- * @param string $consumerName The consumers name +- * @return string The path to file with PID +- */ +- private function getPidFilePath($consumerName) +- { +- return $consumerName . static::PID_FILE_EXT; ++ $connectionName = $consumerConfig->getConnection(); ++ try { ++ $this->mqConnectionTypeResolver->getConnectionType($connectionName); ++ } catch (\LogicException $e) { ++ $this->logger->info( ++ sprintf( ++ 'Consumer "%s" skipped as required connection "%s" is not configured. %s', ++ $consumerName, ++ $connectionName, ++ $e->getMessage() ++ ) ++ ); ++ return false; ++ } ++ ++ return true; + } + } +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +deleted file mode 100644 +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php ++++ /dev/null +@@ -1,127 +0,0 @@ +-filesystem = $filesystem; +- } +- +- /** +- * Checks if consumer process is run by pid from pidFile +- * +- * @param string $pidFilePath The path to file with PID +- * @return bool Returns true if consumer process is run +- * @throws FileSystemException +- */ +- public function isRun($pidFilePath) +- { +- $pid = $this->getPid($pidFilePath); +- if ($pid) { +- if (function_exists('posix_getpgid')) { +- return (bool) posix_getpgid($pid); +- } else { +- return $this->checkIsProcessExists($pid); +- } +- } +- +- return false; +- } +- +- /** +- * Checks that process is running +- * +- * If php function exec is not available throws RuntimeException +- * If shell command returns non-zero code and this code is not 1 throws RuntimeException +- * +- * @param int $pid A pid of process +- * @return bool Returns true if consumer process is run +- * @throws \RuntimeException +- * @SuppressWarnings(PHPMD.UnusedLocalVariable) +- */ +- private function checkIsProcessExists($pid) +- { +- if (!function_exists('exec')) { +- throw new \RuntimeException('Function exec is not available'); +- } +- +- exec(escapeshellcmd('ps -p ' . $pid), $output, $code); +- +- $code = (int) $code; +- +- switch ($code) { +- case 0: +- return true; +- break; +- case 1: +- return false; +- break; +- default: +- throw new \RuntimeException('Exec returned non-zero code', $code); +- break; +- } +- } +- +- /** +- * Returns pid by pidFile path +- * +- * @param string $pidFilePath The path to file with PID +- * @return int Returns pid if pid file exists for consumer else returns 0 +- * @throws FileSystemException +- */ +- public function getPid($pidFilePath) +- { +- /** @var ReadInterface $directory */ +- $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); +- +- if ($directory->isExist($pidFilePath)) { +- return (int) $directory->readFile($pidFilePath); +- } +- +- return 0; +- } +- +- /** +- * Saves pid of current process to file +- * +- * @param string $pidFilePath The path to file with pid +- * @throws FileSystemException +- */ +- public function savePid($pidFilePath) +- { +- /** @var WriteInterface $directory */ +- $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); +- $directory->writeFile($pidFilePath, function_exists('posix_getpid') ? posix_getpid() : getmypid(), 'w'); +- } +-} diff --git a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.6.patch b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.6.patch new file mode 100644 index 00000000..a4b3d37a --- /dev/null +++ b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.6.patch @@ -0,0 +1,572 @@ +diff -Nuar a/vendor/magento/framework/Lock/Backend/Database.php b/vendor/magento/framework/Lock/Backend/Database.php +--- a/vendor/magento/framework/Lock/Backend/Database.php ++++ b/vendor/magento/framework/Lock/Backend/Database.php +@@ -3,8 +3,8 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +- + declare(strict_types=1); ++ + namespace Magento\Framework\Lock\Backend; + + use Magento\Framework\App\DeploymentConfig; +@@ -14,23 +14,40 @@ use Magento\Framework\Exception\AlreadyExistsException; + use Magento\Framework\Exception\InputException; + use Magento\Framework\Phrase; + ++/** ++ * Implementation of the lock manager on the basis of MySQL. ++ */ + class Database implements \Magento\Framework\Lock\LockManagerInterface + { +- /** @var ResourceConnection */ ++ /** ++ * Max time for lock is 1 week ++ * ++ * MariaDB does not support negative timeout value to get infinite timeout, ++ * so we set 1 week for lock timeout ++ */ ++ const MAX_LOCK_TIME = 604800; ++ ++ /** ++ * @var ResourceConnection ++ */ + private $resource; + +- /** @var DeploymentConfig */ ++ /** ++ * @var DeploymentConfig ++ */ + private $deploymentConfig; + +- /** @var string Lock prefix */ ++ /** ++ * @var string Lock prefix ++ */ + private $prefix; + +- /** @var string|false Holds current lock name if set, otherwise false */ ++ /** ++ * @var string|false Holds current lock name if set, otherwise false ++ */ + private $currentLock = false; + + /** +- * Database constructor. +- * + * @param ResourceConnection $resource + * @param DeploymentConfig $deploymentConfig + * @param string|null $prefix +@@ -53,9 +70,13 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @return bool + * @throws InputException + * @throws AlreadyExistsException ++ * @throws \Zend_Db_Statement_Exception + */ + public function lock(string $name, int $timeout = -1): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return true; ++ }; + $name = $this->addPrefix($name); + + /** +@@ -66,7 +87,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + if ($this->currentLock) { + throw new AlreadyExistsException( + new Phrase( +- 'Current connection is already holding lock for $1, only single lock allowed', ++ 'Current connection is already holding lock for %1, only single lock allowed', + [$this->currentLock] + ) + ); +@@ -74,7 +95,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + + $result = (bool)$this->resource->getConnection()->query( + "SELECT GET_LOCK(?, ?);", +- [(string)$name, (int)$timeout] ++ [$name, $timeout < 0 ? self::MAX_LOCK_TIME : $timeout] + )->fetchColumn(); + + if ($result === true) { +@@ -90,9 +111,14 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @param string $name lock name + * @return bool + * @throws InputException ++ * @throws \Zend_Db_Statement_Exception + */ + public function unlock(string $name): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return true; ++ }; ++ + $name = $this->addPrefix($name); + + $result = (bool)$this->resource->getConnection()->query( +@@ -113,14 +139,19 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @param string $name lock name + * @return bool + * @throws InputException ++ * @throws \Zend_Db_Statement_Exception + */ + public function isLocked(string $name): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return false; ++ }; ++ + $name = $this->addPrefix($name); + + return (bool)$this->resource->getConnection()->query( + "SELECT IS_USED_LOCK(?);", +- [(string)$name] ++ [$name] + )->fetchColumn(); + } + +@@ -130,7 +161,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * Limited to 64 characters in MySQL. + * + * @param string $name +- * @return string $name ++ * @return string + * @throws InputException + */ + private function addPrefix(string $name): string +diff -Nuar a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +--- a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php ++++ b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +@@ -11,7 +11,7 @@ use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Framework\MessageQueue\ConsumerFactory; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Command for starting MessageQueue consumers. +@@ -22,6 +22,7 @@ class StartConsumerCommand extends Command + const OPTION_NUMBER_OF_MESSAGES = 'max-messages'; + const OPTION_BATCH_SIZE = 'batch-size'; + const OPTION_AREACODE = 'area-code'; ++ const OPTION_SINGLE_THREAD = 'single-thread'; + const PID_FILE_PATH = 'pid-file-path'; + const COMMAND_QUEUE_CONSUMERS_START = 'queue:consumers:start'; + +@@ -36,9 +37,9 @@ class StartConsumerCommand extends Command + private $appState; + + /** +- * @var PidConsumerManager ++ * @var LockManagerInterface + */ +- private $pidConsumerManager; ++ private $lockManager; + + /** + * StartConsumerCommand constructor. +@@ -47,23 +48,23 @@ class StartConsumerCommand extends Command + * @param \Magento\Framework\App\State $appState + * @param ConsumerFactory $consumerFactory + * @param string $name +- * @param PidConsumerManager $pidConsumerManager ++ * @param LockManagerInterface $lockManager + */ + public function __construct( + \Magento\Framework\App\State $appState, + ConsumerFactory $consumerFactory, + $name = null, +- PidConsumerManager $pidConsumerManager = null ++ LockManagerInterface $lockManager = null + ) { + $this->appState = $appState; + $this->consumerFactory = $consumerFactory; +- $this->pidConsumerManager = $pidConsumerManager ?: \Magento\Framework\App\ObjectManager::getInstance() +- ->get(PidConsumerManager::class); ++ $this->lockManager = $lockManager ?: \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(LockManagerInterface::class); + parent::__construct($name); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { +@@ -71,30 +72,36 @@ class StartConsumerCommand extends Command + $numberOfMessages = $input->getOption(self::OPTION_NUMBER_OF_MESSAGES); + $batchSize = (int)$input->getOption(self::OPTION_BATCH_SIZE); + $areaCode = $input->getOption(self::OPTION_AREACODE); +- $pidFilePath = $input->getOption(self::PID_FILE_PATH); + +- if ($pidFilePath && $this->pidConsumerManager->isRun($pidFilePath)) { +- $output->writeln('Consumer with the same PID is running'); +- return \Magento\Framework\Console\Cli::RETURN_FAILURE; ++ if ($input->getOption(self::PID_FILE_PATH)) { ++ $input->setOption(self::OPTION_SINGLE_THREAD, true); + } + +- if ($pidFilePath) { +- $this->pidConsumerManager->savePid($pidFilePath); ++ $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); ++ ++ if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore ++ $output->writeln('Consumer with the same name is running'); ++ return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + +- if ($areaCode !== null) { +- $this->appState->setAreaCode($areaCode); +- } else { +- $this->appState->setAreaCode('global'); ++ if ($singleThread) { ++ $this->lockManager->lock(md5($consumerName)); //phpcs:ignore + } + ++ $this->appState->setAreaCode($areaCode ?? 'global'); ++ + $consumer = $this->consumerFactory->get($consumerName, $batchSize); + $consumer->process($numberOfMessages); ++ ++ if ($singleThread) { ++ $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore ++ } ++ + return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function configure() + { +@@ -125,11 +132,17 @@ class StartConsumerCommand extends Command + 'The preferred area (global, adminhtml, etc...) ' + . 'default is global.' + ); ++ $this->addOption( ++ self::OPTION_SINGLE_THREAD, ++ null, ++ InputOption::VALUE_NONE, ++ 'This option prevents running multiple copies of one consumer simultaneously.' ++ ); + $this->addOption( + self::PID_FILE_PATH, + null, + InputOption::VALUE_REQUIRED, +- 'The file path for saving PID' ++ 'The file path for saving PID (This option is deprecated, use --single-thread instead)' + ); + $this->setHelp( + <<%command.full_name% someConsumer --area-code='adminhtml' ++ ++To do not run multiple copies of one consumer simultaneously: ++ ++ %command.full_name% someConsumer --single-thread' + +-To save PID enter path: ++To save PID enter path (This option is deprecated, use --single-thread instead): + + %command.full_name% someConsumer --pid-file-path='/var/someConsumer.pid' + HELP +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php ++++ b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +@@ -5,22 +5,21 @@ + */ + namespace Magento\MessageQueue\Model\Cron; + ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\MessageQueue\ConnectionTypeResolver; ++use Magento\Framework\MessageQueue\Consumer\Config\ConsumerConfigItemInterface; + use Magento\Framework\ShellInterface; + use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; + use Magento\Framework\App\DeploymentConfig; ++use Psr\Log\LoggerInterface; + use Symfony\Component\Process\PhpExecutableFinder; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Class for running consumers processes by cron + */ + class ConsumersRunner + { +- /** +- * Extension of PID file +- */ +- const PID_FILE_EXT = '.pid'; +- + /** + * Shell command line wrapper for executing command in background + * +@@ -50,11 +49,21 @@ class ConsumersRunner + private $phpExecutableFinder; + + /** +- * The class for checking status of process by PID ++ * @var ConnectionTypeResolver ++ */ ++ private $mqConnectionTypeResolver; ++ ++ /** ++ * @var LoggerInterface ++ */ ++ private $logger; ++ ++ /** ++ * Lock Manager + * +- * @var PidConsumerManager ++ * @var LockManagerInterface + */ +- private $pidConsumerManager; ++ private $lockManager; + + /** + * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed +@@ -62,20 +71,28 @@ class ConsumersRunner + * @param ConsumerConfigInterface $consumerConfig The consumer config provider + * @param DeploymentConfig $deploymentConfig The application deployment configuration + * @param ShellInterface $shellBackground The shell command line wrapper for executing command in background +- * @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID ++ * @param LockManagerInterface $lockManager The lock manager ++ * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver ++ * @param LoggerInterface $logger Logger + */ + public function __construct( + PhpExecutableFinder $phpExecutableFinder, + ConsumerConfigInterface $consumerConfig, + DeploymentConfig $deploymentConfig, + ShellInterface $shellBackground, +- PidConsumerManager $pidConsumerManager ++ LockManagerInterface $lockManager, ++ ConnectionTypeResolver $mqConnectionTypeResolver = null, ++ LoggerInterface $logger = null + ) { + $this->phpExecutableFinder = $phpExecutableFinder; + $this->consumerConfig = $consumerConfig; + $this->deploymentConfig = $deploymentConfig; + $this->shellBackground = $shellBackground; +- $this->pidConsumerManager = $pidConsumerManager; ++ $this->lockManager = $lockManager; ++ $this->mqConnectionTypeResolver = $mqConnectionTypeResolver ++ ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); ++ $this->logger = $logger ++ ?: ObjectManager::getInstance()->get(LoggerInterface::class); + } + + /** +@@ -94,15 +111,13 @@ class ConsumersRunner + $php = $this->phpExecutableFinder->find() ?: 'php'; + + foreach ($this->consumerConfig->getConsumers() as $consumer) { +- $consumerName = $consumer->getName(); +- +- if (!$this->canBeRun($consumerName, $allowedConsumers)) { ++ if (!$this->canBeRun($consumer, $allowedConsumers)) { + continue; + } + + $arguments = [ +- $consumerName, +- '--pid-file-path=' . $this->getPidFilePath($consumerName), ++ $consumer->getName(), ++ '--single-thread' + ]; + + if ($maxMessages) { +@@ -119,28 +134,38 @@ class ConsumersRunner + /** + * Checks that the consumer can be run + * +- * @param string $consumerName The consumer name ++ * @param ConsumerConfigItemInterface $consumerConfig The consumer config + * @param array $allowedConsumers The list of allowed consumers + * If $allowedConsumers is empty it means that all consumers are allowed + * @return bool Returns true if the consumer can be run ++ * @throws \Magento\Framework\Exception\FileSystemException + */ +- private function canBeRun($consumerName, array $allowedConsumers = []) ++ private function canBeRun(ConsumerConfigItemInterface $consumerConfig, array $allowedConsumers = []): bool + { +- $allowed = empty($allowedConsumers) ?: in_array($consumerName, $allowedConsumers); ++ $consumerName = $consumerConfig->getName(); ++ if (!empty($allowedConsumers) && !in_array($consumerName, $allowedConsumers)) { ++ return false; ++ } + +- return $allowed && !$this->pidConsumerManager->isRun($this->getPidFilePath($consumerName)); +- } ++ if ($this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore ++ return false; ++ } + +- /** +- * Returns default path to file with PID by consumers name +- * +- * @param string $consumerName The consumers name +- * @return string The path to file with PID +- */ +- private function getPidFilePath($consumerName) +- { +- $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); ++ $connectionName = $consumerConfig->getConnection(); ++ try { ++ $this->mqConnectionTypeResolver->getConnectionType($connectionName); ++ } catch (\LogicException $e) { ++ $this->logger->info( ++ sprintf( ++ 'Consumer "%s" skipped as required connection "%s" is not configured. %s', ++ $consumerName, ++ $connectionName, ++ $e->getMessage() ++ ) ++ ); ++ return false; ++ } + +- return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; ++ return true; + } + } +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +deleted file mode 100644 +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php ++++ /dev/null +@@ -1,127 +0,0 @@ +-filesystem = $filesystem; +- } +- +- /** +- * Checks if consumer process is run by pid from pidFile +- * +- * @param string $pidFilePath The path to file with PID +- * @return bool Returns true if consumer process is run +- * @throws FileSystemException +- */ +- public function isRun($pidFilePath) +- { +- $pid = $this->getPid($pidFilePath); +- if ($pid) { +- if (function_exists('posix_getpgid')) { +- return (bool) posix_getpgid($pid); +- } else { +- return $this->checkIsProcessExists($pid); +- } +- } +- +- return false; +- } +- +- /** +- * Checks that process is running +- * +- * If php function exec is not available throws RuntimeException +- * If shell command returns non-zero code and this code is not 1 throws RuntimeException +- * +- * @param int $pid A pid of process +- * @return bool Returns true if consumer process is run +- * @throws \RuntimeException +- * @SuppressWarnings(PHPMD.UnusedLocalVariable) +- */ +- private function checkIsProcessExists($pid) +- { +- if (!function_exists('exec')) { +- throw new \RuntimeException('Function exec is not available'); +- } +- +- exec(escapeshellcmd('ps -p ' . $pid), $output, $code); +- +- $code = (int) $code; +- +- switch ($code) { +- case 0: +- return true; +- break; +- case 1: +- return false; +- break; +- default: +- throw new \RuntimeException('Exec returned non-zero code', $code); +- break; +- } +- } +- +- /** +- * Returns pid by pidFile path +- * +- * @param string $pidFilePath The path to file with PID +- * @return int Returns pid if pid file exists for consumer else returns 0 +- * @throws FileSystemException +- */ +- public function getPid($pidFilePath) +- { +- /** @var ReadInterface $directory */ +- $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); +- +- if ($directory->isExist($pidFilePath)) { +- return (int) $directory->readFile($pidFilePath); +- } +- +- return 0; +- } +- +- /** +- * Saves pid of current process to file +- * +- * @param string $pidFilePath The path to file with pid +- * @throws FileSystemException +- */ +- public function savePid($pidFilePath) +- { +- /** @var WriteInterface $directory */ +- $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); +- $directory->writeFile($pidFilePath, function_exists('posix_getpid') ? posix_getpid() : getmypid(), 'w'); +- } +-} diff --git a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.7.patch b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.7.patch new file mode 100644 index 00000000..f7beda44 --- /dev/null +++ b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.7.patch @@ -0,0 +1,548 @@ +diff -Nuar a/vendor/magento/framework/Lock/Backend/Database.php b/vendor/magento/framework/Lock/Backend/Database.php +--- a/vendor/magento/framework/Lock/Backend/Database.php ++++ b/vendor/magento/framework/Lock/Backend/Database.php +@@ -3,8 +3,8 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +- + declare(strict_types=1); ++ + namespace Magento\Framework\Lock\Backend; + + use Magento\Framework\App\DeploymentConfig; +@@ -14,23 +14,40 @@ use Magento\Framework\Exception\AlreadyExistsException; + use Magento\Framework\Exception\InputException; + use Magento\Framework\Phrase; + ++/** ++ * Implementation of the lock manager on the basis of MySQL. ++ */ + class Database implements \Magento\Framework\Lock\LockManagerInterface + { +- /** @var ResourceConnection */ ++ /** ++ * Max time for lock is 1 week ++ * ++ * MariaDB does not support negative timeout value to get infinite timeout, ++ * so we set 1 week for lock timeout ++ */ ++ const MAX_LOCK_TIME = 604800; ++ ++ /** ++ * @var ResourceConnection ++ */ + private $resource; + +- /** @var DeploymentConfig */ ++ /** ++ * @var DeploymentConfig ++ */ + private $deploymentConfig; + +- /** @var string Lock prefix */ ++ /** ++ * @var string Lock prefix ++ */ + private $prefix; + +- /** @var string|false Holds current lock name if set, otherwise false */ ++ /** ++ * @var string|false Holds current lock name if set, otherwise false ++ */ + private $currentLock = false; + + /** +- * Database constructor. +- * + * @param ResourceConnection $resource + * @param DeploymentConfig $deploymentConfig + * @param string|null $prefix +@@ -53,9 +70,13 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @return bool + * @throws InputException + * @throws AlreadyExistsException ++ * @throws \Zend_Db_Statement_Exception + */ + public function lock(string $name, int $timeout = -1): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return true; ++ }; + $name = $this->addPrefix($name); + + /** +@@ -66,7 +87,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + if ($this->currentLock) { + throw new AlreadyExistsException( + new Phrase( +- 'Current connection is already holding lock for $1, only single lock allowed', ++ 'Current connection is already holding lock for %1, only single lock allowed', + [$this->currentLock] + ) + ); +@@ -74,7 +95,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + + $result = (bool)$this->resource->getConnection()->query( + "SELECT GET_LOCK(?, ?);", +- [(string)$name, (int)$timeout] ++ [$name, $timeout < 0 ? self::MAX_LOCK_TIME : $timeout] + )->fetchColumn(); + + if ($result === true) { +@@ -90,9 +111,14 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @param string $name lock name + * @return bool + * @throws InputException ++ * @throws \Zend_Db_Statement_Exception + */ + public function unlock(string $name): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return true; ++ }; ++ + $name = $this->addPrefix($name); + + $result = (bool)$this->resource->getConnection()->query( +@@ -113,14 +139,19 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @param string $name lock name + * @return bool + * @throws InputException ++ * @throws \Zend_Db_Statement_Exception + */ + public function isLocked(string $name): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return false; ++ }; ++ + $name = $this->addPrefix($name); + + return (bool)$this->resource->getConnection()->query( + "SELECT IS_USED_LOCK(?);", +- [(string)$name] ++ [$name] + )->fetchColumn(); + } + +@@ -130,7 +161,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * Limited to 64 characters in MySQL. + * + * @param string $name +- * @return string $name ++ * @return string + * @throws InputException + */ + private function addPrefix(string $name): string +diff -Nuar a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +--- a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php ++++ b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +@@ -11,7 +11,7 @@ use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Framework\MessageQueue\ConsumerFactory; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Command for starting MessageQueue consumers. +@@ -22,6 +22,7 @@ class StartConsumerCommand extends Command + const OPTION_NUMBER_OF_MESSAGES = 'max-messages'; + const OPTION_BATCH_SIZE = 'batch-size'; + const OPTION_AREACODE = 'area-code'; ++ const OPTION_SINGLE_THREAD = 'single-thread'; + const PID_FILE_PATH = 'pid-file-path'; + const COMMAND_QUEUE_CONSUMERS_START = 'queue:consumers:start'; + +@@ -36,9 +37,9 @@ class StartConsumerCommand extends Command + private $appState; + + /** +- * @var PidConsumerManager ++ * @var LockManagerInterface + */ +- private $pidConsumerManager; ++ private $lockManager; + + /** + * StartConsumerCommand constructor. +@@ -47,23 +48,23 @@ class StartConsumerCommand extends Command + * @param \Magento\Framework\App\State $appState + * @param ConsumerFactory $consumerFactory + * @param string $name +- * @param PidConsumerManager $pidConsumerManager ++ * @param LockManagerInterface $lockManager + */ + public function __construct( + \Magento\Framework\App\State $appState, + ConsumerFactory $consumerFactory, + $name = null, +- PidConsumerManager $pidConsumerManager = null ++ LockManagerInterface $lockManager = null + ) { + $this->appState = $appState; + $this->consumerFactory = $consumerFactory; +- $this->pidConsumerManager = $pidConsumerManager ?: \Magento\Framework\App\ObjectManager::getInstance() +- ->get(PidConsumerManager::class); ++ $this->lockManager = $lockManager ?: \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(LockManagerInterface::class); + parent::__construct($name); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { +@@ -71,30 +72,36 @@ class StartConsumerCommand extends Command + $numberOfMessages = $input->getOption(self::OPTION_NUMBER_OF_MESSAGES); + $batchSize = (int)$input->getOption(self::OPTION_BATCH_SIZE); + $areaCode = $input->getOption(self::OPTION_AREACODE); +- $pidFilePath = $input->getOption(self::PID_FILE_PATH); + +- if ($pidFilePath && $this->pidConsumerManager->isRun($pidFilePath)) { +- $output->writeln('Consumer with the same PID is running'); +- return \Magento\Framework\Console\Cli::RETURN_FAILURE; ++ if ($input->getOption(self::PID_FILE_PATH)) { ++ $input->setOption(self::OPTION_SINGLE_THREAD, true); + } + +- if ($pidFilePath) { +- $this->pidConsumerManager->savePid($pidFilePath); ++ $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); ++ ++ if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore ++ $output->writeln('Consumer with the same name is running'); ++ return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + +- if ($areaCode !== null) { +- $this->appState->setAreaCode($areaCode); +- } else { +- $this->appState->setAreaCode('global'); ++ if ($singleThread) { ++ $this->lockManager->lock(md5($consumerName)); //phpcs:ignore + } + ++ $this->appState->setAreaCode($areaCode ?? 'global'); ++ + $consumer = $this->consumerFactory->get($consumerName, $batchSize); + $consumer->process($numberOfMessages); ++ ++ if ($singleThread) { ++ $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore ++ } ++ + return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function configure() + { +@@ -125,11 +132,17 @@ class StartConsumerCommand extends Command + 'The preferred area (global, adminhtml, etc...) ' + . 'default is global.' + ); ++ $this->addOption( ++ self::OPTION_SINGLE_THREAD, ++ null, ++ InputOption::VALUE_NONE, ++ 'This option prevents running multiple copies of one consumer simultaneously.' ++ ); + $this->addOption( + self::PID_FILE_PATH, + null, + InputOption::VALUE_REQUIRED, +- 'The file path for saving PID' ++ 'The file path for saving PID (This option is deprecated, use --single-thread instead)' + ); + $this->setHelp( + <<%command.full_name% someConsumer --area-code='adminhtml' ++ ++To do not run multiple copies of one consumer simultaneously: ++ ++ %command.full_name% someConsumer --single-thread' + +-To save PID enter path: ++To save PID enter path (This option is deprecated, use --single-thread instead): + + %command.full_name% someConsumer --pid-file-path='/var/someConsumer.pid' + HELP +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php ++++ b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +@@ -13,18 +13,13 @@ use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInt + use Magento\Framework\App\DeploymentConfig; + use Psr\Log\LoggerInterface; + use Symfony\Component\Process\PhpExecutableFinder; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Class for running consumers processes by cron + */ + class ConsumersRunner + { +- /** +- * Extension of PID file +- */ +- const PID_FILE_EXT = '.pid'; +- + /** + * Shell command line wrapper for executing command in background + * +@@ -53,13 +48,6 @@ class ConsumersRunner + */ + private $phpExecutableFinder; + +- /** +- * The class for checking status of process by PID +- * +- * @var PidConsumerManager +- */ +- private $pidConsumerManager; +- + /** + * @var ConnectionTypeResolver + */ +@@ -70,13 +58,20 @@ class ConsumersRunner + */ + private $logger; + ++ /** ++ * Lock Manager ++ * ++ * @var LockManagerInterface ++ */ ++ private $lockManager; ++ + /** + * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed + * for the PHP executable + * @param ConsumerConfigInterface $consumerConfig The consumer config provider + * @param DeploymentConfig $deploymentConfig The application deployment configuration + * @param ShellInterface $shellBackground The shell command line wrapper for executing command in background +- * @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID ++ * @param LockManagerInterface $lockManager The lock manager + * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver + * @param LoggerInterface $logger Logger + */ +@@ -85,7 +80,7 @@ class ConsumersRunner + ConsumerConfigInterface $consumerConfig, + DeploymentConfig $deploymentConfig, + ShellInterface $shellBackground, +- PidConsumerManager $pidConsumerManager, ++ LockManagerInterface $lockManager, + ConnectionTypeResolver $mqConnectionTypeResolver = null, + LoggerInterface $logger = null + ) { +@@ -93,7 +88,7 @@ class ConsumersRunner + $this->consumerConfig = $consumerConfig; + $this->deploymentConfig = $deploymentConfig; + $this->shellBackground = $shellBackground; +- $this->pidConsumerManager = $pidConsumerManager; ++ $this->lockManager = $lockManager; + $this->mqConnectionTypeResolver = $mqConnectionTypeResolver + ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); + $this->logger = $logger +@@ -120,11 +115,9 @@ class ConsumersRunner + continue; + } + +- $consumerName = $consumer->getName(); +- + $arguments = [ +- $consumerName, +- '--pid-file-path=' . $this->getPidFilePath($consumerName), ++ $consumer->getName(), ++ '--single-thread' + ]; + + if ($maxMessages) { +@@ -154,7 +147,7 @@ class ConsumersRunner + return false; + } + +- if ($this->pidConsumerManager->isRun($this->getPidFilePath($consumerName))) { ++ if ($this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore + return false; + } + +@@ -162,28 +155,17 @@ class ConsumersRunner + try { + $this->mqConnectionTypeResolver->getConnectionType($connectionName); + } catch (\LogicException $e) { +- $this->logger->info(sprintf( +- 'Consumer "%s" skipped as required connection "%s" is not configured. %s', +- $consumerName, +- $connectionName, +- $e->getMessage() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Consumer "%s" skipped as required connection "%s" is not configured. %s', ++ $consumerName, ++ $connectionName, ++ $e->getMessage() ++ ) ++ ); + return false; + } + + return true; + } +- +- /** +- * Returns default path to file with PID by consumers name +- * +- * @param string $consumerName The consumers name +- * @return string The path to file with PID +- */ +- private function getPidFilePath($consumerName) +- { +- $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); +- +- return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; +- } + } +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +deleted file mode 100644 +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php ++++ /dev/null +@@ -1,127 +0,0 @@ +-filesystem = $filesystem; +- } +- +- /** +- * Checks if consumer process is run by pid from pidFile +- * +- * @param string $pidFilePath The path to file with PID +- * @return bool Returns true if consumer process is run +- * @throws FileSystemException +- */ +- public function isRun($pidFilePath) +- { +- $pid = $this->getPid($pidFilePath); +- if ($pid) { +- if (function_exists('posix_getpgid')) { +- return (bool) posix_getpgid($pid); +- } else { +- return $this->checkIsProcessExists($pid); +- } +- } +- +- return false; +- } +- +- /** +- * Checks that process is running +- * +- * If php function exec is not available throws RuntimeException +- * If shell command returns non-zero code and this code is not 1 throws RuntimeException +- * +- * @param int $pid A pid of process +- * @return bool Returns true if consumer process is run +- * @throws \RuntimeException +- * @SuppressWarnings(PHPMD.UnusedLocalVariable) +- */ +- private function checkIsProcessExists($pid) +- { +- if (!function_exists('exec')) { +- throw new \RuntimeException('Function exec is not available'); +- } +- +- exec(escapeshellcmd('ps -p ' . $pid), $output, $code); +- +- $code = (int) $code; +- +- switch ($code) { +- case 0: +- return true; +- break; +- case 1: +- return false; +- break; +- default: +- throw new \RuntimeException('Exec returned non-zero code', $code); +- break; +- } +- } +- +- /** +- * Returns pid by pidFile path +- * +- * @param string $pidFilePath The path to file with PID +- * @return int Returns pid if pid file exists for consumer else returns 0 +- * @throws FileSystemException +- */ +- public function getPid($pidFilePath) +- { +- /** @var ReadInterface $directory */ +- $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); +- +- if ($directory->isExist($pidFilePath)) { +- return (int) $directory->readFile($pidFilePath); +- } +- +- return 0; +- } +- +- /** +- * Saves pid of current process to file +- * +- * @param string $pidFilePath The path to file with pid +- * @throws FileSystemException +- */ +- public function savePid($pidFilePath) +- { +- /** @var WriteInterface $directory */ +- $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); +- $directory->writeFile($pidFilePath, function_exists('posix_getpid') ? posix_getpid() : getmypid(), 'w'); +- } +-} diff --git a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.8.patch b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.8.patch new file mode 100644 index 00000000..9be3c502 --- /dev/null +++ b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.8.patch @@ -0,0 +1,496 @@ +diff -Nuar a/vendor/magento/framework/Lock/Backend/Database.php b/vendor/magento/framework/Lock/Backend/Database.php +--- a/vendor/magento/framework/Lock/Backend/Database.php ++++ b/vendor/magento/framework/Lock/Backend/Database.php +@@ -19,21 +19,35 @@ use Magento\Framework\Phrase; + */ + class Database implements \Magento\Framework\Lock\LockManagerInterface + { +- /** @var ResourceConnection */ ++ /** ++ * Max time for lock is 1 week ++ * ++ * MariaDB does not support negative timeout value to get infinite timeout, ++ * so we set 1 week for lock timeout ++ */ ++ const MAX_LOCK_TIME = 604800; ++ ++ /** ++ * @var ResourceConnection ++ */ + private $resource; + +- /** @var DeploymentConfig */ ++ /** ++ * @var DeploymentConfig ++ */ + private $deploymentConfig; + +- /** @var string Lock prefix */ ++ /** ++ * @var string Lock prefix ++ */ + private $prefix; + +- /** @var string|false Holds current lock name if set, otherwise false */ ++ /** ++ * @var string|false Holds current lock name if set, otherwise false ++ */ + private $currentLock = false; + + /** +- * Database constructor. +- * + * @param ResourceConnection $resource + * @param DeploymentConfig $deploymentConfig + * @param string|null $prefix +@@ -81,7 +95,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + + $result = (bool)$this->resource->getConnection()->query( + "SELECT GET_LOCK(?, ?);", +- [(string)$name, (int)$timeout] ++ [$name, $timeout < 0 ? self::MAX_LOCK_TIME : $timeout] + )->fetchColumn(); + + if ($result === true) { +@@ -104,6 +118,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + if (!$this->deploymentConfig->isDbAvailable()) { + return true; + }; ++ + $name = $this->addPrefix($name); + + $result = (bool)$this->resource->getConnection()->query( +@@ -131,11 +146,12 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + if (!$this->deploymentConfig->isDbAvailable()) { + return false; + }; ++ + $name = $this->addPrefix($name); + + return (bool)$this->resource->getConnection()->query( + "SELECT IS_USED_LOCK(?);", +- [(string)$name] ++ [$name] + )->fetchColumn(); + } + +@@ -145,7 +161,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * Limited to 64 characters in MySQL. + * + * @param string $name +- * @return string $name ++ * @return string + * @throws InputException + */ + private function addPrefix(string $name): string +diff -Nuar a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +--- a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php ++++ b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +@@ -11,7 +11,7 @@ use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Framework\MessageQueue\ConsumerFactory; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Command for starting MessageQueue consumers. +@@ -22,6 +22,7 @@ class StartConsumerCommand extends Command + const OPTION_NUMBER_OF_MESSAGES = 'max-messages'; + const OPTION_BATCH_SIZE = 'batch-size'; + const OPTION_AREACODE = 'area-code'; ++ const OPTION_SINGLE_THREAD = 'single-thread'; + const PID_FILE_PATH = 'pid-file-path'; + const COMMAND_QUEUE_CONSUMERS_START = 'queue:consumers:start'; + +@@ -36,9 +37,9 @@ class StartConsumerCommand extends Command + private $appState; + + /** +- * @var PidConsumerManager ++ * @var LockManagerInterface + */ +- private $pidConsumerManager; ++ private $lockManager; + + /** + * StartConsumerCommand constructor. +@@ -47,23 +48,23 @@ class StartConsumerCommand extends Command + * @param \Magento\Framework\App\State $appState + * @param ConsumerFactory $consumerFactory + * @param string $name +- * @param PidConsumerManager $pidConsumerManager ++ * @param LockManagerInterface $lockManager + */ + public function __construct( + \Magento\Framework\App\State $appState, + ConsumerFactory $consumerFactory, + $name = null, +- PidConsumerManager $pidConsumerManager = null ++ LockManagerInterface $lockManager = null + ) { + $this->appState = $appState; + $this->consumerFactory = $consumerFactory; +- $this->pidConsumerManager = $pidConsumerManager ?: \Magento\Framework\App\ObjectManager::getInstance() +- ->get(PidConsumerManager::class); ++ $this->lockManager = $lockManager ?: \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(LockManagerInterface::class); + parent::__construct($name); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { +@@ -71,30 +72,36 @@ class StartConsumerCommand extends Command + $numberOfMessages = $input->getOption(self::OPTION_NUMBER_OF_MESSAGES); + $batchSize = (int)$input->getOption(self::OPTION_BATCH_SIZE); + $areaCode = $input->getOption(self::OPTION_AREACODE); +- $pidFilePath = $input->getOption(self::PID_FILE_PATH); + +- if ($pidFilePath && $this->pidConsumerManager->isRun($pidFilePath)) { +- $output->writeln('Consumer with the same PID is running'); +- return \Magento\Framework\Console\Cli::RETURN_FAILURE; ++ if ($input->getOption(self::PID_FILE_PATH)) { ++ $input->setOption(self::OPTION_SINGLE_THREAD, true); + } + +- if ($pidFilePath) { +- $this->pidConsumerManager->savePid($pidFilePath); ++ $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); ++ ++ if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore ++ $output->writeln('Consumer with the same name is running'); ++ return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + +- if ($areaCode !== null) { +- $this->appState->setAreaCode($areaCode); +- } else { +- $this->appState->setAreaCode('global'); ++ if ($singleThread) { ++ $this->lockManager->lock(md5($consumerName)); //phpcs:ignore + } + ++ $this->appState->setAreaCode($areaCode ?? 'global'); ++ + $consumer = $this->consumerFactory->get($consumerName, $batchSize); + $consumer->process($numberOfMessages); ++ ++ if ($singleThread) { ++ $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore ++ } ++ + return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function configure() + { +@@ -125,11 +132,17 @@ class StartConsumerCommand extends Command + 'The preferred area (global, adminhtml, etc...) ' + . 'default is global.' + ); ++ $this->addOption( ++ self::OPTION_SINGLE_THREAD, ++ null, ++ InputOption::VALUE_NONE, ++ 'This option prevents running multiple copies of one consumer simultaneously.' ++ ); + $this->addOption( + self::PID_FILE_PATH, + null, + InputOption::VALUE_REQUIRED, +- 'The file path for saving PID' ++ 'The file path for saving PID (This option is deprecated, use --single-thread instead)' + ); + $this->setHelp( + <<%command.full_name% someConsumer --area-code='adminhtml' ++ ++To do not run multiple copies of one consumer simultaneously: ++ ++ %command.full_name% someConsumer --single-thread' + +-To save PID enter path: ++To save PID enter path (This option is deprecated, use --single-thread instead): + + %command.full_name% someConsumer --pid-file-path='/var/someConsumer.pid' + HELP +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php ++++ b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +@@ -13,18 +13,13 @@ use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInt + use Magento\Framework\App\DeploymentConfig; + use Psr\Log\LoggerInterface; + use Symfony\Component\Process\PhpExecutableFinder; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Class for running consumers processes by cron + */ + class ConsumersRunner + { +- /** +- * Extension of PID file +- */ +- const PID_FILE_EXT = '.pid'; +- + /** + * Shell command line wrapper for executing command in background + * +@@ -53,13 +48,6 @@ class ConsumersRunner + */ + private $phpExecutableFinder; + +- /** +- * The class for checking status of process by PID +- * +- * @var PidConsumerManager +- */ +- private $pidConsumerManager; +- + /** + * @var ConnectionTypeResolver + */ +@@ -70,13 +58,20 @@ class ConsumersRunner + */ + private $logger; + ++ /** ++ * Lock Manager ++ * ++ * @var LockManagerInterface ++ */ ++ private $lockManager; ++ + /** + * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed + * for the PHP executable + * @param ConsumerConfigInterface $consumerConfig The consumer config provider + * @param DeploymentConfig $deploymentConfig The application deployment configuration + * @param ShellInterface $shellBackground The shell command line wrapper for executing command in background +- * @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID ++ * @param LockManagerInterface $lockManager The lock manager + * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver + * @param LoggerInterface $logger Logger + */ +@@ -85,7 +80,7 @@ class ConsumersRunner + ConsumerConfigInterface $consumerConfig, + DeploymentConfig $deploymentConfig, + ShellInterface $shellBackground, +- PidConsumerManager $pidConsumerManager, ++ LockManagerInterface $lockManager, + ConnectionTypeResolver $mqConnectionTypeResolver = null, + LoggerInterface $logger = null + ) { +@@ -93,7 +88,7 @@ class ConsumersRunner + $this->consumerConfig = $consumerConfig; + $this->deploymentConfig = $deploymentConfig; + $this->shellBackground = $shellBackground; +- $this->pidConsumerManager = $pidConsumerManager; ++ $this->lockManager = $lockManager; + $this->mqConnectionTypeResolver = $mqConnectionTypeResolver + ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); + $this->logger = $logger +@@ -120,11 +115,9 @@ class ConsumersRunner + continue; + } + +- $consumerName = $consumer->getName(); +- + $arguments = [ +- $consumerName, +- '--pid-file-path=' . $this->getPidFilePath($consumerName), ++ $consumer->getName(), ++ '--single-thread' + ]; + + if ($maxMessages) { +@@ -154,7 +147,7 @@ class ConsumersRunner + return false; + } + +- if ($this->pidConsumerManager->isRun($this->getPidFilePath($consumerName))) { ++ if ($this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore + return false; + } + +@@ -162,28 +155,17 @@ class ConsumersRunner + try { + $this->mqConnectionTypeResolver->getConnectionType($connectionName); + } catch (\LogicException $e) { +- $this->logger->info(sprintf( +- 'Consumer "%s" skipped as required connection "%s" is not configured. %s', +- $consumerName, +- $connectionName, +- $e->getMessage() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Consumer "%s" skipped as required connection "%s" is not configured. %s', ++ $consumerName, ++ $connectionName, ++ $e->getMessage() ++ ) ++ ); + return false; + } + + return true; + } +- +- /** +- * Returns default path to file with PID by consumers name +- * +- * @param string $consumerName The consumers name +- * @return string The path to file with PID +- */ +- private function getPidFilePath($consumerName) +- { +- $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); +- +- return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; +- } + } +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +deleted file mode 100644 +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php ++++ /dev/null +@@ -1,127 +0,0 @@ +-filesystem = $filesystem; +- } +- +- /** +- * Checks if consumer process is run by pid from pidFile +- * +- * @param string $pidFilePath The path to file with PID +- * @return bool Returns true if consumer process is run +- * @throws FileSystemException +- */ +- public function isRun($pidFilePath) +- { +- $pid = $this->getPid($pidFilePath); +- if ($pid) { +- if (function_exists('posix_getpgid')) { +- return (bool) posix_getpgid($pid); +- } else { +- return $this->checkIsProcessExists($pid); +- } +- } +- +- return false; +- } +- +- /** +- * Checks that process is running +- * +- * If php function exec is not available throws RuntimeException +- * If shell command returns non-zero code and this code is not 1 throws RuntimeException +- * +- * @param int $pid A pid of process +- * @return bool Returns true if consumer process is run +- * @throws \RuntimeException +- * @SuppressWarnings(PHPMD.UnusedLocalVariable) +- */ +- private function checkIsProcessExists($pid) +- { +- if (!function_exists('exec')) { +- throw new \RuntimeException('Function exec is not available'); +- } +- +- exec(escapeshellcmd('ps -p ' . $pid), $output, $code); +- +- $code = (int) $code; +- +- switch ($code) { +- case 0: +- return true; +- break; +- case 1: +- return false; +- break; +- default: +- throw new \RuntimeException('Exec returned non-zero code', $code); +- break; +- } +- } +- +- /** +- * Returns pid by pidFile path +- * +- * @param string $pidFilePath The path to file with PID +- * @return int Returns pid if pid file exists for consumer else returns 0 +- * @throws FileSystemException +- */ +- public function getPid($pidFilePath) +- { +- /** @var ReadInterface $directory */ +- $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); +- +- if ($directory->isExist($pidFilePath)) { +- return (int) $directory->readFile($pidFilePath); +- } +- +- return 0; +- } +- +- /** +- * Saves pid of current process to file +- * +- * @param string $pidFilePath The path to file with pid +- * @throws FileSystemException +- */ +- public function savePid($pidFilePath) +- { +- /** @var WriteInterface $directory */ +- $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); +- $directory->writeFile($pidFilePath, function_exists('posix_getpid') ? posix_getpid() : getmypid(), 'w'); +- } +-} diff --git a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch new file mode 100644 index 00000000..d231ac6a --- /dev/null +++ b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch @@ -0,0 +1,511 @@ +diff -Nuar a/vendor/magento/framework/Lock/Backend/Database.php b/vendor/magento/framework/Lock/Backend/Database.php +--- a/vendor/magento/framework/Lock/Backend/Database.php ++++ b/vendor/magento/framework/Lock/Backend/Database.php +@@ -15,10 +15,18 @@ use Magento\Framework\Exception\InputException; + use Magento\Framework\Phrase; + + /** +- * LockManager using the DB locks ++ * Implementation of the lock manager on the basis of MySQL. + */ + class Database implements \Magento\Framework\Lock\LockManagerInterface + { ++ /** ++ * Max time for lock is 1 week ++ * ++ * MariaDB does not support negative timeout value to get infinite timeout, ++ * so we set 1 week for lock timeout ++ */ ++ private const MAX_LOCK_TIME = 604800; ++ + /** + * @var ResourceConnection + */ +@@ -62,9 +70,13 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @return bool + * @throws InputException + * @throws AlreadyExistsException ++ * @throws \Zend_Db_Statement_Exception + */ + public function lock(string $name, int $timeout = -1): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return true; ++ }; + $name = $this->addPrefix($name); + + /** +@@ -75,7 +87,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + if ($this->currentLock) { + throw new AlreadyExistsException( + new Phrase( +- 'Current connection is already holding lock for $1, only single lock allowed', ++ 'Current connection is already holding lock for %1, only single lock allowed', + [$this->currentLock] + ) + ); +@@ -83,7 +95,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + + $result = (bool)$this->resource->getConnection()->query( + "SELECT GET_LOCK(?, ?);", +- [(string)$name, (int)$timeout] ++ [$name, $timeout < 0 ? self::MAX_LOCK_TIME : $timeout] + )->fetchColumn(); + + if ($result === true) { +@@ -99,9 +111,14 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @param string $name lock name + * @return bool + * @throws InputException ++ * @throws \Zend_Db_Statement_Exception + */ + public function unlock(string $name): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return true; ++ }; ++ + $name = $this->addPrefix($name); + + $result = (bool)$this->resource->getConnection()->query( +@@ -122,14 +139,19 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * @param string $name lock name + * @return bool + * @throws InputException ++ * @throws \Zend_Db_Statement_Exception + */ + public function isLocked(string $name): bool + { ++ if (!$this->deploymentConfig->isDbAvailable()) { ++ return false; ++ }; ++ + $name = $this->addPrefix($name); + + return (bool)$this->resource->getConnection()->query( + "SELECT IS_USED_LOCK(?);", +- [(string)$name] ++ [$name] + )->fetchColumn(); + } + +@@ -139,7 +161,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + * Limited to 64 characters in MySQL. + * + * @param string $name +- * @return string $name ++ * @return string + * @throws InputException + */ + private function addPrefix(string $name): string +diff -Nuar a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +--- a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php ++++ b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +@@ -11,7 +11,7 @@ use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Framework\MessageQueue\ConsumerFactory; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Command for starting MessageQueue consumers. +@@ -22,6 +22,7 @@ class StartConsumerCommand extends Command + const OPTION_NUMBER_OF_MESSAGES = 'max-messages'; + const OPTION_BATCH_SIZE = 'batch-size'; + const OPTION_AREACODE = 'area-code'; ++ const OPTION_SINGLE_THREAD = 'single-thread'; + const PID_FILE_PATH = 'pid-file-path'; + const COMMAND_QUEUE_CONSUMERS_START = 'queue:consumers:start'; + +@@ -36,9 +37,9 @@ class StartConsumerCommand extends Command + private $appState; + + /** +- * @var PidConsumerManager ++ * @var LockManagerInterface + */ +- private $pidConsumerManager; ++ private $lockManager; + + /** + * StartConsumerCommand constructor. +@@ -47,23 +48,23 @@ class StartConsumerCommand extends Command + * @param \Magento\Framework\App\State $appState + * @param ConsumerFactory $consumerFactory + * @param string $name +- * @param PidConsumerManager $pidConsumerManager ++ * @param LockManagerInterface $lockManager + */ + public function __construct( + \Magento\Framework\App\State $appState, + ConsumerFactory $consumerFactory, + $name = null, +- PidConsumerManager $pidConsumerManager = null ++ LockManagerInterface $lockManager = null + ) { + $this->appState = $appState; + $this->consumerFactory = $consumerFactory; +- $this->pidConsumerManager = $pidConsumerManager ?: \Magento\Framework\App\ObjectManager::getInstance() +- ->get(PidConsumerManager::class); ++ $this->lockManager = $lockManager ?: \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(LockManagerInterface::class); + parent::__construct($name); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { +@@ -71,30 +72,36 @@ class StartConsumerCommand extends Command + $numberOfMessages = $input->getOption(self::OPTION_NUMBER_OF_MESSAGES); + $batchSize = (int)$input->getOption(self::OPTION_BATCH_SIZE); + $areaCode = $input->getOption(self::OPTION_AREACODE); +- $pidFilePath = $input->getOption(self::PID_FILE_PATH); + +- if ($pidFilePath && $this->pidConsumerManager->isRun($pidFilePath)) { +- $output->writeln('Consumer with the same PID is running'); +- return \Magento\Framework\Console\Cli::RETURN_FAILURE; ++ if ($input->getOption(self::PID_FILE_PATH)) { ++ $input->setOption(self::OPTION_SINGLE_THREAD, true); + } + +- if ($pidFilePath) { +- $this->pidConsumerManager->savePid($pidFilePath); ++ $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); ++ ++ if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore ++ $output->writeln('Consumer with the same name is running'); ++ return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + +- if ($areaCode !== null) { +- $this->appState->setAreaCode($areaCode); +- } else { +- $this->appState->setAreaCode('global'); ++ if ($singleThread) { ++ $this->lockManager->lock(md5($consumerName)); //phpcs:ignore + } + ++ $this->appState->setAreaCode($areaCode ?? 'global'); ++ + $consumer = $this->consumerFactory->get($consumerName, $batchSize); + $consumer->process($numberOfMessages); ++ ++ if ($singleThread) { ++ $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore ++ } ++ + return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function configure() + { +@@ -125,11 +132,17 @@ class StartConsumerCommand extends Command + 'The preferred area (global, adminhtml, etc...) ' + . 'default is global.' + ); ++ $this->addOption( ++ self::OPTION_SINGLE_THREAD, ++ null, ++ InputOption::VALUE_NONE, ++ 'This option prevents running multiple copies of one consumer simultaneously.' ++ ); + $this->addOption( + self::PID_FILE_PATH, + null, + InputOption::VALUE_REQUIRED, +- 'The file path for saving PID' ++ 'The file path for saving PID (This option is deprecated, use --single-thread instead)' + ); + $this->setHelp( + <<%command.full_name% someConsumer --area-code='adminhtml' ++ ++To do not run multiple copies of one consumer simultaneously: ++ ++ %command.full_name% someConsumer --single-thread' + +-To save PID enter path: ++To save PID enter path (This option is deprecated, use --single-thread instead): + + %command.full_name% someConsumer --pid-file-path='/var/someConsumer.pid' + HELP +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php ++++ b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +@@ -13,18 +13,13 @@ use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInt + use Magento\Framework\App\DeploymentConfig; + use Psr\Log\LoggerInterface; + use Symfony\Component\Process\PhpExecutableFinder; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Class for running consumers processes by cron + */ + class ConsumersRunner + { +- /** +- * Extension of PID file +- */ +- const PID_FILE_EXT = '.pid'; +- + /** + * Shell command line wrapper for executing command in background + * +@@ -53,13 +48,6 @@ class ConsumersRunner + */ + private $phpExecutableFinder; + +- /** +- * The class for checking status of process by PID +- * +- * @var PidConsumerManager +- */ +- private $pidConsumerManager; +- + /** + * @var ConnectionTypeResolver + */ +@@ -70,13 +58,20 @@ class ConsumersRunner + */ + private $logger; + ++ /** ++ * Lock Manager ++ * ++ * @var LockManagerInterface ++ */ ++ private $lockManager; ++ + /** + * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed + * for the PHP executable + * @param ConsumerConfigInterface $consumerConfig The consumer config provider + * @param DeploymentConfig $deploymentConfig The application deployment configuration + * @param ShellInterface $shellBackground The shell command line wrapper for executing command in background +- * @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID ++ * @param LockManagerInterface $lockManager The lock manager + * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver + * @param LoggerInterface $logger Logger + */ +@@ -85,7 +80,7 @@ class ConsumersRunner + ConsumerConfigInterface $consumerConfig, + DeploymentConfig $deploymentConfig, + ShellInterface $shellBackground, +- PidConsumerManager $pidConsumerManager, ++ LockManagerInterface $lockManager, + ConnectionTypeResolver $mqConnectionTypeResolver = null, + LoggerInterface $logger = null + ) { +@@ -93,7 +88,7 @@ class ConsumersRunner + $this->consumerConfig = $consumerConfig; + $this->deploymentConfig = $deploymentConfig; + $this->shellBackground = $shellBackground; +- $this->pidConsumerManager = $pidConsumerManager; ++ $this->lockManager = $lockManager; + $this->mqConnectionTypeResolver = $mqConnectionTypeResolver + ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); + $this->logger = $logger +@@ -120,11 +115,9 @@ class ConsumersRunner + continue; + } + +- $consumerName = $consumer->getName(); +- + $arguments = [ +- $consumerName, +- '--pid-file-path=' . $this->getPidFilePath($consumerName), ++ $consumer->getName(), ++ '--single-thread' + ]; + + if ($maxMessages) { +@@ -154,7 +147,7 @@ class ConsumersRunner + return false; + } + +- if ($this->pidConsumerManager->isRun($this->getPidFilePath($consumerName))) { ++ if ($this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore + return false; + } + +@@ -162,28 +155,17 @@ class ConsumersRunner + try { + $this->mqConnectionTypeResolver->getConnectionType($connectionName); + } catch (\LogicException $e) { +- $this->logger->info(sprintf( +- 'Consumer "%s" skipped as required connection "%s" is not configured. %s', +- $consumerName, +- $connectionName, +- $e->getMessage() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Consumer "%s" skipped as required connection "%s" is not configured. %s', ++ $consumerName, ++ $connectionName, ++ $e->getMessage() ++ ) ++ ); + return false; + } + + return true; + } +- +- /** +- * Returns default path to file with PID by consumers name +- * +- * @param string $consumerName The consumers name +- * @return string The path to file with PID +- */ +- private function getPidFilePath($consumerName) +- { +- $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); +- +- return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; +- } + } +diff --git a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +deleted file mode 100644 +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php ++++ /dev/null +@@ -1,127 +0,0 @@ +-filesystem = $filesystem; +- } +- +- /** +- * Checks if consumer process is run by pid from pidFile +- * +- * @param string $pidFilePath The path to file with PID +- * @return bool Returns true if consumer process is run +- * @throws FileSystemException +- */ +- public function isRun($pidFilePath) +- { +- $pid = $this->getPid($pidFilePath); +- if ($pid) { +- if (function_exists('posix_getpgid')) { +- return (bool) posix_getpgid($pid); +- } else { +- return $this->checkIsProcessExists($pid); +- } +- } +- +- return false; +- } +- +- /** +- * Checks that process is running +- * +- * If php function exec is not available throws RuntimeException +- * If shell command returns non-zero code and this code is not 1 throws RuntimeException +- * +- * @param int $pid A pid of process +- * @return bool Returns true if consumer process is run +- * @throws \RuntimeException +- * @SuppressWarnings(PHPMD.UnusedLocalVariable) +- */ +- private function checkIsProcessExists($pid) +- { +- if (!function_exists('exec')) { +- throw new \RuntimeException('Function exec is not available'); +- } +- +- exec(escapeshellcmd('ps -p ' . $pid), $output, $code); +- +- $code = (int) $code; +- +- switch ($code) { +- case 0: +- return true; +- break; +- case 1: +- return false; +- break; +- default: +- throw new \RuntimeException('Exec returned non-zero code', $code); +- break; +- } +- } +- +- /** +- * Returns pid by pidFile path +- * +- * @param string $pidFilePath The path to file with PID +- * @return int Returns pid if pid file exists for consumer else returns 0 +- * @throws FileSystemException +- */ +- public function getPid($pidFilePath) +- { +- /** @var ReadInterface $directory */ +- $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); +- +- if ($directory->isExist($pidFilePath)) { +- return (int) $directory->readFile($pidFilePath); +- } +- +- return 0; +- } +- +- /** +- * Saves pid of current process to file +- * +- * @param string $pidFilePath The path to file with pid +- * @throws FileSystemException +- */ +- public function savePid($pidFilePath) +- { +- /** @var WriteInterface $directory */ +- $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); +- $directory->writeFile($pidFilePath, function_exists('posix_getpid') ? posix_getpid() : getmypid(), 'w'); +- } +-} diff --git a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch new file mode 100644 index 00000000..81659538 --- /dev/null +++ b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch @@ -0,0 +1,447 @@ +diff -Nuar a/vendor/magento/framework/Lock/Backend/Database.php b/vendor/magento/framework/Lock/Backend/Database.php +--- a/vendor/magento/framework/Lock/Backend/Database.php ++++ b/vendor/magento/framework/Lock/Backend/Database.php +@@ -19,6 +19,14 @@ use Magento\Framework\Phrase; + */ + class Database implements \Magento\Framework\Lock\LockManagerInterface + { ++ /** ++ * Max time for lock is 1 week ++ * ++ * MariaDB does not support negative timeout value to get infinite timeout, ++ * so we set 1 week for lock timeout ++ */ ++ private const MAX_LOCK_TIME = 604800; ++ + /** + * @var ResourceConnection + */ +@@ -87,7 +95,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + + $result = (bool)$this->resource->getConnection()->query( + "SELECT GET_LOCK(?, ?);", +- [(string)$name, (int)$timeout] ++ [$name, $timeout < 0 ? self::MAX_LOCK_TIME : $timeout] + )->fetchColumn(); + + if ($result === true) { +@@ -143,7 +151,7 @@ class Database implements \Magento\Framework\Lock\LockManagerInterface + + return (bool)$this->resource->getConnection()->query( + "SELECT IS_USED_LOCK(?);", +- [(string)$name] ++ [$name] + )->fetchColumn(); + } + +diff -Nuar a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +--- a/vendor/magento/module-message-queue/Console/StartConsumerCommand.php ++++ b/vendor/magento/module-message-queue/Console/StartConsumerCommand.php +@@ -11,7 +11,7 @@ use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Framework\MessageQueue\ConsumerFactory; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Command for starting MessageQueue consumers. +@@ -22,6 +22,7 @@ class StartConsumerCommand extends Command + const OPTION_NUMBER_OF_MESSAGES = 'max-messages'; + const OPTION_BATCH_SIZE = 'batch-size'; + const OPTION_AREACODE = 'area-code'; ++ const OPTION_SINGLE_THREAD = 'single-thread'; + const PID_FILE_PATH = 'pid-file-path'; + const COMMAND_QUEUE_CONSUMERS_START = 'queue:consumers:start'; + +@@ -36,9 +37,9 @@ class StartConsumerCommand extends Command + private $appState; + + /** +- * @var PidConsumerManager ++ * @var LockManagerInterface + */ +- private $pidConsumerManager; ++ private $lockManager; + + /** + * StartConsumerCommand constructor. +@@ -47,23 +48,23 @@ class StartConsumerCommand extends Command + * @param \Magento\Framework\App\State $appState + * @param ConsumerFactory $consumerFactory + * @param string $name +- * @param PidConsumerManager $pidConsumerManager ++ * @param LockManagerInterface $lockManager + */ + public function __construct( + \Magento\Framework\App\State $appState, + ConsumerFactory $consumerFactory, + $name = null, +- PidConsumerManager $pidConsumerManager = null ++ LockManagerInterface $lockManager = null + ) { + $this->appState = $appState; + $this->consumerFactory = $consumerFactory; +- $this->pidConsumerManager = $pidConsumerManager ?: \Magento\Framework\App\ObjectManager::getInstance() +- ->get(PidConsumerManager::class); ++ $this->lockManager = $lockManager ?: \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(LockManagerInterface::class); + parent::__construct($name); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { +@@ -71,30 +72,36 @@ class StartConsumerCommand extends Command + $numberOfMessages = $input->getOption(self::OPTION_NUMBER_OF_MESSAGES); + $batchSize = (int)$input->getOption(self::OPTION_BATCH_SIZE); + $areaCode = $input->getOption(self::OPTION_AREACODE); +- $pidFilePath = $input->getOption(self::PID_FILE_PATH); + +- if ($pidFilePath && $this->pidConsumerManager->isRun($pidFilePath)) { +- $output->writeln('Consumer with the same PID is running'); +- return \Magento\Framework\Console\Cli::RETURN_FAILURE; ++ if ($input->getOption(self::PID_FILE_PATH)) { ++ $input->setOption(self::OPTION_SINGLE_THREAD, true); + } + +- if ($pidFilePath) { +- $this->pidConsumerManager->savePid($pidFilePath); ++ $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); ++ ++ if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore ++ $output->writeln('Consumer with the same name is running'); ++ return \Magento\Framework\Console\Cli::RETURN_FAILURE; + } + +- if ($areaCode !== null) { +- $this->appState->setAreaCode($areaCode); +- } else { +- $this->appState->setAreaCode('global'); ++ if ($singleThread) { ++ $this->lockManager->lock(md5($consumerName)); //phpcs:ignore + } + ++ $this->appState->setAreaCode($areaCode ?? 'global'); ++ + $consumer = $this->consumerFactory->get($consumerName, $batchSize); + $consumer->process($numberOfMessages); ++ ++ if ($singleThread) { ++ $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore ++ } ++ + return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + protected function configure() + { +@@ -125,11 +132,17 @@ class StartConsumerCommand extends Command + 'The preferred area (global, adminhtml, etc...) ' + . 'default is global.' + ); ++ $this->addOption( ++ self::OPTION_SINGLE_THREAD, ++ null, ++ InputOption::VALUE_NONE, ++ 'This option prevents running multiple copies of one consumer simultaneously.' ++ ); + $this->addOption( + self::PID_FILE_PATH, + null, + InputOption::VALUE_REQUIRED, +- 'The file path for saving PID' ++ 'The file path for saving PID (This option is deprecated, use --single-thread instead)' + ); + $this->setHelp( + <<%command.full_name% someConsumer --area-code='adminhtml' ++ ++To do not run multiple copies of one consumer simultaneously: ++ ++ %command.full_name% someConsumer --single-thread' + +-To save PID enter path: ++To save PID enter path (This option is deprecated, use --single-thread instead): + + %command.full_name% someConsumer --pid-file-path='/var/someConsumer.pid' + HELP +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php ++++ b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php +@@ -13,18 +13,13 @@ use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInt + use Magento\Framework\App\DeploymentConfig; + use Psr\Log\LoggerInterface; + use Symfony\Component\Process\PhpExecutableFinder; +-use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; ++use Magento\Framework\Lock\LockManagerInterface; + + /** + * Class for running consumers processes by cron + */ + class ConsumersRunner + { +- /** +- * Extension of PID file +- */ +- const PID_FILE_EXT = '.pid'; +- + /** + * Shell command line wrapper for executing command in background + * +@@ -53,13 +48,6 @@ class ConsumersRunner + */ + private $phpExecutableFinder; + +- /** +- * The class for checking status of process by PID +- * +- * @var PidConsumerManager +- */ +- private $pidConsumerManager; +- + /** + * @var ConnectionTypeResolver + */ +@@ -70,13 +58,20 @@ class ConsumersRunner + */ + private $logger; + ++ /** ++ * Lock Manager ++ * ++ * @var LockManagerInterface ++ */ ++ private $lockManager; ++ + /** + * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed + * for the PHP executable + * @param ConsumerConfigInterface $consumerConfig The consumer config provider + * @param DeploymentConfig $deploymentConfig The application deployment configuration + * @param ShellInterface $shellBackground The shell command line wrapper for executing command in background +- * @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID ++ * @param LockManagerInterface $lockManager The lock manager + * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver + * @param LoggerInterface $logger Logger + */ +@@ -85,7 +80,7 @@ class ConsumersRunner + ConsumerConfigInterface $consumerConfig, + DeploymentConfig $deploymentConfig, + ShellInterface $shellBackground, +- PidConsumerManager $pidConsumerManager, ++ LockManagerInterface $lockManager, + ConnectionTypeResolver $mqConnectionTypeResolver = null, + LoggerInterface $logger = null + ) { +@@ -93,7 +88,7 @@ class ConsumersRunner + $this->consumerConfig = $consumerConfig; + $this->deploymentConfig = $deploymentConfig; + $this->shellBackground = $shellBackground; +- $this->pidConsumerManager = $pidConsumerManager; ++ $this->lockManager = $lockManager; + $this->mqConnectionTypeResolver = $mqConnectionTypeResolver + ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); + $this->logger = $logger +@@ -120,11 +115,9 @@ class ConsumersRunner + continue; + } + +- $consumerName = $consumer->getName(); +- + $arguments = [ +- $consumerName, +- '--pid-file-path=' . $this->getPidFilePath($consumerName), ++ $consumer->getName(), ++ '--single-thread' + ]; + + if ($maxMessages) { +@@ -154,7 +147,7 @@ class ConsumersRunner + return false; + } + +- if ($this->pidConsumerManager->isRun($this->getPidFilePath($consumerName))) { ++ if ($this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore + return false; + } + +@@ -162,28 +155,17 @@ class ConsumersRunner + try { + $this->mqConnectionTypeResolver->getConnectionType($connectionName); + } catch (\LogicException $e) { +- $this->logger->info(sprintf( +- 'Consumer "%s" skipped as required connection "%s" is not configured. %s', +- $consumerName, +- $connectionName, +- $e->getMessage() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Consumer "%s" skipped as required connection "%s" is not configured. %s', ++ $consumerName, ++ $connectionName, ++ $e->getMessage() ++ ) ++ ); + return false; + } + + return true; + } +- +- /** +- * Returns default path to file with PID by consumers name +- * +- * @param string $consumerName The consumers name +- * @return string The path to file with PID +- */ +- private function getPidFilePath($consumerName) +- { +- $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); +- +- return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; +- } + } +diff --git a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +deleted file mode 100644 +--- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php ++++ /dev/null +@@ -1,127 +0,0 @@ +-filesystem = $filesystem; +- } +- +- /** +- * Checks if consumer process is run by pid from pidFile +- * +- * @param string $pidFilePath The path to file with PID +- * @return bool Returns true if consumer process is run +- * @throws FileSystemException +- */ +- public function isRun($pidFilePath) +- { +- $pid = $this->getPid($pidFilePath); +- if ($pid) { +- if (function_exists('posix_getpgid')) { +- return (bool) posix_getpgid($pid); +- } else { +- return $this->checkIsProcessExists($pid); +- } +- } +- +- return false; +- } +- +- /** +- * Checks that process is running +- * +- * If php function exec is not available throws RuntimeException +- * If shell command returns non-zero code and this code is not 1 throws RuntimeException +- * +- * @param int $pid A pid of process +- * @return bool Returns true if consumer process is run +- * @throws \RuntimeException +- * @SuppressWarnings(PHPMD.UnusedLocalVariable) +- */ +- private function checkIsProcessExists($pid) +- { +- if (!function_exists('exec')) { +- throw new \RuntimeException('Function exec is not available'); +- } +- +- exec(escapeshellcmd('ps -p ' . $pid), $output, $code); +- +- $code = (int) $code; +- +- switch ($code) { +- case 0: +- return true; +- break; +- case 1: +- return false; +- break; +- default: +- throw new \RuntimeException('Exec returned non-zero code', $code); +- break; +- } +- } +- +- /** +- * Returns pid by pidFile path +- * +- * @param string $pidFilePath The path to file with PID +- * @return int Returns pid if pid file exists for consumer else returns 0 +- * @throws FileSystemException +- */ +- public function getPid($pidFilePath) +- { +- /** @var ReadInterface $directory */ +- $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); +- +- if ($directory->isExist($pidFilePath)) { +- return (int) $directory->readFile($pidFilePath); +- } +- +- return 0; +- } +- +- /** +- * Saves pid of current process to file +- * +- * @param string $pidFilePath The path to file with pid +- * @throws FileSystemException +- */ +- public function savePid($pidFilePath) +- { +- /** @var WriteInterface $directory */ +- $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); +- $directory->writeFile($pidFilePath, function_exists('posix_getpid') ? posix_getpid() : getmypid(), 'w'); +- } +-} diff --git a/patches/MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.2.0.patch b/patches/MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.2.0.patch new file mode 100644 index 00000000..2a019dd6 --- /dev/null +++ b/patches/MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.2.0.patch @@ -0,0 +1,170 @@ +diff -Naur a/vendor/magento/framework-message-queue/CallbackInvoker.php b/vendor/magento/framework-message-queue/CallbackInvoker.php +--- a/vendor/magento/framework-message-queue/CallbackInvoker.php ++++ b/vendor/magento/framework-message-queue/CallbackInvoker.php +@@ -6,11 +6,28 @@ + + namespace Magento\Framework\MessageQueue; + ++use Magento\Framework\App\DeploymentConfig; ++ + /** + * Class CallbackInvoker to invoke callbacks for consumer classes + */ + class CallbackInvoker + { ++ /** ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ ++ /** ++ * CallbackInvoker constructor. ++ * @param DeploymentConfig $deploymentConfig ++ */ ++ public function __construct( ++ DeploymentConfig $deploymentConfig ++ ) { ++ $this->deploymentConfig = $deploymentConfig; ++ } ++ + /** + * Run short running process + * +@@ -24,8 +41,23 @@ class CallbackInvoker + for ($i = $maxNumberOfMessages; $i > 0; $i--) { + do { + $message = $queue->dequeue(); +- } while ($message === null && (sleep(1) === 0)); ++ } while ($message === null && $this->isWaitingNextMessage() && (sleep(1) === 0)); ++ ++ if ($message === null) { ++ break; ++ } ++ + $callback($message); + } + } ++ ++ /** ++ * Checks if consumers should wait for message from the queue ++ * ++ * @return bool ++ */ ++ private function isWaitingNextMessage(): bool ++ { ++ return $this->deploymentConfig->get('queue/consumers_wait_for_messages', 1) === 1; ++ } + } +diff -Naur a/vendor/magento/module-message-queue/Setup/ConfigOptionsList.php b/vendor/magento/module-message-queue/Setup/ConfigOptionsList.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-message-queue/Setup/ConfigOptionsList.php +@@ -0,0 +1,108 @@ ++selectOptions, ++ self::CONFIG_PATH_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES, ++ 'Should consumers wait for a message from the queue? 1 - Yes, 0 - No', ++ self::DEFAULT_CONSUMERS_WAIT_FOR_MESSAGES ++ ), ++ ]; ++ } ++ ++ /** ++ * @inheritdoc ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function createConfig(array $data, DeploymentConfig $deploymentConfig) ++ { ++ $configData = new ConfigData(ConfigFilePool::APP_ENV); ++ ++ if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES)) { ++ $configData->set( ++ self::CONFIG_PATH_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES, ++ (int)$data[self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES] ++ ); ++ } ++ ++ return [$configData]; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function validate(array $options, DeploymentConfig $deploymentConfig) ++ { ++ $errors = []; ++ ++ if (!$this->isDataEmpty($options, self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES) ++ && !in_array($options[self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES], $this->selectOptions)) { ++ $errors[] = 'You can use only 1 or 0 for ' . self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES . ' option'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Check if data ($data) with key ($key) is empty ++ * ++ * @param array $data ++ * @param string $key ++ * @return bool ++ */ ++ private function isDataEmpty(array $data, $key) ++ { ++ if (isset($data[$key]) && $data[$key] !== '') { ++ return false; ++ } ++ ++ return true; ++ } ++} diff --git a/patches/MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.3.2.patch b/patches/MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.3.2.patch new file mode 100644 index 00000000..eb1b7855 --- /dev/null +++ b/patches/MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.3.2.patch @@ -0,0 +1,181 @@ +diff -Naur a/vendor/magento/framework-message-queue/CallbackInvoker.php b/vendor/magento/framework-message-queue/CallbackInvoker.php +--- a/vendor/magento/framework-message-queue/CallbackInvoker.php ++++ b/vendor/magento/framework-message-queue/CallbackInvoker.php +@@ -8,6 +8,7 @@ namespace Magento\Framework\MessageQueue; + + use Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface; + use Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface; ++use Magento\Framework\App\DeploymentConfig; + + /** + * Class CallbackInvoker to invoke callbacks for consumer classes +@@ -29,16 +30,24 @@ class CallbackInvoker implements CallbackInvokerInterface + */ + private $poisonPillCompare; + ++ /** ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ + /** + * @param PoisonPillReadInterface $poisonPillRead + * @param PoisonPillCompareInterface $poisonPillCompare ++ * @param DeploymentConfig $deploymentConfig + */ + public function __construct( + PoisonPillReadInterface $poisonPillRead, +- PoisonPillCompareInterface $poisonPillCompare ++ PoisonPillCompareInterface $poisonPillCompare, ++ DeploymentConfig $deploymentConfig + ) { + $this->poisonPillRead = $poisonPillRead; + $this->poisonPillCompare = $poisonPillCompare; ++ $this->deploymentConfig = $deploymentConfig; + } + + /** +@@ -56,13 +65,29 @@ class CallbackInvoker implements CallbackInvokerInterface + do { + $message = $queue->dequeue(); + // phpcs:ignore Magento2.Functions.DiscouragedFunction +- } while ($message === null && (sleep(1) === 0)); ++ } while ($message === null && $this->isWaitingNextMessage() && (sleep(1) === 0)); ++ ++ if ($message === null) { ++ break; ++ } ++ + if (false === $this->poisonPillCompare->isLatestVersion($this->poisonPillVersion)) { + $queue->reject($message); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage + exit(0); + } ++ + $callback($message); + } + } ++ ++ /** ++ * Checks if consumers should wait for message from the queue ++ * ++ * @return bool ++ */ ++ private function isWaitingNextMessage(): bool ++ { ++ return $this->deploymentConfig->get('queue/consumers_wait_for_messages', 1) === 1; ++ } + } +diff -Naur a/vendor/magento/module-message-queue/Setup/ConfigOptionsList.php b/vendor/magento/module-message-queue/Setup/ConfigOptionsList.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-message-queue/Setup/ConfigOptionsList.php +@@ -0,0 +1,108 @@ ++selectOptions, ++ self::CONFIG_PATH_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES, ++ 'Should consumers wait for a message from the queue? 1 - Yes, 0 - No', ++ self::DEFAULT_CONSUMERS_WAIT_FOR_MESSAGES ++ ), ++ ]; ++ } ++ ++ /** ++ * @inheritdoc ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function createConfig(array $data, DeploymentConfig $deploymentConfig) ++ { ++ $configData = new ConfigData(ConfigFilePool::APP_ENV); ++ ++ if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES)) { ++ $configData->set( ++ self::CONFIG_PATH_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES, ++ (int)$data[self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES] ++ ); ++ } ++ ++ return [$configData]; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function validate(array $options, DeploymentConfig $deploymentConfig) ++ { ++ $errors = []; ++ ++ if (!$this->isDataEmpty($options, self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES) ++ && !in_array($options[self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES], $this->selectOptions)) { ++ $errors[] = 'You can use only 1 or 0 for ' . self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES . ' option'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Check if data ($data) with key ($key) is empty ++ * ++ * @param array $data ++ * @param string $key ++ * @return bool ++ */ ++ private function isDataEmpty(array $data, $key) ++ { ++ if (isset($data[$key]) && $data[$key] !== '') { ++ return false; ++ } ++ ++ return true; ++ } ++} diff --git a/patches/MAGECLOUD-414__remove_unnecessary_permission_checks__2.1.4.patch b/patches/MAGECLOUD-414__remove_unnecessary_permission_checks__2.1.4.patch new file mode 100644 index 00000000..7592814d --- /dev/null +++ b/patches/MAGECLOUD-414__remove_unnecessary_permission_checks__2.1.4.patch @@ -0,0 +1,43 @@ +diff -Naur b/vendor/magento/framework/Setup/FilePermissions.php a/vendor/magento/framework/Setup/FilePermissions.php +--- b/vendor/magento/framework/Setup/FilePermissions.php 2016-09-23 16:01:12.000000000 -0500 ++++ a/vendor/magento/framework/Setup/FilePermissions.php 2016-09-23 16:22:09.000000000 -0500 +@@ -233,26 +233,8 @@ + */ + public function getMissingWritablePathsForInstallation($associative = false) + { +- $required = $this->getInstallationWritableDirectories(); +- $current = $this->getInstallationCurrentWritableDirectories(); +- $missingPaths = []; +- foreach (array_diff($required, $current) as $missingPath) { +- if (isset($this->nonWritablePathsInDirectories[$missingPath])) { +- if ($associative) { +- $missingPaths[$missingPath] = $this->nonWritablePathsInDirectories[$missingPath]; +- } else { +- $missingPaths = array_merge( +- $missingPaths, +- $this->nonWritablePathsInDirectories[$missingPath] +- ); +- } +- } +- } +- if ($associative) { +- $required = array_flip($required); +- $missingPaths = array_merge($required, $missingPaths); +- } +- return $missingPaths; ++ // Unnecessary check in controlled environment ++ return []; + } + + /** +@@ -275,8 +257,7 @@ + */ + public function getUnnecessaryWritableDirectoriesForApplication() + { +- $required = $this->getApplicationNonWritableDirectories(); +- $current = $this->getApplicationCurrentNonWritableDirectories(); +- return array_diff($required, $current); ++ // Unnecessary check in controlled environment ++ return []; + } + } diff --git a/patches/MAGECLOUD-414__remove_unnecessary_permission_checks__2.2.0.patch b/patches/MAGECLOUD-414__remove_unnecessary_permission_checks__2.2.0.patch new file mode 100644 index 00000000..7592814d --- /dev/null +++ b/patches/MAGECLOUD-414__remove_unnecessary_permission_checks__2.2.0.patch @@ -0,0 +1,43 @@ +diff -Naur b/vendor/magento/framework/Setup/FilePermissions.php a/vendor/magento/framework/Setup/FilePermissions.php +--- b/vendor/magento/framework/Setup/FilePermissions.php 2016-09-23 16:01:12.000000000 -0500 ++++ a/vendor/magento/framework/Setup/FilePermissions.php 2016-09-23 16:22:09.000000000 -0500 +@@ -233,26 +233,8 @@ + */ + public function getMissingWritablePathsForInstallation($associative = false) + { +- $required = $this->getInstallationWritableDirectories(); +- $current = $this->getInstallationCurrentWritableDirectories(); +- $missingPaths = []; +- foreach (array_diff($required, $current) as $missingPath) { +- if (isset($this->nonWritablePathsInDirectories[$missingPath])) { +- if ($associative) { +- $missingPaths[$missingPath] = $this->nonWritablePathsInDirectories[$missingPath]; +- } else { +- $missingPaths = array_merge( +- $missingPaths, +- $this->nonWritablePathsInDirectories[$missingPath] +- ); +- } +- } +- } +- if ($associative) { +- $required = array_flip($required); +- $missingPaths = array_merge($required, $missingPaths); +- } +- return $missingPaths; ++ // Unnecessary check in controlled environment ++ return []; + } + + /** +@@ -275,8 +257,7 @@ + */ + public function getUnnecessaryWritableDirectoriesForApplication() + { +- $required = $this->getApplicationNonWritableDirectories(); +- $current = $this->getApplicationCurrentNonWritableDirectories(); +- return array_diff($required, $current); ++ // Unnecessary check in controlled environment ++ return []; + } + } diff --git a/patches/MAGECLOUD-589__load_appropriate_js_files__2.1.4.patch b/patches/MAGECLOUD-589__load_appropriate_js_files__2.1.4.patch new file mode 100644 index 00000000..b9f1718a --- /dev/null +++ b/patches/MAGECLOUD-589__load_appropriate_js_files__2.1.4.patch @@ -0,0 +1,26 @@ +MAGECLOUD-589 MAGETWO-64955 +diff -Naur a/vendor/magento/framework/View/Asset/File/FallbackContext.php b/vendor/magento/framework/View/Asset/File/FallbackContext.php +--- a/vendor/magento/framework/View/Asset/File/FallbackContext.php 2016-10-15 12:38:21.595266000 +0000 ++++ b/vendor/magento/framework/View/Asset/File/FallbackContext.php 2016-10-15 12:39:13.587266000 +0000 +@@ -103,6 +103,6 @@ + */ + public function getConfigPath() + { +- return $this->getPath() . ($this->isSecure ? '/' . self::SECURE_PATH : ''); ++ return $this->getPath(); + } + } + +diff -Nuar a/vendor/magento/framework/View/Asset/Repository.php b/vendor/magento/framework/View/Asset/Repository.php +--- a/vendor/magento/framework/View/Asset/Repository.php 2017-02-16 16:50:18.000000000 +0000 ++++ b/vendor/magento/framework/View/Asset/Repository.php 2017-02-22 14:29:40.000000000 +0000 +@@ -269,8 +269,7 @@ + 'baseUrl' => $url, + 'areaType' => $area, + 'themePath' => $themePath, +- 'localeCode' => $locale, +- 'isSecure' => $isSecure ++ 'localeCode' => $locale + ] + ); + } diff --git a/patches/MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch b/patches/MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch new file mode 100644 index 00000000..443388f1 --- /dev/null +++ b/patches/MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch @@ -0,0 +1,15 @@ +Ticket MAGETWO-45357 +diff -Nuar a/vendor/magento/framework/App/ObjectManager/ConfigLoader/Compiled.php b/vendor/magento/framework/App/ObjectManager/ConfigLoader/Compiled.php +index 844d3f0..d087d07 100644 +--- a/vendor/magento/framework/App/ObjectManager/ConfigLoader/Compiled.php ++++ b/vendor/magento/framework/App/ObjectManager/ConfigLoader/Compiled.php +@@ -37,6 +37,9 @@ class Compiled implements ConfigLoaderInterface + */ + public static function getFilePath($area) + { ++ if ($area == 'setup') { ++ $area = 'global'; ++ } + return BP . '/var/di/' . $area . '.ser'; + } + } diff --git a/patches/MAGETWO-53941__fix_enterprise_payment_codes__2.1.8.patch b/patches/MAGETWO-53941__fix_enterprise_payment_codes__2.1.8.patch new file mode 100644 index 00000000..1f75f0d6 --- /dev/null +++ b/patches/MAGETWO-53941__fix_enterprise_payment_codes__2.1.8.patch @@ -0,0 +1,93 @@ +<+>UTF-8 +=================================================================== +diff -Nuar a/vendor/magento/module-braintree/Gateway/Request/ChannelDataBuilder.php b/vendor/magento/module-braintree/Gateway/Request/ChannelDataBuilder.php +--- a/vendor/magento/module-braintree/Gateway/Request/ChannelDataBuilder.php ++++ b/vendor/magento/module-braintree/Gateway/Request/ChannelDataBuilder.php +@@ -26,7 +26,7 @@ + /** + * @var string + */ +- private static $channelValue = 'Magento2_Cart_%s_BT'; ++ private static $channelValue = 'Magento_Enterprise_Cloud_BT'; + + /** + * Constructor +@@ -44,7 +44,7 @@ + public function build(array $buildSubject) + { + return [ +- self::$channel => sprintf(self::$channelValue, $this->productMetadata->getEdition()) ++ self::$channel => self::$channelValue + ]; + } + } +<+>UTF-8 +=================================================================== +diff -Nuar a/vendor/magento/module-braintree/Test/Unit/Gateway/Request/ChannelDataBuilderTest.php b/vendor/magento/module-braintree/Test/Unit/Gateway/Request/ChannelDataBuilderTest.php +--- a/vendor/magento/module-braintree/Test/Unit/Gateway/Request/ChannelDataBuilderTest.php ++++ b/vendor/magento/module-braintree/Test/Unit/Gateway/Request/ChannelDataBuilderTest.php +@@ -40,7 +40,7 @@ + public function testBuild($edition, array $expected) + { + $buildSubject = []; +- $this->productMetadataMock->expects(static::once()) ++ $this->productMetadataMock->expects(static::never()) + ->method('getEdition') + ->willReturn($edition); + +@@ -54,8 +54,8 @@ + public function buildDataProvider() + { + return [ +- ['FirstEdition', ['channel' => 'Magento2_Cart_FirstEdition_BT']], +- ['SecondEdition', ['channel' => 'Magento2_Cart_SecondEdition_BT']], ++ ['FirstEdition', ['channel' => 'Magento_Enterprise_Cloud_BT']], ++ ['SecondEdition', ['channel' => 'Magento_Enterprise_Cloud_BT']], + ]; + } + } +<+>UTF-8 +=================================================================== +diff -Nuar a/vendor/magento/module-paypal/Model/AbstractConfig.php b/vendor/magento/module-paypal/Model/AbstractConfig.php +--- a/vendor/magento/module-paypal/Model/AbstractConfig.php ++++ b/vendor/magento/module-paypal/Model/AbstractConfig.php +@@ -59,7 +59,7 @@ + /** + * @var string + */ +- private static $bnCode = 'Magento_Cart_%s'; ++ private static $bnCode = 'Magento_Enterprise_Cloud'; + + /** + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig +@@ -335,7 +335,7 @@ + */ + public function getBuildNotationCode() + { +- return sprintf(self::$bnCode, $this->getProductMetadata()->getEdition()); ++ return self::$bnCode; + } + + /** +<+>UTF-8 +=================================================================== +diff -Nuar a/vendor/magento/module-paypal/Test/Unit/Model/AbstractConfigTest.php b/vendor/magento/module-paypal/Test/Unit/Model/AbstractConfigTest.php +--- a/vendor/magento/module-paypal/Test/Unit/Model/AbstractConfigTest.php ++++ b/vendor/magento/module-paypal/Test/Unit/Model/AbstractConfigTest.php +@@ -293,7 +293,7 @@ + public function testGetBuildNotationCode() + { + $productMetadata = $this->getMock(ProductMetadataInterface::class, [], [], '', false); +- $productMetadata->expects($this->once()) ++ $productMetadata->expects($this->never()) + ->method('getEdition') + ->will($this->returnValue('SomeEdition')); + +@@ -304,6 +304,6 @@ + $productMetadata + ); + +- $this->assertEquals('Magento_Cart_SomeEdition', $this->config->getBuildNotationCode()); ++ $this->assertEquals('Magento_Enterprise_Cloud', $this->config->getBuildNotationCode()); + } + } diff --git a/patches/MAGETWO-56675__dont_skip_setup_scoped_plugins__2.1.4.patch b/patches/MAGETWO-56675__dont_skip_setup_scoped_plugins__2.1.4.patch new file mode 100644 index 00000000..b1bf5a41 --- /dev/null +++ b/patches/MAGETWO-56675__dont_skip_setup_scoped_plugins__2.1.4.patch @@ -0,0 +1,20 @@ +Patch regarding MAGETWO-56675. +diff -Nuar a/vendor/magento/framework/Interception/PluginList/PluginList.php b/vendor/magento/framework/Interception/PluginList/PluginList.php +--- a/vendor/magento/framework/Interception/PluginList/PluginList.php ++++ b/vendor/magento/framework/Interception/PluginList/PluginList.php +@@ -277,6 +277,7 @@ class PluginList extends Scoped implements InterceptionPluginList + $virtualTypes = []; + foreach ($this->_scopePriorityScheme as $scopeCode) { + if (false == isset($this->_loadedScopes[$scopeCode])) { ++ $this->_loadedScopes[$scopeCode] = true; + $data = $this->_reader->read($scopeCode); + unset($data['preferences']); + if (!count($data)) { +@@ -285,7 +286,6 @@ class PluginList extends Scoped implements InterceptionPluginList + $this->_inherited = []; + $this->_processed = []; + $this->merge($data); +- $this->_loadedScopes[$scopeCode] = true; + foreach ($data as $class => $config) { + if (isset($config['type'])) { + $virtualTypes[] = $class; diff --git a/patches/MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch b/patches/MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch new file mode 100644 index 00000000..c30379b7 --- /dev/null +++ b/patches/MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch @@ -0,0 +1,37 @@ +Ticket MAGETWO-57413 +diff -Naur a/app/autoload.php b/app/autoload.php +index b817baf..1d1873d 100644 +--- a/app/autoload.php ++++ b/app/autoload.php +@@ -13,16 +13,7 @@ use Magento\Framework\Autoload\ClassLoaderWrapper; + */ + define('BP', dirname(__DIR__)); + +-define('VENDOR_PATH', BP . '/app/etc/vendor_path.php'); +- +-if (!file_exists(VENDOR_PATH)) { +- throw new \Exception( +- 'We can\'t read some files that are required to run the Magento application. ' +- . 'This usually means file permissions are set incorrectly.' +- ); +-} +- +-$vendorDir = require VENDOR_PATH; ++$vendorDir = './vendor'; + $vendorAutoload = BP . "/{$vendorDir}/autoload.php"; + + /* 'composer install' validation */ + +diff -Naur a/vendor/magento/framework/App/Arguments/FileResolver/Primary.php b/vendor/magento/framework/App/Arguments/FileResolver/Primary.php +index 40b74e9..0f732c9 100644 +--- a/vendor/magento/framework/App/Arguments/FileResolver/Primary.php ++++ b/vendor/magento/framework/App/Arguments/FileResolver/Primary.php +@@ -29,7 +29,7 @@ class Primary implements \Magento\Framework\Config\FileResolverInterface + \Magento\Framework\Filesystem $filesystem, + \Magento\Framework\Config\FileIteratorFactory $iteratorFactory + ) { +- $this->configDirectory = $filesystem->getDirectoryRead(DirectoryList::CONFIG); ++ $this->configDirectory = $filesystem->getDirectoryRead(DirectoryList::APP); + $this->iteratorFactory = $iteratorFactory; + } + diff --git a/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch b/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch new file mode 100644 index 00000000..e397a0d4 --- /dev/null +++ b/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch @@ -0,0 +1,58 @@ +Ticket MAGETWO-57414 +diff -Naur a/vendor/magento/framework/App/StaticResource.php b/vendor/magento/framework/App/StaticResource.php +index d591deb..6344322 100644 +--- a/vendor/magento/framework/App/StaticResource.php ++++ b/vendor/magento/framework/App/StaticResource.php +@@ -94,24 +94,40 @@ class StaticResource implements \Magento\Framework\AppInterface + { + // disabling profiling when retrieving static resource + \Magento\Framework\Profiler::reset(); +- $appMode = $this->state->getMode(); +- if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION) { ++ $path = $this->getResourcePath(); ++ if (!isset($path)) { + $this->response->setHttpResponseCode(404); +- } else { +- $path = $this->request->get('resource'); +- $params = $this->parsePath($path); +- $this->state->setAreaCode($params['area']); +- $this->objectManager->configure($this->configLoader->load($params['area'])); +- $file = $params['file']; +- unset($params['file']); +- $asset = $this->assetRepo->createAsset($file, $params); +- $this->response->setFilePath($asset->getSourceFile()); +- $this->publisher->publish($asset); ++ return $this->response; + } ++ ++ $params = $this->parsePath($path); ++ $this->state->setAreaCode($params['area']); ++ $this->objectManager->configure($this->configLoader->load($params['area'])); ++ $file = $params['file']; ++ unset($params['file']); ++ $asset = $this->assetRepo->createAsset($file, $params); ++ $this->response->setFilePath($asset->getSourceFile()); ++ $this->publisher->publish($asset); + return $this->response; + } + + /** ++ * Retrieve the path from either the GET parameter or the request ++ * URI, depending on whether webserver rewrites are in use. ++ */ ++ protected function getResourcePath() { ++ $path = $this->request->get('resource'); ++ if (isset($path)) { ++ return $path; ++ } ++ ++ $path = $this->request->getUri()->getPath(); ++ if (preg_match("~^/static/(?:version\d*/)?(.*)$~", $path, $matches)) { ++ return $matches[1]; ++ } ++ } ++ ++ /** + * @inheritdoc + */ + public function catchException(Bootstrap $bootstrap, \Exception $exception) diff --git a/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch b/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch new file mode 100644 index 00000000..d1d93cc6 --- /dev/null +++ b/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch @@ -0,0 +1,58 @@ +Ticket MAGETWO-57414 +diff -Naur a/vendor/magento/framework/App/StaticResource.php b/vendor/magento/framework/App/StaticResource.php +index d591deb..6344322 100644 +--- a/vendor/magento/framework/App/StaticResource.php ++++ b/vendor/magento/framework/App/StaticResource.php +@@ -94,24 +94,40 @@ class StaticResource implements \Magento\Framework\AppInterface + { + // disabling profiling when retrieving static resource + \Magento\Framework\Profiler::reset(); +- $appMode = $this->state->getMode(); +- if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION) { ++ $path = $this->getResourcePath(); ++ if (!isset($path)) { + $this->response->setHttpResponseCode(404); +- } else { +- $path = $this->request->get('resource'); +- $params = $this->parsePath($path); +- $this->state->setAreaCode($params['area']); +- $this->objectManager->configure($this->configLoader->load($params['area'])); +- $file = $params['file']; +- unset($params['file']); +- $asset = $this->assetRepo->createAsset($file, $params); +- $this->response->setFilePath($asset->getSourceFile()); +- $this->publisher->publish($asset); ++ return $this->response; + } ++ ++ $params = $this->parsePath($path); ++ $this->state->setAreaCode($params['area']); ++ $this->objectManager->configure($this->configLoader->load($params['area'])); ++ $file = $params['file']; ++ unset($params['file']); ++ $asset = $this->assetRepo->createAsset($file, $params); ++ $this->response->setFilePath($asset->getSourceFile()); ++ $this->publisher->publish($asset); + return $this->response; + } + + /** ++ * Retrieve the path from either the GET parameter or the request ++ * URI, depending on whether webserver rewrites are in use. ++ */ ++ protected function getResourcePath() { ++ $path = $this->request->get('resource'); ++ if (isset($path)) { ++ return $path; ++ } ++ ++ $path = $this->request->getUri()->getPath(); ++ if (preg_match("~^/static/(?:version\d*/)?(.*)$~", $path, $matches)) { ++ return $matches[1]; ++ } ++ } ++ ++ /** + * {@inheritdoc} + */ + public function catchException(Bootstrap $bootstrap, \Exception $exception) diff --git a/patches/MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch b/patches/MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch new file mode 100644 index 00000000..856c4529 --- /dev/null +++ b/patches/MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch @@ -0,0 +1,521 @@ +diff --git a/vendor/magento/module-deploy/Model/Deploy/JsDictionaryDeploy.php b/vendor/magento/module-deploy/Model/Deploy/JsDictionaryDeploy.php +new file mode 100644 +index 0000000..9c6a309a +--- /dev/null ++++ b/vendor/magento/module-deploy/Model/Deploy/JsDictionaryDeploy.php +@@ -0,0 +1,89 @@ ++assetRepo = $assetRepo; ++ $this->assetPublisher = $assetPublisher; ++ $this->translationJsConfig = $translationJsConfig; ++ $this->translator = $translator; ++ $this->output = $output; ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function deploy($area, $themePath, $locale) ++ { ++ $this->translator->setLocale($locale); ++ $this->translator->loadData($area, true); ++ ++ $asset = $this->assetRepo->createAsset( ++ $this->translationJsConfig->getDictionaryFileName(), ++ ['area' => $area, 'theme' => $themePath, 'locale' => $locale] ++ ); ++ if ($this->output->isVeryVerbose()) { ++ $this->output->writeln("\tDeploying the file to '{$asset->getPath()}'"); ++ } else { ++ $this->output->write('.'); ++ } ++ ++ $this->assetPublisher->publish($asset); ++ ++ return Cli::RETURN_SUCCESS; ++ } ++} +diff --git a/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php b/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php +index 0d990b5..aa23833 100644 +--- a/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php ++++ b/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php +@@ -6,16 +6,21 @@ + + namespace Magento\Deploy\Model\Deploy; + +-use Magento\Deploy\Model\DeployManager; + use Magento\Framework\App\Filesystem\DirectoryList; +-use Magento\Framework\App\Utility\Files; + use Magento\Framework\Filesystem; + use Magento\Framework\Filesystem\Directory\WriteInterface; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Framework\Console\Cli; + use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; +-use \Magento\Framework\RequireJs\Config as RequireJsConfig; ++use Magento\Framework\RequireJs\Config as RequireJsConfig; ++use Magento\Framework\Translate\Js\Config as TranslationJsConfig; ++use Magento\Framework\App\ObjectManager; ++use Magento\Deploy\Model\DeployStrategyFactory; + ++/** ++ * To avoid duplication of deploying of all static content per each theme/local, this class uses copying/symlinking ++ * of default static files to other locales, separately calls deploy for js dictionary per each locale ++ */ + class LocaleQuickDeploy implements DeployInterface + { + /** +@@ -39,15 +44,41 @@ class LocaleQuickDeploy implements DeployInterface + private $options = []; + + /** ++ * @var TranslationJsConfig ++ */ ++ private $translationJsConfig; ++ ++ /** ++ * @var DeployStrategyFactory ++ */ ++ private $deployStrategyFactory; ++ ++ /** ++ * @var DeployInterface[] ++ */ ++ private $deploys; ++ ++ /** + * @param Filesystem $filesystem + * @param OutputInterface $output + * @param array $options ++ * @param TranslationJsConfig $translationJsConfig ++ * @param DeployStrategyFactory $deployStrategyFactory + */ +- public function __construct(\Magento\Framework\Filesystem $filesystem, OutputInterface $output, $options = []) +- { ++ public function __construct( ++ Filesystem $filesystem, ++ OutputInterface $output, ++ $options = [], ++ TranslationJsConfig $translationJsConfig = null, ++ DeployStrategyFactory $deployStrategyFactory = null ++ ) { + $this->filesystem = $filesystem; + $this->output = $output; + $this->options = $options; ++ $this->translationJsConfig = $translationJsConfig ++ ?: ObjectManager::getInstance()->get(TranslationJsConfig::class); ++ $this->deployStrategyFactory = $deployStrategyFactory ++ ?: ObjectManager::getInstance()->get(DeployStrategyFactory::class); + } + + /** +@@ -67,13 +98,13 @@ private function getStaticDirectory() + */ + public function deploy($area, $themePath, $locale) + { +- if (isset($this->options[Options::DRY_RUN]) && $this->options[Options::DRY_RUN]) { ++ if (!empty($this->options[Options::DRY_RUN])) { + return Cli::RETURN_SUCCESS; + } + + $this->output->writeln("=== {$area} -> {$themePath} -> {$locale} ==="); + +- if (!isset($this->options[self::DEPLOY_BASE_LOCALE])) { ++ if (empty($this->options[self::DEPLOY_BASE_LOCALE])) { + throw new \InvalidArgumentException('Deploy base locale must be set for Quick Deploy'); + } + $processedFiles = 0; +@@ -88,7 +119,7 @@ public function deploy($area, $themePath, $locale) + $this->deleteLocaleResource($newLocalePath); + $this->deleteLocaleResource($newRequireJsPath); + +- if (isset($this->options[Options::SYMLINK_LOCALE]) && $this->options[Options::SYMLINK_LOCALE]) { ++ if (!empty($this->options[Options::SYMLINK_LOCALE])) { + $this->getStaticDirectory()->createSymlink($baseLocalePath, $newLocalePath); + $this->getStaticDirectory()->createSymlink($baseRequireJsPath, $newRequireJsPath); + +@@ -98,14 +129,29 @@ public function deploy($area, $themePath, $locale) + $this->getStaticDirectory()->readRecursively($baseLocalePath), + $this->getStaticDirectory()->readRecursively($baseRequireJsPath) + ); ++ $jsDictionaryEnabled = $this->translationJsConfig->dictionaryEnabled(); + foreach ($localeFiles as $path) { + if ($this->getStaticDirectory()->isFile($path)) { +- $destination = $this->replaceLocaleInPath($path, $baseLocale, $locale); +- $this->getStaticDirectory()->copyFile($path, $destination); +- $processedFiles++; ++ if (!$jsDictionaryEnabled || !$this->isJsDictionary($path)) { ++ $destination = $this->replaceLocaleInPath($path, $baseLocale, $locale); ++ $this->getStaticDirectory()->copyFile($path, $destination); ++ $processedFiles++; ++ } + } + } + ++ if ($jsDictionaryEnabled) { ++ $this->getDeploy( ++ DeployStrategyFactory::DEPLOY_STRATEGY_JS_DICTIONARY, ++ [ ++ 'output' => $this->output, ++ 'translationJsConfig' => $this->translationJsConfig ++ ] ++ ) ++ ->deploy($area, $themePath, $locale); ++ $processedFiles++; ++ } ++ + $this->output->writeln("\nSuccessful copied: {$processedFiles} files; errors: {$errorAmount}\n---\n"); + } + +@@ -113,6 +159,32 @@ public function deploy($area, $themePath, $locale) + } + + /** ++ * Get deploy strategy according to required strategy ++ * ++ * @param string $strategy ++ * @param array $params ++ * @return DeployInterface ++ */ ++ private function getDeploy($strategy, $params) ++ { ++ if (empty($this->deploys[$strategy])) { ++ $this->deploys[$strategy] = $this->deployStrategyFactory->create($strategy, $params); ++ } ++ return $this->deploys[$strategy]; ++ } ++ ++ /** ++ * Define if provided path is js dictionary ++ * ++ * @param string $path ++ * @return bool ++ */ ++ private function isJsDictionary($path) ++ { ++ return strpos($path, $this->translationJsConfig->getDictionaryFileName()) !== false; ++ } ++ ++ /** + * @param string $path + * @return void + */ +diff --git a/vendor/magento/module-deploy/Model/DeployStrategyFactory.php b/vendor/magento/module-deploy/Model/DeployStrategyFactory.php +index 536f344..7ba159b 100644 +--- a/vendor/magento/module-deploy/Model/DeployStrategyFactory.php ++++ b/vendor/magento/module-deploy/Model/DeployStrategyFactory.php +@@ -23,6 +23,11 @@ class DeployStrategyFactory + const DEPLOY_STRATEGY_QUICK = 'quick'; + + /** ++ * Strategy for deploying js dictionary ++ */ ++ const DEPLOY_STRATEGY_JS_DICTIONARY = 'js-dictionary'; ++ ++ /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) +@@ -41,6 +46,7 @@ public function create($type, array $arguments = []) + $strategyMap = [ + self::DEPLOY_STRATEGY_STANDARD => Deploy\LocaleDeploy::class, + self::DEPLOY_STRATEGY_QUICK => Deploy\LocaleQuickDeploy::class, ++ self::DEPLOY_STRATEGY_JS_DICTIONARY => Deploy\JsDictionaryDeploy::class + ]; + + if (!isset($strategyMap[$type])) { +diff --git a/vendor/magento/module-deploy/Test/Unit/Model/Deploy/JsDictionaryDeployTest.php b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/JsDictionaryDeployTest.php +new file mode 100644 +index 0000000..2533476 +--- /dev/null ++++ b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/JsDictionaryDeployTest.php +@@ -0,0 +1,103 @@ ++output = $this->getMockBuilder(OutputInterface::class) ++ ->setMethods(['writeln', 'isVeryVerbose']) ++ ->getMockForAbstractClass(); ++ ++ $this->translationJsConfig = $this->getMock(TranslationJsConfig::class, [], [], '', false); ++ $this->translator = $this->getMockForAbstractClass(TranslateInterface::class, [], '', false, false, true); ++ $this->assetRepo = $this->getMock(Repository::class, [], [], '', false); ++ $this->asset = $this->getMockForAbstractClass(Asset::class, [], '', false, false, true); ++ $this->assetPublisher = $this->getMock(Publisher::class, [], [], '', false); ++ ++ $this->model = (new ObjectManager($this))->getObject( ++ JsDictionaryDeploy::class, ++ [ ++ 'translationJsConfig' => $this->translationJsConfig, ++ 'translator' => $this->translator, ++ 'assetRepo' => $this->assetRepo, ++ 'assetPublisher' => $this->assetPublisher, ++ 'output' => $this->output ++ ] ++ ); ++ } ++ ++ public function testDeploy() ++ { ++ $area = 'adminhtml'; ++ $themePath = 'Magento/backend'; ++ $locale = 'uk_UA'; ++ ++ $dictionary = 'js-translation.json'; ++ ++ $this->translationJsConfig->expects(self::once())->method('getDictionaryFileName') ++ ->willReturn($dictionary); ++ ++ $this->translator->expects($this->once())->method('setLocale')->with($locale); ++ $this->translator->expects($this->once())->method('loadData')->with($area, true); ++ ++ $this->assetRepo->expects($this->once())->method('createAsset') ++ ->with( ++ $dictionary, ++ ['area' => $area, 'theme' => $themePath, 'locale' => $locale] ++ ) ++ ->willReturn($this->asset); ++ ++ $this->assetPublisher->expects($this->once())->method('publish'); ++ ++ $this->model->deploy($area, $themePath, $locale); ++ } ++} +diff --git a/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php +index 6c693fe..d50c8ce 100644 +--- a/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php ++++ b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php +@@ -11,7 +11,10 @@ + use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + use Symfony\Component\Console\Output\OutputInterface; + use Magento\Deploy\Console\Command\DeployStaticOptionsInterface as Options; +-use \Magento\Framework\RequireJs\Config as RequireJsConfig; ++use Magento\Framework\RequireJs\Config as RequireJsConfig; ++use Magento\Framework\Translate\Js\Config as TranslationJsConfig; ++use Magento\Deploy\Model\Deploy\JsDictionaryDeploy; ++use Magento\Deploy\Model\DeployStrategyFactory; + + class LocaleQuickDeployTest extends \PHPUnit_Framework_TestCase + { +@@ -25,15 +28,32 @@ class LocaleQuickDeployTest extends \PHPUnit_Framework_TestCase + */ + private $staticDirectoryMock; + ++ /** ++ * @var TranslationJsConfig|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $translationJsConfig; ++ ++ /** ++ * @var JsDictionaryDeploy|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $jsDictionaryDeploy; ++ ++ /** ++ * @var DeployStrategyFactory|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $deployStrategyFactory; ++ + protected function setUp() + { + $this->outputMock = $this->getMockBuilder(OutputInterface::class) +- ->setMethods(['writeln']) ++ ->setMethods(['writeln', 'isVeryVerbose']) + ->getMockForAbstractClass(); +- + $this->staticDirectoryMock = $this->getMockBuilder(WriteInterface::class) + ->setMethods(['createSymlink', 'getAbsolutePath', 'getRelativePath', 'copyFile', 'readRecursively']) + ->getMockForAbstractClass(); ++ $this->translationJsConfig = $this->getMock(TranslationJsConfig::class, [], [], '', false); ++ $this->deployStrategyFactory = $this->getMock(DeployStrategyFactory::class, [], [], '', false); ++ $this->jsDictionaryDeploy = $this->getMock(JsDictionaryDeploy::class, [], [], '', false); + } + + /** +@@ -68,29 +88,53 @@ public function testDeployWithSymlinkStrategy() + + public function testDeployWithCopyStrategy() + { +- + $area = 'adminhtml'; + $themePath = 'Magento/backend'; + $locale = 'uk_UA'; +- $baseLocal = 'en_US'; ++ $baseLocale = 'en_US'; ++ $baseDir = $baseLocale . 'dir'; ++ $file1 = 'file1'; ++ $file2 = 'file2'; ++ $baseFile1 = $baseLocale . $file1; ++ $baseFile2 = $baseLocale . $file2; ++ ++ $dictionary = 'js-translation.json'; ++ $baseDictionary = $baseLocale . $dictionary; + + $this->staticDirectoryMock->expects(self::never())->method('createSymlink'); +- $this->staticDirectoryMock->expects(self::exactly(2))->method('readRecursively')->willReturnMap([ +- ['adminhtml/Magento/backend/en_US', [$baseLocal . 'file1', $baseLocal . 'dir']], +- [RequireJsConfig::DIR_NAME . '/adminhtml/Magento/backend/en_US', [$baseLocal . 'file2']] +- ]); +- $this->staticDirectoryMock->expects(self::exactly(3))->method('isFile')->willReturnMap([ +- [$baseLocal . 'file1', true], +- [$baseLocal . 'dir', false], +- [$baseLocal . 'file2', true], ++ $this->staticDirectoryMock->expects(self::exactly(2))->method('readRecursively')->willReturnMap( ++ [ ++ ['adminhtml/Magento/backend/en_US', [$baseFile1, $baseDir]], ++ [RequireJsConfig::DIR_NAME . '/adminhtml/Magento/backend/en_US', [$baseFile2, $baseDictionary]] ++ ] ++ ); ++ $this->staticDirectoryMock->expects(self::exactly(4))->method('isFile')->willReturnMap([ ++ [$baseFile1, true], ++ [$baseDir, false], ++ [$baseFile2, true], ++ [$baseDictionary, true] + ]); + $this->staticDirectoryMock->expects(self::exactly(2))->method('copyFile')->withConsecutive( +- [$baseLocal . 'file1', $locale . 'file1', null], +- [$baseLocal . 'file2', $locale . 'file2', null] ++ [$baseFile1, $locale . $file1, null], ++ [$baseFile2, $locale . $file2, null] + ); + ++ $this->translationJsConfig->expects(self::exactly(3))->method('getDictionaryFileName') ++ ->willReturn($dictionary); ++ ++ $this->translationJsConfig->expects($this->once())->method('dictionaryEnabled')->willReturn(true); ++ ++ $this->deployStrategyFactory->expects($this->once())->method('create') ++ ->with( ++ DeployStrategyFactory::DEPLOY_STRATEGY_JS_DICTIONARY, ++ ['output' => $this->outputMock, 'translationJsConfig' => $this->translationJsConfig] ++ ) ++ ->willReturn($this->jsDictionaryDeploy); ++ ++ $this->jsDictionaryDeploy->expects($this->once())->method('deploy')->with($area, $themePath, $locale); ++ + $model = $this->getModel([ +- DeployInterface::DEPLOY_BASE_LOCALE => $baseLocal, ++ DeployInterface::DEPLOY_BASE_LOCALE => $baseLocale, + Options::SYMLINK_LOCALE => 0, + ]); + $model->deploy($area, $themePath, $locale); +@@ -107,7 +151,9 @@ private function getModel($options = []) + [ + 'output' => $this->outputMock, + 'staticDirectory' => $this->staticDirectoryMock, +- 'options' => $options ++ 'options' => $options, ++ 'translationJsConfig' => $this->translationJsConfig, ++ 'deployStrategyFactory' => $this->deployStrategyFactory + ] + ); + } diff --git a/patches/MAGETWO-63020__fix_scd_with_multiple_languages__2.1.4.patch b/patches/MAGETWO-63020__fix_scd_with_multiple_languages__2.1.4.patch new file mode 100644 index 00000000..a291f593 --- /dev/null +++ b/patches/MAGETWO-63020__fix_scd_with_multiple_languages__2.1.4.patch @@ -0,0 +1,13 @@ +MAGETWO-63020 +diff --Naur a/vendor/magento/framework/View/Design/Theme/FlyweightFactory.php b/vendor/magento/framework/View/Design/Theme/FlyweightFactory.php +--- a/vendor/magento/framework/View/Design/Theme/FlyweightFactory.php 2017-01-06 20:41:07.000000000 +0000 ++++ b/vendor/magento/framework/View/Design/Theme/FlyweightFactory.php 2017-01-06 20:42:33.000000000 +0000 +@@ -61,7 +61,7 @@ + } else { + $themeModel = $this->_loadByPath($themeKey, $area); + } +- if (!$themeModel->getId()) { ++ if (!$themeModel->getCode()) { + throw new \LogicException("Unable to load theme by specified key: '{$themeKey}'"); + } + $this->_addTheme($themeModel); diff --git a/patches/MAGETWO-63032__skip_unnecessary_write_permission_check__2.1.4.patch b/patches/MAGETWO-63032__skip_unnecessary_write_permission_check__2.1.4.patch new file mode 100644 index 00000000..67c7fb24 --- /dev/null +++ b/patches/MAGETWO-63032__skip_unnecessary_write_permission_check__2.1.4.patch @@ -0,0 +1,20 @@ +MAGETWO-63032 +diff -Naur a/vendor/magento/framework/Console/Cli.php b/vendor/magento/framework/Console/Cli.php +--- a/vendor/magento/framework/Console/Cli.php 2016-09-28 00:46:02.000000000 -0500 ++++ b/vendor/magento/framework/Console/Cli.php 2016-09-28 00:46:38.000000000 -0500 +@@ -56,15 +56,6 @@ + { + $this->serviceManager = \Zend\Mvc\Application::init(require BP . '/setup/config/application.config.php') + ->getServiceManager(); +- $generationDirectoryAccess = new GenerationDirectoryAccess($this->serviceManager); +- if (!$generationDirectoryAccess->check()) { +- $output = new ConsoleOutput(); +- $output->writeln( +- 'Command line user does not have read and write permissions on var/generation directory. Please' +- . ' address this issue before using Magento command line.' +- ); +- exit(0); +- } + /** + * Temporary workaround until the compiler is able to clear the generation directory + * @todo remove after MAGETWO-44493 resolved diff --git a/patches/MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch b/patches/MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch new file mode 100644 index 00000000..6b9b89be --- /dev/null +++ b/patches/MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch @@ -0,0 +1,12 @@ +diff -Nuar a/vendor/colinmollenhour/credis/Client.php b/vendor/colinmollenhour/credis/Client.php +index afbc85d..8368b32 100755 +--- a/vendor/colinmollenhour/credis/Client.php ++++ b/vendor/colinmollenhour/credis/Client.php +@@ -1017,6 +1017,7 @@ class Credis_Client { + } else { + $this->isMulti = TRUE; + $this->redisMulti = call_user_func_array(array($this->redis, $name), $args); ++ return $this; + } + } + else if($name == 'exec' || $name == 'discard') { diff --git a/patches/MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch b/patches/MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch new file mode 100644 index 00000000..15b64511 --- /dev/null +++ b/patches/MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch @@ -0,0 +1,187 @@ +diff -Nuar a/vendor/magento/module-catalog/Block/Product/ImageBlockBuilder.php b/vendor/magento/module-catalog/Block/Product/ImageBlockBuilder.php +index 9fa50c1..1e54cfc 100644 +--- a/vendor/magento/module-catalog/Block/Product/ImageBlockBuilder.php ++++ b/vendor/magento/module-catalog/Block/Product/ImageBlockBuilder.php +@@ -120,12 +120,7 @@ class ImageBlockBuilder + $label = $product->getName(); + } + +- $frame = isset($imageArguments['frame']) ? $imageArguments ['frame'] : null; +- if (empty($frame)) { +- $frame = $this->presentationConfig->getVarValue('Magento_Catalog', 'product_image_white_borders'); +- } +- +- $template = $frame ++ $template = $image['keep_frame'] == 'frame' + ? 'Magento_Catalog::product/image.phtml' + : 'Magento_Catalog::product/image_with_borders.phtml'; + +diff -Nuar a/vendor/magento/module-catalog/Helper/Image.php b/vendor/magento/module-catalog/Helper/Image.php +index 039c260..9a7c13e 100644 +--- a/vendor/magento/module-catalog/Helper/Image.php ++++ b/vendor/magento/module-catalog/Helper/Image.php +@@ -826,7 +826,7 @@ class Image extends AbstractHelper + if ($frame === null) { + $frame = $this->getConfigView()->getVarValue('Magento_Catalog', 'product_image_white_borders'); + } +- return (bool)$frame; ++ return $frame; + } + + /** +diff -Nuar a/vendor/magento/module-catalog/Model/Product/Image.php b/vendor/magento/module-catalog/Model/Product/Image.php +index c1c10da..b543571 100644 +--- a/vendor/magento/module-catalog/Model/Product/Image.php ++++ b/vendor/magento/module-catalog/Model/Product/Image.php +@@ -320,7 +320,7 @@ class Image extends \Magento\Framework\Model\AbstractModel + */ + public function setKeepAspectRatio($keep) + { +- $this->_keepAspectRatio = (bool)$keep; ++ $this->_keepAspectRatio = $keep && $keep !== 'false'; + return $this; + } + +@@ -330,7 +330,7 @@ class Image extends \Magento\Framework\Model\AbstractModel + */ + public function setKeepFrame($keep) + { +- $this->_keepFrame = (bool)$keep; ++ $this->_keepFrame = $keep && $keep !== 'false'; + return $this; + } + +@@ -340,7 +340,7 @@ class Image extends \Magento\Framework\Model\AbstractModel + */ + public function setKeepTransparency($keep) + { +- $this->_keepTransparency = (bool)$keep; ++ $this->_keepTransparency = $keep && $keep !== 'false'; + return $this; + } + +@@ -350,7 +350,7 @@ class Image extends \Magento\Framework\Model\AbstractModel + */ + public function setConstrainOnly($flag) + { +- $this->_constrainOnly = (bool)$flag; ++ $this->_constrainOnly = $flag && $flag !== 'false'; + return $this; + } + +diff -Nuar a/vendor/magento/module-catalog/Model/Product/Image/ParamsBuilder.php b/vendor/magento/module-catalog/Model/Product/Image/ParamsBuilder.php +index 603f3e8..7aa0c32 100644 +--- a/vendor/magento/module-catalog/Model/Product/Image/ParamsBuilder.php ++++ b/vendor/magento/module-catalog/Model/Product/Image/ParamsBuilder.php +@@ -70,6 +70,21 @@ class ParamsBuilder + } + + /** ++ * Reads boolean value from arguments ++ * ++ * Performs correct boolean cast ++ * ++ * @param array $args ++ * @param string $key ++ * @param bool $default ++ * @return bool|null ++ */ ++ private function readBoolValue(array $args, $key, $default = null) ++ { ++ return isset($args[$key]) ? $args[$key] && $args[$key] !== 'false' : $default; ++ } ++ ++ /** + * @param array $imageArguments + * @return array + * @SuppressWarnings(PHPMD.NPathComplexity) +@@ -82,9 +97,11 @@ class ParamsBuilder + $width = isset($imageArguments['width']) ? $imageArguments['width'] : null; + $height = isset($imageArguments['height']) ? $imageArguments['height'] : null; + +- $frame = !empty($imageArguments['frame']) +- ? $imageArguments['frame'] +- : $this->keepFrame; ++ $frame = $this->readBoolValue($imageArguments, 'frame'); ++ ++ if ($frame === null) { ++ $frame = $this->presentationConfig->getVarValue('Magento_Catalog', 'product_image_white_borders'); ++ } + + $constrain = !empty($imageArguments['constrain']) + ? $imageArguments['constrain'] +diff -Nuar a/vendor/magento/module-catalog/etc/view.xml b/vendor/magento/module-catalog/etc/view.xml +index 8c7500d..f2f004c 100644 +--- a/vendor/magento/module-catalog/etc/view.xml ++++ b/vendor/magento/module-catalog/etc/view.xml +@@ -7,6 +7,6 @@ + --> + + +- 1 ++ 0 + + +diff -Nuar a/vendor/magento/module-catalog/view/frontend/templates/product/image_with_borders.phtml b/vendor/magento/module-catalog/view/frontend/templates/product/image_with_borders.phtml +index c3280c5..4d7a825 100644 +--- a/vendor/magento/module-catalog/view/frontend/templates/product/image_with_borders.phtml ++++ b/vendor/magento/module-catalog/view/frontend/templates/product/image_with_borders.phtml +@@ -13,7 +13,5 @@ + getCustomAttributes(); ?> + src="https://github.com/%3C?php%20/*%20@escapeNotVerified%20*/%20echo%20$block-%3EgetImageUrl();%20?%3E" +- width="getResizedImageWidth(); ?>" +- height="getResizedImageHeight(); ?>" + alt="stripTags($block->getLabel(), null, true); ?>"/> + +diff -Nuar a/vendor/magento/theme-adminhtml-backend/etc/view.xml b/vendor/magento/theme-adminhtml-backend/etc/view.xml +index de6b0cf..f352974 100644 +--- a/vendor/magento/theme-adminhtml-backend/etc/view.xml ++++ b/vendor/magento/theme-adminhtml-backend/etc/view.xml +@@ -9,6 +9,9 @@ + + 1MB + ++ ++ 0 ++ + + + +diff -Nuar a/lib/web/mage/gallery/gallery.less b/lib/web/mage/gallery/gallery.less +index fc9d09e..2fd877d 100644 +--- a/lib/web/mage/gallery/gallery.less ++++ b/lib/web/mage/gallery/gallery.less +@@ -691,23 +691,15 @@ + } + + .fotorama__nav-wrap { +- .fotorama_vertical_ratio { +- .fotorama__img { +- .translateY(-50%); +- height: auto; +- position: absolute; +- top: 50%; +- width: 100%; +- } +- } +- .fotorama_horizontal_ratio { +- .fotorama__img { +- .translateX(-50%); +- height: 100%; +- left: 50%; +- position: absolute; +- width: auto; +- } ++ .fotorama__img { ++ bottom: 0; ++ height: auto; ++ left: 0; ++ margin: auto; ++ max-width: 100%; ++ position: absolute; ++ right: 0; ++ top: 0; + } + } + diff --git a/patches/MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch b/patches/MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch new file mode 100644 index 00000000..fd1697ee --- /dev/null +++ b/patches/MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch @@ -0,0 +1,169 @@ +diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Factory.php b/vendor/magento/framework/App/Cache/Frontend/Factory.php +index a71ff27b07..e1c65ef572 100644 +--- a/vendor/magento/framework/App/Cache/Frontend/Factory.php ++++ b/vendor/magento/framework/App/Cache/Frontend/Factory.php +@@ -145,15 +145,17 @@ class Factory + $result = $this->_objectManager->create( + 'Magento\Framework\Cache\Frontend\Adapter\Zend', + [ +- 'frontend' => \Zend_Cache::factory( +- $frontend['type'], +- $backend['type'], +- $frontend, +- $backend['options'], +- true, +- true, +- true +- ) ++ 'frontendFactory' => function () use ($frontend, $backend) { ++ return \Zend_Cache::factory( ++ $frontend['type'], ++ $backend['type'], ++ $frontend, ++ $backend['options'], ++ true, ++ true, ++ true ++ ); ++ } + ] + ); + $result = $this->_applyDecorators($result); +diff -Nuar a/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php b/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php +index 5d72ee6a1e..fe9dc2f453 100644 +--- a/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php ++++ b/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php +@@ -16,11 +16,32 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + protected $_frontend; + + /** +- * @param \Zend_Cache_Core $frontend ++ * Factory that creates the \Zend_Cache_Cores ++ * ++ * @var \Closure ++ */ ++ private $_frontendFactory; ++ ++ /** ++ * The pid that owns the $_frontend object ++ * ++ * @var int ++ */ ++ private $_pid; ++ ++ /** ++ * @var \Zend_Cache_Core[] ++ */ ++ static private $_parentFrontends = []; ++ ++ /** ++ * @param \Closure $frontendFactory + */ +- public function __construct(\Zend_Cache_Core $frontend) ++ public function __construct(\Closure $frontendFactory) + { +- $this->_frontend = $frontend; ++ $this->_frontendFactory = $frontendFactory; ++ $this->_frontend = $frontendFactory(); ++ $this->_pid = getmypid(); + } + + /** +@@ -28,7 +49,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function test($identifier) + { +- return $this->_frontend->test($this->_unifyId($identifier)); ++ return $this->_getFrontEnd()->test($this->_unifyId($identifier)); + } + + /** +@@ -36,7 +57,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function load($identifier) + { +- return $this->_frontend->load($this->_unifyId($identifier)); ++ return $this->_getFrontEnd()->load($this->_unifyId($identifier)); + } + + /** +@@ -44,7 +65,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function save($data, $identifier, array $tags = [], $lifeTime = null) + { +- return $this->_frontend->save($data, $this->_unifyId($identifier), $this->_unifyIds($tags), $lifeTime); ++ return $this->_getFrontEnd()->save($data, $this->_unifyId($identifier), $this->_unifyIds($tags), $lifeTime); + } + + /** +@@ -52,7 +73,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function remove($identifier) + { +- return $this->_frontend->remove($this->_unifyId($identifier)); ++ return $this->_getFrontEnd()->remove($this->_unifyId($identifier)); + } + + /** +@@ -76,7 +97,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + "Magento cache frontend does not support the cleaning mode '{$mode}'." + ); + } +- return $this->_frontend->clean($mode, $this->_unifyIds($tags)); ++ return $this->_getFrontEnd()->clean($mode, $this->_unifyIds($tags)); + } + + /** +@@ -84,7 +105,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function getBackend() + { +- return $this->_frontend->getBackend(); ++ return $this->_getFrontEnd()->getBackend(); + } + + /** +@@ -92,7 +113,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function getLowLevelFrontend() + { +- return $this->_frontend; ++ return $this->_getFrontEnd(); + } + + /** +@@ -119,4 +140,34 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + } + return $ids; + } ++ ++ /** ++ * getter for _frontend so that we can support fork()s ++ * ++ * @return \Zend_Cache_Core ++ */ ++ private function _getFrontEnd() ++ { ++ if (getmypid() === $this->_pid) { ++ return $this->_frontend; ++ } ++ static::$_parentFrontends[] = $this->_frontend; ++ $frontendFactory = $this->_frontendFactory; ++ $this->_frontend = $frontendFactory(); ++ $this->_pid = getmypid(); ++ return $this->_frontend; ++ } ++ ++ /** ++ * If the current _frontend is owned by a different pid, add it to $_parentFrontends so that the ++ * destructor isn't called on it. ++ * ++ * @return void ++ */ ++ public function __destruct() ++ { ++ if (getmypid() !== $this->_pid) { ++ static::$_parentFrontends[] = $this->_frontend; ++ } ++ } + } diff --git a/patches/MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch b/patches/MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch new file mode 100644 index 00000000..8c994b25 --- /dev/null +++ b/patches/MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch @@ -0,0 +1,169 @@ +diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Factory.php b/vendor/magento/framework/App/Cache/Frontend/Factory.php +index 4c539da096..8477d94f28 100644 +--- a/vendor/magento/framework/App/Cache/Frontend/Factory.php ++++ b/vendor/magento/framework/App/Cache/Frontend/Factory.php +@@ -148,15 +148,17 @@ class Factory + $result = $this->_objectManager->create( + \Magento\Framework\Cache\Frontend\Adapter\Zend::class, + [ +- 'frontend' => \Zend_Cache::factory( +- $frontend['type'], +- $backend['type'], +- $frontend, +- $backend['options'], +- true, +- true, +- true +- ) ++ 'frontendFactory' => function () use ($frontend, $backend) { ++ return \Zend_Cache::factory( ++ $frontend['type'], ++ $backend['type'], ++ $frontend, ++ $backend['options'], ++ true, ++ true, ++ true ++ ); ++ } + ] + ); + $result = $this->_applyDecorators($result); +diff -Nuar a/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php b/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php +index c8917a0996..fe9dc2f453 100644 +--- a/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php ++++ b/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php +@@ -16,11 +16,32 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + protected $_frontend; + + /** +- * @param \Zend_Cache_Core $frontend ++ * Factory that creates the \Zend_Cache_Cores ++ * ++ * @var \Closure ++ */ ++ private $_frontendFactory; ++ ++ /** ++ * The pid that owns the $_frontend object ++ * ++ * @var int ++ */ ++ private $_pid; ++ ++ /** ++ * @var \Zend_Cache_Core[] ++ */ ++ static private $_parentFrontends = []; ++ ++ /** ++ * @param \Closure $frontendFactory + */ +- public function __construct(\Zend_Cache_Core $frontend) ++ public function __construct(\Closure $frontendFactory) + { +- $this->_frontend = $frontend; ++ $this->_frontendFactory = $frontendFactory; ++ $this->_frontend = $frontendFactory(); ++ $this->_pid = getmypid(); + } + + /** +@@ -28,7 +49,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function test($identifier) + { +- return $this->_frontend->test($this->_unifyId($identifier)); ++ return $this->_getFrontEnd()->test($this->_unifyId($identifier)); + } + + /** +@@ -36,7 +57,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function load($identifier) + { +- return $this->_frontend->load($this->_unifyId($identifier)); ++ return $this->_getFrontEnd()->load($this->_unifyId($identifier)); + } + + /** +@@ -44,7 +65,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function save($data, $identifier, array $tags = [], $lifeTime = null) + { +- return $this->_frontend->save($data, $this->_unifyId($identifier), $this->_unifyIds($tags), $lifeTime); ++ return $this->_getFrontEnd()->save($data, $this->_unifyId($identifier), $this->_unifyIds($tags), $lifeTime); + } + + /** +@@ -52,7 +73,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function remove($identifier) + { +- return $this->_frontend->remove($this->_unifyId($identifier)); ++ return $this->_getFrontEnd()->remove($this->_unifyId($identifier)); + } + + /** +@@ -76,7 +97,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + "Magento cache frontend does not support the cleaning mode '{$mode}'." + ); + } +- return $this->_frontend->clean($mode, $this->_unifyIds($tags)); ++ return $this->_getFrontEnd()->clean($mode, $this->_unifyIds($tags)); + } + + /** +@@ -84,7 +105,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function getBackend() + { +- return $this->_frontend->getBackend(); ++ return $this->_getFrontEnd()->getBackend(); + } + + /** +@@ -92,7 +113,7 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + */ + public function getLowLevelFrontend() + { +- return $this->_frontend; ++ return $this->_getFrontEnd(); + } + + /** +@@ -119,4 +140,34 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface + } + return $ids; + } ++ ++ /** ++ * getter for _frontend so that we can support fork()s ++ * ++ * @return \Zend_Cache_Core ++ */ ++ private function _getFrontEnd() ++ { ++ if (getmypid() === $this->_pid) { ++ return $this->_frontend; ++ } ++ static::$_parentFrontends[] = $this->_frontend; ++ $frontendFactory = $this->_frontendFactory; ++ $this->_frontend = $frontendFactory(); ++ $this->_pid = getmypid(); ++ return $this->_frontend; ++ } ++ ++ /** ++ * If the current _frontend is owned by a different pid, add it to $_parentFrontends so that the ++ * destructor isn't called on it. ++ * ++ * @return void ++ */ ++ public function __destruct() ++ { ++ if (getmypid() !== $this->_pid) { ++ static::$_parentFrontends[] = $this->_frontend; ++ } ++ } + } diff --git a/patches/MAGETWO-82752__reload_js_translation_data__2.2.0.patch b/patches/MAGETWO-82752__reload_js_translation_data__2.2.0.patch new file mode 100644 index 00000000..6eca987b --- /dev/null +++ b/patches/MAGETWO-82752__reload_js_translation_data__2.2.0.patch @@ -0,0 +1,11 @@ +--- a/vendor/magento/module-translation/Model/Json/PreProcessor.php ++++ b/vendor/magento/module-translation/Model/Json/PreProcessor.php +@@ -77,7 +77,7 @@ class PreProcessor implements PreProcessorInterface + if ($context instanceof FallbackContext) { + $themePath = $context->getThemePath(); + $areaCode = $context->getAreaCode(); +- $this->translate->setLocale($context->getLocale()); ++ $this->translate->setLocale($context->getLocale())->loadData($areaCode); + } + + $area = $this->areaList->getArea($areaCode); diff --git a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.10.patch b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.10.patch new file mode 100644 index 00000000..0b7b5885 --- /dev/null +++ b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.10.patch @@ -0,0 +1,1190 @@ +commit e9aa4e18cfb76b37ce00f6f45d507d22e1e89550 +Author: Viktor Paladiichuk +Date: Tue Nov 28 15:29:54 2017 +0200 + + MAGETWO-84444: Mview does not work with Staging + +diff -Nuar a/vendor/magento/module-catalog-inventory/etc/mview.xml b/vendor/magento/module-catalog-inventory/etc/mview.xml +index 58a051a3d0e..3dd8419d7e3 100644 +--- a/vendor/magento/module-catalog-inventory/etc/mview.xml ++++ b/vendor/magento/module-catalog-inventory/etc/mview.xml +@@ -5,10 +5,13 @@ + * See COPYING.txt for license details. + */ + --> +- ++ + + + ++
++
+ + + +diff -Nuar a/vendor/magento/module-indexer/Setup/RecurringData.php b/vendor/magento/module-indexer/Setup/RecurringData.php +new file mode 100644 +index 00000000000..38ea0e5b79e +--- /dev/null ++++ b/vendor/magento/module-indexer/Setup/RecurringData.php +@@ -0,0 +1,56 @@ ++indexerFactory = $indexerFactory; ++ $this->configInterface = $configInterface; ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) ++ { ++ foreach (array_keys($this->configInterface->getIndexers()) as $indexerId) { ++ $indexer = $this->indexerFactory->create()->load($indexerId); ++ if ($indexer->isScheduled()) { ++ $indexer->getView()->unsubscribe()->subscribe(); ++ } ++ } ++ } ++} +diff -Nuar a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +index aae82938c83..545689a4391 100644 +--- a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php ++++ b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +@@ -62,7 +62,7 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + + $this->resourceMock->expects($this->any()) + ->method('getTableName') +- ->willReturn($this->tableName); ++ ->will($this->returnArgument(0)); + + $this->model = new Subscription( + $this->resourceMock, +@@ -89,11 +89,15 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + $this->assertEquals('columnName', $this->model->getColumnName()); + } + ++ /** ++ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) ++ */ + public function testCreate() + { + $triggerName = 'trigger_name'; + $this->resourceMock->expects($this->atLeastOnce())->method('getTriggerName')->willReturn($triggerName); + $triggerMock = $this->getMockBuilder('Magento\Framework\DB\Ddl\Trigger') ++ ->setMethods(['setName', 'getName', 'setTime', 'setEvent', 'setTable', 'addStatement']) + ->disableOriginalConstructor() + ->getMock(); + $triggerMock->expects($this->exactly(3)) +@@ -114,8 +118,35 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + ->method('setTable') + ->with($this->tableName) + ->will($this->returnSelf()); +- $triggerMock->expects($this->exactly(6)) ++ ++ $triggerMock->expects($this->at(4)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(5)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(11)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(12)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(18)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (OLD.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(19)) + ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (OLD.columnName);") + ->will($this->returnSelf()); + + $changelogMock = $this->getMockForAbstractClass( +diff -Nuar a/vendor/magento/framework/Mview/View/Subscription.php b/vendor/magento/framework/Mview/View/Subscription.php +index c3da91c8331..3c4bd1dce2d 100644 +--- a/vendor/magento/framework/Mview/View/Subscription.php ++++ b/vendor/magento/framework/Mview/View/Subscription.php +@@ -10,6 +10,8 @@ namespace Magento\Framework\Mview\View; + + use Magento\Framework\App\ResourceConnection; + use Magento\Framework\DB\Ddl\Trigger; ++use Magento\Framework\DB\Ddl\TriggerFactory; ++use Magento\Framework\Mview\ViewInterface; + + class Subscription implements SubscriptionInterface + { +@@ -21,12 +23,12 @@ class Subscription implements SubscriptionInterface + protected $connection; + + /** +- * @var \Magento\Framework\DB\Ddl\TriggerFactory ++ * @var TriggerFactory + */ + protected $triggerFactory; + + /** +- * @var \Magento\Framework\Mview\View\CollectionInterface ++ * @var CollectionInterface + */ + protected $viewCollection; + +@@ -58,20 +60,31 @@ class Subscription implements SubscriptionInterface + protected $resource; + + /** ++ * List of columns that can be updated in a subscribed table ++ * without creating a new change log entry ++ * ++ * @var array ++ */ ++ private $ignoredUpdateColumns = []; ++ ++ /** + * @param ResourceConnection $resource +- * @param \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory +- * @param \Magento\Framework\Mview\View\CollectionInterface $viewCollection +- * @param \Magento\Framework\Mview\ViewInterface $view ++ * @param TriggerFactory $triggerFactory ++ * @param CollectionInterface $viewCollection ++ * @param ViewInterface $view + * @param string $tableName + * @param string $columnName ++ * @param array $ignoredUpdateColumns ++ * @throws \DomainException + */ + public function __construct( + ResourceConnection $resource, +- \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory, +- \Magento\Framework\Mview\View\CollectionInterface $viewCollection, +- \Magento\Framework\Mview\ViewInterface $view, ++ TriggerFactory $triggerFactory, ++ CollectionInterface $viewCollection, ++ ViewInterface $view, + $tableName, +- $columnName ++ $columnName, ++ array $ignoredUpdateColumns = [] + ) { + $this->connection = $resource->getConnection(); + $this->triggerFactory = $triggerFactory; +@@ -80,12 +93,14 @@ class Subscription implements SubscriptionInterface + $this->tableName = $tableName; + $this->columnName = $columnName; + $this->resource = $resource; ++ $this->ignoredUpdateColumns = $ignoredUpdateColumns; + } + + /** +- * Create subsciption ++ * Create subscription + * +- * @return \Magento\Framework\Mview\View\SubscriptionInterface ++ * @return SubscriptionInterface ++ * @throws \InvalidArgumentException + */ + public function create() + { +@@ -102,7 +117,7 @@ class Subscription implements SubscriptionInterface + + // Add statements for linked views + foreach ($this->getLinkedViews() as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + $trigger->addStatement($this->buildStatement($event, $view->getChangelog())); + } + +@@ -116,7 +131,8 @@ class Subscription implements SubscriptionInterface + /** + * Remove subscription + * +- * @return \Magento\Framework\Mview\View\SubscriptionInterface ++ * @return SubscriptionInterface ++ * @throws \InvalidArgumentException + */ + public function remove() + { +@@ -131,7 +147,7 @@ class Subscription implements SubscriptionInterface + + // Add statements for linked views + foreach ($this->getLinkedViews() as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + $trigger->addStatement($this->buildStatement($event, $view->getChangelog())); + } + +@@ -154,10 +170,10 @@ class Subscription implements SubscriptionInterface + protected function getLinkedViews() + { + if (!$this->linkedViews) { +- $viewList = $this->viewCollection->getViewsByStateMode(\Magento\Framework\Mview\View\StateInterface::MODE_ENABLED); ++ $viewList = $this->viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); + + foreach ($viewList as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + // Skip the current view + if ($view->getId() == $this->getView()->getId()) { + continue; +@@ -175,35 +191,58 @@ class Subscription implements SubscriptionInterface + } + + /** +- * Build trigger statement for INSER, UPDATE, DELETE events ++ * Build trigger statement for INSERT, UPDATE, DELETE events + * + * @param string $event +- * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog ++ * @param ChangelogInterface $changelog + * @return string + */ + protected function buildStatement($event, $changelog) + { + switch ($event) { + case Trigger::EVENT_INSERT: ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);'; ++ break; ++ + case Trigger::EVENT_UPDATE: +- return sprintf( +- "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);", +- $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), +- $this->connection->quoteIdentifier($changelog->getColumnName()), +- $this->connection->quoteIdentifier($this->getColumnName()) +- ); ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);'; ++ ++ if ($this->connection->isTableExists($this->getTableName()) ++ && $describe = $this->connection->describeTable($this->getTableName()) ++ ) { ++ $columnNames = array_column($describe, 'COLUMN_NAME'); ++ $columnNames = array_diff($columnNames, $this->ignoredUpdateColumns); ++ if ($columnNames) { ++ $columns = []; ++ foreach ($columnNames as $columnName) { ++ $columns[] = sprintf( ++ 'NEW.%1$s != OLD.%1$s', ++ $this->connection->quoteIdentifier($columnName) ++ ); ++ } ++ $trigger = sprintf( ++ "IF (%s) THEN %s END IF;", ++ implode(' OR ', $columns), ++ $trigger ++ ); ++ } ++ } ++ break; + + case Trigger::EVENT_DELETE: +- return sprintf( +- "INSERT IGNORE INTO %s (%s) VALUES (OLD.%s);", +- $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), +- $this->connection->quoteIdentifier($changelog->getColumnName()), +- $this->connection->quoteIdentifier($this->getColumnName()) +- ); ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (OLD.%s);'; ++ break; + + default: + return ''; + } ++ ++ return sprintf( ++ $trigger, ++ $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), ++ $this->connection->quoteIdentifier($changelog->getColumnName()), ++ $this->connection->quoteIdentifier($this->getColumnName()) ++ ); + } + + /** +@@ -225,7 +264,7 @@ class Subscription implements SubscriptionInterface + /** + * Retrieve View related to subscription + * +- * @return \Magento\Framework\Mview\ViewInterface ++ * @return ViewInterface + * @codeCoverageIgnore + */ + public function getView() +diff -Nuar a/vendor/magento/framework/Mview/etc/mview.xsd b/vendor/magento/framework/Mview/etc/mview.xsd +index 1dad5b3f415..b7d6bbdde68 100644 +--- a/vendor/magento/framework/Mview/etc/mview.xsd ++++ b/vendor/magento/framework/Mview/etc/mview.xsd +@@ -106,7 +106,7 @@ + + + +- Subscription model must be a valid PHP class or interface name. ++ DEPRECATED. Subscription model must be a valid PHP class or interface name. + + + +commit a85bf456be38fa943e4144309cd330e199d1e4b6 +Author: Viktor Paladiichuk +Date: Tue Nov 28 15:30:48 2017 +0200 + + MAGETWO-84444: Mview does not work with Staging + +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +index b3e920fb87..f9b1d1a32a 100644 +--- a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +@@ -6,22 +6,16 @@ + namespace Magento\CatalogStaging\Model\Mview\View\Category\Attribute; + + use Magento\Catalog\Api\Data\CategoryInterface; +-use Magento\Framework\DB\Ddl\Trigger; + use Magento\Framework\App\ResourceConnection; + use Magento\Framework\EntityManager\MetadataPool; + + /** +- * Class Subscription ++ * Class Subscription implements statement building for staged category entity attribute subscription + * @package Magento\CatalogStaging\Model\Mview\View\Category\Attribute + */ +-class Subscription extends \Magento\Framework\Mview\View\Subscription ++class Subscription extends \Magento\CatalogStaging\Model\Mview\View\Attribute\Subscription + { + /** +- * @var \Magento\Framework\EntityManager\EntityMetadata +- */ +- protected $entityMetadata; +- +- /** + * @param ResourceConnection $resource + * @param \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory + * @param \Magento\Framework\Mview\View\CollectionInterface $viewCollection +@@ -29,7 +23,8 @@ class Subscription extends \Magento\Framework\Mview\View\Subscription + * @param string $tableName + * @param string $columnName + * @param MetadataPool $metadataPool +- * @throws \Exception ++ * @param string|null $entityInterface ++ * @param array $ignoredUpdateColumns + */ + public function __construct( + ResourceConnection $resource, +@@ -38,50 +33,20 @@ class Subscription extends \Magento\Framework\Mview\View\Subscription + \Magento\Framework\Mview\ViewInterface $view, + $tableName, + $columnName, +- MetadataPool $metadataPool ++ MetadataPool $metadataPool, ++ $entityInterface = CategoryInterface::class, ++ $ignoredUpdateColumns = [] + ) { +- parent::__construct($resource, $triggerFactory, $viewCollection, $view, $tableName, $columnName); +- $this->entityMetadata = $metadataPool->getMetadata(CategoryInterface::class); +- } +- +- /** +- * Build trigger statement for INSERT, UPDATE, DELETE events +- * +- * @param string $event +- * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog +- * @return string +- */ +- protected function buildStatement($event, $changelog) +- { +- $triggerBody = null; +- switch ($event) { +- case Trigger::EVENT_INSERT: +- case Trigger::EVENT_UPDATE: +- $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = NEW.%5\$s;"; +- break; +- case Trigger::EVENT_DELETE: +- $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = OLD.%5\$s;"; +- break; +- default: +- break; +- } +- $params = [ +- $this->connection->quoteIdentifier( +- $this->resource->getTableName($changelog->getName()) +- ), +- $this->connection->quoteIdentifier( +- $changelog->getColumnName() +- ), +- $this->connection->quoteIdentifier( +- $this->entityMetadata->getIdentifierField() +- ), +- $this->connection->quoteIdentifier( +- $this->resource->getTableName($this->entityMetadata->getEntityTable()) +- ), +- $this->connection->quoteIdentifier( +- $this->entityMetadata->getLinkField() +- ) +- ]; +- return vsprintf($triggerBody, $params); ++ parent::__construct( ++ $resource, ++ $triggerFactory, ++ $viewCollection, ++ $view, ++ $tableName, ++ $columnName, ++ $metadataPool, ++ $entityInterface, ++ $ignoredUpdateColumns ++ ); + } + } +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +index 96c0f71164..7c841853f9 100644 +--- a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +@@ -11,34 +11,43 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory + { + /** + * @var array ++ * @deprecated 2.2.0 + */ + private $stagingEntityTables = ['catalog_product_entity', 'catalog_category_entity']; + + /** + * @var array ++ * @deprecated 2.2.0 + */ + private $versionTables; + + /** ++ * @var string[] ++ */ ++ private $subscriptionModels = []; ++ ++ /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\CatalogStaging\Model\VersionTables $versionTables ++ * @param array $subscriptionModels + */ + public function __construct( + \Magento\Framework\ObjectManagerInterface $objectManager, +- \Magento\CatalogStaging\Model\VersionTables $versionTables ++ \Magento\CatalogStaging\Model\VersionTables $versionTables, ++ $subscriptionModels = [] + ) { + parent::__construct($objectManager); + $this->versionTables = $versionTables; ++ $this->subscriptionModels = $subscriptionModels; + } + + /** +- * @param array $data +- * @return \Magento\Framework\Mview\View\CollectionInterface ++ * {@inheritdoc} + */ + public function create(array $data = []) + { +- if ($this->isStagingTable($data)) { +- $data['columnName'] = 'row_id'; ++ if (isset($data['tableName']) && isset($this->subscriptionModels[$data['tableName']])) { ++ $data['subscriptionModel'] = $this->subscriptionModels[$data['tableName']]; + } + return parent::create($data); + } +@@ -46,6 +55,7 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory + /** + * @param array $data + * @return bool ++ * @deprecated + */ + protected function isStagingTable(array $data = []) + { +diff -Nuar a/vendor/magento/module-catalog-staging/Model/VersionTables.php b/vendor/magento/module-catalog-staging/Model/VersionTables.php +index c845f98b31..242aaf2f25 100644 +--- a/vendor/magento/module-catalog-staging/Model/VersionTables.php ++++ b/vendor/magento/module-catalog-staging/Model/VersionTables.php +@@ -5,6 +5,11 @@ + */ + namespace Magento\CatalogStaging\Model; + ++/** ++ * Class VersionTables stores information about staged tables. ++ * ++ * @package Magento\CatalogStaging\Model ++ */ + class VersionTables extends \Magento\Framework\DataObject + { + /** +diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +index d595784134..d5e78767bd 100644 +--- a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php ++++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +@@ -17,11 +17,6 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase + protected $objectManagerMock; + + /** +- * @var \PHPUnit_Framework_MockObject_MockObject +- */ +- protected $versionTablesrMock; +- +- /** + * @var \Magento\CatalogStaging\Model\Mview\View\SubscriptionFactory + */ + protected $model; +@@ -29,110 +24,45 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase + protected function setUp() + { + $objectManager = new ObjectManager($this); +- +- $this->objectManagerMock = $this->getMockBuilder('Magento\Framework\ObjectManagerInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->versionTablesrMock = $this->getMockBuilder('Magento\CatalogStaging\Model\VersionTables') ++ $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->model = $objectManager->getObject( + SubscriptionFactory::class, + [ + 'objectManager' => $this->objectManagerMock, +- 'versionTables' => $this->versionTablesrMock ++ 'subscriptionModels' => [ ++ 'catalog_product_entity_int' => 'ProductEntityIntSubscription' ++ ] + ] + ); + } +- + public function testCreate() + { + $data = ['tableName' => 'catalog_product_entity_int', 'columnName' => 'entity_id']; +- $versionTables = ['catalog_product_entity_int']; +- + $expectedData = $data; +- $expectedData['columnName'] = 'row_id'; +- +- $this->versionTablesrMock->expects($this->once()) +- ->method('getVersionTables') +- ->willReturn($versionTables); +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') ++ $expectedData['columnName'] = 'entity_id'; ++ $subscriptionMock = $this->getMockBuilder(\Magento\Framework\Mview\View\SubscriptionInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->once()) + ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) ++ ->with('ProductEntityIntSubscription', $expectedData) + ->willReturn($subscriptionMock); +- + $result = $this->model->create($data); + $this->assertEquals($subscriptionMock, $result); + } +- + public function testCreateNoTableName() + { + $data = ['columnName' => 'entity_id']; +- +- $expectedData = $data; +- +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->objectManagerMock->expects($this->once()) +- ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) +- ->willReturn($subscriptionMock); +- +- $result = $this->model->create($data); +- $this->assertEquals($subscriptionMock, $result); +- } +- +- /** +- * @param $stagingEntityTable +- * @dataProvider tablesDataProvider +- */ +- public function testCreateStagingEntityTables($stagingEntityTable) +- { +- $data = ['tableName' => $stagingEntityTable, 'columnName' => 'entity_id']; +- + $expectedData = $data; +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') ++ $subscriptionMock = $this->getMockBuilder(\Magento\Framework\Mview\View\SubscriptionInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) + ->willReturn($subscriptionMock); +- +- $result = $this->model->create($data); +- $this->assertEquals($subscriptionMock, $result); +- } +- +- public static function tablesDataProvider() +- { +- return [ +- ['catalog_product_entity'], +- ['catalog_category_entity'] +- ]; +- } +- +- public function testCreateNoVersionTable() +- { +- $data = ['tableName' => 'not_existed_table', 'columnName' => 'entity_id']; +- $versionTables = ['catalog_product_entity_int']; +- +- $expectedData = $data; +- +- $this->versionTablesrMock->expects($this->once()) +- ->method('getVersionTables') +- ->willReturn($versionTables); +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->objectManagerMock->expects($this->once()) +- ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) +- ->willReturn($subscriptionMock); +- + $result = $this->model->create($data); + $this->assertEquals($subscriptionMock, $result); + } +diff -Nuar a/vendor/magento/module-catalog-staging/etc/di.xml b/vendor/magento/module-catalog-staging/etc/di.xml +index aea000b42f..178b0bcf63 100644 +--- a/vendor/magento/module-catalog-staging/etc/di.xml ++++ b/vendor/magento/module-catalog-staging/etc/di.xml +@@ -56,6 +56,35 @@ + + + ++ ++ ++ Magento\Catalog\Api\Data\CategoryInterface ++ ++ ++ ++ ++ Magento\Catalog\Api\Data\ProductInterface ++ ++ ++ ++ ++ ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ ++ ++ + + + +diff -Nuar a/vendor/magento/module-catalog-staging/etc/mview.xml b/vendor/magento/module-catalog-staging/etc/mview.xml +deleted file mode 100644 +index 45bb6589b6..0000000000 +--- a/vendor/magento/module-catalog-staging/etc/mview.xml ++++ /dev/null +@@ -1,23 +0,0 @@ +- +- +- +- +- +-
+-
+-
+-
+-
+- +- +- +- +-
+- +- +- +commit 0193f89517fc864c9ab5acd4b518f03b2c796a2f +Author: Viktor Paladiichuk +Date: Tue Nov 28 15:45:17 2017 +0200 + + MAGETWO-84444: Mview does not work with Staging + +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php +new file mode 100644 +index 0000000000..7c549538c7 +--- /dev/null ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php +@@ -0,0 +1,100 @@ ++entityMetadata = $metadataPool->getMetadata($entityInterface); ++ } ++ ++ /** ++ * Build trigger statement for INSERT, UPDATE, DELETE events ++ * ++ * @param string $event ++ * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog ++ * @return string ++ */ ++ protected function buildStatement($event, $changelog) ++ { ++ $triggerBody = null; ++ switch ($event) { ++ case Trigger::EVENT_INSERT: ++ case Trigger::EVENT_UPDATE: ++ $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = NEW.%5\$s;"; ++ break; ++ case Trigger::EVENT_DELETE: ++ $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = OLD.%5\$s;"; ++ break; ++ default: ++ break; ++ } ++ $params = [ ++ $this->connection->quoteIdentifier( ++ $this->resource->getTableName($changelog->getName()) ++ ), ++ $this->connection->quoteIdentifier( ++ $changelog->getColumnName() ++ ), ++ $this->connection->quoteIdentifier( ++ $this->entityMetadata->getIdentifierField() ++ ), ++ $this->connection->quoteIdentifier( ++ $this->resource->getTableName($this->entityMetadata->getEntityTable()) ++ ), ++ $this->connection->quoteIdentifier( ++ $this->entityMetadata->getLinkField() ++ ) ++ ]; ++ return vsprintf($triggerBody, $params); ++ } ++} +commit a0dc6a745002eacaaf2ef57e37a25f79f65de650 +Author: Viktor Paladiichuk +Date: Tue Nov 28 15:54:17 2017 +0200 + + MAGETWO-84444: Mview does not work with Staging + +diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php +new file mode 100644 +index 0000000000..d396829ef8 +--- /dev/null ++++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php +@@ -0,0 +1,302 @@ ++connectionMock = $this->getMock(Mysql::class, [], [], '', false); ++ $this->resourceMock = $this->getMock(ResourceConnection::class, [], [], '', false, false); ++ $this->connectionMock->expects($this->any()) ++ ->method('quoteIdentifier') ++ ->will($this->returnArgument(0)); ++ $this->resourceMock->expects($this->atLeastOnce()) ++ ->method('getConnection') ++ ->willReturn($this->connectionMock); ++ $this->triggerFactoryMock = $this->getMock(TriggerFactory::class, [], [], '', false, false); ++ $this->viewCollectionMock = $this->getMockForAbstractClass( ++ CollectionInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $this->viewMock = $this->getMockForAbstractClass(ViewInterface::class, [], '', false, false, true, []); ++ $this->resourceMock->expects($this->any()) ++ ->method('getTableName') ++ ->will($this->returnArgument(0)); ++ ++ $entityInterface = 'EntityInterface'; ++ $this->entityMetadataPoolMock = $this->getMock(MetadataPool::class, [], [], '', false); ++ ++ $this->entityMetadataMock = $this->getMock(EntityMetadataInterface::class, [], [], '', false); ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getEntityTable') ++ ->will($this->returnValue('entity_table')); ++ ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getIdentifierField') ++ ->will($this->returnValue('entity_identifier')); ++ ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getLinkField') ++ ->will($this->returnValue('entity_link_field')); ++ ++ $this->entityMetadataPoolMock->expects($this->any()) ++ ->method('getMetadata') ++ ->with($entityInterface) ++ ->will($this->returnValue($this->entityMetadataMock)); ++ ++ $this->model = new SubscriptionModel( ++ $this->resourceMock, ++ $this->triggerFactoryMock, ++ $this->viewCollectionMock, ++ $this->viewMock, ++ $this->tableName, ++ 'columnName', ++ $this->entityMetadataPoolMock, ++ $entityInterface ++ ); ++ } ++ ++ /** ++ * Prepare trigger mock ++ * ++ * @param string $triggerName ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareTriggerMock($triggerName) ++ { ++ $triggerMock = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Trigger::class) ++ ->setMethods(['setName', 'getName', 'setTime', 'setEvent', 'setTable', 'addStatement']) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setName') ++ ->with($triggerName) ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('getName') ++ ->will($this->returnValue('triggerName')); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setTime') ++ ->with(\Magento\Framework\DB\Ddl\Trigger::TIME_AFTER) ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setEvent') ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setTable') ++ ->with($this->tableName) ++ ->will($this->returnSelf()); ++ return $triggerMock; ++ } ++ ++ /** ++ * Prepare expected trigger call map ++ * ++ * @param \PHPUnit_Framework_MockObject_MockObject $triggerMock ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareTriggerTestCallMap(\PHPUnit_Framework_MockObject_MockObject $triggerMock) ++ { ++ $triggerMock->expects($this->at(4)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ ) ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(5)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(11)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(12)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(18)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = OLD.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(19)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = OLD.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ return $triggerMock; ++ } ++ ++ /** ++ * Prepare changelog mock ++ * ++ * @param string $changelogName ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareChangelogMock($changelogName) ++ { ++ $changelogMock = $this->getMockForAbstractClass( ++ \Magento\Framework\Mview\View\ChangelogInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $changelogMock->expects($this->exactly(3)) ++ ->method('getName') ++ ->will($this->returnValue($changelogName)); ++ $changelogMock->expects($this->exactly(3)) ++ ->method('getColumnName') ++ ->will($this->returnValue('entity_id')); ++ return $changelogMock; ++ } ++ ++ public function testCreate() ++ { ++ $triggerName = 'trigger_name'; ++ $this->resourceMock->expects($this->atLeastOnce())->method('getTriggerName')->willReturn($triggerName); ++ $triggerMock = $this->prepareTriggerMock($triggerName); ++ $this->prepareTriggerTestCallMap($triggerMock); ++ $changelogMock = $this->prepareChangelogMock('test_view_cl'); ++ ++ $this->viewMock->expects($this->exactly(3)) ++ ->method('getChangelog') ++ ->will($this->returnValue($changelogMock)); ++ ++ $this->triggerFactoryMock->expects($this->exactly(3)) ++ ->method('create') ++ ->will($this->returnValue($triggerMock)); ++ ++ $otherChangelogMock = $this->prepareChangelogMock('other_test_view_cl'); ++ ++ $otherViewMock = $this->getMockForAbstractClass( ++ ViewInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $otherViewMock->expects($this->exactly(1)) ++ ->method('getId') ++ ->will($this->returnValue('other_id')); ++ $otherViewMock->expects($this->exactly(1)) ++ ->method('getSubscriptions') ++ ->will($this->returnValue([['name' => $this->tableName], ['name' => 'otherTableName']])); ++ $otherViewMock->expects($this->any()) ++ ->method('getChangelog') ++ ->will($this->returnValue($otherChangelogMock)); ++ ++ $this->viewMock->expects($this->exactly(3)) ++ ->method('getId') ++ ->will($this->returnValue('this_id')); ++ $this->viewMock->expects($this->never()) ++ ->method('getSubscriptions'); ++ ++ $this->viewCollectionMock->expects($this->exactly(1)) ++ ->method('getViewsByStateMode') ++ ->with(StateInterface::MODE_ENABLED) ++ ->will($this->returnValue([$this->viewMock, $otherViewMock])); ++ ++ $this->connectionMock->expects($this->exactly(3)) ++ ->method('dropTrigger') ++ ->with('triggerName') ++ ->will($this->returnValue(true)); ++ $this->connectionMock->expects($this->exactly(3)) ++ ->method('createTrigger') ++ ->with($triggerMock); ++ ++ $this->model->create(); ++ } ++} diff --git a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.4.patch b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.4.patch new file mode 100644 index 00000000..9f2719ce --- /dev/null +++ b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.4.patch @@ -0,0 +1,1172 @@ +commit 9f15e97099caa27acf41e8fe9ee16d587b605478 +Author: Viktor Paladiichuk +Date: Mon Nov 27 18:21:54 2017 +0200 + + MAGETWO-84444 + +diff -Nuar a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +index d8094cfa8..da9a2c724 100644 +--- a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php ++++ b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +@@ -62,7 +62,7 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + + $this->resourceMock->expects($this->any()) + ->method('getTableName') +- ->willReturn($this->tableName); ++ ->will($this->returnArgument(0)); + + $this->model = new Subscription( + $this->resourceMock, +@@ -89,11 +89,15 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + $this->assertEquals('columnName', $this->model->getColumnName()); + } + ++ /** ++ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) ++ */ + public function testCreate() + { + $triggerName = 'trigger_name'; + $this->resourceMock->expects($this->atLeastOnce())->method('getTriggerName')->willReturn($triggerName); + $triggerMock = $this->getMockBuilder('Magento\Framework\DB\Ddl\Trigger') ++ ->setMethods(['setName', 'getName', 'setTime', 'setEvent', 'setTable', 'addStatement']) + ->disableOriginalConstructor() + ->getMock(); + $triggerMock->expects($this->exactly(3)) +@@ -114,8 +118,35 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + ->method('setTable') + ->with($this->tableName) + ->will($this->returnSelf()); +- $triggerMock->expects($this->exactly(6)) ++ ++ $triggerMock->expects($this->at(4)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(5)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(11)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(12)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(18)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (OLD.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(19)) + ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (OLD.columnName);") + ->will($this->returnSelf()); + + $changelogMock = $this->getMockForAbstractClass( +diff -Nuar a/vendor/magento/framework/Mview/View/Subscription.php b/vendor/magento/framework/Mview/View/Subscription.php +index 7dd440ae9..54e4aaf53 100644 +--- a/vendor/magento/framework/Mview/View/Subscription.php ++++ b/vendor/magento/framework/Mview/View/Subscription.php +@@ -10,6 +10,8 @@ namespace Magento\Framework\Mview\View; + + use Magento\Framework\App\ResourceConnection; + use Magento\Framework\DB\Ddl\Trigger; ++use Magento\Framework\DB\Ddl\TriggerFactory; ++use Magento\Framework\Mview\ViewInterface; + + class Subscription implements SubscriptionInterface + { +@@ -21,12 +23,12 @@ class Subscription implements SubscriptionInterface + protected $connection; + + /** +- * @var \Magento\Framework\DB\Ddl\TriggerFactory ++ * @var TriggerFactory + */ + protected $triggerFactory; + + /** +- * @var \Magento\Framework\Mview\View\CollectionInterface ++ * @var CollectionInterface + */ + protected $viewCollection; + +@@ -58,20 +60,31 @@ class Subscription implements SubscriptionInterface + protected $resource; + + /** ++ * List of columns that can be updated in a subscribed table ++ * without creating a new change log entry ++ * ++ * @var array ++ */ ++ private $ignoredUpdateColumns = []; ++ ++ /** + * @param ResourceConnection $resource +- * @param \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory +- * @param \Magento\Framework\Mview\View\CollectionInterface $viewCollection +- * @param \Magento\Framework\Mview\ViewInterface $view ++ * @param TriggerFactory $triggerFactory ++ * @param CollectionInterface $viewCollection ++ * @param ViewInterface $view + * @param string $tableName + * @param string $columnName ++ * @param array $ignoredUpdateColumns ++ * @throws \DomainException + */ + public function __construct( + ResourceConnection $resource, +- \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory, +- \Magento\Framework\Mview\View\CollectionInterface $viewCollection, +- \Magento\Framework\Mview\ViewInterface $view, ++ TriggerFactory $triggerFactory, ++ CollectionInterface $viewCollection, ++ ViewInterface $view, + $tableName, +- $columnName ++ $columnName, ++ array $ignoredUpdateColumns = [] + ) { + $this->connection = $resource->getConnection(); + $this->triggerFactory = $triggerFactory; +@@ -80,12 +93,14 @@ class Subscription implements SubscriptionInterface + $this->tableName = $tableName; + $this->columnName = $columnName; + $this->resource = $resource; ++ $this->ignoredUpdateColumns = $ignoredUpdateColumns; + } + + /** +- * Create subsciption ++ * Create subscription + * +- * @return \Magento\Framework\Mview\View\SubscriptionInterface ++ * @return SubscriptionInterface ++ * @throws \InvalidArgumentException + */ + public function create() + { +@@ -102,7 +117,7 @@ class Subscription implements SubscriptionInterface + + // Add statements for linked views + foreach ($this->getLinkedViews() as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + $trigger->addStatement($this->buildStatement($event, $view->getChangelog())); + } + +@@ -116,7 +131,8 @@ class Subscription implements SubscriptionInterface + /** + * Remove subscription + * +- * @return \Magento\Framework\Mview\View\SubscriptionInterface ++ * @return SubscriptionInterface ++ * @throws \InvalidArgumentException + */ + public function remove() + { +@@ -131,7 +147,7 @@ class Subscription implements SubscriptionInterface + + // Add statements for linked views + foreach ($this->getLinkedViews() as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + $trigger->addStatement($this->buildStatement($event, $view->getChangelog())); + } + +@@ -154,10 +170,10 @@ class Subscription implements SubscriptionInterface + protected function getLinkedViews() + { + if (!$this->linkedViews) { +- $viewList = $this->viewCollection->getViewsByStateMode(\Magento\Framework\Mview\View\StateInterface::MODE_ENABLED); ++ $viewList = $this->viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); + + foreach ($viewList as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + // Skip the current view + if ($view->getId() == $this->getView()->getId()) { + continue; +@@ -175,35 +191,58 @@ class Subscription implements SubscriptionInterface + } + + /** +- * Build trigger statement for INSER, UPDATE, DELETE events ++ * Build trigger statement for INSERT, UPDATE, DELETE events + * + * @param string $event +- * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog ++ * @param ChangelogInterface $changelog + * @return string + */ + protected function buildStatement($event, $changelog) + { + switch ($event) { + case Trigger::EVENT_INSERT: ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);'; ++ break; ++ + case Trigger::EVENT_UPDATE: +- return sprintf( +- "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);", +- $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), +- $this->connection->quoteIdentifier($changelog->getColumnName()), +- $this->connection->quoteIdentifier($this->getColumnName()) +- ); ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);'; ++ ++ if ($this->connection->isTableExists($this->getTableName()) ++ && $describe = $this->connection->describeTable($this->getTableName()) ++ ) { ++ $columnNames = array_column($describe, 'COLUMN_NAME'); ++ $columnNames = array_diff($columnNames, $this->ignoredUpdateColumns); ++ if ($columnNames) { ++ $columns = []; ++ foreach ($columnNames as $columnName) { ++ $columns[] = sprintf( ++ 'NEW.%1$s != OLD.%1$s', ++ $this->connection->quoteIdentifier($columnName) ++ ); ++ } ++ $trigger = sprintf( ++ "IF (%s) THEN %s END IF;", ++ implode(' OR ', $columns), ++ $trigger ++ ); ++ } ++ } ++ break; + + case Trigger::EVENT_DELETE: +- return sprintf( +- "INSERT IGNORE INTO %s (%s) VALUES (OLD.%s);", +- $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), +- $this->connection->quoteIdentifier($changelog->getColumnName()), +- $this->connection->quoteIdentifier($this->getColumnName()) +- ); ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (OLD.%s);'; ++ break; + + default: + return ''; + } ++ ++ return sprintf( ++ $trigger, ++ $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), ++ $this->connection->quoteIdentifier($changelog->getColumnName()), ++ $this->connection->quoteIdentifier($this->getColumnName()) ++ ); + } + + /** +@@ -225,7 +264,7 @@ class Subscription implements SubscriptionInterface + /** + * Retrieve View related to subscription + * +- * @return \Magento\Framework\Mview\ViewInterface ++ * @return ViewInterface + * @codeCoverageIgnore + */ + public function getView() +diff -Nuar a/vendor/magento/framework/Mview/etc/mview.xsd b/vendor/magento/framework/Mview/etc/mview.xsd +index d171699c3..0521691e8 100644 +--- a/vendor/magento/framework/Mview/etc/mview.xsd ++++ b/vendor/magento/framework/Mview/etc/mview.xsd +@@ -106,7 +106,7 @@ + + + +- Subscription model must be a valid PHP class or interface name. ++ DEPRECATED. Subscription model must be a valid PHP class or interface name. + + + +diff -Nuar a/vendor/magento/module-catalog-inventory/etc/mview.xml b/vendor/magento/module-catalog-inventory/etc/mview.xml +index 5737fea21..954a3349e 100644 +--- a/vendor/magento/module-catalog-inventory/etc/mview.xml ++++ b/vendor/magento/module-catalog-inventory/etc/mview.xml +@@ -5,10 +5,13 @@ + * See COPYING.txt for license details. + */ + --> +- ++ + + +
++
++
+ + + +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php +new file mode 100644 +index 000000000..7c549538c +--- /dev/null ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php +@@ -0,0 +1,100 @@ ++entityMetadata = $metadataPool->getMetadata($entityInterface); ++ } ++ ++ /** ++ * Build trigger statement for INSERT, UPDATE, DELETE events ++ * ++ * @param string $event ++ * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog ++ * @return string ++ */ ++ protected function buildStatement($event, $changelog) ++ { ++ $triggerBody = null; ++ switch ($event) { ++ case Trigger::EVENT_INSERT: ++ case Trigger::EVENT_UPDATE: ++ $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = NEW.%5\$s;"; ++ break; ++ case Trigger::EVENT_DELETE: ++ $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = OLD.%5\$s;"; ++ break; ++ default: ++ break; ++ } ++ $params = [ ++ $this->connection->quoteIdentifier( ++ $this->resource->getTableName($changelog->getName()) ++ ), ++ $this->connection->quoteIdentifier( ++ $changelog->getColumnName() ++ ), ++ $this->connection->quoteIdentifier( ++ $this->entityMetadata->getIdentifierField() ++ ), ++ $this->connection->quoteIdentifier( ++ $this->resource->getTableName($this->entityMetadata->getEntityTable()) ++ ), ++ $this->connection->quoteIdentifier( ++ $this->entityMetadata->getLinkField() ++ ) ++ ]; ++ return vsprintf($triggerBody, $params); ++ } ++} +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +index 3ffa1a1fc..438318597 100644 +--- a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +@@ -6,22 +6,16 @@ + namespace Magento\CatalogStaging\Model\Mview\View\Category\Attribute; + + use Magento\Catalog\Api\Data\CategoryInterface; +-use Magento\Framework\DB\Ddl\Trigger; + use Magento\Framework\App\ResourceConnection; + use Magento\Framework\EntityManager\MetadataPool; + + /** +- * Class Subscription ++ * Class Subscription implements statement building for staged category entity attribute subscription + * @package Magento\CatalogStaging\Model\Mview\View\Category\Attribute + */ +-class Subscription extends \Magento\Framework\Mview\View\Subscription ++class Subscription extends \Magento\CatalogStaging\Model\Mview\View\Attribute\Subscription + { + /** +- * @var \Magento\Framework\EntityManager\EntityMetadata +- */ +- protected $entityMetadata; +- +- /** + * @param ResourceConnection $resource + * @param \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory + * @param \Magento\Framework\Mview\View\CollectionInterface $viewCollection +@@ -29,7 +23,8 @@ class Subscription extends \Magento\Framework\Mview\View\Subscription + * @param string $tableName + * @param string $columnName + * @param MetadataPool $metadataPool +- * @throws \Exception ++ * @param string|null $entityInterface ++ * @param array $ignoredUpdateColumns + */ + public function __construct( + ResourceConnection $resource, +@@ -38,50 +33,20 @@ class Subscription extends \Magento\Framework\Mview\View\Subscription + \Magento\Framework\Mview\ViewInterface $view, + $tableName, + $columnName, +- MetadataPool $metadataPool ++ MetadataPool $metadataPool, ++ $entityInterface = CategoryInterface::class, ++ $ignoredUpdateColumns = [] + ) { +- parent::__construct($resource, $triggerFactory, $viewCollection, $view, $tableName, $columnName); +- $this->entityMetadata = $metadataPool->getMetadata(CategoryInterface::class); +- } +- +- /** +- * Build trigger statement for INSERT, UPDATE, DELETE events +- * +- * @param string $event +- * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog +- * @return string +- */ +- protected function buildStatement($event, $changelog) +- { +- $triggerBody = null; +- switch ($event) { +- case Trigger::EVENT_INSERT: +- case Trigger::EVENT_UPDATE: +- $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = NEW.%5\$s;"; +- break; +- case Trigger::EVENT_DELETE: +- $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = OLD.%5\$s;"; +- break; +- default: +- break; +- } +- $params = [ +- $this->connection->quoteIdentifier( +- $this->resource->getTableName($changelog->getName()) +- ), +- $this->connection->quoteIdentifier( +- $changelog->getColumnName() +- ), +- $this->connection->quoteIdentifier( +- $this->entityMetadata->getIdentifierField() +- ), +- $this->connection->quoteIdentifier( +- $this->resource->getTableName($this->entityMetadata->getEntityTable()) +- ), +- $this->connection->quoteIdentifier( +- $this->entityMetadata->getLinkField() +- ) +- ]; +- return vsprintf($triggerBody, $params); ++ parent::__construct( ++ $resource, ++ $triggerFactory, ++ $viewCollection, ++ $view, ++ $tableName, ++ $columnName, ++ $metadataPool, ++ $entityInterface, ++ $ignoredUpdateColumns ++ ); + } + } +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +index 6f03681f7..052aa3702 100644 +--- a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +@@ -11,34 +11,43 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory + { + /** + * @var array ++ * @deprecated 2.2.0 + */ + private $stagingEntityTables = ['catalog_product_entity', 'catalog_category_entity']; + + /** + * @var array ++ * @deprecated 2.2.0 + */ + private $versionTables; + + /** ++ * @var string[] ++ */ ++ private $subscriptionModels = []; ++ ++ /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\CatalogStaging\Model\VersionTables $versionTables ++ * @param array $subscriptionModels + */ + public function __construct( + \Magento\Framework\ObjectManagerInterface $objectManager, +- \Magento\CatalogStaging\Model\VersionTables $versionTables ++ \Magento\CatalogStaging\Model\VersionTables $versionTables, ++ $subscriptionModels = [] + ) { + parent::__construct($objectManager); + $this->versionTables = $versionTables; ++ $this->subscriptionModels = $subscriptionModels; + } + + /** +- * @param array $data +- * @return \Magento\Framework\Mview\View\CollectionInterface ++ * {@inheritdoc} + */ + public function create(array $data = []) + { +- if ($this->isStagingTable($data)) { +- $data['columnName'] = 'row_id'; ++ if (isset($data['tableName']) && isset($this->subscriptionModels[$data['tableName']])) { ++ $data['subscriptionModel'] = $this->subscriptionModels[$data['tableName']]; + } + return parent::create($data); + } +@@ -46,6 +55,7 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory + /** + * @param array $data + * @return bool ++ * @deprecated + */ + protected function isStagingTable(array $data = []) + { +diff -Nuar a/vendor/magento/module-catalog-staging/Model/VersionTables.php b/vendor/magento/module-catalog-staging/Model/VersionTables.php +index 7ebd828bd..2cc32669b 100644 +--- a/vendor/magento/module-catalog-staging/Model/VersionTables.php ++++ b/vendor/magento/module-catalog-staging/Model/VersionTables.php +@@ -5,6 +5,11 @@ + */ + namespace Magento\CatalogStaging\Model; + ++/** ++ * Class VersionTables stores information about staged tables. ++ * ++ * @package Magento\CatalogStaging\Model ++ */ + class VersionTables extends \Magento\Framework\DataObject + { + /** +diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php +new file mode 100644 +index 000000000..d396829ef +--- /dev/null ++++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php +@@ -0,0 +1,302 @@ ++connectionMock = $this->getMock(Mysql::class, [], [], '', false); ++ $this->resourceMock = $this->getMock(ResourceConnection::class, [], [], '', false, false); ++ $this->connectionMock->expects($this->any()) ++ ->method('quoteIdentifier') ++ ->will($this->returnArgument(0)); ++ $this->resourceMock->expects($this->atLeastOnce()) ++ ->method('getConnection') ++ ->willReturn($this->connectionMock); ++ $this->triggerFactoryMock = $this->getMock(TriggerFactory::class, [], [], '', false, false); ++ $this->viewCollectionMock = $this->getMockForAbstractClass( ++ CollectionInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $this->viewMock = $this->getMockForAbstractClass(ViewInterface::class, [], '', false, false, true, []); ++ $this->resourceMock->expects($this->any()) ++ ->method('getTableName') ++ ->will($this->returnArgument(0)); ++ ++ $entityInterface = 'EntityInterface'; ++ $this->entityMetadataPoolMock = $this->getMock(MetadataPool::class, [], [], '', false); ++ ++ $this->entityMetadataMock = $this->getMock(EntityMetadataInterface::class, [], [], '', false); ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getEntityTable') ++ ->will($this->returnValue('entity_table')); ++ ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getIdentifierField') ++ ->will($this->returnValue('entity_identifier')); ++ ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getLinkField') ++ ->will($this->returnValue('entity_link_field')); ++ ++ $this->entityMetadataPoolMock->expects($this->any()) ++ ->method('getMetadata') ++ ->with($entityInterface) ++ ->will($this->returnValue($this->entityMetadataMock)); ++ ++ $this->model = new SubscriptionModel( ++ $this->resourceMock, ++ $this->triggerFactoryMock, ++ $this->viewCollectionMock, ++ $this->viewMock, ++ $this->tableName, ++ 'columnName', ++ $this->entityMetadataPoolMock, ++ $entityInterface ++ ); ++ } ++ ++ /** ++ * Prepare trigger mock ++ * ++ * @param string $triggerName ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareTriggerMock($triggerName) ++ { ++ $triggerMock = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Trigger::class) ++ ->setMethods(['setName', 'getName', 'setTime', 'setEvent', 'setTable', 'addStatement']) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setName') ++ ->with($triggerName) ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('getName') ++ ->will($this->returnValue('triggerName')); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setTime') ++ ->with(\Magento\Framework\DB\Ddl\Trigger::TIME_AFTER) ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setEvent') ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setTable') ++ ->with($this->tableName) ++ ->will($this->returnSelf()); ++ return $triggerMock; ++ } ++ ++ /** ++ * Prepare expected trigger call map ++ * ++ * @param \PHPUnit_Framework_MockObject_MockObject $triggerMock ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareTriggerTestCallMap(\PHPUnit_Framework_MockObject_MockObject $triggerMock) ++ { ++ $triggerMock->expects($this->at(4)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ ) ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(5)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(11)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(12)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(18)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = OLD.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(19)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = OLD.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ return $triggerMock; ++ } ++ ++ /** ++ * Prepare changelog mock ++ * ++ * @param string $changelogName ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareChangelogMock($changelogName) ++ { ++ $changelogMock = $this->getMockForAbstractClass( ++ \Magento\Framework\Mview\View\ChangelogInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $changelogMock->expects($this->exactly(3)) ++ ->method('getName') ++ ->will($this->returnValue($changelogName)); ++ $changelogMock->expects($this->exactly(3)) ++ ->method('getColumnName') ++ ->will($this->returnValue('entity_id')); ++ return $changelogMock; ++ } ++ ++ public function testCreate() ++ { ++ $triggerName = 'trigger_name'; ++ $this->resourceMock->expects($this->atLeastOnce())->method('getTriggerName')->willReturn($triggerName); ++ $triggerMock = $this->prepareTriggerMock($triggerName); ++ $this->prepareTriggerTestCallMap($triggerMock); ++ $changelogMock = $this->prepareChangelogMock('test_view_cl'); ++ ++ $this->viewMock->expects($this->exactly(3)) ++ ->method('getChangelog') ++ ->will($this->returnValue($changelogMock)); ++ ++ $this->triggerFactoryMock->expects($this->exactly(3)) ++ ->method('create') ++ ->will($this->returnValue($triggerMock)); ++ ++ $otherChangelogMock = $this->prepareChangelogMock('other_test_view_cl'); ++ ++ $otherViewMock = $this->getMockForAbstractClass( ++ ViewInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $otherViewMock->expects($this->exactly(1)) ++ ->method('getId') ++ ->will($this->returnValue('other_id')); ++ $otherViewMock->expects($this->exactly(1)) ++ ->method('getSubscriptions') ++ ->will($this->returnValue([['name' => $this->tableName], ['name' => 'otherTableName']])); ++ $otherViewMock->expects($this->any()) ++ ->method('getChangelog') ++ ->will($this->returnValue($otherChangelogMock)); ++ ++ $this->viewMock->expects($this->exactly(3)) ++ ->method('getId') ++ ->will($this->returnValue('this_id')); ++ $this->viewMock->expects($this->never()) ++ ->method('getSubscriptions'); ++ ++ $this->viewCollectionMock->expects($this->exactly(1)) ++ ->method('getViewsByStateMode') ++ ->with(StateInterface::MODE_ENABLED) ++ ->will($this->returnValue([$this->viewMock, $otherViewMock])); ++ ++ $this->connectionMock->expects($this->exactly(3)) ++ ->method('dropTrigger') ++ ->with('triggerName') ++ ->will($this->returnValue(true)); ++ $this->connectionMock->expects($this->exactly(3)) ++ ->method('createTrigger') ++ ->with($triggerMock); ++ ++ $this->model->create(); ++ } ++} +diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +index a0abb4230..4a0601569 100644 +--- a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php ++++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +@@ -17,11 +17,6 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase + protected $objectManagerMock; + + /** +- * @var \PHPUnit_Framework_MockObject_MockObject +- */ +- protected $versionTablesrMock; +- +- /** + * @var \Magento\CatalogStaging\Model\Mview\View\SubscriptionFactory + */ + protected $model; +@@ -29,110 +24,45 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase + protected function setUp() + { + $objectManager = new ObjectManager($this); +- +- $this->objectManagerMock = $this->getMockBuilder('Magento\Framework\ObjectManagerInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->versionTablesrMock = $this->getMockBuilder('Magento\CatalogStaging\Model\VersionTables') ++ $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->model = $objectManager->getObject( + SubscriptionFactory::class, + [ + 'objectManager' => $this->objectManagerMock, +- 'versionTables' => $this->versionTablesrMock ++ 'subscriptionModels' => [ ++ 'catalog_product_entity_int' => 'ProductEntityIntSubscription' ++ ] + ] + ); + } +- + public function testCreate() + { + $data = ['tableName' => 'catalog_product_entity_int', 'columnName' => 'entity_id']; +- $versionTables = ['catalog_product_entity_int']; +- + $expectedData = $data; +- $expectedData['columnName'] = 'row_id'; +- +- $this->versionTablesrMock->expects($this->once()) +- ->method('getVersionTables') +- ->willReturn($versionTables); +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') ++ $expectedData['columnName'] = 'entity_id'; ++ $subscriptionMock = $this->getMockBuilder(\Magento\Framework\Mview\View\SubscriptionInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->once()) + ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) ++ ->with('ProductEntityIntSubscription', $expectedData) + ->willReturn($subscriptionMock); +- + $result = $this->model->create($data); + $this->assertEquals($subscriptionMock, $result); + } +- + public function testCreateNoTableName() + { + $data = ['columnName' => 'entity_id']; +- +- $expectedData = $data; +- +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->objectManagerMock->expects($this->once()) +- ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) +- ->willReturn($subscriptionMock); +- +- $result = $this->model->create($data); +- $this->assertEquals($subscriptionMock, $result); +- } +- +- /** +- * @param $stagingEntityTable +- * @dataProvider tablesDataProvider +- */ +- public function testCreateStagingEntityTables($stagingEntityTable) +- { +- $data = ['tableName' => $stagingEntityTable, 'columnName' => 'entity_id']; +- + $expectedData = $data; +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') ++ $subscriptionMock = $this->getMockBuilder(\Magento\Framework\Mview\View\SubscriptionInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) + ->willReturn($subscriptionMock); +- +- $result = $this->model->create($data); +- $this->assertEquals($subscriptionMock, $result); +- } +- +- public static function tablesDataProvider() +- { +- return [ +- ['catalog_product_entity'], +- ['catalog_category_entity'] +- ]; +- } +- +- public function testCreateNoVersionTable() +- { +- $data = ['tableName' => 'not_existed_table', 'columnName' => 'entity_id']; +- $versionTables = ['catalog_product_entity_int']; +- +- $expectedData = $data; +- +- $this->versionTablesrMock->expects($this->once()) +- ->method('getVersionTables') +- ->willReturn($versionTables); +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->objectManagerMock->expects($this->once()) +- ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) +- ->willReturn($subscriptionMock); +- + $result = $this->model->create($data); + $this->assertEquals($subscriptionMock, $result); + } +diff -Nuar a/vendor/magento/module-catalog-staging/etc/di.xml b/vendor/magento/module-catalog-staging/etc/di.xml +index 79f95ec08..e28ac70e7 100644 +--- a/vendor/magento/module-catalog-staging/etc/di.xml ++++ b/vendor/magento/module-catalog-staging/etc/di.xml +@@ -56,6 +56,35 @@ + + + ++ ++ ++ Magento\Catalog\Api\Data\CategoryInterface ++ ++ ++ ++ ++ Magento\Catalog\Api\Data\ProductInterface ++ ++ ++ ++ ++ ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ ++ ++ + + + +diff -Nuar a/vendor/magento/module-catalog-staging/etc/mview.xml b/vendor/magento/module-catalog-staging/etc/mview.xml +deleted file mode 100644 +index 488972e05..000000000 +--- a/vendor/magento/module-catalog-staging/etc/mview.xml ++++ /dev/null +@@ -1,23 +0,0 @@ +- +- +- +- +- +-
+-
+-
+-
+-
+- +- +- +- +-
+- +- +- +diff -Nuar a/vendor/magento/module-indexer/Setup/RecurringData.php b/vendor/magento/module-indexer/Setup/RecurringData.php +new file mode 100644 +index 000000000..38ea0e5b7 +--- /dev/null ++++ b/vendor/magento/module-indexer/Setup/RecurringData.php +@@ -0,0 +1,56 @@ ++indexerFactory = $indexerFactory; ++ $this->configInterface = $configInterface; ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) ++ { ++ foreach (array_keys($this->configInterface->getIndexers()) as $indexerId) { ++ $indexer = $this->indexerFactory->create()->load($indexerId); ++ if ($indexer->isScheduled()) { ++ $indexer->getView()->unsubscribe()->subscribe(); ++ } ++ } ++ } ++} diff --git a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.5.patch b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.5.patch new file mode 100644 index 00000000..c044017d --- /dev/null +++ b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.5.patch @@ -0,0 +1,1190 @@ +commit e9aa4e18cfb76b37ce00f6f45d507d22e1e89550 +Author: Viktor Paladiichuk +Date: Tue Nov 28 15:29:54 2017 +0200 + + MAGETWO-84444: Mview does not work with Staging + +diff -Nuar a/vendor/magento/module-catalog-inventory/etc/mview.xml b/vendor/magento/module-catalog-inventory/etc/mview.xml +index 58a051a3d0e..3dd8419d7e3 100644 +--- a/vendor/magento/module-catalog-inventory/etc/mview.xml ++++ b/vendor/magento/module-catalog-inventory/etc/mview.xml +@@ -5,10 +5,13 @@ + * See COPYING.txt for license details. + */ + --> +- ++ + + +
++
++
+ + + +diff -Nuar a/vendor/magento/module-indexer/Setup/RecurringData.php b/vendor/magento/module-indexer/Setup/RecurringData.php +new file mode 100644 +index 00000000000..38ea0e5b79e +--- /dev/null ++++ b/vendor/magento/module-indexer/Setup/RecurringData.php +@@ -0,0 +1,56 @@ ++indexerFactory = $indexerFactory; ++ $this->configInterface = $configInterface; ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) ++ { ++ foreach (array_keys($this->configInterface->getIndexers()) as $indexerId) { ++ $indexer = $this->indexerFactory->create()->load($indexerId); ++ if ($indexer->isScheduled()) { ++ $indexer->getView()->unsubscribe()->subscribe(); ++ } ++ } ++ } ++} +diff -Nuar a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +index aae82938c83..545689a4391 100644 +--- a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php ++++ b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +@@ -62,7 +62,7 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + + $this->resourceMock->expects($this->any()) + ->method('getTableName') +- ->willReturn($this->tableName); ++ ->will($this->returnArgument(0)); + + $this->model = new Subscription( + $this->resourceMock, +@@ -89,11 +89,15 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + $this->assertEquals('columnName', $this->model->getColumnName()); + } + ++ /** ++ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) ++ */ + public function testCreate() + { + $triggerName = 'trigger_name'; + $this->resourceMock->expects($this->atLeastOnce())->method('getTriggerName')->willReturn($triggerName); + $triggerMock = $this->getMockBuilder('Magento\Framework\DB\Ddl\Trigger') ++ ->setMethods(['setName', 'getName', 'setTime', 'setEvent', 'setTable', 'addStatement']) + ->disableOriginalConstructor() + ->getMock(); + $triggerMock->expects($this->exactly(3)) +@@ -114,8 +118,35 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase + ->method('setTable') + ->with($this->tableName) + ->will($this->returnSelf()); +- $triggerMock->expects($this->exactly(6)) ++ ++ $triggerMock->expects($this->at(4)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(5)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(11)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(12)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (NEW.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(18)) ++ ->method('addStatement') ++ ->with("INSERT IGNORE INTO test_view_cl (entity_id) VALUES (OLD.columnName);") ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(19)) + ->method('addStatement') ++ ->with("INSERT IGNORE INTO other_test_view_cl (entity_id) VALUES (OLD.columnName);") + ->will($this->returnSelf()); + + $changelogMock = $this->getMockForAbstractClass( +diff -Nuar a/vendor/magento/framework/Mview/View/Subscription.php b/vendor/magento/framework/Mview/View/Subscription.php +index c3da91c8331..3c4bd1dce2d 100644 +--- a/vendor/magento/framework/Mview/View/Subscription.php ++++ b/vendor/magento/framework/Mview/View/Subscription.php +@@ -10,6 +10,8 @@ namespace Magento\Framework\Mview\View; + + use Magento\Framework\App\ResourceConnection; + use Magento\Framework\DB\Ddl\Trigger; ++use Magento\Framework\DB\Ddl\TriggerFactory; ++use Magento\Framework\Mview\ViewInterface; + + class Subscription implements SubscriptionInterface + { +@@ -21,12 +23,12 @@ class Subscription implements SubscriptionInterface + protected $connection; + + /** +- * @var \Magento\Framework\DB\Ddl\TriggerFactory ++ * @var TriggerFactory + */ + protected $triggerFactory; + + /** +- * @var \Magento\Framework\Mview\View\CollectionInterface ++ * @var CollectionInterface + */ + protected $viewCollection; + +@@ -58,20 +60,31 @@ class Subscription implements SubscriptionInterface + protected $resource; + + /** ++ * List of columns that can be updated in a subscribed table ++ * without creating a new change log entry ++ * ++ * @var array ++ */ ++ private $ignoredUpdateColumns = []; ++ ++ /** + * @param ResourceConnection $resource +- * @param \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory +- * @param \Magento\Framework\Mview\View\CollectionInterface $viewCollection +- * @param \Magento\Framework\Mview\ViewInterface $view ++ * @param TriggerFactory $triggerFactory ++ * @param CollectionInterface $viewCollection ++ * @param ViewInterface $view + * @param string $tableName + * @param string $columnName ++ * @param array $ignoredUpdateColumns ++ * @throws \DomainException + */ + public function __construct( + ResourceConnection $resource, +- \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory, +- \Magento\Framework\Mview\View\CollectionInterface $viewCollection, +- \Magento\Framework\Mview\ViewInterface $view, ++ TriggerFactory $triggerFactory, ++ CollectionInterface $viewCollection, ++ ViewInterface $view, + $tableName, +- $columnName ++ $columnName, ++ array $ignoredUpdateColumns = [] + ) { + $this->connection = $resource->getConnection(); + $this->triggerFactory = $triggerFactory; +@@ -80,12 +93,14 @@ class Subscription implements SubscriptionInterface + $this->tableName = $tableName; + $this->columnName = $columnName; + $this->resource = $resource; ++ $this->ignoredUpdateColumns = $ignoredUpdateColumns; + } + + /** +- * Create subsciption ++ * Create subscription + * +- * @return \Magento\Framework\Mview\View\SubscriptionInterface ++ * @return SubscriptionInterface ++ * @throws \InvalidArgumentException + */ + public function create() + { +@@ -102,7 +117,7 @@ class Subscription implements SubscriptionInterface + + // Add statements for linked views + foreach ($this->getLinkedViews() as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + $trigger->addStatement($this->buildStatement($event, $view->getChangelog())); + } + +@@ -116,7 +131,8 @@ class Subscription implements SubscriptionInterface + /** + * Remove subscription + * +- * @return \Magento\Framework\Mview\View\SubscriptionInterface ++ * @return SubscriptionInterface ++ * @throws \InvalidArgumentException + */ + public function remove() + { +@@ -131,7 +147,7 @@ class Subscription implements SubscriptionInterface + + // Add statements for linked views + foreach ($this->getLinkedViews() as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + $trigger->addStatement($this->buildStatement($event, $view->getChangelog())); + } + +@@ -154,10 +170,10 @@ class Subscription implements SubscriptionInterface + protected function getLinkedViews() + { + if (!$this->linkedViews) { +- $viewList = $this->viewCollection->getViewsByStateMode(\Magento\Framework\Mview\View\StateInterface::MODE_ENABLED); ++ $viewList = $this->viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); + + foreach ($viewList as $view) { +- /** @var \Magento\Framework\Mview\ViewInterface $view */ ++ /** @var ViewInterface $view */ + // Skip the current view + if ($view->getId() == $this->getView()->getId()) { + continue; +@@ -175,35 +191,58 @@ class Subscription implements SubscriptionInterface + } + + /** +- * Build trigger statement for INSER, UPDATE, DELETE events ++ * Build trigger statement for INSERT, UPDATE, DELETE events + * + * @param string $event +- * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog ++ * @param ChangelogInterface $changelog + * @return string + */ + protected function buildStatement($event, $changelog) + { + switch ($event) { + case Trigger::EVENT_INSERT: ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);'; ++ break; ++ + case Trigger::EVENT_UPDATE: +- return sprintf( +- "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);", +- $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), +- $this->connection->quoteIdentifier($changelog->getColumnName()), +- $this->connection->quoteIdentifier($this->getColumnName()) +- ); ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);'; ++ ++ if ($this->connection->isTableExists($this->getTableName()) ++ && $describe = $this->connection->describeTable($this->getTableName()) ++ ) { ++ $columnNames = array_column($describe, 'COLUMN_NAME'); ++ $columnNames = array_diff($columnNames, $this->ignoredUpdateColumns); ++ if ($columnNames) { ++ $columns = []; ++ foreach ($columnNames as $columnName) { ++ $columns[] = sprintf( ++ 'NEW.%1$s != OLD.%1$s', ++ $this->connection->quoteIdentifier($columnName) ++ ); ++ } ++ $trigger = sprintf( ++ "IF (%s) THEN %s END IF;", ++ implode(' OR ', $columns), ++ $trigger ++ ); ++ } ++ } ++ break; + + case Trigger::EVENT_DELETE: +- return sprintf( +- "INSERT IGNORE INTO %s (%s) VALUES (OLD.%s);", +- $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), +- $this->connection->quoteIdentifier($changelog->getColumnName()), +- $this->connection->quoteIdentifier($this->getColumnName()) +- ); ++ $trigger = 'INSERT IGNORE INTO %s (%s) VALUES (OLD.%s);'; ++ break; + + default: + return ''; + } ++ ++ return sprintf( ++ $trigger, ++ $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), ++ $this->connection->quoteIdentifier($changelog->getColumnName()), ++ $this->connection->quoteIdentifier($this->getColumnName()) ++ ); + } + + /** +@@ -225,7 +264,7 @@ class Subscription implements SubscriptionInterface + /** + * Retrieve View related to subscription + * +- * @return \Magento\Framework\Mview\ViewInterface ++ * @return ViewInterface + * @codeCoverageIgnore + */ + public function getView() +diff -Nuar a/vendor/magento/framework/Mview/etc/mview.xsd b/vendor/magento/framework/Mview/etc/mview.xsd +index 1dad5b3f415..b7d6bbdde68 100644 +--- a/vendor/magento/framework/Mview/etc/mview.xsd ++++ b/vendor/magento/framework/Mview/etc/mview.xsd +@@ -106,7 +106,7 @@ + + + +- Subscription model must be a valid PHP class or interface name. ++ DEPRECATED. Subscription model must be a valid PHP class or interface name. + + + +commit a85bf456be38fa943e4144309cd330e199d1e4b6 +Author: Viktor Paladiichuk +Date: Tue Nov 28 15:30:48 2017 +0200 + + MAGETWO-84444: Mview does not work with Staging + +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +index b3e920fb87..f9b1d1a32a 100644 +--- a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +@@ -6,22 +6,16 @@ + namespace Magento\CatalogStaging\Model\Mview\View\Category\Attribute; + + use Magento\Catalog\Api\Data\CategoryInterface; +-use Magento\Framework\DB\Ddl\Trigger; + use Magento\Framework\App\ResourceConnection; + use Magento\Framework\EntityManager\MetadataPool; + + /** +- * Class Subscription ++ * Class Subscription implements statement building for staged category entity attribute subscription + * @package Magento\CatalogStaging\Model\Mview\View\Category\Attribute + */ +-class Subscription extends \Magento\Framework\Mview\View\Subscription ++class Subscription extends \Magento\CatalogStaging\Model\Mview\View\Attribute\Subscription + { + /** +- * @var \Magento\Framework\EntityManager\EntityMetadata +- */ +- protected $entityMetadata; +- +- /** + * @param ResourceConnection $resource + * @param \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory + * @param \Magento\Framework\Mview\View\CollectionInterface $viewCollection +@@ -29,7 +23,8 @@ class Subscription extends \Magento\Framework\Mview\View\Subscription + * @param string $tableName + * @param string $columnName + * @param MetadataPool $metadataPool +- * @throws \Exception ++ * @param string|null $entityInterface ++ * @param array $ignoredUpdateColumns + */ + public function __construct( + ResourceConnection $resource, +@@ -38,50 +33,20 @@ class Subscription extends \Magento\Framework\Mview\View\Subscription + \Magento\Framework\Mview\ViewInterface $view, + $tableName, + $columnName, +- MetadataPool $metadataPool ++ MetadataPool $metadataPool, ++ $entityInterface = CategoryInterface::class, ++ $ignoredUpdateColumns = [] + ) { +- parent::__construct($resource, $triggerFactory, $viewCollection, $view, $tableName, $columnName); +- $this->entityMetadata = $metadataPool->getMetadata(CategoryInterface::class); +- } +- +- /** +- * Build trigger statement for INSERT, UPDATE, DELETE events +- * +- * @param string $event +- * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog +- * @return string +- */ +- protected function buildStatement($event, $changelog) +- { +- $triggerBody = null; +- switch ($event) { +- case Trigger::EVENT_INSERT: +- case Trigger::EVENT_UPDATE: +- $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = NEW.%5\$s;"; +- break; +- case Trigger::EVENT_DELETE: +- $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = OLD.%5\$s;"; +- break; +- default: +- break; +- } +- $params = [ +- $this->connection->quoteIdentifier( +- $this->resource->getTableName($changelog->getName()) +- ), +- $this->connection->quoteIdentifier( +- $changelog->getColumnName() +- ), +- $this->connection->quoteIdentifier( +- $this->entityMetadata->getIdentifierField() +- ), +- $this->connection->quoteIdentifier( +- $this->resource->getTableName($this->entityMetadata->getEntityTable()) +- ), +- $this->connection->quoteIdentifier( +- $this->entityMetadata->getLinkField() +- ) +- ]; +- return vsprintf($triggerBody, $params); ++ parent::__construct( ++ $resource, ++ $triggerFactory, ++ $viewCollection, ++ $view, ++ $tableName, ++ $columnName, ++ $metadataPool, ++ $entityInterface, ++ $ignoredUpdateColumns ++ ); + } + } +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +index 96c0f71164..7c841853f9 100644 +--- a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +@@ -11,34 +11,43 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory + { + /** + * @var array ++ * @deprecated 2.2.0 + */ + private $stagingEntityTables = ['catalog_product_entity', 'catalog_category_entity']; + + /** + * @var array ++ * @deprecated 2.2.0 + */ + private $versionTables; + + /** ++ * @var string[] ++ */ ++ private $subscriptionModels = []; ++ ++ /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\CatalogStaging\Model\VersionTables $versionTables ++ * @param array $subscriptionModels + */ + public function __construct( + \Magento\Framework\ObjectManagerInterface $objectManager, +- \Magento\CatalogStaging\Model\VersionTables $versionTables ++ \Magento\CatalogStaging\Model\VersionTables $versionTables, ++ $subscriptionModels = [] + ) { + parent::__construct($objectManager); + $this->versionTables = $versionTables; ++ $this->subscriptionModels = $subscriptionModels; + } + + /** +- * @param array $data +- * @return \Magento\Framework\Mview\View\CollectionInterface ++ * {@inheritdoc} + */ + public function create(array $data = []) + { +- if ($this->isStagingTable($data)) { +- $data['columnName'] = 'row_id'; ++ if (isset($data['tableName']) && isset($this->subscriptionModels[$data['tableName']])) { ++ $data['subscriptionModel'] = $this->subscriptionModels[$data['tableName']]; + } + return parent::create($data); + } +@@ -46,6 +55,7 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory + /** + * @param array $data + * @return bool ++ * @deprecated + */ + protected function isStagingTable(array $data = []) + { +diff -Nuar a/vendor/magento/module-catalog-staging/Model/VersionTables.php b/vendor/magento/module-catalog-staging/Model/VersionTables.php +index c845f98b31..242aaf2f25 100644 +--- a/vendor/magento/module-catalog-staging/Model/VersionTables.php ++++ b/vendor/magento/module-catalog-staging/Model/VersionTables.php +@@ -5,6 +5,11 @@ + */ + namespace Magento\CatalogStaging\Model; + ++/** ++ * Class VersionTables stores information about staged tables. ++ * ++ * @package Magento\CatalogStaging\Model ++ */ + class VersionTables extends \Magento\Framework\DataObject + { + /** +diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +index d595784134..d5e78767bd 100644 +--- a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php ++++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +@@ -17,11 +17,6 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase + protected $objectManagerMock; + + /** +- * @var \PHPUnit_Framework_MockObject_MockObject +- */ +- protected $versionTablesrMock; +- +- /** + * @var \Magento\CatalogStaging\Model\Mview\View\SubscriptionFactory + */ + protected $model; +@@ -29,110 +24,45 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase + protected function setUp() + { + $objectManager = new ObjectManager($this); +- +- $this->objectManagerMock = $this->getMockBuilder('Magento\Framework\ObjectManagerInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->versionTablesrMock = $this->getMockBuilder('Magento\CatalogStaging\Model\VersionTables') ++ $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->model = $objectManager->getObject( + SubscriptionFactory::class, + [ + 'objectManager' => $this->objectManagerMock, +- 'versionTables' => $this->versionTablesrMock ++ 'subscriptionModels' => [ ++ 'catalog_product_entity_int' => 'ProductEntityIntSubscription' ++ ] + ] + ); + } +- + public function testCreate() + { + $data = ['tableName' => 'catalog_product_entity_int', 'columnName' => 'entity_id']; +- $versionTables = ['catalog_product_entity_int']; +- + $expectedData = $data; +- $expectedData['columnName'] = 'row_id'; +- +- $this->versionTablesrMock->expects($this->once()) +- ->method('getVersionTables') +- ->willReturn($versionTables); +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') ++ $expectedData['columnName'] = 'entity_id'; ++ $subscriptionMock = $this->getMockBuilder(\Magento\Framework\Mview\View\SubscriptionInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->once()) + ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) ++ ->with('ProductEntityIntSubscription', $expectedData) + ->willReturn($subscriptionMock); +- + $result = $this->model->create($data); + $this->assertEquals($subscriptionMock, $result); + } +- + public function testCreateNoTableName() + { + $data = ['columnName' => 'entity_id']; +- +- $expectedData = $data; +- +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->objectManagerMock->expects($this->once()) +- ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) +- ->willReturn($subscriptionMock); +- +- $result = $this->model->create($data); +- $this->assertEquals($subscriptionMock, $result); +- } +- +- /** +- * @param $stagingEntityTable +- * @dataProvider tablesDataProvider +- */ +- public function testCreateStagingEntityTables($stagingEntityTable) +- { +- $data = ['tableName' => $stagingEntityTable, 'columnName' => 'entity_id']; +- + $expectedData = $data; +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') ++ $subscriptionMock = $this->getMockBuilder(\Magento\Framework\Mview\View\SubscriptionInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) + ->willReturn($subscriptionMock); +- +- $result = $this->model->create($data); +- $this->assertEquals($subscriptionMock, $result); +- } +- +- public static function tablesDataProvider() +- { +- return [ +- ['catalog_product_entity'], +- ['catalog_category_entity'] +- ]; +- } +- +- public function testCreateNoVersionTable() +- { +- $data = ['tableName' => 'not_existed_table', 'columnName' => 'entity_id']; +- $versionTables = ['catalog_product_entity_int']; +- +- $expectedData = $data; +- +- $this->versionTablesrMock->expects($this->once()) +- ->method('getVersionTables') +- ->willReturn($versionTables); +- $subscriptionMock = $this->getMockBuilder('Magento\Framework\Mview\View\SubscriptionInterface') +- ->disableOriginalConstructor() +- ->getMock(); +- $this->objectManagerMock->expects($this->once()) +- ->method('create') +- ->with(FrameworkSubstrictionFactory::INSTANCE_NAME, $expectedData) +- ->willReturn($subscriptionMock); +- + $result = $this->model->create($data); + $this->assertEquals($subscriptionMock, $result); + } +diff -Nuar a/vendor/magento/module-catalog-staging/etc/di.xml b/vendor/magento/module-catalog-staging/etc/di.xml +index aea000b42f..178b0bcf63 100644 +--- a/vendor/magento/module-catalog-staging/etc/di.xml ++++ b/vendor/magento/module-catalog-staging/etc/di.xml +@@ -56,6 +56,35 @@ + + + ++ ++ ++ Magento\Catalog\Api\Data\CategoryInterface ++ ++ ++ ++ ++ Magento\Catalog\Api\Data\ProductInterface ++ ++ ++ ++ ++ ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedCategoryAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ stagedProductAttributeSubscription ++ ++ ++ + + + +diff -Nuar a/vendor/magento/module-catalog-staging/etc/mview.xml b/vendor/magento/module-catalog-staging/etc/mview.xml +deleted file mode 100644 +index 45bb6589b6..0000000000 +--- a/vendor/magento/module-catalog-staging/etc/mview.xml ++++ /dev/null +@@ -1,23 +0,0 @@ +- +- +- +- +- +-
+-
+-
+-
+-
+- +- +- +- +-
+- +- +- +commit 0193f89517fc864c9ab5acd4b518f03b2c796a2f +Author: Viktor Paladiichuk +Date: Tue Nov 28 15:45:17 2017 +0200 + + MAGETWO-84444: Mview does not work with Staging + +diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php +new file mode 100644 +index 0000000000..7c549538c7 +--- /dev/null ++++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php +@@ -0,0 +1,100 @@ ++entityMetadata = $metadataPool->getMetadata($entityInterface); ++ } ++ ++ /** ++ * Build trigger statement for INSERT, UPDATE, DELETE events ++ * ++ * @param string $event ++ * @param \Magento\Framework\Mview\View\ChangelogInterface $changelog ++ * @return string ++ */ ++ protected function buildStatement($event, $changelog) ++ { ++ $triggerBody = null; ++ switch ($event) { ++ case Trigger::EVENT_INSERT: ++ case Trigger::EVENT_UPDATE: ++ $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = NEW.%5\$s;"; ++ break; ++ case Trigger::EVENT_DELETE: ++ $triggerBody = "INSERT IGNORE INTO %1\$s (%2\$s) SELECT %3\$s FROM %4\$s WHERE %5\$s = OLD.%5\$s;"; ++ break; ++ default: ++ break; ++ } ++ $params = [ ++ $this->connection->quoteIdentifier( ++ $this->resource->getTableName($changelog->getName()) ++ ), ++ $this->connection->quoteIdentifier( ++ $changelog->getColumnName() ++ ), ++ $this->connection->quoteIdentifier( ++ $this->entityMetadata->getIdentifierField() ++ ), ++ $this->connection->quoteIdentifier( ++ $this->resource->getTableName($this->entityMetadata->getEntityTable()) ++ ), ++ $this->connection->quoteIdentifier( ++ $this->entityMetadata->getLinkField() ++ ) ++ ]; ++ return vsprintf($triggerBody, $params); ++ } ++} +commit a0dc6a745002eacaaf2ef57e37a25f79f65de650 +Author: Viktor Paladiichuk +Date: Tue Nov 28 15:54:17 2017 +0200 + + MAGETWO-84444: Mview does not work with Staging + +diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php +new file mode 100644 +index 0000000000..d396829ef8 +--- /dev/null ++++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php +@@ -0,0 +1,302 @@ ++connectionMock = $this->getMock(Mysql::class, [], [], '', false); ++ $this->resourceMock = $this->getMock(ResourceConnection::class, [], [], '', false, false); ++ $this->connectionMock->expects($this->any()) ++ ->method('quoteIdentifier') ++ ->will($this->returnArgument(0)); ++ $this->resourceMock->expects($this->atLeastOnce()) ++ ->method('getConnection') ++ ->willReturn($this->connectionMock); ++ $this->triggerFactoryMock = $this->getMock(TriggerFactory::class, [], [], '', false, false); ++ $this->viewCollectionMock = $this->getMockForAbstractClass( ++ CollectionInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $this->viewMock = $this->getMockForAbstractClass(ViewInterface::class, [], '', false, false, true, []); ++ $this->resourceMock->expects($this->any()) ++ ->method('getTableName') ++ ->will($this->returnArgument(0)); ++ ++ $entityInterface = 'EntityInterface'; ++ $this->entityMetadataPoolMock = $this->getMock(MetadataPool::class, [], [], '', false); ++ ++ $this->entityMetadataMock = $this->getMock(EntityMetadataInterface::class, [], [], '', false); ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getEntityTable') ++ ->will($this->returnValue('entity_table')); ++ ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getIdentifierField') ++ ->will($this->returnValue('entity_identifier')); ++ ++ $this->entityMetadataMock->expects($this->any()) ++ ->method('getLinkField') ++ ->will($this->returnValue('entity_link_field')); ++ ++ $this->entityMetadataPoolMock->expects($this->any()) ++ ->method('getMetadata') ++ ->with($entityInterface) ++ ->will($this->returnValue($this->entityMetadataMock)); ++ ++ $this->model = new SubscriptionModel( ++ $this->resourceMock, ++ $this->triggerFactoryMock, ++ $this->viewCollectionMock, ++ $this->viewMock, ++ $this->tableName, ++ 'columnName', ++ $this->entityMetadataPoolMock, ++ $entityInterface ++ ); ++ } ++ ++ /** ++ * Prepare trigger mock ++ * ++ * @param string $triggerName ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareTriggerMock($triggerName) ++ { ++ $triggerMock = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Trigger::class) ++ ->setMethods(['setName', 'getName', 'setTime', 'setEvent', 'setTable', 'addStatement']) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setName') ++ ->with($triggerName) ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('getName') ++ ->will($this->returnValue('triggerName')); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setTime') ++ ->with(\Magento\Framework\DB\Ddl\Trigger::TIME_AFTER) ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setEvent') ++ ->will($this->returnSelf()); ++ $triggerMock->expects($this->exactly(3)) ++ ->method('setTable') ++ ->with($this->tableName) ++ ->will($this->returnSelf()); ++ return $triggerMock; ++ } ++ ++ /** ++ * Prepare expected trigger call map ++ * ++ * @param \PHPUnit_Framework_MockObject_MockObject $triggerMock ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareTriggerTestCallMap(\PHPUnit_Framework_MockObject_MockObject $triggerMock) ++ { ++ $triggerMock->expects($this->at(4)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ ) ++ ->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(5)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(11)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(12)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = NEW.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(18)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = OLD.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ $triggerMock->expects($this->at(19)) ++ ->method('addStatement') ++ ->with( ++ "INSERT IGNORE INTO other_test_view_cl (entity_id) " ++ . "SELECT entity_identifier FROM entity_table WHERE entity_link_field = OLD.entity_link_field;" ++ )->will($this->returnSelf()); ++ ++ return $triggerMock; ++ } ++ ++ /** ++ * Prepare changelog mock ++ * ++ * @param string $changelogName ++ * @return \PHPUnit_Framework_MockObject_MockObject ++ */ ++ protected function prepareChangelogMock($changelogName) ++ { ++ $changelogMock = $this->getMockForAbstractClass( ++ \Magento\Framework\Mview\View\ChangelogInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $changelogMock->expects($this->exactly(3)) ++ ->method('getName') ++ ->will($this->returnValue($changelogName)); ++ $changelogMock->expects($this->exactly(3)) ++ ->method('getColumnName') ++ ->will($this->returnValue('entity_id')); ++ return $changelogMock; ++ } ++ ++ public function testCreate() ++ { ++ $triggerName = 'trigger_name'; ++ $this->resourceMock->expects($this->atLeastOnce())->method('getTriggerName')->willReturn($triggerName); ++ $triggerMock = $this->prepareTriggerMock($triggerName); ++ $this->prepareTriggerTestCallMap($triggerMock); ++ $changelogMock = $this->prepareChangelogMock('test_view_cl'); ++ ++ $this->viewMock->expects($this->exactly(3)) ++ ->method('getChangelog') ++ ->will($this->returnValue($changelogMock)); ++ ++ $this->triggerFactoryMock->expects($this->exactly(3)) ++ ->method('create') ++ ->will($this->returnValue($triggerMock)); ++ ++ $otherChangelogMock = $this->prepareChangelogMock('other_test_view_cl'); ++ ++ $otherViewMock = $this->getMockForAbstractClass( ++ ViewInterface::class, ++ [], ++ '', ++ false, ++ false, ++ true, ++ [] ++ ); ++ $otherViewMock->expects($this->exactly(1)) ++ ->method('getId') ++ ->will($this->returnValue('other_id')); ++ $otherViewMock->expects($this->exactly(1)) ++ ->method('getSubscriptions') ++ ->will($this->returnValue([['name' => $this->tableName], ['name' => 'otherTableName']])); ++ $otherViewMock->expects($this->any()) ++ ->method('getChangelog') ++ ->will($this->returnValue($otherChangelogMock)); ++ ++ $this->viewMock->expects($this->exactly(3)) ++ ->method('getId') ++ ->will($this->returnValue('this_id')); ++ $this->viewMock->expects($this->never()) ++ ->method('getSubscriptions'); ++ ++ $this->viewCollectionMock->expects($this->exactly(1)) ++ ->method('getViewsByStateMode') ++ ->with(StateInterface::MODE_ENABLED) ++ ->will($this->returnValue([$this->viewMock, $otherViewMock])); ++ ++ $this->connectionMock->expects($this->exactly(3)) ++ ->method('dropTrigger') ++ ->with('triggerName') ++ ->will($this->returnValue(true)); ++ $this->connectionMock->expects($this->exactly(3)) ++ ->method('createTrigger') ++ ->with($triggerMock); ++ ++ $this->model->create(); ++ } ++} diff --git a/patches/MAGETWO-84507__fix_complex_folder_js_bundling__2.2.0.patch b/patches/MAGETWO-84507__fix_complex_folder_js_bundling__2.2.0.patch new file mode 100644 index 00000000..7d1ab62d --- /dev/null +++ b/patches/MAGETWO-84507__fix_complex_folder_js_bundling__2.2.0.patch @@ -0,0 +1,13 @@ +diff -Nuar a/vendor/magento/module-require-js/Model/FileManager.php b/vendor/magento/module-require-js/Model/FileManager.php +--- a/vendor/magento/module-require-js/Model/FileManager.php ++++ b/vendor/magento/module-require-js/Model/FileManager.php +@@ -183,6 +183,9 @@ + } + + foreach ($libDir->read($bundleDir) as $bundleFile) { ++ if (pathinfo($bundleFile, PATHINFO_EXTENSION) !== 'js') { ++ continue; ++ } + $relPath = $libDir->getRelativePath($bundleFile); + $bundles[] = $this->assetRepo->createArbitrary($relPath, ''); + } diff --git a/patches/MAGETWO-88336__fix_complex_folder_js_bundling__2.1.4.patch b/patches/MAGETWO-88336__fix_complex_folder_js_bundling__2.1.4.patch new file mode 100644 index 00000000..2d9d8b4e --- /dev/null +++ b/patches/MAGETWO-88336__fix_complex_folder_js_bundling__2.1.4.patch @@ -0,0 +1,13 @@ +diff -Nuar a/vendor/magento/module-require-js/Model/FileManager.php b/vendor/magento/module-require-js/Model/FileManager.php +--- a/vendor/magento/module-require-js/Model/FileManager.php ++++ b/vendor/magento/module-require-js/Model/FileManager.php +@@ -164,6 +164,9 @@ + } + + foreach ($libDir->read($bundleDir) as $bundleFile) { ++ if (pathinfo($bundleFile, PATHINFO_EXTENSION) !== 'js') { ++ continue; ++ } + $relPath = $libDir->getRelativePath($bundleFile); + $bundles[] = $this->assetRepo->createArbitrary($relPath, ''); + } diff --git a/patches/MAGETWO-93265__fix_depth_of_recursive_check_of_directory_permissions__2.1.4.patch b/patches/MAGETWO-93265__fix_depth_of_recursive_check_of_directory_permissions__2.1.4.patch new file mode 100644 index 00000000..3113cb2f --- /dev/null +++ b/patches/MAGETWO-93265__fix_depth_of_recursive_check_of_directory_permissions__2.1.4.patch @@ -0,0 +1,20 @@ +diff -Nuar a/vendor/magento/framework/Setup/FilePermissions.php b/vendor/magento/framework/Setup/FilePermissions.php +--- a/vendor/magento/framework/Setup/FilePermissions.php ++++ b/vendor/magento/framework/Setup/FilePermissions.php +@@ -137,6 +137,7 @@ class FilePermissions + */ + private function checkRecursiveDirectories($directory) + { ++ /** @var $directoryIterator \RecursiveIteratorIterator */ + $directoryIterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST +@@ -155,6 +156,8 @@ class FilePermissions + ] + ); + ++ $directoryIterator->setMaxDepth(1); ++ + $foundNonWritable = false; + + try { diff --git a/patches/MAGETWO-98833__turn_off_google_chart_api__2.x.patch b/patches/MAGETWO-98833__turn_off_google_chart_api__2.x.patch new file mode 100644 index 00000000..4d0dbd83 --- /dev/null +++ b/patches/MAGETWO-98833__turn_off_google_chart_api__2.x.patch @@ -0,0 +1,141 @@ +diff --git a/vendor/magento/module-backend/Block/Dashboard/Graph.php b/vendor/magento/module-backend/Block/Dashboard/Graph.php +index 8e238ccab44c..71a6cf4e938f 100644 +--- a/vendor/magento/module-backend/Block/Dashboard/Graph.php ++++ b/vendor/magento/module-backend/Block/Dashboard/Graph.php +@@ -15,7 +15,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard + /** + * Api URL + */ +- const API_URL = 'http://chart.apis.google.com/chart'; ++ const API_URL = 'https://image-charts.com/chart'; + + /** + * All series +@@ -76,6 +76,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard + /** + * Google chart api data encoding + * ++ * @deprecated since the Google Image Charts API not accessible from March 14, 2019 + * @var string + */ + protected $_encoding = 'e'; +@@ -187,11 +188,12 @@ public function getChartUrl($directUrl = true) + { + $params = [ + 'cht' => 'lc', +- 'chf' => 'bg,s,ffffff', +- 'chco' => 'ef672f', + 'chls' => '7', +- 'chxs' => '0,676056,15,0,l,676056|1,676056,15,0,l,676056', +- 'chm' => 'h,f2ebde,0,0:1:.1,1,-1', ++ 'chf' => 'bg,s,f4f4f4|c,lg,90,ffffff,0.1,ededed,0', ++ 'chm' => 'B,f4d4b2,0,0,0', ++ 'chco' => 'db4814', ++ 'chxs' => '0,0,11|1,0,11', ++ 'chma' => '15,15,15,15' + ]; + + $this->_allSeries = $this->getRowsData($this->_dataRows); +@@ -279,20 +281,11 @@ public function getChartUrl($directUrl = true) + $this->_axisLabels['x'] = $dates; + $this->_allSeries = $datas; + +- //Google encoding values +- if ($this->_encoding == "s") { +- // simple encoding +- $params['chd'] = "s:"; +- $dataDelimiter = ""; +- $dataSetdelimiter = ","; +- $dataMissing = "_"; +- } else { +- // extended encoding +- $params['chd'] = "e:"; +- $dataDelimiter = ""; +- $dataSetdelimiter = ","; +- $dataMissing = "__"; +- } ++ // Image-Charts Awesome data format values ++ $params['chd'] = "a:"; ++ $dataDelimiter = ","; ++ $dataSetdelimiter = "|"; ++ $dataMissing = "_"; + + // process each string in the array, and find the max length + $localmaxvalue = [0]; +@@ -306,7 +299,6 @@ public function getChartUrl($directUrl = true) + $minvalue = min($localminvalue); + + // default values +- $yrange = 0; + $yLabels = []; + $miny = 0; + $maxy = 0; +@@ -321,7 +313,6 @@ public function getChartUrl($directUrl = true) + $maxy = ceil($maxvalue + 1); + $yLabels = range($miny, $maxy, 1); + } +- $yrange = $maxy; + $yorigin = 0; + } + +@@ -329,44 +320,13 @@ public function getChartUrl($directUrl = true) + + foreach ($this->getAllSeries() as $index => $serie) { + $thisdataarray = $serie; +- if ($this->_encoding == "s") { +- // SIMPLE ENCODING +- for ($j = 0; $j < sizeof($thisdataarray); $j++) { +- $currentvalue = $thisdataarray[$j]; +- if (is_numeric($currentvalue)) { +- $ylocation = round( +- (strlen($this->_simpleEncoding) - 1) * ($yorigin + $currentvalue) / $yrange +- ); +- $chartdata[] = substr($this->_simpleEncoding, $ylocation, 1) . $dataDelimiter; +- } else { +- $chartdata[] = $dataMissing . $dataDelimiter; +- } +- } +- } else { +- // EXTENDED ENCODING +- for ($j = 0; $j < sizeof($thisdataarray); $j++) { +- $currentvalue = $thisdataarray[$j]; +- if (is_numeric($currentvalue)) { +- if ($yrange) { +- $ylocation = 4095 * ($yorigin + $currentvalue) / $yrange; +- } else { +- $ylocation = 0; +- } +- $firstchar = floor($ylocation / 64); +- $secondchar = $ylocation % 64; +- $mappedchar = substr( +- $this->_extendedEncoding, +- $firstchar, +- 1 +- ) . substr( +- $this->_extendedEncoding, +- $secondchar, +- 1 +- ); +- $chartdata[] = $mappedchar . $dataDelimiter; +- } else { +- $chartdata[] = $dataMissing . $dataDelimiter; +- } ++ for ($j = 0; $j < sizeof($thisdataarray); $j++) { ++ $currentvalue = $thisdataarray[$j]; ++ if (is_numeric($currentvalue)) { ++ $ylocation = $yorigin + $currentvalue; ++ $chartdata[] = $ylocation . $dataDelimiter; ++ } else { ++ $chartdata[] = $dataMissing . $dataDelimiter; + } + } + $chartdata[] = $dataSetdelimiter; +@@ -540,6 +500,8 @@ protected function getHeight() + } + + /** ++ * Sets data helper ++ * + * @param \Magento\Backend\Helper\Dashboard\AbstractDashboard $dataHelper + * @return void + */ diff --git a/patches/MC-5964__preauth_sql__2.1.4.patch b/patches/MC-5964__preauth_sql__2.1.4.patch new file mode 100644 index 00000000..78a189b6 --- /dev/null +++ b/patches/MC-5964__preauth_sql__2.1.4.patch @@ -0,0 +1,12 @@ +diff -Naur a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +--- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +@@ -2955,7 +2955,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + if (isset($condition['to'])) { + $query .= empty($query) ? '' : ' AND '; + $to = $this->_prepareSqlDateCondition($condition, 'to'); +- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); ++ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); + } + } elseif (array_key_exists($key, $conditionKeyMap)) { + $value = $condition[$key]; diff --git a/patches/MC-5964__preauth_sql__2.2.0.patch b/patches/MC-5964__preauth_sql__2.2.0.patch new file mode 100644 index 00000000..82a111eb --- /dev/null +++ b/patches/MC-5964__preauth_sql__2.2.0.patch @@ -0,0 +1,92 @@ +diff -Naur a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +--- a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php ++++ b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +@@ -138,7 +138,9 @@ private function getProductIdsByActions(array $actions) + $productIds = []; + + foreach ($actions as $action) { +- $productIds[] = $action['product_id']; ++ if (isset($action['product_id']) && is_int($action['product_id'])) { ++ $productIds[] = $action['product_id']; ++ } + } + + return $productIds; +@@ -159,33 +161,37 @@ public function syncActions(array $productsData, $typeId) + $customerId = $this->session->getCustomerId(); + $visitorId = $this->visitor->getId(); + $collection = $this->getActionsByType($typeId); +- $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); +- +- /** +- * Note that collection is also filtered by visitor id and customer id +- * This collection shouldnt be flushed when visitor has products and then login +- * It can remove only products for visitor, or only products for customer +- * +- * ['product_id' => 'added_at'] +- * @var ProductFrontendActionInterface $item +- */ +- foreach ($collection as $item) { +- $this->entityManager->delete($item); +- } +- +- foreach ($productsData as $productId => $productData) { +- /** @var ProductFrontendActionInterface $action */ +- $action = $this->productFrontendActionFactory->create([ +- 'data' => [ +- 'visitor_id' => $customerId ? null : $visitorId, +- 'customer_id' => $this->session->getCustomerId(), +- 'added_at' => $productData['added_at'], +- 'product_id' => $productId, +- 'type_id' => $typeId +- ] +- ]); +- +- $this->entityManager->save($action); ++ $productIds = $this->getProductIdsByActions($productsData); ++ ++ if ($productIds) { ++ $collection->addFieldToFilter('product_id', $productIds); ++ ++ /** ++ * Note that collection is also filtered by visitor id and customer id ++ * This collection shouldnt be flushed when visitor has products and then login ++ * It can remove only products for visitor, or only products for customer ++ * ++ * ['product_id' => 'added_at'] ++ * @var ProductFrontendActionInterface $item ++ */ ++ foreach ($collection as $item) { ++ $this->entityManager->delete($item); ++ } ++ ++ foreach ($productsData as $productId => $productData) { ++ /** @var ProductFrontendActionInterface $action */ ++ $action = $this->productFrontendActionFactory->create([ ++ 'data' => [ ++ 'visitor_id' => $customerId ? null : $visitorId, ++ 'customer_id' => $this->session->getCustomerId(), ++ 'added_at' => $productData['added_at'], ++ 'product_id' => $productId, ++ 'type_id' => $typeId ++ ] ++ ]); ++ ++ $this->entityManager->save($action); ++ } + } + } + +diff -Naur a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +index 3d06e27542f0..a6c0dba6e175 100644 +--- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +@@ -2904,7 +2904,7 @@ public function prepareSqlCondition($fieldName, $condition) + if (isset($condition['to'])) { + $query .= empty($query) ? '' : ' AND '; + $to = $this->_prepareSqlDateCondition($condition, 'to'); +- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); ++ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); + } + } elseif (array_key_exists($key, $conditionKeyMap)) { + $value = $condition[$key]; diff --git a/patches/MC-5964__preauth_sql__2.3.0.patch b/patches/MC-5964__preauth_sql__2.3.0.patch new file mode 100644 index 00000000..fe7b2cfd --- /dev/null +++ b/patches/MC-5964__preauth_sql__2.3.0.patch @@ -0,0 +1,123 @@ +diff -Naur a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +--- a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php ++++ b/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/Synchronizer.php +@@ -16,6 +16,8 @@ + use Magento\Framework\EntityManager\EntityManager; + + /** ++ * A Product Widget Synchronizer. ++ * + * Service which allows to sync product widget information, such as product id with db. In order to reuse this info + * on different devices + */ +@@ -85,9 +87,10 @@ public function __construct( + } + + /** +- * Find lifetime in configuration. Configuration is hold in Stores Configuration +- * Also this configuration is generated by: +- * @see \Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration ++ * Finds lifetime in configuration. ++ * ++ * Configuration is hold in Stores Configuration. Also this configuration is generated by ++ * {@see Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration} + * + * @param string $namespace + * @return int +@@ -108,6 +111,8 @@ private function getLifeTimeByNamespace($namespace) + } + + /** ++ * Filters actions. ++ * + * In order to avoid suspicious actions, we need to filter them in DESC order, and slice only items that + * can be persisted in database. + * +@@ -138,7 +143,9 @@ private function getProductIdsByActions(array $actions) + $productIds = []; + + foreach ($actions as $action) { +- $productIds[] = $action['product_id']; ++ if (isset($action['product_id']) && is_int($action['product_id'])) { ++ $productIds[] = $action['product_id']; ++ } + } + + return $productIds; +@@ -159,33 +166,37 @@ public function syncActions(array $productsData, $typeId) + $customerId = $this->session->getCustomerId(); + $visitorId = $this->visitor->getId(); + $collection = $this->getActionsByType($typeId); +- $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); +- +- /** +- * Note that collection is also filtered by visitor id and customer id +- * This collection shouldn't be flushed when visitor has products and then login +- * It can remove only products for visitor, or only products for customer +- * +- * ['product_id' => 'added_at'] +- * @var ProductFrontendActionInterface $item +- */ +- foreach ($collection as $item) { +- $this->entityManager->delete($item); +- } +- +- foreach ($productsData as $productId => $productData) { +- /** @var ProductFrontendActionInterface $action */ +- $action = $this->productFrontendActionFactory->create([ +- 'data' => [ +- 'visitor_id' => $customerId ? null : $visitorId, +- 'customer_id' => $this->session->getCustomerId(), +- 'added_at' => $productData['added_at'], +- 'product_id' => $productId, +- 'type_id' => $typeId +- ] +- ]); +- +- $this->entityManager->save($action); ++ $productIds = $this->getProductIdsByActions($productsData); ++ ++ if ($productIds) { ++ $collection->addFieldToFilter('product_id', $productIds); ++ ++ /** ++ * Note that collection is also filtered by visitor id and customer id ++ * This collection shouldn't be flushed when visitor has products and then login ++ * It can remove only products for visitor, or only products for customer ++ * ++ * ['product_id' => 'added_at'] ++ * @var ProductFrontendActionInterface $item ++ */ ++ foreach ($collection as $item) { ++ $this->entityManager->delete($item); ++ } ++ ++ foreach ($productsData as $productId => $productData) { ++ /** @var ProductFrontendActionInterface $action */ ++ $action = $this->productFrontendActionFactory->create([ ++ 'data' => [ ++ 'visitor_id' => $customerId ? null : $visitorId, ++ 'customer_id' => $this->session->getCustomerId(), ++ 'added_at' => $productData['added_at'], ++ 'product_id' => $productId, ++ 'type_id' => $typeId ++ ] ++ ]); ++ ++ $this->entityManager->save($action); ++ } + } + } + +diff -Naur a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +--- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php ++++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +@@ -2955,7 +2955,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface + if (isset($condition['to'])) { + $query .= empty($query) ? '' : ' AND '; + $to = $this->_prepareSqlDateCondition($condition, 'to'); +- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); ++ $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); + } + } elseif (array_key_exists($key, $conditionKeyMap)) { + $value = $condition[$key]; diff --git a/patches/MDVA-2470__fix_asset_locking_race_condition__2.1.4.patch b/patches/MDVA-2470__fix_asset_locking_race_condition__2.1.4.patch new file mode 100644 index 00000000..01528397 --- /dev/null +++ b/patches/MDVA-2470__fix_asset_locking_race_condition__2.1.4.patch @@ -0,0 +1,109 @@ +MDVA-2470 +diff -Naur a/vendor/magento/module-deploy/Model/Deploy/LocaleDeploy.php b/vendor/magento/module-deploy/Model/Deploy/LocaleDeploy.php +--- a/vendor/magento/module-deploy/Model/Deploy/LocaleDeploy.php 2017-02-08 19:50:59.000000000 +0000 ++++ b/vendor/magento/module-deploy/Model/Deploy/LocaleDeploy.php 2017-02-13 18:50:46.000000000 +0000 +@@ -410,7 +410,7 @@ + $this->logger->critical($errorMessage); + } catch (\Exception $exception) { + $this->output->write('.'); +- if ($this->output->isVerbose()) { ++ if ($this->output->isVerbose() || $this->output->isVeryVerbose()) { + $this->output->writeln($exception->getTraceAsString()); + } + $this->errorCount++; + +diff -Naur a/vendor/magento/framework/View/Asset/LockerProcess.php b/vendor/magento/framework/View/Asset/LockerProcess.php +--- a/vendor/magento/framework/View/Asset/LockerProcess.php 2017-02-13 17:38:15.000000000 +0000 ++++ b/vendor/magento/framework/View/Asset/LockerProcess.php 2017-02-13 18:53:22.000000000 +0000 +@@ -68,12 +68,26 @@ + + $this->tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->lockFilePath = $this->getFilePath($lockName); ++ $this->waitForLock(); + +- while ($this->isProcessLocked()) { +- usleep(1000); ++ try { ++ $this->tmpDirectory->writeFile($this->lockFilePath, time(), 'x+'); ++ }catch (\Exception $e) { ++ $this->waitForLock(); ++ try { ++ $this->tmpDirectory->writeFile($this->lockFilePath, time(), 'x+'); ++ }catch (\Exception $e) { ++ throw new \Exception($e->getMessage()); ++ } + } + +- $this->tmpDirectory->writeFile($this->lockFilePath, time()); ++ } ++ ++ public function waitForLock() ++ { ++ while ($this->isProcessLocked() ) { ++ usleep(500); ++ } + } + + /** + +diff -Nuar a/vendor/magento/module-deploy/Model/Deploy/LocaleDeploy.php b/vendor/magento/module-deploy/Model/Deploy/LocaleDeploy.php +--- a/vendor/magento/module-deploy/Model/Deploy/LocaleDeploy.php 2017-02-14 20:20:28.000000000 +0000 ++++ b/vendor/magento/module-deploy/Model/Deploy/LocaleDeploy.php 2017-02-14 20:40:07.000000000 +0000 +@@ -410,9 +410,7 @@ + $this->logger->critical($errorMessage); + } catch (\Exception $exception) { + $this->output->write('.'); +- if ($this->output->isVerbose() || $this->output->isVeryVerbose()) { +- $this->output->writeln($exception->getTraceAsString()); +- } ++ $this->output->writeln($exception->getTraceAsString()); + $this->errorCount++; + } + +diff -Naur a/vendor/magento/framework/View/Asset/PreProcessor/AlternativeSource.php b/vendor/magento/framework/View/Asset/PreProcessor/AlternativeSource.php +--- a/vendor/magento/framework/View/Asset/PreProcessor/AlternativeSource.php 2017-02-14 20:49:33.000000000 +0000 ++++ b/vendor/magento/framework/View/Asset/PreProcessor/AlternativeSource.php 2017-02-15 15:00:41.000000000 +0000 +@@ -106,7 +106,7 @@ + } + + try { +- $this->lockerProcess->lockProcess($this->lockName); ++ $this->lockerProcess->lockProcess($chain->getAsset()->getPath()); + + $module = $chain->getAsset()->getModule(); + +diff -Naur a/vendor/magento/framework/View/Asset/LockerProcess.php b/vendor/magento/framework/View/Asset/LockerProcess.php +--- a/vendor/magento/framework/View/Asset/LockerProcess.php 2017-02-14 21:50:57.000000000 +0000 ++++ b/vendor/magento/framework/View/Asset/LockerProcess.php 2017-02-15 15:00:41.000000000 +0000 +@@ -67,7 +67,7 @@ + } + + $this->tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); +- $this->lockFilePath = $this->getFilePath($lockName); ++ $this->lockFilePath = $this->getFilePath(str_replace(DIRECTORY_SEPARATOR, '_', $lockName)); + $this->waitForLock(); + + try { +@@ -77,7 +77,8 @@ + try { + $this->tmpDirectory->writeFile($this->lockFilePath, time(), 'x+'); + }catch (\Exception $e) { +- throw new \Exception($e->getMessage()); ++ echo($this->lockFilePath); ++ throw new \Exception("In exception for lock process" . $e->getMessage()); + } + } + +diff -Naur a/vendor/magento/module-developer/Model/View/Asset/PreProcessor/FrontendCompilation.php b/vendor/magento/module-developer/Model/View/Asset/PreProcessor/FrontendCompilation.php +--- a/vendor/magento/module-developer/Model/View/Asset/PreProcessor/FrontendCompilation.php 2017-02-15 16:24:07.000000000 +0000 ++++ b/vendor/magento/module-developer/Model/View/Asset/PreProcessor/FrontendCompilation.php 2017-02-15 16:24:07.000000000 +0000 +@@ -80,7 +80,7 @@ + } + + try { +- $this->lockerProcess->lockProcess($this->lockName); ++ $this->lockerProcess->lockProcess($chain->getAsset()->getPath()); + + $path = $chain->getAsset()->getFilePath(); + $module = $chain->getAsset()->getModule(); + diff --git a/patches/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch b/patches/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch new file mode 100644 index 00000000..ed82a402 --- /dev/null +++ b/patches/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch @@ -0,0 +1,80 @@ +MDVA-2470 +diff -Nuar a/vendor/magento/framework/View/Asset/LockerProcess.php b/vendor/magento/framework/View/Asset/LockerProcess.php +--- a/vendor/magento/framework/View/Asset/LockerProcess.php 2017-02-13 17:38:15.000000000 +0000 ++++ b/vendor/magento/framework/View/Asset/LockerProcess.php 2017-02-13 18:53:22.000000000 +0000 +@@ -68,12 +68,26 @@ + + $this->tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->lockFilePath = $this->getFilePath($lockName); ++ $this->waitForLock(); + +- while ($this->isProcessLocked()) { +- usleep(1000); ++ try { ++ $this->tmpDirectory->writeFile($this->lockFilePath, time(), 'x+'); ++ }catch (\Exception $e) { ++ $this->waitForLock(); ++ try { ++ $this->tmpDirectory->writeFile($this->lockFilePath, time(), 'x+'); ++ }catch (\Exception $e) { ++ throw new \Exception($e->getMessage()); ++ } + } + +- $this->tmpDirectory->writeFile($this->lockFilePath, time()); ++ } ++ ++ public function waitForLock() ++ { ++ while ($this->isProcessLocked() ) { ++ usleep(500); ++ } + } + + /** +diff -Nuar a/vendor/magento/framework/View/Asset/PreProcessor/AlternativeSource.php b/vendor/magento/framework/View/Asset/PreProcessor/AlternativeSource.php +--- a/vendor/magento/framework/View/Asset/PreProcessor/AlternativeSource.php 2017-02-14 20:49:33.000000000 +0000 ++++ b/vendor/magento/framework/View/Asset/PreProcessor/AlternativeSource.php 2017-02-15 15:00:41.000000000 +0000 +@@ -106,7 +106,7 @@ + } + + try { +- $this->lockerProcess->lockProcess($this->lockName); ++ $this->lockerProcess->lockProcess($chain->getAsset()->getPath()); + + $module = $chain->getAsset()->getModule(); + +diff -Nuar a/vendor/magento/framework/View/Asset/LockerProcess.php b/vendor/magento/framework/View/Asset/LockerProcess.php +--- a/vendor/magento/framework/View/Asset/LockerProcess.php 2017-02-14 21:50:57.000000000 +0000 ++++ b/vendor/magento/framework/View/Asset/LockerProcess.php 2017-02-15 15:00:41.000000000 +0000 +@@ -67,7 +67,7 @@ + } + + $this->tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); +- $this->lockFilePath = $this->getFilePath($lockName); ++ $this->lockFilePath = $this->getFilePath(str_replace(DIRECTORY_SEPARATOR, '_', $lockName)); + $this->waitForLock(); + + try { +@@ -77,7 +77,8 @@ + try { + $this->tmpDirectory->writeFile($this->lockFilePath, time(), 'x+'); + }catch (\Exception $e) { +- throw new \Exception($e->getMessage()); ++ echo($this->lockFilePath); ++ throw new \Exception("In exception for lock process" . $e->getMessage()); + } + } + +diff -Nuar a/vendor/magento/module-developer/Model/View/Asset/PreProcessor/FrontendCompilation.php b/vendor/magento/module-developer/Model/View/Asset/PreProcessor/FrontendCompilation.php +--- a/vendor/magento/module-developer/Model/View/Asset/PreProcessor/FrontendCompilation.php 2017-02-15 16:24:07.000000000 +0000 ++++ b/vendor/magento/module-developer/Model/View/Asset/PreProcessor/FrontendCompilation.php 2017-02-15 16:24:07.000000000 +0000 +@@ -76,7 +76,7 @@ + { + + try { +- $this->lockerProcess->lockProcess($this->lockName); ++ $this->lockerProcess->lockProcess($chain->getAsset()->getPath()); + + $path = $chain->getAsset()->getFilePath(); + $module = $chain->getAsset()->getModule(); diff --git a/patches/MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch b/patches/MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch new file mode 100644 index 00000000..6e2036c4 --- /dev/null +++ b/patches/MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch @@ -0,0 +1,12 @@ +diff -Nuar a/vendor/magento/framework/Mail/Message.php b/vendor/magento/framework/Mail/Message.php +index 36a0e6a..fad0910 100644 +--- a/vendor/magento/framework/Mail/Message.php ++++ b/vendor/magento/framework/Mail/Message.php +@@ -15,6 +15,7 @@ class Message extends \Zend_Mail implements MessageInterface + public function __construct($charset = 'utf-8') + { + parent::__construct($charset); ++ $this->setHeaderEncoding(\Zend_Mime::ENCODING_BASE64); + } + + /** diff --git a/patches/MSI-2210__price_indexer_fails_with_large_catalogs__1.0.3.patch b/patches/MSI-2210__price_indexer_fails_with_large_catalogs__1.0.3.patch new file mode 100644 index 00000000..976f65ed --- /dev/null +++ b/patches/MSI-2210__price_indexer_fails_with_large_catalogs__1.0.3.patch @@ -0,0 +1,88 @@ +diff -Nuar a/vendor/magento/module-inventory-catalog/Plugin/CatalogInventory/Model/Indexer/ModifySelectInProductPriceIndexFilter.php b/vendor/magento/module-inventory-catalog/Plugin/CatalogInventory/Model/Indexer/ModifySelectInProductPriceIndexFilter.php +--- a/vendor/magento/module-inventory-catalog/Plugin/CatalogInventory/Model/Indexer/ModifySelectInProductPriceIndexFilter.php ++++ b/vendor/magento/module-inventory-catalog/Plugin/CatalogInventory/Model/Indexer/ModifySelectInProductPriceIndexFilter.php +@@ -11,6 +11,8 @@ + use Magento\CatalogInventory\Api\StockConfigurationInterface; + use Magento\CatalogInventory\Model\Indexer\ProductPriceIndexFilter; + use Magento\Framework\App\ResourceConnection; ++use Magento\InventoryApi\Api\Data\StockInterface; ++use Magento\InventoryCatalogApi\Api\DefaultStockProviderInterface; + use Magento\InventoryIndexer\Model\StockIndexTableNameResolverInterface; + use Magento\InventorySalesApi\Model\StockByWebsiteIdResolverInterface; + +@@ -39,22 +41,30 @@ class ModifySelectInProductPriceIndexFilter + */ + private $stockByWebsiteIdResolver; + ++ /** ++ * @var DefaultStockProviderInterface ++ */ ++ private $defaultStockProvider; ++ + /** + * @param StockIndexTableNameResolverInterface $stockIndexTableNameResolver + * @param StockConfigurationInterface $stockConfiguration + * @param ResourceConnection $resourceConnection + * @param StockByWebsiteIdResolverInterface $stockByWebsiteIdResolver ++ * @param DefaultStockProviderInterface $defaultStockProvider + */ + public function __construct( + StockIndexTableNameResolverInterface $stockIndexTableNameResolver, + StockConfigurationInterface $stockConfiguration, + ResourceConnection $resourceConnection, +- StockByWebsiteIdResolverInterface $stockByWebsiteIdResolver ++ StockByWebsiteIdResolverInterface $stockByWebsiteIdResolver, ++ DefaultStockProviderInterface $defaultStockProvider + ) { + $this->stockIndexTableNameResolver = $stockIndexTableNameResolver; + $this->stockConfiguration = $stockConfiguration; + $this->resourceConnection = $resourceConnection; + $this->stockByWebsiteIdResolver = $stockByWebsiteIdResolver; ++ $this->defaultStockProvider = $defaultStockProvider; + } + + /** +@@ -84,7 +94,8 @@ public function aroundModifyPrice( + $select->from(['price_index' => $priceTable->getTableName()], []); + $priceEntityField = $priceTable->getEntityField(); + +- if ($this->resourceConnection->getConnection()->isTableExists($stockTable)) { ++ if (!$this->isDefaultStock($stock) ++ && $this->resourceConnection->getConnection()->isTableExists($stockTable)) { + $select->joinInner( + ['product_entity' => $this->resourceConnection->getTableName('catalog_product_entity')], + "product_entity.entity_id = price_index.{$priceEntityField}", +@@ -95,6 +106,17 @@ public function aroundModifyPrice( + [] + ); + $select->where('inventory_stock.is_salable = 0 OR inventory_stock.is_salable IS NULL'); ++ } else { ++ $legacyStockTableName = $this->resourceConnection->getTableName('cataloginventory_stock_status'); ++ $select->joinLeft( ++ ['stock_status' => $legacyStockTableName], ++ sprintf( ++ 'stock_status.product_id = price_index.%s', ++ $priceEntityField ++ ), ++ [] ++ ); ++ $select->where('stock_status.stock_status = 0 OR stock_status.stock_status IS NULL'); + } + + $select->where('price_index.website_id = ?', $websiteId); +@@ -126,4 +148,15 @@ private function getWebsiteIdsFromProducts(array $entityIds): array + + return $result; + } ++ ++ /** ++ * Checks if inventory stock is DB view ++ * ++ * @param StockInterface $stock ++ * @return bool ++ */ ++ private function isDefaultStock(StockInterface $stock): bool ++ { ++ return (int)$stock->getStockId() === $this->defaultStockProvider->getId(); ++ } + } diff --git a/patches/MSI-GH-2350__avoid_quering_inventory_default_stock_view_in_storefront__1.0.3.patch b/patches/MSI-GH-2350__avoid_quering_inventory_default_stock_view_in_storefront__1.0.3.patch new file mode 100644 index 00000000..7fd18731 --- /dev/null +++ b/patches/MSI-GH-2350__avoid_quering_inventory_default_stock_view_in_storefront__1.0.3.patch @@ -0,0 +1,82 @@ +diff -Nuar a/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetStockItemData.php b/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetStockItemData.php +--- a/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetStockItemData.php ++++ b/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetStockItemData.php +@@ -12,6 +12,8 @@ + use Magento\InventoryIndexer\Model\StockIndexTableNameResolverInterface; + use Magento\InventorySalesApi\Model\GetStockItemDataInterface; + use Magento\InventoryIndexer\Indexer\IndexStructure; ++use Magento\InventoryCatalogApi\Api\DefaultStockProviderInterface; ++use Magento\InventoryCatalogApi\Model\GetProductIdsBySkusInterface; + + /** + * @inheritdoc +@@ -28,16 +30,32 @@ class GetStockItemData implements GetStockItemDataInterface + */ + private $stockIndexTableNameResolver; + ++ /** ++ * @var DefaultStockProviderInterface ++ */ ++ private $defaultStockProvider; ++ ++ /** ++ * @var GetProductIdsBySkusInterface ++ */ ++ private $getProductIdsBySkus; ++ + /** + * @param ResourceConnection $resource + * @param StockIndexTableNameResolverInterface $stockIndexTableNameResolver ++ * @param DefaultStockProviderInterface $defaultStockProvider ++ * @param GetProductIdsBySkusInterface $getProductIdsBySkus + */ + public function __construct( + ResourceConnection $resource, +- StockIndexTableNameResolverInterface $stockIndexTableNameResolver ++ StockIndexTableNameResolverInterface $stockIndexTableNameResolver, ++ DefaultStockProviderInterface $defaultStockProvider, ++ GetProductIdsBySkusInterface $getProductIdsBySkus + ) { + $this->resource = $resource; + $this->stockIndexTableNameResolver = $stockIndexTableNameResolver; ++ $this->defaultStockProvider = $defaultStockProvider; ++ $this->getProductIdsBySkus = $getProductIdsBySkus; + } + + /** +@@ -45,18 +63,29 @@ public function __construct( + */ + public function execute(string $sku, int $stockId): ?array + { +- $stockItemTableName = $this->stockIndexTableNameResolver->execute($stockId); +- + $connection = $this->resource->getConnection(); +- $select = $connection->select() +- ->from( ++ $select = $connection->select(); ++ ++ if ($this->defaultStockProvider->getId() === $stockId) { ++ $productId = current($this->getProductIdsBySkus->execute([$sku])); ++ $stockItemTableName = $this->resource->getTableName('cataloginventory_stock_status'); ++ $select->from( ++ $stockItemTableName, ++ [ ++ GetStockItemDataInterface::QUANTITY => 'qty', ++ GetStockItemDataInterface::IS_SALABLE => 'stock_status', ++ ] ++ )->where('product_id = ?', $productId); ++ } else { ++ $stockItemTableName = $this->stockIndexTableNameResolver->execute($stockId); ++ $select->from( + $stockItemTableName, + [ + GetStockItemDataInterface::QUANTITY => IndexStructure::QUANTITY, + GetStockItemDataInterface::IS_SALABLE => IndexStructure::IS_SALABLE, + ] +- ) +- ->where(IndexStructure::SKU . ' = ?', $sku); ++ )->where(IndexStructure::SKU . ' = ?', $sku); ++ } + + try { + if ($connection->isTableExists($stockItemTableName)) { diff --git a/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__configurable-product-indexer__1.0.3.patch b/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__configurable-product-indexer__1.0.3.patch new file mode 100644 index 00000000..cec0575e --- /dev/null +++ b/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__configurable-product-indexer__1.0.3.patch @@ -0,0 +1,102 @@ +diff -Nuar a/vendor/magento/module-inventory-configurable-product-indexer/Indexer/SourceItem/SiblingSkuListInStockProvider.php b/vendor/magento/module-inventory-configurable-product-indexer/Indexer/SourceItem/SiblingSkuListInStockProvider.php +--- a/vendor/magento/module-inventory-configurable-product-indexer/Indexer/SourceItem/SiblingSkuListInStockProvider.php ++++ b/vendor/magento/module-inventory-configurable-product-indexer/Indexer/SourceItem/SiblingSkuListInStockProvider.php +@@ -31,10 +31,6 @@ class SiblingSkuListInStockProvider + */ + private $skuListInStockFactory; + +- /** +- * @var int +- */ +- private $groupConcatMaxLen; + /** + * @var MetadataPool + */ +@@ -56,7 +52,6 @@ class SiblingSkuListInStockProvider + * @param ResourceConnection $resourceConnection + * @param SkuListInStockFactory $skuListInStockFactory + * @param MetadataPool $metadataPool +- * @param int $groupConcatMaxLen + * @param string $tableNameSourceItem + * @param string $tableNameStockSourceLink + */ +@@ -64,13 +59,11 @@ public function __construct( + ResourceConnection $resourceConnection, + SkuListInStockFactory $skuListInStockFactory, + MetadataPool $metadataPool, +- int $groupConcatMaxLen, + $tableNameSourceItem, + $tableNameStockSourceLink + ) { + $this->resourceConnection = $resourceConnection; + $this->skuListInStockFactory = $skuListInStockFactory; +- $this->groupConcatMaxLen = $groupConcatMaxLen; + $this->metadataPool = $metadataPool; + $this->tableNameSourceItem = $tableNameSourceItem; + $this->tableNameStockSourceLink = $tableNameStockSourceLink; +@@ -91,15 +84,13 @@ public function execute(array $sourceItemIds): array + + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); ++ $items = []; + + $select = $connection + ->select() + ->from( + ['source_item' => $sourceItemTable], +- [ +- SourceItemInterface::SKU => +- "GROUP_CONCAT(DISTINCT sibling_product_entity." . SourceItemInterface::SKU . " SEPARATOR ',')" +- ] ++ [SourceItemInterface::SKU => 'sibling_product_entity.' . SourceItemInterface::SKU] + )->joinInner( + ['stock_source_link' => $sourceStockLinkTable], + sprintf( +@@ -124,11 +115,17 @@ public function execute(array $sourceItemIds): array + ['sibling_product_entity' => $this->resourceConnection->getTableName('catalog_product_entity')], + 'sibling_product_entity.' . $linkField . ' = sibling_link.product_id', + [] +- )->where('source_item.source_item_id IN (?)', $sourceItemIds) +- ->group(['stock_source_link.' . StockSourceLinkInterface::STOCK_ID]); ++ )->where( ++ 'source_item.source_item_id IN (?)', ++ $sourceItemIds ++ ); ++ ++ $dbStatement = $connection->query($select); ++ while ($item = $dbStatement->fetch()) { ++ $items[$item[StockSourceLinkInterface::STOCK_ID]][$item[SourceItemInterface::SKU]] = ++ $item[SourceItemInterface::SKU]; ++ } + +- $connection->query('SET group_concat_max_len = ' . $this->groupConcatMaxLen); +- $items = $connection->fetchAll($select); + return $this->getStockIdToSkuList($items); + } + +@@ -141,11 +138,11 @@ public function execute(array $sourceItemIds): array + private function getStockIdToSkuList(array $items): array + { + $skuListInStockList = []; +- foreach ($items as $item) { ++ foreach ($items as $stockId => $skuList) { + /** @var SkuListInStock $skuListInStock */ + $skuListInStock = $this->skuListInStockFactory->create(); +- $skuListInStock->setStockId((int)$item[StockSourceLinkInterface::STOCK_ID]); +- $skuListInStock->setSkuList(explode(',', $item[SourceItemInterface::SKU])); ++ $skuListInStock->setStockId((int)$stockId); ++ $skuListInStock->setSkuList($skuList); + $skuListInStockList[] = $skuListInStock; + } + return $skuListInStockList; +diff -Nuar a/vendor/magento/module-inventory-configurable-product-indexer/etc/di.xml b/vendor/magento/module-inventory-configurable-product-indexer/etc/di.xml +--- a/vendor/magento/module-inventory-configurable-product-indexer/etc/di.xml ++++ b/vendor/magento/module-inventory-configurable-product-indexer/etc/di.xml +@@ -27,7 +27,6 @@ + + + +- 2000 + Magento\Inventory\Model\ResourceModel\SourceItem::TABLE_NAME_SOURCE_ITEM + Magento\Inventory\Model\ResourceModel\StockSourceLink::TABLE_NAME_STOCK_SOURCE_LINK + diff --git a/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__grouped-product-indexer__1.0.3.patch b/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__grouped-product-indexer__1.0.3.patch new file mode 100644 index 00000000..96347539 --- /dev/null +++ b/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__grouped-product-indexer__1.0.3.patch @@ -0,0 +1,106 @@ +diff -Nuar a/vendor/magento/module-inventory-grouped-product-indexer/Indexer/SourceItem/SiblingSkuListInStockProvider.php b/vendor/magento/module-inventory-grouped-product-indexer/Indexer/SourceItem/SiblingSkuListInStockProvider.php +--- a/vendor/magento/module-inventory-grouped-product-indexer/Indexer/SourceItem/SiblingSkuListInStockProvider.php ++++ b/vendor/magento/module-inventory-grouped-product-indexer/Indexer/SourceItem/SiblingSkuListInStockProvider.php +@@ -32,10 +32,6 @@ class SiblingSkuListInStockProvider + */ + private $skuListInStockFactory; + +- /** +- * @var int +- */ +- private $groupConcatMaxLen; + /** + * @var MetadataPool + */ +@@ -52,12 +48,9 @@ class SiblingSkuListInStockProvider + private $tableNameStockSourceLink; + + /** +- * GetSkuListInStock constructor. +- * + * @param ResourceConnection $resourceConnection + * @param SkuListInStockFactory $skuListInStockFactory + * @param MetadataPool $metadataPool +- * @param int $groupConcatMaxLen + * @param string $tableNameSourceItem + * @param string $tableNameStockSourceLink + */ +@@ -65,13 +58,11 @@ public function __construct( + ResourceConnection $resourceConnection, + SkuListInStockFactory $skuListInStockFactory, + MetadataPool $metadataPool, +- int $groupConcatMaxLen, + $tableNameSourceItem, + $tableNameStockSourceLink + ) { + $this->resourceConnection = $resourceConnection; + $this->skuListInStockFactory = $skuListInStockFactory; +- $this->groupConcatMaxLen = $groupConcatMaxLen; + $this->metadataPool = $metadataPool; + $this->tableNameSourceItem = $tableNameSourceItem; + $this->tableNameStockSourceLink = $tableNameStockSourceLink; +@@ -92,15 +83,13 @@ public function execute(array $sourceItemIds): array + + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); ++ $items = []; + + $select = $connection + ->select() + ->from( + ['source_item' => $sourceItemTable], +- [ +- SourceItemInterface::SKU => +- "GROUP_CONCAT(DISTINCT sibling_product_entity." . SourceItemInterface::SKU . " SEPARATOR ',')" +- ] ++ [SourceItemInterface::SKU => 'sibling_product_entity.' . SourceItemInterface::SKU] + )->joinInner( + ['stock_source_link' => $sourceStockLinkTable], + sprintf( +@@ -127,11 +116,16 @@ public function execute(array $sourceItemIds): array + ['sibling_product_entity' => $this->resourceConnection->getTableName('catalog_product_entity')], + 'sibling_product_entity.' . $linkField . ' = sibling_link.linked_product_id', + [] +- )->where('source_item.source_item_id IN (?)', $sourceItemIds) +- ->group(['stock_source_link.' . StockSourceLinkInterface::STOCK_ID]); ++ )->where( ++ 'source_item.source_item_id IN (?)', ++ $sourceItemIds ++ ); + +- $connection->query('SET group_concat_max_len = ' . $this->groupConcatMaxLen); +- $items = $connection->fetchAll($select); ++ $dbStatement = $connection->query($select); ++ while ($item = $dbStatement->fetch()) { ++ $items[$item[StockSourceLinkInterface::STOCK_ID]][$item[SourceItemInterface::SKU]] = ++ $item[SourceItemInterface::SKU]; ++ } + + return $this->getStockIdToSkuList($items); + } +@@ -145,11 +139,11 @@ public function execute(array $sourceItemIds): array + private function getStockIdToSkuList(array $items): array + { + $skuListInStockList = []; +- foreach ($items as $item) { ++ foreach ($items as $stockId => $skuList) { + /** @var SkuListInStock $skuListInStock */ + $skuListInStock = $this->skuListInStockFactory->create(); +- $skuListInStock->setStockId((int)$item[StockSourceLinkInterface::STOCK_ID]); +- $skuListInStock->setSkuList(explode(',', $item[SourceItemInterface::SKU])); ++ $skuListInStock->setStockId((int)$stockId); ++ $skuListInStock->setSkuList($skuList); + $skuListInStockList[] = $skuListInStock; + } + return $skuListInStockList; +diff -Nuar a/vendor/magento/module-inventory-grouped-product-indexer/etc/di.xml b/vendor/magento/module-inventory-grouped-product-indexer/etc/di.xml +--- a/vendor/magento/module-inventory-grouped-product-indexer/etc/di.xml ++++ b/vendor/magento/module-inventory-grouped-product-indexer/etc/di.xml +@@ -27,7 +27,6 @@ + + + +- 2000 + Magento\Inventory\Model\ResourceModel\SourceItem::TABLE_NAME_SOURCE_ITEM + Magento\Inventory\Model\ResourceModel\StockSourceLink::TABLE_NAME_STOCK_SOURCE_LINK + diff --git a/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__indexer__1.0.3.patch b/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__indexer__1.0.3.patch new file mode 100644 index 00000000..61a09e0a --- /dev/null +++ b/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__indexer__1.0.3.patch @@ -0,0 +1,99 @@ +diff -Nuar a/vendor/magento/module-inventory-indexer/Indexer/SourceItem/GetSkuListInStock.php b/vendor/magento/module-inventory-indexer/Indexer/SourceItem/GetSkuListInStock.php +--- a/vendor/magento/module-inventory-indexer/Indexer/SourceItem/GetSkuListInStock.php ++++ b/vendor/magento/module-inventory-indexer/Indexer/SourceItem/GetSkuListInStock.php +@@ -29,25 +29,15 @@ class GetSkuListInStock + private $skuListInStockFactory; + + /** +- * @var int +- */ +- private $groupConcatMaxLen; +- +- /** +- * GetSkuListInStock constructor. +- * + * @param ResourceConnection $resourceConnection + * @param SkuListInStockFactory $skuListInStockFactory +- * @param int $groupConcatMaxLen + */ + public function __construct( + ResourceConnection $resourceConnection, +- SkuListInStockFactory $skuListInStockFactory, +- int $groupConcatMaxLen ++ SkuListInStockFactory $skuListInStockFactory + ) { + $this->resourceConnection = $resourceConnection; + $this->skuListInStockFactory = $skuListInStockFactory; +- $this->groupConcatMaxLen = $groupConcatMaxLen; + } + + /** +@@ -65,15 +55,13 @@ public function execute(array $sourceItemIds): array + $sourceItemTable = $this->resourceConnection->getTableName( + SourceItemResourceModel::TABLE_NAME_SOURCE_ITEM + ); ++ $items = []; + + $select = $connection + ->select() + ->from( + ['source_item' => $sourceItemTable], +- [ +- SourceItemInterface::SKU => +- sprintf("GROUP_CONCAT(DISTINCT %s SEPARATOR ',')", 'source_item.' . SourceItemInterface::SKU) +- ] ++ [SourceItemInterface::SKU => 'source_item.' . SourceItemInterface::SKU] + )->joinInner( + ['stock_source_link' => $sourceStockLinkTable], + sprintf( +@@ -82,11 +70,16 @@ public function execute(array $sourceItemIds): array + StockSourceLink::SOURCE_CODE + ), + [StockSourceLink::STOCK_ID] +- )->where('source_item.source_item_id IN (?)', $sourceItemIds) +- ->group(['stock_source_link.' . StockSourceLink::STOCK_ID]); ++ )->where( ++ 'source_item.source_item_id IN (?)', ++ $sourceItemIds ++ ); ++ ++ $dbStatement = $connection->query($select); ++ while ($item = $dbStatement->fetch()) { ++ $items[$item[StockSourceLink::STOCK_ID]][$item[SourceItemInterface::SKU]] = $item[SourceItemInterface::SKU]; ++ } + +- $connection->query('SET group_concat_max_len = ' . $this->groupConcatMaxLen); +- $items = $connection->fetchAll($select); + return $this->getStockIdToSkuList($items); + } + +@@ -99,11 +92,11 @@ public function execute(array $sourceItemIds): array + private function getStockIdToSkuList(array $items): array + { + $skuListInStockList = []; +- foreach ($items as $item) { ++ foreach ($items as $stockId => $skuList) { + /** @var SkuListInStock $skuListInStock */ + $skuListInStock = $this->skuListInStockFactory->create(); +- $skuListInStock->setStockId((int)$item[StockSourceLink::STOCK_ID]); +- $skuListInStock->setSkuList(explode(',', $item[SourceItemInterface::SKU])); ++ $skuListInStock->setStockId((int)$stockId); ++ $skuListInStock->setSkuList($skuList); + $skuListInStockList[] = $skuListInStock; + } + return $skuListInStockList; +diff -Nuar a/vendor/magento/module-inventory-indexer/etc/di.xml b/vendor/magento/module-inventory-indexer/etc/di.xml +--- a/vendor/magento/module-inventory-indexer/etc/di.xml ++++ b/vendor/magento/module-inventory-indexer/etc/di.xml +@@ -25,11 +25,6 @@ + Magento\InventoryIndexer\Indexer\IndexHandler + + +- +- +- 2000 +- +- + + + catalog_product_entity diff --git a/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__reservations__1.0.3.patch b/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__reservations__1.0.3.patch new file mode 100644 index 00000000..96ad36d0 --- /dev/null +++ b/patches/MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__reservations__1.0.3.patch @@ -0,0 +1,12 @@ +diff -Nuar a/vendor/magento/module-inventory-reservations/etc/di.xml b/vendor/magento/module-inventory-reservations/etc/di.xml +--- a/vendor/magento/module-inventory-reservations/etc/di.xml ++++ b/vendor/magento/module-inventory-reservations/etc/di.xml +@@ -13,7 +13,7 @@ + + + +- 2000 ++ 32768 + + + diff --git a/patches/SET-36__fix_oom_during_customer_import__2.1.11.patch b/patches/SET-36__fix_oom_during_customer_import__2.1.11.patch new file mode 100644 index 00000000..2d1d7e3f --- /dev/null +++ b/patches/SET-36__fix_oom_during_customer_import__2.1.11.patch @@ -0,0 +1,104 @@ +diff -Naur a/vendor/magento/module-customer-import-export/Model/Import/Address.php b/vendor/magento/module-customer-import-export/Model/Import/Address.php +index 3f745267fd2..13e659e4e62 100644 +--- a/vendor/magento/module-customer-import-export/Model/Import/Address.php ++++ b/vendor/magento/module-customer-import-export/Model/Import/Address.php +@@ -257,6 +257,11 @@ class Address extends AbstractCustomer + private $optionsByWebsite = []; + + /** ++ * @var array ++ */ ++ private $loadedAddresses; ++ ++ /** + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\ImportExport\Model\ImportFactory $importFactory +@@ -458,21 +463,50 @@ class Address extends AbstractCustomer + */ + protected function _initAddresses() + { +- /** @var $address \Magento\Customer\Model\Address */ +- foreach ($this->_addressCollection as $address) { +- $customerId = $address->getParentId(); +- if (!isset($this->_addresses[$customerId])) { +- $this->_addresses[$customerId] = []; ++ if ($this->_addressCollection->isLoaded()) { ++ /** @var $address \Magento\Customer\Model\Address */ ++ foreach ($this->_addressCollection as $address) { ++ $customerId = $address->getParentId(); ++ if (!isset($this->_addresses[$customerId])) { ++ $this->_addresses[$customerId] = []; ++ } ++ $addressId = $address->getId(); ++ if (!in_array($addressId, $this->_addresses[$customerId])) { ++ $this->_addresses[$customerId][] = $addressId; ++ } + } +- $addressId = $address->getId(); +- if (!in_array($addressId, $this->_addresses[$customerId])) { +- $this->_addresses[$customerId][] = $addressId; ++ } else { ++ foreach ($this->getLoadedAddresses() as $addressId => $address) { ++ $customerId = $address['parent_id']; ++ if (!isset($this->_addresses[$customerId])) { ++ $this->_addresses[$customerId] = []; ++ } ++ if (!in_array($addressId, $this->_addresses[$customerId])) { ++ $this->_addresses[$customerId][] = $addressId; ++ } + } + } + return $this; + } + + /** ++ * @return array ++ */ ++ private function getLoadedAddresses() ++ { ++ if (empty($this->loadedAddresses)) { ++ $collection = clone $this->_addressCollection; ++ $table = $collection->getMainTable(); ++ $select = $collection->getSelect(); ++ $select->reset('columns'); ++ $select->reset('from'); ++ $select->from($table, ['entity_id', 'parent_id']); ++ $this->loadedAddresses = $collection->getResource()->getConnection()->fetchAssoc($select); ++ } ++ return $this->loadedAddresses; ++ } ++ ++ /** + * Initialize country regions hash for clever recognition + * + * @return $this +diff -Naur a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +index 4e6687bff28..359822df6d9 100644 +--- a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php ++++ b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +@@ -117,13 +117,18 @@ class Storage + */ + public function getCustomerId($email, $websiteId) + { +- // lazy loading +- $this->load(); ++ if (!isset($this->_customerIds[$email][$websiteId])) { ++ $collection = clone $this->_customerCollection; ++ $mainTable = $collection->getResource()->getEntityTable(); + +- if (isset($this->_customerIds[$email][$websiteId])) { +- return $this->_customerIds[$email][$websiteId]; +- } ++ $select = $collection->getSelect(); ++ $select->reset(); ++ $select->from($mainTable, ['entity_id']); ++ $select->where($mainTable . '.email = ?', $email); ++ $select->where($mainTable . '.website_id = ?', $websiteId); + +- return false; ++ $this->_customerIds[$email][$websiteId] = $collection->getResource()->getConnection()->fetchOne($select); ++ } ++ return $this->_customerIds[$email][$websiteId]; + } + } diff --git a/patches/SET-36__fix_oom_during_customer_import__2.1.4.patch b/patches/SET-36__fix_oom_during_customer_import__2.1.4.patch new file mode 100644 index 00000000..1e79c673 --- /dev/null +++ b/patches/SET-36__fix_oom_during_customer_import__2.1.4.patch @@ -0,0 +1,110 @@ +commit 4ee8443a262e18c08b942aef313710b2c070a7a4 +Author: Viktor Paladiichuk +Date: Thu Nov 16 18:55:15 2017 +0200 + + SET-36: Memory limit exhausted during import of customers and addresses + +diff -Naur a/vendor/magento/module-customer-import-export/Model/Import/Address.php b/vendor/magento/module-customer-import-export/Model/Import/Address.php +index eb5742d24c7..70b8c34ef41 100644 +--- a/vendor/magento/module-customer-import-export/Model/Import/Address.php ++++ b/vendor/magento/module-customer-import-export/Model/Import/Address.php +@@ -238,6 +238,11 @@ class Address extends AbstractCustomer + protected $postcodeValidator; + + /** ++ * @var array ++ */ ++ private $loadedAddresses; ++ ++ /** + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\ImportExport\Model\ImportFactory $importFactory +@@ -368,21 +373,50 @@ class Address extends AbstractCustomer + */ + protected function _initAddresses() + { +- /** @var $address \Magento\Customer\Model\Address */ +- foreach ($this->_addressCollection as $address) { +- $customerId = $address->getParentId(); +- if (!isset($this->_addresses[$customerId])) { +- $this->_addresses[$customerId] = []; ++ if ($this->_addressCollection->isLoaded()) { ++ /** @var $address \Magento\Customer\Model\Address */ ++ foreach ($this->_addressCollection as $address) { ++ $customerId = $address->getParentId(); ++ if (!isset($this->_addresses[$customerId])) { ++ $this->_addresses[$customerId] = []; ++ } ++ $addressId = $address->getId(); ++ if (!in_array($addressId, $this->_addresses[$customerId])) { ++ $this->_addresses[$customerId][] = $addressId; ++ } + } +- $addressId = $address->getId(); +- if (!in_array($addressId, $this->_addresses[$customerId])) { +- $this->_addresses[$customerId][] = $addressId; ++ } else { ++ foreach ($this->getLoadedAddresses() as $addressId => $address) { ++ $customerId = $address['parent_id']; ++ if (!isset($this->_addresses[$customerId])) { ++ $this->_addresses[$customerId] = []; ++ } ++ if (!in_array($addressId, $this->_addresses[$customerId])) { ++ $this->_addresses[$customerId][] = $addressId; ++ } + } + } + return $this; + } + + /** ++ * @return array ++ */ ++ private function getLoadedAddresses() ++ { ++ if (empty($this->loadedAddresses)) { ++ $collection = clone $this->_addressCollection; ++ $table = $collection->getMainTable(); ++ $select = $collection->getSelect(); ++ $select->reset('columns'); ++ $select->reset('from'); ++ $select->from($table, ['entity_id', 'parent_id']); ++ $this->loadedAddresses = $collection->getResource()->getConnection()->fetchAssoc($select); ++ } ++ return $this->loadedAddresses; ++ } ++ ++ /** + * Initialize country regions hash for clever recognition + * + * @return $this +diff -Naur a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +index 4e6687bff28..359822df6d9 100644 +--- a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php ++++ b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +@@ -117,13 +117,18 @@ class Storage + */ + public function getCustomerId($email, $websiteId) + { +- // lazy loading +- $this->load(); ++ if (!isset($this->_customerIds[$email][$websiteId])) { ++ $collection = clone $this->_customerCollection; ++ $mainTable = $collection->getResource()->getEntityTable(); + +- if (isset($this->_customerIds[$email][$websiteId])) { +- return $this->_customerIds[$email][$websiteId]; +- } ++ $select = $collection->getSelect(); ++ $select->reset(); ++ $select->from($mainTable, ['entity_id']); ++ $select->where($mainTable . '.email = ?', $email); ++ $select->where($mainTable . '.website_id = ?', $websiteId); + +- return false; ++ $this->_customerIds[$email][$websiteId] = $collection->getResource()->getConnection()->fetchOne($select); ++ } ++ return $this->_customerIds[$email][$websiteId]; + } + } diff --git a/patches/SET-36__fix_oom_during_customer_import__2.2.0.patch b/patches/SET-36__fix_oom_during_customer_import__2.2.0.patch new file mode 100644 index 00000000..7f546b8c --- /dev/null +++ b/patches/SET-36__fix_oom_during_customer_import__2.2.0.patch @@ -0,0 +1,110 @@ +commit 4ee8443a262e18c08b942aef313710b2c070a7a4 +Author: Viktor Paladiichuk +Date: Thu Nov 16 18:55:15 2017 +0200 + + SET-36: Memory limit exhausted during import of customers and addresses + +diff -Nuar a/vendor/magento/module-customer-import-export/Model/Import/Address.php b/vendor/magento/module-customer-import-export/Model/Import/Address.php +index eb5742d24c7..70b8c34ef41 100644 +--- a/vendor/magento/module-customer-import-export/Model/Import/Address.php ++++ b/vendor/magento/module-customer-import-export/Model/Import/Address.php +@@ -238,6 +238,11 @@ class Address extends AbstractCustomer + protected $postcodeValidator; + + /** ++ * @var array ++ */ ++ private $loadedAddresses; ++ ++ /** + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\ImportExport\Model\ImportFactory $importFactory +@@ -368,21 +373,50 @@ class Address extends AbstractCustomer + */ + protected function _initAddresses() + { +- /** @var $address \Magento\Customer\Model\Address */ +- foreach ($this->_addressCollection as $address) { +- $customerId = $address->getParentId(); +- if (!isset($this->_addresses[$customerId])) { +- $this->_addresses[$customerId] = []; ++ if ($this->_addressCollection->isLoaded()) { ++ /** @var $address \Magento\Customer\Model\Address */ ++ foreach ($this->_addressCollection as $address) { ++ $customerId = $address->getParentId(); ++ if (!isset($this->_addresses[$customerId])) { ++ $this->_addresses[$customerId] = []; ++ } ++ $addressId = $address->getId(); ++ if (!in_array($addressId, $this->_addresses[$customerId])) { ++ $this->_addresses[$customerId][] = $addressId; ++ } + } +- $addressId = $address->getId(); +- if (!in_array($addressId, $this->_addresses[$customerId])) { +- $this->_addresses[$customerId][] = $addressId; ++ } else { ++ foreach ($this->getLoadedAddresses() as $addressId => $address) { ++ $customerId = $address['parent_id']; ++ if (!isset($this->_addresses[$customerId])) { ++ $this->_addresses[$customerId] = []; ++ } ++ if (!in_array($addressId, $this->_addresses[$customerId])) { ++ $this->_addresses[$customerId][] = $addressId; ++ } + } + } + return $this; + } + + /** ++ * @return array ++ */ ++ private function getLoadedAddresses() ++ { ++ if (empty($this->loadedAddresses)) { ++ $collection = clone $this->_addressCollection; ++ $table = $collection->getMainTable(); ++ $select = $collection->getSelect(); ++ $select->reset('columns'); ++ $select->reset('from'); ++ $select->from($table, ['entity_id', 'parent_id']); ++ $this->loadedAddresses = $collection->getResource()->getConnection()->fetchAssoc($select); ++ } ++ return $this->loadedAddresses; ++ } ++ ++ /** + * Initialize country regions hash for clever recognition + * + * @return $this +diff -Nuar a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +index 4e6687bff28..359822df6d9 100644 +--- a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php ++++ b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +@@ -117,13 +117,18 @@ class Storage + */ + public function getCustomerId($email, $websiteId) + { +- // lazy loading +- $this->load(); ++ if (!isset($this->_customerIds[$email][$websiteId])) { ++ $collection = clone $this->_customerCollection; ++ $mainTable = $collection->getResource()->getEntityTable(); + +- if (isset($this->_customerIds[$email][$websiteId])) { +- return $this->_customerIds[$email][$websiteId]; +- } ++ $select = $collection->getSelect(); ++ $select->reset(); ++ $select->from($mainTable, ['entity_id']); ++ $select->where($mainTable . '.email = ?', $email); ++ $select->where($mainTable . '.website_id = ?', $websiteId); + +- return false; ++ $this->_customerIds[$email][$websiteId] = $collection->getResource()->getConnection()->fetchOne($select); ++ } ++ return $this->_customerIds[$email][$websiteId]; + } + } diff --git a/patches/SET-36__fix_oom_during_customer_import__2.2.4.patch b/patches/SET-36__fix_oom_during_customer_import__2.2.4.patch new file mode 100644 index 00000000..6db2e0e7 --- /dev/null +++ b/patches/SET-36__fix_oom_during_customer_import__2.2.4.patch @@ -0,0 +1,102 @@ +diff -Nuar a/vendor/magento/module-customer-import-export/Model/Import/Address.php b/vendor/magento/module-import-export/Model/Import/Address.php +index 1e1221e..41e0512 100644 +--- a/vendor/magento/module-customer-import-export/Model/Import/Address.php ++++ b/vendor/magento/module-customer-import-export/Model/Import/Address.php +@@ -253,6 +253,11 @@ class Address extends AbstractCustomer + private $optionsByWebsite = []; + + /** ++ * @var array ++ */ ++ private $loadedAddresses; ++ ++ /** + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\ImportExport\Model\ImportFactory $importFactory +@@ -443,20 +448,49 @@ class Address extends AbstractCustomer + */ + protected function _initAddresses() + { +- /** @var $address \Magento\Customer\Model\Address */ +- foreach ($this->_addressCollection as $address) { +- $customerId = $address->getParentId(); +- if (!isset($this->_addresses[$customerId])) { +- $this->_addresses[$customerId] = []; ++ if ($this->_addressCollection->isLoaded()) { ++ /** @var $address \Magento\Customer\Model\Address */ ++ foreach ($this->_addressCollection as $address) { ++ $customerId = $address->getParentId(); ++ if (!isset($this->_addresses[$customerId])) { ++ $this->_addresses[$customerId] = []; ++ } ++ $addressId = $address->getId(); ++ if (!in_array($addressId, $this->_addresses[$customerId])) { ++ $this->_addresses[$customerId][] = $addressId; ++ } + } +- $addressId = $address->getId(); +- if (!in_array($addressId, $this->_addresses[$customerId])) { +- $this->_addresses[$customerId][] = $addressId; ++ } else { ++ foreach ($this->getLoadedAddresses() as $addressId => $address) { ++ $customerId = $address['parent_id']; ++ if (!isset($this->_addresses[$customerId])) { ++ $this->_addresses[$customerId] = []; ++ } ++ if (!in_array($addressId, $this->_addresses[$customerId])) { ++ $this->_addresses[$customerId][] = $addressId; ++ } + } + } + return $this; + } + ++ /** ++ * @return array ++ */ ++ private function getLoadedAddresses() ++ { ++ if (empty($this->loadedAddresses)) { ++ $collection = clone $this->_addressCollection; ++ $table = $collection->getMainTable(); ++ $select = $collection->getSelect(); ++ $select->reset('columns'); ++ $select->reset('from'); ++ $select->from($table, ['entity_id', 'parent_id']); ++ $this->loadedAddresses = $collection->getResource()->getConnection()->fetchAssoc($select); ++ } ++ return $this->loadedAddresses; ++ } ++ + /** + * Initialize country regions hash for clever recognition + * +diff -Nuar a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +index ae88e96..a6f7aa3 100644 +--- a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php ++++ b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +@@ -117,13 +117,18 @@ class Storage + */ + public function getCustomerId($email, $websiteId) + { +- // lazy loading +- $this->load(); ++ if (!isset($this->_customerIds[$email][$websiteId])) { ++ $collection = clone $this->_customerCollection; ++ $mainTable = $collection->getResource()->getEntityTable(); + +- if (isset($this->_customerIds[$email][$websiteId])) { +- return $this->_customerIds[$email][$websiteId]; ++ $select = $collection->getSelect(); ++ $select->reset(); ++ $select->from($mainTable, ['entity_id']); ++ $select->where($mainTable . '.email = ?', $email); ++ $select->where($mainTable . '.website_id = ?', $websiteId); ++ $this->_customerIds[$email][$websiteId] = $collection->getResource()->getConnection()->fetchOne($select); + } + +- return false; ++ return $this->_customerIds[$email][$websiteId]; + } + } diff --git a/src/App/Container.php b/src/App/Container.php new file mode 100644 index 00000000..3620b926 --- /dev/null +++ b/src/App/Container.php @@ -0,0 +1,96 @@ +set('container', $containerBuilder); + $containerBuilder->setDefinition('container', new Definition(__CLASS__)) + ->setArguments([$basePath, $magentoBasePath]); + + $directoryList = new DirectoryList($basePath, $magentoBasePath); + $composer = $this->createComposerInstance($directoryList); + + $containerBuilder->set(DirectoryList::class, $directoryList); + $containerBuilder->setDefinition(DirectoryList::class, new Definition(DirectoryList::class)); + + $containerBuilder->set(Composer\Composer::class, $composer); + $containerBuilder->setDefinition(Composer\Composer::class, new Definition(Composer\Composer::class)); + + $loader = new XmlFileLoader($containerBuilder, new FileLocator($basePath . '/config')); + $loader->load('services.xml'); + $containerBuilder->compile(); + + $this->container = $containerBuilder; + } + + /** + * @param DirectoryList $directoryList + * @return Composer\Composer + */ + private function createComposerInstance(DirectoryList $directoryList): Composer\Composer + { + $composerFactory = new Composer\Factory(); + $composerFile = file_exists($directoryList->getMagentoRoot() . '/composer.json') + ? $directoryList->getMagentoRoot() . '/composer.json' + : $directoryList->getRoot() . '/composer.json'; + + $composer = $composerFactory->createComposer( + new Composer\IO\BufferIO(), + $composerFile, + false, + $directoryList->getMagentoRoot() + ); + + return $composer; + } + + /** + * @inheritDoc + */ + public function get($id) + { + return $this->container->get($id); + } + + /** + * @inheritdoc + */ + public function has($id): bool + { + return $this->container->has($id); + } +} diff --git a/src/App/GenericException.php b/src/App/GenericException.php new file mode 100644 index 00000000..06955e80 --- /dev/null +++ b/src/App/GenericException.php @@ -0,0 +1,26 @@ +container = $container; + + parent::__construct( + $container->get(Composer::class)->getPackage()->getPrettyName(), + $container->get(Composer::class)->getPackage()->getPrettyVersion() + ); + } + + /** + * @inheritdoc + */ + protected function getDefaultCommands() + { + return array_merge(parent::getDefaultCommands(), [ + $this->container->get(Command\Apply::class), + ]); + } +} diff --git a/src/Command/Apply.php b/src/Command/Apply.php new file mode 100644 index 00000000..e8908bfb --- /dev/null +++ b/src/Command/Apply.php @@ -0,0 +1,71 @@ +manager = $manager; + + parent::__construct(self::NAME); + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName(self::NAME) + ->setDescription('Apply patches') + ->addOption( + self::OPT_GIT_INSTALLATION, + null, + InputOption::VALUE_OPTIONAL, + 'Is git installation', + false + ); + + parent::configure(); + } + + /** + * {@inheritDoc} + * + * @throws ManagerException + * @throws ApplierException + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $this->manager->applyComposerPatches($input, $output); + $this->manager->applyHotFixes($input, $output); + } +} diff --git a/src/Command/Patch/Manager.php b/src/Command/Patch/Manager.php new file mode 100644 index 00000000..66617569 --- /dev/null +++ b/src/Command/Patch/Manager.php @@ -0,0 +1,163 @@ +applier = $applier; + $this->filesystem = $filesystem; + $this->fileList = $fileList; + $this->directoryList = $directoryList; + } + + /** + * Applies patches from composer.json file. + * Patches are applying from top to bottom of config list. + * + * ``` + * "colinmollenhour/credis" : { + * "Fix Redis issue": { + * "1.6": "patches/redis-pipeline.patch" + * } + * } + * + * Each patch must have corresponding constraint of target package, + * in one of the following format: + * - 1.6 + * - 1.6.* + * - ^1.6 + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @throws ManagerException + * @throws ApplierException + */ + public function applyComposerPatches(InputInterface $input, OutputInterface $output) + { + try { + $content = $this->filesystem->get($this->fileList->getPatches()); + } catch (FileNotFoundException $exception) { + throw new ManagerException($exception->getMessage(), $exception->getCode(), $exception); + } + + $patches = json_decode($content, true); + + if (!$patches) { + $output->writeln('Composer patches not found'); + + return; + } + + $deployedFromGit = $input->getOption(Apply::OPT_GIT_INSTALLATION); + + foreach ($patches as $packageName => $patchesInfo) { + foreach ($patchesInfo as $patchName => $packageInfo) { + if (!is_array($packageInfo)) { + throw new ManagerException('Wrong patch constraints'); + } + + foreach ($packageInfo as $constraint => $path) { + $message = $this->applier->apply( + (string)$path, + (string)$patchName, + (string)$packageName, + (string)$constraint, + $deployedFromGit + ); + + if (null !== $message) { + $output->writeln($message); + } + } + } + } + } + + /** + * Applies patches from root directory m2-hotfixes. + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @throws ApplierException + */ + public function applyHotFixes(InputInterface $input, OutputInterface $output) + { + $hotFixesDir = $this->directoryList->getMagentoRoot() . '/' . static::HOT_FIXES_DIR; + + if (!$this->filesystem->isDirectory($hotFixesDir)) { + $output->writeln('Hot-fixes directory was not found. Skipping'); + + return; + } + + $files = glob($hotFixesDir . '/*.patch'); + sort($files); + + $deployedFromGit = $input->getOption(Apply::OPT_GIT_INSTALLATION); + + $output->writeln('Applying hot-fixes'); + + foreach ($files as $file) { + $output->writeln( + $this->applier->applyFile($file, $deployedFromGit) + ); + } + } +} diff --git a/src/Command/Patch/ManagerException.php b/src/Command/Patch/ManagerException.php new file mode 100644 index 00000000..81aaae68 --- /dev/null +++ b/src/Command/Patch/ManagerException.php @@ -0,0 +1,17 @@ +root = realpath($root); + $this->magentoRoot = realpath($magentoRoot); + } + + /** + * @return string + */ + public function getRoot(): string + { + return $this->root; + } + + /** + * @return string + */ + public function getMagentoRoot(): string + { + return $this->magentoRoot; + } + + /** + * @return string + */ + public function getPatches(): string + { + return $this->getRoot() . '/patches'; + } +} diff --git a/src/Filesystem/FileList.php b/src/Filesystem/FileList.php new file mode 100644 index 00000000..10fd856c --- /dev/null +++ b/src/Filesystem/FileList.php @@ -0,0 +1,35 @@ +directoryList = $directoryList; + } + + /** + * @return string + */ + public function getPatches(): string + { + return $this->directoryList->getRoot() . '/patches.json'; + } +} diff --git a/src/Filesystem/FileNotFoundException.php b/src/Filesystem/FileNotFoundException.php new file mode 100644 index 00000000..af9e0621 --- /dev/null +++ b/src/Filesystem/FileNotFoundException.php @@ -0,0 +1,17 @@ +isFile($path)) { + return file_get_contents($path); + } + + throw new FileNotFoundException("File does not exist at path {$path}"); + } + + /** + * Determine if the given path is a file. + * + * @param string $file + * @return bool + */ + public function isFile(string $file): bool + { + return is_file($file); + } +} diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php new file mode 100644 index 00000000..d7e1d2c4 --- /dev/null +++ b/src/Patch/Applier.php @@ -0,0 +1,170 @@ +repository = $composer->getRepositoryManager()->getLocalRepository(); + $this->processFactory = $processFactory; + $this->directoryList = $directoryList; + $this->filesystem = $filesystem; + } + + /** + * @param string $path + * @param bool $deployedFromGit + * @return string + * + * @throws ApplierException + */ + public function applyFile(string $path, bool $deployedFromGit): string + { + return $this->processApply($path, $path, $deployedFromGit); + } + + /** + * Applies patch, using 'git apply' command. + * + * If the patch fails to apply, checks if it has already been applied which is considered ok. + * + * @param string $path Path to patch + * @param string $name Name of patch + * @param string $packageName Name of package to be patched + * @param string $constraint Specific constraint of package to be fixed + * @param bool $deployedFromGit + * @return string|null + * + * @throws ApplierException + */ + public function apply( + string $path, + string $name, + string $packageName, + string $constraint, + bool $deployedFromGit + ) { + $fullName = sprintf( + '%s %s', + sprintf('%s (%s)', $name, $path), + $constraint + ); + + if ($packageName && !$this->matchConstraint($packageName, $constraint)) { + return null; + } + + /** + * Support for relative paths. + */ + if (!$this->filesystem->exists($path)) { + $path = $this->directoryList->getPatches() . '/' . $path; + } + + return $this->processApply($path, $fullName, $deployedFromGit); + } + + /** + * General apply processing. + * + * @param string $path + * @param string $fullName + * @param bool $deployedFromGit + * @return string + * + * @throws ApplierException + */ + private function processApply(string $path, string $fullName, bool $deployedFromGit): string + { + try { + $this->processFactory->create(['git', 'apply', $path]) + ->mustRun(); + } catch (ProcessFailedException $exception) { + if ($deployedFromGit) { + return sprintf( + 'Patch "%s" was not applied. (%s)', + $fullName, + $exception->getMessage() + ); + } + + try { + $this->processFactory->create(['git', 'apply', '--check', '--reverse', $path]) + ->mustRun(); + } catch (ProcessFailedException $reverseException) { + throw new ApplierException( + $reverseException->getMessage(), + $reverseException->getCode(), + $reverseException + ); + } + + return sprintf( + 'Patch "%s" was already applied', + $fullName + ); + } + + return sprintf( + 'Patch "%s" applied', + $fullName + ); + } + + /** + * Checks whether package with specific constraint exists in the system. + * + * @param string $packageName + * @param string $constraint + * @return bool True if patch with provided constraint exists, false otherwise. + */ + private function matchConstraint(string $packageName, string $constraint): bool + { + return $this->repository->findPackage($packageName, $constraint) instanceof Composer\Package\PackageInterface; + } +} diff --git a/src/Patch/ApplierException.php b/src/Patch/ApplierException.php new file mode 100644 index 00000000..2058a0d6 --- /dev/null +++ b/src/Patch/ApplierException.php @@ -0,0 +1,17 @@ +directoryList = $directoryList; + } + + /** + * @param array $cmd + * @return Process + */ + public function create(array $cmd): Process + { + return new Process( + implode(' ', $cmd), + $this->directoryList->getMagentoRoot() + ); + } +} diff --git a/src/Test/Unit/Command/ApplyTest.php b/src/Test/Unit/Command/ApplyTest.php new file mode 100644 index 00000000..c751f658 --- /dev/null +++ b/src/Test/Unit/Command/ApplyTest.php @@ -0,0 +1,64 @@ +managerMock = $this->createMock(Manager::class); + + $this->command = new Apply( + $this->managerMock + ); + } + + /** + * @throws ManagerException + * @throws ApplierException + */ + public function testExecute() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->managerMock->expects($this->once()) + ->method('applyComposerPatches'); + $this->managerMock->expects($this->once()) + ->method('applyHotFixes'); + + $this->command->execute($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Command/Patch/ManagerTest.php b/src/Test/Unit/Command/Patch/ManagerTest.php new file mode 100644 index 00000000..bb2d775f --- /dev/null +++ b/src/Test/Unit/Command/Patch/ManagerTest.php @@ -0,0 +1,209 @@ +applierMock = $this->createMock(Applier::class); + $this->composerPackageMock = $this->getMockForAbstractClass(RootPackageInterface::class); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->directoryListMock = $this->createMock(DirectoryList::class); + $this->fileListMock = $this->createMock(FileList::class); + + $this->manager = new Manager( + $this->applierMock, + $this->filesystemMock, + $this->fileListMock, + $this->directoryListMock + ); + } + + /** + * @throws ApplierException + * @throws ManagerException + */ + public function testApplyComposerPatches() + { + $this->filesystemMock->expects($this->once()) + ->method('get') + ->willReturn(json_encode( + [ + 'package1' => [ + 'patchName1' => [ + '100' => 'patchPath1', + ], + ], + 'package2' => [ + 'patchName2' => [ + '101.*' => 'patchPath2', + ], + 'patchName3' => [ + '102.*' => 'patchPath3', + ], + ] + ] + )); + $this->applierMock->expects($this->exactly(3)) + ->method('apply') + ->withConsecutive( + ['patchPath1', 'patchName1', 'package1', '100'], + ['patchPath2', 'patchName2', 'package2', '101.*'], + ['patchPath3', 'patchName3', 'package2', '102.*'] + ); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $inputMock->method('getOption') + ->with(Apply::OPT_GIT_INSTALLATION) + ->willReturn(false); + + $this->manager->applyComposerPatches($inputMock, $outputMock); + } + + /** + * @expectedException \Magento\CloudPatches\Command\Patch\ManagerException + * @expectedExceptionMessage Not Found + * + * @throws ApplierException + * @throws ManagerException + */ + public function testApplyComposerPatchesWithFSException() + { + $this->filesystemMock->expects($this->once()) + ->method('get') + ->willThrowException(new FileNotFoundException('Not Found')); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $inputMock->method('getOption') + ->with(Apply::OPT_GIT_INSTALLATION) + ->willReturn(false); + + $this->manager->applyComposerPatches($inputMock, $outputMock); + } + + /** + * @throws ApplierException + */ + public function testExecuteApplyHotFixes() + { + $this->directoryListMock->expects($this->any()) + ->method('getMagentoRoot') + ->willReturn(__DIR__ . '/_files'); + $this->filesystemMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(true); + $this->applierMock->expects($this->exactly(2)) + ->method('applyFile') + ->willReturnMap([ + [__DIR__ . '/_files/' . Manager::HOT_FIXES_DIR . '/patch1.patch', false, 'Patch 1 applied'], + [__DIR__ . '/_files/' . Manager::HOT_FIXES_DIR . '/patch2.patch', false, 'Patch 2 applied'] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock->expects($this->once()) + ->method('getOption') + ->with(Apply::OPT_GIT_INSTALLATION) + ->willReturn(false); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock->expects($this->exactly(3)) + ->method('writeln') + ->withConsecutive( + ['Applying hot-fixes', 0], + ['Patch 1 applied', 0], + ['Patch 2 applied', 0] + ); + + $this->manager->applyHotFixes($inputMock, $outputMock); + } + + /** + * @throws ApplierException + */ + public function testExecuteApplyHotFixesNotFound() + { + $this->directoryListMock->expects($this->any()) + ->method('getMagentoRoot') + ->willReturn(__DIR__ . '/_files'); + $this->filesystemMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(false); + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock->expects($this->once()) + ->method('writeln') + ->withConsecutive( + ['Hot-fixes directory was not found. Skipping', 0] + ); + + $this->manager->applyHotFixes($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch1.patch b/src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch1.patch new file mode 100644 index 00000000..e69de29b diff --git a/src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch2.patch b/src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch2.patch new file mode 100644 index 00000000..e69de29b diff --git a/src/Test/Unit/Command/Patch/_files/m2-hotfixes/readme.md b/src/Test/Unit/Command/Patch/_files/m2-hotfixes/readme.md new file mode 100644 index 00000000..e69de29b diff --git a/src/Test/Unit/Filesystem/DirectoryListTest.php b/src/Test/Unit/Filesystem/DirectoryListTest.php new file mode 100644 index 00000000..c2ee383e --- /dev/null +++ b/src/Test/Unit/Filesystem/DirectoryListTest.php @@ -0,0 +1,67 @@ +directoryList = new DirectoryList( + $this->root, + $this->magentoRoot + ); + } + + public function testGetRoot() + { + $this->assertSame( + $this->root, + $this->directoryList->getRoot() + ); + } + + public function testGetMagentoRoot() + { + $this->assertSame( + $this->magentoRoot, + $this->directoryList->getMagentoRoot() + ); + } + + public function testGetPatches() + { + $this->assertSame( + $this->root . '/patches', + $this->directoryList->getPatches() + ); + } +} diff --git a/src/Test/Unit/Filesystem/FileListTest.php b/src/Test/Unit/Filesystem/FileListTest.php new file mode 100644 index 00000000..5d3f8b93 --- /dev/null +++ b/src/Test/Unit/Filesystem/FileListTest.php @@ -0,0 +1,51 @@ +directoryListMock = $this->createMock(DirectoryList::class); + + $this->directoryListMock->method('getRoot') + ->willReturn('root'); + + $this->fileList = new FileList( + $this->directoryListMock + ); + } + + public function testGetPatches() + { + $this->assertSame( + 'root/patches.json', + $this->fileList->getPatches() + ); + } +} diff --git a/src/Test/Unit/Filesystem/_files/.gitignore b/src/Test/Unit/Filesystem/_files/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php new file mode 100644 index 00000000..cd2171b6 --- /dev/null +++ b/src/Test/Unit/Patch/ApplierTest.php @@ -0,0 +1,283 @@ +composerMock = $this->createMock(Composer::class); + $this->localRepositoryMock = $this->getMockForAbstractClass(RepositoryInterface::class); + $this->directoryListMock = $this->createMock(DirectoryList::class); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->processFactoryMock = $this->createMock(ProcessFactory::class); + + $repositoryManagerMock = $this->createMock(RepositoryManager::class); + + $repositoryManagerMock->expects($this->once()) + ->method('getLocalRepository') + ->willReturn($this->localRepositoryMock); + $this->composerMock->expects($this->once()) + ->method('getRepositoryManager') + ->willReturn($repositoryManagerMock); + + $this->applier = new Applier( + $this->composerMock, + $this->processFactoryMock, + $this->directoryListMock, + $this->filesystemMock + ); + } + + /** + * @param string $path + * @param string $name + * @param string $packageName + * @param string $constraint + * @param string $expectedLog + * @dataProvider applyDataProvider + * + * @throws ApplierException + */ + public function testApply(string $path, string $name, string $packageName, string $constraint, string $expectedLog) + { + $this->filesystemMock->expects($this->once()) + ->method('exists') + ->with($path) + ->willReturn(true); + $this->localRepositoryMock->method('findPackage') + ->with($packageName, $constraint) + ->willReturn($this->getMockForAbstractClass(PackageInterface::class)); + + $processMock = $this->createMock(Process::class); + + $this->processFactoryMock->expects($this->once()) + ->method('create') + ->with(['git', 'apply', $path]) + ->willReturn($processMock); + $processMock->expects($this->once()) + ->method('mustRun'); + + $this->assertSame( + $expectedLog, + $this->applier->apply($path, $name, $packageName, $constraint, false) + ); + } + + /** + * @return array + */ + public function applyDataProvider(): array + { + return [ + ['path/to/patch', 'patchName', 'packageName', '1.0', 'Patch "patchName (path/to/patch) 1.0" applied'] + ]; + } + + /** + * @param string $path + * @param string $expectedLog + * @dataProvider applyFileDataProvider + * + * @throws ApplierException + */ + public function testApplyFile(string $path, string $expectedLog) + { + $processMock = $this->createMock(Process::class); + + $this->processFactoryMock->expects($this->once()) + ->method('create') + ->with(['git', 'apply', $path]) + ->willReturn($processMock); + $processMock->expects($this->once()) + ->method('mustRun'); + + $this->assertSame( + $expectedLog, + $this->applier->applyFile($path, false) + ); + } + + /** + * @return array + */ + public function applyFileDataProvider(): array + { + return [ + ['path/to/patch2', 'Patch "path/to/patch2" applied'], + ]; + } + + /** + * @throws ApplierException + */ + public function testApplyPathNotExists() + { + $path = 'path/to/patch'; + $name = 'patchName'; + $packageName = 'packageName'; + $constraint = '1.0'; + + $this->filesystemMock->expects($this->once()) + ->method('exists') + ->with($path) + ->willReturn(false); + $this->localRepositoryMock->expects($this->once()) + ->method('findPackage') + ->with($packageName, $constraint) + ->willReturn($this->getMockForAbstractClass(PackageInterface::class)); + $this->directoryListMock->expects($this->once()) + ->method('getPatches') + ->willReturn('root'); + + $processMock = $this->createMock(Process::class); + + $this->processFactoryMock->expects($this->once()) + ->method('create') + ->with(['git', 'apply', 'root/path/to/patch']) + ->willReturn($processMock); + $processMock->expects($this->once()) + ->method('mustRun'); + + $this->applier->apply($path, $name, $packageName, $constraint, false); + } + + /** + * @throws ApplierException + */ + public function testApplyPathNotExistsAndNotMatchedConstraints() + { + $path = 'path/to/patch'; + $name = 'patchName'; + $packageName = 'packageName'; + $constraint = '1.0'; + + $this->localRepositoryMock->expects($this->once()) + ->method('findPackage') + ->with($packageName, $constraint) + ->willReturn(null); + + $processMock = $this->createMock(Process::class); + + $this->processFactoryMock->expects($this->never()) + ->method('create') + ->with(['git', 'apply', 'root/path/to/patch']) + ->willReturn($processMock); + + $this->applier->apply($path, $name, $packageName, $constraint, false); + } + + /** + * @throws ApplierException + */ + public function testApplyPatchAlreadyApplied() + { + $path = 'path/to/patch'; + $name = 'patchName'; + $packageName = 'packageName'; + $constraint = '1.0'; + + $this->filesystemMock->expects($this->once()) + ->method('exists') + ->with($path) + ->willReturn(true); + $this->localRepositoryMock->expects($this->once()) + ->method('findPackage') + ->with($packageName, $constraint) + ->willReturn($this->getMockForAbstractClass(PackageInterface::class)); + + $this->processFactoryMock->expects($this->exactly(2)) + ->method('create') + ->willReturnMap([ + [['git', 'apply', 'path/to/patch']], + [['git', 'apply', 'path/to/patch', '--revert']] + ])->willReturnCallback([$this, 'shellMockReverseCallback']); + + $this->assertSame( + 'Patch "patchName (path/to/patch) 1.0" was already applied', + $this->applier->apply($path, $name, $packageName, $constraint, false) + ); + } + + /** + * @param array $command + * @return Process + * + * @throws ProcessFailedException when the command isn't a reverse + */ + public function shellMockReverseCallback(array $command): Process + { + if (in_array('--reverse', $command, true) && in_array('--check', $command, true)) { + // Command was the reverse check, it's all good. + /** @var Process|MockObject $result */ + $result = $this->createMock(Process::class); + $result->expects($this->once()) + ->method('mustRun'); + + return $result; + } + + /** @var Process|MockObject $result */ + $result = $this->createMock(Process::class); + $result->expects($this->once()) + ->method('mustRun') + ->willThrowException(new ProcessFailedException($result)); + + return $result; + } +} diff --git a/tests/static/Sniffs/Directives/StrictTypesSniff.php b/tests/static/Sniffs/Directives/StrictTypesSniff.php new file mode 100644 index 00000000..9bd9728e --- /dev/null +++ b/tests/static/Sniffs/Directives/StrictTypesSniff.php @@ -0,0 +1,120 @@ +getTokens(); + + // Tokens to look for. + $findTokens = [ + T_DECLARE, + T_NAMESPACE, + T_CLASS + ]; + + // Find the first occurrence of the tokens to look for. + $position = $phpcsFile->findNext($findTokens, $stackPtr); + + // If the first token found is not T_DECLARE, then the file does not include a strict_types declaration. + if($tokens[$position]['code'] !== T_DECLARE) { + // Fix and set the boolean flag to true. + $this->fix($phpcsFile, $position); + } + + // If the file includes a declare directive, and the file has not already been fixed, scan specifically + // for strict_types and fix as needed. + if(!$this->fixed) { + if(!$this->scan($phpcsFile, $tokens, $position)) { + $this->fix($phpcsFile, $position); + } + } + } + + /** + * Fixer to add the strict_types declaration. + * + * @param File $phpcsFile + * @param int $position + */ + private function fix(File $phpcsFile, int $position) : void + { + // Get the fixer. + $fixer = $phpcsFile->fixer; + // Record the error. + $phpcsFile->addFixableError("Missing strict_types declaration", $position, self::class); + // Prepend content at the given position. + $fixer->addContentBefore($position, "declare(strict_types=1);\n\n"); + // Set flag. + $this->fixed = true; + } + + /** + * Recursive method to scan declare statements for strict_types. + * + * @param File $phpcsFile + * @param array $tokens + * @param int $position + * @return bool + */ + private function scan(File $phpcsFile, array $tokens, int $position) : bool + { + // Exit statement, if the beginning of the file has been reached. + if($tokens[$position]['code'] === T_OPEN_TAG || $position === 0) { + return false; + } + + if(!$phpcsFile->findNext([T_STRING], $position)) { + // If there isn't a T_STRING token for the declare directive, continue scan. + return $this->scan($phpcsFile, $tokens, $phpcsFile->findPrevious([T_DECLARE], $position - 1)); + } else { + // Checking specifically for strict_types. + $temp = $phpcsFile->findNext([T_STRING], $position); + if($tokens[$temp]['content'] === 'strict_types') { + // Return true as strict_types directive has been found. + return true; + } else { + // Continue scan if strict_types hasn't been found. + return $this->scan($phpcsFile, $tokens, $phpcsFile->findPrevious([T_DECLARE], $position - 1)); + } + } + } +} diff --git a/tests/static/Sniffs/Whitespace/MultipleEmptyLinesSniff.php b/tests/static/Sniffs/Whitespace/MultipleEmptyLinesSniff.php new file mode 100644 index 00000000..e1060b7b --- /dev/null +++ b/tests/static/Sniffs/Whitespace/MultipleEmptyLinesSniff.php @@ -0,0 +1,51 @@ +getTokens(); + if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION) + || $phpcsFile->hasCondition($stackPtr, T_CLASS) + || $phpcsFile->hasCondition($stackPtr, T_INTERFACE) + ) { + if ($tokens[($stackPtr - 1)]['line'] < $tokens[$stackPtr]['line'] + && $tokens[($stackPtr - 2)]['line'] === $tokens[($stackPtr - 1)]['line'] + ) { + // This is an empty line and the line before this one is not + // empty, so this could be the start of a multiple empty line block + $next = $phpcsFile->findNext(T_WHITESPACE, $stackPtr, null, true); + $lines = $tokens[$next]['line'] - $tokens[$stackPtr]['line']; + if ($lines > 1) { + $error = 'Code must not contain multiple empty lines in a row; found %s empty lines'; + $data = [$lines]; + $phpcsFile->addError($error, $stackPtr, 'MultipleEmptyLines', $data); + } + } + } + } +} diff --git a/tests/static/phpcs-ruleset.xml b/tests/static/phpcs-ruleset.xml new file mode 100644 index 00000000..b5ae2aa5 --- /dev/null +++ b/tests/static/phpcs-ruleset.xml @@ -0,0 +1,30 @@ + + + + Custom Magento ECE Tools coding standard. + + + + + + + + + + + + + + + + + + + _files + _file + diff --git a/tests/static/phpmd-ruleset.xml b/tests/static/phpmd-ruleset.xml new file mode 100644 index 00000000..0eb30e25 --- /dev/null +++ b/tests/static/phpmd-ruleset.xml @@ -0,0 +1,48 @@ + + + + Magento Cloud Code Check Rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _files + diff --git a/tests/unit/.gitignore b/tests/unit/.gitignore new file mode 100644 index 00000000..319b3826 --- /dev/null +++ b/tests/unit/.gitignore @@ -0,0 +1 @@ +/phpunit.xml diff --git a/tests/unit/phpunit.xml.dist b/tests/unit/phpunit.xml.dist new file mode 100644 index 00000000..de58085e --- /dev/null +++ b/tests/unit/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + ../../src/Test/Unit + + + + + ../../src + + ../../src/Test + + + + + + + + From 2914e4e785ed00f1c8c0c1c7898b2637bf9c7aa3 Mon Sep 17 00:00:00 2001 From: Oleksandr Shmyheliuk Date: Mon, 28 Oct 2019 11:26:14 -0500 Subject: [PATCH 004/157] MAGECLOUD-4442: Deliver fix for mail interfaces --- patches.json | 3 + ...bility_of_new_mail_interfaces__2.3.3.patch | 381 ++++++++++++++++++ 2 files changed, 384 insertions(+) create mode 100644 patches/MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch diff --git a/patches.json b/patches.json index 0a5ec42a..1b015f95 100644 --- a/patches.json +++ b/patches.json @@ -180,6 +180,9 @@ "Re-work consumers to terminate as soon as there is nothing left to process": { "2.2.0 - 2.3.1": "MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.2.0.patch", "2.3.2 - 2.3.3": "MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.3.2.patch" + }, + "Backward Compatibility of new Mail Interfaces": { + "2.3.3": "MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch" } }, "monolog/monolog": { diff --git a/patches/MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch b/patches/MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch new file mode 100644 index 00000000..ea6787e2 --- /dev/null +++ b/patches/MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch @@ -0,0 +1,381 @@ +diff --git a/vendor/magento/module-email/Model/Transport.php b/vendor/magento/module-email/Model/Transport.php +index cbce1682cb5..79ceb56a883 100644 +--- a/vendor/magento/module-email/Model/Transport.php ++++ b/vendor/magento/module-email/Model/Transport.php +@@ -9,7 +9,6 @@ namespace Magento\Email\Model; + + use Magento\Framework\App\Config\ScopeConfigInterface; + use Magento\Framework\Exception\MailException; +-use Magento\Framework\Mail\EmailMessageInterface; + use Magento\Framework\Mail\MessageInterface; + use Magento\Framework\Mail\TransportInterface; + use Magento\Framework\Phrase; +@@ -62,12 +61,12 @@ class Transport implements TransportInterface + private $message; + + /** +- * @param EmailMessageInterface $message Email message object ++ * @param MessageInterface $message Email message object + * @param ScopeConfigInterface $scopeConfig Core store config + * @param null|string|array|\Traversable $parameters Config options for sendmail parameters + */ + public function __construct( +- EmailMessageInterface $message, ++ MessageInterface $message, + ScopeConfigInterface $scopeConfig, + $parameters = null + ) { +diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +index 32f12dada57..4210312c55a 100644 +--- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php ++++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +@@ -769,7 +769,7 @@ class AccountTest extends \Magento\TestFramework\TestCase\AbstractController + $message = $this->transportBuilderMock->getSentMessage(); + $rawMessage = $message->getRawMessage(); + +- $this->assertContains('To: John Smith <' . $email . '>', $rawMessage); ++ $this->assertContains('To: =?utf-8?Q?John=20Smith?= <' . $email . '>', $rawMessage); + + $content = $message->getBody()->getParts()[0]->getRawContent(); + $confirmationUrl = $this->getConfirmationUrlFromMessageContent($content); +diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php +index 10a54b4e1b8..186c3e2796c 100644 +--- a/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php ++++ b/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php +@@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; + + /** + * Class EmailMessageTest ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + class EmailMessageTest extends TestCase + { +@@ -164,23 +165,42 @@ class EmailMessageTest extends TestCase + 'cc' => $cc, + 'replyTo' => $replyTo, + 'bcc' => $bcc, +- 'sender' => $sender ++ 'sender' => $sender, + ]; + $message = $this->messageFactory->create($data); + + $this->assertContains($content, $message->toString()); + $this->assertContains('Content-Type: ' . $type, $message->toString()); +- $senderString = 'Sender: ' . $sender->getName() . ' <' . $sender->getEmail() . '>'; ++ $senderString = 'Sender: =?utf-8?Q?' ++ . str_replace(' ', '=20', $sender->getName()) ++ . '?= <' ++ . $sender->getEmail() ++ . '>'; + $this->assertContains($senderString, $message->toString()); + $this->assertContains('From: ' . $from[0]->getEmail(), $message->toString()); +- $replyToString = 'Reply-To: ' . $replyTo[0]->getName() . ' <' . $replyTo[0]->getEmail() . '>'; ++ $replyToString = 'Reply-To: =?utf-8?Q?' ++ . str_replace(' ', '=20', $replyTo[0]->getName()) ++ . '?= <' ++ . $replyTo[0]->getEmail() ++ . '>'; + $this->assertContains($replyToString, $message->toString()); +- $toString = 'To: ' . $to[0]->getName() . ' <' . $to[0]->getEmail() . '>'; ++ $toString = 'To: =?utf-8?Q?' ++ . str_replace(' ', '=20', $to[0]->getName()) ++ . '?= <' ++ . $to[0]->getEmail() ++ . '>'; + $this->assertContains($toString, $message->toString()); +- $ccString = 'Cc: ' . $cc[0]->getName() . ' <' . $cc[0]->getEmail() . '>'; ++ $ccString = 'Cc: =?utf-8?Q?' ++ . str_replace(' ', '=20', $cc[0]->getName()) ++ . '?= <' ++ . $cc[0]->getEmail() ++ . '>'; + $this->assertContains($ccString, $message->toString()); + $this->assertContains('Bcc: ' . $bcc[0]->getEmail(), $message->toString()); +- $this->assertContains('Content-Description: ' . $this->description, $message->toString()); ++ $contentDescription = 'Content-Description: =?utf-8?Q?' ++ . str_replace(' ', '=20', $this->description) ++ . '?='; ++ $this->assertContains($contentDescription, $message->toString()); + $this->assertContains('Subject: ' . $this->subject, $message->toString()); + $this->assertContains($content, $message->toString()); + //tests address factory +@@ -235,6 +255,7 @@ class EmailMessageTest extends TestCase + 'body' => $mimeMessage, + 'subject' => $this->subject, + 'to' => [$addressTo], ++ 'encoding' => '', + ]; + $message = $this->messageFactory->create($data); + +diff --git a/vendor/magento/framework/Mail/EmailMessage.php b/vendor/magento/framework/Mail/EmailMessage.php +index aaef9750751..02c75977cd0 100644 +--- a/vendor/magento/framework/Mail/EmailMessage.php ++++ b/vendor/magento/framework/Mail/EmailMessage.php +@@ -10,19 +10,13 @@ namespace Magento\Framework\Mail; + use Magento\Framework\Mail\Exception\InvalidArgumentException; + use Zend\Mail\Address as ZendAddress; + use Zend\Mail\AddressList; +-use Zend\Mail\Message as ZendMessage; + use Zend\Mime\Message as ZendMimeMessage; + + /** +- * Class EmailMessage ++ * Email message + */ +-class EmailMessage implements EmailMessageInterface ++class EmailMessage extends Message implements EmailMessageInterface + { +- /** +- * @var ZendMessage +- */ +- private $message; +- + /** + * @var MimeMessageInterfaceFactory + */ +@@ -64,38 +58,35 @@ class EmailMessage implements EmailMessageInterface + ?array $replyTo = null, + ?Address $sender = null, + ?string $subject = '', +- ?string $encoding = '' ++ ?string $encoding = 'utf-8' + ) { +- $this->message = new ZendMessage(); ++ parent::__construct($encoding); + $mimeMessage = new ZendMimeMessage(); + $mimeMessage->setParts($body->getParts()); +- $this->message->setBody($mimeMessage); +- if ($encoding) { +- $this->message->setEncoding($encoding); +- } ++ $this->zendMessage->setBody($mimeMessage); + if ($subject) { +- $this->message->setSubject($subject); ++ $this->zendMessage->setSubject($subject); + } + if ($sender) { +- $this->message->setSender($sender->getEmail(), $sender->getName()); ++ $this->zendMessage->setSender($sender->getEmail(), $sender->getName()); + } + if (count($to) < 1) { + throw new InvalidArgumentException('Email message must have at list one addressee'); + } + if ($to) { +- $this->message->setTo($this->convertAddressArrayToAddressList($to)); ++ $this->zendMessage->setTo($this->convertAddressArrayToAddressList($to)); + } + if ($replyTo) { +- $this->message->setReplyTo($this->convertAddressArrayToAddressList($replyTo)); ++ $this->zendMessage->setReplyTo($this->convertAddressArrayToAddressList($replyTo)); + } + if ($from) { +- $this->message->setFrom($this->convertAddressArrayToAddressList($from)); ++ $this->zendMessage->setFrom($this->convertAddressArrayToAddressList($from)); + } + if ($cc) { +- $this->message->setCc($this->convertAddressArrayToAddressList($cc)); ++ $this->zendMessage->setCc($this->convertAddressArrayToAddressList($cc)); + } + if ($bcc) { +- $this->message->setBcc($this->convertAddressArrayToAddressList($bcc)); ++ $this->zendMessage->setBcc($this->convertAddressArrayToAddressList($bcc)); + } + $this->mimeMessageFactory = $mimeMessageFactory; + $this->addressFactory = $addressFactory; +@@ -106,7 +97,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function getEncoding(): string + { +- return $this->message->getEncoding(); ++ return $this->zendMessage->getEncoding(); + } + + /** +@@ -114,7 +105,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function getHeaders(): array + { +- return $this->message->getHeaders()->toArray(); ++ return $this->zendMessage->getHeaders()->toArray(); + } + + /** +@@ -122,7 +113,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function getFrom(): ?array + { +- return $this->convertAddressListToAddressArray($this->message->getFrom()); ++ return $this->convertAddressListToAddressArray($this->zendMessage->getFrom()); + } + + /** +@@ -130,7 +121,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function getTo(): array + { +- return $this->convertAddressListToAddressArray($this->message->getTo()); ++ return $this->convertAddressListToAddressArray($this->zendMessage->getTo()); + } + + /** +@@ -138,7 +129,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function getCc(): ?array + { +- return $this->convertAddressListToAddressArray($this->message->getCc()); ++ return $this->convertAddressListToAddressArray($this->zendMessage->getCc()); + } + + /** +@@ -146,7 +137,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function getBcc(): ?array + { +- return $this->convertAddressListToAddressArray($this->message->getBcc()); ++ return $this->convertAddressListToAddressArray($this->zendMessage->getBcc()); + } + + /** +@@ -154,7 +145,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function getReplyTo(): ?array + { +- return $this->convertAddressListToAddressArray($this->message->getReplyTo()); ++ return $this->convertAddressListToAddressArray($this->zendMessage->getReplyTo()); + } + + /** +@@ -163,7 +154,7 @@ class EmailMessage implements EmailMessageInterface + public function getSender(): ?Address + { + /** @var ZendAddress $zendSender */ +- if (!$zendSender = $this->message->getSender()) { ++ if (!$zendSender = $this->zendMessage->getSender()) { + return null; + } + +@@ -178,18 +169,10 @@ class EmailMessage implements EmailMessageInterface + /** + * @inheritDoc + */ +- public function getSubject(): ?string +- { +- return $this->message->getSubject(); +- } +- +- /** +- * @inheritDoc +- */ +- public function getBody(): MimeMessageInterface ++ public function getMessageBody(): MimeMessageInterface + { + return $this->mimeMessageFactory->create( +- ['parts' => $this->message->getBody()->getParts()] ++ ['parts' => $this->zendMessage->getBody()->getParts()] + ); + } + +@@ -198,15 +181,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function getBodyText(): string + { +- return $this->message->getBodyText(); +- } +- +- /** +- * @inheritdoc +- */ +- public function getRawMessage(): string +- { +- return $this->toString(); ++ return $this->zendMessage->getBodyText(); + } + + /** +@@ -214,7 +189,7 @@ class EmailMessage implements EmailMessageInterface + */ + public function toString(): string + { +- return $this->message->toString(); ++ return $this->zendMessage->toString(); + } + + /** +diff --git a/vendor/magento/framework/Mail/EmailMessageInterface.php b/vendor/magento/framework/Mail/EmailMessageInterface.php +index 95f83ff679c..93eaa4acde3 100644 +--- a/vendor/magento/framework/Mail/EmailMessageInterface.php ++++ b/vendor/magento/framework/Mail/EmailMessageInterface.php +@@ -9,7 +9,7 @@ namespace Magento\Framework\Mail; + /** + * Interface EmailMessageInterface + */ +-interface EmailMessageInterface ++interface EmailMessageInterface extends MailMessageInterface + { + /** + * Get the message encoding +@@ -72,14 +72,14 @@ interface EmailMessageInterface + * + * @return null|string + */ +- public function getSubject(): ?string; ++ public function getSubject(); + + /** + * Return the currently set message body + * + * @return MimeMessageInterface + */ +- public function getBody(): MimeMessageInterface; ++ public function getMessageBody(): MimeMessageInterface; + + /** + * Get the string-serialized message body text +diff --git a/vendor/magento/framework/Mail/MailMessageInterface.php b/vendor/magento/framework/Mail/MailMessageInterface.php +index da010be2702..5179e6057c4 100644 +--- a/vendor/magento/framework/Mail/MailMessageInterface.php ++++ b/vendor/magento/framework/Mail/MailMessageInterface.php +@@ -10,6 +10,8 @@ namespace Magento\Framework\Mail; + * + * @api + * @since 101.0.8 ++ * @deprecated ++ * @see \Magento\Framework\Mail\EmailMessageInterface + */ + interface MailMessageInterface extends MessageInterface + { +diff --git a/vendor/magento/framework/Mail/Message.php b/vendor/magento/framework/Mail/Message.php +index b15b75ca9ac..1f423e80108 100644 +--- a/vendor/magento/framework/Mail/Message.php ++++ b/vendor/magento/framework/Mail/Message.php +@@ -10,13 +10,16 @@ use Zend\Mime\Part; + + /** + * Class Message for email transportation ++ * ++ * @deprecated ++ * @see \Magento\Framework\Mail\EmailMessage + */ + class Message implements MailMessageInterface + { + /** + * @var \Zend\Mail\Message + */ +- private $zendMessage; ++ protected $zendMessage; + + /** + * Message type +diff --git a/vendor/magento/framework/Mail/Template/TransportBuilder.php b/vendor/magento/framework/Mail/Template/TransportBuilder.php +index 4a8d6572faa..2d28e2f560a 100644 +--- a/vendor/magento/framework/Mail/Template/TransportBuilder.php ++++ b/vendor/magento/framework/Mail/Template/TransportBuilder.php +@@ -87,7 +87,7 @@ class TransportBuilder + /** + * Message + * +- * @var EmailMessageInterface ++ * @var MessageInterface + */ + protected $message; + From 5d1b009d618075bfc144084e8bf9ee91dcee8014 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak Date: Fri, 1 Nov 2019 10:48:17 -0500 Subject: [PATCH 005/157] MAGECLOUD-4448-4449: Deliver core patches (#4) --- composer.json | 2 +- patches.json | 6 ++++++ ...-21696__fix_broken_pagination_on_ES6__2.3.3.patch | 11 +++++++++++ ...MC-21820__fix_broken_method_chaining__2.3.3.patch | 12 ++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 patches/MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch create mode 100644 patches/MC-21820__fix_broken_method_chaining__2.3.3.patch diff --git a/composer.json b/composer.json index 59750816..fbf184e0 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", "version": "1.0.0", - "license": "OSL-3", + "license": "OSL-3.0", "require": { "php": "^7.0", "ext-json": "*", diff --git a/patches.json b/patches.json index 1b015f95..bec8c20f 100644 --- a/patches.json +++ b/patches.json @@ -183,6 +183,12 @@ }, "Backward Compatibility of new Mail Interfaces": { "2.3.3": "MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch" + }, + "Catalog Product pagination does not work on Elasticsearch 6.x": { + "2.3.3": "MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch" + }, + "Catalog Product collection method chaining is broken": { + "2.3.3": "MC-21820__fix_broken_method_chaining__2.3.3.patch" } }, "monolog/monolog": { diff --git a/patches/MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch b/patches/MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch new file mode 100644 index 00000000..8c3806ac --- /dev/null +++ b/patches/MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch @@ -0,0 +1,11 @@ +diff -Naur a/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +--- a/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php ++++ b/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +@@ -50,6 +50,7 @@ class SearchResultApplier implements SearchResultApplierInterface + foreach ($this->searchResult->getItems() as $item) { + $ids[] = (int)$item->getId(); + } ++ $this->collection->setPageSize(null); + $this->collection->getSelect()->where('e.entity_id IN (?)', $ids); + $orderList = join(',', $ids); + $this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER); diff --git a/patches/MC-21820__fix_broken_method_chaining__2.3.3.patch b/patches/MC-21820__fix_broken_method_chaining__2.3.3.patch new file mode 100644 index 00000000..7c51043e --- /dev/null +++ b/patches/MC-21820__fix_broken_method_chaining__2.3.3.patch @@ -0,0 +1,12 @@ +diff -Naur a/vendor/magento/module-catalog/Model/ResourceModel/Product/Collection.php b/vendor/magento/module-catalog/Model/ResourceModel/Product/Collection.php +--- a/vendor/magento/module-catalog/Model/ResourceModel/Product/Collection.php ++++ b/vendor/magento/module-catalog/Model/ResourceModel/Product/Collection.php +@@ -1584,6 +1584,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac + } else { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } ++ ++ return $this; + } + + /** From 3b0fee2847dde7b53da1b2b1c490170baf0d7d84 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak Date: Tue, 5 Nov 2019 15:49:31 -0600 Subject: [PATCH 006/157] MAGECLOUD-4585: Reduce q-ty of Reports Created in /app/*/var/report (#5) --- patches.json | 17 + ...e_q-ty_of_error_report_files__2.1.17.patch | 980 +++++++++++++++ ...ce_q-ty_of_error_report_files__2.1.4.patch | 971 +++++++++++++++ ...ce_q-ty_of_error_report_files__2.1.8.patch | 970 +++++++++++++++ ...ce_q-ty_of_error_report_files__2.2.0.patch | 996 ++++++++++++++++ ...ce_q-ty_of_error_report_files__2.2.5.patch | 996 ++++++++++++++++ ...ce_q-ty_of_error_report_files__2.2.6.patch | 996 ++++++++++++++++ ...ce_q-ty_of_error_report_files__2.2.8.patch | 985 ++++++++++++++++ ...ce_q-ty_of_error_report_files__2.3.0.patch | 1049 +++++++++++++++++ ...ce_q-ty_of_error_report_files__2.3.1.patch | 1006 ++++++++++++++++ ...ce_q-ty_of_error_report_files__2.3.2.patch | 982 +++++++++++++++ ...407__fix_namespace_vertex_tax__3.2.0.patch | 12 + 12 files changed, 9960 insertions(+) create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch create mode 100644 patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch create mode 100644 patches/MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch diff --git a/patches.json b/patches.json index bec8c20f..f81ca123 100644 --- a/patches.json +++ b/patches.json @@ -189,6 +189,18 @@ }, "Catalog Product collection method chaining is broken": { "2.3.3": "MC-21820__fix_broken_method_chaining__2.3.3.patch" + }, + "Reduce q-ty of error report files": { + "2.1.4 - 2.1.7": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch", + "2.1.8 - 2.1.16": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch", + "~2.1.17": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch", + "2.2.0 - 2.2.4": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch", + "2.2.5": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch", + "2.2.6 - 2.2.7": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch", + "~2.2.8": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch", + "2.3.0": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch", + "2.3.1": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch", + "2.3.2 - 2.3.3": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch" } }, "monolog/monolog": { @@ -235,5 +247,10 @@ "Avoid group concat from source item indexer": { ">=1.0.3 <1.0.5": "MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__grouped-product-indexer__1.0.3.patch" } + }, + "vertex/module-tax": { + "Fix wrong namespace": { + "3.2.0": "MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch" + } } } diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch new file mode 100644 index 00000000..a12bf65f --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch @@ -0,0 +1,980 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -38,6 +38,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,278 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ) { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ) { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception) ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception &$exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ) { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ) { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception) ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -101,38 +97,23 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws \InvalidArgumentException|LocalizedException + */ + public function launch() + { + $areaCode = $this->_areaList->getCodeByFrontName($this->_request->getFrontName()); + $this->_state->setAreaCode($areaCode); + $this->_objectManager->configure($this->_configLoader->load($areaCode)); +- /** @var \Magento\Framework\App\FrontControllerInterface $frontController */ +- $frontController = $this->_objectManager->get(\Magento\Framework\App\FrontControllerInterface::class); ++ /** @var FrontControllerInterface $frontController */ ++ $frontController = $this->_objectManager->get(FrontControllerInterface::class); + $result = $frontController->dispatch($this->_request); + // TODO: Temporary solution until all controllers return ResultInterface (MAGETWO-28359) + if ($result instanceof ResultInterface) { +@@ -154,188 +135,6 @@ class Http implements \Magento\Framework\AppInterface + */ + public function catchException(Bootstrap $bootstrap, \Exception $exception) + { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- Debug::trace( +- $exception->getTrace(), +- true, +- true, +- (bool)getenv('MAGE_DEBUG_SHOW_ARGS') +- ) +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleSessionException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [ +- $exception->getMessage(), +- Debug::trace( +- $exception->getTrace(), +- true, +- true, +- (bool)getenv('MAGE_DEBUG_SHOW_ARGS') +- ) +- ]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/vendor/magento/framework/Logger/Monolog.php b/vendor/magento/framework/Logger/Monolog.php +--- a/vendor/magento/framework/Logger/Monolog.php ++++ b/vendor/magento/framework/Logger/Monolog.php +@@ -20,7 +20,11 @@ class Monolog extends Logger + */ + public function addRecord($level, $message, array $context = []) + { ++ if ($message instanceof \Exception && !isset($context['exception'])) { ++ $context['exception'] = $message; ++ } + $context['is_exception'] = $message instanceof \Exception; ++ $message = $message instanceof \Exception ? $message->getMessage() : $message; + return parent::addRecord($level, $message, $context); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -4,7 +4,10 @@ + * See COPYING.txt for license details. + */ + namespace Magento\Framework\Error; +-use Magento\Framework\Filesystem\DriverInterface; ++ ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor +@@ -15,6 +18,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -61,7 +65,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -122,18 +126,25 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response) ++ public function __construct(Http $response, Escaper $escaper = null) + { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -143,11 +154,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -155,6 +161,10 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -341,6 +351,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -435,7 +448,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -446,18 +459,19 @@ class Processor + * @param array $reportData + * @return void + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData) + { +- $this->reportData = $reportData; +- $this->reportId = abs(intval(microtime(true) * rand(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } +- +- @file_put_contents($this->_reportFile, serialize($reportData)); ++ $this->_setReportData($reportData); ++ @file_put_contents($this->_reportFile, serialize($reportData).PHP_EOL); + + if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) { + $this->_setSkin($reportData['skin']); +@@ -480,14 +494,108 @@ class Processor + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData(unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } ++ ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile($reportId) ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } ++ /** ++ * Redirect to a base url ++ * @return void ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid($reportId) ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel($reportId) ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = isset($_ENV[$envName]) ? $_ENV[$envName] : getenv($envName); ++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue = $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel($reportId) ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath( $reportDirNestingLevel, $reportId) ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; + } +- $this->_setReportData(unserialize(file_get_contents($this->_reportFile))); ++ return $reportDirPath . $reportId; + } + + /** +@@ -499,11 +607,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch new file mode 100644 index 00000000..dd4b2740 --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch @@ -0,0 +1,971 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -38,6 +38,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,278 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ) { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ) { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception) ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception &$exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ) { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ) { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception) ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -100,38 +97,23 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws \InvalidArgumentException|LocalizedException + */ + public function launch() + { + $areaCode = $this->_areaList->getCodeByFrontName($this->_request->getFrontName()); + $this->_state->setAreaCode($areaCode); + $this->_objectManager->configure($this->_configLoader->load($areaCode)); +- /** @var \Magento\Framework\App\FrontControllerInterface $frontController */ +- $frontController = $this->_objectManager->get('Magento\Framework\App\FrontControllerInterface'); ++ /** @var FrontControllerInterface $frontController */ ++ $frontController = $this->_objectManager->get(FrontControllerInterface::class); + $result = $frontController->dispatch($this->_request); + // TODO: Temporary solution until all controllers return ResultInterface (MAGETWO-28359) + if ($result instanceof ResultInterface) { +@@ -149,179 +131,10 @@ class Http implements \Magento\Framework\AppInterface + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function catchException(Bootstrap $bootstrap, \Exception $exception) + { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- $exception->getTraceAsString() +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- $newMessage .= 'If you are using the sample nginx configuration, please go to ' +- . $this->_request->getScheme(). '://' . $this->_request->getHttpHost() . $setupInfo->getUrl(); +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleSessionException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/vendor/magento/framework/Logger/Monolog.php b/vendor/magento/framework/Logger/Monolog.php +--- a/vendor/magento/framework/Logger/Monolog.php ++++ b/vendor/magento/framework/Logger/Monolog.php +@@ -20,7 +20,11 @@ class Monolog extends Logger + */ + public function addRecord($level, $message, array $context = []) + { ++ if ($message instanceof \Exception && !isset($context['exception'])) { ++ $context['exception'] = $message; ++ } + $context['is_exception'] = $message instanceof \Exception; ++ $message = $message instanceof \Exception ? $message->getMessage() : $message; + return parent::addRecord($level, $message, $context); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -4,7 +4,10 @@ + * See COPYING.txt for license details. + */ + namespace Magento\Framework\Error; +-use Magento\Framework\Filesystem\DriverInterface; ++ ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor +@@ -15,6 +18,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -61,7 +65,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -122,18 +126,25 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response) ++ public function __construct(Http $response, Escaper $escaper = null) + { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -143,11 +154,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -155,6 +161,10 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -341,6 +351,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -435,7 +448,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -446,18 +459,19 @@ class Processor + * @param array $reportData + * @return void + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData) + { +- $this->reportData = $reportData; +- $this->reportId = abs(intval(microtime(true) * rand(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } +- +- @file_put_contents($this->_reportFile, serialize($reportData)); ++ $this->_setReportData($reportData); ++ @file_put_contents($this->_reportFile, serialize($reportData).PHP_EOL); + + if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) { + $this->_setSkin($reportData['skin']); +@@ -480,14 +494,108 @@ class Processor + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData(unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } ++ ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile($reportId) ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } ++ /** ++ * Redirect to a base url ++ * @return void ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid($reportId) ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel($reportId) ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = isset($_ENV[$envName]) ? $_ENV[$envName] : getenv($envName); ++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue = $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel($reportId) ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath( $reportDirNestingLevel, $reportId) ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; + } +- $this->_setReportData(unserialize(file_get_contents($this->_reportFile))); ++ return $reportDirPath . $reportId; + } + + /** +@@ -499,11 +607,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch new file mode 100644 index 00000000..eb244a81 --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch @@ -0,0 +1,970 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -38,6 +38,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,278 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ) { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ) { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception) ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception &$exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ) { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ) { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception) ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -100,38 +97,23 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws \InvalidArgumentException|LocalizedException + */ + public function launch() + { + $areaCode = $this->_areaList->getCodeByFrontName($this->_request->getFrontName()); + $this->_state->setAreaCode($areaCode); + $this->_objectManager->configure($this->_configLoader->load($areaCode)); +- /** @var \Magento\Framework\App\FrontControllerInterface $frontController */ +- $frontController = $this->_objectManager->get('Magento\Framework\App\FrontControllerInterface'); ++ /** @var FrontControllerInterface $frontController */ ++ $frontController = $this->_objectManager->get(FrontControllerInterface::class); + $result = $frontController->dispatch($this->_request); + // TODO: Temporary solution until all controllers return ResultInterface (MAGETWO-28359) + if ($result instanceof ResultInterface) { +@@ -149,178 +131,10 @@ class Http implements \Magento\Framework\AppInterface + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function catchException(Bootstrap $bootstrap, \Exception $exception) + { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- $exception->getTraceAsString() +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleSessionException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/vendor/magento/framework/Logger/Monolog.php b/vendor/magento/framework/Logger/Monolog.php +--- a/vendor/magento/framework/Logger/Monolog.php ++++ b/vendor/magento/framework/Logger/Monolog.php +@@ -20,7 +20,11 @@ class Monolog extends Logger + */ + public function addRecord($level, $message, array $context = []) + { ++ if ($message instanceof \Exception && !isset($context['exception'])) { ++ $context['exception'] = $message; ++ } + $context['is_exception'] = $message instanceof \Exception; ++ $message = $message instanceof \Exception ? $message->getMessage() : $message; + return parent::addRecord($level, $message, $context); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -4,7 +4,10 @@ + * See COPYING.txt for license details. + */ + namespace Magento\Framework\Error; +-use Magento\Framework\Filesystem\DriverInterface; ++ ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor +@@ -15,6 +18,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -61,7 +65,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -122,18 +126,25 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response) ++ public function __construct(Http $response, Escaper $escaper = null) + { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -143,11 +154,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -155,6 +161,10 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -341,6 +351,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -435,7 +448,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -446,18 +459,19 @@ class Processor + * @param array $reportData + * @return void + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData) + { +- $this->reportData = $reportData; +- $this->reportId = abs(intval(microtime(true) * rand(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } +- +- @file_put_contents($this->_reportFile, serialize($reportData)); ++ $this->_setReportData($reportData); ++ @file_put_contents($this->_reportFile, serialize($reportData).PHP_EOL); + + if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) { + $this->_setSkin($reportData['skin']); +@@ -480,14 +494,108 @@ class Processor + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData(unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } ++ ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile($reportId) ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } ++ /** ++ * Redirect to a base url ++ * @return void ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid($reportId) ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel($reportId) ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = isset($_ENV[$envName]) ? $_ENV[$envName] : getenv($envName); ++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue = $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel($reportId) ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath( $reportDirNestingLevel, $reportId) ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; + } +- $this->_setReportData(unserialize(file_get_contents($this->_reportFile))); ++ return $reportDirPath . $reportId; + } + + /** +@@ -499,11 +607,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch new file mode 100644 index 00000000..8e61140d --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch @@ -0,0 +1,996 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -43,6 +43,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,280 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ): bool { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception) :string ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception &$exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ): bool { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception) : bool ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -100,30 +97,15 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated 100.1.0 +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws \InvalidArgumentException|LocalizedException + */ + public function launch() + { +@@ -149,178 +131,10 @@ class Http implements \Magento\Framework\AppInterface + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ +- public function catchException(Bootstrap $bootstrap, \Exception $exception) ++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool + { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- $exception->getTraceAsString() +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleSessionException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -6,11 +6,15 @@ + namespace Magento\Framework\Error; + + use Magento\Framework\Serialize\Serializer\Json; ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor + * + * @SuppressWarnings(PHPMD.TooManyFields) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ + class Processor + { +@@ -18,6 +22,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -64,7 +69,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -125,7 +130,7 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + +@@ -137,15 +142,25 @@ class Processor + private $serializer; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response + * @param Json $serializer ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null) +- { ++ public function __construct( ++ Http $response, ++ Json $serializer = null, ++ Escaper $escaper = null ++ ) { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; +- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); ++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -155,11 +170,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -167,6 +177,10 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -268,10 +282,11 @@ class Processor + $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off'); + $url = ($isSecure ? 'https://' : 'http://') . $host; + +- if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 433]) ++ $port = explode(':', $host); ++ if (isset($port[1]) && !in_array($port[1], [80, 443]) + && !preg_match('/.*?\:[0-9]+$/', $url) + ) { +- $url .= ':' . $_SERVER['SERVER_PORT']; ++ $url .= ':' . $port[1]; + } + return $url; + } +@@ -355,6 +370,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -379,6 +397,8 @@ class Processor + } + + /** ++ * Render page ++ * + * @param string $template + * @return string + */ +@@ -449,7 +469,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -460,16 +480,18 @@ class Processor + * @param array $reportData + * @return string + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData): string + { +- $this->reportData = $reportData; +- $this->reportId = abs(intval(microtime(true) * rand(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } ++ $this->_setReportData($reportData); + + @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData)); + +@@ -484,20 +506,118 @@ class Processor + /** + * Get report + * +- * @param int $reportId ++ * @param string $reportId + * @return void +- * @SuppressWarnings(PHPMD.ExitExpression) + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } ++ ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile(string $reportId): string ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } ++ ++ /** ++ * Redirect to a base url ++ * @return void ++ * @SuppressWarnings(PHPMD.ExitExpression) ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid(string $reportId): bool ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel(string $reportId): int ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = $_ENV[$envName] ?? getenv($envName); ++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue = $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel(string $reportId): int ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; + } +- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ return $reportDirPath . $reportId; + } + + /** +@@ -511,11 +631,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch new file mode 100644 index 00000000..5ae791e6 --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch @@ -0,0 +1,996 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -44,6 +44,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,280 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ): bool { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception) :string ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception &$exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ): bool { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception) : bool ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -100,30 +97,15 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated 100.1.0 +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws \InvalidArgumentException|LocalizedException + */ + public function launch() + { +@@ -149,178 +131,10 @@ class Http implements \Magento\Framework\AppInterface + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ +- public function catchException(Bootstrap $bootstrap, \Exception $exception) ++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool + { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- $exception->getTraceAsString() +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleSessionException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -6,11 +6,15 @@ + namespace Magento\Framework\Error; + + use Magento\Framework\Serialize\Serializer\Json; ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor + * + * @SuppressWarnings(PHPMD.TooManyFields) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ + class Processor + { +@@ -18,6 +22,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -64,7 +69,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -125,7 +130,7 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + +@@ -137,15 +142,25 @@ class Processor + private $serializer; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response + * @param Json $serializer ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null) +- { ++ public function __construct( ++ Http $response, ++ Json $serializer = null, ++ Escaper $escaper = null ++ ) { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; +- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); ++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -155,11 +170,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -167,6 +177,10 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -268,10 +282,11 @@ class Processor + $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off'); + $url = ($isSecure ? 'https://' : 'http://') . $host; + +- if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 443]) ++ $port = explode(':', $host); ++ if (isset($port[1]) && !in_array($port[1], [80, 443]) + && !preg_match('/.*?\:[0-9]+$/', $url) + ) { +- $url .= ':' . $_SERVER['SERVER_PORT']; ++ $url .= ':' . $port[1]; + } + return $url; + } +@@ -355,6 +370,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -379,6 +397,8 @@ class Processor + } + + /** ++ * Render page ++ * + * @param string $template + * @return string + */ +@@ -449,7 +469,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -460,16 +480,18 @@ class Processor + * @param array $reportData + * @return string + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData): string + { +- $this->reportData = $reportData; +- $this->reportId = abs(intval(microtime(true) * rand(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } ++ $this->_setReportData($reportData); + + @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData)); + +@@ -484,20 +506,118 @@ class Processor + /** + * Get report + * +- * @param int $reportId ++ * @param string $reportId + * @return void +- * @SuppressWarnings(PHPMD.ExitExpression) + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } ++ ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile(string $reportId): string ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } ++ ++ /** ++ * Redirect to a base url ++ * @return void ++ * @SuppressWarnings(PHPMD.ExitExpression) ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid(string $reportId): bool ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel(string $reportId): int ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = $_ENV[$envName] ?? getenv($envName); ++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue = $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel(string $reportId): int ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; + } +- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ return $reportDirPath . $reportId; + } + + /** +@@ -511,11 +631,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch new file mode 100644 index 00000000..91e8ebca --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch @@ -0,0 +1,996 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -44,6 +44,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,280 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ): bool { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception) :string ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception &$exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ): bool { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception) : bool ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -100,30 +97,15 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated 100.1.0 +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws \InvalidArgumentException|LocalizedException + */ + public function launch() + { +@@ -149,178 +131,10 @@ class Http implements \Magento\Framework\AppInterface + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ +- public function catchException(Bootstrap $bootstrap, \Exception $exception) ++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool + { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- $exception->getTraceAsString() +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleSessionException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -6,11 +6,15 @@ + namespace Magento\Framework\Error; + + use Magento\Framework\Serialize\Serializer\Json; ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor + * + * @SuppressWarnings(PHPMD.TooManyFields) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ + class Processor + { +@@ -18,6 +22,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -64,7 +69,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -125,7 +130,7 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + +@@ -137,15 +142,25 @@ class Processor + private $serializer; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response + * @param Json $serializer ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null) +- { ++ public function __construct( ++ Http $response, ++ Json $serializer = null, ++ Escaper $escaper = null ++ ) { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; +- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); ++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -155,11 +170,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -167,6 +177,10 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -268,10 +282,11 @@ class Processor + $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off'); + $url = ($isSecure ? 'https://' : 'http://') . $host; + +- if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 443]) ++ $port = explode(':', $host); ++ if (isset($port[1]) && !in_array($port[1], [80, 443]) + && !preg_match('/.*?\:[0-9]+$/', $url) + ) { +- $url .= ':' . $_SERVER['SERVER_PORT']; ++ $url .= ':' . $port[1]; + } + return $url; + } +@@ -355,6 +370,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -379,6 +397,8 @@ class Processor + } + + /** ++ * Render page ++ * + * @param string $template + * @return string + */ +@@ -449,7 +469,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -460,16 +480,18 @@ class Processor + * @param array $reportData + * @return string + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData): string + { +- $this->reportData = $reportData; +- $this->reportId = abs(intval(microtime(true) * random_int(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } ++ $this->_setReportData($reportData); + + @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData)); + +@@ -484,20 +506,118 @@ class Processor + /** + * Get report + * +- * @param int $reportId ++ * @param string $reportId + * @return void +- * @SuppressWarnings(PHPMD.ExitExpression) + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } ++ ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile(string $reportId): string ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } ++ ++ /** ++ * Redirect to a base url ++ * @return void ++ * @SuppressWarnings(PHPMD.ExitExpression) ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid(string $reportId): bool ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel(string $reportId): int ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = $_ENV[$envName] ?? getenv($envName); ++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue = $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel(string $reportId): int ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; + } +- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ return $reportDirPath . $reportId; + } + + /** +@@ -511,11 +631,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch new file mode 100644 index 00000000..5559c0e1 --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch @@ -0,0 +1,985 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -44,6 +44,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,280 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ): bool { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception) :string ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception &$exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ): bool { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception) : bool ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -101,30 +97,15 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated 100.1.0 +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws \InvalidArgumentException|LocalizedException + */ + public function launch() + { +@@ -152,190 +133,8 @@ class Http implements \Magento\Framework\AppInterface + /** + * @inheritdoc + */ +- public function catchException(Bootstrap $bootstrap, \Exception $exception) +- { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- Debug::trace( +- $exception->getTrace(), +- true, +- true, +- (bool)getenv('MAGE_DEBUG_SHOW_ARGS') +- ) +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) ++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool + { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleSessionException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [ +- $exception->getMessage(), +- Debug::trace( +- $exception->getTrace(), +- true, +- true, +- (bool)getenv('MAGE_DEBUG_SHOW_ARGS') +- ) +- ]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -6,11 +6,15 @@ + namespace Magento\Framework\Error; + + use Magento\Framework\Serialize\Serializer\Json; ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor + * + * @SuppressWarnings(PHPMD.TooManyFields) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ + class Processor + { +@@ -18,6 +22,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -64,7 +69,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -125,7 +130,7 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + +@@ -137,15 +142,25 @@ class Processor + private $serializer; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response + * @param Json $serializer ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null) +- { ++ public function __construct( ++ Http $response, ++ Json $serializer = null, ++ Escaper $escaper = null ++ ) { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; +- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); ++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -155,11 +170,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -167,6 +177,10 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -356,6 +370,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -452,7 +469,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -463,16 +480,18 @@ class Processor + * @param array $reportData + * @return string + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData): string + { +- $this->reportData = $reportData; +- $this->reportId = abs((int)microtime(true) * random_int(100, 1000)); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } ++ $this->_setReportData($reportData); + + @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData)); + +@@ -487,20 +506,118 @@ class Processor + /** + * Get report + * +- * @param int $reportId ++ * @param string $reportId + * @return void +- * @SuppressWarnings(PHPMD.ExitExpression) + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } ++ ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile(string $reportId): string ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Redirect to a base url ++ * @return void ++ * @SuppressWarnings(PHPMD.ExitExpression) ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid(string $reportId): bool ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel(string $reportId): int ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = $_ENV[$envName] ?? getenv($envName); ++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue = $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel(string $reportId): int ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); ++ } ++ ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; + } +- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ return $reportDirPath . $reportId; + } + + /** +@@ -514,11 +631,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch new file mode 100644 index 00000000..37cbf154 --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch @@ -0,0 +1,1049 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -46,6 +46,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,284 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ): bool { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception): string ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ): bool { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception): bool ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ // phpcs:ignore Magento2.Exceptions.DirectThrow ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -100,30 +96,15 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated 100.1.0 +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws LocalizedException|\InvalidArgumentException + */ + public function launch() + { +@@ -142,6 +123,9 @@ class Http implements \Magento\Framework\AppInterface + } else { + throw new \InvalidArgumentException('Invalid return type'); + } ++ if ($this->_request->isHead() && $this->_response->getHttpResponseCode() == 200) { ++ $this->handleHeadRequest(); ++ } + // This event gives possibility to launch something before sending output (allow cookie setting) + $eventParams = ['request' => $this->_request, 'response' => $this->_response]; + $this->_eventManager->dispatch('controller_front_send_response_before', $eventParams); +@@ -149,178 +133,26 @@ class Http implements \Magento\Framework\AppInterface + } + + /** +- * {@inheritdoc} +- */ +- public function catchException(Bootstrap $bootstrap, \Exception $exception) +- { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- $exception->getTraceAsString() +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard ++ * Handle HEAD requests by adding the Content-Length header and removing the body from the response. + * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception + * @return void +- * @throws \Exception + */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) ++ private function handleHeadRequest() + { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- +- throw new \Exception($newMessage, 0, $exception); +- } ++ // It is possible that some PHP installations have overloaded strlen to use mb_strlen instead. ++ // This means strlen might return the actual number of characters in a non-ascii string instead ++ // of the number of bytes. Use mb_strlen explicitly with a single byte character encoding to ensure ++ // that the content length is calculated in bytes. ++ $contentLength = mb_strlen($this->_response->getContent(), '8bit'); ++ $this->_response->clearBody(); ++ $this->_response->setHeader('Content-Length', $contentLength); + } + + /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool ++ * @inheritdoc + */ +- private function handleSessionException(\Exception $exception) ++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool + { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -3,14 +3,21 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++declare(strict_types=1); ++ + namespace Magento\Framework\Error; + + use Magento\Framework\Serialize\Serializer\Json; ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor + * + * @SuppressWarnings(PHPMD.TooManyFields) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) ++ * phpcs:ignoreFile + */ + class Processor + { +@@ -18,6 +25,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -64,7 +72,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -125,7 +133,7 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + +@@ -137,15 +145,25 @@ class Processor + private $serializer; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response + * @param Json $serializer ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null) +- { ++ public function __construct( ++ Http $response, ++ Json $serializer = null, ++ Escaper $escaper = null ++ ) { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; +- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); ++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -155,11 +173,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -167,6 +180,9 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -257,25 +273,38 @@ class Processor + /** + * Define server http host + */ +- if (!empty($_SERVER['HTTP_HOST'])) { +- $host = $_SERVER['HTTP_HOST']; +- } elseif (!empty($_SERVER['SERVER_NAME'])) { +- $host = $_SERVER['SERVER_NAME']; +- } else { +- $host = 'localhost'; +- } ++ $host = $this->resolveHostName(); + +- $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off'); ++ $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] !== 'off') ++ || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'); + $url = ($isSecure ? 'https://' : 'http://') . $host; + +- if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 443]) ++ $port = explode(':', $host); ++ if (isset($port[1]) && !in_array($port[1], [80, 443]) + && !preg_match('/.*?\:[0-9]+$/', $url) + ) { +- $url .= ':' . $_SERVER['SERVER_PORT']; ++ $url .= ':' . $port[1]; + } + return $url; + } + ++ /** ++ * Resolve hostname ++ * ++ * @return string ++ */ ++ private function resolveHostName() : string ++ { ++ if (!empty($_SERVER['HTTP_HOST'])) { ++ $host = $_SERVER['HTTP_HOST']; ++ } elseif (!empty($_SERVER['SERVER_NAME'])) { ++ $host = $_SERVER['SERVER_NAME']; ++ } else { ++ $host = 'localhost'; ++ } ++ return $host; ++ } ++ + /** + * Retrieve base URL + * +@@ -355,6 +384,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -379,6 +411,8 @@ class Processor + } + + /** ++ * Render page ++ * + * @param string $template + * @return string + */ +@@ -449,7 +483,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -460,18 +494,20 @@ class Processor + * @param array $reportData + * @return string + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData): string + { +- $this->reportData = $reportData; +- $this->reportId = abs(intval(microtime(true) * random_int(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } ++ $this->_setReportData($reportData); + +- @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData)); ++ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData). PHP_EOL); + + if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) { + $this->_setSkin($reportData['skin']); +@@ -484,20 +520,117 @@ class Processor + /** + * Get report + * +- * @param int $reportId ++ * @param string $reportId + * @return void +- * @SuppressWarnings(PHPMD.ExitExpression) + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile(string $reportId): string ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; + } +- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } ++ ++ /** ++ * Redirect to a base url ++ * @return void ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid(string $reportId): bool ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; ++ } ++ return $reportDirPath . $reportId; ++ } ++ ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel(string $reportId): int ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = $_ENV[$envName] ?? getenv($envName); ++ if(false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue= $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel(string $reportId): int ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); + } + + /** +@@ -511,11 +644,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch new file mode 100644 index 00000000..9eedde78 --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch @@ -0,0 +1,1006 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -47,6 +47,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,284 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ): bool { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception): string ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ): bool { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception): bool ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ // phpcs:ignore Magento2.Exceptions.DirectThrow ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -101,30 +96,15 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated 100.1.0 +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws LocalizedException|\InvalidArgumentException + */ + public function launch() + { +@@ -143,6 +123,9 @@ class Http implements \Magento\Framework\AppInterface + } else { + throw new \InvalidArgumentException('Invalid return type'); + } ++ if ($this->_request->isHead() && $this->_response->getHttpResponseCode() == 200) { ++ $this->handleHeadRequest(); ++ } + // This event gives possibility to launch something before sending output (allow cookie setting) + $eventParams = ['request' => $this->_request, 'response' => $this->_response]; + $this->_eventManager->dispatch('controller_front_send_response_before', $eventParams); +@@ -150,192 +133,26 @@ class Http implements \Magento\Framework\AppInterface + } + + /** +- * @inheritdoc +- */ +- public function catchException(Bootstrap $bootstrap, \Exception $exception) +- { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) +- { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception ++ * Handle HEAD requests by adding the Content-Length header and removing the body from the response. + * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- Debug::trace( +- $exception->getTrace(), +- true, +- true, +- (bool)getenv('MAGE_DEBUG_SHOW_ARGS') +- ) +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception + * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception &$exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool + */ +- private function handleSessionException(\Exception $exception) ++ private function handleHeadRequest() + { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; ++ // It is possible that some PHP installations have overloaded strlen to use mb_strlen instead. ++ // This means strlen might return the actual number of characters in a non-ascii string instead ++ // of the number of bytes. Use mb_strlen explicitly with a single byte character encoding to ensure ++ // that the content length is calculated in bytes. ++ $contentLength = mb_strlen($this->_response->getContent(), '8bit'); ++ $this->_response->clearBody(); ++ $this->_response->setHeader('Content-Length', $contentLength); + } + + /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool ++ * @inheritdoc + */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) ++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool + { +- $reportData = [ +- $exception->getMessage(), +- Debug::trace( +- $exception->getTrace(), +- true, +- true, +- (bool)getenv('MAGE_DEBUG_SHOW_ARGS') +- ) +- ]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -3,14 +3,21 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++declare(strict_types=1); ++ + namespace Magento\Framework\Error; + + use Magento\Framework\Serialize\Serializer\Json; ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor + * + * @SuppressWarnings(PHPMD.TooManyFields) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) ++ * phpcs:ignoreFile + */ + class Processor + { +@@ -18,6 +25,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -64,7 +72,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -125,7 +133,7 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + +@@ -137,15 +145,25 @@ class Processor + private $serializer; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response + * @param Json $serializer ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null) +- { ++ public function __construct( ++ Http $response, ++ Json $serializer = null, ++ Escaper $escaper = null ++ ) { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; +- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); ++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -155,11 +173,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -167,6 +180,9 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -368,6 +384,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -464,7 +483,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -475,18 +494,20 @@ class Processor + * @param array $reportData + * @return string + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData): string + { +- $this->reportData = $reportData; +- $this->reportId = abs((int)(microtime(true) * random_int(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } ++ $this->_setReportData($reportData); + +- @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData)); ++ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData). PHP_EOL); + + if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) { + $this->_setSkin($reportData['skin']); +@@ -499,20 +520,117 @@ class Processor + /** + * Get report + * +- * @param int $reportId ++ * @param string $reportId + * @return void +- * @SuppressWarnings(PHPMD.ExitExpression) + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile(string $reportId): string ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } + } +- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ throw new \RuntimeException("Report file not found"); ++ } ++ ++ /** ++ * Redirect to a base url ++ * @return void ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid(string $reportId): bool ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; ++ } ++ return $reportDirPath . $reportId; ++ } ++ ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel(string $reportId): int ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = $_ENV[$envName] ?? getenv($envName); ++ if(false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; ++ } ++ $value = (int)$value; ++ $maxValue= $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel(string $reportId): int ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); + } + + /** +@@ -526,11 +644,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch new file mode 100644 index 00000000..a486cd93 --- /dev/null +++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch @@ -0,0 +1,982 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -47,6 +47,7 @@ + + + ++ + + + system/currency/installed +diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandler.php +@@ -0,0 +1,284 @@ ++encryptor = $encryptor; ++ $this->filesystem = $filesystem; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Handles exception of HTTP web application ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ public function handle( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response) ++ || $this->handleBootstrapErrors($bootstrap, $exception, $response) ++ || $this->handleSessionException($exception, $response, $request) ++ || $this->handleInitException($exception) ++ || $this->handleGenericReport($bootstrap, $exception); ++ return $result; ++ } ++ ++ /** ++ * Error handler for developer mode ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleDeveloperMode( ++ Bootstrap $bootstrap, ++ \Exception $exception, ++ ResponseHttp $response ++ ): bool { ++ if ($bootstrap->isDeveloperMode()) { ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ $response->setHttpResponseCode(500); ++ $response->setHeader('Content-Type', 'text/plain'); ++ $response->setBody($this->buildContentFromException($exception)); ++ $response->sendResponse(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Build content based on an exception ++ * ++ * @param \Exception $exception ++ * @return string ++ */ ++ private function buildContentFromException(\Exception $exception): string ++ { ++ /** @var \Exception[] $exceptions */ ++ $exceptions = []; ++ ++ do { ++ $exceptions[] = $exception; ++ } while ($exception = $exception->getPrevious()); ++ ++ $buffer = sprintf("%d exception(s):\n", count($exceptions)); ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "Exception #%d (%s): %s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage() ++ ); ++ } ++ ++ foreach ($exceptions as $index => $exception) { ++ $buffer .= sprintf( ++ "\nException #%d (%s): %s\n%s\n", ++ $index, ++ get_class($exception), ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ true, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ); ++ } ++ ++ return $buffer; ++ } ++ ++ /** ++ * Handler for bootstrap errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return bool ++ */ ++ private function handleBootstrapErrors( ++ Bootstrap $bootstrap, ++ \Exception &$exception, ++ ResponseHttp $response ++ ): bool { ++ $bootstrapCode = $bootstrap->getErrorCode(); ++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/503.php'); ++ return true; ++ } ++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { ++ try { ++ $this->redirectToSetup($bootstrap, $exception, $response); ++ return true; ++ } catch (\Exception $e) { ++ $exception = $e; ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for session errors ++ * ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @param RequestHttp $request ++ * @return bool ++ */ ++ private function handleSessionException( ++ \Exception $exception, ++ ResponseHttp $response, ++ RequestHttp $request ++ ): bool { ++ if ($exception instanceof SessionException) { ++ $response->setRedirect($request->getDistroBaseUrl()); ++ $response->sendHeaders(); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handler for application initialization errors ++ * ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleInitException(\Exception $exception): bool ++ { ++ if ($exception instanceof InitException) { ++ $this->logger->critical($exception); ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/404.php'); ++ return true; ++ } ++ return false; ++ } ++ ++ /** ++ * Handle for any other errors ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @return bool ++ */ ++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool ++ { ++ $reportData = [ ++ $exception->getMessage(), ++ Debug::trace( ++ $exception->getTrace(), ++ true, ++ false, ++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS') ++ ) ++ ]; ++ $params = $bootstrap->getParams(); ++ if (isset($params['REQUEST_URI'])) { ++ $reportData['url'] = $params['REQUEST_URI']; ++ } ++ if (isset($params['SCRIPT_NAME'])) { ++ $reportData['script_name'] = $params['SCRIPT_NAME']; ++ } ++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); ++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); ++ // phpcs:ignore Magento2.Security.IncludeFile ++ require $this->filesystem ++ ->getDirectoryRead(DirectoryList::PUB) ++ ->getAbsolutePath('errors/report.php'); ++ return true; ++ } ++ ++ /** ++ * If not installed, try to redirect to installation wizard ++ * ++ * @param Bootstrap $bootstrap ++ * @param \Exception $exception ++ * @param ResponseHttp $response ++ * @return void ++ * @throws \Exception ++ */ ++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) ++ { ++ $setupInfo = new SetupInfo($bootstrap->getParams()); ++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); ++ if ($setupInfo->isAvailable()) { ++ $response->setRedirect($setupInfo->getUrl()); ++ $response->sendHeaders(); ++ } else { ++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " ++ . "because the Magento setup directory cannot be accessed. \n" ++ . 'You can install Magento using either the command line or you must restore access ' ++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; ++ // phpcs:ignore Magento2.Exceptions.DirectThrow ++ throw new \Exception($newMessage, 0, $exception); ++ } ++ } ++} +diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php +--- /dev/null ++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php +@@ -0,0 +1,31 @@ ++_objectManager = $objectManager; + $this->_eventManager = $eventManager; +@@ -102,30 +96,15 @@ class Http implements \Magento\Framework\AppInterface + $this->_response = $response; + $this->_configLoader = $configLoader; + $this->_state = $state; +- $this->_filesystem = $filesystem; + $this->registry = $registry; +- } +- +- /** +- * Add new dependency +- * +- * @return \Psr\Log\LoggerInterface +- * +- * @deprecated 100.1.0 +- */ +- private function getLogger() +- { +- if (!$this->logger instanceof \Psr\Log\LoggerInterface) { +- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); +- } +- return $this->logger; ++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); + } + + /** + * Run application + * +- * @throws \InvalidArgumentException + * @return ResponseInterface ++ * @throws LocalizedException|\InvalidArgumentException + */ + public function launch() + { +@@ -172,193 +151,8 @@ class Http implements \Magento\Framework\AppInterface + /** + * @inheritdoc + */ +- public function catchException(Bootstrap $bootstrap, \Exception $exception) +- { +- $result = $this->handleDeveloperMode($bootstrap, $exception) +- || $this->handleBootstrapErrors($bootstrap, $exception) +- || $this->handleSessionException($exception) +- || $this->handleInitException($exception) +- || $this->handleGenericReport($bootstrap, $exception); +- return $result; +- } +- +- /** +- * Error handler for developer mode +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) ++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool + { +- if ($bootstrap->isDeveloperMode()) { +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- $this->_response->setHttpResponseCode(500); +- $this->_response->setHeader('Content-Type', 'text/plain'); +- $this->_response->setBody($this->buildContentFromException($exception)); +- $this->_response->sendResponse(); +- return true; +- } +- return false; +- } +- +- /** +- * Build content based on an exception +- * +- * @param \Exception $exception +- * @return string +- */ +- private function buildContentFromException(\Exception $exception) +- { +- /** @var \Exception[] $exceptions */ +- $exceptions = []; +- +- do { +- $exceptions[] = $exception; +- } while ($exception = $exception->getPrevious()); +- +- $buffer = sprintf("%d exception(s):\n", count($exceptions)); +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); +- } +- +- foreach ($exceptions as $index => $exception) { +- $buffer .= sprintf( +- "\nException #%d (%s): %s\n%s\n", +- $index, +- get_class($exception), +- $exception->getMessage(), +- Debug::trace( +- $exception->getTrace(), +- true, +- true, +- (bool)getenv('MAGE_DEBUG_SHOW_ARGS') +- ) +- ); +- } +- +- return $buffer; +- } +- +- /** +- * If not installed, try to redirect to installation wizard +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return void +- * @throws \Exception +- */ +- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) +- { +- $setupInfo = new SetupInfo($bootstrap->getParams()); +- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); +- if ($setupInfo->isAvailable()) { +- $this->_response->setRedirect($setupInfo->getUrl()); +- $this->_response->sendHeaders(); +- } else { +- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " +- . "because the Magento setup directory cannot be accessed. \n" +- . 'You can install Magento using either the command line or you must restore access ' +- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; +- // phpcs:ignore Magento2.Exceptions.DirectThrow +- throw new \Exception($newMessage, 0, $exception); +- } +- } +- +- /** +- * Handler for bootstrap errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) +- { +- $bootstrapCode = $bootstrap->getErrorCode(); +- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { +- // phpcs:ignore Magento2.Security.IncludeFile +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); +- return true; +- } +- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { +- try { +- $this->redirectToSetup($bootstrap, $exception); +- return true; +- } catch (\Exception $e) { +- $exception = $e; +- } +- } +- return false; +- } +- +- /** +- * Handler for session errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleSessionException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\SessionException) { +- $this->_response->setRedirect($this->_request->getDistroBaseUrl()); +- $this->_response->sendHeaders(); +- return true; +- } +- return false; +- } +- +- /** +- * Handler for application initialization errors +- * +- * @param \Exception $exception +- * @return bool +- */ +- private function handleInitException(\Exception $exception) +- { +- if ($exception instanceof \Magento\Framework\Exception\State\InitException) { +- $this->getLogger()->critical($exception); +- // phpcs:ignore Magento2.Security.IncludeFile +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); +- return true; +- } +- return false; +- } +- +- /** +- * Handle for any other errors +- * +- * @param Bootstrap $bootstrap +- * @param \Exception $exception +- * @return bool +- */ +- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) +- { +- $reportData = [ +- $exception->getMessage(), +- Debug::trace( +- $exception->getTrace(), +- true, +- true, +- (bool)getenv('MAGE_DEBUG_SHOW_ARGS') +- ) +- ]; +- $params = $bootstrap->getParams(); +- if (isset($params['REQUEST_URI'])) { +- $reportData['url'] = $params['REQUEST_URI']; +- } +- if (isset($params['SCRIPT_NAME'])) { +- $reportData['script_name'] = $params['SCRIPT_NAME']; +- } +- // phpcs:ignore Magento2.Security.IncludeFile +- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); +- return true; ++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); + } + } +diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample +--- a/pub/errors/local.xml.sample ++++ b/pub/errors/local.xml.sample +@@ -27,5 +27,22 @@ + value "delete" is for cleaning + --> + leave ++ ++ 0 + + +diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php +--- a/pub/errors/processor.php ++++ b/pub/errors/processor.php +@@ -8,11 +8,15 @@ declare(strict_types=1); + namespace Magento\Framework\Error; + + use Magento\Framework\Serialize\Serializer\Json; ++use Magento\Framework\Escaper; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\App\Response\Http; + + /** + * Error processor + * + * @SuppressWarnings(PHPMD.TooManyFields) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * phpcs:ignoreFile + */ + class Processor +@@ -21,6 +25,7 @@ class Processor + const MAGE_ERRORS_DESIGN_XML = 'design.xml'; + const DEFAULT_SKIN = 'default'; + const ERROR_DIR = 'pub/errors'; ++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; + + /** + * Page title +@@ -67,7 +72,7 @@ class Processor + /** + * Report ID + * +- * @var int ++ * @var string + */ + public $reportId; + +@@ -128,7 +133,7 @@ class Processor + /** + * Http response + * +- * @var \Magento\Framework\App\Response\Http ++ * @var Http + */ + protected $_response; + +@@ -140,15 +145,25 @@ class Processor + private $serializer; + + /** +- * @param \Magento\Framework\App\Response\Http $response ++ * @var Escaper ++ */ ++ private $escaper; ++ ++ /** ++ * @param Http $response + * @param Json $serializer ++ * @param Escaper $escaper + */ +- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null) +- { ++ public function __construct( ++ Http $response, ++ Json $serializer = null, ++ Escaper $escaper = null ++ ) { + $this->_response = $response; + $this->_errorDir = __DIR__ . '/'; + $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; +- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); ++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + + if (!empty($_SERVER['SCRIPT_NAME'])) { + if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { +@@ -158,11 +173,6 @@ class Processor + } + } + +- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; +- if ($reportId) { +- $this->loadReport($reportId); +- } +- + $this->_indexDir = $this->_getIndexDir(); + $this->_root = is_dir($this->_indexDir . 'app'); + +@@ -170,6 +180,9 @@ class Processor + if (isset($_GET['skin'])) { + $this->_setSkin($_GET['skin']); + } ++ if (isset($_GET['id'])) { ++ $this->loadReport($_GET['id']); ++ } + } + + /** +@@ -371,6 +384,9 @@ class Processor + if ((string)$local->report->trash) { + $config->trash = $local->report->trash; + } ++ if ($local->report->dir_nesting_level) { ++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level; ++ } + if ((string)$local->skin) { + $this->_setSkin((string)$local->skin, $config); + } +@@ -467,7 +483,7 @@ class Processor + $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; + } + +- if ($this->reportData['script_name']) { ++ if (isset($this->reportData['script_name'])) { + $this->_scriptName = $this->reportData['script_name']; + } + } +@@ -478,18 +494,20 @@ class Processor + * @param array $reportData + * @return string + */ +- public function saveReport($reportData) ++ public function saveReport(array $reportData): string + { +- $this->reportData = $reportData; +- $this->reportId = abs((int)(microtime(true) * random_int(100, 1000))); +- $this->_reportFile = $this->_reportDir . '/' . $this->reportId; +- $this->_setReportData($reportData); +- +- if (!file_exists($this->_reportDir)) { +- @mkdir($this->_reportDir, 0777, true); ++ $this->reportId = $reportData['report_id']; ++ $this->_reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($this->reportId), ++ $this->reportId ++ ); ++ $reportDirName = dirname($this->_reportFile); ++ if (!file_exists($reportDirName)) { ++ @mkdir($reportDirName, 0777, true); + } ++ $this->_setReportData($reportData); + +- @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData)); ++ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData). PHP_EOL); + + if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) { + $this->_setSkin($reportData['skin']); +@@ -502,19 +520,117 @@ class Processor + /** + * Get report + * +- * @param int $reportId ++ * @param string $reportId + * @return void + */ + public function loadReport($reportId) + { +- $this->reportId = $reportId; +- $this->_reportFile = $this->_reportDir . '/' . $reportId; ++ try { ++ if (!$this->isReportIdValid($reportId)) { ++ throw new \RuntimeException("Report Id is invalid"); ++ } ++ $reportFile = $this->findReportFile($reportId); ++ if (!is_readable($reportFile)) { ++ throw new \RuntimeException("Report file cannot be read"); ++ } ++ $this->reportId = $reportId; ++ $this->_reportFile = $reportFile; ++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ } catch (\RuntimeException $e) { ++ $this->redirectToBaseUrl(); ++ } ++ } ++ ++ /** ++ * Searches for the report file and returns the path to it ++ * ++ * @param string $reportId ++ * @return string ++ * @throws \RuntimeException ++ */ ++ private function findReportFile(string $reportId): string ++ { ++ $reportFile = $this->getReportPath( ++ $this->getReportDirNestingLevel($reportId), ++ $reportId ++ ); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); ++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { ++ $reportFile = $this->getReportPath($i, $reportId); ++ if (file_exists($reportFile)) { ++ return $reportFile; ++ } ++ } ++ throw new \RuntimeException("Report file not found"); ++ } ++ ++ /** ++ * Redirect to a base url ++ * @return void ++ */ ++ private function redirectToBaseUrl() ++ { ++ header("Location: " . $this->getBaseUrl()); ++ die(); ++ } ++ ++ /** ++ * Checks report id ++ * ++ * @param string $reportId ++ * @return bool ++ */ ++ private function isReportIdValid(string $reportId): bool ++ { ++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); ++ } ++ ++ /** ++ * Get path to reports ++ * ++ * @param integer $reportDirNestingLevel ++ * @param string $reportId ++ * @return string ++ */ ++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string ++ { ++ $reportDirPath = $this->_reportDir; ++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { ++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; ++ } ++ return $reportDirPath . $reportId; ++ } + +- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { +- header("Location: " . $this->getBaseUrl()); +- die(); ++ /** ++ * Returns nesting Level for the report files ++ * ++ * @var $reportId ++ * @return int ++ */ ++ private function getReportDirNestingLevel(string $reportId): int ++ { ++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; ++ $value = $_ENV[$envName] ?? getenv($envName); ++ if(false === $value && property_exists($this->_config, 'dir_nesting_level')) { ++ $value = $this->_config->dir_nesting_level; + } +- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); ++ $value = (int)$value; ++ $maxValue= $this->getMaxReportDirNestingLevel($reportId); ++ return 0 < $value && $maxValue >= $value ? $value : 0; ++ } ++ ++ /** ++ * Returns maximum nesting level directories of report files ++ * ++ * @param string $reportId ++ * @return integer ++ */ ++ private function getMaxReportDirNestingLevel(string $reportId): int ++ { ++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); + } + + /** +@@ -528,11 +644,16 @@ class Processor + { + $this->pageTitle = 'Error Submission Form'; + +- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; +- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; +- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; +- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; +- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; ++ $this->postData['firstName'] = (isset($_POST['firstname'])) ++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; ++ $this->postData['lastName'] = (isset($_POST['lastname'])) ++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; ++ $this->postData['email'] = (isset($_POST['email'])) ++ ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; ++ $this->postData['telephone'] = (isset($_POST['telephone'])) ++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; ++ $this->postData['comment'] = (isset($_POST['comment'])) ++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; + + if (isset($_POST['submit'])) { + if ($this->_validate()) { diff --git a/patches/MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch b/patches/MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch new file mode 100644 index 00000000..85eff360 --- /dev/null +++ b/patches/MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch @@ -0,0 +1,12 @@ +diff -Naur a/vendor/vertex/module-tax/Model/FlexField/Processor/OrderCurrencyGetterProcessor.php b/vendor/vertex/module-tax/Model/FlexField/Processor/OrderCurrencyGetterProcessor.php +--- a/vendor/vertex/module-tax/Model/FlexField/Processor/OrderCurrencyGetterProcessor.php ++++ b/vendor/vertex/module-tax/Model/FlexField/Processor/OrderCurrencyGetterProcessor.php +@@ -4,7 +4,7 @@ + * @author Mediotype https://www.mediotype.com/ + */ + +-namespace Vertex\Tax\Model\Flexfield\Processor; ++namespace Vertex\Tax\Model\FlexField\Processor; + + use Magento\Framework\Exception\NoSuchEntityException; + use Magento\Quote\Api\CartRepositoryInterface; From 93cc54f1905d2a89b0056fdde569956569e39b98 Mon Sep 17 00:00:00 2001 From: Oleksandr Shmyheliuk Date: Mon, 11 Nov 2019 13:21:23 -0600 Subject: [PATCH 007/157] MAGECLOUD-4649: Merge PageBuilder Security Patches to m-cloud-patches (#6) --- patches.json | 4 + ...22979__fix_pagebuilder_module__2.3.1.patch | 177 ++++++++++++++++++ ...22979__fix_pagebuilder_module__2.3.2.patch | 177 ++++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch create mode 100644 patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch diff --git a/patches.json b/patches.json index f81ca123..0b372922 100644 --- a/patches.json +++ b/patches.json @@ -201,6 +201,10 @@ "2.3.0": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch", "2.3.1": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch", "2.3.2 - 2.3.3": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch" + }, + "Fix pagebuilder module": { + "2.3.1": "MDVA-22979__fix_pagebuilder_module__2.3.1.patch", + "2.3.2": "MDVA-22979__fix_pagebuilder_module__2.3.2.patch" } }, "monolog/monolog": { diff --git a/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch b/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch new file mode 100644 index 00000000..231fe7e3 --- /dev/null +++ b/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch @@ -0,0 +1,177 @@ +diff -Nuar a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php +index 5f22a36510c..7f3fe8e91eb 100644 +--- a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php ++++ b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php +@@ -53,19 +53,26 @@ class Preview extends \Magento\Backend\Block\Widget + * Prepare html output + * + * @return string ++ * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _toHtml() + { ++ $request = $this->getRequest(); ++ ++ if (!$request instanceof \Magento\Framework\App\RequestSafetyInterface || !$request->isSafeMethod()) { ++ throw new \Magento\Framework\Exception\LocalizedException(__('Wrong request.')); ++ } ++ + $storeId = $this->getAnyStoreView()->getId(); + /** @var $template \Magento\Email\Model\Template */ + $template = $this->_emailFactory->create(); + +- if ($id = (int)$this->getRequest()->getParam('id')) { ++ if ($id = (int)$request->getParam('id')) { + $template->load($id); + } else { +- $template->setTemplateType($this->getRequest()->getParam('type')); +- $template->setTemplateText($this->getRequest()->getParam('text')); +- $template->setTemplateStyles($this->getRequest()->getParam('styles')); ++ $template->setTemplateType($request->getParam('type')); ++ $template->setTemplateText($request->getParam('text')); ++ $template->setTemplateStyles($request->getParam('styles')); + } + + $template->setTemplateText($this->_maliciousCode->filter($template->getTemplateText())); +diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +index b7d59ecc8..09f8e8510 100644 +--- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php ++++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +@@ -26,19 +26,28 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc + */ + private $rendererPool; + ++ /** ++ * @var \Magento\Backend\Model\Auth ++ */ ++ private $auth; ++ + /** + * Constructor + * + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool ++ * @param \Magento\Backend\Model\Auth $auth + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, +- \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool ++ \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool, ++ \Magento\Backend\Model\Auth $auth = null + ) { + parent::__construct($context); + + $this->rendererPool = $rendererPool; ++ $this->auth = $auth ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\Backend\Model\Auth::class); + } + + /** +@@ -48,14 +57,18 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc + */ + public function execute() + { +- $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); +- // Some template filters and directive processors expect this to be called in order to function. +- $pageResult->initLayout(); ++ if ($this->auth->isLoggedIn()) { ++ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); ++ // Some template filters and directive processors expect this to be called in order to function. ++ $pageResult->initLayout(); ++ ++ $params = $this->getRequest()->getParams(); ++ $renderer = $this->rendererPool->getRenderer($params['role']); ++ $result = ['data' => $renderer->render($params)]; + +- $params = $this->getRequest()->getParams(); +- $renderer = $this->rendererPool->getRenderer($params['role']); +- $result = ['data' => $renderer->render($params)]; ++ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ } + +- return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ $this->_forward('noroute'); + } + } +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php +index 17288978e..baf3ce106 100644 +--- a/vendor/magento/module-page-builder/Model/Stage/Config.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Config.php +@@ -135,7 +135,9 @@ class Config + 'content_types' => $this->getContentTypes(), + 'stage_config' => $this->data, + 'media_url' => $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]), +- 'preview_url' => $this->frontendUrlBuilder->getUrl('pagebuilder/contenttype/preview'), ++ 'preview_url' => $this->frontendUrlBuilder ++ ->addSessionParam() ++ ->getUrl('pagebuilder/contenttype/preview'), + 'column_grid_default' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_DEFAULT), + 'column_grid_max' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_MAX), + 'can_use_inline_editing_on_stage' => $this->isWysiwygProvisionedForEditingOnStage(), +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php +new file mode 100644 +index 000000000..a1e9d943a +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php +@@ -0,0 +1,49 @@ ++request = $request; ++ } ++ ++ /** ++ * Get Sid for pagebuilder preview ++ * ++ * @param \Magento\Framework\Session\SidResolver $subject ++ * @param string|null $result ++ * @param \Magento\Framework\Session\SessionManagerInterface $session ++ * ++ * @return string|null ++ */ ++ public function afterGetSid( ++ \Magento\Framework\Session\SidResolver $subject, ++ $result, ++ \Magento\Framework\Session\SessionManagerInterface $session ++ ) { ++ if (strpos($this->request->getPathInfo(), '/pagebuilder/contenttype/preview') === 0) { ++ return $this->request->getQuery( ++ $subject->getSessionIdQueryParam($session) ++ ); ++ } ++ ++ return $result; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml +index a147ab1b2..e7374870b 100644 +--- a/vendor/magento/module-page-builder/etc/di.xml ++++ b/vendor/magento/module-page-builder/etc/di.xml +@@ -140,4 +140,7 @@ + + + ++ ++ ++ + diff --git a/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch b/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch new file mode 100644 index 00000000..f6de20e8 --- /dev/null +++ b/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch @@ -0,0 +1,177 @@ +diff -Nuar a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php +index acc367de742..4f0479a9573 100644 +--- a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php ++++ b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php +@@ -55,19 +55,26 @@ class Preview extends \Magento\Backend\Block\Widget + * Prepare html output + * + * @return string ++ * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _toHtml() + { ++ $request = $this->getRequest(); ++ ++ if (!$request instanceof \Magento\Framework\App\RequestSafetyInterface || !$request->isSafeMethod()) { ++ throw new \Magento\Framework\Exception\LocalizedException(__('Wrong request.')); ++ } ++ + $storeId = $this->getAnyStoreView()->getId(); + /** @var $template \Magento\Email\Model\Template */ + $template = $this->_emailFactory->create(); + +- if ($id = (int)$this->getRequest()->getParam('id')) { ++ if ($id = (int)$request->getParam('id')) { + $template->load($id); + } else { +- $template->setTemplateType($this->getRequest()->getParam('type')); +- $template->setTemplateText($this->getRequest()->getParam('text')); +- $template->setTemplateStyles($this->getRequest()->getParam('styles')); ++ $template->setTemplateType($request->getParam('type')); ++ $template->setTemplateText($request->getParam('text')); ++ $template->setTemplateStyles($request->getParam('styles')); + } + + \Magento\Framework\Profiler::start($this->profilerName); +diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +index b7d59ecc8..09f8e8510 100644 +--- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php ++++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +@@ -26,19 +26,28 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc + */ + private $rendererPool; + ++ /** ++ * @var \Magento\Backend\Model\Auth ++ */ ++ private $auth; ++ + /** + * Constructor + * + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool ++ * @param \Magento\Backend\Model\Auth $auth + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, +- \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool ++ \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool, ++ \Magento\Backend\Model\Auth $auth = null + ) { + parent::__construct($context); + + $this->rendererPool = $rendererPool; ++ $this->auth = $auth ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\Backend\Model\Auth::class); + } + + /** +@@ -48,14 +57,18 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc + */ + public function execute() + { +- $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); +- // Some template filters and directive processors expect this to be called in order to function. +- $pageResult->initLayout(); ++ if ($this->auth->isLoggedIn()) { ++ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); ++ // Some template filters and directive processors expect this to be called in order to function. ++ $pageResult->initLayout(); ++ ++ $params = $this->getRequest()->getParams(); ++ $renderer = $this->rendererPool->getRenderer($params['role']); ++ $result = ['data' => $renderer->render($params)]; + +- $params = $this->getRequest()->getParams(); +- $renderer = $this->rendererPool->getRenderer($params['role']); +- $result = ['data' => $renderer->render($params)]; ++ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ } + +- return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ $this->_forward('noroute'); + } + } +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php +index 17288978e..baf3ce106 100644 +--- a/vendor/magento/module-page-builder/Model/Stage/Config.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Config.php +@@ -135,7 +135,9 @@ class Config + 'content_types' => $this->getContentTypes(), + 'stage_config' => $this->data, + 'media_url' => $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]), +- 'preview_url' => $this->frontendUrlBuilder->getUrl('pagebuilder/contenttype/preview'), ++ 'preview_url' => $this->frontendUrlBuilder ++ ->addSessionParam() ++ ->getUrl('pagebuilder/contenttype/preview'), + 'column_grid_default' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_DEFAULT), + 'column_grid_max' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_MAX), + 'can_use_inline_editing_on_stage' => $this->isWysiwygProvisionedForEditingOnStage(), +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php +new file mode 100644 +index 000000000..a1e9d943a +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php +@@ -0,0 +1,49 @@ ++request = $request; ++ } ++ ++ /** ++ * Get Sid for pagebuilder preview ++ * ++ * @param \Magento\Framework\Session\SidResolver $subject ++ * @param string|null $result ++ * @param \Magento\Framework\Session\SessionManagerInterface $session ++ * ++ * @return string|null ++ */ ++ public function afterGetSid( ++ \Magento\Framework\Session\SidResolver $subject, ++ $result, ++ \Magento\Framework\Session\SessionManagerInterface $session ++ ) { ++ if (strpos($this->request->getPathInfo(), '/pagebuilder/contenttype/preview') === 0) { ++ return $this->request->getQuery( ++ $subject->getSessionIdQueryParam($session) ++ ); ++ } ++ ++ return $result; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml +index a147ab1b2..e7374870b 100644 +--- a/vendor/magento/module-page-builder/etc/di.xml ++++ b/vendor/magento/module-page-builder/etc/di.xml +@@ -140,4 +140,7 @@ + + + ++ ++ ++ + From 2c776c0aa2b796b33564acc0b38cbe9e69e82843 Mon Sep 17 00:00:00 2001 From: Oleksandr Shmyheliuk Date: Wed, 13 Nov 2019 11:42:56 -0600 Subject: [PATCH 008/157] MAGECLOUD-4661: Security-only Releases Broke Cloud Patching Process (#7) --- patches.json | 10 +- ...__fix_session_manager_locking__2.2.0.patch | 1 - ...overhaul_cron_implementation__2.1.13.patch | 1 - ...overhaul_cron_implementation__2.1.14.patch | 1 - ..._overhaul_cron_implementation__2.1.4.patch | 1 - ..._overhaul_cron_implementation__2.1.5.patch | 1 - ..._overhaul_cron_implementation__2.2.0.patch | 2 - ..._overhaul_cron_implementation__2.2.2.patch | 1 - ...respect_minification_override__2.1.4.patch | 3 +- ...respect_minification_override__2.2.0.patch | 3 +- ...event_deadlock_during_db_dump__2.2.0.patch | 3 +- ...le_editing_when_scd_on_demand__2.2.0.patch | 2 +- ..._run_cron_when_it_is_disabled__2.1.4.patch | 1 - ...lation_without_admin_creation__2.2.2.patch | 2 +- ...793__fix_monolog_slack_handler_2.1.x.patch | 2 - ...850_fix_amazon_payment_module__2.2.6.patch | 1 - ...ECLOUD-3611__multi_thread_scd__2.2.0.patch | 1 - ...ECLOUD-3611__multi_thread_scd__2.2.4.patch | 1 - ...ECLOUD-3611__multi_thread_scd__2.3.0.patch | 1 - ...ECLOUD-3611__multi_thread_scd__2.3.2.patch | 1 - ...mer_runners_on_cloud_clusters__2.3.0.patch | 2 +- ...mer_runners_on_cloud_clusters__2.3.1.patch | 2 +- ...bility_of_new_mail_interfaces__2.3.3.patch | 100 ++---------------- ..._avoid_nonexistent_setup_area__2.1.4.patch | 1 - ...__move_vendor_path_autoloader__2.1.4.patch | 2 - ...atic_assets_without_rewrites__2.1.17.patch | 1 - ...tatic_assets_without_rewrites__2.1.4.patch | 1 - ...ent_excessive_js_optimization__2.1.4.patch | 15 +-- ...7097__fix_credis_pipeline_bug__2.1.4.patch | 1 - ..._image_resizing_after_upgrade__2.1.6.patch | 8 -- ...ort_credis_forking_during_scd__2.1.4.patch | 2 - ...ort_credis_forking_during_scd__2.2.0.patch | 2 - ...-84444__fix_mview_on_staging__2.1.10.patch | 13 --- ...O-84444__fix_mview_on_staging__2.1.4.patch | 13 --- ...O-84444__fix_mview_on_staging__2.1.5.patch | 13 --- ...8833__turn_off_google_chart_api__2.x.patch | 3 +- patches/MC-5964__preauth_sql__2.2.0.patch | 1 - ...22979__fix_pagebuilder_module__2.3.1.patch | 5 - ...22979__fix_pagebuilder_module__2.3.2.patch | 5 - ...y_encode_characters_in_emails__2.1.4.patch | 1 - ...x_oom_during_customer_import__2.1.11.patch | 2 - ...ix_oom_during_customer_import__2.1.4.patch | 2 - ...ix_oom_during_customer_import__2.2.0.patch | 2 - ...ix_oom_during_customer_import__2.2.4.patch | 2 - 44 files changed, 24 insertions(+), 214 deletions(-) diff --git a/patches.json b/patches.json index 0b372922..34208a33 100644 --- a/patches.json +++ b/patches.json @@ -113,7 +113,7 @@ "2.2.7": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.7.patch", "2.2.8 - 2.2.9": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.2.8.patch", "2.3.0": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch", - "2.3.1 - 2.3.2": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch" + ">=2.3.1 <2.3.3": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch" }, "Resolve Issues with Cron Schedule": { "2.1.10 - 2.1.14 || 2.2.2 - 2.2.5": "MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch" @@ -170,16 +170,16 @@ "2.2.0 - 2.2.3": "MAGECLOUD-3611__multi_thread_scd__2.2.0.patch", "2.2.4 - 2.2.9": "MAGECLOUD-3611__multi_thread_scd__2.2.4.patch", "2.3.0 - 2.3.1": "MAGECLOUD-3611__multi_thread_scd__2.3.0.patch", - "2.3.2": "MAGECLOUD-3611__multi_thread_scd__2.3.2.patch" + ">=2.3.2 <2.3.3": "MAGECLOUD-3611__multi_thread_scd__2.3.2.patch" }, "setup:upgrade returns error code if app:config:import failed": { "2.2.0": "MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.0.patch", "2.2.1 - 2.2.9": "MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.2.1.patch", - "2.3.0 - 2.3.2": "MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.3.0.patch" + ">=2.3.0 <2.3.3": "MAGECLOUD-3806__error_code_fix_for_setup_upgrade__2.3.0.patch" }, "Re-work consumers to terminate as soon as there is nothing left to process": { "2.2.0 - 2.3.1": "MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.2.0.patch", - "2.3.2 - 2.3.3": "MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.3.2.patch" + ">=2.3.2 <2.3.4": "MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.3.2.patch" }, "Backward Compatibility of new Mail Interfaces": { "2.3.3": "MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch" @@ -200,7 +200,7 @@ "~2.2.8": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch", "2.3.0": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch", "2.3.1": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch", - "2.3.2 - 2.3.3": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch" + ">=2.3.2 <2.3.4": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch" }, "Fix pagebuilder module": { "2.3.1": "MDVA-22979__fix_pagebuilder_module__2.3.1.patch", diff --git a/patches/MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch b/patches/MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch index 33635d7f..8e3a5a0d 100644 --- a/patches/MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch +++ b/patches/MAGECLOUD-1582__fix_session_manager_locking__2.2.0.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/framework/Session/SessionManager.php b/vendor/magento/framework/Session/SessionManager.php -index 2cea02f..272d3d9 100644 --- a/vendor/magento/framework/Session/SessionManager.php +++ b/vendor/magento/framework/Session/SessionManager.php @@ -504,18 +504,8 @@ class SessionManager implements SessionManagerInterface diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch index 48905b15..d80236d3 100644 --- a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.13.patch @@ -828,7 +828,6 @@ diff -Naur a/vendor/magento/module-cron/Model/Schedule.php b/vendor/magento/modu self::STATUS_PENDING diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php -index dca4e22..25dd02c 100644 --- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php @@ -8,7 +8,8 @@ namespace Magento\Cron\Model\ResourceModel; diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch index 117d4d74..12742895 100644 --- a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.14.patch @@ -823,7 +823,6 @@ diff -Naur a/vendor/magento/module-cron/Model/Schedule.php b/vendor/magento/modu self::STATUS_PENDING diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php -index dca4e22..25dd02c 100644 --- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php @@ -8,7 +8,8 @@ namespace Magento\Cron\Model\ResourceModel; diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch index be3173c7..5d56e9be 100644 --- a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.4.patch @@ -730,7 +730,6 @@ diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/ } diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php -index dc401e3..25dd02c 100644 --- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php @@ -1,6 +1,6 @@ diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch index 363704e7..acb5856b 100644 --- a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.1.5.patch @@ -837,7 +837,6 @@ diff -Naur a/vendor/magento/module-cron/Model/Schedule.php b/vendor/magento/modu self::STATUS_PENDING diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php -index dca4e22..25dd02c 100644 --- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php @@ -1,6 +1,6 @@ diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch index 17cdb82c..0efbd8a0 100644 --- a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.0.patch @@ -1,5 +1,4 @@ diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php -index f772a6c..d760e92 100644 --- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php @@ -13,6 +13,8 @@ use Magento\Framework\App\State; @@ -595,7 +594,6 @@ index f772a6c..d760e92 100644 } diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php -index a47227b..25dd02c 100644 --- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php +++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php @@ -66,7 +66,14 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb diff --git a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch index 94b780ba..5be5bacb 100644 --- a/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch +++ b/patches/MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch @@ -1,5 +1,4 @@ diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php -index f772a6c..d760e92 100644 --- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php @@ -13,6 +13,8 @@ use Magento\Framework\App\State; diff --git a/patches/MAGECLOUD-1736__respect_minification_override__2.1.4.patch b/patches/MAGECLOUD-1736__respect_minification_override__2.1.4.patch index 3d43d061..fedcbc53 100644 --- a/patches/MAGECLOUD-1736__respect_minification_override__2.1.4.patch +++ b/patches/MAGECLOUD-1736__respect_minification_override__2.1.4.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php -index 82cb8ac51..7997d7cb6 100644 +diff -Nuar a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php --- a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +++ b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php @@ -107,11 +107,15 @@ class TemplateFile extends File diff --git a/patches/MAGECLOUD-1736__respect_minification_override__2.2.0.patch b/patches/MAGECLOUD-1736__respect_minification_override__2.2.0.patch index ee97f396..0c9b345d 100644 --- a/patches/MAGECLOUD-1736__respect_minification_override__2.2.0.patch +++ b/patches/MAGECLOUD-1736__respect_minification_override__2.2.0.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php -index 09f87d878..5ef71afcc 100644 +diff -Nuar a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php --- a/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php +++ b/vendor/magento/framework/View/Design/FileResolution/Fallback/TemplateFile.php @@ -107,9 +107,12 @@ class TemplateFile extends File diff --git a/patches/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch b/patches/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch index 43027043..5e2edaed 100644 --- a/patches/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch +++ b/patches/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php b/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php -index 673d65ec1b2..aa198e265b0 100644 +diff -Nuar a/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php b/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php --- a/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php +++ b/vendor/magento/module-support/Console/Command/AbstractBackupDumpCommand.php @@ -181,7 +181,7 @@ class AbstractBackupDumpCommand extends AbstractBackupCommand diff --git a/patches/MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch b/patches/MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch index 7d62c396..691453dc 100644 --- a/patches/MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch +++ b/patches/MAGECLOUD-2159__unlock_locale_editing_when_scd_on_demand__2.2.0.patch @@ -266,7 +266,7 @@ new file mode 100644 + } +} -diff --git a/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php b/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php +diff -Nuar a/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php b/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php new file mode 100644 --- /dev/null +++ b/vendor/magento/module-config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php diff --git a/patches/MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch b/patches/MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch index 89817a0f..5cd57f71 100644 --- a/patches/MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch +++ b/patches/MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/module-cron/Console/Command/CronCommand.php b/vendor/magento/module-cron/Console/Command/CronCommand.php -index 6a9686c514e..4df6888f461 100644 --- a/vendor/magento/module-cron/Console/Command/CronCommand.php +++ b/vendor/magento/module-cron/Console/Command/CronCommand.php @@ -9,10 +9,12 @@ use Symfony\Component\Console\Command\Command; diff --git a/patches/MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch b/patches/MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch index e88214b7..b6abc8c6 100644 --- a/patches/MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch +++ b/patches/MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch @@ -104,7 +104,7 @@ diff -Nuar a/setup/src/Magento/Setup/Console/Command/InstallCommand.php b/setup/ + } } -diff --git a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php +diff -Nuar a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php --- a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php +++ b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php @@ -15,6 +15,9 @@ use Symfony\Component\Console\Input\InputOption; diff --git a/patches/MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch b/patches/MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch index 0a8ec565..bff44e28 100644 --- a/patches/MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch +++ b/patches/MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php -index 643b003..ac2af02 100644 --- a/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php +++ b/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php @@ -70,15 +70,16 @@ class SlackHandler extends SocketHandler @@ -143,7 +142,6 @@ index 643b003..ac2af02 100644 */ protected function stringify($fields) diff -Nuar a/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php -index a3e7252e..e4c3c37f 100644 --- a/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php +++ b/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php @@ -41,6 +41,15 @@ class SocketHandler extends AbstractProcessingHandler diff --git a/patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch b/patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch index df9b29e1..6e11f288 100644 --- a/patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch +++ b/patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/amzn/amazon-pay-module/etc/di.xml b/vendor/amzn/amazon-pay-module/etc/di.xml -index c954f48..e585eae 100644 --- a/vendor/amzn/amazon-pay-module/etc/di.xml +++ b/vendor/amzn/amazon-pay-module/etc/di.xml @@ -39,24 +39,20 @@ diff --git a/patches/MAGECLOUD-3611__multi_thread_scd__2.2.0.patch b/patches/MAGECLOUD-3611__multi_thread_scd__2.2.0.patch index 171e72d9..71f5d0cd 100644 --- a/patches/MAGECLOUD-3611__multi_thread_scd__2.2.0.patch +++ b/patches/MAGECLOUD-3611__multi_thread_scd__2.2.0.patch @@ -1,5 +1,4 @@ diff -Naur a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php -index e5e10c8f54a..85ef6514432 100644 --- a/vendor/magento/module-deploy/Process/Queue.php +++ b/vendor/magento/module-deploy/Process/Queue.php @@ -291,12 +291,30 @@ class Queue diff --git a/patches/MAGECLOUD-3611__multi_thread_scd__2.2.4.patch b/patches/MAGECLOUD-3611__multi_thread_scd__2.2.4.patch index 5425daa4..d682c4eb 100644 --- a/patches/MAGECLOUD-3611__multi_thread_scd__2.2.4.patch +++ b/patches/MAGECLOUD-3611__multi_thread_scd__2.2.4.patch @@ -1,5 +1,4 @@ diff -Naur a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php -index d8089457ce5b..ca75bf1acb73 100644 --- a/vendor/magento/module-deploy/Process/Queue.php +++ b/vendor/magento/module-deploy/Process/Queue.php @@ -3,14 +3,16 @@ diff --git a/patches/MAGECLOUD-3611__multi_thread_scd__2.3.0.patch b/patches/MAGECLOUD-3611__multi_thread_scd__2.3.0.patch index 3ba98d4b..a28f8208 100644 --- a/patches/MAGECLOUD-3611__multi_thread_scd__2.3.0.patch +++ b/patches/MAGECLOUD-3611__multi_thread_scd__2.3.0.patch @@ -1,5 +1,4 @@ diff -Naur a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php -index e5e10c8f54a..85ef6514432 100644 --- a/vendor/magento/module-deploy/Process/Queue.php +++ b/vendor/magento/module-deploy/Process/Queue.php @@ -291,12 +291,30 @@ class Queue diff --git a/patches/MAGECLOUD-3611__multi_thread_scd__2.3.2.patch b/patches/MAGECLOUD-3611__multi_thread_scd__2.3.2.patch index 4bb7aa18..d37ab608 100644 --- a/patches/MAGECLOUD-3611__multi_thread_scd__2.3.2.patch +++ b/patches/MAGECLOUD-3611__multi_thread_scd__2.3.2.patch @@ -1,5 +1,4 @@ diff -Naur a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php -index d7bb816e61c..c7fe02a4b02 100644 --- a/vendor/magento/module-deploy/Process/Queue.php +++ b/vendor/magento/module-deploy/Process/Queue.php @@ -338,14 +338,37 @@ class Queue diff --git a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch index d231ac6a..b44b45f3 100644 --- a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch +++ b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.0.patch @@ -377,7 +377,7 @@ diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php - return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; - } } -diff --git a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php deleted file mode 100644 --- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +++ /dev/null diff --git a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch index 81659538..91cbfc5f 100644 --- a/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch +++ b/patches/MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch @@ -313,7 +313,7 @@ diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner.php - return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; - } } -diff --git a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +diff -Nuar a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php deleted file mode 100644 --- a/vendor/magento/module-message-queue/Model/Cron/ConsumersRunner/PidConsumerManager.php +++ /dev/null diff --git a/patches/MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch b/patches/MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch index ea6787e2..4418ef9e 100644 --- a/patches/MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch +++ b/patches/MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/module-email/Model/Transport.php b/vendor/magento/module-email/Model/Transport.php -index cbce1682cb5..79ceb56a883 100644 +diff -Nuar a/vendor/magento/module-email/Model/Transport.php b/vendor/magento/module-email/Model/Transport.php --- a/vendor/magento/module-email/Model/Transport.php +++ b/vendor/magento/module-email/Model/Transport.php @@ -9,7 +9,6 @@ namespace Magento\Email\Model; @@ -25,90 +24,7 @@ index cbce1682cb5..79ceb56a883 100644 ScopeConfigInterface $scopeConfig, $parameters = null ) { -diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php -index 32f12dada57..4210312c55a 100644 ---- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php -+++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php -@@ -769,7 +769,7 @@ class AccountTest extends \Magento\TestFramework\TestCase\AbstractController - $message = $this->transportBuilderMock->getSentMessage(); - $rawMessage = $message->getRawMessage(); - -- $this->assertContains('To: John Smith <' . $email . '>', $rawMessage); -+ $this->assertContains('To: =?utf-8?Q?John=20Smith?= <' . $email . '>', $rawMessage); - - $content = $message->getBody()->getParts()[0]->getRawContent(); - $confirmationUrl = $this->getConfirmationUrlFromMessageContent($content); -diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php -index 10a54b4e1b8..186c3e2796c 100644 ---- a/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php -+++ b/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php -@@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; - - /** - * Class EmailMessageTest -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class EmailMessageTest extends TestCase - { -@@ -164,23 +165,42 @@ class EmailMessageTest extends TestCase - 'cc' => $cc, - 'replyTo' => $replyTo, - 'bcc' => $bcc, -- 'sender' => $sender -+ 'sender' => $sender, - ]; - $message = $this->messageFactory->create($data); - - $this->assertContains($content, $message->toString()); - $this->assertContains('Content-Type: ' . $type, $message->toString()); -- $senderString = 'Sender: ' . $sender->getName() . ' <' . $sender->getEmail() . '>'; -+ $senderString = 'Sender: =?utf-8?Q?' -+ . str_replace(' ', '=20', $sender->getName()) -+ . '?= <' -+ . $sender->getEmail() -+ . '>'; - $this->assertContains($senderString, $message->toString()); - $this->assertContains('From: ' . $from[0]->getEmail(), $message->toString()); -- $replyToString = 'Reply-To: ' . $replyTo[0]->getName() . ' <' . $replyTo[0]->getEmail() . '>'; -+ $replyToString = 'Reply-To: =?utf-8?Q?' -+ . str_replace(' ', '=20', $replyTo[0]->getName()) -+ . '?= <' -+ . $replyTo[0]->getEmail() -+ . '>'; - $this->assertContains($replyToString, $message->toString()); -- $toString = 'To: ' . $to[0]->getName() . ' <' . $to[0]->getEmail() . '>'; -+ $toString = 'To: =?utf-8?Q?' -+ . str_replace(' ', '=20', $to[0]->getName()) -+ . '?= <' -+ . $to[0]->getEmail() -+ . '>'; - $this->assertContains($toString, $message->toString()); -- $ccString = 'Cc: ' . $cc[0]->getName() . ' <' . $cc[0]->getEmail() . '>'; -+ $ccString = 'Cc: =?utf-8?Q?' -+ . str_replace(' ', '=20', $cc[0]->getName()) -+ . '?= <' -+ . $cc[0]->getEmail() -+ . '>'; - $this->assertContains($ccString, $message->toString()); - $this->assertContains('Bcc: ' . $bcc[0]->getEmail(), $message->toString()); -- $this->assertContains('Content-Description: ' . $this->description, $message->toString()); -+ $contentDescription = 'Content-Description: =?utf-8?Q?' -+ . str_replace(' ', '=20', $this->description) -+ . '?='; -+ $this->assertContains($contentDescription, $message->toString()); - $this->assertContains('Subject: ' . $this->subject, $message->toString()); - $this->assertContains($content, $message->toString()); - //tests address factory -@@ -235,6 +255,7 @@ class EmailMessageTest extends TestCase - 'body' => $mimeMessage, - 'subject' => $this->subject, - 'to' => [$addressTo], -+ 'encoding' => '', - ]; - $message = $this->messageFactory->create($data); - -diff --git a/vendor/magento/framework/Mail/EmailMessage.php b/vendor/magento/framework/Mail/EmailMessage.php -index aaef9750751..02c75977cd0 100644 +diff -Nuar a/vendor/magento/framework/Mail/EmailMessage.php b/vendor/magento/framework/Mail/EmailMessage.php --- a/vendor/magento/framework/Mail/EmailMessage.php +++ b/vendor/magento/framework/Mail/EmailMessage.php @@ -10,19 +10,13 @@ namespace Magento\Framework\Mail; @@ -301,8 +217,7 @@ index aaef9750751..02c75977cd0 100644 } /** -diff --git a/vendor/magento/framework/Mail/EmailMessageInterface.php b/vendor/magento/framework/Mail/EmailMessageInterface.php -index 95f83ff679c..93eaa4acde3 100644 +diff -Nuar a/vendor/magento/framework/Mail/EmailMessageInterface.php b/vendor/magento/framework/Mail/EmailMessageInterface.php --- a/vendor/magento/framework/Mail/EmailMessageInterface.php +++ b/vendor/magento/framework/Mail/EmailMessageInterface.php @@ -9,7 +9,7 @@ namespace Magento\Framework\Mail; @@ -331,8 +246,7 @@ index 95f83ff679c..93eaa4acde3 100644 /** * Get the string-serialized message body text -diff --git a/vendor/magento/framework/Mail/MailMessageInterface.php b/vendor/magento/framework/Mail/MailMessageInterface.php -index da010be2702..5179e6057c4 100644 +diff -Nuar a/vendor/magento/framework/Mail/MailMessageInterface.php b/vendor/magento/framework/Mail/MailMessageInterface.php --- a/vendor/magento/framework/Mail/MailMessageInterface.php +++ b/vendor/magento/framework/Mail/MailMessageInterface.php @@ -10,6 +10,8 @@ namespace Magento\Framework\Mail; @@ -344,8 +258,7 @@ index da010be2702..5179e6057c4 100644 */ interface MailMessageInterface extends MessageInterface { -diff --git a/vendor/magento/framework/Mail/Message.php b/vendor/magento/framework/Mail/Message.php -index b15b75ca9ac..1f423e80108 100644 +diff -Nuar a/vendor/magento/framework/Mail/Message.php b/vendor/magento/framework/Mail/Message.php --- a/vendor/magento/framework/Mail/Message.php +++ b/vendor/magento/framework/Mail/Message.php @@ -10,13 +10,16 @@ use Zend\Mime\Part; @@ -366,8 +279,7 @@ index b15b75ca9ac..1f423e80108 100644 /** * Message type -diff --git a/vendor/magento/framework/Mail/Template/TransportBuilder.php b/vendor/magento/framework/Mail/Template/TransportBuilder.php -index 4a8d6572faa..2d28e2f560a 100644 +diff -Nuar a/vendor/magento/framework/Mail/Template/TransportBuilder.php b/vendor/magento/framework/Mail/Template/TransportBuilder.php --- a/vendor/magento/framework/Mail/Template/TransportBuilder.php +++ b/vendor/magento/framework/Mail/Template/TransportBuilder.php @@ -87,7 +87,7 @@ class TransportBuilder diff --git a/patches/MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch b/patches/MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch index 443388f1..b775cea9 100644 --- a/patches/MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch +++ b/patches/MAGETWO-45357__avoid_nonexistent_setup_area__2.1.4.patch @@ -1,6 +1,5 @@ Ticket MAGETWO-45357 diff -Nuar a/vendor/magento/framework/App/ObjectManager/ConfigLoader/Compiled.php b/vendor/magento/framework/App/ObjectManager/ConfigLoader/Compiled.php -index 844d3f0..d087d07 100644 --- a/vendor/magento/framework/App/ObjectManager/ConfigLoader/Compiled.php +++ b/vendor/magento/framework/App/ObjectManager/ConfigLoader/Compiled.php @@ -37,6 +37,9 @@ class Compiled implements ConfigLoaderInterface diff --git a/patches/MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch b/patches/MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch index c30379b7..3e9bcc27 100644 --- a/patches/MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch +++ b/patches/MAGETWO-57413__move_vendor_path_autoloader__2.1.4.patch @@ -1,6 +1,5 @@ Ticket MAGETWO-57413 diff -Naur a/app/autoload.php b/app/autoload.php -index b817baf..1d1873d 100644 --- a/app/autoload.php +++ b/app/autoload.php @@ -13,16 +13,7 @@ use Magento\Framework\Autoload\ClassLoaderWrapper; @@ -23,7 +22,6 @@ index b817baf..1d1873d 100644 /* 'composer install' validation */ diff -Naur a/vendor/magento/framework/App/Arguments/FileResolver/Primary.php b/vendor/magento/framework/App/Arguments/FileResolver/Primary.php -index 40b74e9..0f732c9 100644 --- a/vendor/magento/framework/App/Arguments/FileResolver/Primary.php +++ b/vendor/magento/framework/App/Arguments/FileResolver/Primary.php @@ -29,7 +29,7 @@ class Primary implements \Magento\Framework\Config\FileResolverInterface diff --git a/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch b/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch index e397a0d4..a1ced89c 100644 --- a/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch +++ b/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.17.patch @@ -1,6 +1,5 @@ Ticket MAGETWO-57414 diff -Naur a/vendor/magento/framework/App/StaticResource.php b/vendor/magento/framework/App/StaticResource.php -index d591deb..6344322 100644 --- a/vendor/magento/framework/App/StaticResource.php +++ b/vendor/magento/framework/App/StaticResource.php @@ -94,24 +94,40 @@ class StaticResource implements \Magento\Framework\AppInterface diff --git a/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch b/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch index d1d93cc6..2cabbbee 100644 --- a/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch +++ b/patches/MAGETWO-57414__load_static_assets_without_rewrites__2.1.4.patch @@ -1,6 +1,5 @@ Ticket MAGETWO-57414 diff -Naur a/vendor/magento/framework/App/StaticResource.php b/vendor/magento/framework/App/StaticResource.php -index d591deb..6344322 100644 --- a/vendor/magento/framework/App/StaticResource.php +++ b/vendor/magento/framework/App/StaticResource.php @@ -94,24 +94,40 @@ class StaticResource implements \Magento\Framework\AppInterface diff --git a/patches/MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch b/patches/MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch index 856c4529..beb5f10d 100644 --- a/patches/MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch +++ b/patches/MAGETWO-62660__prevent_excessive_js_optimization__2.1.4.patch @@ -1,6 +1,5 @@ -diff --git a/vendor/magento/module-deploy/Model/Deploy/JsDictionaryDeploy.php b/vendor/magento/module-deploy/Model/Deploy/JsDictionaryDeploy.php +diff -Nuar a/vendor/magento/module-deploy/Model/Deploy/JsDictionaryDeploy.php b/vendor/magento/module-deploy/Model/Deploy/JsDictionaryDeploy.php new file mode 100644 -index 0000000..9c6a309a --- /dev/null +++ b/vendor/magento/module-deploy/Model/Deploy/JsDictionaryDeploy.php @@ -0,0 +1,89 @@ @@ -93,8 +92,7 @@ index 0000000..9c6a309a + return Cli::RETURN_SUCCESS; + } +} -diff --git a/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php b/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php -index 0d990b5..aa23833 100644 +diff -Nuar a/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php b/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php --- a/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php +++ b/vendor/magento/module-deploy/Model/Deploy/LocaleQuickDeploy.php @@ -6,16 +6,21 @@ @@ -257,8 +255,7 @@ index 0d990b5..aa23833 100644 * @param string $path * @return void */ -diff --git a/vendor/magento/module-deploy/Model/DeployStrategyFactory.php b/vendor/magento/module-deploy/Model/DeployStrategyFactory.php -index 536f344..7ba159b 100644 +diff -Nuar a/vendor/magento/module-deploy/Model/DeployStrategyFactory.php b/vendor/magento/module-deploy/Model/DeployStrategyFactory.php --- a/vendor/magento/module-deploy/Model/DeployStrategyFactory.php +++ b/vendor/magento/module-deploy/Model/DeployStrategyFactory.php @@ -23,6 +23,11 @@ class DeployStrategyFactory @@ -281,9 +278,8 @@ index 536f344..7ba159b 100644 ]; if (!isset($strategyMap[$type])) { -diff --git a/vendor/magento/module-deploy/Test/Unit/Model/Deploy/JsDictionaryDeployTest.php b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/JsDictionaryDeployTest.php +diff -Nuar a/vendor/magento/module-deploy/Test/Unit/Model/Deploy/JsDictionaryDeployTest.php b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/JsDictionaryDeployTest.php new file mode 100644 -index 0000000..2533476 --- /dev/null +++ b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/JsDictionaryDeployTest.php @@ -0,0 +1,103 @@ @@ -390,8 +386,7 @@ index 0000000..2533476 + $this->model->deploy($area, $themePath, $locale); + } +} -diff --git a/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php -index 6c693fe..d50c8ce 100644 +diff -Nuar a/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php --- a/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php +++ b/vendor/magento/module-deploy/Test/Unit/Model/Deploy/LocaleQuickDeployTest.php @@ -11,7 +11,10 @@ diff --git a/patches/MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch b/patches/MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch index 6b9b89be..5ca1bfdc 100644 --- a/patches/MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch +++ b/patches/MAGETWO-67097__fix_credis_pipeline_bug__2.1.4.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/colinmollenhour/credis/Client.php b/vendor/colinmollenhour/credis/Client.php -index afbc85d..8368b32 100755 --- a/vendor/colinmollenhour/credis/Client.php +++ b/vendor/colinmollenhour/credis/Client.php @@ -1017,6 +1017,7 @@ class Credis_Client { diff --git a/patches/MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch b/patches/MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch index 15b64511..d17aaf8f 100644 --- a/patches/MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch +++ b/patches/MAGETWO-67805__fix_image_resizing_after_upgrade__2.1.6.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/module-catalog/Block/Product/ImageBlockBuilder.php b/vendor/magento/module-catalog/Block/Product/ImageBlockBuilder.php -index 9fa50c1..1e54cfc 100644 --- a/vendor/magento/module-catalog/Block/Product/ImageBlockBuilder.php +++ b/vendor/magento/module-catalog/Block/Product/ImageBlockBuilder.php @@ -120,12 +120,7 @@ class ImageBlockBuilder @@ -17,7 +16,6 @@ index 9fa50c1..1e54cfc 100644 : 'Magento_Catalog::product/image_with_borders.phtml'; diff -Nuar a/vendor/magento/module-catalog/Helper/Image.php b/vendor/magento/module-catalog/Helper/Image.php -index 039c260..9a7c13e 100644 --- a/vendor/magento/module-catalog/Helper/Image.php +++ b/vendor/magento/module-catalog/Helper/Image.php @@ -826,7 +826,7 @@ class Image extends AbstractHelper @@ -30,7 +28,6 @@ index 039c260..9a7c13e 100644 /** diff -Nuar a/vendor/magento/module-catalog/Model/Product/Image.php b/vendor/magento/module-catalog/Model/Product/Image.php -index c1c10da..b543571 100644 --- a/vendor/magento/module-catalog/Model/Product/Image.php +++ b/vendor/magento/module-catalog/Model/Product/Image.php @@ -320,7 +320,7 @@ class Image extends \Magento\Framework\Model\AbstractModel @@ -70,7 +67,6 @@ index c1c10da..b543571 100644 } diff -Nuar a/vendor/magento/module-catalog/Model/Product/Image/ParamsBuilder.php b/vendor/magento/module-catalog/Model/Product/Image/ParamsBuilder.php -index 603f3e8..7aa0c32 100644 --- a/vendor/magento/module-catalog/Model/Product/Image/ParamsBuilder.php +++ b/vendor/magento/module-catalog/Model/Product/Image/ParamsBuilder.php @@ -70,6 +70,21 @@ class ParamsBuilder @@ -111,7 +107,6 @@ index 603f3e8..7aa0c32 100644 $constrain = !empty($imageArguments['constrain']) ? $imageArguments['constrain'] diff -Nuar a/vendor/magento/module-catalog/etc/view.xml b/vendor/magento/module-catalog/etc/view.xml -index 8c7500d..f2f004c 100644 --- a/vendor/magento/module-catalog/etc/view.xml +++ b/vendor/magento/module-catalog/etc/view.xml @@ -7,6 +7,6 @@ @@ -123,7 +118,6 @@ index 8c7500d..f2f004c 100644 diff -Nuar a/vendor/magento/module-catalog/view/frontend/templates/product/image_with_borders.phtml b/vendor/magento/module-catalog/view/frontend/templates/product/image_with_borders.phtml -index c3280c5..4d7a825 100644 --- a/vendor/magento/module-catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/vendor/magento/module-catalog/view/frontend/templates/product/image_with_borders.phtml @@ -13,7 +13,5 @@ @@ -135,7 +129,6 @@ index c3280c5..4d7a825 100644 alt="stripTags($block->getLabel(), null, true); ?>"/> diff -Nuar a/vendor/magento/theme-adminhtml-backend/etc/view.xml b/vendor/magento/theme-adminhtml-backend/etc/view.xml -index de6b0cf..f352974 100644 --- a/vendor/magento/theme-adminhtml-backend/etc/view.xml +++ b/vendor/magento/theme-adminhtml-backend/etc/view.xml @@ -9,6 +9,9 @@ @@ -149,7 +142,6 @@ index de6b0cf..f352974 100644 diff -Nuar a/lib/web/mage/gallery/gallery.less b/lib/web/mage/gallery/gallery.less -index fc9d09e..2fd877d 100644 --- a/lib/web/mage/gallery/gallery.less +++ b/lib/web/mage/gallery/gallery.less @@ -691,23 +691,15 @@ diff --git a/patches/MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch b/patches/MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch index fd1697ee..be2259fe 100644 --- a/patches/MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch +++ b/patches/MAGETWO-69847__support_credis_forking_during_scd__2.1.4.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Factory.php b/vendor/magento/framework/App/Cache/Frontend/Factory.php -index a71ff27b07..e1c65ef572 100644 --- a/vendor/magento/framework/App/Cache/Frontend/Factory.php +++ b/vendor/magento/framework/App/Cache/Frontend/Factory.php @@ -145,15 +145,17 @@ class Factory @@ -30,7 +29,6 @@ index a71ff27b07..e1c65ef572 100644 ); $result = $this->_applyDecorators($result); diff -Nuar a/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php b/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php -index 5d72ee6a1e..fe9dc2f453 100644 --- a/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php +++ b/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php @@ -16,11 +16,32 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface diff --git a/patches/MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch b/patches/MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch index 8c994b25..907ada79 100644 --- a/patches/MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch +++ b/patches/MAGETWO-69847__support_credis_forking_during_scd__2.2.0.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Factory.php b/vendor/magento/framework/App/Cache/Frontend/Factory.php -index 4c539da096..8477d94f28 100644 --- a/vendor/magento/framework/App/Cache/Frontend/Factory.php +++ b/vendor/magento/framework/App/Cache/Frontend/Factory.php @@ -148,15 +148,17 @@ class Factory @@ -30,7 +29,6 @@ index 4c539da096..8477d94f28 100644 ); $result = $this->_applyDecorators($result); diff -Nuar a/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php b/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php -index c8917a0996..fe9dc2f453 100644 --- a/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php +++ b/vendor/magento/framework/Cache/Frontend/Adapter/Zend.php @@ -16,11 +16,32 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface diff --git a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.10.patch b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.10.patch index 0b7b5885..a972c8c4 100644 --- a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.10.patch +++ b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.10.patch @@ -5,7 +5,6 @@ Date: Tue Nov 28 15:29:54 2017 +0200 MAGETWO-84444: Mview does not work with Staging diff -Nuar a/vendor/magento/module-catalog-inventory/etc/mview.xml b/vendor/magento/module-catalog-inventory/etc/mview.xml -index 58a051a3d0e..3dd8419d7e3 100644 --- a/vendor/magento/module-catalog-inventory/etc/mview.xml +++ b/vendor/magento/module-catalog-inventory/etc/mview.xml @@ -5,10 +5,13 @@ @@ -25,7 +24,6 @@ index 58a051a3d0e..3dd8419d7e3 100644 diff -Nuar a/vendor/magento/module-indexer/Setup/RecurringData.php b/vendor/magento/module-indexer/Setup/RecurringData.php new file mode 100644 -index 00000000000..38ea0e5b79e --- /dev/null +++ b/vendor/magento/module-indexer/Setup/RecurringData.php @@ -0,0 +1,56 @@ @@ -86,7 +84,6 @@ index 00000000000..38ea0e5b79e + } +} diff -Nuar a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php -index aae82938c83..545689a4391 100644 --- a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +++ b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php @@ -62,7 +62,7 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase @@ -152,7 +149,6 @@ index aae82938c83..545689a4391 100644 $changelogMock = $this->getMockForAbstractClass( diff -Nuar a/vendor/magento/framework/Mview/View/Subscription.php b/vendor/magento/framework/Mview/View/Subscription.php -index c3da91c8331..3c4bd1dce2d 100644 --- a/vendor/magento/framework/Mview/View/Subscription.php +++ b/vendor/magento/framework/Mview/View/Subscription.php @@ -10,6 +10,8 @@ namespace Magento\Framework\Mview\View; @@ -359,7 +355,6 @@ index c3da91c8331..3c4bd1dce2d 100644 */ public function getView() diff -Nuar a/vendor/magento/framework/Mview/etc/mview.xsd b/vendor/magento/framework/Mview/etc/mview.xsd -index 1dad5b3f415..b7d6bbdde68 100644 --- a/vendor/magento/framework/Mview/etc/mview.xsd +++ b/vendor/magento/framework/Mview/etc/mview.xsd @@ -106,7 +106,7 @@ @@ -378,7 +373,6 @@ Date: Tue Nov 28 15:30:48 2017 +0200 MAGETWO-84444: Mview does not work with Staging diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php -index b3e920fb87..f9b1d1a32a 100644 --- a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php @@ -6,22 +6,16 @@ @@ -482,7 +476,6 @@ index b3e920fb87..f9b1d1a32a 100644 } } diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php -index 96c0f71164..7c841853f9 100644 --- a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php @@ -11,34 +11,43 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory @@ -543,7 +536,6 @@ index 96c0f71164..7c841853f9 100644 protected function isStagingTable(array $data = []) { diff -Nuar a/vendor/magento/module-catalog-staging/Model/VersionTables.php b/vendor/magento/module-catalog-staging/Model/VersionTables.php -index c845f98b31..242aaf2f25 100644 --- a/vendor/magento/module-catalog-staging/Model/VersionTables.php +++ b/vendor/magento/module-catalog-staging/Model/VersionTables.php @@ -5,6 +5,11 @@ @@ -559,7 +551,6 @@ index c845f98b31..242aaf2f25 100644 { /** diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php -index d595784134..d5e78767bd 100644 --- a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php @@ -17,11 +17,6 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase @@ -694,7 +685,6 @@ index d595784134..d5e78767bd 100644 $this->assertEquals($subscriptionMock, $result); } diff -Nuar a/vendor/magento/module-catalog-staging/etc/di.xml b/vendor/magento/module-catalog-staging/etc/di.xml -index aea000b42f..178b0bcf63 100644 --- a/vendor/magento/module-catalog-staging/etc/di.xml +++ b/vendor/magento/module-catalog-staging/etc/di.xml @@ -56,6 +56,35 @@ @@ -735,7 +725,6 @@ index aea000b42f..178b0bcf63 100644 diff -Nuar a/vendor/magento/module-catalog-staging/etc/mview.xml b/vendor/magento/module-catalog-staging/etc/mview.xml deleted file mode 100644 -index 45bb6589b6..0000000000 --- a/vendor/magento/module-catalog-staging/etc/mview.xml +++ /dev/null @@ -1,23 +0,0 @@ @@ -770,7 +759,6 @@ Date: Tue Nov 28 15:45:17 2017 +0200 diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php new file mode 100644 -index 0000000000..7c549538c7 --- /dev/null +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php @@ -0,0 +1,100 @@ @@ -882,7 +870,6 @@ Date: Tue Nov 28 15:54:17 2017 +0200 diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php new file mode 100644 -index 0000000000..d396829ef8 --- /dev/null +++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php @@ -0,0 +1,302 @@ diff --git a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.4.patch b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.4.patch index 9f2719ce..d9a44c00 100644 --- a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.4.patch +++ b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.4.patch @@ -5,7 +5,6 @@ Date: Mon Nov 27 18:21:54 2017 +0200 MAGETWO-84444 diff -Nuar a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php -index d8094cfa8..da9a2c724 100644 --- a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +++ b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php @@ -62,7 +62,7 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase @@ -71,7 +70,6 @@ index d8094cfa8..da9a2c724 100644 $changelogMock = $this->getMockForAbstractClass( diff -Nuar a/vendor/magento/framework/Mview/View/Subscription.php b/vendor/magento/framework/Mview/View/Subscription.php -index 7dd440ae9..54e4aaf53 100644 --- a/vendor/magento/framework/Mview/View/Subscription.php +++ b/vendor/magento/framework/Mview/View/Subscription.php @@ -10,6 +10,8 @@ namespace Magento\Framework\Mview\View; @@ -278,7 +276,6 @@ index 7dd440ae9..54e4aaf53 100644 */ public function getView() diff -Nuar a/vendor/magento/framework/Mview/etc/mview.xsd b/vendor/magento/framework/Mview/etc/mview.xsd -index d171699c3..0521691e8 100644 --- a/vendor/magento/framework/Mview/etc/mview.xsd +++ b/vendor/magento/framework/Mview/etc/mview.xsd @@ -106,7 +106,7 @@ @@ -291,7 +288,6 @@ index d171699c3..0521691e8 100644 diff -Nuar a/vendor/magento/module-catalog-inventory/etc/mview.xml b/vendor/magento/module-catalog-inventory/etc/mview.xml -index 5737fea21..954a3349e 100644 --- a/vendor/magento/module-catalog-inventory/etc/mview.xml +++ b/vendor/magento/module-catalog-inventory/etc/mview.xml @@ -5,10 +5,13 @@ @@ -311,7 +307,6 @@ index 5737fea21..954a3349e 100644 diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php new file mode 100644 -index 000000000..7c549538c --- /dev/null +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php @@ -0,0 +1,100 @@ @@ -416,7 +411,6 @@ index 000000000..7c549538c + } +} diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php -index 3ffa1a1fc..438318597 100644 --- a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php @@ -6,22 +6,16 @@ @@ -520,7 +514,6 @@ index 3ffa1a1fc..438318597 100644 } } diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php -index 6f03681f7..052aa3702 100644 --- a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php @@ -11,34 +11,43 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory @@ -581,7 +574,6 @@ index 6f03681f7..052aa3702 100644 protected function isStagingTable(array $data = []) { diff -Nuar a/vendor/magento/module-catalog-staging/Model/VersionTables.php b/vendor/magento/module-catalog-staging/Model/VersionTables.php -index 7ebd828bd..2cc32669b 100644 --- a/vendor/magento/module-catalog-staging/Model/VersionTables.php +++ b/vendor/magento/module-catalog-staging/Model/VersionTables.php @@ -5,6 +5,11 @@ @@ -598,7 +590,6 @@ index 7ebd828bd..2cc32669b 100644 /** diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php new file mode 100644 -index 000000000..d396829ef --- /dev/null +++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php @@ -0,0 +1,302 @@ @@ -905,7 +896,6 @@ index 000000000..d396829ef + } +} diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php -index a0abb4230..4a0601569 100644 --- a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php @@ -17,11 +17,6 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase @@ -1040,7 +1030,6 @@ index a0abb4230..4a0601569 100644 $this->assertEquals($subscriptionMock, $result); } diff -Nuar a/vendor/magento/module-catalog-staging/etc/di.xml b/vendor/magento/module-catalog-staging/etc/di.xml -index 79f95ec08..e28ac70e7 100644 --- a/vendor/magento/module-catalog-staging/etc/di.xml +++ b/vendor/magento/module-catalog-staging/etc/di.xml @@ -56,6 +56,35 @@ @@ -1081,7 +1070,6 @@ index 79f95ec08..e28ac70e7 100644 diff -Nuar a/vendor/magento/module-catalog-staging/etc/mview.xml b/vendor/magento/module-catalog-staging/etc/mview.xml deleted file mode 100644 -index 488972e05..000000000 --- a/vendor/magento/module-catalog-staging/etc/mview.xml +++ /dev/null @@ -1,23 +0,0 @@ @@ -1110,7 +1098,6 @@ index 488972e05..000000000 - diff -Nuar a/vendor/magento/module-indexer/Setup/RecurringData.php b/vendor/magento/module-indexer/Setup/RecurringData.php new file mode 100644 -index 000000000..38ea0e5b7 --- /dev/null +++ b/vendor/magento/module-indexer/Setup/RecurringData.php @@ -0,0 +1,56 @@ diff --git a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.5.patch b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.5.patch index c044017d..a76fd781 100644 --- a/patches/MAGETWO-84444__fix_mview_on_staging__2.1.5.patch +++ b/patches/MAGETWO-84444__fix_mview_on_staging__2.1.5.patch @@ -5,7 +5,6 @@ Date: Tue Nov 28 15:29:54 2017 +0200 MAGETWO-84444: Mview does not work with Staging diff -Nuar a/vendor/magento/module-catalog-inventory/etc/mview.xml b/vendor/magento/module-catalog-inventory/etc/mview.xml -index 58a051a3d0e..3dd8419d7e3 100644 --- a/vendor/magento/module-catalog-inventory/etc/mview.xml +++ b/vendor/magento/module-catalog-inventory/etc/mview.xml @@ -5,10 +5,13 @@ @@ -25,7 +24,6 @@ index 58a051a3d0e..3dd8419d7e3 100644 diff -Nuar a/vendor/magento/module-indexer/Setup/RecurringData.php b/vendor/magento/module-indexer/Setup/RecurringData.php new file mode 100644 -index 00000000000..38ea0e5b79e --- /dev/null +++ b/vendor/magento/module-indexer/Setup/RecurringData.php @@ -0,0 +1,56 @@ @@ -86,7 +84,6 @@ index 00000000000..38ea0e5b79e + } +} diff -Nuar a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php -index aae82938c83..545689a4391 100644 --- a/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php +++ b/vendor/magento/framework/Mview/Test/Unit/View/SubscriptionTest.php @@ -62,7 +62,7 @@ class SubscriptionTest extends \PHPUnit_Framework_TestCase @@ -152,7 +149,6 @@ index aae82938c83..545689a4391 100644 $changelogMock = $this->getMockForAbstractClass( diff -Nuar a/vendor/magento/framework/Mview/View/Subscription.php b/vendor/magento/framework/Mview/View/Subscription.php -index c3da91c8331..3c4bd1dce2d 100644 --- a/vendor/magento/framework/Mview/View/Subscription.php +++ b/vendor/magento/framework/Mview/View/Subscription.php @@ -10,6 +10,8 @@ namespace Magento\Framework\Mview\View; @@ -359,7 +355,6 @@ index c3da91c8331..3c4bd1dce2d 100644 */ public function getView() diff -Nuar a/vendor/magento/framework/Mview/etc/mview.xsd b/vendor/magento/framework/Mview/etc/mview.xsd -index 1dad5b3f415..b7d6bbdde68 100644 --- a/vendor/magento/framework/Mview/etc/mview.xsd +++ b/vendor/magento/framework/Mview/etc/mview.xsd @@ -106,7 +106,7 @@ @@ -378,7 +373,6 @@ Date: Tue Nov 28 15:30:48 2017 +0200 MAGETWO-84444: Mview does not work with Staging diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php -index b3e920fb87..f9b1d1a32a 100644 --- a/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Category/Attribute/Subscription.php @@ -6,22 +6,16 @@ @@ -482,7 +476,6 @@ index b3e920fb87..f9b1d1a32a 100644 } } diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php -index 96c0f71164..7c841853f9 100644 --- a/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/SubscriptionFactory.php @@ -11,34 +11,43 @@ class SubscriptionFactory extends FrameworkSubscriptionFactory @@ -543,7 +536,6 @@ index 96c0f71164..7c841853f9 100644 protected function isStagingTable(array $data = []) { diff -Nuar a/vendor/magento/module-catalog-staging/Model/VersionTables.php b/vendor/magento/module-catalog-staging/Model/VersionTables.php -index c845f98b31..242aaf2f25 100644 --- a/vendor/magento/module-catalog-staging/Model/VersionTables.php +++ b/vendor/magento/module-catalog-staging/Model/VersionTables.php @@ -5,6 +5,11 @@ @@ -559,7 +551,6 @@ index c845f98b31..242aaf2f25 100644 { /** diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php -index d595784134..d5e78767bd 100644 --- a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php +++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/SubscriptionFactoryTest.php @@ -17,11 +17,6 @@ class SubscriptionFactoryTest extends \PHPUnit_Framework_TestCase @@ -694,7 +685,6 @@ index d595784134..d5e78767bd 100644 $this->assertEquals($subscriptionMock, $result); } diff -Nuar a/vendor/magento/module-catalog-staging/etc/di.xml b/vendor/magento/module-catalog-staging/etc/di.xml -index aea000b42f..178b0bcf63 100644 --- a/vendor/magento/module-catalog-staging/etc/di.xml +++ b/vendor/magento/module-catalog-staging/etc/di.xml @@ -56,6 +56,35 @@ @@ -735,7 +725,6 @@ index aea000b42f..178b0bcf63 100644 diff -Nuar a/vendor/magento/module-catalog-staging/etc/mview.xml b/vendor/magento/module-catalog-staging/etc/mview.xml deleted file mode 100644 -index 45bb6589b6..0000000000 --- a/vendor/magento/module-catalog-staging/etc/mview.xml +++ /dev/null @@ -1,23 +0,0 @@ @@ -770,7 +759,6 @@ Date: Tue Nov 28 15:45:17 2017 +0200 diff -Nuar a/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php new file mode 100644 -index 0000000000..7c549538c7 --- /dev/null +++ b/vendor/magento/module-catalog-staging/Model/Mview/View/Attribute/Subscription.php @@ -0,0 +1,100 @@ @@ -882,7 +870,6 @@ Date: Tue Nov 28 15:54:17 2017 +0200 diff -Nuar a/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php new file mode 100644 -index 0000000000..d396829ef8 --- /dev/null +++ b/vendor/magento/module-catalog-staging/Test/Unit/Model/Mview/View/Attribute/SubscriptionTest.php @@ -0,0 +1,302 @@ diff --git a/patches/MAGETWO-98833__turn_off_google_chart_api__2.x.patch b/patches/MAGETWO-98833__turn_off_google_chart_api__2.x.patch index 4d0dbd83..631bf16c 100644 --- a/patches/MAGETWO-98833__turn_off_google_chart_api__2.x.patch +++ b/patches/MAGETWO-98833__turn_off_google_chart_api__2.x.patch @@ -1,5 +1,4 @@ -diff --git a/vendor/magento/module-backend/Block/Dashboard/Graph.php b/vendor/magento/module-backend/Block/Dashboard/Graph.php -index 8e238ccab44c..71a6cf4e938f 100644 +diff -Nuar a/vendor/magento/module-backend/Block/Dashboard/Graph.php b/vendor/magento/module-backend/Block/Dashboard/Graph.php --- a/vendor/magento/module-backend/Block/Dashboard/Graph.php +++ b/vendor/magento/module-backend/Block/Dashboard/Graph.php @@ -15,7 +15,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard diff --git a/patches/MC-5964__preauth_sql__2.2.0.patch b/patches/MC-5964__preauth_sql__2.2.0.patch index 82a111eb..744f775e 100644 --- a/patches/MC-5964__preauth_sql__2.2.0.patch +++ b/patches/MC-5964__preauth_sql__2.2.0.patch @@ -78,7 +78,6 @@ diff -Naur a/vendor/magento/module-catalog/Model/Product/ProductFrontendAction/S } diff -Naur a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php -index 3d06e27542f0..a6c0dba6e175 100644 --- a/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php +++ b/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php @@ -2904,7 +2904,7 @@ public function prepareSqlCondition($fieldName, $condition) diff --git a/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch b/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch index 231fe7e3..839f713c 100644 --- a/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch +++ b/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php -index 5f22a36510c..7f3fe8e91eb 100644 --- a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php +++ b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php @@ -53,19 +53,26 @@ class Preview extends \Magento\Backend\Block\Widget @@ -34,7 +33,6 @@ index 5f22a36510c..7f3fe8e91eb 100644 $template->setTemplateText($this->_maliciousCode->filter($template->getTemplateText())); diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php -index b7d59ecc8..09f8e8510 100644 --- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php @@ -26,19 +26,28 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc @@ -94,7 +92,6 @@ index b7d59ecc8..09f8e8510 100644 } } diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php -index 17288978e..baf3ce106 100644 --- a/vendor/magento/module-page-builder/Model/Stage/Config.php +++ b/vendor/magento/module-page-builder/Model/Stage/Config.php @@ -135,7 +135,9 @@ class Config @@ -110,7 +107,6 @@ index 17288978e..baf3ce106 100644 'can_use_inline_editing_on_stage' => $this->isWysiwygProvisionedForEditingOnStage(), diff -Nuar a/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php new file mode 100644 -index 000000000..a1e9d943a --- /dev/null +++ b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php @@ -0,0 +1,49 @@ @@ -164,7 +160,6 @@ index 000000000..a1e9d943a + } +} diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml -index a147ab1b2..e7374870b 100644 --- a/vendor/magento/module-page-builder/etc/di.xml +++ b/vendor/magento/module-page-builder/etc/di.xml @@ -140,4 +140,7 @@ diff --git a/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch b/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch index f6de20e8..97b68b83 100644 --- a/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch +++ b/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php -index acc367de742..4f0479a9573 100644 --- a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php +++ b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php @@ -55,19 +55,26 @@ class Preview extends \Magento\Backend\Block\Widget @@ -34,7 +33,6 @@ index acc367de742..4f0479a9573 100644 \Magento\Framework\Profiler::start($this->profilerName); diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php -index b7d59ecc8..09f8e8510 100644 --- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php @@ -26,19 +26,28 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc @@ -94,7 +92,6 @@ index b7d59ecc8..09f8e8510 100644 } } diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php -index 17288978e..baf3ce106 100644 --- a/vendor/magento/module-page-builder/Model/Stage/Config.php +++ b/vendor/magento/module-page-builder/Model/Stage/Config.php @@ -135,7 +135,9 @@ class Config @@ -110,7 +107,6 @@ index 17288978e..baf3ce106 100644 'can_use_inline_editing_on_stage' => $this->isWysiwygProvisionedForEditingOnStage(), diff -Nuar a/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php new file mode 100644 -index 000000000..a1e9d943a --- /dev/null +++ b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php @@ -0,0 +1,49 @@ @@ -164,7 +160,6 @@ index 000000000..a1e9d943a + } +} diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml -index a147ab1b2..e7374870b 100644 --- a/vendor/magento/module-page-builder/etc/di.xml +++ b/vendor/magento/module-page-builder/etc/di.xml @@ -140,4 +140,7 @@ diff --git a/patches/MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch b/patches/MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch index 6e2036c4..826693fc 100644 --- a/patches/MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch +++ b/patches/MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/framework/Mail/Message.php b/vendor/magento/framework/Mail/Message.php -index 36a0e6a..fad0910 100644 --- a/vendor/magento/framework/Mail/Message.php +++ b/vendor/magento/framework/Mail/Message.php @@ -15,6 +15,7 @@ class Message extends \Zend_Mail implements MessageInterface diff --git a/patches/SET-36__fix_oom_during_customer_import__2.1.11.patch b/patches/SET-36__fix_oom_during_customer_import__2.1.11.patch index 2d1d7e3f..ab19736c 100644 --- a/patches/SET-36__fix_oom_during_customer_import__2.1.11.patch +++ b/patches/SET-36__fix_oom_during_customer_import__2.1.11.patch @@ -1,5 +1,4 @@ diff -Naur a/vendor/magento/module-customer-import-export/Model/Import/Address.php b/vendor/magento/module-customer-import-export/Model/Import/Address.php -index 3f745267fd2..13e659e4e62 100644 --- a/vendor/magento/module-customer-import-export/Model/Import/Address.php +++ b/vendor/magento/module-customer-import-export/Model/Import/Address.php @@ -257,6 +257,11 @@ class Address extends AbstractCustomer @@ -74,7 +73,6 @@ index 3f745267fd2..13e659e4e62 100644 * * @return $this diff -Naur a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php -index 4e6687bff28..359822df6d9 100644 --- a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +++ b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php @@ -117,13 +117,18 @@ class Storage diff --git a/patches/SET-36__fix_oom_during_customer_import__2.1.4.patch b/patches/SET-36__fix_oom_during_customer_import__2.1.4.patch index 1e79c673..6749f4c1 100644 --- a/patches/SET-36__fix_oom_during_customer_import__2.1.4.patch +++ b/patches/SET-36__fix_oom_during_customer_import__2.1.4.patch @@ -5,7 +5,6 @@ Date: Thu Nov 16 18:55:15 2017 +0200 SET-36: Memory limit exhausted during import of customers and addresses diff -Naur a/vendor/magento/module-customer-import-export/Model/Import/Address.php b/vendor/magento/module-customer-import-export/Model/Import/Address.php -index eb5742d24c7..70b8c34ef41 100644 --- a/vendor/magento/module-customer-import-export/Model/Import/Address.php +++ b/vendor/magento/module-customer-import-export/Model/Import/Address.php @@ -238,6 +238,11 @@ class Address extends AbstractCustomer @@ -80,7 +79,6 @@ index eb5742d24c7..70b8c34ef41 100644 * * @return $this diff -Naur a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php -index 4e6687bff28..359822df6d9 100644 --- a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +++ b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php @@ -117,13 +117,18 @@ class Storage diff --git a/patches/SET-36__fix_oom_during_customer_import__2.2.0.patch b/patches/SET-36__fix_oom_during_customer_import__2.2.0.patch index 7f546b8c..4c800130 100644 --- a/patches/SET-36__fix_oom_during_customer_import__2.2.0.patch +++ b/patches/SET-36__fix_oom_during_customer_import__2.2.0.patch @@ -5,7 +5,6 @@ Date: Thu Nov 16 18:55:15 2017 +0200 SET-36: Memory limit exhausted during import of customers and addresses diff -Nuar a/vendor/magento/module-customer-import-export/Model/Import/Address.php b/vendor/magento/module-customer-import-export/Model/Import/Address.php -index eb5742d24c7..70b8c34ef41 100644 --- a/vendor/magento/module-customer-import-export/Model/Import/Address.php +++ b/vendor/magento/module-customer-import-export/Model/Import/Address.php @@ -238,6 +238,11 @@ class Address extends AbstractCustomer @@ -80,7 +79,6 @@ index eb5742d24c7..70b8c34ef41 100644 * * @return $this diff -Nuar a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php -index 4e6687bff28..359822df6d9 100644 --- a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +++ b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php @@ -117,13 +117,18 @@ class Storage diff --git a/patches/SET-36__fix_oom_during_customer_import__2.2.4.patch b/patches/SET-36__fix_oom_during_customer_import__2.2.4.patch index 6db2e0e7..edb9a8f6 100644 --- a/patches/SET-36__fix_oom_during_customer_import__2.2.4.patch +++ b/patches/SET-36__fix_oom_during_customer_import__2.2.4.patch @@ -1,5 +1,4 @@ diff -Nuar a/vendor/magento/module-customer-import-export/Model/Import/Address.php b/vendor/magento/module-import-export/Model/Import/Address.php -index 1e1221e..41e0512 100644 --- a/vendor/magento/module-customer-import-export/Model/Import/Address.php +++ b/vendor/magento/module-customer-import-export/Model/Import/Address.php @@ -253,6 +253,11 @@ class Address extends AbstractCustomer @@ -73,7 +72,6 @@ index 1e1221e..41e0512 100644 * Initialize country regions hash for clever recognition * diff -Nuar a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php -index ae88e96..a6f7aa3 100644 --- a/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php +++ b/vendor/magento/module-customer-import-export/Model/ResourceModel/Import/Customer/Storage.php @@ -117,13 +117,18 @@ class Storage From 382df44deb7a02b7f49206d2eea8ae6653559832 Mon Sep 17 00:00:00 2001 From: Oleksandr Shmyheliuk Date: Wed, 11 Dec 2019 14:25:31 -0600 Subject: [PATCH 009/157] MAGECLOUD-4845: Patch applying is failing on installation from git (#9) --- src/Command/Patch/Manager.php | 4 ++-- src/Test/Unit/Command/Patch/ManagerTest.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Command/Patch/Manager.php b/src/Command/Patch/Manager.php index 66617569..d9434002 100644 --- a/src/Command/Patch/Manager.php +++ b/src/Command/Patch/Manager.php @@ -118,7 +118,7 @@ public function applyComposerPatches(InputInterface $input, OutputInterface $out (string)$patchName, (string)$packageName, (string)$constraint, - $deployedFromGit + (bool)$deployedFromGit ); if (null !== $message) { @@ -156,7 +156,7 @@ public function applyHotFixes(InputInterface $input, OutputInterface $output) foreach ($files as $file) { $output->writeln( - $this->applier->applyFile($file, $deployedFromGit) + $this->applier->applyFile($file, (bool)$deployedFromGit) ); } } diff --git a/src/Test/Unit/Command/Patch/ManagerTest.php b/src/Test/Unit/Command/Patch/ManagerTest.php index bb2d775f..7028f129 100644 --- a/src/Test/Unit/Command/Patch/ManagerTest.php +++ b/src/Test/Unit/Command/Patch/ManagerTest.php @@ -141,7 +141,7 @@ public function testApplyComposerPatchesWithFSException() $inputMock->method('getOption') ->with(Apply::OPT_GIT_INSTALLATION) - ->willReturn(false); + ->willReturn('0'); $this->manager->applyComposerPatches($inputMock, $outputMock); } @@ -160,8 +160,8 @@ public function testExecuteApplyHotFixes() $this->applierMock->expects($this->exactly(2)) ->method('applyFile') ->willReturnMap([ - [__DIR__ . '/_files/' . Manager::HOT_FIXES_DIR . '/patch1.patch', false, 'Patch 1 applied'], - [__DIR__ . '/_files/' . Manager::HOT_FIXES_DIR . '/patch2.patch', false, 'Patch 2 applied'] + [__DIR__ . '/_files/' . Manager::HOT_FIXES_DIR . '/patch1.patch', true, 'Patch 1 applied'], + [__DIR__ . '/_files/' . Manager::HOT_FIXES_DIR . '/patch2.patch', true, 'Patch 2 applied'] ]); /** @var InputInterface|MockObject $inputMock */ @@ -169,7 +169,7 @@ public function testExecuteApplyHotFixes() $inputMock->expects($this->once()) ->method('getOption') ->with(Apply::OPT_GIT_INSTALLATION) - ->willReturn(false); + ->willReturn('1'); /** @var OutputInterface|MockObject $outputMock */ $outputMock = $this->getMockForAbstractClass(OutputInterface::class); $outputMock->expects($this->exactly(3)) From 7251aa93123eb892ffd4f9f90c0b93988db02917 Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Thu, 2 Jan 2020 10:15:48 -0600 Subject: [PATCH 010/157] MAGECLOUD-4606: Review Hotfixes List and Add All Missing for 2.1.4+ (#8) --- patches.json | 29 + ...ulti-site_configuration_issue__2.2.4.patch | 15 + ...nnot_change_the_applied_theme__2.2.5.patch | 16 + ...retrieving_product_attributes__2.2.5.patch | 181 +++ ...91__customer_attributes_issue__2.2.6.patch | 1419 +++++++++++++++++ ...d_is_removed_by_authorize_net__2.2.0.patch | 322 ++++ ...e_view_scope_in_async_web_api__2.3.1.patch | 542 +++++++ ...e_view_scope_in_async_web_api__2.3.2.patch | 556 +++++++ ...233__fix_xss_in_order_history__2.2.0.patch | 146 ++ ...233__fix_xss_in_order_history__2.2.7.patch | 119 ++ ...233__fix_xss_in_order_history__2.3.0.patch | 209 +++ ...32__admin_path_disclosure_bug__2.1.4.patch | 21 + ...32__admin_path_disclosure_bug__2.2.0.patch | 22 + ...32__admin_path_disclosure_bug__2.3.0.patch | 22 + 14 files changed, 3619 insertions(+) create mode 100644 patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch create mode 100644 patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch create mode 100644 patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch create mode 100644 patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch create mode 100644 patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch create mode 100644 patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch create mode 100644 patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch create mode 100644 patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch create mode 100644 patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch create mode 100644 patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch create mode 100644 patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch create mode 100644 patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch create mode 100644 patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch diff --git a/patches.json b/patches.json index 34208a33..09e34493 100644 --- a/patches.json +++ b/patches.json @@ -205,6 +205,35 @@ "Fix pagebuilder module": { "2.3.1": "MDVA-22979__fix_pagebuilder_module__2.3.1.patch", "2.3.2": "MDVA-22979__fix_pagebuilder_module__2.3.2.patch" + }, + "Fix XSS in order history": { + "2.2.0 - 2.2.6": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch", + "2.2.7 - 2.2.8": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch", + "2.3.0 - 2.3.1": "PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch" + }, + "Pass Store View scope in the Async/Bulk Web API": { + "2.3.1": "MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch", + ">=2.3.2 <2.3.3": "MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch" + }, + "Admin path disclosure bug": { + "2.1.4 - 2.1.17": "PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch", + "2.2.0 - 2.2.8": "PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch", + "2.3.0 - 2.3.1": "PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch" + }, + "Transaction MD5 hash field is removed by Authorize.net": { + "2.2.0 - 2.2.7": "MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch" + }, + "Customer attributes issue": { + "2.2.6": "MAGETWO-95591__customer_attributes_issue__2.2.6.patch" + }, + "Optimize retrieving product attributes": { + "2.2.5": "MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch" + }, + "Cannot change the applied theme": { + "2.2.5": "MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch" + }, + "Fix for multi-site configuration issue": { + "2.2.4": "MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch" } }, "monolog/monolog": { diff --git a/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch b/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch new file mode 100644 index 00000000..4a47ef5d --- /dev/null +++ b/patches/MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch @@ -0,0 +1,15 @@ +diff -Nuar a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -232,6 +233,11 @@ + Magento\Backend\App\Request\PathInfoProcessor\Proxy + + ++ ++ ++ Magento\Framework\Session\Config\ConfigInterface\Proxy ++ ++ + + + diff --git a/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch b/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch new file mode 100644 index 00000000..cb5fdb47 --- /dev/null +++ b/patches/MAGETWO-93036__cannot_change_the_applied_theme__2.2.5.patch @@ -0,0 +1,16 @@ +diff -Nuar a/vendor/magento/module-email/Model/AbstractTemplate.php b/vendor/magento/module-email/Model/AbstractTemplate.php +--- a/vendor/magento/module-email/Model/AbstractTemplate.php ++++ b/vendor/magento/module-email/Model/AbstractTemplate.php +@@ -534,10 +534,9 @@ protected function cancelDesignConfig() + */ + public function setForcedArea($templateId) + { +- if ($this->area) { +- throw new \LogicException(__('Area is already set')); ++ if (!isset($this->area)) { ++ $this->area = $this->emailConfig->getTemplateArea($templateId); + } +- $this->area = $this->emailConfig->getTemplateArea($templateId); + return $this; + } + diff --git a/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch b/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch new file mode 100644 index 00000000..d35cf4e9 --- /dev/null +++ b/patches/MAGETWO-93083__optimize_retrieving_product_attributes__2.2.5.patch @@ -0,0 +1,181 @@ +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -12,6 +12,7 @@ + use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool; + use Magento\Framework\Api\AttributeValueFactory; + use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\DataObject\IdentityInterface; + use Magento\Framework\Pricing\SaleableInterface; + +@@ -270,6 +271,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + + /** + * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface ++ * @deprecated Not used anymore due to performance issue (loaded all product attributes) + */ + protected $metadataService; + +@@ -346,6 +348,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + protected $linkTypeProvider; + ++ /** ++ * @var \Magento\Eav\Model\Config ++ */ ++ private $eavConfig; ++ + /** + * Product constructor. + * @param \Magento\Framework\Model\Context $context +@@ -383,7 +390,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor + * @param array $data +- * ++ * @param \Magento\Eav\Model\Config|null $config + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +@@ -422,7 +429,8 @@ public function __construct( + EntryConverterPool $mediaGalleryEntryConverterPool, + \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, + \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor, +- array $data = [] ++ array $data = [], ++ \Magento\Eav\Model\Config $config = null + ) { + $this->metadataService = $metadataService; + $this->_itemOptionFactory = $itemOptionFactory; +@@ -461,6 +469,7 @@ public function __construct( + $resourceCollection, + $data + ); ++ $this->eavConfig = $config ?? ObjectManager::getInstance()->get(\Magento\Eav\Model\Config::class); + } + + /** +@@ -474,12 +483,18 @@ protected function _construct() + } + + /** +- * {@inheritdoc} ++ * Get a list of custom attribute codes that belongs to product attribute set. If attribute set not specified for ++ * product will return all attribute codes ++ * ++ * @return string[] + */ + protected function getCustomAttributesCodes() + { + if ($this->customAttributesCodes === null) { +- $this->customAttributesCodes = $this->getEavAttributesCodes($this->metadataService); ++ $this->customAttributesCodes = array_keys($this->eavConfig->getEntityAttributes( ++ self::ENTITY, ++ $this ++ )); + $this->customAttributesCodes = array_diff($this->customAttributesCodes, $this->interfaceAttributes); + } + return $this->customAttributesCodes; +diff -Nuar a/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php +--- a/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php ++++ b/vendor/magento/module-catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php +@@ -58,7 +58,9 @@ public function afterExecute(ReadSnapshot $subject, array $entityData, $entityTy + $globalAttributes = []; + $attributesMap = []; + $eavEntityType = $metadata->getEavEntityType(); +- $attributes = (null === $eavEntityType) ? [] : $this->config->getEntityAttributes($eavEntityType); ++ $attributes = null === $eavEntityType ++ ? [] ++ : $this->config->getEntityAttributes($eavEntityType, new \Magento\Framework\DataObject($entityData)); + + /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ + foreach ($attributes as $attribute) { +diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php b/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php +--- a/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php ++++ b/vendor/magento/module-eav/Model/ResourceModel/ReadHandler.php +@@ -5,6 +5,7 @@ + */ + namespace Magento\Eav\Model\ResourceModel; + ++use Magento\Framework\DataObject; + use Magento\Framework\EntityManager\MetadataPool; + use Magento\Framework\EntityManager\Operation\AttributeInterface; + use Magento\Framework\Model\Entity\ScopeInterface; +@@ -59,13 +60,29 @@ public function __construct( + * @param string $entityType + * @return \Magento\Eav\Api\Data\AttributeInterface[] + * @throws \Exception if for unknown entity type ++ * @deprecated Not used anymore ++ * @see ReadHandler::getEntityAttributes + */ + protected function getAttributes($entityType) + { + $metadata = $this->metadataPool->getMetadata($entityType); + $eavEntityType = $metadata->getEavEntityType(); +- $attributes = (null === $eavEntityType) ? [] : $this->config->getAttributes($eavEntityType); +- return $attributes; ++ return null === $eavEntityType ? [] : $this->config->getEntityAttributes($eavEntityType); ++ } ++ ++ /** ++ * Get attribute of given entity type ++ * ++ * @param string $entityType ++ * @param DataObject $entity ++ * @return \Magento\Eav\Api\Data\AttributeInterface[] ++ * @throws \Exception if for unknown entity type ++ */ ++ private function getEntityAttributes(string $entityType, DataObject $entity): array ++ { ++ $metadata = $this->metadataPool->getMetadata($entityType); ++ $eavEntityType = $metadata->getEavEntityType(); ++ return null === $eavEntityType ? [] : $this->config->getEntityAttributes($eavEntityType, $entity); + } + + /** +@@ -105,7 +122,7 @@ public function execute($entityType, $entityData, $arguments = []) + $selects = []; + + /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ +- foreach ($this->getAttributes($entityType) as $attribute) { ++ foreach ($this->getEntityAttributes($entityType, new DataObject($entityData)) as $attribute) { + if (!$attribute->isStatic()) { + $attributeTables[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId(); + $attributesMap[$attribute->getAttributeId()] = $attribute->getAttributeCode(); +diff -Nuar a/vendor/magento/module-swatches/Model/Plugin/ProductImage.php b/vendor/magento/module-swatches/Model/Plugin/ProductImage.php +--- a/vendor/magento/module-swatches/Model/Plugin/ProductImage.php ++++ b/vendor/magento/module-swatches/Model/Plugin/ProductImage.php +@@ -69,7 +69,7 @@ public function beforeGetImage( + && ($location == self::CATEGORY_PAGE_GRID_LOCATION || $location == self::CATEGORY_PAGE_LIST_LOCATION)) { + $request = $this->request->getParams(); + if (is_array($request)) { +- $filterArray = $this->getFilterArray($request); ++ $filterArray = $this->getFilterArray($request, $product); + if (!empty($filterArray)) { + $product = $this->loadSimpleVariation($product, $filterArray); + } +@@ -99,16 +99,18 @@ protected function loadSimpleVariation(\Magento\Catalog\Model\Product $parentPro + * Get filters from request + * + * @param array $request ++ * @param \Magento\Catalog\Model\Product $product + * @return array + */ +- protected function getFilterArray(array $request) ++ private function getFilterArray(array $request, \Magento\Catalog\Model\Product $product) + { + $filterArray = []; +- $attributeCodes = $this->eavConfig->getEntityAttributeCodes(\Magento\Catalog\Model\Product::ENTITY); ++ $attributes = $this->eavConfig->getEntityAttributes(\Magento\Catalog\Model\Product::ENTITY, $product); ++ + foreach ($request as $code => $value) { +- if (in_array($code, $attributeCodes)) { +- $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $code); +- if ($attribute->getId() && $this->canReplaceImageWithSwatch($attribute)) { ++ if (isset($attributes[$code])) { ++ $attribute = $attributes[$code]; ++ if ($this->canReplaceImageWithSwatch($attribute)) { + $filterArray[$code] = $value; + } + } diff --git a/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch new file mode 100644 index 00000000..4f963b80 --- /dev/null +++ b/patches/MAGETWO-95591__customer_attributes_issue__2.2.6.patch @@ -0,0 +1,1419 @@ +diff -Nuar a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php +--- a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php ++++ b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Save.php +@@ -18,6 +18,8 @@ + use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator; + use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; + use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; ++use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Serialize\Serializer\FormData; + use Magento\Framework\Cache\FrontendInterface; + use Magento\Framework\Controller\ResultFactory; + use Magento\Framework\Controller\Result\Json; +@@ -68,6 +70,11 @@ class Save extends Attribute + */ + private $layoutFactory; + ++ /** ++ * @var FormData ++ */ ++ private $formDataSerializer; ++ + /** + * @param Context $context + * @param FrontendInterface $attributeLabelCache +@@ -80,6 +87,7 @@ class Save extends Attribute + * @param FilterManager $filterManager + * @param Product $productHelper + * @param LayoutFactory $layoutFactory ++ * @param FormData|null $formDataSerializer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -93,7 +101,8 @@ public function __construct( + CollectionFactory $groupCollectionFactory, + FilterManager $filterManager, + Product $productHelper, +- LayoutFactory $layoutFactory ++ LayoutFactory $layoutFactory, ++ FormData $formDataSerializer = null + ) { + parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); + $this->buildFactory = $buildFactory; +@@ -103,19 +112,37 @@ public function __construct( + $this->validatorFactory = $validatorFactory; + $this->groupCollectionFactory = $groupCollectionFactory; + $this->layoutFactory = $layoutFactory; ++ $this->formDataSerializer = $formDataSerializer ?? ObjectManager::getInstance()->get(FormData::class); + } + + /** +- * @return Redirect ++ * @inheritdoc ++ * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function execute() + { ++ try { ++ $optionData = $this->formDataSerializer->unserialize( ++ $this->getRequest()->getParam('serialized_options', '[]') ++ ); ++ } catch (\InvalidArgumentException $e) { ++ $message = __("The attribute couldn't be saved due to an error. Verify your information and try again. " ++ . "If the error persists, please try again later."); ++ $this->messageManager->addErrorMessage($message); ++ ++ return $this->returnResult('catalog/*/edit', ['_current' => true], ['error' => true]); ++ } ++ + $data = $this->getRequest()->getPostValue(); ++ $data = array_replace_recursive( ++ $data, ++ $optionData ++ ); ++ + if ($data) { +- $this->preprocessOptionsData($data); + $setId = $this->getRequest()->getParam('set'); + + $attributeSet = null; +@@ -124,7 +151,7 @@ public function execute() + $name = trim($name); + + try { +- /** @var $attributeSet Set */ ++ /** @var Set $attributeSet */ + $attributeSet = $this->buildFactory->create() + ->setEntityTypeId($this->_entityTypeId) + ->setSkeletonId($setId) +@@ -147,7 +174,7 @@ public function execute() + + $attributeId = $this->getRequest()->getParam('attribute_id'); + +- /** @var $model ProductAttributeInterface */ ++ /** @var ProductAttributeInterface $model */ + $model = $this->attributeFactory->create(); + if ($attributeId) { + $model->load($attributeId); +@@ -180,7 +207,7 @@ public function execute() + + //validate frontend_input + if (isset($data['frontend_input'])) { +- /** @var $inputType Validator */ ++ /** @var Validator $inputType */ + $inputType = $this->validatorFactory->create(); + if (!$inputType->isValid($data['frontend_input'])) { + foreach ($inputType->getMessages() as $message) { +@@ -313,28 +340,8 @@ public function execute() + } + + /** +- * Extract options data from serialized options field and append to data array. +- * +- * This logic is required to overcome max_input_vars php limit +- * that may vary and/or be inaccessible to change on different instances. ++ * Provides an initialized Result object. + * +- * @param array $data +- * @return void +- */ +- private function preprocessOptionsData(&$data) +- { +- if (isset($data['serialized_options'])) { +- $serializedOptions = json_decode($data['serialized_options'], JSON_OBJECT_AS_ARRAY); +- foreach ($serializedOptions as $serializedOption) { +- $option = []; +- parse_str($serializedOption, $option); +- $data = array_replace_recursive($data, $option); +- } +- } +- unset($data['serialized_options']); +- } +- +- /** + * @param string $path + * @param array $params + * @param array $response +diff -Nuar a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php +--- a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php ++++ b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Attribute/Validate.php +@@ -6,8 +6,15 @@ + */ + namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; + ++use Magento\Framework\Serialize\Serializer\FormData; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\DataObject; + ++/** ++ * Product attribute validate controller. ++ * ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ */ + class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute + { + const DEFAULT_MESSAGE_KEY = 'message'; +@@ -27,6 +34,11 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute + */ + private $multipleAttributeList; + ++ /** ++ * @var FormData ++ */ ++ private $formDataSerializer; ++ + /** + * Constructor + * +@@ -37,6 +49,7 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute + * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param array $multipleAttributeList ++ * @param FormData|null $formDataSerializer + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, +@@ -45,16 +58,19 @@ public function __construct( + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + \Magento\Framework\View\LayoutFactory $layoutFactory, +- array $multipleAttributeList = [] ++ array $multipleAttributeList = [], ++ FormData $formDataSerializer = null + ) { + parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); + $this->resultJsonFactory = $resultJsonFactory; + $this->layoutFactory = $layoutFactory; + $this->multipleAttributeList = $multipleAttributeList; ++ $this->formDataSerializer = $formDataSerializer ?? ObjectManager::getInstance()->get(FormData::class); + } + + /** +- * @return \Magento\Framework\Controller\ResultInterface ++ * @inheritdoc ++ * + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ +@@ -62,6 +78,16 @@ public function execute() + { + $response = new DataObject(); + $response->setError(false); ++ try { ++ $optionsData = $this->formDataSerializer->unserialize( ++ $this->getRequest()->getParam('serialized_options', '[]') ++ ); ++ } catch (\InvalidArgumentException $e) { ++ $message = __("The attribute couldn't be validated due to an error. Verify your information and try again. " ++ . "If the error persists, please try again later."); ++ $this->setMessageToResponse($response, [$message]); ++ $response->setError(true); ++ } + + $attributeCode = $this->getRequest()->getParam('attribute_code'); + $frontendLabel = $this->getRequest()->getParam('frontend_label'); +@@ -101,10 +127,10 @@ public function execute() + } + + $multipleOption = $this->getRequest()->getParam("frontend_input"); +- $multipleOption = null == $multipleOption ? 'select' : $multipleOption; ++ $multipleOption = (null === $multipleOption) ? 'select' : $multipleOption; + + if (isset($this->multipleAttributeList[$multipleOption]) && !(null == ($multipleOption))) { +- $options = $this->getRequest()->getParam($this->multipleAttributeList[$multipleOption]); ++ $options = $optionsData[$this->multipleAttributeList[$multipleOption]] ?? null; + $this->checkUniqueOption( + $response, + $options +@@ -122,7 +148,8 @@ public function execute() + } + + /** +- * Throws Exception if not unique values into options ++ * Throws Exception if not unique values into options. ++ * + * @param array $optionsValues + * @param array $deletedOptions + * @return bool +@@ -156,6 +183,8 @@ private function setMessageToResponse($response, $messages) + } + + /** ++ * Performs checking the uniqueness of the attribute options. ++ * + * @param DataObject $response + * @param array|null $options + * @return $this +diff -Nuar a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +--- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php ++++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +@@ -3,8 +3,10 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; + ++use Magento\Catalog\Api\Data\ProductAttributeInterface; + use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; + use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; + use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; +@@ -13,11 +15,16 @@ + use Magento\Eav\Api\Data\AttributeSetInterface; + use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; + use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; ++use Magento\Framework\Message\ManagerInterface; ++use Magento\Framework\Serialize\Serializer\FormData; ++use Magento\Framework\Controller\ResultFactory; + use Magento\Framework\Filter\FilterManager; + use Magento\Catalog\Helper\Product as ProductHelper; ++use Magento\Framework\View\Element\Messages; + use Magento\Framework\View\LayoutFactory; + use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; + use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator as InputTypeValidator; ++use Magento\Framework\View\LayoutInterface; + + /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) +@@ -79,6 +86,21 @@ class SaveTest extends AttributeTest + */ + protected $inputTypeValidatorMock; + ++ /** ++ * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $messageManagerMock; ++ ++ /** ++ * @var FormData|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $formDataSerializerMock; ++ ++ /** ++ * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $productAttributeMock; ++ + protected function setUp() + { + parent::setUp(); +@@ -108,6 +130,7 @@ protected function setUp() + ->disableOriginalConstructor() + ->getMock(); + $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) ++ ->setMethods(['setData', 'setPath']) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) +@@ -119,6 +142,15 @@ protected function setUp() + $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) + ->disableOriginalConstructor() + ->getMock(); ++ $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) ++ ->setMethods(['getId', 'get']) ++ ->getMockForAbstractClass(); + + $this->buildFactoryMock->expects($this->any()) + ->method('create') +@@ -126,6 +158,9 @@ protected function setUp() + $this->validatorFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->inputTypeValidatorMock); ++ $this->attributeFactoryMock ++ ->method('create') ++ ->willReturn($this->productAttributeMock); + } + + /** +@@ -135,6 +170,7 @@ protected function getModel() + { + return $this->objectManager->getObject(Save::class, [ + 'context' => $this->contextMock, ++ 'messageManager' => $this->messageManagerMock, + 'attributeLabelCache' => $this->attributeLabelCacheMock, + 'coreRegistry' => $this->coreRegistryMock, + 'resultPageFactory' => $this->resultPageFactoryMock, +@@ -145,11 +181,22 @@ protected function getModel() + 'validatorFactory' => $this->validatorFactoryMock, + 'groupCollectionFactory' => $this->groupCollectionFactoryMock, + 'layoutFactory' => $this->layoutFactoryMock, ++ 'formDataSerializer' => $this->formDataSerializerMock, + ]); + } + + public function testExecuteWithEmptyData() + { ++ $this->requestMock->expects($this->any()) ++ ->method('getParam') ++ ->willReturnMap([ ++ ['isAjax', null, null], ++ ['serialized_options', '[]', ''], ++ ]); ++ $this->formDataSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with('') ++ ->willReturn([]); + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn([]); +@@ -170,6 +217,22 @@ public function testExecute() + 'frontend_input' => 'test_frontend_input', + ]; + ++ $this->requestMock->expects($this->any()) ++ ->method('getParam') ++ ->willReturnMap([ ++ ['isAjax', null, null], ++ ['serialized_options', '[]', ''], ++ ]); ++ $this->formDataSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with('') ++ ->willReturn([]); ++ $this->productAttributeMock->expects($this->once()) ++ ->method('getId') ++ ->willReturn(1); ++ $this->productAttributeMock->expects($this->once()) ++ ->method('getAttributeCode') ++ ->willReturn('test_code'); + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn($data); +@@ -203,4 +266,74 @@ public function testExecute() + + $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); + } ++ ++ /** ++ * @return void ++ * @throws \Magento\Framework\Exception\NotFoundException ++ */ ++ public function testExecuteWithOptionsDataError() ++ { ++ $serializedOptions = '{"key":"value"}'; ++ $message = "The attribute couldn't be saved due to an error. Verify your information and try again. " ++ . "If the error persists, please try again later."; ++ ++ $this->requestMock->expects($this->any()) ++ ->method('getParam') ++ ->willReturnMap([ ++ ['isAjax', null, true], ++ ['serialized_options', '[]', $serializedOptions], ++ ]); ++ $this->formDataSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedOptions) ++ ->willThrowException(new \InvalidArgumentException('Some exception')); ++ $this->messageManagerMock->expects($this->once()) ++ ->method('addErrorMessage') ++ ->with($message); ++ $this->addReturnResultConditions('catalog/*/edit', ['_current' => true], ['error' => true]); ++ ++ $this->getModel()->execute(); ++ } ++ ++ /** ++ * @param string $path ++ * @param array $params ++ * @param array $response ++ * @return void ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ private function addReturnResultConditions(string $path = '', array $params = [], array $response = []) ++ { ++ $layoutMock = $this->getMockBuilder(LayoutInterface::class) ++ ->setMethods(['initMessages', 'getMessagesBlock']) ++ ->getMockForAbstractClass(); ++ $this->layoutFactoryMock->expects($this->once()) ++ ->method('create') ++ ->with() ++ ->willReturn($layoutMock); ++ $layoutMock->expects($this->once()) ++ ->method('initMessages') ++ ->with(); ++ $messageBlockMock = $this->getMockBuilder(Messages::class) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $layoutMock->expects($this->once()) ++ ->method('getMessagesBlock') ++ ->willReturn($messageBlockMock); ++ $messageBlockMock->expects($this->once()) ++ ->method('getGroupedHtml') ++ ->willReturn('message1'); ++ $this->resultFactoryMock->expects($this->once()) ++ ->method('create') ++ ->with(ResultFactory::TYPE_JSON) ++ ->willReturn($this->redirectMock); ++ $response = array_merge($response, [ ++ 'messages' => ['message1'], ++ 'params' => $params, ++ ]); ++ $this->redirectMock->expects($this->once()) ++ ->method('setData') ++ ->with($response) ++ ->willReturnSelf(); ++ } + } +diff -Nuar a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +--- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php ++++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +@@ -9,6 +9,7 @@ + use Magento\Catalog\Model\ResourceModel\Eav\Attribute; + use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; + use Magento\Eav\Model\Entity\Attribute\Set as AttributeSet; ++use Magento\Framework\Serialize\Serializer\FormData; + use Magento\Framework\Controller\Result\Json as ResultJson; + use Magento\Framework\Controller\Result\JsonFactory as ResultJsonFactory; + use Magento\Framework\Escaper; +@@ -61,6 +62,11 @@ class ValidateTest extends AttributeTest + */ + protected $layoutMock; + ++ /** ++ * @var FormData|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $formDataSerializer; ++ + protected function setUp() + { + parent::setUp(); +@@ -86,6 +92,9 @@ protected function setUp() + ->getMock(); + $this->layoutMock = $this->getMockBuilder(LayoutInterface::class) + ->getMockForAbstractClass(); ++ $this->formDataSerializer = $this->getMockBuilder(FormData::class) ++ ->disableOriginalConstructor() ++ ->getMock(); + + $this->contextMock->expects($this->any()) + ->method('getObjectManager') +@@ -100,25 +109,28 @@ protected function getModel() + return $this->objectManager->getObject( + Validate::class, + [ +- 'context' => $this->contextMock, +- 'attributeLabelCache' => $this->attributeLabelCacheMock, +- 'coreRegistry' => $this->coreRegistryMock, +- 'resultPageFactory' => $this->resultPageFactoryMock, +- 'resultJsonFactory' => $this->resultJsonFactoryMock, +- 'layoutFactory' => $this->layoutFactoryMock, +- 'multipleAttributeList' => ['select' => 'option'] ++ 'context' => $this->contextMock, ++ 'attributeLabelCache' => $this->attributeLabelCacheMock, ++ 'coreRegistry' => $this->coreRegistryMock, ++ 'resultPageFactory' => $this->resultPageFactoryMock, ++ 'resultJsonFactory' => $this->resultJsonFactoryMock, ++ 'layoutFactory' => $this->layoutFactoryMock, ++ 'multipleAttributeList' => ['select' => 'option'], ++ 'formDataSerializer' => $this->formDataSerializer, + ] + ); + } + + public function testExecute() + { ++ $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['frontend_label', null, 'test_frontend_label'], + ['attribute_code', null, 'test_attribute_code'], + ['new_attribute_set_name', null, 'test_attribute_set_name'], ++ ['serialized_options', '[]', $serializedOptions], + ]); + $this->objectManagerMock->expects($this->exactly(2)) + ->method('create') +@@ -160,6 +172,7 @@ public function testExecute() + */ + public function testUniqueValidation(array $options, $isError) + { ++ $serializedOptions = '{"key":"value"}'; + $countFunctionCalls = ($isError) ? 6 : 5; + $this->requestMock->expects($this->exactly($countFunctionCalls)) + ->method('getParam') +@@ -167,10 +180,15 @@ public function testUniqueValidation(array $options, $isError) + ['frontend_label', null, null], + ['attribute_code', null, "test_attribute_code"], + ['new_attribute_set_name', null, 'test_attribute_set_name'], +- ['option', null, $options], +- ['message_key', null, Validate::DEFAULT_MESSAGE_KEY] ++ ['message_key', null, Validate::DEFAULT_MESSAGE_KEY], ++ ['serialized_options', '[]', $serializedOptions], + ]); + ++ $this->formDataSerializer->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedOptions) ++ ->willReturn($options); ++ + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn($this->attributeMock); +@@ -203,68 +221,84 @@ public function provideUniqueData() + return [ + 'no values' => [ + [ +- 'delete' => [ +- "option_0" => "", +- "option_1" => "", +- "option_2" => "", +- ] +- ], false ++ 'option' => [ ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "", ++ "option_2" => "", ++ ], ++ ], ++ ++ ], ++ false, + ], + 'valid options' => [ + [ +- 'value' => [ +- "option_0" => [1, 0], +- "option_1" => [2, 0], +- "option_2" => [3, 0], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [1, 0], ++ "option_1" => [2, 0], ++ "option_2" => [3, 0], ++ ], ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "", ++ "option_2" => "", ++ ], + ], +- 'delete' => [ +- "option_0" => "", +- "option_1" => "", +- "option_2" => "", +- ] +- ], false ++ ], ++ false, + ], + 'duplicate options' => [ + [ +- 'value' => [ +- "option_0" => [1, 0], +- "option_1" => [1, 0], +- "option_2" => [3, 0], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [1, 0], ++ "option_1" => [1, 0], ++ "option_2" => [3, 0], ++ ], ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "", ++ "option_2" => "", ++ ], + ], +- 'delete' => [ +- "option_0" => "", +- "option_1" => "", +- "option_2" => "", +- ] +- ], true ++ ], ++ true, + ], + 'duplicate and deleted' => [ + [ +- 'value' => [ +- "option_0" => [1, 0], +- "option_1" => [1, 0], +- "option_2" => [3, 0], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [1, 0], ++ "option_1" => [1, 0], ++ "option_2" => [3, 0], ++ ], ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "1", ++ "option_2" => "", ++ ], + ], +- 'delete' => [ +- "option_0" => "", +- "option_1" => "1", +- "option_2" => "", +- ] +- ], false ++ ], ++ false, + ], + 'empty and deleted' => [ + [ +- 'value' => [ +- "option_0" => [1, 0], +- "option_1" => [2, 0], +- "option_2" => ["", ""], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [1, 0], ++ "option_1" => [2, 0], ++ "option_2" => ["", ""], ++ ], ++ 'delete' => [ ++ "option_0" => "", ++ "option_1" => "", ++ "option_2" => "1", ++ ], + ], +- 'delete' => [ +- "option_0" => "", +- "option_1" => "", +- "option_2" => "1", +- ] +- ], false ++ ], ++ false, + ], + ]; + } +@@ -278,6 +312,7 @@ public function provideUniqueData() + */ + public function testEmptyOption(array $options, $result) + { ++ $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ +@@ -285,10 +320,15 @@ public function testEmptyOption(array $options, $result) + ['frontend_input', 'select', 'multipleselect'], + ['attribute_code', null, "test_attribute_code"], + ['new_attribute_set_name', null, 'test_attribute_set_name'], +- ['option', null, $options], + ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], ++ ['serialized_options', '[]', $serializedOptions], + ]); + ++ $this->formDataSerializer->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedOptions) ++ ->willReturn($options); ++ + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn($this->attributeMock); +@@ -320,32 +360,38 @@ public function provideEmptyOption() + return [ + 'empty admin scope options' => [ + [ +- 'value' => [ +- "option_0" => [''], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [''], ++ ], + ], + ], + (object) [ + 'error' => true, + 'message' => 'The value of Admin scope can\'t be empty.', +- ] ++ ], + ], + 'not empty admin scope options' => [ + [ +- 'value' => [ +- "option_0" => ['asdads'], ++ 'option' => [ ++ 'value' => [ ++ "option_0" => ['asdads'], ++ ], + ], + ], + (object) [ + 'error' => false, +- ] ++ ], + ], + 'empty admin scope options and deleted' => [ + [ +- 'value' => [ +- "option_0" => [''], +- ], +- 'delete' => [ +- 'option_0' => '1', ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [''], ++ ], ++ 'delete' => [ ++ 'option_0' => '1', ++ ], + ], + ], + (object) [ +@@ -354,11 +400,13 @@ public function provideEmptyOption() + ], + 'empty admin scope options and not deleted' => [ + [ +- 'value' => [ +- "option_0" => [''], +- ], +- 'delete' => [ +- 'option_0' => '0', ++ 'option' => [ ++ 'value' => [ ++ "option_0" => [''], ++ ], ++ 'delete' => [ ++ 'option_0' => '0', ++ ], + ], + ], + (object) [ +@@ -368,4 +416,55 @@ public function provideEmptyOption() + ], + ]; + } ++ ++ /** ++ * @return void ++ * @throws \Magento\Framework\Exception\NotFoundException ++ */ ++ public function testExecuteWithOptionsDataError() ++ { ++ $serializedOptions = '{"key":"value"}'; ++ $message = "The attribute couldn't be validated due to an error. Verify your information and try again. " ++ . "If the error persists, please try again later."; ++ $this->requestMock->expects($this->any()) ++ ->method('getParam') ++ ->willReturnMap([ ++ ['frontend_label', null, 'test_frontend_label'], ++ ['attribute_code', null, 'test_attribute_code'], ++ ['new_attribute_set_name', null, 'test_attribute_set_name'], ++ ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], ++ ['serialized_options', '[]', $serializedOptions], ++ ]); ++ ++ $this->formDataSerializer->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedOptions) ++ ->willThrowException(new \InvalidArgumentException('Some exception')); ++ ++ $this->objectManagerMock->expects($this->once()) ++ ->method('create') ++ ->willReturnMap([ ++ [\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, [], $this->attributeMock], ++ [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] ++ ]); ++ ++ $this->attributeMock->expects($this->once()) ++ ->method('loadByCode') ++ ->willReturnSelf(); ++ $this->attributeSetMock->expects($this->never()) ++ ->method('setEntityTypeId') ++ ->willReturnSelf(); ++ $this->resultJsonFactoryMock->expects($this->once()) ++ ->method('create') ++ ->willReturn($this->resultJson); ++ $this->resultJson->expects($this->once()) ++ ->method('setJsonData') ++ ->with(json_encode([ ++ 'error' => true, ++ 'message' => $message, ++ ])) ++ ->willReturnSelf(); ++ ++ $this->getModel()->execute(); ++ } + } +diff -Nuar a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php +--- a/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php ++++ b/vendor/magento/module-catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php +@@ -9,8 +9,9 @@ + use Magento\Catalog\Controller\Adminhtml\Product\Attribute; + use Magento\Framework\App\RequestInterface; + use Magento\Framework\Cache\FrontendInterface; ++use Magento\Framework\Message\ManagerInterface; + use Magento\Framework\Registry; +-use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; ++use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + use Magento\Framework\View\Result\PageFactory; + use Magento\Framework\Controller\ResultFactory; + +@@ -20,7 +21,7 @@ + class AttributeTest extends \PHPUnit\Framework\TestCase + { + /** +- * @var ObjectManager ++ * @var ObjectManagerHelper + */ + protected $objectManager; + +@@ -54,9 +55,14 @@ class AttributeTest extends \PHPUnit\Framework\TestCase + */ + protected $resultFactoryMock; + ++ /** ++ * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject ++ */ ++ private $messageManager; ++ + protected function setUp() + { +- $this->objectManager = new ObjectManager($this); ++ $this->objectManager = new ObjectManagerHelper($this); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); +@@ -74,6 +80,9 @@ protected function setUp() + $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->getMock(); ++ $this->messageManager = $this->getMockBuilder(ManagerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMock(); + + $this->contextMock->expects($this->any()) + ->method('getRequest') +@@ -81,6 +90,9 @@ protected function setUp() + $this->contextMock->expects($this->any()) + ->method('getResultFactory') + ->willReturn($this->resultFactoryMock); ++ $this->contextMock->expects($this->once()) ++ ->method('getMessageManager') ++ ->willReturn($this->messageManager); + } + + /** +diff -Nuar a/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +--- a/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml ++++ b/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +@@ -40,13 +40,16 @@ function getFrontTab() { + + function checkOptionsPanelVisibility(){ + if($('manage-options-panel')){ +- var panel = $('manage-options-panel').up('.fieldset'); ++ var panel = $('manage-options-panel').up('.fieldset'), ++ activePanelClass = 'selected-type-options'; + + if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect')){ + panel.show(); ++ panel.addClass(activePanelClass); + } + else { + panel.hide(); ++ panel.removeClass(activePanelClass); + } + } + } +diff -Nuar a/vendor/magento/module-catalog/view/adminhtml/web/js/options.js b/vendor/magento/module-catalog/view/adminhtml/web/js/options.js +--- a/vendor/magento/module-catalog/view/adminhtml/web/js/options.js ++++ b/vendor/magento/module-catalog/view/adminhtml/web/js/options.js +@@ -20,7 +20,6 @@ define([ + + return function (config) { + var optionPanel = jQuery('#manage-options-panel'), +- optionsValues = [], + editForm = jQuery('#edit_form'), + attributeOption = { + table: $('attribute-options-table'), +@@ -145,7 +144,9 @@ define([ + + return optionDefaultInputType; + } +- }; ++ }, ++ tableBody = jQuery(), ++ activePanelClass = 'selected-type-options'; + + if ($('add_new_option_button')) { + Event.observe('add_new_option_button', 'click', attributeOption.add.bind(attributeOption, {}, true)); +@@ -180,30 +181,32 @@ define([ + }); + }); + } +- editForm.on('submit', function () { +- optionPanel.find('input') +- .each(function () { +- if (this.disabled) { +- return; ++ editForm.on('beforeSubmit', function () { ++ var optionContainer = optionPanel.find('table tbody'), ++ optionsValues; ++ ++ if (optionPanel.hasClass(activePanelClass)) { ++ optionsValues = jQuery.map( ++ optionContainer.find('tr'), ++ function (row) { ++ return jQuery(row).find('input, select, textarea').serialize(); + } +- +- if (this.type === 'checkbox' || this.type === 'radio') { +- if (this.checked) { +- optionsValues.push(this.name + '=' + jQuery(this).val()); +- } +- } else { +- optionsValues.push(this.name + '=' + jQuery(this).val()); +- } +- }); +- jQuery('') +- .attr({ +- type: 'hidden', +- name: 'serialized_options' +- }) +- .val(JSON.stringify(optionsValues)) +- .prependTo(editForm); +- optionPanel.find('table') +- .replaceWith(jQuery('
').text(jQuery.mage.__('Sending attribute values as package.'))); ++ ); ++ jQuery('') ++ .attr({ ++ type: 'hidden', ++ name: 'serialized_options' ++ }) ++ .val(JSON.stringify(optionsValues)) ++ .prependTo(editForm); ++ } ++ tableBody = optionContainer.detach(); ++ }); ++ editForm.on('afterValidate.error highlight.validate', function () { ++ if (optionPanel.hasClass(activePanelClass)) { ++ optionPanel.find('table').append(tableBody); ++ jQuery('input[name="serialized_options"]').remove(); ++ } + }); + window.attributeOption = attributeOption; + window.optionDefaultInputType = attributeOption.getOptionInputType(); +diff -Nuar a/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php b/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php +--- a/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php ++++ b/vendor/magento/module-swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php +@@ -16,6 +16,8 @@ + class Save + { + /** ++ * Performs the conversion of the frontend input value. ++ * + * @param Attribute\Save $subject + * @param RequestInterface $request + * @return array +@@ -26,15 +28,6 @@ public function beforeDispatch(Attribute\Save $subject, RequestInterface $reques + $data = $request->getPostValue(); + + if (isset($data['frontend_input'])) { +- //Data is serialized to overcome issues caused by max_input_vars value if it's modification is unavailable. +- //See subject controller code and comments for more info. +- if (isset($data['serialized_swatch_values']) +- && in_array($data['frontend_input'], ['swatch_visual', 'swatch_text']) +- ) { +- $data['serialized_options'] = $data['serialized_swatch_values']; +- unset($data['serialized_swatch_values']); +- } +- + switch ($data['frontend_input']) { + case 'swatch_visual': + $data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL; +diff -Nuar a/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js b/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js +--- a/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js ++++ b/vendor/magento/module-swatches/view/adminhtml/web/js/product-attributes.js +@@ -16,7 +16,8 @@ define([ + 'use strict'; + + return function (optionConfig) { +- var swatchProductAttributes = { ++ var activePanelClass = 'selected-type-options', ++ swatchProductAttributes = { + frontendInput: $('#frontend_input'), + isFilterable: $('#is_filterable'), + isFilterableInSearch: $('#is_filterable_in_search'), +@@ -337,6 +338,7 @@ define([ + */ + _showPanel: function (el) { + el.closest('.fieldset').show(); ++ el.addClass(activePanelClass); + this._render(el.attr('id')); + }, + +@@ -346,6 +348,7 @@ define([ + */ + _hidePanel: function (el) { + el.closest('.fieldset').hide(); ++ el.removeClass(activePanelClass); + }, + + /** +@@ -413,7 +416,11 @@ define([ + }; + + $(function () { +- var editForm = $('#edit_form'); ++ var editForm = $('#edit_form'), ++ swatchVisualPanel = $('#swatch-visual-options-panel'), ++ swatchTextPanel = $('#swatch-text-options-panel'), ++ tableBody = $(), ++ activePanel = $(); + + $('#frontend_input').bind('change', function () { + swatchProductAttributes.bindAttributeInputType(); +@@ -429,30 +436,35 @@ define([ + .collapsable() + .collapse('hide'); + +- editForm.on('submit', function () { +- var activePanel, +- swatchValues = [], +- swatchVisualPanel = $('#swatch-visual-options-panel'), +- swatchTextPanel = $('#swatch-text-options-panel'); ++ editForm.on('beforeSubmit', function () { ++ var optionContainer, optionsValues; + +- activePanel = swatchTextPanel.is(':visible') ? swatchTextPanel : swatchVisualPanel; ++ activePanel = swatchTextPanel.hasClass(activePanelClass) ? swatchTextPanel : swatchVisualPanel; ++ optionContainer = activePanel.find('table tbody'); + +- activePanel.find('table input') +- .each(function () { +- swatchValues.push(this.name + '=' + $(this).val()); +- }); ++ if (activePanel.hasClass(activePanelClass)) { ++ optionsValues = $.map( ++ optionContainer.find('tr'), ++ function (row) { ++ return $(row).find('input, select, textarea').serialize(); ++ } ++ ); ++ $('') ++ .attr({ ++ type: 'hidden', ++ name: 'serialized_options' ++ }) ++ .val(JSON.stringify(optionsValues)) ++ .prependTo(editForm); ++ } + +- $('').attr({ +- type: 'hidden', +- name: 'serialized_swatch_values' +- }) +- .val(JSON.stringify(swatchValues)) +- .prependTo(editForm); +- +- [swatchVisualPanel, swatchTextPanel].forEach(function (el) { +- $(el).find('table') +- .replaceWith($('
').text($.mage.__('Sending swatch values as package.'))); +- }); ++ tableBody = optionContainer.detach(); ++ }); ++ editForm.on('afterValidate.error highlight.validate', function () { ++ if (activePanel.hasClass(activePanelClass)) { ++ activePanel.find('table').append(tableBody); ++ $('input[name="serialized_options"]').remove(); ++ } + }); + }); + +diff -Nuar a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php +--- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php ++++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php +@@ -91,15 +91,18 @@ public function persist(FixtureInterface $fixture = null) + $data['frontend_label'] = [0 => $data['frontend_label']]; + + if (isset($data['options'])) { ++ $optionsData = []; + foreach ($data['options'] as $key => $values) { ++ $optionRowData = []; + $index = 'option_' . $key; + if ($values['is_default'] == 'Yes') { +- $data['default'][] = $index; ++ $optionRowData['default'][] = $index; + } +- $data['option']['value'][$index] = [$values['admin'], $values['view']]; +- $data['option']['order'][$index] = $key; ++ $optionRowData['option']['value'][$index] = [$values['admin'], $values['view']]; ++ $optionRowData['option']['order'][$index] = $key; ++ $optionsData[] = $optionRowData; + } +- unset($data['options']); ++ $data['options'] = $optionsData; + } + + $data = $this->changeStructureOfTheData($data); +@@ -134,11 +137,39 @@ public function persist(FixtureInterface $fixture = null) + } + + /** ++ * Additional data handling. ++ * + * @param array $data + * @return array + */ +- protected function changeStructureOfTheData(array $data) ++ protected function changeStructureOfTheData(array $data): array + { ++ if (!isset($data['options'])) { ++ return $data; ++ } ++ ++ $serializedOptions = $this->getSerializeOptions($data['options']); ++ if ($serializedOptions) { ++ $data['serialized_options'] = $serializedOptions; ++ unset($data['options']); ++ } ++ + return $data; + } ++ ++ /** ++ * Provides serialized product attribute options. ++ * ++ * @param array $data ++ * @return string ++ */ ++ protected function getSerializeOptions(array $data): string ++ { ++ $options = []; ++ foreach ($data as $optionRowData) { ++ $options[] = http_build_query($optionRowData); ++ } ++ ++ return json_encode($options); ++ } + } +diff -Nuar a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php +--- a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php ++++ b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php +@@ -29,21 +29,32 @@ public function __construct(DataInterface $configuration, EventManagerInterface + ]; + } + ++ /** ++ * @inheritdoc ++ */ ++ protected function changeStructureOfTheData(array $data): array ++ { ++ return parent::changeStructureOfTheData($data); ++ } ++ + /** + * Re-map options from default options structure to swatches structure, + * as swatches was initially created with name convention differ from other attributes. + * +- * @param array $data +- * @return array ++ * @inheritdoc + */ +- protected function changeStructureOfTheData(array $data) ++ protected function getSerializeOptions(array $data): string + { +- $data = parent::changeStructureOfTheData($data); +- $data['optiontext'] = $data['option']; +- $data['swatchtext'] = [ +- 'value' => $data['option']['value'] +- ]; +- unset($data['option']); +- return $data; ++ $options = []; ++ foreach ($data as $optionRowData) { ++ $optionRowData['optiontext'] = $optionRowData['option']; ++ $optionRowData['swatchtext'] = [ ++ 'value' => $optionRowData['option']['value'] ++ ]; ++ unset($optionRowData['option']); ++ $options[] = http_build_query($optionRowData); ++ } ++ ++ return json_encode($options); + } + } +diff -Nuar a/vendor/magento/framework/Serialize/Serializer/FormData.php b/vendor/magento/framework/Serialize/Serializer/FormData.php +--- /dev/null ++++ b/vendor/magento/framework/Serialize/Serializer/FormData.php +@@ -0,0 +1,55 @@ ++serializer = $serializer; ++ } ++ ++ /** ++ * Provides form data from the serialized data. ++ * ++ * @param string $serializedData ++ * @return array ++ * @throws \InvalidArgumentException ++ */ ++ public function unserialize(string $serializedData): array ++ { ++ $encodedFields = $this->serializer->unserialize($serializedData); ++ ++ if (!is_array($encodedFields)) { ++ throw new \InvalidArgumentException('Unable to unserialize value.'); ++ } ++ ++ $formData = []; ++ foreach ($encodedFields as $item) { ++ $decodedFieldData = []; ++ parse_str($item, $decodedFieldData); ++ $formData = array_replace_recursive($formData, $decodedFieldData); ++ } ++ ++ return $formData; ++ } ++} +diff -Nuar a/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php +--- /dev/null ++++ b/vendor/magento/framework/Serialize/Test/Unit/Serializer/FormDataTest.php +@@ -0,0 +1,106 @@ ++jsonSerializerMock = $this->createMock(Json::class); ++ $this->formDataSerializer = new FormData($this->jsonSerializerMock); ++ } ++ ++ /** ++ * @param string $serializedData ++ * @param array $encodedFields ++ * @param array $expectedFormData ++ * @return void ++ * @dataProvider unserializeDataProvider ++ */ ++ public function testUnserialize(string $serializedData, array $encodedFields, array $expectedFormData) ++ { ++ $this->jsonSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedData) ++ ->willReturn($encodedFields); ++ ++ $this->assertEquals($expectedFormData, $this->formDataSerializer->unserialize($serializedData)); ++ } ++ ++ /** ++ * @return array ++ */ ++ public function unserializeDataProvider(): array ++ { ++ return [ ++ [ ++ 'serializedData' => ++ '["option[order][option_0]=1","option[value][option_0]=1","option[delete][option_0]="]', ++ 'encodedFields' => [ ++ 'option[order][option_0]=1', ++ 'option[value][option_0]=1', ++ 'option[delete][option_0]=', ++ ], ++ 'expectedFormData' => [ ++ 'option' => [ ++ 'order' => [ ++ 'option_0' => '1', ++ ], ++ 'value' => [ ++ 'option_0' => '1', ++ ], ++ 'delete' => [ ++ 'option_0' => '', ++ ], ++ ], ++ ], ++ ], ++ [ ++ 'serializedData' => '[]', ++ 'encodedFields' => [], ++ 'expectedFormData' => [], ++ ], ++ ]; ++ } ++ ++ /** ++ * @return void ++ * @expectedException InvalidArgumentException ++ * @expectedExceptionMessage Unable to unserialize value. ++ */ ++ public function testUnserializeWithWrongSerializedData() ++ { ++ $serializedData = 'test'; ++ ++ $this->jsonSerializerMock->expects($this->once()) ++ ->method('unserialize') ++ ->with($serializedData) ++ ->willReturn('test'); ++ ++ $this->formDataSerializer->unserialize($serializedData); ++ } ++} +diff -Nuar a/lib/web/mage/backend/validation.js b/lib/web/mage/backend/validation.js +--- a/lib/web/mage/backend/validation.js ++++ b/lib/web/mage/backend/validation.js +@@ -171,6 +171,7 @@ + this._submit(); + } else { + this._showErrors(response); ++ $(this.element[0]).trigger('afterValidate.error'); + $('body').trigger('processStop'); + } + }, diff --git a/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch b/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch new file mode 100644 index 00000000..be518ee0 --- /dev/null +++ b/patches/MAGETWO-98129__transaction_field_is_removed_by_authorize_net__2.2.0.patch @@ -0,0 +1,322 @@ +diff -Nuar a/vendor/magento/module-authorizenet/Model/Directpost.php b/vendor/magento/module-authorizenet/Model/Directpost.php +--- a/vendor/magento/module-authorizenet/Model/Directpost.php ++++ b/vendor/magento/module-authorizenet/Model/Directpost.php +@@ -543,15 +543,16 @@ + public function validateResponse() + { + $response = $this->getResponse(); +- //md5 check +- if (!$this->getConfigData('trans_md5') +- || !$this->getConfigData('login') +- || !$response->isValidHash($this->getConfigData('trans_md5'), $this->getConfigData('login')) ++ $hashConfigKey = !empty($response->getData('x_SHA2_Hash')) ? 'signature_key' : 'trans_md5'; ++ ++ //hash check ++ if (!$response->isValidHash($this->getConfigData($hashConfigKey), $this->getConfigData('login')) + ) { + throw new \Magento\Framework\Exception\LocalizedException( + __('The transaction was declined because the response hash validation failed.') + ); + } ++ + return true; + } + +diff -Nuar a/vendor/magento/module-authorizenet/Model/Directpost/Request.php b/vendor/magento/module-authorizenet/Model/Directpost/Request.php +--- a/vendor/magento/module-authorizenet/Model/Directpost/Request.php ++++ b/vendor/magento/module-authorizenet/Model/Directpost/Request.php +@@ -7,6 +7,7 @@ + namespace Magento\Authorizenet\Model\Directpost; + + use Magento\Authorizenet\Model\Request as AuthorizenetRequest; ++use Magento\Framework\Intl\DateTimeFactory; + + /** + * Authorize.net request model for DirectPost model +@@ -18,9 +19,33 @@ + */ + protected $_transKey = null; + ++ /** ++ * Hexadecimal signature key. ++ * ++ * @var string ++ */ ++ private $signatureKey = ''; ++ ++ /** ++ * @var DateTimeFactory ++ */ ++ private $dateTimeFactory; ++ ++ /** ++ * @param DateTimeFactory $dateTimeFactory ++ * @param array $data ++ */ ++ public function __construct( ++ DateTimeFactory $dateTimeFactory, ++ array $data = [] ++ ) { ++ $this->dateTimeFactory = $dateTimeFactory; ++ parent::__construct($data); ++ } ++ + /** + * Return merchant transaction key. +- * Needed to generate sign. ++ * Needed to generate MD5 sign. + * + * @return string + */ +@@ -31,7 +56,7 @@ + + /** + * Set merchant transaction key. +- * Needed to generate sign. ++ * Needed to generate MD5 sign. + * + * @param string $transKey + * @return $this +@@ -43,7 +68,7 @@ + } + + /** +- * Generates the fingerprint for request. ++ * Generates the MD5 fingerprint for request. + * + * @param string $merchantApiLoginId + * @param string $merchantTransactionKey +@@ -63,7 +88,7 @@ + ) { + return hash_hmac( + "md5", +- $merchantApiLoginId . "^" . $fpSequence . "^" . $fpTimestamp . "^" . $amount . "^" . $currencyCode, ++ $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode, + $merchantTransactionKey + ); + } +@@ -85,6 +110,7 @@ + ->setXRelayUrl($paymentMethod->getRelayUrl()); + + $this->_setTransactionKey($paymentMethod->getConfigData('trans_key')); ++ $this->setSignatureKey($paymentMethod->getConfigData('signature_key')); + return $this; + } + +@@ -168,17 +194,81 @@ + */ + public function signRequestData() + { +- $fpTimestamp = time(); +- $hash = $this->generateRequestSign( +- $this->getXLogin(), +- $this->_getTransactionKey(), +- $this->getXAmount(), +- $this->getXCurrencyCode(), +- $this->getXFpSequence(), +- $fpTimestamp +- ); ++ $fpDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); ++ $fpTimestamp = $fpDate->getTimestamp(); ++ ++ if (!empty($this->getSignatureKey())) { ++ $hash = $this->generateSha2RequestSign( ++ $this->getXLogin(), ++ $this->getSignatureKey(), ++ $this->getXAmount(), ++ $this->getXCurrencyCode(), ++ $this->getXFpSequence(), ++ $fpTimestamp ++ ); ++ } else { ++ $hash = $this->generateRequestSign( ++ $this->getXLogin(), ++ $this->_getTransactionKey(), ++ $this->getXAmount(), ++ $this->getXCurrencyCode(), ++ $this->getXFpSequence(), ++ $fpTimestamp ++ ); ++ } ++ + $this->setXFpTimestamp($fpTimestamp); + $this->setXFpHash($hash); ++ + return $this; + } ++ ++ /** ++ * Generates the SHA2 fingerprint for request. ++ * ++ * @param string $merchantApiLoginId ++ * @param string $merchantSignatureKey ++ * @param string $amount ++ * @param string $currencyCode ++ * @param string $fpSequence An invoice number or random number. ++ * @param string $fpTimestamp ++ * @return string The fingerprint. ++ */ ++ private function generateSha2RequestSign( ++ $merchantApiLoginId, ++ $merchantSignatureKey, ++ $amount, ++ $currencyCode, ++ $fpSequence, ++ $fpTimestamp ++ ): string { ++ $message = $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode; ++ ++ return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantSignatureKey))); ++ } ++ ++ /** ++ * Return merchant hexadecimal signature key. ++ * ++ * Needed to generate SHA2 sign. ++ * ++ * @return string ++ */ ++ private function getSignatureKey(): string ++ { ++ return $this->signatureKey; ++ } ++ ++ /** ++ * Set merchant hexadecimal signature key. ++ * ++ * Needed to generate SHA2 sign. ++ * ++ * @param string $signatureKey ++ * @return void ++ */ ++ private function setSignatureKey(string $signatureKey) ++ { ++ $this->signatureKey = $signatureKey; ++ } + } +diff -Nuar a/vendor/magento/module-authorizenet/Model/Directpost/Response.php b/vendor/magento/module-authorizenet/Model/Directpost/Response.php +--- a/vendor/magento/module-authorizenet/Model/Directpost/Response.php ++++ b/vendor/magento/module-authorizenet/Model/Directpost/Response.php +@@ -24,27 +24,33 @@ + */ + public function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) + { +- if (!$amount) { +- $amount = '0.00'; +- } +- + return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); + } + + /** + * Return if is valid order id. + * +- * @param string $merchantMd5 ++ * @param string $storedHash + * @param string $merchantApiLogin + * @return bool + */ +- public function isValidHash($merchantMd5, $merchantApiLogin) ++ public function isValidHash($storedHash, $merchantApiLogin) + { +- $hash = $this->generateHash($merchantMd5, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); ++ if (empty($this->getData('x_amount'))) { ++ $this->setData('x_amount', '0.00'); ++ } + +- return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); +- } ++ if (!empty($this->getData('x_SHA2_Hash'))) { ++ $hash = $this->generateSha2Hash($storedHash); ++ return Security::compareStrings($hash, $this->getData('x_SHA2_Hash')); ++ } elseif (!empty($this->getData('x_MD5_Hash'))) { ++ $hash = $this->generateHash($storedHash, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); ++ return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); ++ } + ++ return false; ++ } ++ + /** + * Return if this is approved response from Authorize.net auth request. + * +@@ -54,4 +60,54 @@ + { + return $this->getXResponseCode() == \Magento\Authorizenet\Model\Directpost::RESPONSE_CODE_APPROVED; + } ++ ++ /** ++ * Generates an SHA2 hash to compare against AuthNet's. ++ * ++ * @param string $signatureKey ++ * @return string ++ * @see https://support.authorize.net/s/article/MD5-Hash-End-of-Life-Signature-Key-Replacement ++ */ ++ private function generateSha2Hash(string $signatureKey): string ++ { ++ $hashFields = [ ++ 'x_trans_id', ++ 'x_test_request', ++ 'x_response_code', ++ 'x_auth_code', ++ 'x_cvv2_resp_code', ++ 'x_cavv_response', ++ 'x_avs_code', ++ 'x_method', ++ 'x_account_number', ++ 'x_amount', ++ 'x_company', ++ 'x_first_name', ++ 'x_last_name', ++ 'x_address', ++ 'x_city', ++ 'x_state', ++ 'x_zip', ++ 'x_country', ++ 'x_phone', ++ 'x_fax', ++ 'x_email', ++ 'x_ship_to_company', ++ 'x_ship_to_first_name', ++ 'x_ship_to_last_name', ++ 'x_ship_to_address', ++ 'x_ship_to_city', ++ 'x_ship_to_state', ++ 'x_ship_to_zip', ++ 'x_ship_to_country', ++ 'x_invoice_num', ++ ]; ++ ++ $message = '^'; ++ foreach ($hashFields as $field) { ++ $message .= ($this->getData($field) ?? '') . '^'; ++ } ++ ++ return strtoupper(hash_hmac('sha512', $message, pack('H*', $signatureKey))); ++ } + } +diff -Nuar a/vendor/magento/module-authorizenet/etc/adminhtml/system.xml b/vendor/magento/module-authorizenet/etc/adminhtml/system.xml +--- a/vendor/magento/module-authorizenet/etc/adminhtml/system.xml ++++ b/vendor/magento/module-authorizenet/etc/adminhtml/system.xml +@@ -29,6 +29,10 @@ + + Magento\Config\Model\Config\Backend\Encrypted + ++ ++ ++ Magento\Config\Model\Config\Backend\Encrypted ++ + + + Magento\Config\Model\Config\Backend\Encrypted +diff -Nuar a/vendor/magento/module-authorizenet/etc/config.xml b/vendor/magento/module-authorizenet/etc/config.xml +--- a/vendor/magento/module-authorizenet/etc/config.xml ++++ b/vendor/magento/module-authorizenet/etc/config.xml +@@ -22,6 +22,7 @@ + Credit Card Direct Post (Authorize.net) + + ++ + 0 + USD + 1 diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch new file mode 100644 index 00000000..feebf9d6 --- /dev/null +++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch @@ -0,0 +1,542 @@ +diff -Nuar a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +--- /dev/null ++++ b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +@@ -0,0 +1,102 @@ ++storeManager = $storeManager; ++ $this->envelopeFactory = $envelopeFactory; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Check if amqpProperties['application_headers'] have 'store_id' and use it to setCurrentStore ++ * Restore original store value in consumer process after execution. ++ * Reject queue messages because of wrong store_id. ++ * ++ * @param SubjectMassConsumerEnvelopeCallback $subject ++ * @param callable $proceed ++ * @param EnvelopeInterface $message ++ * @return void ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function aroundExecute( ++ SubjectMassConsumerEnvelopeCallback $subject, ++ callable $proceed, ++ EnvelopeInterface $message ++ ): void { ++ $amqpProperties = $message->getProperties(); ++ if (isset($amqpProperties['application_headers'])) { ++ $headers = $amqpProperties['application_headers']; ++ if ($headers instanceof AMQPTable) { ++ $headers = $headers->getNativeData(); ++ } ++ if (isset($headers['store_id'])) { ++ $storeId = $headers['store_id']; ++ try { ++ $currentStoreId = $this->storeManager->getStore()->getId(); ++ } catch (NoSuchEntityException $e) { ++ $this->logger->error( ++ sprintf( ++ "Can't set currentStoreId during processing queue. Message rejected. Error %s.", ++ $e->getMessage() ++ ) ++ ); ++ $subject->getQueue()->reject($message, false, $e->getMessage()); ++ ++ return; ++ } ++ if ($storeId !== $currentStoreId) { ++ $this->storeManager->setCurrentStore($storeId); ++ } ++ } ++ } ++ $proceed($message); ++ if (isset($storeId, $currentStoreId) && $storeId !== $currentStoreId) { ++ $this->storeManager->setCurrentStore($currentStoreId);//restore original store value ++ } ++ } ++} +diff -Nuar a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +--- /dev/null ++++ b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +@@ -0,0 +1,115 @@ ++storeManager = $storeManager; ++ $this->envelopeFactory = $envelopeFactory; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Set current store_id in amqpProperties['application_headers'] ++ * so consumer may check store_id and execute operation in correct store scope. ++ * Prevent publishing inconsistent messages because of store_id not defined or wrong. ++ * ++ * @param SubjectExchange $subject ++ * @param string $topic ++ * @param EnvelopeInterface[] $envelopes ++ * @return array ++ * @throws AMQPInvalidArgumentException ++ * @throws \LogicException ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforeEnqueue(SubjectExchange $subject, $topic, array $envelopes): array ++ { ++ try { ++ $storeId = $this->storeManager->getStore()->getId(); ++ } catch (NoSuchEntityException $e) { ++ $errorMessage = sprintf( ++ "Can't get current storeId and inject to amqp message. Error %s.", ++ $e->getMessage() ++ ); ++ $this->logger->error($errorMessage); ++ throw new \LogicException($errorMessage); ++ } ++ ++ $updatedEnvelopes = []; ++ foreach ($envelopes as $envelope) { ++ $properties = $envelope->getProperties(); ++ if (empty($properties)) { ++ $properties = []; ++ } ++ if (isset($properties['application_headers'])) { ++ $headers = $properties['application_headers']; ++ if ($headers instanceof AMQPTable) { ++ try { ++ $headers->set('store_id', $storeId); ++ // phpcs:ignore Magento2.Exceptions.ThrowCatch ++ } catch (AMQPInvalidArgumentException $ea) { ++ $errorMessage = sprintf("Can't set storeId to amqp message. Error %s.", $ea->getMessage()); ++ $this->logger->error($errorMessage); ++ throw new AMQPInvalidArgumentException($errorMessage); ++ } ++ } ++ } else { ++ $properties['application_headers'] = new AMQPTable(['store_id' => $storeId]); ++ } ++ $updatedEnvelopes[] = $this->envelopeFactory->create( ++ [ ++ 'body' => $envelope->getBody(), ++ 'properties' => $properties, ++ ] ++ ); ++ } ++ if (!empty($updatedEnvelopes)) { ++ $envelopes = $updatedEnvelopes; ++ } ++ ++ return [$topic, $envelopes]; ++ } ++} +diff -Nuar a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-amqp/composer.json +--- a/vendor/magento/module-amqp/composer.json ++++ b/vendor/magento/module-amqp/composer.json +@@ -8,6 +11,10 @@ + "magento/framework": "102.0.*", + "magento/framework-amqp": "100.3.*", + "magento/framework-message-queue": "100.3.*", ++ "magento/module-store": "101.0.*", + "php": "~7.1.3||~7.2.0" + }, ++ "suggest": { ++ "magento/module-asynchronous-operations": "*", ++ }, + "type": "magento2-module", + "license": [ + "OSL-3.0", +diff -Nuar a/vendor/magento/module-amqp/etc/di.xml b/vendor/magento/module-amqp/etc/di.xml +--- a/vendor/magento/module-amqp/etc/di.xml ++++ b/vendor/magento/module-amqp/etc/di.xml +@@ -72,4 +72,10 @@ + \Magento\Framework\Amqp\Bulk\Exchange + + ++ ++ ++ ++ ++ ++ + +diff -Nuar a/vendor/magento/module-amqp/etc/module.xml b/vendor/magento/module-amqp/etc/module.xml +--- a/vendor/magento/module-amqp/etc/module.xml ++++ b/vendor/magento/module-amqp/etc/module.xml +@@ -6,5 +6,9 @@ + */ + --> + +- ++ ++ ++ ++ ++ + +diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +--- a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php ++++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +@@ -8,17 +8,10 @@ declare(strict_types=1); + + namespace Magento\AsynchronousOperations\Model; + +-use Magento\Framework\App\ResourceConnection; +-use Psr\Log\LoggerInterface; +-use Magento\Framework\MessageQueue\MessageLockException; +-use Magento\Framework\MessageQueue\ConnectionLostException; +-use Magento\Framework\Exception\NotFoundException; + use Magento\Framework\MessageQueue\CallbackInvoker; + use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; + use Magento\Framework\MessageQueue\EnvelopeInterface; + use Magento\Framework\MessageQueue\QueueInterface; +-use Magento\Framework\MessageQueue\LockInterface; +-use Magento\Framework\MessageQueue\MessageController; + use Magento\Framework\MessageQueue\ConsumerInterface; + + /** +@@ -33,57 +27,31 @@ class MassConsumer implements ConsumerInterface + */ + private $invoker; + +- /** +- * @var \Magento\Framework\App\ResourceConnection +- */ +- private $resource; +- + /** + * @var \Magento\Framework\MessageQueue\ConsumerConfigurationInterface + */ + private $configuration; +- +- /** +- * @var \Magento\Framework\MessageQueue\MessageController +- */ +- private $messageController; +- +- /** +- * @var LoggerInterface +- */ +- private $logger; + + /** +- * @var OperationProcessor ++ * @var MassConsumerEnvelopeCallbackFactory + */ +- private $operationProcessor; ++ private $massConsumerEnvelopeCallback; + + /** + * Initialize dependencies. + * + * @param CallbackInvoker $invoker +- * @param ResourceConnection $resource +- * @param MessageController $messageController + * @param ConsumerConfigurationInterface $configuration +- * @param OperationProcessorFactory $operationProcessorFactory +- * @param LoggerInterface $logger ++ * @param MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback + */ + public function __construct( + CallbackInvoker $invoker, +- ResourceConnection $resource, +- MessageController $messageController, + ConsumerConfigurationInterface $configuration, +- OperationProcessorFactory $operationProcessorFactory, +- LoggerInterface $logger ++ MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback + ) { + $this->invoker = $invoker; +- $this->resource = $resource; +- $this->messageController = $messageController; + $this->configuration = $configuration; +- $this->operationProcessor = $operationProcessorFactory->create([ +- 'configuration' => $configuration +- ]); +- $this->logger = $logger; ++ $this->massConsumerEnvelopeCallback = $massConsumerEnvelopeCallback; + } + + /** +@@ -108,38 +84,15 @@ class MassConsumer implements ConsumerInterface + */ + private function getTransactionCallback(QueueInterface $queue) + { +- return function (EnvelopeInterface $message) use ($queue) { +- /** @var LockInterface $lock */ +- $lock = null; +- try { +- $topicName = $message->getProperties()['topic_name']; +- $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); ++ $callbackInstance = $this->massConsumerEnvelopeCallback->create( ++ [ ++ 'configuration' => $this->configuration, ++ 'queue' => $queue, ++ ] ++ ); + +- $allowedTopics = $this->configuration->getTopicNames(); +- if (in_array($topicName, $allowedTopics)) { +- $this->operationProcessor->process($message->getBody()); +- } else { +- $queue->reject($message); +- return; +- } +- $queue->acknowledge($message); +- } catch (MessageLockException $exception) { +- $queue->acknowledge($message); +- } catch (ConnectionLostException $e) { +- if ($lock) { +- $this->resource->getConnection() +- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); +- } +- } catch (NotFoundException $e) { +- $queue->acknowledge($message); +- $this->logger->warning($e->getMessage()); +- } catch (\Exception $e) { +- $queue->reject($message, false, $e->getMessage()); +- if ($lock) { +- $this->resource->getConnection() +- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); +- } +- } ++ return function (EnvelopeInterface $message) use ($callbackInstance) { ++ $callbackInstance->execute($message); + }; + } + } +diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +--- /dev/null ++++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +@@ -0,0 +1,138 @@ ++resource = $resource; ++ $this->messageController = $messageController; ++ $this->configuration = $configuration; ++ $this->operationProcessor = $operationProcessorFactory->create( ++ [ ++ 'configuration' => $configuration, ++ ] ++ ); ++ $this->logger = $logger; ++ $this->queue = $queue; ++ } ++ ++ /** ++ * Get transaction callback. This handles the case of async. ++ * ++ * @param EnvelopeInterface $message ++ * @return void ++ */ ++ public function execute(EnvelopeInterface $message): void ++ { ++ $queue = $this->queue; ++ /** @var LockInterface $lock */ ++ $lock = null; ++ try { ++ $topicName = $message->getProperties()['topic_name']; ++ $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); ++ ++ $allowedTopics = $this->configuration->getTopicNames(); ++ if (in_array($topicName, $allowedTopics)) { ++ $this->operationProcessor->process($message->getBody()); ++ } else { ++ $queue->reject($message); ++ ++ return; ++ } ++ $queue->acknowledge($message); ++ } catch (MessageLockException $exception) { ++ $queue->acknowledge($message); ++ } catch (ConnectionLostException $e) { ++ if ($lock) { ++ $this->resource->getConnection() ++ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); ++ } ++ } catch (NotFoundException $e) { ++ $queue->acknowledge($message); ++ $this->logger->warning($e->getMessage()); ++ } catch (\Exception $e) { ++ $queue->reject($message, false, $e->getMessage()); ++ if ($lock) { ++ $this->resource->getConnection() ++ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); ++ } ++ } ++ } ++ ++ /** ++ * Get message queue. ++ * ++ * @return QueueInterface ++ */ ++ public function getQueue(): QueueInterface ++ { ++ return $this->queue; ++ } ++} diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch new file mode 100644 index 00000000..99257df2 --- /dev/null +++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch @@ -0,0 +1,556 @@ +diff -Nuar a/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +--- /dev/null ++++ b/vendor/magento/module-amqp/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php +@@ -0,0 +1,102 @@ ++storeManager = $storeManager; ++ $this->envelopeFactory = $envelopeFactory; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Check if amqpProperties['application_headers'] have 'store_id' and use it to setCurrentStore ++ * Restore original store value in consumer process after execution. ++ * Reject queue messages because of wrong store_id. ++ * ++ * @param SubjectMassConsumerEnvelopeCallback $subject ++ * @param callable $proceed ++ * @param EnvelopeInterface $message ++ * @return void ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function aroundExecute( ++ SubjectMassConsumerEnvelopeCallback $subject, ++ callable $proceed, ++ EnvelopeInterface $message ++ ): void { ++ $amqpProperties = $message->getProperties(); ++ if (isset($amqpProperties['application_headers'])) { ++ $headers = $amqpProperties['application_headers']; ++ if ($headers instanceof AMQPTable) { ++ $headers = $headers->getNativeData(); ++ } ++ if (isset($headers['store_id'])) { ++ $storeId = $headers['store_id']; ++ try { ++ $currentStoreId = $this->storeManager->getStore()->getId(); ++ } catch (NoSuchEntityException $e) { ++ $this->logger->error( ++ sprintf( ++ "Can't set currentStoreId during processing queue. Message rejected. Error %s.", ++ $e->getMessage() ++ ) ++ ); ++ $subject->getQueue()->reject($message, false, $e->getMessage()); ++ ++ return; ++ } ++ if ($storeId !== $currentStoreId) { ++ $this->storeManager->setCurrentStore($storeId); ++ } ++ } ++ } ++ $proceed($message); ++ if (isset($storeId, $currentStoreId) && $storeId !== $currentStoreId) { ++ $this->storeManager->setCurrentStore($currentStoreId);//restore original store value ++ } ++ } ++} +diff -Nuar a/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +--- /dev/null ++++ b/vendor/magento/module-amqp/Plugin/Framework/Amqp/Bulk/Exchange.php +@@ -0,0 +1,115 @@ ++storeManager = $storeManager; ++ $this->envelopeFactory = $envelopeFactory; ++ $this->logger = $logger; ++ } ++ ++ /** ++ * Set current store_id in amqpProperties['application_headers'] ++ * so consumer may check store_id and execute operation in correct store scope. ++ * Prevent publishing inconsistent messages because of store_id not defined or wrong. ++ * ++ * @param SubjectExchange $subject ++ * @param string $topic ++ * @param EnvelopeInterface[] $envelopes ++ * @return array ++ * @throws AMQPInvalidArgumentException ++ * @throws \LogicException ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforeEnqueue(SubjectExchange $subject, $topic, array $envelopes): array ++ { ++ try { ++ $storeId = $this->storeManager->getStore()->getId(); ++ } catch (NoSuchEntityException $e) { ++ $errorMessage = sprintf( ++ "Can't get current storeId and inject to amqp message. Error %s.", ++ $e->getMessage() ++ ); ++ $this->logger->error($errorMessage); ++ throw new \LogicException($errorMessage); ++ } ++ ++ $updatedEnvelopes = []; ++ foreach ($envelopes as $envelope) { ++ $properties = $envelope->getProperties(); ++ if (empty($properties)) { ++ $properties = []; ++ } ++ if (isset($properties['application_headers'])) { ++ $headers = $properties['application_headers']; ++ if ($headers instanceof AMQPTable) { ++ try { ++ $headers->set('store_id', $storeId); ++ // phpcs:ignore Magento2.Exceptions.ThrowCatch ++ } catch (AMQPInvalidArgumentException $ea) { ++ $errorMessage = sprintf("Can't set storeId to amqp message. Error %s.", $ea->getMessage()); ++ $this->logger->error($errorMessage); ++ throw new AMQPInvalidArgumentException($errorMessage); ++ } ++ } ++ } else { ++ $properties['application_headers'] = new AMQPTable(['store_id' => $storeId]); ++ } ++ $updatedEnvelopes[] = $this->envelopeFactory->create( ++ [ ++ 'body' => $envelope->getBody(), ++ 'properties' => $properties, ++ ] ++ ); ++ } ++ if (!empty($updatedEnvelopes)) { ++ $envelopes = $updatedEnvelopes; ++ } ++ ++ return [$topic, $envelopes]; ++ } ++} +diff -Nuar a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-amqp/composer.json +--- a/vendor/magento/module-amqp/composer.json ++++ b/vendor/magento/module-amqp/composer.json +@@ -8,6 +11,10 @@ + "magento/framework": "102.0.*", + "magento/framework-amqp": "100.3.*", + "magento/framework-message-queue": "100.3.*", ++ "magento/module-store": "101.0.*", + "php": "~7.1.3||~7.2.0" + }, ++ "suggest": { ++ "magento/module-asynchronous-operations": "*", ++ }, + "type": "magento2-module", + "license": [ + "OSL-3.0", +diff -Nuar a/vendor/magento/module-amqp/etc/di.xml b/vendor/magento/module-amqp/etc/di.xml +--- a/vendor/magento/module-amqp/etc/di.xml ++++ b/vendor/magento/module-amqp/etc/di.xml +@@ -72,4 +72,10 @@ + \Magento\Framework\Amqp\Bulk\Exchange + + ++ ++ ++ ++ ++ ++ + +diff -Nuar a/vendor/magento/module-amqp/etc/module.xml b/vendor/magento/module-amqp/etc/module.xml +--- a/vendor/magento/module-amqp/etc/module.xml ++++ b/vendor/magento/module-amqp/etc/module.xml +@@ -6,5 +6,9 @@ + */ + --> + +- ++ ++ ++ ++ ++ + +diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +--- a/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php ++++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumer.php +@@ -8,18 +8,11 @@ declare(strict_types=1); + + namespace Magento\AsynchronousOperations\Model; + +-use Magento\Framework\App\ResourceConnection; + use Magento\Framework\Registry; +-use Psr\Log\LoggerInterface; +-use Magento\Framework\MessageQueue\MessageLockException; +-use Magento\Framework\MessageQueue\ConnectionLostException; +-use Magento\Framework\Exception\NotFoundException; + use Magento\Framework\MessageQueue\CallbackInvokerInterface; + use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; + use Magento\Framework\MessageQueue\EnvelopeInterface; + use Magento\Framework\MessageQueue\QueueInterface; +-use Magento\Framework\MessageQueue\LockInterface; +-use Magento\Framework\MessageQueue\MessageController; + use Magento\Framework\MessageQueue\ConsumerInterface; + + /** +@@ -34,66 +27,39 @@ class MassConsumer implements ConsumerInterface + */ + private $invoker; + +- /** +- * @var \Magento\Framework\App\ResourceConnection +- */ +- private $resource; +- + /** + * @var \Magento\Framework\MessageQueue\ConsumerConfigurationInterface + */ + private $configuration; + + /** +- * @var \Magento\Framework\MessageQueue\MessageController +- */ +- private $messageController; +- +- /** +- * @var LoggerInterface +- */ +- private $logger; +- +- /** +- * @var OperationProcessor ++ * @var Registry + */ +- private $operationProcessor; ++ private $registry; + + /** +- * @var Registry ++ * @var MassConsumerEnvelopeCallbackFactory + */ +- private $registry; ++ private $massConsumerEnvelopeCallback; + + /** + * Initialize dependencies. + * + * @param CallbackInvokerInterface $invoker +- * @param ResourceConnection $resource +- * @param MessageController $messageController + * @param ConsumerConfigurationInterface $configuration +- * @param OperationProcessorFactory $operationProcessorFactory +- * @param LoggerInterface $logger ++ * @param MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback + * @param Registry $registry + */ + public function __construct( + CallbackInvokerInterface $invoker, +- ResourceConnection $resource, +- MessageController $messageController, + ConsumerConfigurationInterface $configuration, +- OperationProcessorFactory $operationProcessorFactory, +- LoggerInterface $logger, ++ MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback, + Registry $registry = null + ) { + $this->invoker = $invoker; +- $this->resource = $resource; +- $this->messageController = $messageController; + $this->configuration = $configuration; +- $this->operationProcessor = $operationProcessorFactory->create([ +- 'configuration' => $configuration +- ]); +- $this->logger = $logger; +- $this->registry = $registry ?? \Magento\Framework\App\ObjectManager::getInstance() +- ->get(Registry::class); ++ $this->massConsumerEnvelopeCallback = $massConsumerEnvelopeCallback; ++ $this->registry = $registry ?? \Magento\Framework\App\ObjectManager::getInstance()->get(Registry::class); + } + + /** +@@ -122,38 +88,15 @@ class MassConsumer implements ConsumerInterface + */ + private function getTransactionCallback(QueueInterface $queue) + { +- return function (EnvelopeInterface $message) use ($queue) { +- /** @var LockInterface $lock */ +- $lock = null; +- try { +- $topicName = $message->getProperties()['topic_name']; +- $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); +- +- $allowedTopics = $this->configuration->getTopicNames(); +- if (in_array($topicName, $allowedTopics)) { +- $this->operationProcessor->process($message->getBody()); +- } else { +- $queue->reject($message); +- return; +- } +- $queue->acknowledge($message); +- } catch (MessageLockException $exception) { +- $queue->acknowledge($message); +- } catch (ConnectionLostException $e) { +- if ($lock) { +- $this->resource->getConnection() +- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); +- } +- } catch (NotFoundException $e) { +- $queue->acknowledge($message); +- $this->logger->warning($e->getMessage()); +- } catch (\Exception $e) { +- $queue->reject($message, false, $e->getMessage()); +- if ($lock) { +- $this->resource->getConnection() +- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); +- } +- } ++ $callbackInstance = $this->massConsumerEnvelopeCallback->create( ++ [ ++ 'configuration' => $this->configuration, ++ 'queue' => $queue, ++ ] ++ ); ++ ++ return function (EnvelopeInterface $message) use ($callbackInstance) { ++ $callbackInstance->execute($message); + }; + } + } +diff -Nuar a/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +--- /dev/null ++++ b/vendor/magento/module-asynchronous-operations/Model/MassConsumerEnvelopeCallback.php +@@ -0,0 +1,138 @@ ++resource = $resource; ++ $this->messageController = $messageController; ++ $this->configuration = $configuration; ++ $this->operationProcessor = $operationProcessorFactory->create( ++ [ ++ 'configuration' => $configuration, ++ ] ++ ); ++ $this->logger = $logger; ++ $this->queue = $queue; ++ } ++ ++ /** ++ * Get transaction callback. This handles the case of async. ++ * ++ * @param EnvelopeInterface $message ++ * @return void ++ */ ++ public function execute(EnvelopeInterface $message): void ++ { ++ $queue = $this->queue; ++ /** @var LockInterface $lock */ ++ $lock = null; ++ try { ++ $topicName = $message->getProperties()['topic_name']; ++ $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); ++ ++ $allowedTopics = $this->configuration->getTopicNames(); ++ if (in_array($topicName, $allowedTopics)) { ++ $this->operationProcessor->process($message->getBody()); ++ } else { ++ $queue->reject($message); ++ ++ return; ++ } ++ $queue->acknowledge($message); ++ } catch (MessageLockException $exception) { ++ $queue->acknowledge($message); ++ } catch (ConnectionLostException $e) { ++ if ($lock) { ++ $this->resource->getConnection() ++ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); ++ } ++ } catch (NotFoundException $e) { ++ $queue->acknowledge($message); ++ $this->logger->warning($e->getMessage()); ++ } catch (\Exception $e) { ++ $queue->reject($message, false, $e->getMessage()); ++ if ($lock) { ++ $this->resource->getConnection() ++ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); ++ } ++ } ++ } ++ ++ /** ++ * Get message queue. ++ * ++ * @return QueueInterface ++ */ ++ public function getQueue(): QueueInterface ++ { ++ return $this->queue; ++ } ++} diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch new file mode 100644 index 00000000..ce71620e --- /dev/null +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch @@ -0,0 +1,146 @@ +diff -Nuar a/vendor/magento/module-sales/Helper/Admin.php b/vendor/magento/module-sales/Helper/Admin.php +--- a/vendor/magento/module-sales/Helper/Admin.php ++++ b/vendor/magento/module-sales/Helper/Admin.php +@@ -3,8 +3,14 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + namespace Magento\Sales\Helper; + ++use Magento\Framework\App\ObjectManager; ++ ++/** ++ * Sales admin helper. ++ */ + class Admin extends \Magento\Framework\App\Helper\AbstractHelper + { + /** +@@ -27,24 +33,33 @@ class Admin extends \Magento\Framework\App\Helper\AbstractHelper + */ + protected $escaper; + ++ /** ++ * @var \DOMDocumentFactory ++ */ ++ private $domDocumentFactory; ++ + /** + * @param \Magento\Framework\App\Helper\Context $context + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Sales\Model\Config $salesConfig + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Framework\Escaper $escaper ++ * @param \DOMDocumentFactory|null $domDocumentFactory + */ + public function __construct( + \Magento\Framework\App\Helper\Context $context, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Sales\Model\Config $salesConfig, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, +- \Magento\Framework\Escaper $escaper ++ \Magento\Framework\Escaper $escaper, ++ \DOMDocumentFactory $domDocumentFactory = null + ) { + $this->priceCurrency = $priceCurrency; + $this->_storeManager = $storeManager; + $this->_salesConfig = $salesConfig; + $this->escaper = $escaper; ++ $this->domDocumentFactory = $domDocumentFactory ++ ?: ObjectManager::getInstance()->get(\DOMDocumentFactory::class); + parent::__construct($context); + } + +@@ -145,37 +160,65 @@ class Admin extends \Magento\Framework\App\Helper\AbstractHelper + public function escapeHtmlWithLinks($data, $allowedTags = null) + { + if (!empty($data) && is_array($allowedTags) && in_array('a', $allowedTags)) { +- $links = []; +- $i = 1; +- $data = str_replace('%', '%%', $data); +- $regexp = "/]*href\s*?=\s*?([\"\']??)([^\" >]*?)\\1[^>]*>(.*)<\/a>/siU"; +- while (preg_match($regexp, $data, $matches)) { +- //Revert the sprintf escaping +- $url = str_replace('%%', '%', $matches[2]); +- $text = str_replace('%%', '%', $matches[3]); +- //Check for an valid url +- if ($url) { +- $urlScheme = strtolower(parse_url(https://github.com/$url,%20PHP_URL_SCHEME)); +- if ($urlScheme !== 'http' && $urlScheme !== 'https') { +- $url = null; +- } ++ $wrapperElementId = uniqid(); ++ $domDocument = $this->domDocumentFactory->create(); ++ ++ $internalErrors = libxml_use_internal_errors(true); ++ ++ $domDocument->loadHTML( ++ '' . $data . '' ++ ); ++ ++ libxml_use_internal_errors($internalErrors); ++ ++ $linkTags = $domDocument->getElementsByTagName('a'); ++ ++ foreach ($linkTags as $linkNode) { ++ $linkAttributes = []; ++ foreach ($linkNode->attributes as $attribute) { ++ $linkAttributes[$attribute->name] = $attribute->value; + } +- //Use hash tag as fallback +- if (!$url) { +- $url = '#'; ++ ++ foreach ($linkAttributes as $attributeName => $attributeValue) { ++ if ($attributeName === 'href') { ++ $url = $this->filterUrl($attributeValue ?? ''); ++ $url = $this->escaper->escapeUrl($url); ++ $linkNode->setAttribute('href', $url); ++ } else { ++ $linkNode->removeAttribute($attributeName); ++ } + } +- //Recreate a minimalistic secure a tag +- $links[] = sprintf( +- '%s', +- htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false), +- $this->escaper->escapeHtml($text) +- ); +- $data = str_replace($matches[0], '%' . $i . '$s', $data); +- ++$i; + } +- $data = $this->escaper->escapeHtml($data, $allowedTags); +- return vsprintf($data, $links); ++ ++ $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); ++ preg_match('/(.+)<\/body><\/html>$/si', $result, $matches); ++ $data = !empty($matches) ? $matches[1] : ''; + } ++ + return $this->escaper->escapeHtml($data, $allowedTags); + } ++ ++ /** ++ * Filter the URL for allowed protocols. ++ * ++ * @param string $url ++ * @return string ++ */ ++ private function filterUrl(string $url): string ++ { ++ if ($url) { ++ //Revert the sprintf escaping ++ $urlScheme = parse_url(https://github.com/$url,%20PHP_URL_SCHEME); ++ $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; ++ if ($urlScheme !== 'http' && $urlScheme !== 'https') { ++ $url = null; ++ } ++ } ++ ++ if (!$url) { ++ $url = '#'; ++ } ++ ++ return $url; ++ } + } diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch new file mode 100644 index 00000000..a367b77e --- /dev/null +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch @@ -0,0 +1,119 @@ +diff -Nuar a/vendor/magento/module-sales/Helper/Admin.php b/vendor/magento/module-sales/Helper/Admin.php +--- a/vendor/magento/module-sales/Helper/Admin.php ++++ b/vendor/magento/module-sales/Helper/Admin.php +@@ -6,6 +6,8 @@ + + namespace Magento\Sales\Helper; + ++use Magento\Framework\App\ObjectManager; ++ + /** + * Sales admin helper. + */ +@@ -31,24 +33,33 @@ + */ + protected $escaper; + ++ /** ++ * @var \DOMDocumentFactory ++ */ ++ private $domDocumentFactory; ++ + /** + * @param \Magento\Framework\App\Helper\Context $context + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Sales\Model\Config $salesConfig + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Framework\Escaper $escaper ++ * @param \DOMDocumentFactory|null $domDocumentFactory + */ + public function __construct( + \Magento\Framework\App\Helper\Context $context, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Sales\Model\Config $salesConfig, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, +- \Magento\Framework\Escaper $escaper ++ \Magento\Framework\Escaper $escaper, ++ \DOMDocumentFactory $domDocumentFactory = null + ) { + $this->priceCurrency = $priceCurrency; + $this->_storeManager = $storeManager; + $this->_salesConfig = $salesConfig; + $this->escaper = $escaper; ++ $this->domDocumentFactory = $domDocumentFactory ++ ?: ObjectManager::getInstance()->get(\DOMDocumentFactory::class); + parent::__construct($context); + } + +@@ -149,30 +160,41 @@ + public function escapeHtmlWithLinks($data, $allowedTags = null) + { + if (!empty($data) && is_array($allowedTags) && in_array('a', $allowedTags)) { +- $links = []; +- $i = 1; +- $data = str_replace('%', '%%', $data); +- $regexp = "#(?J).*?)\\1\s*)|(?:\S+\s*=\s*(['\"])(.*?)\\3)\s*)*)|>)" +- .">?(?:(?:(?.*?)(?:<\/a\s*>?|(?=<\w))|(?.*)))#si"; +- while (preg_match($regexp, $data, $matches)) { +- $text = ''; +- if (!empty($matches['text'])) { +- $text = str_replace('%%', '%', $matches['text']); +- } +- $url = $this->filterUrl($matches['link'] ?? ''); +- //Recreate a minimalistic secure a tag +- $links[] = sprintf( +- '%s', +- htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false), +- $this->escaper->escapeHtml($text) +- ); +- $data = str_replace($matches[0], '%' . $i . '$s', $data); +- ++$i; ++ $wrapperElementId = uniqid(); ++ $domDocument = $this->domDocumentFactory->create(); ++ ++ $internalErrors = libxml_use_internal_errors(true); ++ ++ $domDocument->loadHTML( ++ '' . $data . '' ++ ); ++ ++ libxml_use_internal_errors($internalErrors); ++ ++ $linkTags = $domDocument->getElementsByTagName('a'); ++ ++ foreach ($linkTags as $linkNode) { ++ $linkAttributes = []; ++ foreach ($linkNode->attributes as $attribute) { ++ $linkAttributes[$attribute->name] = $attribute->value; ++ } ++ ++ foreach ($linkAttributes as $attributeName => $attributeValue) { ++ if ($attributeName === 'href') { ++ $url = $this->filterUrl($attributeValue ?? ''); ++ $url = $this->escaper->escapeUrl($url); ++ $linkNode->setAttribute('href', $url); ++ } else { ++ $linkNode->removeAttribute($attributeName); ++ } ++ } + } +- $data = $this->escaper->escapeHtml($data, $allowedTags); +- return vsprintf($data, $links); ++ ++ $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); ++ preg_match('/(.+)<\/body><\/html>$/si', $result, $matches); ++ $data = !empty($matches) ? $matches[1] : ''; + } ++ + return $this->escaper->escapeHtml($data, $allowedTags); + } + +@@ -186,7 +208,6 @@ + { + if ($url) { + //Revert the sprintf escaping +- $url = str_replace('%%', '%', $url); + $urlScheme = parse_url(https://github.com/$url,%20PHP_URL_SCHEME); + $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; + if ($urlScheme !== 'http' && $urlScheme !== 'https') { diff --git a/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch new file mode 100644 index 00000000..dec16081 --- /dev/null +++ b/patches/PRODSECBUG-2233__fix_xss_in_order_history__2.3.0.patch @@ -0,0 +1,209 @@ +diff -Nuar a/vendor/magento/module-sales/Helper/Admin.php b/vendor/magento/module-sales/Helper/Admin.php +--- a/vendor/magento/module-sales/Helper/Admin.php ++++ b/vendor/magento/module-sales/Helper/Admin.php +@@ -7,6 +7,8 @@ + + namespace Magento\Sales\Helper; + ++use Magento\Framework\App\ObjectManager; ++ + /** + * Sales admin helper. + */ +@@ -32,24 +34,33 @@ + */ + protected $escaper; + ++ /** ++ * @var \DOMDocumentFactory ++ */ ++ private $domDocumentFactory; ++ + /** + * @param \Magento\Framework\App\Helper\Context $context + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Sales\Model\Config $salesConfig + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Framework\Escaper $escaper ++ * @param \DOMDocumentFactory|null $domDocumentFactory + */ + public function __construct( + \Magento\Framework\App\Helper\Context $context, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Sales\Model\Config $salesConfig, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, +- \Magento\Framework\Escaper $escaper ++ \Magento\Framework\Escaper $escaper, ++ \DOMDocumentFactory $domDocumentFactory = null + ) { + $this->priceCurrency = $priceCurrency; + $this->_storeManager = $storeManager; + $this->_salesConfig = $salesConfig; + $this->escaper = $escaper; ++ $this->domDocumentFactory = $domDocumentFactory ++ ?: ObjectManager::getInstance()->get(\DOMDocumentFactory::class); + parent::__construct($context); + } + +@@ -150,30 +161,41 @@ + public function escapeHtmlWithLinks($data, $allowedTags = null) + { + if (!empty($data) && is_array($allowedTags) && in_array('a', $allowedTags)) { +- $links = []; +- $i = 1; +- $data = str_replace('%', '%%', $data); +- $regexp = "#(?J).*?)\\1\s*)|(?:\S+\s*=\s*(['\"])(.*?)\\3)\s*)*)|>)" +- .">?(?:(?:(?.*?)(?:<\/a\s*>?|(?=<\w))|(?.*)))#si"; +- while (preg_match($regexp, $data, $matches)) { +- $text = ''; +- if (!empty($matches['text'])) { +- $text = str_replace('%%', '%', $matches['text']); +- } +- $url = $this->filterUrl($matches['link'] ?? ''); +- //Recreate a minimalistic secure a tag +- $links[] = sprintf( +- '%s', +- htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false), +- $this->escaper->escapeHtml($text) +- ); +- $data = str_replace($matches[0], '%' . $i . '$s', $data); +- ++$i; ++ $wrapperElementId = uniqid(); ++ $domDocument = $this->domDocumentFactory->create(); ++ ++ $internalErrors = libxml_use_internal_errors(true); ++ ++ $domDocument->loadHTML( ++ '' . $data . '' ++ ); ++ ++ libxml_use_internal_errors($internalErrors); ++ ++ $linkTags = $domDocument->getElementsByTagName('a'); ++ ++ foreach ($linkTags as $linkNode) { ++ $linkAttributes = []; ++ foreach ($linkNode->attributes as $attribute) { ++ $linkAttributes[$attribute->name] = $attribute->value; ++ } ++ ++ foreach ($linkAttributes as $attributeName => $attributeValue) { ++ if ($attributeName === 'href') { ++ $url = $this->filterUrl($attributeValue ?? ''); ++ $url = $this->escaper->escapeUrl($url); ++ $linkNode->setAttribute('href', $url); ++ } else { ++ $linkNode->removeAttribute($attributeName); ++ } ++ } + } +- $data = $this->escaper->escapeHtml($data, $allowedTags); +- return vsprintf($data, $links); ++ ++ $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); ++ preg_match('/(.+)<\/body><\/html>$/si', $result, $matches); ++ $data = !empty($matches) ? $matches[1] : ''; + } ++ + return $this->escaper->escapeHtml($data, $allowedTags); + } + +@@ -187,7 +209,7 @@ + { + if ($url) { + //Revert the sprintf escaping +- $url = str_replace('%%', '%', $url); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction + $urlScheme = parse_url(https://github.com/$url,%20PHP_URL_SCHEME); + $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; + if ($urlScheme !== 'http' && $urlScheme !== 'https') { +diff -Nuar a/vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php b/vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php +--- a/vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php ++++ b/vendor/magento/module-sales/Test/Unit/Helper/AdminTest.php +@@ -71,7 +71,7 @@ + ->disableOriginalConstructor() + ->getMock(); + +- $this->adminHelper = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( ++ $this->adminHelper = (new ObjectManager($this))->getObject( + \Magento\Sales\Helper\Admin::class, + [ + 'context' => $this->contextMock, +@@ -330,72 +330,16 @@ + } + + /** +- * @param string $data +- * @param string $expected +- * @param null|array $allowedTags +- * @dataProvider escapeHtmlWithLinksDataProvider ++ * @return void + */ +- public function testEscapeHtmlWithLinks($data, $expected, $allowedTags = null) ++ public function testEscapeHtmlWithLinks(): void + { ++ $expected = '<a>some text in tags</a>'; + $this->escaperMock + ->expects($this->any()) + ->method('escapeHtml') + ->will($this->returnValue($expected)); +- $actual = $this->adminHelper->escapeHtmlWithLinks($data, $allowedTags); ++ $actual = $this->adminHelper->escapeHtmlWithLinks('some text in tags'); + $this->assertEquals($expected, $actual); + } +- +- /** +- * @return array +- */ +- public function escapeHtmlWithLinksDataProvider() +- { +- return [ +- [ +- 'some text in tags', +- '<a>some text in tags</a>', +- 'allowedTags' => null +- ], +- [ +- 'Transaction ID: "XX123XX"', +- 'Transaction ID: "XX123XX"', +- 'allowedTags' => ['b', 'br', 'strong', 'i', 'u', 'a'] +- ], +- [ +- 'some text in tags', +- 'some text in tags', +- 'allowedTags' => ['a'] +- ], +- 'Not replacement with placeholders' => [ +- "", +- '<script>alert(1)</script>', +- 'allowedTags' => ['a'] +- ], +- 'Normal usage, url escaped' => [ +- 'Foo', +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Normal usage, url not escaped' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'XSS test' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Additional regex test' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- 'Break of valid urls' => [ +- "Foo", +- 'Foo', +- 'allowedTags' => ['a'] +- ], +- ]; +- } + } diff --git a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch new file mode 100644 index 00000000..67bcdd34 --- /dev/null +++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.1.4.patch @@ -0,0 +1,21 @@ +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +--- a/vendor/magento/framework/App/Router/ActionList.php ++++ b/vendor/magento/framework/App/Router/ActionList.php +@@ -8,6 +7,9 @@ namespace Magento\Framework\App\Router; + + use Magento\Framework\Module\Dir\Reader as ModuleReader; + ++/** ++ * Class to retrieve action class. ++ */ + class ActionList + { + /** +@@ -74,6 +76,7 @@ class ActionList + if ($area) { + $area = '\\' . $area; + } ++ $namespace = strtolower($namespace); + if (strpos($namespace, self::NOT_ALLOWED_IN_NAMESPACE_PATH) !== false) { + return null; + } diff --git a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch new file mode 100644 index 00000000..c2b230de --- /dev/null +++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.2.0.patch @@ -0,0 +1,22 @@ +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +--- a/vendor/magento/framework/App/Router/ActionList.php ++++ b/vendor/magento/framework/App/Router/ActionList.php +@@ -10,6 +9,9 @@ use Magento\Framework\Serialize\SerializerInterface; + use Magento\Framework\Serialize\Serializer\Serialize; + use Magento\Framework\Module\Dir\Reader as ModuleReader; + ++/** ++ * Class to retrieve action class. ++ */ + class ActionList + { + /** +@@ -91,6 +93,7 @@ class ActionList + if ($area) { + $area = '\\' . $area; + } ++ $namespace = strtolower($namespace); + if (strpos($namespace, self::NOT_ALLOWED_IN_NAMESPACE_PATH) !== false) { + return null; + } + diff --git a/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch new file mode 100644 index 00000000..c2b230de --- /dev/null +++ b/patches/PRODSECBUG-2432__admin_path_disclosure_bug__2.3.0.patch @@ -0,0 +1,22 @@ +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +--- a/vendor/magento/framework/App/Router/ActionList.php ++++ b/vendor/magento/framework/App/Router/ActionList.php +@@ -10,6 +9,9 @@ use Magento\Framework\Serialize\SerializerInterface; + use Magento\Framework\Serialize\Serializer\Serialize; + use Magento\Framework\Module\Dir\Reader as ModuleReader; + ++/** ++ * Class to retrieve action class. ++ */ + class ActionList + { + /** +@@ -91,6 +93,7 @@ class ActionList + if ($area) { + $area = '\\' . $area; + } ++ $namespace = strtolower($namespace); + if (strpos($namespace, self::NOT_ALLOWED_IN_NAMESPACE_PATH) !== false) { + return null; + } + From b593939016258d7576d415abe9b8a0b6bfaf0346 Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko Date: Thu, 2 Jan 2020 10:17:15 -0600 Subject: [PATCH 011/157] MAGECLOUD-4847: Issues caused by MC-21696 hot fix from ece-tools patches (#10) --- patches.json | 2 +- ...arch_and_products_display_fix__2.3.3.patch | 779 ++++++++++++++++++ ..._fix_broken_pagination_on_ES6__2.3.3.patch | 11 - 3 files changed, 780 insertions(+), 12 deletions(-) create mode 100644 patches/MAGECLOUD-4847__elasticsearch_and_products_display_fix__2.3.3.patch delete mode 100644 patches/MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch diff --git a/patches.json b/patches.json index 09e34493..c5e14486 100644 --- a/patches.json +++ b/patches.json @@ -185,7 +185,7 @@ "2.3.3": "MAGECLOUD-4442__fix_backward_compatibility_of_new_mail_interfaces__2.3.3.patch" }, "Catalog Product pagination does not work on Elasticsearch 6.x": { - "2.3.3": "MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch" + "2.3.3": "MAGECLOUD-4847__elasticsearch_and_products_display_fix__2.3.3.patch" }, "Catalog Product collection method chaining is broken": { "2.3.3": "MC-21820__fix_broken_method_chaining__2.3.3.patch" diff --git a/patches/MAGECLOUD-4847__elasticsearch_and_products_display_fix__2.3.3.patch b/patches/MAGECLOUD-4847__elasticsearch_and_products_display_fix__2.3.3.patch new file mode 100644 index 00000000..130fcaef --- /dev/null +++ b/patches/MAGECLOUD-4847__elasticsearch_and_products_display_fix__2.3.3.patch @@ -0,0 +1,779 @@ +diff -Nuar a/vendor/magento/module-catalog/Model/Indexer/Category/Flat/Action/Full.php b/vendor/magento/module-catalog/Model/Indexer/Category/Flat/Action/Full.php +--- a/vendor/magento/module-catalog/Model/Indexer/Category/Flat/Action/Full.php ++++ b/vendor/magento/module-catalog/Model/Indexer/Category/Flat/Action/Full.php +@@ -79,6 +79,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction + } + $category['store_id'] = $store->getId(); + $data[] = $this->prepareValuesToInsert( ++ // phpcs:ignore Magento2.Performance.ForeachArrayMerge + array_merge($category, $attributesData[$category[$linkField]]) + ); + } +@@ -183,7 +184,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction + foreach ($this->storeManager->getStores() as $store) { + $actualStoreTables[] = sprintf( + '%s_store_%s', +- $this->connection->getTableName('catalog_category_flat'), ++ $this->connection->getTableName($this->getTableName('catalog_category_flat')), + $store->getId() + ); + } +@@ -199,7 +200,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction + private function deleteAbandonedStoreCategoryFlatTables(): void + { + $existentTables = $this->connection->getTables( +- $this->connection->getTableName('catalog_category_flat_store_%') ++ $this->connection->getTableName($this->getTableName('catalog_category_flat_store_%')) + ); + $actualStoreTables = $this->getActualStoreTablesForCategoryFlat(); + +diff -Nuar a/vendor/magento/module-catalog/Model/Indexer/Product/Flat/AbstractAction.php b/vendor/magento/module-catalog/Model/Indexer/Product/Flat/AbstractAction.php +--- a/vendor/magento/module-catalog/Model/Indexer/Product/Flat/AbstractAction.php ++++ b/vendor/magento/module-catalog/Model/Indexer/Product/Flat/AbstractAction.php +@@ -222,7 +222,7 @@ abstract class AbstractAction + ['t' => $this->_productIndexerHelper->getTable($relation->getTable())], + ['entity_table.entity_id', $relation->getChildFieldName(), new \Zend_Db_Expr('1')] + )->join( +- ['entity_table' => $this->_connection->getTableName('catalog_product_entity')], ++ ['entity_table' => $this->_productIndexerHelper->getTable('catalog_product_entity')], + "entity_table.{$metadata->getLinkField()} = t.{$relation->getParentFieldName()}", + [] + )->join( +diff -Nuar a/vendor/magento/module-catalog-search/Model/ResourceModel/Advanced/Collection.php b/vendor/magento/module-catalog-search/Model/ResourceModel/Advanced/Collection.php +--- a/vendor/magento/module-catalog-search/Model/ResourceModel/Advanced/Collection.php ++++ b/vendor/magento/module-catalog-search/Model/ResourceModel/Advanced/Collection.php +@@ -394,7 +394,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection + 'collection' => $this, + 'searchResult' => $searchResult, + /** This variable sets by serOrder method, but doesn't have a getter method. */ +- 'orders' => $this->_orders ++ 'orders' => $this->_orders, ++ 'size' => $this->getPageSize(), + ] + ); + } +diff -Nuar a/vendor/magento/module-catalog-search/Model/ResourceModel/Fulltext/Collection.php b/vendor/magento/module-catalog-search/Model/ResourceModel/Fulltext/Collection.php +--- a/vendor/magento/module-catalog-search/Model/ResourceModel/Fulltext/Collection.php ++++ b/vendor/magento/module-catalog-search/Model/ResourceModel/Fulltext/Collection.php +@@ -486,12 +486,12 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection + { + return $this->searchCriteriaResolverFactory->create( + [ +- 'builder' => $this->getSearchCriteriaBuilder(), +- 'collection' => $this, +- 'searchRequestName' => $this->searchRequestName, +- 'currentPage' => $this->_curPage, +- 'size' => $this->getPageSize(), +- 'orders' => $this->searchOrders, ++ 'builder' => $this->getSearchCriteriaBuilder(), ++ 'collection' => $this, ++ 'searchRequestName' => $this->searchRequestName, ++ 'currentPage' => (int)$this->_curPage, ++ 'size' => $this->getPageSize(), ++ 'orders' => $this->searchOrders, + ] + ); + } +@@ -506,10 +506,12 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection + { + return $this->searchResultApplierFactory->create( + [ +- 'collection' => $this, +- 'searchResult' => $searchResult, +- /** This variable sets by serOrder method, but doesn't have a getter method. */ +- 'orders' => $this->_orders, ++ 'collection' => $this, ++ 'searchResult' => $searchResult, ++ /** This variable sets by serOrder method, but doesn't have a getter method. */ ++ 'orders' => $this->_orders, ++ 'size' => $this->getPageSize(), ++ 'currentPage' => (int)$this->_curPage, + ] + ); + } +diff -Nuar a/vendor/magento/module-checkout/etc/frontend/sections.xml b/vendor/magento/module-checkout/etc/frontend/sections.xml +--- a/vendor/magento/module-checkout/etc/frontend/sections.xml ++++ b/vendor/magento/module-checkout/etc/frontend/sections.xml +@@ -41,7 +41,6 @@ + + +
+-
+
+ + +diff -Nuar a/vendor/magento/module-downloadable-graph-ql/Model/ResourceModel/GetPurchasedDownloadableProducts.php b/vendor/magento/module-downloadable-graph-ql/Model/ResourceModel/GetPurchasedDownloadableProducts.php +--- a/vendor/magento/module-downloadable-graph-ql/Model/ResourceModel/GetPurchasedDownloadableProducts.php ++++ b/vendor/magento/module-downloadable-graph-ql/Model/ResourceModel/GetPurchasedDownloadableProducts.php +@@ -41,13 +41,13 @@ class GetPurchasedDownloadableProducts + { + $connection = $this->resourceConnection->getConnection(); + $allowedItemsStatuses = [Item::LINK_STATUS_PENDING_PAYMENT, Item::LINK_STATUS_PAYMENT_REVIEW]; +- $downloadablePurchasedTable = $connection->getTableName('downloadable_link_purchased'); ++ $downloadablePurchasedTable = $this->resourceConnection->getTableName('downloadable_link_purchased'); + + /* The fields names are hardcoded since there's no existing name reference in the code */ + $selectQuery = $connection->select() + ->from($downloadablePurchasedTable) + ->joinLeft( +- ['item' => $connection->getTableName('downloadable_link_purchased_item')], ++ ['item' => $this->resourceConnection->getTableName('downloadable_link_purchased_item')], + "$downloadablePurchasedTable.purchased_id = item.purchased_id" + ) + ->where("$downloadablePurchasedTable.customer_id = ?", $customerId) +diff -Nuar a/vendor/magento/module-eav/Block/Adminhtml/Attribute/Edit/Options/Options.php b/vendor/magento/module-eav/Block/Adminhtml/Attribute/Edit/Options/Options.php +--- a/vendor/magento/module-eav/Block/Adminhtml/Attribute/Edit/Options/Options.php ++++ b/vendor/magento/module-eav/Block/Adminhtml/Attribute/Edit/Options/Options.php +@@ -4,16 +4,14 @@ + * See COPYING.txt for license details. + */ + +-/** +- * Attribute add/edit form options tab +- * +- * @author Magento Core Team +- */ + namespace Magento\Eav\Block\Adminhtml\Attribute\Edit\Options; + + use Magento\Store\Model\ResourceModel\Store\Collection; ++use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; + + /** ++ * Attribute add/edit form options tab ++ * + * @api + * @since 100.0.2 + */ +@@ -61,6 +59,7 @@ class Options extends \Magento\Backend\Block\Template + + /** + * Is true only for system attributes which use source model ++ * + * Option labels and position for such attributes are kept in source model and thus cannot be overridden + * + * @return bool +@@ -96,12 +95,16 @@ class Options extends \Magento\Backend\Block\Template + { + $stores = $this->getStores(); + if (is_array($stores)) { +- usort($stores, function ($storeA, $storeB) { +- if ($storeA->getSortOrder() == $storeB->getSortOrder()) { +- return $storeA->getId() < $storeB->getId() ? -1 : 1; ++ usort( ++ $stores, ++ function ($storeA, $storeB) { ++ if ($storeA->getSortOrder() == $storeB->getSortOrder()) { ++ return $storeA->getId() < $storeB->getId() ? -1 : 1; ++ } ++ ++ return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1; + } +- return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1; +- }); ++ ); + } + return $stores; + } +@@ -130,12 +133,14 @@ class Options extends \Magento\Backend\Block\Template + } + + /** +- * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute ++ * Preparing values of attribute options ++ * ++ * @param AbstractAttribute $attribute + * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection + * @return array + */ + protected function _prepareOptionValues( +- \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute, ++ AbstractAttribute $attribute, + $optionCollection + ) { + $type = $attribute->getFrontendInput(); +@@ -149,6 +154,41 @@ class Options extends \Magento\Backend\Block\Template + + $values = []; + $isSystemAttribute = is_array($optionCollection); ++ if ($isSystemAttribute) { ++ $values = $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues); ++ } else { ++ $optionCollection->setPageSize(200); ++ $pageCount = $optionCollection->getLastPageNumber(); ++ $currentPage = 1; ++ while ($currentPage <= $pageCount) { ++ $optionCollection->clear(); ++ $optionCollection->setCurPage($currentPage); ++ $values = array_merge( ++ $values, ++ $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues) ++ ); ++ $currentPage++; ++ } ++ } ++ ++ return $values; ++ } ++ ++ /** ++ * Return prepared values of system or user defined attribute options ++ * ++ * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection ++ * @param bool $isSystemAttribute ++ * @param string $inputType ++ * @param array $defaultValues ++ */ ++ private function getPreparedValues( ++ $optionCollection, ++ bool $isSystemAttribute, ++ string $inputType, ++ array $defaultValues ++ ) { ++ $values = []; + foreach ($optionCollection as $option) { + $bunch = $isSystemAttribute ? $this->_prepareSystemAttributeOptionValues( + $option, +@@ -169,12 +209,13 @@ class Options extends \Magento\Backend\Block\Template + + /** + * Retrieve option values collection ++ * + * It is represented by an array in case of system attribute + * +- * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute ++ * @param AbstractAttribute $attribute + * @return array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection + */ +- protected function _getOptionValuesCollection(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute) ++ protected function _getOptionValuesCollection(AbstractAttribute $attribute) + { + if ($this->canManageOptionDefaultOnly()) { + $options = $this->_universalFactory->create( +@@ -226,7 +267,7 @@ class Options extends \Magento\Backend\Block\Template + foreach ($this->getStores() as $store) { + $storeId = $store->getId(); + $value['store' . $storeId] = $storeId == +- \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : ''; ++ \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : ''; + } + + return [$value]; +diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute.php +--- a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute.php ++++ b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute.php +@@ -477,6 +477,7 @@ class Attribute extends AbstractDb + if (!empty($option['delete'][$optionId])) { + if ($intOptionId) { + $connection->delete($table, ['option_id = ?' => $intOptionId]); ++ $this->clearSelectedOptionInEntities($object, $intOptionId); + } + return false; + } +@@ -495,6 +496,41 @@ class Attribute extends AbstractDb + return $intOptionId; + } + ++ /** ++ * Clear selected option in entities ++ * ++ * @param EntityAttribute|AbstractModel $object ++ * @param int $optionId ++ * @return void ++ */ ++ private function clearSelectedOptionInEntities(AbstractModel $object, int $optionId) ++ { ++ $backendTable = $object->getBackendTable(); ++ $attributeId = $object->getAttributeId(); ++ if (!$backendTable || !$attributeId) { ++ return; ++ } ++ ++ $connection = $this->getConnection(); ++ $where = $connection->quoteInto('attribute_id = ?', $attributeId); ++ $update = []; ++ ++ if ($object->getBackendType() === 'varchar') { ++ $where.= ' AND ' . $connection->prepareSqlCondition('value', ['finset' => $optionId]); ++ $concat = $connection->getConcatSql(["','", 'value', "','"]); ++ $expr = $connection->quoteInto( ++ "TRIM(BOTH ',' FROM REPLACE($concat,',?,',','))", ++ $optionId ++ ); ++ $update['value'] = new \Zend_Db_Expr($expr); ++ } else { ++ $where.= $connection->quoteInto(' AND value = ?', $optionId); ++ $update['value'] = null; ++ } ++ ++ $connection->update($backendTable, $update, $where); ++ } ++ + /** + * Save option values records per store + * +diff -Nuar a/vendor/magento/module-elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/vendor/magento/module-elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +--- a/vendor/magento/module-elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php ++++ b/vendor/magento/module-elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +@@ -278,7 +278,7 @@ class Elasticsearch implements ClientInterface + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'integer', +- 'index' => false, ++ 'index' => true, + ], + ], + ], +@@ -289,7 +289,7 @@ class Elasticsearch implements ClientInterface + 'mapping' => $this->prepareFieldInfo( + [ + 'type' => 'text', +- 'index' => false, ++ 'index' => true, + ] + ), + ], +diff -Nuar a/vendor/magento/module-elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/vendor/magento/module-elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +--- a/vendor/magento/module-elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php ++++ b/vendor/magento/module-elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +@@ -62,6 +62,11 @@ class StaticField implements FieldProviderInterface + */ + private $fieldNameResolver; + ++ /** ++ * @var array ++ */ ++ private $excludedAttributes; ++ + /** + * @param Config $eavConfig + * @param FieldTypeConverterInterface $fieldTypeConverter +@@ -70,6 +75,7 @@ class StaticField implements FieldProviderInterface + * @param FieldIndexResolver $fieldIndexResolver + * @param AttributeProvider $attributeAdapterProvider + * @param FieldName\ResolverInterface|null $fieldNameResolver ++ * @param array $excludedAttributes + */ + public function __construct( + Config $eavConfig, +@@ -78,7 +84,8 @@ class StaticField implements FieldProviderInterface + FieldTypeResolver $fieldTypeResolver, + FieldIndexResolver $fieldIndexResolver, + AttributeProvider $attributeAdapterProvider, +- FieldName\ResolverInterface $fieldNameResolver = null ++ FieldName\ResolverInterface $fieldNameResolver = null, ++ array $excludedAttributes = [] + ) { + $this->eavConfig = $eavConfig; + $this->fieldTypeConverter = $fieldTypeConverter; +@@ -88,6 +95,7 @@ class StaticField implements FieldProviderInterface + $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(FieldName\ResolverInterface::class); ++ $this->excludedAttributes = $excludedAttributes; + } + + /** +@@ -103,6 +111,9 @@ class StaticField implements FieldProviderInterface + $allAttributes = []; + + foreach ($attributes as $attribute) { ++ if (in_array($attribute->getAttributeCode(), $this->excludedAttributes, true)) { ++ continue; ++ } + $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); + $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); + +diff -Nuar a/vendor/magento/module-elasticsearch/Model/Client/Elasticsearch.php b/vendor/magento/module-elasticsearch/Model/Client/Elasticsearch.php +--- a/vendor/magento/module-elasticsearch/Model/Client/Elasticsearch.php ++++ b/vendor/magento/module-elasticsearch/Model/Client/Elasticsearch.php +@@ -271,7 +271,7 @@ class Elasticsearch implements ClientInterface + 'match_mapping' => 'string', + 'mapping' => [ + 'type' => 'integer', +- 'index' => 'no' ++ 'index' => 'not_analyzed', + ], + ], + ], +@@ -281,7 +281,7 @@ class Elasticsearch implements ClientInterface + 'match_mapping' => 'string', + 'mapping' => [ + 'type' => 'string', +- 'index' => 'no' ++ 'index' => 'not_analyzed', + ], + ], + ] +diff -Nuar a/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +--- a/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php ++++ b/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +@@ -25,16 +25,32 @@ class SearchResultApplier implements SearchResultApplierInterface + */ + private $searchResult; + ++ /** ++ * @var int ++ */ ++ private $size; ++ ++ /** ++ * @var int ++ */ ++ private $currentPage; ++ + /** + * @param Collection $collection + * @param SearchResultInterface $searchResult ++ * @param int $size ++ * @param int $currentPage + */ + public function __construct( + Collection $collection, +- SearchResultInterface $searchResult ++ SearchResultInterface $searchResult, ++ int $size, ++ int $currentPage + ) { + $this->collection = $collection; + $this->searchResult = $searchResult; ++ $this->size = $size; ++ $this->currentPage = $currentPage; + } + + /** +@@ -44,10 +60,13 @@ class SearchResultApplier implements SearchResultApplierInterface + { + if (empty($this->searchResult->getItems())) { + $this->collection->getSelect()->where('NULL'); ++ + return; + } ++ ++ $items = $this->sliceItems($this->searchResult->getItems(), $this->size, $this->currentPage); + $ids = []; +- foreach ($this->searchResult->getItems() as $item) { ++ foreach ($items as $item) { + $ids[] = (int)$item->getId(); + } + $this->collection->getSelect()->where('e.entity_id IN (?)', $ids); +@@ -55,4 +74,45 @@ class SearchResultApplier implements SearchResultApplierInterface + $this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER); + $this->collection->getSelect()->order("FIELD(e.entity_id,$orderList)"); + } ++ ++ /** ++ * Slice current items ++ * ++ * @param array $items ++ * @param int $size ++ * @param int $currentPage ++ * @return array ++ */ ++ private function sliceItems(array $items, int $size, int $currentPage): array ++ { ++ if ($size !== 0) { ++ // Check that current page is in a range of allowed page numbers, based on items count and items per page, ++ // than calculate offset for slicing items array. ++ $itemsCount = count($items); ++ $maxAllowedPageNumber = ceil($itemsCount/$size); ++ if ($currentPage < 1) { ++ $currentPage = 1; ++ } ++ if ($currentPage > $maxAllowedPageNumber) { ++ $currentPage = $maxAllowedPageNumber; ++ } ++ ++ $offset = $this->getOffset($currentPage, $size); ++ $items = array_slice($items, $offset, $size); ++ } ++ ++ return $items; ++ } ++ ++ /** ++ * Get offset for given page. ++ * ++ * @param int $pageNumber ++ * @param int $pageSize ++ * @return int ++ */ ++ private function getOffset(int $pageNumber, int $pageSize): int ++ { ++ return ($pageNumber - 1) * $pageSize; ++ } + } +diff -Nuar a/vendor/magento/module-elasticsearch/etc/di.xml b/vendor/magento/module-elasticsearch/etc/di.xml +--- a/vendor/magento/module-elasticsearch/etc/di.xml ++++ b/vendor/magento/module-elasticsearch/etc/di.xml +@@ -558,4 +558,11 @@ + + + ++ ++ ++ ++ price ++ ++ ++ + +diff -Nuar a/vendor/magento/module-elasticsearch-6/Model/Client/Elasticsearch.php b/vendor/magento/module-elasticsearch-6/Model/Client/Elasticsearch.php +--- a/vendor/magento/module-elasticsearch-6/Model/Client/Elasticsearch.php ++++ b/vendor/magento/module-elasticsearch-6/Model/Client/Elasticsearch.php +@@ -271,7 +271,7 @@ class Elasticsearch implements ClientInterface + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'integer', +- 'index' => false, ++ 'index' => true, + ], + ], + ], +@@ -281,7 +281,7 @@ class Elasticsearch implements ClientInterface + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', +- 'index' => false, ++ 'index' => true, + 'copy_to' => '_search' + ], + ], +diff -Nuar a/vendor/magento/module-sales-rule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php b/vendor/magento/module-sales-rule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php +--- a/vendor/magento/module-sales-rule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php ++++ b/vendor/magento/module-sales-rule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php +@@ -1,12 +1,19 @@ + getRequest()->getParam('id'); +- $formName = $this->getRequest()->getParam('form'); ++ $id = $this->getRequest() ++ ->getParam('id'); ++ $formName = $this->getRequest() ++ ->getParam('form_namespace'); + $typeArr = explode('|', str_replace('-', '/', $this->getRequest()->getParam('type'))); + $type = $typeArr[0]; + +@@ -27,7 +36,7 @@ class NewActionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote + )->setType( + $type + )->setRule( +- $this->_objectManager->create(\Magento\SalesRule\Model\Rule::class) ++ $this->_objectManager->create(Rule::class) + )->setPrefix( + 'actions' + ); +@@ -35,12 +44,14 @@ class NewActionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote + $model->setAttribute($typeArr[1]); + } + +- if ($model instanceof \Magento\Rule\Model\Condition\AbstractCondition) { ++ if ($model instanceof AbstractCondition) { + $model->setJsFormObject($formName); ++ $model->setFormName($formName); + $html = $model->asHtmlRecursive(); + } else { + $html = ''; + } +- $this->getResponse()->setBody($html); ++ $this->getResponse() ++ ->setBody($html); + } + } +diff -Nuar a/vendor/magento/module-swatches/view/frontend/web/js/swatch-renderer.js b/vendor/magento/module-swatches/view/frontend/web/js/swatch-renderer.js +--- a/vendor/magento/module-swatches/view/frontend/web/js/swatch-renderer.js ++++ b/vendor/magento/module-swatches/view/frontend/web/js/swatch-renderer.js +@@ -754,7 +754,7 @@ define([ + $widget.options.jsonConfig.optionPrices + ]); + +- if (checkAdditionalData['update_product_preview_image'] === '1') { ++ if (parseInt(checkAdditionalData['update_product_preview_image'], 10) === 1) { + $widget._loadMedia(); + } + +diff -Nuar a/vendor/magento/framework/Session/SaveHandler.php b/vendor/magento/framework/Session/SaveHandler.php +--- a/vendor/magento/framework/Session/SaveHandler.php ++++ b/vendor/magento/framework/Session/SaveHandler.php +@@ -6,7 +6,7 @@ + namespace Magento\Framework\Session; + + use Magento\Framework\Session\Config\ConfigInterface; +-use \Magento\Framework\Exception\SessionException; ++use Magento\Framework\Exception\SessionException; + + /** + * Magento session save handler +@@ -21,8 +21,21 @@ class SaveHandler implements SaveHandlerInterface + protected $saveHandlerAdapter; + + /** +- * Constructor +- * ++ * @var SaveHandlerFactory ++ */ ++ private $saveHandlerFactory; ++ ++ /** ++ * @var ConfigInterface ++ */ ++ private $sessionConfig; ++ ++ /** ++ * @var string ++ */ ++ private $defaultHandler; ++ ++ /** + * @param SaveHandlerFactory $saveHandlerFactory + * @param ConfigInterface $sessionConfig + * @param string $default +@@ -32,19 +45,9 @@ class SaveHandler implements SaveHandlerInterface + ConfigInterface $sessionConfig, + $default = self::DEFAULT_HANDLER + ) { +- /** +- * Session handler +- * +- * Save handler may be set to custom value in deployment config, which will override everything else. +- * Otherwise, try to read PHP settings for session.save_handler value. Otherwise, use 'files' as default. +- */ +- $saveMethod = $sessionConfig->getOption('session.save_handler') ?: $default; +- +- try { +- $this->saveHandlerAdapter = $saveHandlerFactory->create($saveMethod); +- } catch (SessionException $e) { +- $this->saveHandlerAdapter = $saveHandlerFactory->create($default); +- } ++ $this->saveHandlerFactory = $saveHandlerFactory; ++ $this->sessionConfig = $sessionConfig; ++ $this->defaultHandler = $default; + } + + /** +@@ -56,7 +59,7 @@ class SaveHandler implements SaveHandlerInterface + */ + public function open($savePath, $name) + { +- return $this->saveHandlerAdapter->open($savePath, $name); ++ return $this->callSafely('open', $savePath, $name); + } + + /** +@@ -66,7 +69,7 @@ class SaveHandler implements SaveHandlerInterface + */ + public function close() + { +- return $this->saveHandlerAdapter->close(); ++ return $this->callSafely('close'); + } + + /** +@@ -77,7 +80,7 @@ class SaveHandler implements SaveHandlerInterface + */ + public function read($sessionId) + { +- return $this->saveHandlerAdapter->read($sessionId); ++ return $this->callSafely('read', $sessionId); + } + + /** +@@ -89,7 +92,7 @@ class SaveHandler implements SaveHandlerInterface + */ + public function write($sessionId, $data) + { +- return $this->saveHandlerAdapter->write($sessionId, $data); ++ return $this->callSafely('write', $sessionId, $data); + } + + /** +@@ -100,12 +103,11 @@ class SaveHandler implements SaveHandlerInterface + */ + public function destroy($sessionId) + { +- return $this->saveHandlerAdapter->destroy($sessionId); ++ return $this->callSafely('destroy', $sessionId); + } + + /** +- * Garbage Collection - remove old session data older +- * than $maxLifetime (in seconds) ++ * Garbage Collection - remove old session data older than $maxLifetime (in seconds) + * + * @param int $maxLifetime + * @return bool +@@ -113,6 +115,30 @@ class SaveHandler implements SaveHandlerInterface + */ + public function gc($maxLifetime) + { +- return $this->saveHandlerAdapter->gc($maxLifetime); ++ return $this->callSafely('gc', $maxLifetime); ++ } ++ ++ /** ++ * Call save handler adapter method. ++ * ++ * In case custom handler failed, default files handler is used. ++ * ++ * @param string $method ++ * @param mixed $arguments ++ * ++ * @return mixed ++ */ ++ private function callSafely(string $method, ...$arguments) ++ { ++ try { ++ if ($this->saveHandlerAdapter === null) { ++ $saveMethod = $this->sessionConfig->getOption('session.save_handler') ?: $this->defaultHandler; ++ $this->saveHandlerAdapter = $this->saveHandlerFactory->create($saveMethod); ++ } ++ return $this->saveHandlerAdapter->{$method}(...$arguments); ++ } catch (SessionException $exception) { ++ $this->saveHandlerAdapter = $this->saveHandlerFactory->create($this->defaultHandler); ++ return $this->saveHandlerAdapter->{$method}(...$arguments); ++ } + } + } +diff -Nuar a/vendor/magento/framework/Session/SaveHandler/Redis.php b/vendor/magento/framework/Session/SaveHandler/Redis.php +--- a/vendor/magento/framework/Session/SaveHandler/Redis.php ++++ b/vendor/magento/framework/Session/SaveHandler/Redis.php +@@ -14,6 +14,9 @@ use Magento\Framework\Phrase; + use Magento\Framework\Filesystem; + use Magento\Framework\App\Filesystem\DirectoryList; + ++/** ++ * Redis session save handler ++ */ + class Redis implements \SessionHandlerInterface + { + /** +@@ -40,7 +43,6 @@ class Redis implements \SessionHandlerInterface + * @param ConfigInterface $config + * @param LoggerInterface $logger + * @param Filesystem $filesystem +- * @throws SessionException + */ + public function __construct(ConfigInterface $config, LoggerInterface $logger, Filesystem $filesystem) + { diff --git a/patches/MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch b/patches/MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch deleted file mode 100644 index 8c3806ac..00000000 --- a/patches/MC-21696__fix_broken_pagination_on_ES6__2.3.3.patch +++ /dev/null @@ -1,11 +0,0 @@ -diff -Naur a/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php ---- a/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php -+++ b/vendor/magento/module-elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php -@@ -50,6 +50,7 @@ class SearchResultApplier implements SearchResultApplierInterface - foreach ($this->searchResult->getItems() as $item) { - $ids[] = (int)$item->getId(); - } -+ $this->collection->setPageSize(null); - $this->collection->getSelect()->where('e.entity_id IN (?)', $ids); - $orderList = join(',', $ids); - $this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER); From 7eb8f4a2631fc0a8cbc54c093a9ecd321361ec6b Mon Sep 17 00:00:00 2001 From: Billy Gilbert Date: Tue, 7 Jan 2020 13:03:29 -0600 Subject: [PATCH 012/157] MAGECLOUD-4884: Update PageBuilder Patches for RCE (#11) --- patches.json | 6 +- ...22979__fix_pagebuilder_module__2.3.1.patch | 172 --- ...22979__fix_pagebuilder_module__2.3.2.patch | 172 --- ...B-319__fix_pagebuilder_module__2.3.1.patch | 1341 +++++++++++++++++ ...B-320__fix_pagebuilder_module__2.3.2.patch | 1341 +++++++++++++++++ ...22__fix_pagebuilder_module__2.3.2-p1.patch | 1271 ++++++++++++++++ ...B-323__fix_pagebuilder_module__2.3.3.patch | 1280 ++++++++++++++++ 7 files changed, 5237 insertions(+), 346 deletions(-) delete mode 100644 patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch delete mode 100644 patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch create mode 100644 patches/PB-319__fix_pagebuilder_module__2.3.1.patch create mode 100644 patches/PB-320__fix_pagebuilder_module__2.3.2.patch create mode 100644 patches/PB-322__fix_pagebuilder_module__2.3.2-p1.patch create mode 100644 patches/PB-323__fix_pagebuilder_module__2.3.3.patch diff --git a/patches.json b/patches.json index c5e14486..34dbd5f4 100644 --- a/patches.json +++ b/patches.json @@ -203,8 +203,10 @@ ">=2.3.2 <2.3.4": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch" }, "Fix pagebuilder module": { - "2.3.1": "MDVA-22979__fix_pagebuilder_module__2.3.1.patch", - "2.3.2": "MDVA-22979__fix_pagebuilder_module__2.3.2.patch" + "2.3.1": "PB-319__fix_pagebuilder_module__2.3.1.patch", + "2.3.2": "PB-320__fix_pagebuilder_module__2.3.2.patch", + ">=2.3.2-p1 <2.3.3": "PB-322__fix_pagebuilder_module__2.3.2-p1.patch", + "2.3.3": "PB-323__fix_pagebuilder_module__2.3.3.patch" }, "Fix XSS in order history": { "2.2.0 - 2.2.6": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch", diff --git a/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch b/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch deleted file mode 100644 index 839f713c..00000000 --- a/patches/MDVA-22979__fix_pagebuilder_module__2.3.1.patch +++ /dev/null @@ -1,172 +0,0 @@ -diff -Nuar a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php ---- a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php -+++ b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php -@@ -53,19 +53,26 @@ class Preview extends \Magento\Backend\Block\Widget - * Prepare html output - * - * @return string -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function _toHtml() - { -+ $request = $this->getRequest(); -+ -+ if (!$request instanceof \Magento\Framework\App\RequestSafetyInterface || !$request->isSafeMethod()) { -+ throw new \Magento\Framework\Exception\LocalizedException(__('Wrong request.')); -+ } -+ - $storeId = $this->getAnyStoreView()->getId(); - /** @var $template \Magento\Email\Model\Template */ - $template = $this->_emailFactory->create(); - -- if ($id = (int)$this->getRequest()->getParam('id')) { -+ if ($id = (int)$request->getParam('id')) { - $template->load($id); - } else { -- $template->setTemplateType($this->getRequest()->getParam('type')); -- $template->setTemplateText($this->getRequest()->getParam('text')); -- $template->setTemplateStyles($this->getRequest()->getParam('styles')); -+ $template->setTemplateType($request->getParam('type')); -+ $template->setTemplateText($request->getParam('text')); -+ $template->setTemplateStyles($request->getParam('styles')); - } - - $template->setTemplateText($this->_maliciousCode->filter($template->getTemplateText())); -diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php ---- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php -+++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php -@@ -26,19 +26,28 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc - */ - private $rendererPool; - -+ /** -+ * @var \Magento\Backend\Model\Auth -+ */ -+ private $auth; -+ - /** - * Constructor - * - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool -+ * @param \Magento\Backend\Model\Auth $auth - */ - public function __construct( - \Magento\Backend\App\Action\Context $context, -- \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool -+ \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool, -+ \Magento\Backend\Model\Auth $auth = null - ) { - parent::__construct($context); - - $this->rendererPool = $rendererPool; -+ $this->auth = $auth ?? \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Backend\Model\Auth::class); - } - - /** -@@ -48,14 +57,18 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc - */ - public function execute() - { -- $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); -- // Some template filters and directive processors expect this to be called in order to function. -- $pageResult->initLayout(); -+ if ($this->auth->isLoggedIn()) { -+ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); -+ // Some template filters and directive processors expect this to be called in order to function. -+ $pageResult->initLayout(); -+ -+ $params = $this->getRequest()->getParams(); -+ $renderer = $this->rendererPool->getRenderer($params['role']); -+ $result = ['data' => $renderer->render($params)]; - -- $params = $this->getRequest()->getParams(); -- $renderer = $this->rendererPool->getRenderer($params['role']); -- $result = ['data' => $renderer->render($params)]; -+ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); -+ } - -- return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); -+ $this->_forward('noroute'); - } - } -diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php ---- a/vendor/magento/module-page-builder/Model/Stage/Config.php -+++ b/vendor/magento/module-page-builder/Model/Stage/Config.php -@@ -135,7 +135,9 @@ class Config - 'content_types' => $this->getContentTypes(), - 'stage_config' => $this->data, - 'media_url' => $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]), -- 'preview_url' => $this->frontendUrlBuilder->getUrl('pagebuilder/contenttype/preview'), -+ 'preview_url' => $this->frontendUrlBuilder -+ ->addSessionParam() -+ ->getUrl('pagebuilder/contenttype/preview'), - 'column_grid_default' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_DEFAULT), - 'column_grid_max' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_MAX), - 'can_use_inline_editing_on_stage' => $this->isWysiwygProvisionedForEditingOnStage(), -diff -Nuar a/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php -new file mode 100644 ---- /dev/null -+++ b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php -@@ -0,0 +1,49 @@ -+request = $request; -+ } -+ -+ /** -+ * Get Sid for pagebuilder preview -+ * -+ * @param \Magento\Framework\Session\SidResolver $subject -+ * @param string|null $result -+ * @param \Magento\Framework\Session\SessionManagerInterface $session -+ * -+ * @return string|null -+ */ -+ public function afterGetSid( -+ \Magento\Framework\Session\SidResolver $subject, -+ $result, -+ \Magento\Framework\Session\SessionManagerInterface $session -+ ) { -+ if (strpos($this->request->getPathInfo(), '/pagebuilder/contenttype/preview') === 0) { -+ return $this->request->getQuery( -+ $subject->getSessionIdQueryParam($session) -+ ); -+ } -+ -+ return $result; -+ } -+} -diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml ---- a/vendor/magento/module-page-builder/etc/di.xml -+++ b/vendor/magento/module-page-builder/etc/di.xml -@@ -140,4 +140,7 @@ - - - -+ -+ -+ - diff --git a/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch b/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch deleted file mode 100644 index 97b68b83..00000000 --- a/patches/MDVA-22979__fix_pagebuilder_module__2.3.2.patch +++ /dev/null @@ -1,172 +0,0 @@ -diff -Nuar a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php ---- a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php -+++ b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php -@@ -55,19 +55,26 @@ class Preview extends \Magento\Backend\Block\Widget - * Prepare html output - * - * @return string -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function _toHtml() - { -+ $request = $this->getRequest(); -+ -+ if (!$request instanceof \Magento\Framework\App\RequestSafetyInterface || !$request->isSafeMethod()) { -+ throw new \Magento\Framework\Exception\LocalizedException(__('Wrong request.')); -+ } -+ - $storeId = $this->getAnyStoreView()->getId(); - /** @var $template \Magento\Email\Model\Template */ - $template = $this->_emailFactory->create(); - -- if ($id = (int)$this->getRequest()->getParam('id')) { -+ if ($id = (int)$request->getParam('id')) { - $template->load($id); - } else { -- $template->setTemplateType($this->getRequest()->getParam('type')); -- $template->setTemplateText($this->getRequest()->getParam('text')); -- $template->setTemplateStyles($this->getRequest()->getParam('styles')); -+ $template->setTemplateType($request->getParam('type')); -+ $template->setTemplateText($request->getParam('text')); -+ $template->setTemplateStyles($request->getParam('styles')); - } - - \Magento\Framework\Profiler::start($this->profilerName); -diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php ---- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php -+++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php -@@ -26,19 +26,28 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc - */ - private $rendererPool; - -+ /** -+ * @var \Magento\Backend\Model\Auth -+ */ -+ private $auth; -+ - /** - * Constructor - * - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool -+ * @param \Magento\Backend\Model\Auth $auth - */ - public function __construct( - \Magento\Backend\App\Action\Context $context, -- \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool -+ \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool, -+ \Magento\Backend\Model\Auth $auth = null - ) { - parent::__construct($context); - - $this->rendererPool = $rendererPool; -+ $this->auth = $auth ?? \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Backend\Model\Auth::class); - } - - /** -@@ -48,14 +57,18 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc - */ - public function execute() - { -- $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); -- // Some template filters and directive processors expect this to be called in order to function. -- $pageResult->initLayout(); -+ if ($this->auth->isLoggedIn()) { -+ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); -+ // Some template filters and directive processors expect this to be called in order to function. -+ $pageResult->initLayout(); -+ -+ $params = $this->getRequest()->getParams(); -+ $renderer = $this->rendererPool->getRenderer($params['role']); -+ $result = ['data' => $renderer->render($params)]; - -- $params = $this->getRequest()->getParams(); -- $renderer = $this->rendererPool->getRenderer($params['role']); -- $result = ['data' => $renderer->render($params)]; -+ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); -+ } - -- return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); -+ $this->_forward('noroute'); - } - } -diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php ---- a/vendor/magento/module-page-builder/Model/Stage/Config.php -+++ b/vendor/magento/module-page-builder/Model/Stage/Config.php -@@ -135,7 +135,9 @@ class Config - 'content_types' => $this->getContentTypes(), - 'stage_config' => $this->data, - 'media_url' => $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]), -- 'preview_url' => $this->frontendUrlBuilder->getUrl('pagebuilder/contenttype/preview'), -+ 'preview_url' => $this->frontendUrlBuilder -+ ->addSessionParam() -+ ->getUrl('pagebuilder/contenttype/preview'), - 'column_grid_default' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_DEFAULT), - 'column_grid_max' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_MAX), - 'can_use_inline_editing_on_stage' => $this->isWysiwygProvisionedForEditingOnStage(), -diff -Nuar a/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php -new file mode 100644 ---- /dev/null -+++ b/vendor/magento/module-page-builder/Plugin/Framework/Session/SidResolver.php -@@ -0,0 +1,49 @@ -+request = $request; -+ } -+ -+ /** -+ * Get Sid for pagebuilder preview -+ * -+ * @param \Magento\Framework\Session\SidResolver $subject -+ * @param string|null $result -+ * @param \Magento\Framework\Session\SessionManagerInterface $session -+ * -+ * @return string|null -+ */ -+ public function afterGetSid( -+ \Magento\Framework\Session\SidResolver $subject, -+ $result, -+ \Magento\Framework\Session\SessionManagerInterface $session -+ ) { -+ if (strpos($this->request->getPathInfo(), '/pagebuilder/contenttype/preview') === 0) { -+ return $this->request->getQuery( -+ $subject->getSessionIdQueryParam($session) -+ ); -+ } -+ -+ return $result; -+ } -+} -diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml ---- a/vendor/magento/module-page-builder/etc/di.xml -+++ b/vendor/magento/module-page-builder/etc/di.xml -@@ -140,4 +140,7 @@ - - - -+ -+ -+ - diff --git a/patches/PB-319__fix_pagebuilder_module__2.3.1.patch b/patches/PB-319__fix_pagebuilder_module__2.3.1.patch new file mode 100644 index 00000000..d8adfb19 --- /dev/null +++ b/patches/PB-319__fix_pagebuilder_module__2.3.1.patch @@ -0,0 +1,1341 @@ +diff -Nuar a/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php b/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php +@@ -0,0 +1,71 @@ ++rendererPool = $rendererPool; ++ $this->preview = $preview; ++ } ++ ++ /** ++ * Generates an HTML preview for the stage ++ * ++ * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|mixed ++ * @throws \Exception ++ */ ++ public function execute() ++ { ++ return $this->preview->startPreviewMode( ++ function () { ++ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); ++ // Some template filters and directive processors expect this to be called in order to function. ++ $pageResult->initLayout(); ++ ++ $params = $this->getRequest()->getParams(); ++ $renderer = $this->rendererPool->getRenderer($params['role']); ++ $result = ['data' => $renderer->render($params)]; ++ ++ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ } ++ ); ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +--- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php ++++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +@@ -17,6 +17,8 @@ use Magento\Framework\App\Action\HttpPostActionInterface; + * This isn't placed within the adminhtml folder as it has to extend from the front-end controllers app action to + * ensure the content is rendered in the storefront scope. + * ++ * @deprecated use \Magento\PageBuilder\Controller\Adminhtml\Stage\Preview ++ * + * @api + */ + class Preview extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface +@@ -26,19 +28,28 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc + */ + private $rendererPool; + ++ /** ++ * @var \Magento\Backend\Model\Auth ++ */ ++ private $auth; ++ + /** + * Constructor + * + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool ++ * @param \Magento\Backend\Model\Auth $auth + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, +- \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool ++ \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool, ++ \Magento\Backend\Model\Auth $auth = null + ) { + parent::__construct($context); + + $this->rendererPool = $rendererPool; ++ $this->auth = $auth ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\Backend\Model\Auth::class); + } + + /** +@@ -48,14 +59,18 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc + */ + public function execute() + { +- $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); +- // Some template filters and directive processors expect this to be called in order to function. +- $pageResult->initLayout(); ++ if ($this->auth->isLoggedIn()) { ++ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); ++ // Some template filters and directive processors expect this to be called in order to function. ++ $pageResult->initLayout(); ++ ++ $params = $this->getRequest()->getParams(); ++ $renderer = $this->rendererPool->getRenderer($params['role']); ++ $result = ['data' => $renderer->render($params)]; + +- $params = $this->getRequest()->getParams(); +- $renderer = $this->rendererPool->getRenderer($params['role']); +- $result = ['data' => $renderer->render($params)]; ++ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ } + +- return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ $this->_forward('noroute'); + } + } +diff -Nuar a/vendor/magento/module-page-builder/Model/Filter/Template.php b/vendor/magento/module-page-builder/Model/Filter/Template.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Model/Filter/Template.php +@@ -0,0 +1,334 @@ ++logger = $logger; ++ $this->viewConfig = $viewConfig; ++ $this->mathRandom = $mathRandom; ++ $this->json = $json; ++ } ++ ++ /** ++ * After filter of template data apply transformations ++ * ++ * @param string $result ++ * ++ * @return string ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function filter(string $result) : string ++ { ++ $this->domDocument = false; ++ ++ // Validate if the filtered result requires background image processing ++ if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::BACKGROUND_IMAGE_PATTERN, $result)) { ++ $document = $this->getDomDocument($result); ++ $this->generateBackgroundImageStyles($document); ++ } ++ ++ // Process any HTML content types, they need to be decoded on the front-end ++ if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::HTML_CONTENT_TYPE_PATTERN, $result)) { ++ $document = $this->getDomDocument($result); ++ $uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document); ++ } ++ ++ // If a document was retrieved we've modified the output so need to retrieve it from within the document ++ if (isset($document)) { ++ // Match the contents of the body from our generated document ++ preg_match( ++ '/(.+)<\/body><\/html>$/si', ++ $document->saveHTML(), ++ $matches ++ ); ++ ++ if (!empty($matches)) { ++ $docHtml = $matches[1]; ++ ++ // restore any encoded directives ++ $docHtml = preg_replace_callback( ++ '/=\"(%7B%7B[^"]*%7D%7D)\"/m', ++ function ($matches) { ++ return urldecode($matches[0]); ++ }, ++ $docHtml ++ ); ++ ++ if (isset($uniqueNodeNameToDecodedOuterHtmlMap)) { ++ foreach ($uniqueNodeNameToDecodedOuterHtmlMap as $uniqueNodeName => $decodedOuterHtml) { ++ $docHtml = str_replace( ++ '<' . $uniqueNodeName . '>' . '', ++ $decodedOuterHtml, ++ $docHtml ++ ); ++ } ++ } ++ ++ $result = $docHtml; ++ } ++ } ++ ++ return $result; ++ } ++ ++ /** ++ * Create a DOM document from a given string ++ * ++ * @param string $html ++ * ++ * @return \DOMDocument ++ */ ++ private function getDomDocument(string $html) : \DOMDocument ++ { ++ if (!$this->domDocument) { ++ $this->domDocument = $this->createDomDocument($html); ++ } ++ ++ return $this->domDocument; ++ } ++ ++ /** ++ * Create a DOMDocument from a string ++ * ++ * @param string $html ++ * ++ * @return \DOMDocument ++ */ ++ private function createDomDocument(string $html) : \DOMDocument ++ { ++ $domDocument = new \DOMDocument('1.0', 'UTF-8'); ++ set_error_handler( ++ function ($errorNumber, $errorString) { ++ throw new \DOMException($errorString, $errorNumber); ++ } ++ ); ++ $string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); ++ try { ++ libxml_use_internal_errors(true); ++ $domDocument->loadHTML( ++ '' . $string . '' ++ ); ++ libxml_clear_errors(); ++ } catch (\Exception $e) { ++ restore_error_handler(); ++ $this->logger->critical($e); ++ } ++ restore_error_handler(); ++ ++ return $domDocument; ++ } ++ ++ /** ++ * Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement ++ * ++ * @param \DOMDocument $document ++ * @return array ++ * @throws \Magento\Framework\Exception\LocalizedException ++ */ ++ private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array ++ { ++ $xpath = new \DOMXPath($document); ++ ++ // construct xpath query to fetch top-level ancestor html content type nodes ++ /** @var $htmlContentTypeNodes \DOMNode[] */ ++ $htmlContentTypeNodes = $xpath->query( ++ '//*[@data-content-type="html" and not(@data-decoded="true")]' . ++ '[not(ancestor::*[@data-content-type="html"])]' ++ ); ++ ++ $uniqueNodeNameToDecodedOuterHtmlMap = []; ++ ++ foreach ($htmlContentTypeNodes as $htmlContentTypeNode) { ++ // Set decoded attribute on all encoded html content types so we don't double decode; ++ $htmlContentTypeNode->setAttribute('data-decoded', 'true'); ++ ++ // if nothing exists inside the node, continue ++ if (!strlen(trim($htmlContentTypeNode->nodeValue))) { ++ continue; ++ } ++ ++ // clone html code content type to save reference to its attributes/outerHTML, which we are not going to ++ // decode ++ $clonedHtmlContentTypeNode = clone $htmlContentTypeNode; ++ ++ // clear inner contents of cloned node for replacement later with $decodedInnerHtml using sprintf; ++ // we want to retain html content type node and avoid doing any manipulation on it ++ $clonedHtmlContentTypeNode->nodeValue = '%s'; ++ ++ // remove potentially harmful attributes on html content type node itself ++ while ($htmlContentTypeNode->attributes->length) { ++ $htmlContentTypeNode->removeAttribute($htmlContentTypeNode->attributes->item(0)->name); ++ } ++ ++ // decode outerHTML safely ++ $preDecodedOuterHtml = $document->saveHTML($htmlContentTypeNode); ++ ++ // clear empty
wrapper around outerHTML to replace with $clonedHtmlContentTypeNode ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $decodedInnerHtml = preg_replace('#^<[^>]*>|]*>$#', '', html_entity_decode($preDecodedOuterHtml)); ++ ++ // Use $clonedHtmlContentTypeNode's placeholder to inject decoded inner html ++ $decodedOuterHtml = sprintf($document->saveHTML($clonedHtmlContentTypeNode), $decodedInnerHtml); ++ ++ // generate unique node name element to replace with decoded html contents at end of processing; ++ // goal is to create a document as few times as possible to prevent inadvertent parsing of contents as html ++ // by the dom library ++ $uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS); ++ ++ $uniqueNode = new \DOMElement($uniqueNodeName); ++ $htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode); ++ ++ $uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml; ++ } ++ ++ return $uniqueNodeNameToDecodedOuterHtmlMap; ++ } ++ ++ /** ++ * Generate the CSS for any background images on the page ++ * ++ * @param \DOMDocument $document ++ */ ++ private function generateBackgroundImageStyles(\DOMDocument $document) : void ++ { ++ $xpath = new \DOMXPath($document); ++ $nodes = $xpath->query('//*[@data-background-images]'); ++ foreach ($nodes as $node) { ++ /* @var \DOMElement $node */ ++ $backgroundImages = $node->attributes->getNamedItem('data-background-images'); ++ if ($backgroundImages->nodeValue !== '') { ++ $elementClass = uniqid('background-image-'); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $images = $this->json->unserialize(stripslashes($backgroundImages->nodeValue)); ++ if (count($images) > 0) { ++ $style = $xpath->document->createElement( ++ 'style', ++ $this->generateCssFromImages($elementClass, $images) ++ ); ++ $style->setAttribute('type', 'text/css'); ++ $node->parentNode->appendChild($style); ++ ++ // Append our new class to the DOM element ++ $classes = ''; ++ if ($node->attributes->getNamedItem('class')) { ++ $classes = $node->attributes->getNamedItem('class')->nodeValue . ' '; ++ } ++ $node->setAttribute('class', $classes . $elementClass); ++ } ++ } ++ } ++ } ++ ++ /** ++ * Generate CSS based on the images array from our attribute ++ * ++ * @param string $elementClass ++ * @param array $images ++ * ++ * @return string ++ */ ++ private function generateCssFromImages(string $elementClass, array $images) : string ++ { ++ $css = []; ++ if (isset($images['desktop_image'])) { ++ $css['.' . $elementClass] = [ ++ 'background-image' => 'url('https://github.com/.%20$images['desktop_image']%20.')', ++ ]; ++ } ++ if (isset($images['mobile_image']) && $this->getMobileMediaQuery()) { ++ $css[$this->getMobileMediaQuery()]['.' . $elementClass] = [ ++ 'background-image' => 'url('https://github.com/.%20$images['mobile_image']%20.')', ++ ]; ++ } ++ return $this->cssFromArray($css); ++ } ++ ++ /** ++ * Generate a CSS string from an array ++ * ++ * @param array $css ++ * ++ * @return string ++ */ ++ private function cssFromArray(array $css) : string ++ { ++ $output = ''; ++ foreach ($css as $selector => $body) { ++ if (is_array($body)) { ++ $output .= $selector . ' {'; ++ $output .= $this->cssFromArray($body); ++ $output .= '}'; ++ } else { ++ $output .= $selector . ': ' . $body . ';'; ++ } ++ } ++ return $output; ++ } ++ ++ /** ++ * Generate the mobile media query from view configuration ++ * ++ * @return null|string ++ */ ++ private function getMobileMediaQuery() : ?string ++ { ++ $breakpoints = $this->viewConfig->getViewConfig()->getVarValue( ++ 'Magento_PageBuilder', ++ 'breakpoints/mobile/conditions' ++ ); ++ if ($breakpoints && count($breakpoints) > 0) { ++ $mobileBreakpoint = '@media only screen '; ++ foreach ($breakpoints as $key => $value) { ++ $mobileBreakpoint .= 'and (' . $key . ': ' . $value . ') '; ++ } ++ return rtrim($mobileBreakpoint); ++ } ++ return null; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php +--- a/vendor/magento/module-page-builder/Model/Stage/Config.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Config.php +@@ -135,7 +135,7 @@ class Config + 'content_types' => $this->getContentTypes(), + 'stage_config' => $this->data, + 'media_url' => $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]), +- 'preview_url' => $this->frontendUrlBuilder->getUrl('pagebuilder/contenttype/preview'), ++ 'preview_url' => $this->urlBuilder->getUrl('pagebuilder/stage/preview'), + 'column_grid_default' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_DEFAULT), + 'column_grid_max' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_MAX), + 'can_use_inline_editing_on_stage' => $this->isWysiwygProvisionedForEditingOnStage(), +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Preview.php b/vendor/magento/module-page-builder/Model/Stage/Preview.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Model/Stage/Preview.php +@@ -0,0 +1,134 @@ ++emulation = $emulation; ++ $this->appState = $appState; ++ $this->design = $design; ++ $this->themeProvider = $themeProvider; ++ $this->storeManager = $storeManager; ++ $this->scopeConfig = $scopeConfig; ++ } ++ ++ /** ++ * @var bool ++ */ ++ private $isPreview; ++ ++ /** ++ * Retrieve the area in which the preview needs to be ran in ++ * ++ * @return string ++ */ ++ public function getPreviewArea() : string ++ { ++ return \Magento\Framework\App\Area::AREA_FRONTEND; ++ } ++ ++ /** ++ * Start Page Builder preview mode and emulate store front ++ * ++ * @param callable $callback ++ * @param int $storeId ++ * @return mixed ++ * @throws \Exception ++ */ ++ public function startPreviewMode($callback, $storeId = null) ++ { ++ $this->isPreview = true; ++ ++ if (!$storeId) { ++ $storeId = $this->storeManager->getDefaultStoreView()->getId(); ++ } ++ $this->emulation->startEnvironmentEmulation($storeId); ++ ++ return $this->appState->emulateAreaCode( ++ $this->getPreviewArea(), ++ function () use ($callback) { ++ $themeId = $this->scopeConfig->getValue( ++ 'design/theme/theme_id', ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ $theme = $this->themeProvider->getThemeById($themeId); ++ $this->design->setDesignTheme($theme, $this->getPreviewArea()); ++ ++ try { ++ $result = $callback(); ++ } catch (\Exception $e) { ++ $this->isPreview = false; ++ throw $e; ++ } ++ ++ $this->emulation->stopEnvironmentEmulation(); ++ return $result; ++ } ++ ); ++ } ++ ++ /** ++ * Determine if the system is in preview mode ++ * ++ * @return bool ++ */ ++ public function isPreviewMode() : bool ++ { ++ return $this->isPreview; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Magento\Framework\Controller\ResultFactory; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a block for the stage +@@ -31,20 +32,27 @@ class Block implements \Magento\PageBuilder\Model\Stage\RendererInterface + private $resultFactory; + + /** +- * Constructor +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\PageBuilder\Model\Config $config + * @param \Magento\Framework\View\Element\BlockFactory $blockFactory + * @param ResultFactory $resultFactory ++ * @param Template|null $templateFilter + */ + public function __construct( + \Magento\PageBuilder\Model\Config $config, + \Magento\Framework\View\Element\BlockFactory $blockFactory, +- ResultFactory $resultFactory ++ ResultFactory $resultFactory, ++ Template $templateFilter = null + ) { + $this->config = $config; + $this->blockFactory = $blockFactory; + $this->resultFactory = $resultFactory; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -77,7 +85,7 @@ class Block implements \Magento\PageBuilder\Model\Stage\RendererInterface + $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $pageResult->getLayout()->addBlock($backendBlockInstance); + +- $result['content'] = $backendBlockInstance->toHtml(); ++ $result['content'] = $this->templateFilter->filter($backendBlockInstance->toHtml()); + } + + return $result; +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php +@@ -9,6 +9,8 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Psr\Log\LoggerInterface; ++use Magento\PageBuilder\Model\Stage\HtmlFilter; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a CMS Block for the stage +@@ -33,28 +35,35 @@ class CmsStaticBlock implements \Magento\PageBuilder\Model\Stage\RendererInterfa + private $loggerInterface; + + /** +- * @var \Magento\PageBuilder\Model\Stage\HtmlFilter ++ * @var HtmlFilter + */ + private $htmlFilter; + + /** +- * CmsStaticBlock constructor. +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\Cms\Model\ResourceModel\Block\CollectionFactory $blockCollectionFactory + * @param WidgetDirective $widgetDirectiveRenderer + * @param LoggerInterface $loggerInterface + * @param \Magento\PageBuilder\Model\Stage\HtmlFilter $htmlFilter ++ * @param \Magento\PageBuilder\Model\Filter\Template|null $templateFilter + */ + public function __construct( + \Magento\Cms\Model\ResourceModel\Block\CollectionFactory $blockCollectionFactory, + WidgetDirective $widgetDirectiveRenderer, + LoggerInterface $loggerInterface, +- \Magento\PageBuilder\Model\Stage\HtmlFilter $htmlFilter ++ HtmlFilter $htmlFilter, ++ Template $templateFilter = null + ) { + $this->blockCollectionFactory = $blockCollectionFactory; + $this->widgetDirectiveRenderer = $widgetDirectiveRenderer; + $this->loggerInterface = $loggerInterface; + $this->htmlFilter = $htmlFilter; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -96,7 +105,9 @@ class CmsStaticBlock implements \Magento\PageBuilder\Model\Stage\RendererInterfa + + if ($block->isActive()) { + $directiveResult = $this->widgetDirectiveRenderer->render($params); +- $result['content'] = $this->htmlFilter->filterHtml($directiveResult['content']); ++ $result['content'] = $this->htmlFilter->filterHtml( ++ $this->templateFilter->filter($directiveResult['content']) ++ ); + } else { + $result['error'] = __('Block disabled'); + } +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Magento\Store\Model\Store; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a widget directive for the stage +@@ -28,17 +29,24 @@ class WidgetDirective implements \Magento\PageBuilder\Model\Stage\RendererInterf + private $directiveFilter; + + /** +- * Constructor +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Widget\Model\Template\Filter $directiveFilter ++ * @param Template $templateFilter + */ + public function __construct( + \Magento\Store\Model\StoreManagerInterface $storeManager, +- \Magento\Widget\Model\Template\Filter $directiveFilter ++ \Magento\Widget\Model\Template\Filter $directiveFilter, ++ Template $templateFilter = null + ) { + $this->storeManager = $storeManager; + $this->directiveFilter = $directiveFilter; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -61,7 +69,7 @@ class WidgetDirective implements \Magento\PageBuilder\Model\Stage\RendererInterf + try { + $result['content'] = $this->directiveFilter + ->setStoreId(Store::DEFAULT_STORE_ID) +- ->filter($params['directive']); ++ ->filter($this->templateFilter->filter($params['directive'])); + } catch (\Exception $e) { + $result['error'] = __($e->getMessage()); + } +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php b/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php +@@ -0,0 +1,48 @@ ++stock = $stock; ++ } ++ ++ /** ++ * Allow to sort product collection ++ * ++ * @param ProductsList $subject ++ * @param Collection $result ++ * @return Collection ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function afterCreateCollection( ++ ProductsList $subject, ++ Collection $result ++ ) { ++ $this->stock->addIsInStockFilterToCollection($result); ++ return $result; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Plugin/DesignLoader.php b/vendor/magento/module-page-builder/Plugin/DesignLoader.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/DesignLoader.php +@@ -0,0 +1,98 @@ ++designLoader = $designLoader; ++ $this->messageManager = $messageManager; ++ $this->appState = $appState; ++ $this->preview = $preview; ++ } ++ ++ /** ++ * Before create load the design files ++ * ++ * @param \Magento\Catalog\Block\Product\ImageFactory $subject ++ * @param Product $product ++ * @param string $imageId ++ * @param array|null $attributes ++ * @throws \Exception ++ * ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforeCreate( ++ \Magento\Catalog\Block\Product\ImageFactory $subject, ++ Product $product, ++ string $imageId, ++ array $attributes = null ++ ) { ++ if ($this->preview->isPreviewMode()) { ++ $this->appState->emulateAreaCode( ++ $this->preview->getPreviewArea(), ++ [$this, 'loadDesignConfig'] ++ ); ++ } ++ } ++ ++ /** ++ * Load the design config ++ */ ++ public function loadDesignConfig() ++ { ++ try { ++ $this->designLoader->load(); ++ } catch (\Magento\Framework\Exception\LocalizedException $e) { ++ if ($e->getPrevious() instanceof \Magento\Framework\Config\Dom\ValidationException) { ++ /** @var MessageInterface $message */ ++ $message = $this->messageManager ++ ->createMessage(MessageInterface::TYPE_ERROR) ++ ->setText($e->getMessage()); ++ $this->messageManager->addUniqueMessages([$message]); ++ } ++ } ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php b/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php +--- a/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php ++++ b/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php +@@ -7,50 +7,27 @@ declare(strict_types=1); + + namespace Magento\PageBuilder\Plugin\Filter; + +-use Magento\Store\Model\Store; +- + /** + * Plugin to the template filter to process any background images added by Page Builder + */ + class TemplatePlugin + { +- const BACKGROUND_IMAGE_PATTERN = '/data-background-images/si'; ++ const BACKGROUND_IMAGE_PATTERN = '/data-background-images=(?:\'|"){.+}(?:\'|")/si'; + + const HTML_CONTENT_TYPE_PATTERN = '/data-content-type="html"/si'; + + /** +- * @var \Magento\Framework\View\ConfigInterface +- */ +- private $viewConfig; +- +- /** +- * @var \Psr\Log\LoggerInterface +- */ +- private $logger; +- +- /** +- * @var \DOMDocument +- */ +- private $domDocument; +- +- /** +- * @var \Magento\Framework\Math\Random ++ * @var \Magento\PageBuilder\Model\Filter\Template + */ +- private $mathRandom; ++ private $templateFilter; + + /** +- * @param \Psr\Log\LoggerInterface $logger +- * @param \Magento\Framework\View\ConfigInterface $viewConfig +- * @param \Magento\Framework\Math\Random $mathRandom ++ * @param \Magento\PageBuilder\Model\Filter\Template $templateFilter + */ + public function __construct( +- \Psr\Log\LoggerInterface $logger, +- \Magento\Framework\View\ConfigInterface $viewConfig, +- \Magento\Framework\Math\Random $mathRandom ++ \Magento\PageBuilder\Model\Filter\Template $templateFilter + ) { +- $this->logger = $logger; +- $this->viewConfig = $viewConfig; +- $this->mathRandom = $mathRandom; ++ $this->templateFilter = $templateFilter; + } + + /** +@@ -64,284 +41,6 @@ class TemplatePlugin + */ + public function afterFilter(\Magento\Framework\Filter\Template $subject, string $result) : string + { +- $this->domDocument = false; +- +- // Validate if the filtered result requires background image processing +- if (preg_match(self::BACKGROUND_IMAGE_PATTERN, $result)) { +- $document = $this->getDomDocument($result); +- $this->generateBackgroundImageStyles($document); +- } +- +- // Process any HTML content types, they need to be decoded on the front-end +- if (preg_match(self::HTML_CONTENT_TYPE_PATTERN, $result)) { +- $document = $this->getDomDocument($result); +- $uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document); +- } +- +- // If a document was retrieved we've modified the output so need to retrieve it from within the document +- if (isset($document)) { +- // Match the contents of the body from our generated document +- preg_match( +- '/(.+)<\/body><\/html>$/si', +- $document->saveHTML(), +- $matches +- ); +- +- if (!empty($matches)) { +- $docHtml = $matches[1]; +- +- if (isset($uniqueNodeNameToDecodedOuterHtmlMap)) { +- foreach ($uniqueNodeNameToDecodedOuterHtmlMap as $uniqueNodeName => $decodedOuterHtml) { +- $docHtml = str_replace( +- '<' . $uniqueNodeName . '>' . '', +- $decodedOuterHtml, +- $docHtml +- ); +- } +- } +- +- $result = $docHtml; +- } +- } +- +- return $result; +- } +- +- /** +- * Determine if custom variable directive's return value needs to be escaped and do so if true +- * +- * @param \Magento\Framework\Filter\Template $subject +- * @param \Closure $proceed +- * @param string[] $construction +- * @return string +- */ +- public function aroundCustomvarDirective( +- \Magento\Framework\Filter\Template $subject, +- \Closure $proceed, +- $construction +- ) { +- // Determine the need to escape the return value of observed method. +- // Admin context requires store ID of 0; in that context return value should be escaped +- $shouldEscape = $subject->getStoreId() !== null && (int) $subject->getStoreId() === Store::DEFAULT_STORE_ID; +- +- if (!$shouldEscape) { +- return $proceed($construction); +- } +- +- $result = $proceed($construction); +- +- return htmlspecialchars($result); +- } +- +- /** +- * Create a DOM document from a given string +- * +- * @param string $html +- * +- * @return \DOMDocument +- */ +- private function getDomDocument(string $html) : \DOMDocument +- { +- if (!$this->domDocument) { +- $this->domDocument = $this->createDomDocument($html); +- } +- +- return $this->domDocument; +- } +- +- /** +- * Create a DOMDocument from a string +- * +- * @param string $html +- * +- * @return \DOMDocument +- */ +- private function createDomDocument(string $html) : \DOMDocument +- { +- $domDocument = new \DOMDocument('1.0', 'UTF-8'); +- set_error_handler( +- function ($errorNumber, $errorString) { +- throw new \Exception($errorString, $errorNumber); +- } +- ); +- $string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); +- try { +- libxml_use_internal_errors(true); +- $domDocument->loadHTML( +- '' . $string . '' +- ); +- libxml_clear_errors(); +- } catch (\Exception $e) { +- restore_error_handler(); +- $this->logger->critical($e); +- } +- restore_error_handler(); +- +- return $domDocument; +- } +- +- /** +- * Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement +- * +- * @param \DOMDocument $document +- * @return array - map of unique node name to decoded html +- */ +- private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array +- { +- $xpath = new \DOMXPath($document); +- +- // construct xpath query to fetch top-level ancestor html content type nodes +- /** @var $htmlContentTypeNodes \DOMNode[] */ +- $htmlContentTypeNodes = $xpath->query( +- '//*[@data-content-type="html" and not(@data-decoded="true")]' . +- '[not(ancestor::*[@data-content-type="html"])]' +- ); +- +- $uniqueNodeNameToDecodedOuterHtmlMap = []; +- +- foreach ($htmlContentTypeNodes as $htmlContentTypeNode) { +- // Set decoded attribute on all encoded html content types so we don't double decode; +- $htmlContentTypeNode->setAttribute('data-decoded', 'true'); +- +- // if nothing exists inside the node, continue +- if (!strlen(trim($htmlContentTypeNode->nodeValue))) { +- continue; +- } +- +- // clone html code content type to save reference to its attributes/outerHTML, which we are not going to +- // decode +- $clonedHtmlContentTypeNode = clone $htmlContentTypeNode; +- +- // clear inner contents of cloned node for replacement later with $decodedInnerHtml using sprintf; +- // we want to retain html content type node and avoid doing any manipulation on it +- $clonedHtmlContentTypeNode->nodeValue = '%s'; +- +- // remove potentially harmful attributes on html content type node itself +- while ($htmlContentTypeNode->attributes->length) { +- $htmlContentTypeNode->removeAttribute($htmlContentTypeNode->attributes->item(0)->name); +- } +- +- // decode outerHTML safely +- $preDecodedOuterHtml = $document->saveHTML($htmlContentTypeNode); +- +- // clear empty
wrapper around outerHTML to replace with $clonedHtmlContentTypeNode +- $decodedInnerHtml = preg_replace('#^<[^>]*>|]*>$#', '', html_entity_decode($preDecodedOuterHtml)); +- +- // Use $clonedHtmlContentTypeNode's placeholder to inject decoded inner html +- $decodedOuterHtml = sprintf($document->saveHTML($clonedHtmlContentTypeNode), $decodedInnerHtml); +- +- // generate unique node name element to replace with decoded html contents at end of processing; +- // goal is to create a document as few times as possible to prevent inadvertent parsing of contents as html +- // by the dom library +- $uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS); +- +- $uniqueNode = new \DOMElement($uniqueNodeName); +- $htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode); +- +- $uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml; +- } +- +- return $uniqueNodeNameToDecodedOuterHtmlMap; +- } +- +- /** +- * Generate the CSS for any background images on the page +- * +- * @param \DOMDocument $document +- */ +- private function generateBackgroundImageStyles(\DOMDocument $document) : void +- { +- $xpath = new \DOMXPath($document); +- $nodes = $xpath->query('//*[@data-background-images]'); +- foreach ($nodes as $node) { +- /* @var \DOMElement $node */ +- $backgroundImages = $node->attributes->getNamedItem('data-background-images'); +- if ($backgroundImages->nodeValue !== '') { +- $elementClass = uniqid('background-image-'); +- $images = json_decode(stripslashes($backgroundImages->nodeValue), true); +- if (count($images) > 0) { +- $style = $xpath->document->createElement( +- 'style', +- $this->generateCssFromImages($elementClass, $images) +- ); +- $style->setAttribute('type', 'text/css'); +- $node->parentNode->appendChild($style); +- +- // Append our new class to the DOM element +- $classes = ''; +- if ($node->attributes->getNamedItem('class')) { +- $classes = $node->attributes->getNamedItem('class')->nodeValue . ' '; +- } +- $node->setAttribute('class', $classes . $elementClass); +- } +- } +- } +- } +- +- /** +- * Generate CSS based on the images array from our attribute +- * +- * @param string $elementClass +- * @param array $images +- * +- * @return string +- */ +- private function generateCssFromImages(string $elementClass, array $images) : string +- { +- $css = []; +- if (isset($images['desktop_image'])) { +- $css['.' . $elementClass] = [ +- 'background-image' => 'url('https://github.com/.%20$images['desktop_image']%20.')', +- ]; +- } +- if (isset($images['mobile_image']) && $this->getMobileMediaQuery()) { +- $css[$this->getMobileMediaQuery()]['.' . $elementClass] = [ +- 'background-image' => 'url('https://github.com/.%20$images['mobile_image']%20.')', +- ]; +- } +- return $this->cssFromArray($css); +- } +- +- /** +- * Generate a CSS string from an array +- * +- * @param array $css +- * +- * @return string +- */ +- private function cssFromArray(array $css) : string +- { +- $output = ''; +- foreach ($css as $selector => $body) { +- if (is_array($body)) { +- $output .= $selector . ' {'; +- $output .= $this->cssFromArray($body); +- $output .= '}'; +- } else { +- $output .= $selector . ': ' . $body . ';'; +- } +- } +- return $output; +- } +- +- /** +- * Generate the mobile media query from view configuration +- * +- * @return null|string +- */ +- private function getMobileMediaQuery() : ?string +- { +- $breakpoints = $this->viewConfig->getViewConfig()->getVarValue( +- 'Magento_PageBuilder', +- 'breakpoints/mobile/conditions' +- ); +- if ($breakpoints && count($breakpoints) > 0) { +- $mobileBreakpoint = '@media only screen '; +- foreach ($breakpoints as $key => $value) { +- $mobileBreakpoint .= 'and (' . $key . ': ' . $value . ') '; +- } +- return rtrim($mobileBreakpoint); +- } +- return null; ++ return $this->templateFilter->filter($result); + } + } +diff -Nuar a/vendor/magento/module-page-builder/etc/adminhtml/di.xml b/vendor/magento/module-page-builder/etc/adminhtml/di.xml +--- a/vendor/magento/module-page-builder/etc/adminhtml/di.xml ++++ b/vendor/magento/module-page-builder/etc/adminhtml/di.xml +@@ -6,6 +6,9 @@ + */ + --> + ++ ++ ++ + + + ns = pagebuilder_modal_form, index = modal +diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml +--- a/vendor/magento/module-page-builder/etc/di.xml ++++ b/vendor/magento/module-page-builder/etc/di.xml +@@ -131,6 +131,7 @@ + + pageBuilderProductCollectionFactory + ++ + + + +diff -Nuar a/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml b/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml +@@ -0,0 +1,14 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ diff --git a/patches/PB-320__fix_pagebuilder_module__2.3.2.patch b/patches/PB-320__fix_pagebuilder_module__2.3.2.patch new file mode 100644 index 00000000..d8adfb19 --- /dev/null +++ b/patches/PB-320__fix_pagebuilder_module__2.3.2.patch @@ -0,0 +1,1341 @@ +diff -Nuar a/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php b/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php +@@ -0,0 +1,71 @@ ++rendererPool = $rendererPool; ++ $this->preview = $preview; ++ } ++ ++ /** ++ * Generates an HTML preview for the stage ++ * ++ * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|mixed ++ * @throws \Exception ++ */ ++ public function execute() ++ { ++ return $this->preview->startPreviewMode( ++ function () { ++ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); ++ // Some template filters and directive processors expect this to be called in order to function. ++ $pageResult->initLayout(); ++ ++ $params = $this->getRequest()->getParams(); ++ $renderer = $this->rendererPool->getRenderer($params['role']); ++ $result = ['data' => $renderer->render($params)]; ++ ++ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ } ++ ); ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +--- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php ++++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +@@ -17,6 +17,8 @@ use Magento\Framework\App\Action\HttpPostActionInterface; + * This isn't placed within the adminhtml folder as it has to extend from the front-end controllers app action to + * ensure the content is rendered in the storefront scope. + * ++ * @deprecated use \Magento\PageBuilder\Controller\Adminhtml\Stage\Preview ++ * + * @api + */ + class Preview extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface +@@ -26,19 +28,28 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc + */ + private $rendererPool; + ++ /** ++ * @var \Magento\Backend\Model\Auth ++ */ ++ private $auth; ++ + /** + * Constructor + * + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool ++ * @param \Magento\Backend\Model\Auth $auth + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, +- \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool ++ \Magento\PageBuilder\Model\Stage\RendererPool $rendererPool, ++ \Magento\Backend\Model\Auth $auth = null + ) { + parent::__construct($context); + + $this->rendererPool = $rendererPool; ++ $this->auth = $auth ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\Backend\Model\Auth::class); + } + + /** +@@ -48,14 +59,18 @@ class Preview extends \Magento\Framework\App\Action\Action implements HttpPostAc + */ + public function execute() + { +- $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); +- // Some template filters and directive processors expect this to be called in order to function. +- $pageResult->initLayout(); ++ if ($this->auth->isLoggedIn()) { ++ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); ++ // Some template filters and directive processors expect this to be called in order to function. ++ $pageResult->initLayout(); ++ ++ $params = $this->getRequest()->getParams(); ++ $renderer = $this->rendererPool->getRenderer($params['role']); ++ $result = ['data' => $renderer->render($params)]; + +- $params = $this->getRequest()->getParams(); +- $renderer = $this->rendererPool->getRenderer($params['role']); +- $result = ['data' => $renderer->render($params)]; ++ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ } + +- return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ $this->_forward('noroute'); + } + } +diff -Nuar a/vendor/magento/module-page-builder/Model/Filter/Template.php b/vendor/magento/module-page-builder/Model/Filter/Template.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Model/Filter/Template.php +@@ -0,0 +1,334 @@ ++logger = $logger; ++ $this->viewConfig = $viewConfig; ++ $this->mathRandom = $mathRandom; ++ $this->json = $json; ++ } ++ ++ /** ++ * After filter of template data apply transformations ++ * ++ * @param string $result ++ * ++ * @return string ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function filter(string $result) : string ++ { ++ $this->domDocument = false; ++ ++ // Validate if the filtered result requires background image processing ++ if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::BACKGROUND_IMAGE_PATTERN, $result)) { ++ $document = $this->getDomDocument($result); ++ $this->generateBackgroundImageStyles($document); ++ } ++ ++ // Process any HTML content types, they need to be decoded on the front-end ++ if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::HTML_CONTENT_TYPE_PATTERN, $result)) { ++ $document = $this->getDomDocument($result); ++ $uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document); ++ } ++ ++ // If a document was retrieved we've modified the output so need to retrieve it from within the document ++ if (isset($document)) { ++ // Match the contents of the body from our generated document ++ preg_match( ++ '/(.+)<\/body><\/html>$/si', ++ $document->saveHTML(), ++ $matches ++ ); ++ ++ if (!empty($matches)) { ++ $docHtml = $matches[1]; ++ ++ // restore any encoded directives ++ $docHtml = preg_replace_callback( ++ '/=\"(%7B%7B[^"]*%7D%7D)\"/m', ++ function ($matches) { ++ return urldecode($matches[0]); ++ }, ++ $docHtml ++ ); ++ ++ if (isset($uniqueNodeNameToDecodedOuterHtmlMap)) { ++ foreach ($uniqueNodeNameToDecodedOuterHtmlMap as $uniqueNodeName => $decodedOuterHtml) { ++ $docHtml = str_replace( ++ '<' . $uniqueNodeName . '>' . '', ++ $decodedOuterHtml, ++ $docHtml ++ ); ++ } ++ } ++ ++ $result = $docHtml; ++ } ++ } ++ ++ return $result; ++ } ++ ++ /** ++ * Create a DOM document from a given string ++ * ++ * @param string $html ++ * ++ * @return \DOMDocument ++ */ ++ private function getDomDocument(string $html) : \DOMDocument ++ { ++ if (!$this->domDocument) { ++ $this->domDocument = $this->createDomDocument($html); ++ } ++ ++ return $this->domDocument; ++ } ++ ++ /** ++ * Create a DOMDocument from a string ++ * ++ * @param string $html ++ * ++ * @return \DOMDocument ++ */ ++ private function createDomDocument(string $html) : \DOMDocument ++ { ++ $domDocument = new \DOMDocument('1.0', 'UTF-8'); ++ set_error_handler( ++ function ($errorNumber, $errorString) { ++ throw new \DOMException($errorString, $errorNumber); ++ } ++ ); ++ $string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); ++ try { ++ libxml_use_internal_errors(true); ++ $domDocument->loadHTML( ++ '' . $string . '' ++ ); ++ libxml_clear_errors(); ++ } catch (\Exception $e) { ++ restore_error_handler(); ++ $this->logger->critical($e); ++ } ++ restore_error_handler(); ++ ++ return $domDocument; ++ } ++ ++ /** ++ * Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement ++ * ++ * @param \DOMDocument $document ++ * @return array ++ * @throws \Magento\Framework\Exception\LocalizedException ++ */ ++ private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array ++ { ++ $xpath = new \DOMXPath($document); ++ ++ // construct xpath query to fetch top-level ancestor html content type nodes ++ /** @var $htmlContentTypeNodes \DOMNode[] */ ++ $htmlContentTypeNodes = $xpath->query( ++ '//*[@data-content-type="html" and not(@data-decoded="true")]' . ++ '[not(ancestor::*[@data-content-type="html"])]' ++ ); ++ ++ $uniqueNodeNameToDecodedOuterHtmlMap = []; ++ ++ foreach ($htmlContentTypeNodes as $htmlContentTypeNode) { ++ // Set decoded attribute on all encoded html content types so we don't double decode; ++ $htmlContentTypeNode->setAttribute('data-decoded', 'true'); ++ ++ // if nothing exists inside the node, continue ++ if (!strlen(trim($htmlContentTypeNode->nodeValue))) { ++ continue; ++ } ++ ++ // clone html code content type to save reference to its attributes/outerHTML, which we are not going to ++ // decode ++ $clonedHtmlContentTypeNode = clone $htmlContentTypeNode; ++ ++ // clear inner contents of cloned node for replacement later with $decodedInnerHtml using sprintf; ++ // we want to retain html content type node and avoid doing any manipulation on it ++ $clonedHtmlContentTypeNode->nodeValue = '%s'; ++ ++ // remove potentially harmful attributes on html content type node itself ++ while ($htmlContentTypeNode->attributes->length) { ++ $htmlContentTypeNode->removeAttribute($htmlContentTypeNode->attributes->item(0)->name); ++ } ++ ++ // decode outerHTML safely ++ $preDecodedOuterHtml = $document->saveHTML($htmlContentTypeNode); ++ ++ // clear empty
wrapper around outerHTML to replace with $clonedHtmlContentTypeNode ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $decodedInnerHtml = preg_replace('#^<[^>]*>|]*>$#', '', html_entity_decode($preDecodedOuterHtml)); ++ ++ // Use $clonedHtmlContentTypeNode's placeholder to inject decoded inner html ++ $decodedOuterHtml = sprintf($document->saveHTML($clonedHtmlContentTypeNode), $decodedInnerHtml); ++ ++ // generate unique node name element to replace with decoded html contents at end of processing; ++ // goal is to create a document as few times as possible to prevent inadvertent parsing of contents as html ++ // by the dom library ++ $uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS); ++ ++ $uniqueNode = new \DOMElement($uniqueNodeName); ++ $htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode); ++ ++ $uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml; ++ } ++ ++ return $uniqueNodeNameToDecodedOuterHtmlMap; ++ } ++ ++ /** ++ * Generate the CSS for any background images on the page ++ * ++ * @param \DOMDocument $document ++ */ ++ private function generateBackgroundImageStyles(\DOMDocument $document) : void ++ { ++ $xpath = new \DOMXPath($document); ++ $nodes = $xpath->query('//*[@data-background-images]'); ++ foreach ($nodes as $node) { ++ /* @var \DOMElement $node */ ++ $backgroundImages = $node->attributes->getNamedItem('data-background-images'); ++ if ($backgroundImages->nodeValue !== '') { ++ $elementClass = uniqid('background-image-'); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $images = $this->json->unserialize(stripslashes($backgroundImages->nodeValue)); ++ if (count($images) > 0) { ++ $style = $xpath->document->createElement( ++ 'style', ++ $this->generateCssFromImages($elementClass, $images) ++ ); ++ $style->setAttribute('type', 'text/css'); ++ $node->parentNode->appendChild($style); ++ ++ // Append our new class to the DOM element ++ $classes = ''; ++ if ($node->attributes->getNamedItem('class')) { ++ $classes = $node->attributes->getNamedItem('class')->nodeValue . ' '; ++ } ++ $node->setAttribute('class', $classes . $elementClass); ++ } ++ } ++ } ++ } ++ ++ /** ++ * Generate CSS based on the images array from our attribute ++ * ++ * @param string $elementClass ++ * @param array $images ++ * ++ * @return string ++ */ ++ private function generateCssFromImages(string $elementClass, array $images) : string ++ { ++ $css = []; ++ if (isset($images['desktop_image'])) { ++ $css['.' . $elementClass] = [ ++ 'background-image' => 'url('https://github.com/.%20$images['desktop_image']%20.')', ++ ]; ++ } ++ if (isset($images['mobile_image']) && $this->getMobileMediaQuery()) { ++ $css[$this->getMobileMediaQuery()]['.' . $elementClass] = [ ++ 'background-image' => 'url('https://github.com/.%20$images['mobile_image']%20.')', ++ ]; ++ } ++ return $this->cssFromArray($css); ++ } ++ ++ /** ++ * Generate a CSS string from an array ++ * ++ * @param array $css ++ * ++ * @return string ++ */ ++ private function cssFromArray(array $css) : string ++ { ++ $output = ''; ++ foreach ($css as $selector => $body) { ++ if (is_array($body)) { ++ $output .= $selector . ' {'; ++ $output .= $this->cssFromArray($body); ++ $output .= '}'; ++ } else { ++ $output .= $selector . ': ' . $body . ';'; ++ } ++ } ++ return $output; ++ } ++ ++ /** ++ * Generate the mobile media query from view configuration ++ * ++ * @return null|string ++ */ ++ private function getMobileMediaQuery() : ?string ++ { ++ $breakpoints = $this->viewConfig->getViewConfig()->getVarValue( ++ 'Magento_PageBuilder', ++ 'breakpoints/mobile/conditions' ++ ); ++ if ($breakpoints && count($breakpoints) > 0) { ++ $mobileBreakpoint = '@media only screen '; ++ foreach ($breakpoints as $key => $value) { ++ $mobileBreakpoint .= 'and (' . $key . ': ' . $value . ') '; ++ } ++ return rtrim($mobileBreakpoint); ++ } ++ return null; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php +--- a/vendor/magento/module-page-builder/Model/Stage/Config.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Config.php +@@ -135,7 +135,7 @@ class Config + 'content_types' => $this->getContentTypes(), + 'stage_config' => $this->data, + 'media_url' => $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]), +- 'preview_url' => $this->frontendUrlBuilder->getUrl('pagebuilder/contenttype/preview'), ++ 'preview_url' => $this->urlBuilder->getUrl('pagebuilder/stage/preview'), + 'column_grid_default' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_DEFAULT), + 'column_grid_max' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_MAX), + 'can_use_inline_editing_on_stage' => $this->isWysiwygProvisionedForEditingOnStage(), +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Preview.php b/vendor/magento/module-page-builder/Model/Stage/Preview.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Model/Stage/Preview.php +@@ -0,0 +1,134 @@ ++emulation = $emulation; ++ $this->appState = $appState; ++ $this->design = $design; ++ $this->themeProvider = $themeProvider; ++ $this->storeManager = $storeManager; ++ $this->scopeConfig = $scopeConfig; ++ } ++ ++ /** ++ * @var bool ++ */ ++ private $isPreview; ++ ++ /** ++ * Retrieve the area in which the preview needs to be ran in ++ * ++ * @return string ++ */ ++ public function getPreviewArea() : string ++ { ++ return \Magento\Framework\App\Area::AREA_FRONTEND; ++ } ++ ++ /** ++ * Start Page Builder preview mode and emulate store front ++ * ++ * @param callable $callback ++ * @param int $storeId ++ * @return mixed ++ * @throws \Exception ++ */ ++ public function startPreviewMode($callback, $storeId = null) ++ { ++ $this->isPreview = true; ++ ++ if (!$storeId) { ++ $storeId = $this->storeManager->getDefaultStoreView()->getId(); ++ } ++ $this->emulation->startEnvironmentEmulation($storeId); ++ ++ return $this->appState->emulateAreaCode( ++ $this->getPreviewArea(), ++ function () use ($callback) { ++ $themeId = $this->scopeConfig->getValue( ++ 'design/theme/theme_id', ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ $theme = $this->themeProvider->getThemeById($themeId); ++ $this->design->setDesignTheme($theme, $this->getPreviewArea()); ++ ++ try { ++ $result = $callback(); ++ } catch (\Exception $e) { ++ $this->isPreview = false; ++ throw $e; ++ } ++ ++ $this->emulation->stopEnvironmentEmulation(); ++ return $result; ++ } ++ ); ++ } ++ ++ /** ++ * Determine if the system is in preview mode ++ * ++ * @return bool ++ */ ++ public function isPreviewMode() : bool ++ { ++ return $this->isPreview; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Magento\Framework\Controller\ResultFactory; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a block for the stage +@@ -31,20 +32,27 @@ class Block implements \Magento\PageBuilder\Model\Stage\RendererInterface + private $resultFactory; + + /** +- * Constructor +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\PageBuilder\Model\Config $config + * @param \Magento\Framework\View\Element\BlockFactory $blockFactory + * @param ResultFactory $resultFactory ++ * @param Template|null $templateFilter + */ + public function __construct( + \Magento\PageBuilder\Model\Config $config, + \Magento\Framework\View\Element\BlockFactory $blockFactory, +- ResultFactory $resultFactory ++ ResultFactory $resultFactory, ++ Template $templateFilter = null + ) { + $this->config = $config; + $this->blockFactory = $blockFactory; + $this->resultFactory = $resultFactory; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -77,7 +85,7 @@ class Block implements \Magento\PageBuilder\Model\Stage\RendererInterface + $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $pageResult->getLayout()->addBlock($backendBlockInstance); + +- $result['content'] = $backendBlockInstance->toHtml(); ++ $result['content'] = $this->templateFilter->filter($backendBlockInstance->toHtml()); + } + + return $result; +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php +@@ -9,6 +9,8 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Psr\Log\LoggerInterface; ++use Magento\PageBuilder\Model\Stage\HtmlFilter; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a CMS Block for the stage +@@ -33,28 +35,35 @@ class CmsStaticBlock implements \Magento\PageBuilder\Model\Stage\RendererInterfa + private $loggerInterface; + + /** +- * @var \Magento\PageBuilder\Model\Stage\HtmlFilter ++ * @var HtmlFilter + */ + private $htmlFilter; + + /** +- * CmsStaticBlock constructor. +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\Cms\Model\ResourceModel\Block\CollectionFactory $blockCollectionFactory + * @param WidgetDirective $widgetDirectiveRenderer + * @param LoggerInterface $loggerInterface + * @param \Magento\PageBuilder\Model\Stage\HtmlFilter $htmlFilter ++ * @param \Magento\PageBuilder\Model\Filter\Template|null $templateFilter + */ + public function __construct( + \Magento\Cms\Model\ResourceModel\Block\CollectionFactory $blockCollectionFactory, + WidgetDirective $widgetDirectiveRenderer, + LoggerInterface $loggerInterface, +- \Magento\PageBuilder\Model\Stage\HtmlFilter $htmlFilter ++ HtmlFilter $htmlFilter, ++ Template $templateFilter = null + ) { + $this->blockCollectionFactory = $blockCollectionFactory; + $this->widgetDirectiveRenderer = $widgetDirectiveRenderer; + $this->loggerInterface = $loggerInterface; + $this->htmlFilter = $htmlFilter; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -96,7 +105,9 @@ class CmsStaticBlock implements \Magento\PageBuilder\Model\Stage\RendererInterfa + + if ($block->isActive()) { + $directiveResult = $this->widgetDirectiveRenderer->render($params); +- $result['content'] = $this->htmlFilter->filterHtml($directiveResult['content']); ++ $result['content'] = $this->htmlFilter->filterHtml( ++ $this->templateFilter->filter($directiveResult['content']) ++ ); + } else { + $result['error'] = __('Block disabled'); + } +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Magento\Store\Model\Store; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a widget directive for the stage +@@ -28,17 +29,24 @@ class WidgetDirective implements \Magento\PageBuilder\Model\Stage\RendererInterf + private $directiveFilter; + + /** +- * Constructor +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Widget\Model\Template\Filter $directiveFilter ++ * @param Template $templateFilter + */ + public function __construct( + \Magento\Store\Model\StoreManagerInterface $storeManager, +- \Magento\Widget\Model\Template\Filter $directiveFilter ++ \Magento\Widget\Model\Template\Filter $directiveFilter, ++ Template $templateFilter = null + ) { + $this->storeManager = $storeManager; + $this->directiveFilter = $directiveFilter; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -61,7 +69,7 @@ class WidgetDirective implements \Magento\PageBuilder\Model\Stage\RendererInterf + try { + $result['content'] = $this->directiveFilter + ->setStoreId(Store::DEFAULT_STORE_ID) +- ->filter($params['directive']); ++ ->filter($this->templateFilter->filter($params['directive'])); + } catch (\Exception $e) { + $result['error'] = __($e->getMessage()); + } +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php b/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php +@@ -0,0 +1,48 @@ ++stock = $stock; ++ } ++ ++ /** ++ * Allow to sort product collection ++ * ++ * @param ProductsList $subject ++ * @param Collection $result ++ * @return Collection ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function afterCreateCollection( ++ ProductsList $subject, ++ Collection $result ++ ) { ++ $this->stock->addIsInStockFilterToCollection($result); ++ return $result; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Plugin/DesignLoader.php b/vendor/magento/module-page-builder/Plugin/DesignLoader.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/DesignLoader.php +@@ -0,0 +1,98 @@ ++designLoader = $designLoader; ++ $this->messageManager = $messageManager; ++ $this->appState = $appState; ++ $this->preview = $preview; ++ } ++ ++ /** ++ * Before create load the design files ++ * ++ * @param \Magento\Catalog\Block\Product\ImageFactory $subject ++ * @param Product $product ++ * @param string $imageId ++ * @param array|null $attributes ++ * @throws \Exception ++ * ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforeCreate( ++ \Magento\Catalog\Block\Product\ImageFactory $subject, ++ Product $product, ++ string $imageId, ++ array $attributes = null ++ ) { ++ if ($this->preview->isPreviewMode()) { ++ $this->appState->emulateAreaCode( ++ $this->preview->getPreviewArea(), ++ [$this, 'loadDesignConfig'] ++ ); ++ } ++ } ++ ++ /** ++ * Load the design config ++ */ ++ public function loadDesignConfig() ++ { ++ try { ++ $this->designLoader->load(); ++ } catch (\Magento\Framework\Exception\LocalizedException $e) { ++ if ($e->getPrevious() instanceof \Magento\Framework\Config\Dom\ValidationException) { ++ /** @var MessageInterface $message */ ++ $message = $this->messageManager ++ ->createMessage(MessageInterface::TYPE_ERROR) ++ ->setText($e->getMessage()); ++ $this->messageManager->addUniqueMessages([$message]); ++ } ++ } ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php b/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php +--- a/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php ++++ b/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php +@@ -7,50 +7,27 @@ declare(strict_types=1); + + namespace Magento\PageBuilder\Plugin\Filter; + +-use Magento\Store\Model\Store; +- + /** + * Plugin to the template filter to process any background images added by Page Builder + */ + class TemplatePlugin + { +- const BACKGROUND_IMAGE_PATTERN = '/data-background-images/si'; ++ const BACKGROUND_IMAGE_PATTERN = '/data-background-images=(?:\'|"){.+}(?:\'|")/si'; + + const HTML_CONTENT_TYPE_PATTERN = '/data-content-type="html"/si'; + + /** +- * @var \Magento\Framework\View\ConfigInterface +- */ +- private $viewConfig; +- +- /** +- * @var \Psr\Log\LoggerInterface +- */ +- private $logger; +- +- /** +- * @var \DOMDocument +- */ +- private $domDocument; +- +- /** +- * @var \Magento\Framework\Math\Random ++ * @var \Magento\PageBuilder\Model\Filter\Template + */ +- private $mathRandom; ++ private $templateFilter; + + /** +- * @param \Psr\Log\LoggerInterface $logger +- * @param \Magento\Framework\View\ConfigInterface $viewConfig +- * @param \Magento\Framework\Math\Random $mathRandom ++ * @param \Magento\PageBuilder\Model\Filter\Template $templateFilter + */ + public function __construct( +- \Psr\Log\LoggerInterface $logger, +- \Magento\Framework\View\ConfigInterface $viewConfig, +- \Magento\Framework\Math\Random $mathRandom ++ \Magento\PageBuilder\Model\Filter\Template $templateFilter + ) { +- $this->logger = $logger; +- $this->viewConfig = $viewConfig; +- $this->mathRandom = $mathRandom; ++ $this->templateFilter = $templateFilter; + } + + /** +@@ -64,284 +41,6 @@ class TemplatePlugin + */ + public function afterFilter(\Magento\Framework\Filter\Template $subject, string $result) : string + { +- $this->domDocument = false; +- +- // Validate if the filtered result requires background image processing +- if (preg_match(self::BACKGROUND_IMAGE_PATTERN, $result)) { +- $document = $this->getDomDocument($result); +- $this->generateBackgroundImageStyles($document); +- } +- +- // Process any HTML content types, they need to be decoded on the front-end +- if (preg_match(self::HTML_CONTENT_TYPE_PATTERN, $result)) { +- $document = $this->getDomDocument($result); +- $uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document); +- } +- +- // If a document was retrieved we've modified the output so need to retrieve it from within the document +- if (isset($document)) { +- // Match the contents of the body from our generated document +- preg_match( +- '/(.+)<\/body><\/html>$/si', +- $document->saveHTML(), +- $matches +- ); +- +- if (!empty($matches)) { +- $docHtml = $matches[1]; +- +- if (isset($uniqueNodeNameToDecodedOuterHtmlMap)) { +- foreach ($uniqueNodeNameToDecodedOuterHtmlMap as $uniqueNodeName => $decodedOuterHtml) { +- $docHtml = str_replace( +- '<' . $uniqueNodeName . '>' . '', +- $decodedOuterHtml, +- $docHtml +- ); +- } +- } +- +- $result = $docHtml; +- } +- } +- +- return $result; +- } +- +- /** +- * Determine if custom variable directive's return value needs to be escaped and do so if true +- * +- * @param \Magento\Framework\Filter\Template $subject +- * @param \Closure $proceed +- * @param string[] $construction +- * @return string +- */ +- public function aroundCustomvarDirective( +- \Magento\Framework\Filter\Template $subject, +- \Closure $proceed, +- $construction +- ) { +- // Determine the need to escape the return value of observed method. +- // Admin context requires store ID of 0; in that context return value should be escaped +- $shouldEscape = $subject->getStoreId() !== null && (int) $subject->getStoreId() === Store::DEFAULT_STORE_ID; +- +- if (!$shouldEscape) { +- return $proceed($construction); +- } +- +- $result = $proceed($construction); +- +- return htmlspecialchars($result); +- } +- +- /** +- * Create a DOM document from a given string +- * +- * @param string $html +- * +- * @return \DOMDocument +- */ +- private function getDomDocument(string $html) : \DOMDocument +- { +- if (!$this->domDocument) { +- $this->domDocument = $this->createDomDocument($html); +- } +- +- return $this->domDocument; +- } +- +- /** +- * Create a DOMDocument from a string +- * +- * @param string $html +- * +- * @return \DOMDocument +- */ +- private function createDomDocument(string $html) : \DOMDocument +- { +- $domDocument = new \DOMDocument('1.0', 'UTF-8'); +- set_error_handler( +- function ($errorNumber, $errorString) { +- throw new \Exception($errorString, $errorNumber); +- } +- ); +- $string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); +- try { +- libxml_use_internal_errors(true); +- $domDocument->loadHTML( +- '' . $string . '' +- ); +- libxml_clear_errors(); +- } catch (\Exception $e) { +- restore_error_handler(); +- $this->logger->critical($e); +- } +- restore_error_handler(); +- +- return $domDocument; +- } +- +- /** +- * Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement +- * +- * @param \DOMDocument $document +- * @return array - map of unique node name to decoded html +- */ +- private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array +- { +- $xpath = new \DOMXPath($document); +- +- // construct xpath query to fetch top-level ancestor html content type nodes +- /** @var $htmlContentTypeNodes \DOMNode[] */ +- $htmlContentTypeNodes = $xpath->query( +- '//*[@data-content-type="html" and not(@data-decoded="true")]' . +- '[not(ancestor::*[@data-content-type="html"])]' +- ); +- +- $uniqueNodeNameToDecodedOuterHtmlMap = []; +- +- foreach ($htmlContentTypeNodes as $htmlContentTypeNode) { +- // Set decoded attribute on all encoded html content types so we don't double decode; +- $htmlContentTypeNode->setAttribute('data-decoded', 'true'); +- +- // if nothing exists inside the node, continue +- if (!strlen(trim($htmlContentTypeNode->nodeValue))) { +- continue; +- } +- +- // clone html code content type to save reference to its attributes/outerHTML, which we are not going to +- // decode +- $clonedHtmlContentTypeNode = clone $htmlContentTypeNode; +- +- // clear inner contents of cloned node for replacement later with $decodedInnerHtml using sprintf; +- // we want to retain html content type node and avoid doing any manipulation on it +- $clonedHtmlContentTypeNode->nodeValue = '%s'; +- +- // remove potentially harmful attributes on html content type node itself +- while ($htmlContentTypeNode->attributes->length) { +- $htmlContentTypeNode->removeAttribute($htmlContentTypeNode->attributes->item(0)->name); +- } +- +- // decode outerHTML safely +- $preDecodedOuterHtml = $document->saveHTML($htmlContentTypeNode); +- +- // clear empty
wrapper around outerHTML to replace with $clonedHtmlContentTypeNode +- $decodedInnerHtml = preg_replace('#^<[^>]*>|]*>$#', '', html_entity_decode($preDecodedOuterHtml)); +- +- // Use $clonedHtmlContentTypeNode's placeholder to inject decoded inner html +- $decodedOuterHtml = sprintf($document->saveHTML($clonedHtmlContentTypeNode), $decodedInnerHtml); +- +- // generate unique node name element to replace with decoded html contents at end of processing; +- // goal is to create a document as few times as possible to prevent inadvertent parsing of contents as html +- // by the dom library +- $uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS); +- +- $uniqueNode = new \DOMElement($uniqueNodeName); +- $htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode); +- +- $uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml; +- } +- +- return $uniqueNodeNameToDecodedOuterHtmlMap; +- } +- +- /** +- * Generate the CSS for any background images on the page +- * +- * @param \DOMDocument $document +- */ +- private function generateBackgroundImageStyles(\DOMDocument $document) : void +- { +- $xpath = new \DOMXPath($document); +- $nodes = $xpath->query('//*[@data-background-images]'); +- foreach ($nodes as $node) { +- /* @var \DOMElement $node */ +- $backgroundImages = $node->attributes->getNamedItem('data-background-images'); +- if ($backgroundImages->nodeValue !== '') { +- $elementClass = uniqid('background-image-'); +- $images = json_decode(stripslashes($backgroundImages->nodeValue), true); +- if (count($images) > 0) { +- $style = $xpath->document->createElement( +- 'style', +- $this->generateCssFromImages($elementClass, $images) +- ); +- $style->setAttribute('type', 'text/css'); +- $node->parentNode->appendChild($style); +- +- // Append our new class to the DOM element +- $classes = ''; +- if ($node->attributes->getNamedItem('class')) { +- $classes = $node->attributes->getNamedItem('class')->nodeValue . ' '; +- } +- $node->setAttribute('class', $classes . $elementClass); +- } +- } +- } +- } +- +- /** +- * Generate CSS based on the images array from our attribute +- * +- * @param string $elementClass +- * @param array $images +- * +- * @return string +- */ +- private function generateCssFromImages(string $elementClass, array $images) : string +- { +- $css = []; +- if (isset($images['desktop_image'])) { +- $css['.' . $elementClass] = [ +- 'background-image' => 'url('https://github.com/.%20$images['desktop_image']%20.')', +- ]; +- } +- if (isset($images['mobile_image']) && $this->getMobileMediaQuery()) { +- $css[$this->getMobileMediaQuery()]['.' . $elementClass] = [ +- 'background-image' => 'url('https://github.com/.%20$images['mobile_image']%20.')', +- ]; +- } +- return $this->cssFromArray($css); +- } +- +- /** +- * Generate a CSS string from an array +- * +- * @param array $css +- * +- * @return string +- */ +- private function cssFromArray(array $css) : string +- { +- $output = ''; +- foreach ($css as $selector => $body) { +- if (is_array($body)) { +- $output .= $selector . ' {'; +- $output .= $this->cssFromArray($body); +- $output .= '}'; +- } else { +- $output .= $selector . ': ' . $body . ';'; +- } +- } +- return $output; +- } +- +- /** +- * Generate the mobile media query from view configuration +- * +- * @return null|string +- */ +- private function getMobileMediaQuery() : ?string +- { +- $breakpoints = $this->viewConfig->getViewConfig()->getVarValue( +- 'Magento_PageBuilder', +- 'breakpoints/mobile/conditions' +- ); +- if ($breakpoints && count($breakpoints) > 0) { +- $mobileBreakpoint = '@media only screen '; +- foreach ($breakpoints as $key => $value) { +- $mobileBreakpoint .= 'and (' . $key . ': ' . $value . ') '; +- } +- return rtrim($mobileBreakpoint); +- } +- return null; ++ return $this->templateFilter->filter($result); + } + } +diff -Nuar a/vendor/magento/module-page-builder/etc/adminhtml/di.xml b/vendor/magento/module-page-builder/etc/adminhtml/di.xml +--- a/vendor/magento/module-page-builder/etc/adminhtml/di.xml ++++ b/vendor/magento/module-page-builder/etc/adminhtml/di.xml +@@ -6,6 +6,9 @@ + */ + --> + ++ ++ ++ + + + ns = pagebuilder_modal_form, index = modal +diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml +--- a/vendor/magento/module-page-builder/etc/di.xml ++++ b/vendor/magento/module-page-builder/etc/di.xml +@@ -131,6 +131,7 @@ + + pageBuilderProductCollectionFactory + ++ + + + +diff -Nuar a/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml b/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml +@@ -0,0 +1,14 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ diff --git a/patches/PB-322__fix_pagebuilder_module__2.3.2-p1.patch b/patches/PB-322__fix_pagebuilder_module__2.3.2-p1.patch new file mode 100644 index 00000000..b806b01b --- /dev/null +++ b/patches/PB-322__fix_pagebuilder_module__2.3.2-p1.patch @@ -0,0 +1,1271 @@ +diff -Nuar a/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php b/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php +@@ -0,0 +1,71 @@ ++rendererPool = $rendererPool; ++ $this->preview = $preview; ++ } ++ ++ /** ++ * Generates an HTML preview for the stage ++ * ++ * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|mixed ++ * @throws \Exception ++ */ ++ public function execute() ++ { ++ return $this->preview->startPreviewMode( ++ function () { ++ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); ++ // Some template filters and directive processors expect this to be called in order to function. ++ $pageResult->initLayout(); ++ ++ $params = $this->getRequest()->getParams(); ++ $renderer = $this->rendererPool->getRenderer($params['role']); ++ $result = ['data' => $renderer->render($params)]; ++ ++ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ } ++ ); ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +--- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php ++++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +@@ -17,6 +17,8 @@ use Magento\Framework\App\Action\HttpPostActionInterface; + * This isn't placed within the adminhtml folder as it has to extend from the front-end controllers app action to + * ensure the content is rendered in the storefront scope. + * ++ * @deprecated use \Magento\PageBuilder\Controller\Adminhtml\Stage\Preview ++ * + * @api + */ + class Preview extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface +diff -Nuar a/vendor/magento/module-page-builder/Model/Filter/Template.php b/vendor/magento/module-page-builder/Model/Filter/Template.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Model/Filter/Template.php +@@ -0,0 +1,334 @@ ++logger = $logger; ++ $this->viewConfig = $viewConfig; ++ $this->mathRandom = $mathRandom; ++ $this->json = $json; ++ } ++ ++ /** ++ * After filter of template data apply transformations ++ * ++ * @param string $result ++ * ++ * @return string ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function filter(string $result) : string ++ { ++ $this->domDocument = false; ++ ++ // Validate if the filtered result requires background image processing ++ if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::BACKGROUND_IMAGE_PATTERN, $result)) { ++ $document = $this->getDomDocument($result); ++ $this->generateBackgroundImageStyles($document); ++ } ++ ++ // Process any HTML content types, they need to be decoded on the front-end ++ if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::HTML_CONTENT_TYPE_PATTERN, $result)) { ++ $document = $this->getDomDocument($result); ++ $uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document); ++ } ++ ++ // If a document was retrieved we've modified the output so need to retrieve it from within the document ++ if (isset($document)) { ++ // Match the contents of the body from our generated document ++ preg_match( ++ '/(.+)<\/body><\/html>$/si', ++ $document->saveHTML(), ++ $matches ++ ); ++ ++ if (!empty($matches)) { ++ $docHtml = $matches[1]; ++ ++ // restore any encoded directives ++ $docHtml = preg_replace_callback( ++ '/=\"(%7B%7B[^"]*%7D%7D)\"/m', ++ function ($matches) { ++ return urldecode($matches[0]); ++ }, ++ $docHtml ++ ); ++ ++ if (isset($uniqueNodeNameToDecodedOuterHtmlMap)) { ++ foreach ($uniqueNodeNameToDecodedOuterHtmlMap as $uniqueNodeName => $decodedOuterHtml) { ++ $docHtml = str_replace( ++ '<' . $uniqueNodeName . '>' . '', ++ $decodedOuterHtml, ++ $docHtml ++ ); ++ } ++ } ++ ++ $result = $docHtml; ++ } ++ } ++ ++ return $result; ++ } ++ ++ /** ++ * Create a DOM document from a given string ++ * ++ * @param string $html ++ * ++ * @return \DOMDocument ++ */ ++ private function getDomDocument(string $html) : \DOMDocument ++ { ++ if (!$this->domDocument) { ++ $this->domDocument = $this->createDomDocument($html); ++ } ++ ++ return $this->domDocument; ++ } ++ ++ /** ++ * Create a DOMDocument from a string ++ * ++ * @param string $html ++ * ++ * @return \DOMDocument ++ */ ++ private function createDomDocument(string $html) : \DOMDocument ++ { ++ $domDocument = new \DOMDocument('1.0', 'UTF-8'); ++ set_error_handler( ++ function ($errorNumber, $errorString) { ++ throw new \DOMException($errorString, $errorNumber); ++ } ++ ); ++ $string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); ++ try { ++ libxml_use_internal_errors(true); ++ $domDocument->loadHTML( ++ '' . $string . '' ++ ); ++ libxml_clear_errors(); ++ } catch (\Exception $e) { ++ restore_error_handler(); ++ $this->logger->critical($e); ++ } ++ restore_error_handler(); ++ ++ return $domDocument; ++ } ++ ++ /** ++ * Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement ++ * ++ * @param \DOMDocument $document ++ * @return array ++ * @throws \Magento\Framework\Exception\LocalizedException ++ */ ++ private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array ++ { ++ $xpath = new \DOMXPath($document); ++ ++ // construct xpath query to fetch top-level ancestor html content type nodes ++ /** @var $htmlContentTypeNodes \DOMNode[] */ ++ $htmlContentTypeNodes = $xpath->query( ++ '//*[@data-content-type="html" and not(@data-decoded="true")]' . ++ '[not(ancestor::*[@data-content-type="html"])]' ++ ); ++ ++ $uniqueNodeNameToDecodedOuterHtmlMap = []; ++ ++ foreach ($htmlContentTypeNodes as $htmlContentTypeNode) { ++ // Set decoded attribute on all encoded html content types so we don't double decode; ++ $htmlContentTypeNode->setAttribute('data-decoded', 'true'); ++ ++ // if nothing exists inside the node, continue ++ if (!strlen(trim($htmlContentTypeNode->nodeValue))) { ++ continue; ++ } ++ ++ // clone html code content type to save reference to its attributes/outerHTML, which we are not going to ++ // decode ++ $clonedHtmlContentTypeNode = clone $htmlContentTypeNode; ++ ++ // clear inner contents of cloned node for replacement later with $decodedInnerHtml using sprintf; ++ // we want to retain html content type node and avoid doing any manipulation on it ++ $clonedHtmlContentTypeNode->nodeValue = '%s'; ++ ++ // remove potentially harmful attributes on html content type node itself ++ while ($htmlContentTypeNode->attributes->length) { ++ $htmlContentTypeNode->removeAttribute($htmlContentTypeNode->attributes->item(0)->name); ++ } ++ ++ // decode outerHTML safely ++ $preDecodedOuterHtml = $document->saveHTML($htmlContentTypeNode); ++ ++ // clear empty
wrapper around outerHTML to replace with $clonedHtmlContentTypeNode ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $decodedInnerHtml = preg_replace('#^<[^>]*>|]*>$#', '', html_entity_decode($preDecodedOuterHtml)); ++ ++ // Use $clonedHtmlContentTypeNode's placeholder to inject decoded inner html ++ $decodedOuterHtml = sprintf($document->saveHTML($clonedHtmlContentTypeNode), $decodedInnerHtml); ++ ++ // generate unique node name element to replace with decoded html contents at end of processing; ++ // goal is to create a document as few times as possible to prevent inadvertent parsing of contents as html ++ // by the dom library ++ $uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS); ++ ++ $uniqueNode = new \DOMElement($uniqueNodeName); ++ $htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode); ++ ++ $uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml; ++ } ++ ++ return $uniqueNodeNameToDecodedOuterHtmlMap; ++ } ++ ++ /** ++ * Generate the CSS for any background images on the page ++ * ++ * @param \DOMDocument $document ++ */ ++ private function generateBackgroundImageStyles(\DOMDocument $document) : void ++ { ++ $xpath = new \DOMXPath($document); ++ $nodes = $xpath->query('//*[@data-background-images]'); ++ foreach ($nodes as $node) { ++ /* @var \DOMElement $node */ ++ $backgroundImages = $node->attributes->getNamedItem('data-background-images'); ++ if ($backgroundImages->nodeValue !== '') { ++ $elementClass = uniqid('background-image-'); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $images = $this->json->unserialize(stripslashes($backgroundImages->nodeValue)); ++ if (count($images) > 0) { ++ $style = $xpath->document->createElement( ++ 'style', ++ $this->generateCssFromImages($elementClass, $images) ++ ); ++ $style->setAttribute('type', 'text/css'); ++ $node->parentNode->appendChild($style); ++ ++ // Append our new class to the DOM element ++ $classes = ''; ++ if ($node->attributes->getNamedItem('class')) { ++ $classes = $node->attributes->getNamedItem('class')->nodeValue . ' '; ++ } ++ $node->setAttribute('class', $classes . $elementClass); ++ } ++ } ++ } ++ } ++ ++ /** ++ * Generate CSS based on the images array from our attribute ++ * ++ * @param string $elementClass ++ * @param array $images ++ * ++ * @return string ++ */ ++ private function generateCssFromImages(string $elementClass, array $images) : string ++ { ++ $css = []; ++ if (isset($images['desktop_image'])) { ++ $css['.' . $elementClass] = [ ++ 'background-image' => 'url('https://github.com/.%20$images['desktop_image']%20.')', ++ ]; ++ } ++ if (isset($images['mobile_image']) && $this->getMobileMediaQuery()) { ++ $css[$this->getMobileMediaQuery()]['.' . $elementClass] = [ ++ 'background-image' => 'url('https://github.com/.%20$images['mobile_image']%20.')', ++ ]; ++ } ++ return $this->cssFromArray($css); ++ } ++ ++ /** ++ * Generate a CSS string from an array ++ * ++ * @param array $css ++ * ++ * @return string ++ */ ++ private function cssFromArray(array $css) : string ++ { ++ $output = ''; ++ foreach ($css as $selector => $body) { ++ if (is_array($body)) { ++ $output .= $selector . ' {'; ++ $output .= $this->cssFromArray($body); ++ $output .= '}'; ++ } else { ++ $output .= $selector . ': ' . $body . ';'; ++ } ++ } ++ return $output; ++ } ++ ++ /** ++ * Generate the mobile media query from view configuration ++ * ++ * @return null|string ++ */ ++ private function getMobileMediaQuery() : ?string ++ { ++ $breakpoints = $this->viewConfig->getViewConfig()->getVarValue( ++ 'Magento_PageBuilder', ++ 'breakpoints/mobile/conditions' ++ ); ++ if ($breakpoints && count($breakpoints) > 0) { ++ $mobileBreakpoint = '@media only screen '; ++ foreach ($breakpoints as $key => $value) { ++ $mobileBreakpoint .= 'and (' . $key . ': ' . $value . ') '; ++ } ++ return rtrim($mobileBreakpoint); ++ } ++ return null; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php +--- a/vendor/magento/module-page-builder/Model/Stage/Config.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Config.php +@@ -135,9 +135,7 @@ class Config + 'content_types' => $this->getContentTypes(), + 'stage_config' => $this->data, + 'media_url' => $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]), +- 'preview_url' => $this->frontendUrlBuilder +- ->addSessionParam() +- ->getUrl('pagebuilder/contenttype/preview'), ++ 'preview_url' => $this->urlBuilder->getUrl('pagebuilder/stage/preview'), + 'render_url' => $this->urlBuilder->getUrl('pagebuilder/stage/render'), + 'column_grid_default' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_DEFAULT), + 'column_grid_max' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_MAX), +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Preview.php b/vendor/magento/module-page-builder/Model/Stage/Preview.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Model/Stage/Preview.php +@@ -0,0 +1,134 @@ ++emulation = $emulation; ++ $this->appState = $appState; ++ $this->design = $design; ++ $this->themeProvider = $themeProvider; ++ $this->storeManager = $storeManager; ++ $this->scopeConfig = $scopeConfig; ++ } ++ ++ /** ++ * @var bool ++ */ ++ private $isPreview; ++ ++ /** ++ * Retrieve the area in which the preview needs to be ran in ++ * ++ * @return string ++ */ ++ public function getPreviewArea() : string ++ { ++ return \Magento\Framework\App\Area::AREA_FRONTEND; ++ } ++ ++ /** ++ * Start Page Builder preview mode and emulate store front ++ * ++ * @param callable $callback ++ * @param int $storeId ++ * @return mixed ++ * @throws \Exception ++ */ ++ public function startPreviewMode($callback, $storeId = null) ++ { ++ $this->isPreview = true; ++ ++ if (!$storeId) { ++ $storeId = $this->storeManager->getDefaultStoreView()->getId(); ++ } ++ $this->emulation->startEnvironmentEmulation($storeId); ++ ++ return $this->appState->emulateAreaCode( ++ $this->getPreviewArea(), ++ function () use ($callback) { ++ $themeId = $this->scopeConfig->getValue( ++ 'design/theme/theme_id', ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ $theme = $this->themeProvider->getThemeById($themeId); ++ $this->design->setDesignTheme($theme, $this->getPreviewArea()); ++ ++ try { ++ $result = $callback(); ++ } catch (\Exception $e) { ++ $this->isPreview = false; ++ throw $e; ++ } ++ ++ $this->emulation->stopEnvironmentEmulation(); ++ return $result; ++ } ++ ); ++ } ++ ++ /** ++ * Determine if the system is in preview mode ++ * ++ * @return bool ++ */ ++ public function isPreviewMode() : bool ++ { ++ return $this->isPreview; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Magento\Framework\Controller\ResultFactory; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a block for the stage +@@ -31,20 +32,27 @@ class Block implements \Magento\PageBuilder\Model\Stage\RendererInterface + private $resultFactory; + + /** +- * Constructor +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\PageBuilder\Model\Config $config + * @param \Magento\Framework\View\Element\BlockFactory $blockFactory + * @param ResultFactory $resultFactory ++ * @param Template|null $templateFilter + */ + public function __construct( + \Magento\PageBuilder\Model\Config $config, + \Magento\Framework\View\Element\BlockFactory $blockFactory, +- ResultFactory $resultFactory ++ ResultFactory $resultFactory, ++ Template $templateFilter = null + ) { + $this->config = $config; + $this->blockFactory = $blockFactory; + $this->resultFactory = $resultFactory; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -77,7 +85,7 @@ class Block implements \Magento\PageBuilder\Model\Stage\RendererInterface + $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $pageResult->getLayout()->addBlock($backendBlockInstance); + +- $result['content'] = $backendBlockInstance->toHtml(); ++ $result['content'] = $this->templateFilter->filter($backendBlockInstance->toHtml()); + } + + return $result; +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php +@@ -9,6 +9,8 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Psr\Log\LoggerInterface; ++use Magento\PageBuilder\Model\Stage\HtmlFilter; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a CMS Block for the stage +@@ -33,28 +35,35 @@ class CmsStaticBlock implements \Magento\PageBuilder\Model\Stage\RendererInterfa + private $loggerInterface; + + /** +- * @var \Magento\PageBuilder\Model\Stage\HtmlFilter ++ * @var HtmlFilter + */ + private $htmlFilter; + + /** +- * CmsStaticBlock constructor. +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\Cms\Model\ResourceModel\Block\CollectionFactory $blockCollectionFactory + * @param WidgetDirective $widgetDirectiveRenderer + * @param LoggerInterface $loggerInterface + * @param \Magento\PageBuilder\Model\Stage\HtmlFilter $htmlFilter ++ * @param \Magento\PageBuilder\Model\Filter\Template|null $templateFilter + */ + public function __construct( + \Magento\Cms\Model\ResourceModel\Block\CollectionFactory $blockCollectionFactory, + WidgetDirective $widgetDirectiveRenderer, + LoggerInterface $loggerInterface, +- \Magento\PageBuilder\Model\Stage\HtmlFilter $htmlFilter ++ HtmlFilter $htmlFilter, ++ Template $templateFilter = null + ) { + $this->blockCollectionFactory = $blockCollectionFactory; + $this->widgetDirectiveRenderer = $widgetDirectiveRenderer; + $this->loggerInterface = $loggerInterface; + $this->htmlFilter = $htmlFilter; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -96,7 +105,9 @@ class CmsStaticBlock implements \Magento\PageBuilder\Model\Stage\RendererInterfa + + if ($block->isActive()) { + $directiveResult = $this->widgetDirectiveRenderer->render($params); +- $result['content'] = $this->htmlFilter->filterHtml($directiveResult['content']); ++ $result['content'] = $this->htmlFilter->filterHtml( ++ $this->templateFilter->filter($directiveResult['content']) ++ ); + } else { + $result['error'] = __('Block disabled'); + } +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Magento\Store\Model\Store; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a widget directive for the stage +@@ -28,17 +29,24 @@ class WidgetDirective implements \Magento\PageBuilder\Model\Stage\RendererInterf + private $directiveFilter; + + /** +- * Constructor +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Widget\Model\Template\Filter $directiveFilter ++ * @param Template $templateFilter + */ + public function __construct( + \Magento\Store\Model\StoreManagerInterface $storeManager, +- \Magento\Widget\Model\Template\Filter $directiveFilter ++ \Magento\Widget\Model\Template\Filter $directiveFilter, ++ Template $templateFilter = null + ) { + $this->storeManager = $storeManager; + $this->directiveFilter = $directiveFilter; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -61,7 +69,7 @@ class WidgetDirective implements \Magento\PageBuilder\Model\Stage\RendererInterf + try { + $result['content'] = $this->directiveFilter + ->setStoreId(Store::DEFAULT_STORE_ID) +- ->filter($params['directive']); ++ ->filter($this->templateFilter->filter($params['directive'])); + } catch (\Exception $e) { + $result['error'] = __($e->getMessage()); + } +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php b/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php +@@ -0,0 +1,48 @@ ++stock = $stock; ++ } ++ ++ /** ++ * Allow to sort product collection ++ * ++ * @param ProductsList $subject ++ * @param Collection $result ++ * @return Collection ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function afterCreateCollection( ++ ProductsList $subject, ++ Collection $result ++ ) { ++ $this->stock->addIsInStockFilterToCollection($result); ++ return $result; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Plugin/DesignLoader.php b/vendor/magento/module-page-builder/Plugin/DesignLoader.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/DesignLoader.php +@@ -0,0 +1,98 @@ ++designLoader = $designLoader; ++ $this->messageManager = $messageManager; ++ $this->appState = $appState; ++ $this->preview = $preview; ++ } ++ ++ /** ++ * Before create load the design files ++ * ++ * @param \Magento\Catalog\Block\Product\ImageFactory $subject ++ * @param Product $product ++ * @param string $imageId ++ * @param array|null $attributes ++ * @throws \Exception ++ * ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforeCreate( ++ \Magento\Catalog\Block\Product\ImageFactory $subject, ++ Product $product, ++ string $imageId, ++ array $attributes = null ++ ) { ++ if ($this->preview->isPreviewMode()) { ++ $this->appState->emulateAreaCode( ++ $this->preview->getPreviewArea(), ++ [$this, 'loadDesignConfig'] ++ ); ++ } ++ } ++ ++ /** ++ * Load the design config ++ */ ++ public function loadDesignConfig() ++ { ++ try { ++ $this->designLoader->load(); ++ } catch (\Magento\Framework\Exception\LocalizedException $e) { ++ if ($e->getPrevious() instanceof \Magento\Framework\Config\Dom\ValidationException) { ++ /** @var MessageInterface $message */ ++ $message = $this->messageManager ++ ->createMessage(MessageInterface::TYPE_ERROR) ++ ->setText($e->getMessage()); ++ $this->messageManager->addUniqueMessages([$message]); ++ } ++ } ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php b/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php +--- a/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php ++++ b/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php +@@ -7,56 +7,29 @@ declare(strict_types=1); + + namespace Magento\PageBuilder\Plugin\Filter; + ++use Magento\Store\Model\Store; ++ + /** + * Plugin to the template filter to process any background images added by Page Builder + */ + class TemplatePlugin + { +- const BACKGROUND_IMAGE_PATTERN = '/data-background-images/si'; ++ const BACKGROUND_IMAGE_PATTERN = '/data-background-images=(?:\'|"){.+}(?:\'|")/si'; + + const HTML_CONTENT_TYPE_PATTERN = '/data-content-type="html"/si'; + + /** +- * @var \Magento\Framework\View\ConfigInterface +- */ +- private $viewConfig; +- +- /** +- * @var \Psr\Log\LoggerInterface +- */ +- private $logger; +- +- /** +- * @var \DOMDocument +- */ +- private $domDocument; +- +- /** +- * @var \Magento\Framework\Math\Random +- */ +- private $mathRandom; +- +- /** +- * @var \Magento\Framework\Serialize\Serializer\Json ++ * @var \Magento\PageBuilder\Model\Filter\Template + */ +- private $json; ++ private $templateFilter; + + /** +- * @param \Psr\Log\LoggerInterface $logger +- * @param \Magento\Framework\View\ConfigInterface $viewConfig +- * @param \Magento\Framework\Math\Random $mathRandom +- * @param \Magento\Framework\Serialize\Serializer\Json $json ++ * @param \Magento\PageBuilder\Model\Filter\Template $templateFilter + */ + public function __construct( +- \Psr\Log\LoggerInterface $logger, +- \Magento\Framework\View\ConfigInterface $viewConfig, +- \Magento\Framework\Math\Random $mathRandom, +- \Magento\Framework\Serialize\Serializer\Json $json ++ \Magento\PageBuilder\Model\Filter\Template $templateFilter + ) { +- $this->logger = $logger; +- $this->viewConfig = $viewConfig; +- $this->mathRandom = $mathRandom; +- $this->json = $json; ++ $this->templateFilter = $templateFilter; + } + + /** +@@ -70,260 +43,6 @@ class TemplatePlugin + */ + public function afterFilter(\Magento\Framework\Filter\Template $subject, string $result) : string + { +- $this->domDocument = false; +- +- // Validate if the filtered result requires background image processing +- if (preg_match(self::BACKGROUND_IMAGE_PATTERN, $result)) { +- $document = $this->getDomDocument($result); +- $this->generateBackgroundImageStyles($document); +- } +- +- // Process any HTML content types, they need to be decoded on the front-end +- if (preg_match(self::HTML_CONTENT_TYPE_PATTERN, $result)) { +- $document = $this->getDomDocument($result); +- $uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document); +- } +- +- // If a document was retrieved we've modified the output so need to retrieve it from within the document +- if (isset($document)) { +- // Match the contents of the body from our generated document +- preg_match( +- '/(.+)<\/body><\/html>$/si', +- $document->saveHTML(), +- $matches +- ); +- +- if (!empty($matches)) { +- $docHtml = $matches[1]; +- +- if (isset($uniqueNodeNameToDecodedOuterHtmlMap)) { +- foreach ($uniqueNodeNameToDecodedOuterHtmlMap as $uniqueNodeName => $decodedOuterHtml) { +- $docHtml = str_replace( +- '<' . $uniqueNodeName . '>' . '', +- $decodedOuterHtml, +- $docHtml +- ); +- } +- } +- +- $result = $docHtml; +- } +- } +- +- return $result; +- } +- +- /** +- * Create a DOM document from a given string +- * +- * @param string $html +- * +- * @return \DOMDocument +- */ +- private function getDomDocument(string $html) : \DOMDocument +- { +- if (!$this->domDocument) { +- $this->domDocument = $this->createDomDocument($html); +- } +- +- return $this->domDocument; +- } +- +- /** +- * Create a DOMDocument from a string +- * +- * @param string $html +- * +- * @return \DOMDocument +- */ +- private function createDomDocument(string $html) : \DOMDocument +- { +- $domDocument = new \DOMDocument('1.0', 'UTF-8'); +- set_error_handler( +- function ($errorNumber, $errorString) { +- throw new \DOMException($errorString, $errorNumber); +- } +- ); +- $string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); +- try { +- libxml_use_internal_errors(true); +- $domDocument->loadHTML( +- '' . $string . '' +- ); +- libxml_clear_errors(); +- } catch (\Exception $e) { +- restore_error_handler(); +- $this->logger->critical($e); +- } +- restore_error_handler(); +- +- return $domDocument; +- } +- +- /** +- * Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement +- * +- * @param \DOMDocument $document +- * @return array - map of unique node name to decoded html +- */ +- private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array +- { +- $xpath = new \DOMXPath($document); +- +- // construct xpath query to fetch top-level ancestor html content type nodes +- /** @var $htmlContentTypeNodes \DOMNode[] */ +- $htmlContentTypeNodes = $xpath->query( +- '//*[@data-content-type="html" and not(@data-decoded="true")]' . +- '[not(ancestor::*[@data-content-type="html"])]' +- ); +- +- $uniqueNodeNameToDecodedOuterHtmlMap = []; +- +- foreach ($htmlContentTypeNodes as $htmlContentTypeNode) { +- // Set decoded attribute on all encoded html content types so we don't double decode; +- $htmlContentTypeNode->setAttribute('data-decoded', 'true'); +- +- // if nothing exists inside the node, continue +- if (!strlen(trim($htmlContentTypeNode->nodeValue))) { +- continue; +- } +- +- // clone html code content type to save reference to its attributes/outerHTML, which we are not going to +- // decode +- $clonedHtmlContentTypeNode = clone $htmlContentTypeNode; +- +- // clear inner contents of cloned node for replacement later with $decodedInnerHtml using sprintf; +- // we want to retain html content type node and avoid doing any manipulation on it +- $clonedHtmlContentTypeNode->nodeValue = '%s'; +- +- // remove potentially harmful attributes on html content type node itself +- while ($htmlContentTypeNode->attributes->length) { +- $htmlContentTypeNode->removeAttribute($htmlContentTypeNode->attributes->item(0)->name); +- } +- +- // decode outerHTML safely +- $preDecodedOuterHtml = $document->saveHTML($htmlContentTypeNode); +- +- // clear empty
wrapper around outerHTML to replace with $clonedHtmlContentTypeNode +- // phpcs:ignore Magento2.Functions.DiscouragedFunction +- $decodedInnerHtml = preg_replace('#^<[^>]*>|]*>$#', '', html_entity_decode($preDecodedOuterHtml)); +- +- // Use $clonedHtmlContentTypeNode's placeholder to inject decoded inner html +- $decodedOuterHtml = sprintf($document->saveHTML($clonedHtmlContentTypeNode), $decodedInnerHtml); +- +- // generate unique node name element to replace with decoded html contents at end of processing; +- // goal is to create a document as few times as possible to prevent inadvertent parsing of contents as html +- // by the dom library +- $uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS); +- +- $uniqueNode = new \DOMElement($uniqueNodeName); +- $htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode); +- +- $uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml; +- } +- +- return $uniqueNodeNameToDecodedOuterHtmlMap; +- } +- +- /** +- * Generate the CSS for any background images on the page +- * +- * @param \DOMDocument $document +- */ +- private function generateBackgroundImageStyles(\DOMDocument $document) : void +- { +- $xpath = new \DOMXPath($document); +- $nodes = $xpath->query('//*[@data-background-images]'); +- foreach ($nodes as $node) { +- /* @var \DOMElement $node */ +- $backgroundImages = $node->attributes->getNamedItem('data-background-images'); +- if ($backgroundImages->nodeValue !== '') { +- $elementClass = uniqid('background-image-'); +- // phpcs:ignore Magento2.Functions.DiscouragedFunction +- $images = $this->json->unserialize(stripslashes($backgroundImages->nodeValue)); +- if (count($images) > 0) { +- $style = $xpath->document->createElement( +- 'style', +- $this->generateCssFromImages($elementClass, $images) +- ); +- $style->setAttribute('type', 'text/css'); +- $node->parentNode->appendChild($style); +- +- // Append our new class to the DOM element +- $classes = ''; +- if ($node->attributes->getNamedItem('class')) { +- $classes = $node->attributes->getNamedItem('class')->nodeValue . ' '; +- } +- $node->setAttribute('class', $classes . $elementClass); +- } +- } +- } +- } +- +- /** +- * Generate CSS based on the images array from our attribute +- * +- * @param string $elementClass +- * @param array $images +- * +- * @return string +- */ +- private function generateCssFromImages(string $elementClass, array $images) : string +- { +- $css = []; +- if (isset($images['desktop_image'])) { +- $css['.' . $elementClass] = [ +- 'background-image' => 'url('https://github.com/.%20$images['desktop_image']%20.')', +- ]; +- } +- if (isset($images['mobile_image']) && $this->getMobileMediaQuery()) { +- $css[$this->getMobileMediaQuery()]['.' . $elementClass] = [ +- 'background-image' => 'url('https://github.com/.%20$images['mobile_image']%20.')', +- ]; +- } +- return $this->cssFromArray($css); +- } +- +- /** +- * Generate a CSS string from an array +- * +- * @param array $css +- * +- * @return string +- */ +- private function cssFromArray(array $css) : string +- { +- $output = ''; +- foreach ($css as $selector => $body) { +- if (is_array($body)) { +- $output .= $selector . ' {'; +- $output .= $this->cssFromArray($body); +- $output .= '}'; +- } else { +- $output .= $selector . ': ' . $body . ';'; +- } +- } +- return $output; +- } +- +- /** +- * Generate the mobile media query from view configuration +- * +- * @return null|string +- */ +- private function getMobileMediaQuery() : ?string +- { +- $breakpoints = $this->viewConfig->getViewConfig()->getVarValue( +- 'Magento_PageBuilder', +- 'breakpoints/mobile/conditions' +- ); +- if ($breakpoints && count($breakpoints) > 0) { +- $mobileBreakpoint = '@media only screen '; +- foreach ($breakpoints as $key => $value) { +- $mobileBreakpoint .= 'and (' . $key . ': ' . $value . ') '; +- } +- return rtrim($mobileBreakpoint); +- } +- return null; ++ return $this->templateFilter->filter($result); + } + } +diff -Nuar a/vendor/magento/module-page-builder/etc/adminhtml/di.xml b/vendor/magento/module-page-builder/etc/adminhtml/di.xml +--- a/vendor/magento/module-page-builder/etc/adminhtml/di.xml ++++ b/vendor/magento/module-page-builder/etc/adminhtml/di.xml +@@ -6,6 +6,9 @@ + */ + --> + ++ ++ ++ + + + ns = pagebuilder_modal_form, index = modal +diff -Nuar a/vendor/magento/module-page-builder/etc/di.xml b/vendor/magento/module-page-builder/etc/di.xml +--- a/vendor/magento/module-page-builder/etc/di.xml ++++ b/vendor/magento/module-page-builder/etc/di.xml +@@ -131,6 +131,7 @@ + + pageBuilderProductCollectionFactory + ++ + + + +diff -Nuar a/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml b/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml +@@ -0,0 +1,14 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ diff --git a/patches/PB-323__fix_pagebuilder_module__2.3.3.patch b/patches/PB-323__fix_pagebuilder_module__2.3.3.patch new file mode 100644 index 00000000..3ec55eb6 --- /dev/null +++ b/patches/PB-323__fix_pagebuilder_module__2.3.3.patch @@ -0,0 +1,1280 @@ +diff -Nuar a/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php b/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Controller/Adminhtml/Stage/Preview.php +@@ -0,0 +1,71 @@ ++rendererPool = $rendererPool; ++ $this->preview = $preview; ++ } ++ ++ /** ++ * Generates an HTML preview for the stage ++ * ++ * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|mixed ++ * @throws \Exception ++ */ ++ public function execute() ++ { ++ return $this->preview->startPreviewMode( ++ function () { ++ $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); ++ // Some template filters and directive processors expect this to be called in order to function. ++ $pageResult->initLayout(); ++ ++ $params = $this->getRequest()->getParams(); ++ $renderer = $this->rendererPool->getRenderer($params['role']); ++ $result = ['data' => $renderer->render($params)]; ++ ++ return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); ++ } ++ ); ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +--- a/vendor/magento/module-page-builder/Controller/ContentType/Preview.php ++++ b/vendor/magento/module-page-builder/Controller/ContentType/Preview.php +@@ -17,6 +17,8 @@ use Magento\Framework\App\Action\HttpPostActionInterface; + * This isn't placed within the adminhtml folder as it has to extend from the front-end controllers app action to + * ensure the content is rendered in the storefront scope. + * ++ * @deprecated use \Magento\PageBuilder\Controller\Adminhtml\Stage\Preview ++ * + * @api + */ + class Preview extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface +diff -Nuar a/vendor/magento/module-page-builder/Model/Filter/Template.php b/vendor/magento/module-page-builder/Model/Filter/Template.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Model/Filter/Template.php +@@ -0,0 +1,334 @@ ++logger = $logger; ++ $this->viewConfig = $viewConfig; ++ $this->mathRandom = $mathRandom; ++ $this->json = $json; ++ } ++ ++ /** ++ * After filter of template data apply transformations ++ * ++ * @param string $result ++ * ++ * @return string ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function filter(string $result) : string ++ { ++ $this->domDocument = false; ++ ++ // Validate if the filtered result requires background image processing ++ if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::BACKGROUND_IMAGE_PATTERN, $result)) { ++ $document = $this->getDomDocument($result); ++ $this->generateBackgroundImageStyles($document); ++ } ++ ++ // Process any HTML content types, they need to be decoded on the front-end ++ if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::HTML_CONTENT_TYPE_PATTERN, $result)) { ++ $document = $this->getDomDocument($result); ++ $uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document); ++ } ++ ++ // If a document was retrieved we've modified the output so need to retrieve it from within the document ++ if (isset($document)) { ++ // Match the contents of the body from our generated document ++ preg_match( ++ '/(.+)<\/body><\/html>$/si', ++ $document->saveHTML(), ++ $matches ++ ); ++ ++ if (!empty($matches)) { ++ $docHtml = $matches[1]; ++ ++ // restore any encoded directives ++ $docHtml = preg_replace_callback( ++ '/=\"(%7B%7B[^"]*%7D%7D)\"/m', ++ function ($matches) { ++ return urldecode($matches[0]); ++ }, ++ $docHtml ++ ); ++ ++ if (isset($uniqueNodeNameToDecodedOuterHtmlMap)) { ++ foreach ($uniqueNodeNameToDecodedOuterHtmlMap as $uniqueNodeName => $decodedOuterHtml) { ++ $docHtml = str_replace( ++ '<' . $uniqueNodeName . '>' . '', ++ $decodedOuterHtml, ++ $docHtml ++ ); ++ } ++ } ++ ++ $result = $docHtml; ++ } ++ } ++ ++ return $result; ++ } ++ ++ /** ++ * Create a DOM document from a given string ++ * ++ * @param string $html ++ * ++ * @return \DOMDocument ++ */ ++ private function getDomDocument(string $html) : \DOMDocument ++ { ++ if (!$this->domDocument) { ++ $this->domDocument = $this->createDomDocument($html); ++ } ++ ++ return $this->domDocument; ++ } ++ ++ /** ++ * Create a DOMDocument from a string ++ * ++ * @param string $html ++ * ++ * @return \DOMDocument ++ */ ++ private function createDomDocument(string $html) : \DOMDocument ++ { ++ $domDocument = new \DOMDocument('1.0', 'UTF-8'); ++ set_error_handler( ++ function ($errorNumber, $errorString) { ++ throw new \DOMException($errorString, $errorNumber); ++ } ++ ); ++ $string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); ++ try { ++ libxml_use_internal_errors(true); ++ $domDocument->loadHTML( ++ '' . $string . '' ++ ); ++ libxml_clear_errors(); ++ } catch (\Exception $e) { ++ restore_error_handler(); ++ $this->logger->critical($e); ++ } ++ restore_error_handler(); ++ ++ return $domDocument; ++ } ++ ++ /** ++ * Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement ++ * ++ * @param \DOMDocument $document ++ * @return array ++ * @throws \Magento\Framework\Exception\LocalizedException ++ */ ++ private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array ++ { ++ $xpath = new \DOMXPath($document); ++ ++ // construct xpath query to fetch top-level ancestor html content type nodes ++ /** @var $htmlContentTypeNodes \DOMNode[] */ ++ $htmlContentTypeNodes = $xpath->query( ++ '//*[@data-content-type="html" and not(@data-decoded="true")]' . ++ '[not(ancestor::*[@data-content-type="html"])]' ++ ); ++ ++ $uniqueNodeNameToDecodedOuterHtmlMap = []; ++ ++ foreach ($htmlContentTypeNodes as $htmlContentTypeNode) { ++ // Set decoded attribute on all encoded html content types so we don't double decode; ++ $htmlContentTypeNode->setAttribute('data-decoded', 'true'); ++ ++ // if nothing exists inside the node, continue ++ if (!strlen(trim($htmlContentTypeNode->nodeValue))) { ++ continue; ++ } ++ ++ // clone html code content type to save reference to its attributes/outerHTML, which we are not going to ++ // decode ++ $clonedHtmlContentTypeNode = clone $htmlContentTypeNode; ++ ++ // clear inner contents of cloned node for replacement later with $decodedInnerHtml using sprintf; ++ // we want to retain html content type node and avoid doing any manipulation on it ++ $clonedHtmlContentTypeNode->nodeValue = '%s'; ++ ++ // remove potentially harmful attributes on html content type node itself ++ while ($htmlContentTypeNode->attributes->length) { ++ $htmlContentTypeNode->removeAttribute($htmlContentTypeNode->attributes->item(0)->name); ++ } ++ ++ // decode outerHTML safely ++ $preDecodedOuterHtml = $document->saveHTML($htmlContentTypeNode); ++ ++ // clear empty
wrapper around outerHTML to replace with $clonedHtmlContentTypeNode ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $decodedInnerHtml = preg_replace('#^<[^>]*>|]*>$#', '', html_entity_decode($preDecodedOuterHtml)); ++ ++ // Use $clonedHtmlContentTypeNode's placeholder to inject decoded inner html ++ $decodedOuterHtml = sprintf($document->saveHTML($clonedHtmlContentTypeNode), $decodedInnerHtml); ++ ++ // generate unique node name element to replace with decoded html contents at end of processing; ++ // goal is to create a document as few times as possible to prevent inadvertent parsing of contents as html ++ // by the dom library ++ $uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS); ++ ++ $uniqueNode = new \DOMElement($uniqueNodeName); ++ $htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode); ++ ++ $uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml; ++ } ++ ++ return $uniqueNodeNameToDecodedOuterHtmlMap; ++ } ++ ++ /** ++ * Generate the CSS for any background images on the page ++ * ++ * @param \DOMDocument $document ++ */ ++ private function generateBackgroundImageStyles(\DOMDocument $document) : void ++ { ++ $xpath = new \DOMXPath($document); ++ $nodes = $xpath->query('//*[@data-background-images]'); ++ foreach ($nodes as $node) { ++ /* @var \DOMElement $node */ ++ $backgroundImages = $node->attributes->getNamedItem('data-background-images'); ++ if ($backgroundImages->nodeValue !== '') { ++ $elementClass = uniqid('background-image-'); ++ // phpcs:ignore Magento2.Functions.DiscouragedFunction ++ $images = $this->json->unserialize(stripslashes($backgroundImages->nodeValue)); ++ if (count($images) > 0) { ++ $style = $xpath->document->createElement( ++ 'style', ++ $this->generateCssFromImages($elementClass, $images) ++ ); ++ $style->setAttribute('type', 'text/css'); ++ $node->parentNode->appendChild($style); ++ ++ // Append our new class to the DOM element ++ $classes = ''; ++ if ($node->attributes->getNamedItem('class')) { ++ $classes = $node->attributes->getNamedItem('class')->nodeValue . ' '; ++ } ++ $node->setAttribute('class', $classes . $elementClass); ++ } ++ } ++ } ++ } ++ ++ /** ++ * Generate CSS based on the images array from our attribute ++ * ++ * @param string $elementClass ++ * @param array $images ++ * ++ * @return string ++ */ ++ private function generateCssFromImages(string $elementClass, array $images) : string ++ { ++ $css = []; ++ if (isset($images['desktop_image'])) { ++ $css['.' . $elementClass] = [ ++ 'background-image' => 'url('https://github.com/.%20$images['desktop_image']%20.')', ++ ]; ++ } ++ if (isset($images['mobile_image']) && $this->getMobileMediaQuery()) { ++ $css[$this->getMobileMediaQuery()]['.' . $elementClass] = [ ++ 'background-image' => 'url('https://github.com/.%20$images['mobile_image']%20.')', ++ ]; ++ } ++ return $this->cssFromArray($css); ++ } ++ ++ /** ++ * Generate a CSS string from an array ++ * ++ * @param array $css ++ * ++ * @return string ++ */ ++ private function cssFromArray(array $css) : string ++ { ++ $output = ''; ++ foreach ($css as $selector => $body) { ++ if (is_array($body)) { ++ $output .= $selector . ' {'; ++ $output .= $this->cssFromArray($body); ++ $output .= '}'; ++ } else { ++ $output .= $selector . ': ' . $body . ';'; ++ } ++ } ++ return $output; ++ } ++ ++ /** ++ * Generate the mobile media query from view configuration ++ * ++ * @return null|string ++ */ ++ private function getMobileMediaQuery() : ?string ++ { ++ $breakpoints = $this->viewConfig->getViewConfig()->getVarValue( ++ 'Magento_PageBuilder', ++ 'breakpoints/mobile/conditions' ++ ); ++ if ($breakpoints && count($breakpoints) > 0) { ++ $mobileBreakpoint = '@media only screen '; ++ foreach ($breakpoints as $key => $value) { ++ $mobileBreakpoint .= 'and (' . $key . ': ' . $value . ') '; ++ } ++ return rtrim($mobileBreakpoint); ++ } ++ return null; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Config.php b/vendor/magento/module-page-builder/Model/Stage/Config.php +--- a/vendor/magento/module-page-builder/Model/Stage/Config.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Config.php +@@ -135,9 +135,7 @@ class Config + 'content_types' => $this->getContentTypes(), + 'stage_config' => $this->data, + 'media_url' => $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]), +- 'preview_url' => $this->frontendUrlBuilder +- ->addSessionParam() +- ->getUrl('pagebuilder/contenttype/preview'), ++ 'preview_url' => $this->urlBuilder->getUrl('pagebuilder/stage/preview'), + 'render_url' => $this->urlBuilder->getUrl('pagebuilder/stage/render'), + 'column_grid_default' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_DEFAULT), + 'column_grid_max' => $this->scopeConfig->getValue(self::XML_PATH_COLUMN_GRID_MAX), +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Preview.php b/vendor/magento/module-page-builder/Model/Stage/Preview.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Model/Stage/Preview.php +@@ -0,0 +1,134 @@ ++emulation = $emulation; ++ $this->appState = $appState; ++ $this->design = $design; ++ $this->themeProvider = $themeProvider; ++ $this->storeManager = $storeManager; ++ $this->scopeConfig = $scopeConfig; ++ } ++ ++ /** ++ * @var bool ++ */ ++ private $isPreview; ++ ++ /** ++ * Retrieve the area in which the preview needs to be ran in ++ * ++ * @return string ++ */ ++ public function getPreviewArea() : string ++ { ++ return \Magento\Framework\App\Area::AREA_FRONTEND; ++ } ++ ++ /** ++ * Start Page Builder preview mode and emulate store front ++ * ++ * @param callable $callback ++ * @param int $storeId ++ * @return mixed ++ * @throws \Exception ++ */ ++ public function startPreviewMode($callback, $storeId = null) ++ { ++ $this->isPreview = true; ++ ++ if (!$storeId) { ++ $storeId = $this->storeManager->getDefaultStoreView()->getId(); ++ } ++ $this->emulation->startEnvironmentEmulation($storeId); ++ ++ return $this->appState->emulateAreaCode( ++ $this->getPreviewArea(), ++ function () use ($callback) { ++ $themeId = $this->scopeConfig->getValue( ++ 'design/theme/theme_id', ++ \Magento\Store\Model\ScopeInterface::SCOPE_STORE ++ ); ++ $theme = $this->themeProvider->getThemeById($themeId); ++ $this->design->setDesignTheme($theme, $this->getPreviewArea()); ++ ++ try { ++ $result = $callback(); ++ } catch (\Exception $e) { ++ $this->isPreview = false; ++ throw $e; ++ } ++ ++ $this->emulation->stopEnvironmentEmulation(); ++ return $result; ++ } ++ ); ++ } ++ ++ /** ++ * Determine if the system is in preview mode ++ * ++ * @return bool ++ */ ++ public function isPreviewMode() : bool ++ { ++ return $this->isPreview; ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/Block.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Magento\Framework\Controller\ResultFactory; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a block for the stage +@@ -31,20 +32,27 @@ class Block implements \Magento\PageBuilder\Model\Stage\RendererInterface + private $resultFactory; + + /** +- * Constructor +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\PageBuilder\Model\Config $config + * @param \Magento\Framework\View\Element\BlockFactory $blockFactory + * @param ResultFactory $resultFactory ++ * @param Template|null $templateFilter + */ + public function __construct( + \Magento\PageBuilder\Model\Config $config, + \Magento\Framework\View\Element\BlockFactory $blockFactory, +- ResultFactory $resultFactory ++ ResultFactory $resultFactory, ++ Template $templateFilter = null + ) { + $this->config = $config; + $this->blockFactory = $blockFactory; + $this->resultFactory = $resultFactory; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -77,7 +85,7 @@ class Block implements \Magento\PageBuilder\Model\Stage\RendererInterface + $pageResult = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $pageResult->getLayout()->addBlock($backendBlockInstance); + +- $result['content'] = $backendBlockInstance->toHtml(); ++ $result['content'] = $this->templateFilter->filter($backendBlockInstance->toHtml()); + } + + return $result; +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/CmsStaticBlock.php +@@ -9,6 +9,8 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Psr\Log\LoggerInterface; ++use Magento\PageBuilder\Model\Stage\HtmlFilter; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a CMS Block for the stage +@@ -33,28 +35,35 @@ class CmsStaticBlock implements \Magento\PageBuilder\Model\Stage\RendererInterfa + private $loggerInterface; + + /** +- * @var \Magento\PageBuilder\Model\Stage\HtmlFilter ++ * @var HtmlFilter + */ + private $htmlFilter; + + /** +- * CmsStaticBlock constructor. +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\Cms\Model\ResourceModel\Block\CollectionFactory $blockCollectionFactory + * @param WidgetDirective $widgetDirectiveRenderer + * @param LoggerInterface $loggerInterface + * @param \Magento\PageBuilder\Model\Stage\HtmlFilter $htmlFilter ++ * @param \Magento\PageBuilder\Model\Filter\Template|null $templateFilter + */ + public function __construct( + \Magento\Cms\Model\ResourceModel\Block\CollectionFactory $blockCollectionFactory, + WidgetDirective $widgetDirectiveRenderer, + LoggerInterface $loggerInterface, +- \Magento\PageBuilder\Model\Stage\HtmlFilter $htmlFilter ++ HtmlFilter $htmlFilter, ++ Template $templateFilter = null + ) { + $this->blockCollectionFactory = $blockCollectionFactory; + $this->widgetDirectiveRenderer = $widgetDirectiveRenderer; + $this->loggerInterface = $loggerInterface; + $this->htmlFilter = $htmlFilter; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -96,7 +105,9 @@ class CmsStaticBlock implements \Magento\PageBuilder\Model\Stage\RendererInterfa + + if ($block->isActive()) { + $directiveResult = $this->widgetDirectiveRenderer->render($params); +- $result['content'] = $this->htmlFilter->filterHtml($directiveResult['content']); ++ $result['content'] = $this->htmlFilter->filterHtml( ++ $this->templateFilter->filter($directiveResult['content']) ++ ); + } else { + $result['error'] = __('Block disabled'); + } +diff -Nuar a/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php b/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php +--- a/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php ++++ b/vendor/magento/module-page-builder/Model/Stage/Renderer/WidgetDirective.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Model\Stage\Renderer; + + use Magento\Store\Model\Store; ++use Magento\PageBuilder\Model\Filter\Template; + + /** + * Renders a widget directive for the stage +@@ -28,17 +29,24 @@ class WidgetDirective implements \Magento\PageBuilder\Model\Stage\RendererInterf + private $directiveFilter; + + /** +- * Constructor +- * ++ * @var Template ++ */ ++ private $templateFilter; ++ ++ /** + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Widget\Model\Template\Filter $directiveFilter ++ * @param Template $templateFilter + */ + public function __construct( + \Magento\Store\Model\StoreManagerInterface $storeManager, +- \Magento\Widget\Model\Template\Filter $directiveFilter ++ \Magento\Widget\Model\Template\Filter $directiveFilter, ++ Template $templateFilter = null + ) { + $this->storeManager = $storeManager; + $this->directiveFilter = $directiveFilter; ++ $this->templateFilter = $templateFilter ?? \Magento\Framework\App\ObjectManager::getInstance() ++ ->get(\Magento\PageBuilder\Model\Filter\Template::class); + } + + /** +@@ -61,7 +69,7 @@ class WidgetDirective implements \Magento\PageBuilder\Model\Stage\RendererInterf + try { + $result['content'] = $this->directiveFilter + ->setStoreId(Store::DEFAULT_STORE_ID) +- ->filter($params['directive']); ++ ->filter($this->templateFilter->filter($params['directive'])); + } catch (\Exception $e) { + $result['error'] = __($e->getMessage()); + } +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php b/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php +--- a/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php ++++ b/vendor/magento/module-page-builder/Plugin/Catalog/Block/Product/ProductsListPlugin.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + namespace Magento\PageBuilder\Plugin\Catalog\Block\Product; + + use Magento\PageBuilder\Model\Catalog\Sorting; ++use Magento\CatalogInventory\Helper\Stock; + + /** + * Catalog Products List widget block plugin +@@ -20,15 +21,23 @@ class ProductsListPlugin + */ + private $sorting; + ++ /** ++ * @var Stock ++ */ ++ private $stock; ++ + /** + * ProductsListPlugin constructor. + * + * @param Sorting $sorting ++ * @param Stock $stock + */ + public function __construct( +- Sorting $sorting ++ Sorting $sorting, ++ Stock $stock + ) { + $this->sorting = $sorting; ++ $this->stock = $stock; + } + + /** +@@ -42,7 +51,7 @@ class ProductsListPlugin + \Magento\CatalogWidget\Block\Product\ProductsList $subject, + \Magento\Catalog\Model\ResourceModel\Product\Collection $result + ) { +- ++ $this->stock->addIsInStockFilterToCollection($result); + $sortOption = $subject->getData('sort_order'); + if (isset($sortOption)) { + $sortedResult = $this->sorting->applySorting($sortOption, $result); +diff -Nuar a/vendor/magento/module-page-builder/Plugin/DesignLoader.php b/vendor/magento/module-page-builder/Plugin/DesignLoader.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/Plugin/DesignLoader.php +@@ -0,0 +1,98 @@ ++designLoader = $designLoader; ++ $this->messageManager = $messageManager; ++ $this->appState = $appState; ++ $this->preview = $preview; ++ } ++ ++ /** ++ * Before create load the design files ++ * ++ * @param \Magento\Catalog\Block\Product\ImageFactory $subject ++ * @param Product $product ++ * @param string $imageId ++ * @param array|null $attributes ++ * @throws \Exception ++ * ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforeCreate( ++ \Magento\Catalog\Block\Product\ImageFactory $subject, ++ Product $product, ++ string $imageId, ++ array $attributes = null ++ ) { ++ if ($this->preview->isPreviewMode()) { ++ $this->appState->emulateAreaCode( ++ $this->preview->getPreviewArea(), ++ [$this, 'loadDesignConfig'] ++ ); ++ } ++ } ++ ++ /** ++ * Load the design config ++ */ ++ public function loadDesignConfig() ++ { ++ try { ++ $this->designLoader->load(); ++ } catch (\Magento\Framework\Exception\LocalizedException $e) { ++ if ($e->getPrevious() instanceof \Magento\Framework\Config\Dom\ValidationException) { ++ /** @var MessageInterface $message */ ++ $message = $this->messageManager ++ ->createMessage(MessageInterface::TYPE_ERROR) ++ ->setText($e->getMessage()); ++ $this->messageManager->addUniqueMessages([$message]); ++ } ++ } ++ } ++} +diff -Nuar a/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php b/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php +--- a/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php ++++ b/vendor/magento/module-page-builder/Plugin/Filter/TemplatePlugin.php +@@ -7,56 +7,29 @@ declare(strict_types=1); + + namespace Magento\PageBuilder\Plugin\Filter; + ++use Magento\Store\Model\Store; ++ + /** + * Plugin to the template filter to process any background images added by Page Builder + */ + class TemplatePlugin + { +- const BACKGROUND_IMAGE_PATTERN = '/data-background-images/si'; ++ const BACKGROUND_IMAGE_PATTERN = '/data-background-images=(?:\'|"){.+}(?:\'|")/si'; + + const HTML_CONTENT_TYPE_PATTERN = '/data-content-type="html"/si'; + + /** +- * @var \Magento\Framework\View\ConfigInterface +- */ +- private $viewConfig; +- +- /** +- * @var \Psr\Log\LoggerInterface +- */ +- private $logger; +- +- /** +- * @var \DOMDocument +- */ +- private $domDocument; +- +- /** +- * @var \Magento\Framework\Math\Random +- */ +- private $mathRandom; +- +- /** +- * @var \Magento\Framework\Serialize\Serializer\Json ++ * @var \Magento\PageBuilder\Model\Filter\Template + */ +- private $json; ++ private $templateFilter; + + /** +- * @param \Psr\Log\LoggerInterface $logger +- * @param \Magento\Framework\View\ConfigInterface $viewConfig +- * @param \Magento\Framework\Math\Random $mathRandom +- * @param \Magento\Framework\Serialize\Serializer\Json $json ++ * @param \Magento\PageBuilder\Model\Filter\Template $templateFilter + */ + public function __construct( +- \Psr\Log\LoggerInterface $logger, +- \Magento\Framework\View\ConfigInterface $viewConfig, +- \Magento\Framework\Math\Random $mathRandom, +- \Magento\Framework\Serialize\Serializer\Json $json ++ \Magento\PageBuilder\Model\Filter\Template $templateFilter + ) { +- $this->logger = $logger; +- $this->viewConfig = $viewConfig; +- $this->mathRandom = $mathRandom; +- $this->json = $json; ++ $this->templateFilter = $templateFilter; + } + + /** +@@ -70,260 +43,6 @@ class TemplatePlugin + */ + public function afterFilter(\Magento\Framework\Filter\Template $subject, string $result) : string + { +- $this->domDocument = false; +- +- // Validate if the filtered result requires background image processing +- if (preg_match(self::BACKGROUND_IMAGE_PATTERN, $result)) { +- $document = $this->getDomDocument($result); +- $this->generateBackgroundImageStyles($document); +- } +- +- // Process any HTML content types, they need to be decoded on the front-end +- if (preg_match(self::HTML_CONTENT_TYPE_PATTERN, $result)) { +- $document = $this->getDomDocument($result); +- $uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document); +- } +- +- // If a document was retrieved we've modified the output so need to retrieve it from within the document +- if (isset($document)) { +- // Match the contents of the body from our generated document +- preg_match( +- '/(.+)<\/body><\/html>$/si', +- $document->saveHTML(), +- $matches +- ); +- +- if (!empty($matches)) { +- $docHtml = $matches[1]; +- +- if (isset($uniqueNodeNameToDecodedOuterHtmlMap)) { +- foreach ($uniqueNodeNameToDecodedOuterHtmlMap as $uniqueNodeName => $decodedOuterHtml) { +- $docHtml = str_replace( +- '<' . $uniqueNodeName . '>' . '', +- $decodedOuterHtml, +- $docHtml +- ); +- } +- } +- +- $result = $docHtml; +- } +- } +- +- return $result; +- } +- +- /** +- * Create a DOM document from a given string +- * +- * @param string $html +- * +- * @return \DOMDocument +- */ +- private function getDomDocument(string $html) : \DOMDocument +- { +- if (!$this->domDocument) { +- $this->domDocument = $this->createDomDocument($html); +- } +- +- return $this->domDocument; +- } +- +- /** +- * Create a DOMDocument from a string +- * +- * @param string $html +- * +- * @return \DOMDocument +- */ +- private function createDomDocument(string $html) : \DOMDocument +- { +- $domDocument = new \DOMDocument('1.0', 'UTF-8'); +- set_error_handler( +- function ($errorNumber, $errorString) { +- throw new \DOMException($errorString, $errorNumber); +- } +- ); +- $string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); +- try { +- libxml_use_internal_errors(true); +- $domDocument->loadHTML( +- '' . $string . '' +- ); +- libxml_clear_errors(); +- } catch (\Exception $e) { +- restore_error_handler(); +- $this->logger->critical($e); +- } +- restore_error_handler(); +- +- return $domDocument; +- } +- +- /** +- * Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement +- * +- * @param \DOMDocument $document +- * @return array - map of unique node name to decoded html +- */ +- private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array +- { +- $xpath = new \DOMXPath($document); +- +- // construct xpath query to fetch top-level ancestor html content type nodes +- /** @var $htmlContentTypeNodes \DOMNode[] */ +- $htmlContentTypeNodes = $xpath->query( +- '//*[@data-content-type="html" and not(@data-decoded="true")]' . +- '[not(ancestor::*[@data-content-type="html"])]' +- ); +- +- $uniqueNodeNameToDecodedOuterHtmlMap = []; +- +- foreach ($htmlContentTypeNodes as $htmlContentTypeNode) { +- // Set decoded attribute on all encoded html content types so we don't double decode; +- $htmlContentTypeNode->setAttribute('data-decoded', 'true'); +- +- // if nothing exists inside the node, continue +- if (!strlen(trim($htmlContentTypeNode->nodeValue))) { +- continue; +- } +- +- // clone html code content type to save reference to its attributes/outerHTML, which we are not going to +- // decode +- $clonedHtmlContentTypeNode = clone $htmlContentTypeNode; +- +- // clear inner contents of cloned node for replacement later with $decodedInnerHtml using sprintf; +- // we want to retain html content type node and avoid doing any manipulation on it +- $clonedHtmlContentTypeNode->nodeValue = '%s'; +- +- // remove potentially harmful attributes on html content type node itself +- while ($htmlContentTypeNode->attributes->length) { +- $htmlContentTypeNode->removeAttribute($htmlContentTypeNode->attributes->item(0)->name); +- } +- +- // decode outerHTML safely +- $preDecodedOuterHtml = $document->saveHTML($htmlContentTypeNode); +- +- // clear empty
wrapper around outerHTML to replace with $clonedHtmlContentTypeNode +- // phpcs:ignore Magento2.Functions.DiscouragedFunction +- $decodedInnerHtml = preg_replace('#^<[^>]*>|]*>$#', '', html_entity_decode($preDecodedOuterHtml)); +- +- // Use $clonedHtmlContentTypeNode's placeholder to inject decoded inner html +- $decodedOuterHtml = sprintf($document->saveHTML($clonedHtmlContentTypeNode), $decodedInnerHtml); +- +- // generate unique node name element to replace with decoded html contents at end of processing; +- // goal is to create a document as few times as possible to prevent inadvertent parsing of contents as html +- // by the dom library +- $uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS); +- +- $uniqueNode = new \DOMElement($uniqueNodeName); +- $htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode); +- +- $uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml; +- } +- +- return $uniqueNodeNameToDecodedOuterHtmlMap; +- } +- +- /** +- * Generate the CSS for any background images on the page +- * +- * @param \DOMDocument $document +- */ +- private function generateBackgroundImageStyles(\DOMDocument $document) : void +- { +- $xpath = new \DOMXPath($document); +- $nodes = $xpath->query('//*[@data-background-images]'); +- foreach ($nodes as $node) { +- /* @var \DOMElement $node */ +- $backgroundImages = $node->attributes->getNamedItem('data-background-images'); +- if ($backgroundImages->nodeValue !== '') { +- $elementClass = uniqid('background-image-'); +- // phpcs:ignore Magento2.Functions.DiscouragedFunction +- $images = $this->json->unserialize(stripslashes($backgroundImages->nodeValue)); +- if (count($images) > 0) { +- $style = $xpath->document->createElement( +- 'style', +- $this->generateCssFromImages($elementClass, $images) +- ); +- $style->setAttribute('type', 'text/css'); +- $node->parentNode->appendChild($style); +- +- // Append our new class to the DOM element +- $classes = ''; +- if ($node->attributes->getNamedItem('class')) { +- $classes = $node->attributes->getNamedItem('class')->nodeValue . ' '; +- } +- $node->setAttribute('class', $classes . $elementClass); +- } +- } +- } +- } +- +- /** +- * Generate CSS based on the images array from our attribute +- * +- * @param string $elementClass +- * @param array $images +- * +- * @return string +- */ +- private function generateCssFromImages(string $elementClass, array $images) : string +- { +- $css = []; +- if (isset($images['desktop_image'])) { +- $css['.' . $elementClass] = [ +- 'background-image' => 'url('https://github.com/.%20$images['desktop_image']%20.')', +- ]; +- } +- if (isset($images['mobile_image']) && $this->getMobileMediaQuery()) { +- $css[$this->getMobileMediaQuery()]['.' . $elementClass] = [ +- 'background-image' => 'url('https://github.com/.%20$images['mobile_image']%20.')', +- ]; +- } +- return $this->cssFromArray($css); +- } +- +- /** +- * Generate a CSS string from an array +- * +- * @param array $css +- * +- * @return string +- */ +- private function cssFromArray(array $css) : string +- { +- $output = ''; +- foreach ($css as $selector => $body) { +- if (is_array($body)) { +- $output .= $selector . ' {'; +- $output .= $this->cssFromArray($body); +- $output .= '}'; +- } else { +- $output .= $selector . ': ' . $body . ';'; +- } +- } +- return $output; +- } +- +- /** +- * Generate the mobile media query from view configuration +- * +- * @return null|string +- */ +- private function getMobileMediaQuery() : ?string +- { +- $breakpoints = $this->viewConfig->getViewConfig()->getVarValue( +- 'Magento_PageBuilder', +- 'breakpoints/mobile/conditions' +- ); +- if ($breakpoints && count($breakpoints) > 0) { +- $mobileBreakpoint = '@media only screen '; +- foreach ($breakpoints as $key => $value) { +- $mobileBreakpoint .= 'and (' . $key . ': ' . $value . ') '; +- } +- return rtrim($mobileBreakpoint); +- } +- return null; ++ return $this->templateFilter->filter($result); + } + } +diff -Nuar a/vendor/magento/module-page-builder/composer.json b/vendor/magento/module-page-builder/composer.json +--- a/vendor/magento/module-page-builder/composer.json ++++ b/vendor/magento/module-page-builder/composer.json +@@ -9,6 +9,7 @@ + "magento/module-backend": "~101.0.3", + "magento/module-cms": "*", + "magento/module-catalog": "*", ++ "magento/module-catalog-inventory": "~100.3.3", + "magento/module-config": "~101.1.3", + "magento/module-ui": "*", + "magento/module-variable": "*", +diff -Nuar a/vendor/magento/module-page-builder/etc/adminhtml/di.xml b/vendor/magento/module-page-builder/etc/adminhtml/di.xml +--- a/vendor/magento/module-page-builder/etc/adminhtml/di.xml ++++ b/vendor/magento/module-page-builder/etc/adminhtml/di.xml +@@ -6,6 +6,9 @@ + */ + --> + ++ ++ ++ + + + ns = pagebuilder_modal_form, index = modal +diff -Nuar a/vendor/magento/module-page-builder/etc/module.xml b/vendor/magento/module-page-builder/etc/module.xml +--- a/vendor/magento/module-page-builder/etc/module.xml ++++ b/vendor/magento/module-page-builder/etc/module.xml +@@ -6,11 +6,12 @@ + */ + --> + +- ++ + + + + ++ + + + +diff -Nuar a/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml b/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-page-builder/view/adminhtml/layout/pagebuilder_stage_preview.xml +@@ -0,0 +1,14 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ From 9524c874d2228ef29a630312f5b70cb2cc0f048d Mon Sep 17 00:00:00 2001 From: Billy Gilbert Date: Wed, 8 Jan 2020 14:30:17 -0600 Subject: [PATCH 013/157] MAGECLOUD-4980: Pagebuilder patches fail CE functional tests (#14) --- patches.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/patches.json b/patches.json index 34dbd5f4..9a906311 100644 --- a/patches.json +++ b/patches.json @@ -202,12 +202,6 @@ "2.3.1": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch", ">=2.3.2 <2.3.4": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch" }, - "Fix pagebuilder module": { - "2.3.1": "PB-319__fix_pagebuilder_module__2.3.1.patch", - "2.3.2": "PB-320__fix_pagebuilder_module__2.3.2.patch", - ">=2.3.2-p1 <2.3.3": "PB-322__fix_pagebuilder_module__2.3.2-p1.patch", - "2.3.3": "PB-323__fix_pagebuilder_module__2.3.3.patch" - }, "Fix XSS in order history": { "2.2.0 - 2.2.6": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.0.patch", "2.2.7 - 2.2.8": "PRODSECBUG-2233__fix_xss_in_order_history__2.2.7.patch", @@ -287,5 +281,13 @@ "Fix wrong namespace": { "3.2.0": "MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch" } + }, + "magento/magento2-ee-base": { + "Fix pagebuilder module": { + "2.3.1": "PB-319__fix_pagebuilder_module__2.3.1.patch", + "2.3.2": "PB-320__fix_pagebuilder_module__2.3.2.patch", + ">=2.3.2-p1 <2.3.3": "PB-322__fix_pagebuilder_module__2.3.2-p1.patch", + "2.3.3": "PB-323__fix_pagebuilder_module__2.3.3.patch" + } } } From 0a22b2aaf0e7a3aa15913e0e3d0738107eae427e Mon Sep 17 00:00:00 2001 From: Ben Batschelet Date: Thu, 9 Jan 2020 15:29:22 -0600 Subject: [PATCH 014/157] Add v2.8 of Symfony packages --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index fbf184e0..52129763 100644 --- a/composer.json +++ b/composer.json @@ -8,9 +8,9 @@ "php": "^7.0", "ext-json": "*", "composer/composer": "@stable", - "symfony/config": "^3.3||^4.3", + "symfony/config": "^2.8||^3.3||^4.3", "symfony/console": "^2.6||^4.0", - "symfony/dependency-injection": "^3.3||^4.3", + "symfony/dependency-injection": "^2.8||^3.3||^4.3", "symfony/process": "^2.1||^4.1" }, "conflict": { From b5937b7cf92ec40695c94419c08005dfa49747e3 Mon Sep 17 00:00:00 2001 From: Billy Gilbert Date: Fri, 10 Jan 2020 09:41:43 -0600 Subject: [PATCH 015/157] MAGECLOUD-4982: De-composed magento-cloud-patches fail to apply locally --- patches.json | 1 - ...ECLOUD-1601__trim_static_content_path__2.1.4.patch | 11 ----------- 2 files changed, 12 deletions(-) delete mode 100644 patches/MAGECLOUD-1601__trim_static_content_path__2.1.4.patch diff --git a/patches.json b/patches.json index 9a906311..3365a43c 100644 --- a/patches.json +++ b/patches.json @@ -43,7 +43,6 @@ "2.2.0": "MDVA-8695__properly_encode_characters_in_emails__2.1.4.patch" }, "Enable SCD on demand in production": { - ">=2.1.4": "MAGECLOUD-1601__trim_static_content_path__2.1.4.patch", "~2.1.4": "MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.1.4.patch", "2.2.0 - 2.2.3": "MAGECLOUD-1601__configure_scd_on_demand_for_cloud__2.2.0.patch" }, diff --git a/patches/MAGECLOUD-1601__trim_static_content_path__2.1.4.patch b/patches/MAGECLOUD-1601__trim_static_content_path__2.1.4.patch deleted file mode 100644 index 759df9e8..00000000 --- a/patches/MAGECLOUD-1601__trim_static_content_path__2.1.4.patch +++ /dev/null @@ -1,11 +0,0 @@ -diff -Naur a/pub/front-static.php b/pub/front-static.php ---- a/pub/front-static.php -+++ b/pub/front-static.php -@@ -6,6 +6,7 @@ - * See COPYING.txt for license details. - */ - -+$_GET['resource'] = preg_replace('/^(\/static\/)(version(\d+)?\/)?|(\?.*)/', '', $_SERVER['REQUEST_URI'] ?: ''); - require realpath(__DIR__) . '/../app/bootstrap.php'; - $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); - /** @var \Magento\Framework\App\StaticResource $app */ From db908015c6acde70879f623febb333efc636f823 Mon Sep 17 00:00:00 2001 From: Ben Batschelet Date: Fri, 10 Jan 2020 10:49:17 -0600 Subject: [PATCH 016/157] Minimize dependency set a bit --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 52129763..fc559096 100644 --- a/composer.json +++ b/composer.json @@ -8,9 +8,9 @@ "php": "^7.0", "ext-json": "*", "composer/composer": "@stable", - "symfony/config": "^2.8||^3.3||^4.3", - "symfony/console": "^2.6||^4.0", - "symfony/dependency-injection": "^2.8||^3.3||^4.3", + "symfony/config": "^4.4", + "symfony/console": "^2.8||^4.0", + "symfony/dependency-injection": "^2.8||^4.3", "symfony/process": "^2.1||^4.1" }, "conflict": { From f7806a678dd0931174641d30c7c851be6dac71dd Mon Sep 17 00:00:00 2001 From: Ben Batschelet Date: Mon, 13 Jan 2020 12:19:04 -0600 Subject: [PATCH 017/157] Add legacy version of symfony/config for PHP 7.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fc559096..38a3a282 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "php": "^7.0", "ext-json": "*", "composer/composer": "@stable", - "symfony/config": "^4.4", + "symfony/config": "^2.8||^4.4", "symfony/console": "^2.8||^4.0", "symfony/dependency-injection": "^2.8||^4.3", "symfony/process": "^2.1||^4.1" From 7db534026a0d75419225ccca521d4b6f96761db1 Mon Sep 17 00:00:00 2001 From: Ben Batschelet Date: Wed, 15 Jan 2020 15:05:23 -0600 Subject: [PATCH 018/157] Add support for ^3.3 version of symfony/dependency-injection --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 38a3a282..08dc3033 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "composer/composer": "@stable", "symfony/config": "^2.8||^4.4", "symfony/console": "^2.8||^4.0", - "symfony/dependency-injection": "^2.8||^4.3", + "symfony/dependency-injection": "^3.3||^4.3", "symfony/process": "^2.1||^4.1" }, "conflict": { From 06f0896939ef0a2b45ba95a5a87cdf637028ba58 Mon Sep 17 00:00:00 2001 From: Ben Batschelet Date: Thu, 16 Jan 2020 10:23:21 -0600 Subject: [PATCH 019/157] Allow symfony/config 3.3 instead of 2.8 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 08dc3033..4f9637eb 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "php": "^7.0", "ext-json": "*", "composer/composer": "@stable", - "symfony/config": "^2.8||^4.4", + "symfony/config": "^3.3||^4.4", "symfony/console": "^2.8||^4.0", "symfony/dependency-injection": "^3.3||^4.3", "symfony/process": "^2.1||^4.1" From 5b5594db58c4f9e4a19c1614b72b1aa3fec24803 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov Date: Fri, 24 Jan 2020 11:44:28 -0600 Subject: [PATCH 020/157] MAGECLOUD-4530: Create patch for deadlocks related to crons / consumers (#17) --- patches.json | 9 + ...ocks_and_improve_job_locking__2.2.10.patch | 700 +++++++++++++++ ...locks_and_improve_job_locking__2.2.5.patch | 794 ++++++++++++++++++ ...locks_and_improve_job_locking__2.2.9.patch | 752 +++++++++++++++++ ...locks_and_improve_job_locking__2.3.0.patch | 623 ++++++++++++++ ...locks_and_improve_job_locking__2.3.1.patch | 622 ++++++++++++++ ...locks_and_improve_job_locking__2.3.2.patch | 578 +++++++++++++ ...locks_and_improve_job_locking__2.3.3.patch | 527 ++++++++++++ 8 files changed, 4605 insertions(+) create mode 100644 patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.10.patch create mode 100644 patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.5.patch create mode 100644 patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.9.patch create mode 100644 patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.0.patch create mode 100644 patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.1.patch create mode 100644 patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.2.patch create mode 100644 patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.3.patch diff --git a/patches.json b/patches.json index 3365a43c..a64fce05 100644 --- a/patches.json +++ b/patches.json @@ -59,6 +59,15 @@ "2.2.2 - 2.2.3": "MAGECLOUD-1607__overhaul_cron_implementation__2.2.2.patch", "2.2.4": "MAGECLOUD-1607__overhaul_cron_implementation__2.2.4.patch" }, + "Fix cron deadlocks and improve cron locking": { + "2.2.5 - 2.2.8": "MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.5.patch", + "2.2.9": "MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.9.patch", + ">=2.2.10 <2.3.0": "MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.10.patch", + "2.3.0": "MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.0.patch", + "2.3.1": "MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.1.patch", + ">=2.3.2 <2.3.3": "MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.2.patch", + ">=2.3.3 <2.3.5": "MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.3.patch" + }, "Add Zookeeper and flock locks": { "2.2.5 - 2.2.8": "MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.2.5.patch", "2.3.0 - 2.3.1": "MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch" diff --git a/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.10.patch b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.10.patch new file mode 100644 index 00000000..4556b0bf --- /dev/null +++ b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.10.patch @@ -0,0 +1,700 @@ +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrier.php b/vendor/magento/module-cron/Model/DeadlockRetrier.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrier.php +@@ -0,0 +1,39 @@ ++getTransactionLevel() !== 0) { ++ return $callback(); ++ } ++ ++ for ($retries = self::MAX_RETRIES - 1; $retries > 0; $retries--) { ++ try { ++ return $callback(); ++ } catch (DeadlockException $e) { ++ continue; ++ } ++ } ++ ++ return $callback(); ++ } ++} +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +@@ -0,0 +1,33 @@ ++timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); ++ $this->retrier = $retrier ?: ObjectManager::getInstance()->get(DeadlockRetrierInterface::class); + } + + /** +- * @return void ++ * @inheritdoc + */ + public function _construct() + { +@@ -88,6 +96,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Set cron expression. ++ * + * @param string $expr + * @return $this + * @throws \Magento\Framework\Exception\CronException +@@ -95,7 +105,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + public function setCronExpr($expr) + { + $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); +- if (sizeof($e) < 5 || sizeof($e) > 6) { ++ if (count($e) < 5 || count($e) > 6) { + throw new CronException(__('Invalid cron expression: %1', $expr)); + } + +@@ -104,7 +114,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Checks the observer's cron expression against time ++ * Checks the observer's cron expression against time. + * + * Supports $this->setCronExpr('* 0-5,10-59/5 2-10,15-25 january-june/2 mon-fri') + * +@@ -137,6 +147,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Match cron expression. ++ * + * @param string $expr + * @param int $num + * @return bool +@@ -164,7 +176,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + // handle modulus + if (strpos($expr, '/') !== false) { + $e = explode('/', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'match/modulus\': %1', $expr)); + } + if (!is_numeric($e[1])) { +@@ -183,7 +195,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } elseif (strpos($expr, '-') !== false) { + // handle range + $e = explode('-', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'from-to\' structure: %1', $expr)); + } + +@@ -203,6 +215,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Get number of a month. ++ * + * @param int|string $value + * @return bool|int|string + */ +@@ -245,21 +259,42 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Lock the cron job so no other scheduled instances run simultaneously. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. + * +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING +- * and no other jobs of the same code are currently in STATUS_RUNNING. + * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobUniqueStatusAtomic( +- $this->getId(), +- self::STATUS_RUNNING, +- self::STATUS_PENDING +- )) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_getResource(); ++ ++ // Change statuses from running to error for terminated jobs ++ $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->getConnection()->update( ++ $scheduleResource->getTable('cron_schedule'), ++ ['status' => self::STATUS_ERROR], ++ ['job_code = ?' => $this->getJobCode(), 'status = ?' => self::STATUS_RUNNING] ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ // Change status from pending to running for ran jobs ++ $result = $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->trySetJobStatusAtomic( ++ $this->getId(), ++ self::STATUS_RUNNING, ++ self::STATUS_PENDING ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ if ($result) { + $this->setStatus(self::STATUS_RUNNING); + return true; + } +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -3,6 +3,7 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + /** + * Handling cron jobs + */ +@@ -14,9 +15,13 @@ use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; + use Magento\Framework\Profiler\Driver\Standard\Stat; + use Magento\Framework\Profiler\Driver\Standard\StatFactory; ++use Magento\Cron\Model\DeadlockRetrierInterface; + + /** ++ * The observer for processing cron jobs. ++ * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.TooManyFields) + */ + class ProcessCronQueueObserver implements ObserverInterface + { +@@ -60,12 +65,17 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * How long to wait for cron group to become unlocked + */ +- const LOCK_TIMEOUT = 5; ++ const LOCK_TIMEOUT = 60; + + /** + * Static lock prefix for cron group locking + */ +- const LOCK_PREFIX = 'CRON_GROUP_'; ++ const LOCK_PREFIX = 'CRON_'; ++ ++ /** ++ * Max retries for acquire locks for cron jobs ++ */ ++ const MAX_RETRIES = 5; + + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection +@@ -142,6 +152,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private $statProfiler; + ++ /** ++ * @var \Magento\Framework\Event\ManagerInterface ++ */ ++ private $eventManager; ++ ++ /** ++ * @var DeadlockRetrierInterface ++ */ ++ private $retrier; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory +@@ -153,8 +173,11 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime + * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory + * @param \Psr\Log\LoggerInterface $logger +- * @param \Magento\Framework\App\State $state ++ * @param State $state + * @param StatFactory $statFactory ++ * @param \Magento\Framework\Lock\LockManagerInterface $lockManager ++ * @param \Magento\Framework\Event\ManagerInterface $eventManager ++ * @param DeadlockRetrierInterface $retrier + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -170,7 +193,9 @@ class ProcessCronQueueObserver implements ObserverInterface + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\App\State $state, + StatFactory $statFactory, +- \Magento\Framework\Lock\LockManagerInterface $lockManager ++ \Magento\Framework\Lock\LockManagerInterface $lockManager, ++ \Magento\Framework\Event\ManagerInterface $eventManager, ++ DeadlockRetrierInterface $retrier + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -185,6 +210,8 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; ++ $this->eventManager = $eventManager; ++ $this->retrier = $retrier; + } + + /** +@@ -232,12 +259,12 @@ class ProcessCronQueueObserver implements ObserverInterface + + $this->lockGroup( + $groupId, +- function ($groupId) use ($currentTime, $jobsRoot) { ++ function ($groupId) use ($currentTime) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); +- $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + ); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -306,9 +333,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())); ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + + $this->startProfiling(); ++ $this->eventManager->dispatch('cron_job_run', ['job_name' => 'cron/' . $groupId . '/' . $jobCode]); ++ + try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + //phpcs:ignore Magento2.Functions.DiscouragedFunction +@@ -387,8 +422,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Return job collection from data base with status 'pending' ++ * Return job collection from data base with status 'pending'. + * ++ * @param string $groupId + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ + private function getPendingSchedules($groupId) +@@ -400,28 +436,6 @@ class ProcessCronQueueObserver implements ObserverInterface + return $pendingJobs; + } + +- /** +- * Return job collection from database with status 'pending', 'running' or 'success' +- * +- * @param string $groupId +- * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection +- */ +- private function getNonExitedSchedules($groupId) +- { +- $jobs = $this->_config->getJobs(); +- $pendingJobs = $this->_scheduleFactory->create()->getCollection(); +- $pendingJobs->addFieldToFilter( +- 'status', +- [ +- 'in' => [ +- Schedule::STATUS_PENDING, Schedule::STATUS_RUNNING, Schedule::STATUS_SUCCESS +- ] +- ] +- ); +- $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); +- return $pendingJobs; +- } +- + /** + * Generate cron schedule + * +@@ -453,7 +467,7 @@ class ProcessCronQueueObserver implements ObserverInterface + null + ); + +- $schedules = $this->getNonExitedSchedules($groupId); ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -495,8 +509,8 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * Clean expired jobs + * +- * @param $groupId +- * @param $currentTime ++ * @param string $groupId ++ * @param int $currentTime + * @return void + */ + private function cleanupJobs($groupId, $currentTime) +@@ -527,16 +541,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ]; + + $jobs = $this->_config->getJobs()[$groupId]; +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { +- $count += $connection->delete( +- $scheduleResource->getMainTable(), ++ $count += $this->cleanup( + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), +- 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ 'created_at < ?' => $this->_scheduleFactory ++ ->create() ++ ->getResource() ++ ->getConnection() ++ ->formatDate($currentTime - $time) + ] + ); + } +@@ -547,6 +562,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get config of schedule. ++ * + * @param array $jobConfig + * @return mixed + */ +@@ -561,6 +578,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Save a schedule of cron job. ++ * + * @param string $jobCode + * @param string $cronExpression + * @param int $timeInterval +@@ -593,6 +612,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Create a schedule of cron job. ++ * + * @param string $jobCode + * @param string $cronExpression + * @param int $time +@@ -611,6 +632,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get time interval for scheduling. ++ * + * @param string $groupId + * @return int + */ +@@ -623,8 +646,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Clean up scheduled jobs that are disabled in the configuration +- * This can happen when you turn off a cron job in the config and flush the cache ++ * Clean up scheduled jobs that are disabled in the configuration. ++ * ++ * This can happen when you turn off a cron job in the config and flush the cache. + * + * @param string $groupId + * @return void +@@ -641,9 +665,7 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + if (count($jobsToCleanup) > 0) { +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $count = $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $count = $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, +@@ -655,6 +677,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get cron expression of cron job. ++ * + * @param array $jobConfig + * @return null|string + */ +@@ -674,18 +698,16 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Clean up scheduled jobs that do not match their cron expression anymore +- * This can happen when you change the cron expression and flush the cache ++ * Clean up scheduled jobs that do not match their cron expression anymore. ++ * ++ * This can happen when you change the cron expression and flush the cache. + * + * @return $this + */ + private function cleanupScheduleMismatches() + { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code = ?' => $jobCode, +@@ -693,13 +715,15 @@ class ProcessCronQueueObserver implements ObserverInterface + ] + ); + } ++ + return $this; + } + + /** +- * Get CronGroup Configuration Value ++ * Get CronGroup Configuration Value. + * +- * @param $groupId ++ * @param string $groupId ++ * @param string $path + * @return int + */ + private function getCronGroupConfigurationValue($groupId, $path) +@@ -711,9 +735,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Is Group In Filter ++ * Is Group In Filter. + * +- * @param $groupId ++ * @param string $groupId + * @return bool + */ + private function isGroupInFilter($groupId): bool +@@ -723,17 +747,17 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Process pending jobs ++ * Process pending jobs. + * +- * @param $groupId +- * @param $jobsRoot +- * @param $currentTime ++ * @param string $groupId ++ * @param array $jobsRoot ++ * @param int $currentTime + */ + private function processPendingJobs($groupId, $jobsRoot, $currentTime) + { + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); +- /** @var \Magento\Cron\Model\Schedule $schedule */ ++ /** @var Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run +@@ -749,26 +773,59 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $this->processError($schedule, $e); +- } ++ $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } +- $schedule->save(); ++ ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + } + } + + /** ++ * Try to acquire lock for cron job and try to run this job. ++ * ++ * @param int $scheduledTime ++ * @param int $currentTime ++ * @param string[] $jobConfig ++ * @param Schedule $schedule ++ * @param string $groupId ++ */ ++ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) ++ { ++ // use sha1 to limit length ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); ++ ++ try { ++ for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { ++ if ($this->lockManager->lock($lockName, 0) && $schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ break; ++ } ++ $this->logger->warning("Could not acquire lock for cron job: {$schedule->getJobCode()}"); ++ } ++ } catch (\Exception $e) { ++ $this->processError($schedule, $e); ++ } finally { ++ $this->lockManager->unlock($lockName); ++ } ++ } ++ ++ /** ++ * Process error messages. ++ * + * @param Schedule $schedule + * @param \Exception $exception + * @return void + */ +- private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) ++ private function processError(Schedule $schedule, \Exception $exception) + { + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +@@ -780,4 +837,26 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->logger->info($schedule->getMessages()); + } + } ++ ++ /** ++ * Clean up schedule ++ * ++ * @param mixed $where ++ * @return int ++ */ ++ private function cleanup($where = ''): int ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ ++ return (int) $this->retrier->execute( ++ function () use ($scheduleResource, $where) { ++ return $scheduleResource->getConnection()->delete( ++ $scheduleResource->getTable('cron_schedule'), ++ $where ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ } + } +diff -Naur a/vendor/magento/module-cron/etc/di.xml b/vendor/magento/module-cron/etc/di.xml +--- a/vendor/magento/module-cron/etc/di.xml ++++ b/vendor/magento/module-cron/etc/di.xml +@@ -76,4 +76,5 @@ + + + ++ + diff --git a/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.5.patch b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.5.patch new file mode 100644 index 00000000..26b455b4 --- /dev/null +++ b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.5.patch @@ -0,0 +1,794 @@ +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrier.php b/vendor/magento/module-cron/Model/DeadlockRetrier.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrier.php +@@ -0,0 +1,39 @@ ++getTransactionLevel() !== 0) { ++ return $callback(); ++ } ++ ++ for ($retries = self::MAX_RETRIES - 1; $retries > 0; $retries--) { ++ try { ++ return $callback(); ++ } catch (DeadlockException $e) { ++ continue; ++ } ++ } ++ ++ return $callback(); ++ } ++} +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +@@ -0,0 +1,33 @@ ++timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); ++ $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); ++ $this->retrier = $retrier ?: ObjectManager::getInstance()->get(DeadlockRetrierInterface::class); + } + + /** +- * @return void ++ * @inheritdoc + */ + public function _construct() + { +@@ -79,6 +96,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Set cron expression. ++ * + * @param string $expr + * @return $this + * @throws \Magento\Framework\Exception\CronException +@@ -86,7 +105,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + public function setCronExpr($expr) + { + $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); +- if (sizeof($e) < 5 || sizeof($e) > 6) { ++ if (count($e) < 5 || count($e) > 6) { + throw new CronException(__('Invalid cron expression: %1', $expr)); + } + +@@ -95,7 +114,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Checks the observer's cron expression against time ++ * Checks the observer's cron expression against time. + * + * Supports $this->setCronExpr('* 0-5,10-59/5 2-10,15-25 january-june/2 mon-fri') + * +@@ -109,22 +128,27 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + if (!$e || !$time) { + return false; + } ++ $configTimeZone = $this->timezoneConverter->getConfigTimezone(); ++ $storeDateTime = $this->dateTimeFactory->create(null, new \DateTimeZone($configTimeZone)); + if (!is_numeric($time)) { + //convert time from UTC to admin store timezone + //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone +- $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); +- $time = strtotime($time); ++ $dateTimeUtc = $this->dateTimeFactory->create($time); ++ $time = $dateTimeUtc->getTimestamp(); + } +- $match = $this->matchCronExpression($e[0], strftime('%M', $time)) +- && $this->matchCronExpression($e[1], strftime('%H', $time)) +- && $this->matchCronExpression($e[2], strftime('%d', $time)) +- && $this->matchCronExpression($e[3], strftime('%m', $time)) +- && $this->matchCronExpression($e[4], strftime('%w', $time)); ++ $time = $storeDateTime->setTimestamp($time); ++ $match = $this->matchCronExpression($e[0], $time->format('i')) ++ && $this->matchCronExpression($e[1], $time->format('H')) ++ && $this->matchCronExpression($e[2], $time->format('d')) ++ && $this->matchCronExpression($e[3], $time->format('m')) ++ && $this->matchCronExpression($e[4], $time->format('w')); + + return $match; + } + + /** ++ * Match cron expression. ++ * + * @param string $expr + * @param int $num + * @return bool +@@ -152,7 +176,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + // handle modulus + if (strpos($expr, '/') !== false) { + $e = explode('/', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'match/modulus\': %1', $expr)); + } + if (!is_numeric($e[1])) { +@@ -171,7 +195,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } elseif (strpos($expr, '-') !== false) { + // handle range + $e = explode('-', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'from-to\' structure: %1', $expr)); + } + +@@ -191,6 +215,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Get number of a month. ++ * + * @param int|string $value + * @return bool|int|string + */ +@@ -233,21 +259,42 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Lock the cron job so no other scheduled instances run simultaneously. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. + * +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING +- * and no other jobs of the same code are currently in STATUS_RUNNING. + * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobUniqueStatusAtomic( +- $this->getId(), +- self::STATUS_RUNNING, +- self::STATUS_PENDING +- )) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_getResource(); ++ ++ // Change statuses from running to error for terminated jobs ++ $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->getConnection()->update( ++ $scheduleResource->getTable('cron_schedule'), ++ ['status' => self::STATUS_ERROR], ++ ['job_code = ?' => $this->getJobCode(), 'status = ?' => self::STATUS_RUNNING] ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ // Change status from pending to running for ran jobs ++ $result = $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->trySetJobStatusAtomic( ++ $this->getId(), ++ self::STATUS_RUNNING, ++ self::STATUS_PENDING ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ if ($result) { + $this->setStatus(self::STATUS_RUNNING); + return true; + } +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -9,15 +9,19 @@ + */ + namespace Magento\Cron\Observer; + ++use Magento\Cron\Model\Schedule; + use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; +-use \Magento\Cron\Model\Schedule; + use Magento\Framework\Profiler\Driver\Standard\Stat; + use Magento\Framework\Profiler\Driver\Standard\StatFactory; ++use Magento\Cron\Model\DeadlockRetrierInterface; + + /** ++ * The observer for processing cron jobs. ++ * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.TooManyFields) + */ + class ProcessCronQueueObserver implements ObserverInterface + { +@@ -61,12 +65,17 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * How long to wait for cron group to become unlocked + */ +- const LOCK_TIMEOUT = 5; ++ const LOCK_TIMEOUT = 60; + + /** + * Static lock prefix for cron group locking + */ +- const LOCK_PREFIX = 'CRON_GROUP_'; ++ const LOCK_PREFIX = 'CRON_'; ++ ++ /** ++ * Max retries for acquire locks for cron jobs ++ */ ++ const MAX_RETRIES = 5; + + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection +@@ -143,6 +152,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private $statProfiler; + ++ /** ++ * @var \Magento\Framework\Event\ManagerInterface ++ */ ++ private $eventManager; ++ ++ /** ++ * @var DeadlockRetrierInterface ++ */ ++ private $retrier; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory +@@ -154,8 +173,11 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime + * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory + * @param \Psr\Log\LoggerInterface $logger +- * @param \Magento\Framework\App\State $state ++ * @param State $state + * @param StatFactory $statFactory ++ * @param \Magento\Framework\Lock\LockManagerInterface $lockManager ++ * @param \Magento\Framework\Event\ManagerInterface $eventManager ++ * @param DeadlockRetrierInterface $retrier + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -171,7 +193,9 @@ class ProcessCronQueueObserver implements ObserverInterface + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\App\State $state, + StatFactory $statFactory, +- \Magento\Framework\Lock\LockManagerInterface $lockManager ++ \Magento\Framework\Lock\LockManagerInterface $lockManager, ++ \Magento\Framework\Event\ManagerInterface $eventManager, ++ DeadlockRetrierInterface $retrier + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -186,6 +210,8 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; ++ $this->eventManager = $eventManager; ++ $this->retrier = $retrier; + } + + /** +@@ -201,7 +227,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- + $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); + // sort jobs groups to start from used in separated process +@@ -234,12 +259,12 @@ class ProcessCronQueueObserver implements ObserverInterface + + $this->lockGroup( + $groupId, +- function ($groupId) use ($currentTime, $jobsRoot) { ++ function ($groupId) use ($currentTime) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); +- $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + ); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -255,7 +280,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function lockGroup($groupId, callable $callback) + { +- + if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { + $this->logger->warning( + sprintf( +@@ -290,36 +314,50 @@ class ProcessCronQueueObserver implements ObserverInterface + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); +- throw new \Exception('No callbacks found'); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow ++ throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode)); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())); ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + + $this->startProfiling(); ++ $this->eventManager->dispatch('cron_job_run', ['job_name' => 'cron/' . $groupId . '/' . $jobCode]); ++ + try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); ++ //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array($callback, [$schedule]); + } catch (\Throwable $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); +- $this->logger->error(sprintf( +- 'Cron Job %s has an error: %s. Statistics: %s', +- $jobCode, +- $e->getMessage(), +- $this->getProfilingStat() +- )); ++ $this->logger->error( ++ sprintf( ++ 'Cron Job %s has an error: %s. Statistics: %s', ++ $jobCode, ++ $e->getMessage(), ++ $this->getProfilingStat() ++ ) ++ ); + if (!$e instanceof \Exception) { + $e = new \RuntimeException( + 'Error when running a cron job', +@@ -332,16 +370,22 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->stopProfiling(); + } + +- $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( +- '%Y-%m-%d %H:%M:%S', +- $this->dateTime->gmtTimestamp() +- )); ++ $schedule->setStatus( ++ Schedule::STATUS_SUCCESS ++ )->setFinishedAt( ++ strftime( ++ '%Y-%m-%d %H:%M:%S', ++ $this->dateTime->gmtTimestamp() ++ ) ++ ); + +- $this->logger->info(sprintf( +- 'Cron Job %s is successfully finished. Statistics: %s', +- $jobCode, +- $this->getProfilingStat() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() ++ ) ++ ); + } + + /** +@@ -378,8 +422,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Return job collection from data base with status 'pending' ++ * Return job collection from data base with status 'pending'. + * ++ * @param string $groupId + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ + private function getPendingSchedules($groupId) +@@ -464,8 +509,8 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * Clean expired jobs + * +- * @param $groupId +- * @param $currentTime ++ * @param string $groupId ++ * @param int $currentTime + * @return void + */ + private function cleanupJobs($groupId, $currentTime) +@@ -496,16 +541,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ]; + + $jobs = $this->_config->getJobs()[$groupId]; +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { +- $count += $connection->delete( +- $scheduleResource->getMainTable(), ++ $count += $this->cleanup( + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), +- 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ 'created_at < ?' => $this->_scheduleFactory ++ ->create() ++ ->getResource() ++ ->getConnection() ++ ->formatDate($currentTime - $time) + ] + ); + } +@@ -516,6 +562,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get config of schedule. ++ * + * @param array $jobConfig + * @return mixed + */ +@@ -530,6 +578,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Save a schedule of cron job. ++ * + * @param string $jobCode + * @param string $cronExpression + * @param int $timeInterval +@@ -562,6 +612,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Create a schedule of cron job. ++ * + * @param string $jobCode + * @param string $cronExpression + * @param int $time +@@ -580,6 +632,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get time interval for scheduling. ++ * + * @param string $groupId + * @return int + */ +@@ -592,8 +646,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Clean up scheduled jobs that are disabled in the configuration +- * This can happen when you turn off a cron job in the config and flush the cache ++ * Clean up scheduled jobs that are disabled in the configuration. ++ * ++ * This can happen when you turn off a cron job in the config and flush the cache. + * + * @param string $groupId + * @return void +@@ -610,9 +665,7 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + if (count($jobsToCleanup) > 0) { +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $count = $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $count = $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, +@@ -624,6 +677,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get cron expression of cron job. ++ * + * @param array $jobConfig + * @return null|string + */ +@@ -643,29 +698,32 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Clean up scheduled jobs that do not match their cron expression anymore +- * This can happen when you change the cron expression and flush the cache ++ * Clean up scheduled jobs that do not match their cron expression anymore. ++ * ++ * This can happen when you change the cron expression and flush the cache. + * + * @return $this + */ + private function cleanupScheduleMismatches() + { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status = ?' => Schedule::STATUS_PENDING, +- 'job_code = ?' => $jobCode, +- 'scheduled_at in (?)' => $scheduledAtList, +- ]); ++ $this->cleanup( ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ] ++ ); + } ++ + return $this; + } + + /** +- * Get CronGroup Configuration Value ++ * Get CronGroup Configuration Value. + * +- * @param $groupId ++ * @param string $groupId ++ * @param string $path + * @return int + */ + private function getCronGroupConfigurationValue($groupId, $path) +@@ -677,9 +735,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Is Group In Filter ++ * Is Group In Filter. + * +- * @param $groupId ++ * @param string $groupId + * @return bool + */ + private function isGroupInFilter($groupId): bool +@@ -689,17 +747,17 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Process pending jobs ++ * Process pending jobs. + * +- * @param $groupId +- * @param $jobsRoot +- * @param $currentTime ++ * @param string $groupId ++ * @param array $jobsRoot ++ * @param int $currentTime + */ + private function processPendingJobs($groupId, $jobsRoot, $currentTime) + { + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); +- /** @var \Magento\Cron\Model\Schedule $schedule */ ++ /** @var Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run +@@ -715,26 +773,59 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $this->processError($schedule, $e); +- } ++ $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } +- $schedule->save(); ++ ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + } + } + + /** ++ * Try to acquire lock for cron job and try to run this job. ++ * ++ * @param int $scheduledTime ++ * @param int $currentTime ++ * @param string[] $jobConfig ++ * @param Schedule $schedule ++ * @param string $groupId ++ */ ++ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) ++ { ++ // use sha1 to limit length ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); ++ ++ try { ++ for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { ++ if ($this->lockManager->lock($lockName, 0) && $schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ break; ++ } ++ $this->logger->warning("Could not acquire lock for cron job: {$schedule->getJobCode()}"); ++ } ++ } catch (\Exception $e) { ++ $this->processError($schedule, $e); ++ } finally { ++ $this->lockManager->unlock($lockName); ++ } ++ } ++ ++ /** ++ * Process error messages. ++ * + * @param Schedule $schedule + * @param \Exception $exception + * @return void + */ +- private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) ++ private function processError(Schedule $schedule, \Exception $exception) + { + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +@@ -746,4 +837,26 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->logger->info($schedule->getMessages()); + } + } ++ ++ /** ++ * Clean up schedule ++ * ++ * @param mixed $where ++ * @return int ++ */ ++ private function cleanup($where = ''): int ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ ++ return (int) $this->retrier->execute( ++ function () use ($scheduleResource, $where) { ++ return $scheduleResource->getConnection()->delete( ++ $scheduleResource->getTable('cron_schedule'), ++ $where ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ } + } +diff -Naur a/vendor/magento/module-cron/etc/di.xml b/vendor/magento/module-cron/etc/di.xml +--- a/vendor/magento/module-cron/etc/di.xml ++++ b/vendor/magento/module-cron/etc/di.xml +@@ -76,4 +76,5 @@ + + + ++ + diff --git a/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.9.patch b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.9.patch new file mode 100644 index 00000000..637ea42f --- /dev/null +++ b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.2.9.patch @@ -0,0 +1,752 @@ +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrier.php b/vendor/magento/module-cron/Model/DeadlockRetrier.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrier.php +@@ -0,0 +1,39 @@ ++getTransactionLevel() !== 0) { ++ return $callback(); ++ } ++ ++ for ($retries = self::MAX_RETRIES - 1; $retries > 0; $retries--) { ++ try { ++ return $callback(); ++ } catch (DeadlockException $e) { ++ continue; ++ } ++ } ++ ++ return $callback(); ++ } ++} +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +@@ -0,0 +1,33 @@ ++timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); ++ $this->retrier = $retrier ?: ObjectManager::getInstance()->get(DeadlockRetrierInterface::class); + } + + /** +- * @return void ++ * @inheritdoc + */ + public function _construct() + { +@@ -88,6 +96,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Set cron expression. ++ * + * @param string $expr + * @return $this + * @throws \Magento\Framework\Exception\CronException +@@ -95,7 +105,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + public function setCronExpr($expr) + { + $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); +- if (sizeof($e) < 5 || sizeof($e) > 6) { ++ if (count($e) < 5 || count($e) > 6) { + throw new CronException(__('Invalid cron expression: %1', $expr)); + } + +@@ -104,7 +114,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Checks the observer's cron expression against time ++ * Checks the observer's cron expression against time. + * + * Supports $this->setCronExpr('* 0-5,10-59/5 2-10,15-25 january-june/2 mon-fri') + * +@@ -137,6 +147,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Match cron expression. ++ * + * @param string $expr + * @param int $num + * @return bool +@@ -164,7 +176,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + // handle modulus + if (strpos($expr, '/') !== false) { + $e = explode('/', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'match/modulus\': %1', $expr)); + } + if (!is_numeric($e[1])) { +@@ -183,7 +195,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } elseif (strpos($expr, '-') !== false) { + // handle range + $e = explode('-', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'from-to\' structure: %1', $expr)); + } + +@@ -203,6 +215,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Get number of a month. ++ * + * @param int|string $value + * @return bool|int|string + */ +@@ -245,21 +259,42 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Lock the cron job so no other scheduled instances run simultaneously. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. + * +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING +- * and no other jobs of the same code are currently in STATUS_RUNNING. + * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobUniqueStatusAtomic( +- $this->getId(), +- self::STATUS_RUNNING, +- self::STATUS_PENDING +- )) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_getResource(); ++ ++ // Change statuses from running to error for terminated jobs ++ $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->getConnection()->update( ++ $scheduleResource->getTable('cron_schedule'), ++ ['status' => self::STATUS_ERROR], ++ ['job_code = ?' => $this->getJobCode(), 'status = ?' => self::STATUS_RUNNING] ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ // Change status from pending to running for ran jobs ++ $result = $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->trySetJobStatusAtomic( ++ $this->getId(), ++ self::STATUS_RUNNING, ++ self::STATUS_PENDING ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ if ($result) { + $this->setStatus(self::STATUS_RUNNING); + return true; + } +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -9,15 +9,19 @@ + */ + namespace Magento\Cron\Observer; + ++use Magento\Cron\Model\Schedule; + use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; +-use \Magento\Cron\Model\Schedule; + use Magento\Framework\Profiler\Driver\Standard\Stat; + use Magento\Framework\Profiler\Driver\Standard\StatFactory; ++use Magento\Cron\Model\DeadlockRetrierInterface; + + /** ++ * The observer for processing cron jobs. ++ * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.TooManyFields) + */ + class ProcessCronQueueObserver implements ObserverInterface + { +@@ -61,12 +65,17 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * How long to wait for cron group to become unlocked + */ +- const LOCK_TIMEOUT = 5; ++ const LOCK_TIMEOUT = 60; + + /** + * Static lock prefix for cron group locking + */ +- const LOCK_PREFIX = 'CRON_GROUP_'; ++ const LOCK_PREFIX = 'CRON_'; ++ ++ /** ++ * Max retries for acquire locks for cron jobs ++ */ ++ const MAX_RETRIES = 5; + + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection +@@ -143,6 +152,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private $statProfiler; + ++ /** ++ * @var \Magento\Framework\Event\ManagerInterface ++ */ ++ private $eventManager; ++ ++ /** ++ * @var DeadlockRetrierInterface ++ */ ++ private $retrier; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory +@@ -154,8 +173,11 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime + * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory + * @param \Psr\Log\LoggerInterface $logger +- * @param \Magento\Framework\App\State $state ++ * @param State $state + * @param StatFactory $statFactory ++ * @param \Magento\Framework\Lock\LockManagerInterface $lockManager ++ * @param \Magento\Framework\Event\ManagerInterface $eventManager ++ * @param DeadlockRetrierInterface $retrier + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -171,7 +193,9 @@ class ProcessCronQueueObserver implements ObserverInterface + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\App\State $state, + StatFactory $statFactory, +- \Magento\Framework\Lock\LockManagerInterface $lockManager ++ \Magento\Framework\Lock\LockManagerInterface $lockManager, ++ \Magento\Framework\Event\ManagerInterface $eventManager, ++ DeadlockRetrierInterface $retrier + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -186,6 +210,8 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; ++ $this->eventManager = $eventManager; ++ $this->retrier = $retrier; + } + + /** +@@ -201,7 +227,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- + $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); + // sort jobs groups to start from used in separated process +@@ -234,12 +259,12 @@ class ProcessCronQueueObserver implements ObserverInterface + + $this->lockGroup( + $groupId, +- function ($groupId) use ($currentTime, $jobsRoot) { ++ function ($groupId) use ($currentTime) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); +- $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + ); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -255,7 +280,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function lockGroup($groupId, callable $callback) + { +- + if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { + $this->logger->warning( + sprintf( +@@ -290,36 +314,50 @@ class ProcessCronQueueObserver implements ObserverInterface + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); +- throw new \Exception('No callbacks found'); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow ++ throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode)); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())); ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + + $this->startProfiling(); ++ $this->eventManager->dispatch('cron_job_run', ['job_name' => 'cron/' . $groupId . '/' . $jobCode]); ++ + try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); ++ //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array($callback, [$schedule]); + } catch (\Throwable $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); +- $this->logger->error(sprintf( +- 'Cron Job %s has an error: %s. Statistics: %s', +- $jobCode, +- $e->getMessage(), +- $this->getProfilingStat() +- )); ++ $this->logger->error( ++ sprintf( ++ 'Cron Job %s has an error: %s. Statistics: %s', ++ $jobCode, ++ $e->getMessage(), ++ $this->getProfilingStat() ++ ) ++ ); + if (!$e instanceof \Exception) { + $e = new \RuntimeException( + 'Error when running a cron job', +@@ -332,16 +370,22 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->stopProfiling(); + } + +- $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( +- '%Y-%m-%d %H:%M:%S', +- $this->dateTime->gmtTimestamp() +- )); ++ $schedule->setStatus( ++ Schedule::STATUS_SUCCESS ++ )->setFinishedAt( ++ strftime( ++ '%Y-%m-%d %H:%M:%S', ++ $this->dateTime->gmtTimestamp() ++ ) ++ ); + +- $this->logger->info(sprintf( +- 'Cron Job %s is successfully finished. Statistics: %s', +- $jobCode, +- $this->getProfilingStat() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() ++ ) ++ ); + } + + /** +@@ -378,8 +422,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Return job collection from data base with status 'pending' ++ * Return job collection from data base with status 'pending'. + * ++ * @param string $groupId + * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + */ + private function getPendingSchedules($groupId) +@@ -464,8 +509,8 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * Clean expired jobs + * +- * @param $groupId +- * @param $currentTime ++ * @param string $groupId ++ * @param int $currentTime + * @return void + */ + private function cleanupJobs($groupId, $currentTime) +@@ -496,16 +541,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ]; + + $jobs = $this->_config->getJobs()[$groupId]; +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { +- $count += $connection->delete( +- $scheduleResource->getMainTable(), ++ $count += $this->cleanup( + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), +- 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ 'created_at < ?' => $this->_scheduleFactory ++ ->create() ++ ->getResource() ++ ->getConnection() ++ ->formatDate($currentTime - $time) + ] + ); + } +@@ -516,6 +562,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get config of schedule. ++ * + * @param array $jobConfig + * @return mixed + */ +@@ -530,6 +578,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Save a schedule of cron job. ++ * + * @param string $jobCode + * @param string $cronExpression + * @param int $timeInterval +@@ -562,6 +612,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Create a schedule of cron job. ++ * + * @param string $jobCode + * @param string $cronExpression + * @param int $time +@@ -580,6 +632,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get time interval for scheduling. ++ * + * @param string $groupId + * @return int + */ +@@ -592,8 +646,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Clean up scheduled jobs that are disabled in the configuration +- * This can happen when you turn off a cron job in the config and flush the cache ++ * Clean up scheduled jobs that are disabled in the configuration. ++ * ++ * This can happen when you turn off a cron job in the config and flush the cache. + * + * @param string $groupId + * @return void +@@ -610,9 +665,7 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + if (count($jobsToCleanup) > 0) { +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $count = $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $count = $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, +@@ -624,6 +677,8 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** ++ * Get cron expression of cron job. ++ * + * @param array $jobConfig + * @return null|string + */ +@@ -643,29 +698,32 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Clean up scheduled jobs that do not match their cron expression anymore +- * This can happen when you change the cron expression and flush the cache ++ * Clean up scheduled jobs that do not match their cron expression anymore. ++ * ++ * This can happen when you change the cron expression and flush the cache. + * + * @return $this + */ + private function cleanupScheduleMismatches() + { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status = ?' => Schedule::STATUS_PENDING, +- 'job_code = ?' => $jobCode, +- 'scheduled_at in (?)' => $scheduledAtList, +- ]); ++ $this->cleanup( ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ] ++ ); + } ++ + return $this; + } + + /** +- * Get CronGroup Configuration Value ++ * Get CronGroup Configuration Value. + * +- * @param $groupId ++ * @param string $groupId ++ * @param string $path + * @return int + */ + private function getCronGroupConfigurationValue($groupId, $path) +@@ -677,9 +735,9 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Is Group In Filter ++ * Is Group In Filter. + * +- * @param $groupId ++ * @param string $groupId + * @return bool + */ + private function isGroupInFilter($groupId): bool +@@ -689,17 +747,17 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + /** +- * Process pending jobs ++ * Process pending jobs. + * +- * @param $groupId +- * @param $jobsRoot +- * @param $currentTime ++ * @param string $groupId ++ * @param array $jobsRoot ++ * @param int $currentTime + */ + private function processPendingJobs($groupId, $jobsRoot, $currentTime) + { + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); +- /** @var \Magento\Cron\Model\Schedule $schedule */ ++ /** @var Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run +@@ -715,26 +773,59 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $this->processError($schedule, $e); +- } ++ $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } +- $schedule->save(); ++ ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + } + } + + /** ++ * Try to acquire lock for cron job and try to run this job. ++ * ++ * @param int $scheduledTime ++ * @param int $currentTime ++ * @param string[] $jobConfig ++ * @param Schedule $schedule ++ * @param string $groupId ++ */ ++ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) ++ { ++ // use sha1 to limit length ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); ++ ++ try { ++ for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { ++ if ($this->lockManager->lock($lockName, 0) && $schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ break; ++ } ++ $this->logger->warning("Could not acquire lock for cron job: {$schedule->getJobCode()}"); ++ } ++ } catch (\Exception $e) { ++ $this->processError($schedule, $e); ++ } finally { ++ $this->lockManager->unlock($lockName); ++ } ++ } ++ ++ /** ++ * Process error messages. ++ * + * @param Schedule $schedule + * @param \Exception $exception + * @return void + */ +- private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) ++ private function processError(Schedule $schedule, \Exception $exception) + { + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +@@ -746,4 +837,26 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->logger->info($schedule->getMessages()); + } + } ++ ++ /** ++ * Clean up schedule ++ * ++ * @param mixed $where ++ * @return int ++ */ ++ private function cleanup($where = ''): int ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ ++ return (int) $this->retrier->execute( ++ function () use ($scheduleResource, $where) { ++ return $scheduleResource->getConnection()->delete( ++ $scheduleResource->getTable('cron_schedule'), ++ $where ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ } + } +diff -Naur a/vendor/magento/module-cron/etc/di.xml b/vendor/magento/module-cron/etc/di.xml +--- a/vendor/magento/module-cron/etc/di.xml ++++ b/vendor/magento/module-cron/etc/di.xml +@@ -76,4 +76,5 @@ + + + ++ + diff --git a/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.0.patch b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.0.patch new file mode 100644 index 00000000..bd083e18 --- /dev/null +++ b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.0.patch @@ -0,0 +1,623 @@ +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrier.php b/vendor/magento/module-cron/Model/DeadlockRetrier.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrier.php +@@ -0,0 +1,39 @@ ++getTransactionLevel() !== 0) { ++ return $callback(); ++ } ++ ++ for ($retries = self::MAX_RETRIES - 1; $retries > 0; $retries--) { ++ try { ++ return $callback(); ++ } catch (DeadlockException $e) { ++ continue; ++ } ++ } ++ ++ return $callback(); ++ } ++} +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +@@ -0,0 +1,33 @@ ++timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); ++ $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); ++ $this->retrier = $retrier ?: ObjectManager::getInstance()->get(DeadlockRetrierInterface::class); + } + + /** +@@ -88,7 +105,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + public function setCronExpr($expr) + { + $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); +- if (sizeof($e) < 5 || sizeof($e) > 6) { ++ if (count($e) < 5 || count($e) > 6) { + throw new CronException(__('Invalid cron expression: %1', $expr)); + } + +@@ -111,17 +128,20 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + if (!$e || !$time) { + return false; + } ++ $configTimeZone = $this->timezoneConverter->getConfigTimezone(); ++ $storeDateTime = $this->dateTimeFactory->create(null, new \DateTimeZone($configTimeZone)); + if (!is_numeric($time)) { + //convert time from UTC to admin store timezone + //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone +- $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); +- $time = strtotime($time); ++ $dateTimeUtc = $this->dateTimeFactory->create($time); ++ $time = $dateTimeUtc->getTimestamp(); + } +- $match = $this->matchCronExpression($e[0], strftime('%M', $time)) +- && $this->matchCronExpression($e[1], strftime('%H', $time)) +- && $this->matchCronExpression($e[2], strftime('%d', $time)) +- && $this->matchCronExpression($e[3], strftime('%m', $time)) +- && $this->matchCronExpression($e[4], strftime('%w', $time)); ++ $time = $storeDateTime->setTimestamp($time); ++ $match = $this->matchCronExpression($e[0], $time->format('i')) ++ && $this->matchCronExpression($e[1], $time->format('H')) ++ && $this->matchCronExpression($e[2], $time->format('d')) ++ && $this->matchCronExpression($e[3], $time->format('m')) ++ && $this->matchCronExpression($e[4], $time->format('w')); + + return $match; + } +@@ -156,7 +176,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + // handle modulus + if (strpos($expr, '/') !== false) { + $e = explode('/', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'match/modulus\': %1', $expr)); + } + if (!is_numeric($e[1])) { +@@ -175,7 +195,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } elseif (strpos($expr, '-') !== false) { + // handle range + $e = explode('-', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'from-to\' structure: %1', $expr)); + } + +@@ -239,21 +259,42 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Lock the cron job so no other scheduled instances run simultaneously. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. + * +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING +- * and no other jobs of the same code are currently in STATUS_RUNNING. + * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobUniqueStatusAtomic( +- $this->getId(), +- self::STATUS_RUNNING, +- self::STATUS_PENDING +- )) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_getResource(); ++ ++ // Change statuses from running to error for terminated jobs ++ $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->getConnection()->update( ++ $scheduleResource->getTable('cron_schedule'), ++ ['status' => self::STATUS_ERROR], ++ ['job_code = ?' => $this->getJobCode(), 'status = ?' => self::STATUS_RUNNING] ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ // Change status from pending to running for ran jobs ++ $result = $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->trySetJobStatusAtomic( ++ $this->getId(), ++ self::STATUS_RUNNING, ++ self::STATUS_PENDING ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ if ($result) { + $this->setStatus(self::STATUS_RUNNING); + return true; + } +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -9,17 +9,19 @@ + */ + namespace Magento\Cron\Observer; + ++use Magento\Cron\Model\Schedule; + use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; +-use Magento\Cron\Model\Schedule; + use Magento\Framework\Profiler\Driver\Standard\Stat; + use Magento\Framework\Profiler\Driver\Standard\StatFactory; ++use Magento\Cron\Model\DeadlockRetrierInterface; + + /** + * The observer for processing cron jobs. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.TooManyFields) + */ + class ProcessCronQueueObserver implements ObserverInterface + { +@@ -63,12 +65,17 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * How long to wait for cron group to become unlocked + */ +- const LOCK_TIMEOUT = 5; ++ const LOCK_TIMEOUT = 60; + + /** + * Static lock prefix for cron group locking + */ +- const LOCK_PREFIX = 'CRON_GROUP_'; ++ const LOCK_PREFIX = 'CRON_'; ++ ++ /** ++ * Max retries for acquire locks for cron jobs ++ */ ++ const MAX_RETRIES = 5; + + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection +@@ -145,6 +152,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private $statProfiler; + ++ /** ++ * @var \Magento\Framework\Event\ManagerInterface ++ */ ++ private $eventManager; ++ ++ /** ++ * @var DeadlockRetrierInterface ++ */ ++ private $retrier; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory +@@ -159,6 +176,8 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param State $state + * @param StatFactory $statFactory + * @param \Magento\Framework\Lock\LockManagerInterface $lockManager ++ * @param \Magento\Framework\Event\ManagerInterface $eventManager ++ * @param DeadlockRetrierInterface $retrier + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -174,7 +193,9 @@ class ProcessCronQueueObserver implements ObserverInterface + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\App\State $state, + StatFactory $statFactory, +- \Magento\Framework\Lock\LockManagerInterface $lockManager ++ \Magento\Framework\Lock\LockManagerInterface $lockManager, ++ \Magento\Framework\Event\ManagerInterface $eventManager, ++ DeadlockRetrierInterface $retrier + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -189,6 +210,8 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; ++ $this->eventManager = $eventManager; ++ $this->retrier = $retrier; + } + + /** +@@ -204,7 +227,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- + $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); + // sort jobs groups to start from used in separated process +@@ -237,12 +259,12 @@ class ProcessCronQueueObserver implements ObserverInterface + + $this->lockGroup( + $groupId, +- function ($groupId) use ($currentTime, $jobsRoot) { ++ function ($groupId) use ($currentTime) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); +- $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + ); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -258,7 +280,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function lockGroup($groupId, callable $callback) + { +- + if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { + $this->logger->warning( + sprintf( +@@ -293,36 +314,50 @@ class ProcessCronQueueObserver implements ObserverInterface + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); +- throw new \Exception('No callbacks found'); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow ++ throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode)); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())); ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + + $this->startProfiling(); ++ $this->eventManager->dispatch('cron_job_run', ['job_name' => 'cron/' . $groupId . '/' . $jobCode]); ++ + try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); ++ //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array($callback, [$schedule]); + } catch (\Throwable $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); +- $this->logger->error(sprintf( +- 'Cron Job %s has an error: %s. Statistics: %s', +- $jobCode, +- $e->getMessage(), +- $this->getProfilingStat() +- )); ++ $this->logger->error( ++ sprintf( ++ 'Cron Job %s has an error: %s. Statistics: %s', ++ $jobCode, ++ $e->getMessage(), ++ $this->getProfilingStat() ++ ) ++ ); + if (!$e instanceof \Exception) { + $e = new \RuntimeException( + 'Error when running a cron job', +@@ -335,16 +370,22 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->stopProfiling(); + } + +- $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( +- '%Y-%m-%d %H:%M:%S', +- $this->dateTime->gmtTimestamp() +- )); ++ $schedule->setStatus( ++ Schedule::STATUS_SUCCESS ++ )->setFinishedAt( ++ strftime( ++ '%Y-%m-%d %H:%M:%S', ++ $this->dateTime->gmtTimestamp() ++ ) ++ ); + +- $this->logger->info(sprintf( +- 'Cron Job %s is successfully finished. Statistics: %s', +- $jobCode, +- $this->getProfilingStat() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() ++ ) ++ ); + } + + /** +@@ -500,16 +541,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ]; + + $jobs = $this->_config->getJobs()[$groupId]; +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { +- $count += $connection->delete( +- $scheduleResource->getMainTable(), ++ $count += $this->cleanup( + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), +- 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ 'created_at < ?' => $this->_scheduleFactory ++ ->create() ++ ->getResource() ++ ->getConnection() ++ ->formatDate($currentTime - $time) + ] + ); + } +@@ -623,9 +665,7 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + if (count($jobsToCleanup) > 0) { +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $count = $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $count = $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, +@@ -666,15 +706,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function cleanupScheduleMismatches() + { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status = ?' => Schedule::STATUS_PENDING, +- 'job_code = ?' => $jobCode, +- 'scheduled_at in (?)' => $scheduledAtList, +- ]); ++ $this->cleanup( ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ] ++ ); + } ++ + return $this; + } + +@@ -716,7 +757,7 @@ class ProcessCronQueueObserver implements ObserverInterface + { + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); +- /** @var \Magento\Cron\Model\Schedule $schedule */ ++ /** @var Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run +@@ -732,17 +773,48 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $this->processError($schedule, $e); +- } ++ $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } +- $schedule->save(); ++ ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); ++ } ++ } ++ ++ /** ++ * Try to acquire lock for cron job and try to run this job. ++ * ++ * @param int $scheduledTime ++ * @param int $currentTime ++ * @param string[] $jobConfig ++ * @param Schedule $schedule ++ * @param string $groupId ++ */ ++ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) ++ { ++ // use sha1 to limit length ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); ++ ++ try { ++ for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { ++ if ($this->lockManager->lock($lockName, 0) && $schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ break; ++ } ++ $this->logger->warning("Could not acquire lock for cron job: {$schedule->getJobCode()}"); ++ } ++ } catch (\Exception $e) { ++ $this->processError($schedule, $e); ++ } finally { ++ $this->lockManager->unlock($lockName); + } + } + +@@ -753,7 +825,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Exception $exception + * @return void + */ +- private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) ++ private function processError(Schedule $schedule, \Exception $exception) + { + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +@@ -765,4 +837,26 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->logger->info($schedule->getMessages()); + } + } ++ ++ /** ++ * Clean up schedule ++ * ++ * @param mixed $where ++ * @return int ++ */ ++ private function cleanup($where = ''): int ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ ++ return (int) $this->retrier->execute( ++ function () use ($scheduleResource, $where) { ++ return $scheduleResource->getConnection()->delete( ++ $scheduleResource->getTable('cron_schedule'), ++ $where ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ } + } +diff -Naur a/vendor/magento/module-cron/etc/di.xml b/vendor/magento/module-cron/etc/di.xml +--- a/vendor/magento/module-cron/etc/di.xml ++++ b/vendor/magento/module-cron/etc/di.xml +@@ -63,4 +63,5 @@ + + + ++ + diff --git a/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.1.patch b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.1.patch new file mode 100644 index 00000000..a267f7e7 --- /dev/null +++ b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.1.patch @@ -0,0 +1,622 @@ +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrier.php b/vendor/magento/module-cron/Model/DeadlockRetrier.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrier.php +@@ -0,0 +1,39 @@ ++getTransactionLevel() !== 0) { ++ return $callback(); ++ } ++ ++ for ($retries = self::MAX_RETRIES - 1; $retries > 0; $retries--) { ++ try { ++ return $callback(); ++ } catch (DeadlockException $e) { ++ continue; ++ } ++ } ++ ++ return $callback(); ++ } ++} +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +@@ -0,0 +1,33 @@ ++timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); ++ $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); ++ $this->retrier = $retrier ?: ObjectManager::getInstance()->get(DeadlockRetrierInterface::class); + } + + /** +@@ -88,7 +105,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + public function setCronExpr($expr) + { + $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); +- if (sizeof($e) < 5 || sizeof($e) > 6) { ++ if (count($e) < 5 || count($e) > 6) { + throw new CronException(__('Invalid cron expression: %1', $expr)); + } + +@@ -111,17 +128,20 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + if (!$e || !$time) { + return false; + } ++ $configTimeZone = $this->timezoneConverter->getConfigTimezone(); ++ $storeDateTime = $this->dateTimeFactory->create(null, new \DateTimeZone($configTimeZone)); + if (!is_numeric($time)) { + //convert time from UTC to admin store timezone + //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone +- $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); +- $time = strtotime($time); ++ $dateTimeUtc = $this->dateTimeFactory->create($time); ++ $time = $dateTimeUtc->getTimestamp(); + } +- $match = $this->matchCronExpression($e[0], strftime('%M', $time)) +- && $this->matchCronExpression($e[1], strftime('%H', $time)) +- && $this->matchCronExpression($e[2], strftime('%d', $time)) +- && $this->matchCronExpression($e[3], strftime('%m', $time)) +- && $this->matchCronExpression($e[4], strftime('%w', $time)); ++ $time = $storeDateTime->setTimestamp($time); ++ $match = $this->matchCronExpression($e[0], $time->format('i')) ++ && $this->matchCronExpression($e[1], $time->format('H')) ++ && $this->matchCronExpression($e[2], $time->format('d')) ++ && $this->matchCronExpression($e[3], $time->format('m')) ++ && $this->matchCronExpression($e[4], $time->format('w')); + + return $match; + } +@@ -156,7 +176,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + // handle modulus + if (strpos($expr, '/') !== false) { + $e = explode('/', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'match/modulus\': %1', $expr)); + } + if (!is_numeric($e[1])) { +@@ -175,7 +195,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } elseif (strpos($expr, '-') !== false) { + // handle range + $e = explode('-', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'from-to\' structure: %1', $expr)); + } + +@@ -239,21 +259,42 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Lock the cron job so no other scheduled instances run simultaneously. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. + * +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING +- * and no other jobs of the same code are currently in STATUS_RUNNING. + * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobUniqueStatusAtomic( +- $this->getId(), +- self::STATUS_RUNNING, +- self::STATUS_PENDING +- )) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_getResource(); ++ ++ // Change statuses from running to error for terminated jobs ++ $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->getConnection()->update( ++ $scheduleResource->getTable('cron_schedule'), ++ ['status' => self::STATUS_ERROR], ++ ['job_code = ?' => $this->getJobCode(), 'status = ?' => self::STATUS_RUNNING] ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ // Change status from pending to running for ran jobs ++ $result = $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->trySetJobStatusAtomic( ++ $this->getId(), ++ self::STATUS_RUNNING, ++ self::STATUS_PENDING ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ if ($result) { + $this->setStatus(self::STATUS_RUNNING); + return true; + } +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -9,17 +9,19 @@ + */ + namespace Magento\Cron\Observer; + ++use Magento\Cron\Model\Schedule; + use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; +-use Magento\Cron\Model\Schedule; + use Magento\Framework\Profiler\Driver\Standard\Stat; + use Magento\Framework\Profiler\Driver\Standard\StatFactory; ++use Magento\Cron\Model\DeadlockRetrierInterface; + + /** + * The observer for processing cron jobs. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.TooManyFields) + */ + class ProcessCronQueueObserver implements ObserverInterface + { +@@ -63,12 +65,17 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * How long to wait for cron group to become unlocked + */ +- const LOCK_TIMEOUT = 5; ++ const LOCK_TIMEOUT = 60; + + /** + * Static lock prefix for cron group locking + */ +- const LOCK_PREFIX = 'CRON_GROUP_'; ++ const LOCK_PREFIX = 'CRON_'; ++ ++ /** ++ * Max retries for acquire locks for cron jobs ++ */ ++ const MAX_RETRIES = 5; + + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection +@@ -145,6 +152,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private $statProfiler; + ++ /** ++ * @var \Magento\Framework\Event\ManagerInterface ++ */ ++ private $eventManager; ++ ++ /** ++ * @var DeadlockRetrierInterface ++ */ ++ private $retrier; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory +@@ -159,6 +176,8 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param State $state + * @param StatFactory $statFactory + * @param \Magento\Framework\Lock\LockManagerInterface $lockManager ++ * @param \Magento\Framework\Event\ManagerInterface $eventManager ++ * @param DeadlockRetrierInterface $retrier + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -174,7 +193,9 @@ class ProcessCronQueueObserver implements ObserverInterface + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\App\State $state, + StatFactory $statFactory, +- \Magento\Framework\Lock\LockManagerInterface $lockManager ++ \Magento\Framework\Lock\LockManagerInterface $lockManager, ++ \Magento\Framework\Event\ManagerInterface $eventManager, ++ DeadlockRetrierInterface $retrier + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -189,6 +210,8 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; ++ $this->eventManager = $eventManager; ++ $this->retrier = $retrier; + } + + /** +@@ -204,7 +227,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- + $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); + // sort jobs groups to start from used in separated process +@@ -237,12 +259,12 @@ class ProcessCronQueueObserver implements ObserverInterface + + $this->lockGroup( + $groupId, +- function ($groupId) use ($currentTime, $jobsRoot) { ++ function ($groupId) use ($currentTime) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); +- $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + ); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -258,7 +280,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function lockGroup($groupId, callable $callback) + { +- + if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { + $this->logger->warning( + sprintf( +@@ -293,36 +314,50 @@ class ProcessCronQueueObserver implements ObserverInterface + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode)); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())); ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + + $this->startProfiling(); ++ $this->eventManager->dispatch('cron_job_run', ['job_name' => 'cron/' . $groupId . '/' . $jobCode]); ++ + try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); ++ //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array($callback, [$schedule]); + } catch (\Throwable $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); +- $this->logger->error(sprintf( +- 'Cron Job %s has an error: %s. Statistics: %s', +- $jobCode, +- $e->getMessage(), +- $this->getProfilingStat() +- )); ++ $this->logger->error( ++ sprintf( ++ 'Cron Job %s has an error: %s. Statistics: %s', ++ $jobCode, ++ $e->getMessage(), ++ $this->getProfilingStat() ++ ) ++ ); + if (!$e instanceof \Exception) { + $e = new \RuntimeException( + 'Error when running a cron job', +@@ -335,16 +370,22 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->stopProfiling(); + } + +- $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( +- '%Y-%m-%d %H:%M:%S', +- $this->dateTime->gmtTimestamp() +- )); ++ $schedule->setStatus( ++ Schedule::STATUS_SUCCESS ++ )->setFinishedAt( ++ strftime( ++ '%Y-%m-%d %H:%M:%S', ++ $this->dateTime->gmtTimestamp() ++ ) ++ ); + +- $this->logger->info(sprintf( +- 'Cron Job %s is successfully finished. Statistics: %s', +- $jobCode, +- $this->getProfilingStat() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() ++ ) ++ ); + } + + /** +@@ -500,16 +541,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ]; + + $jobs = $this->_config->getJobs()[$groupId]; +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { +- $count += $connection->delete( +- $scheduleResource->getMainTable(), ++ $count += $this->cleanup( + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), +- 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ 'created_at < ?' => $this->_scheduleFactory ++ ->create() ++ ->getResource() ++ ->getConnection() ++ ->formatDate($currentTime - $time) + ] + ); + } +@@ -623,9 +665,7 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + if (count($jobsToCleanup) > 0) { +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $count = $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $count = $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, +@@ -666,15 +706,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function cleanupScheduleMismatches() + { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status = ?' => Schedule::STATUS_PENDING, +- 'job_code = ?' => $jobCode, +- 'scheduled_at in (?)' => $scheduledAtList, +- ]); ++ $this->cleanup( ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ] ++ ); + } ++ + return $this; + } + +@@ -716,7 +757,7 @@ class ProcessCronQueueObserver implements ObserverInterface + { + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); +- /** @var \Magento\Cron\Model\Schedule $schedule */ ++ /** @var Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run +@@ -732,17 +773,48 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $this->processError($schedule, $e); +- } ++ $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } +- $schedule->save(); ++ ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); ++ } ++ } ++ ++ /** ++ * Try to acquire lock for cron job and try to run this job. ++ * ++ * @param int $scheduledTime ++ * @param int $currentTime ++ * @param string[] $jobConfig ++ * @param Schedule $schedule ++ * @param string $groupId ++ */ ++ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) ++ { ++ // use sha1 to limit length ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); ++ ++ try { ++ for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { ++ if ($this->lockManager->lock($lockName, 0) && $schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ break; ++ } ++ $this->logger->warning("Could not acquire lock for cron job: {$schedule->getJobCode()}"); ++ } ++ } catch (\Exception $e) { ++ $this->processError($schedule, $e); ++ } finally { ++ $this->lockManager->unlock($lockName); + } + } + +@@ -753,7 +825,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Exception $exception + * @return void + */ +- private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) ++ private function processError(Schedule $schedule, \Exception $exception) + { + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +@@ -765,4 +837,26 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->logger->info($schedule->getMessages()); + } + } ++ ++ /** ++ * Clean up schedule ++ * ++ * @param mixed $where ++ * @return int ++ */ ++ private function cleanup($where = ''): int ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ ++ return (int) $this->retrier->execute( ++ function () use ($scheduleResource, $where) { ++ return $scheduleResource->getConnection()->delete( ++ $scheduleResource->getTable('cron_schedule'), ++ $where ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ } + } +diff -Naur a/vendor/magento/module-cron/etc/di.xml b/vendor/magento/module-cron/etc/di.xml +--- a/vendor/magento/module-cron/etc/di.xml ++++ b/vendor/magento/module-cron/etc/di.xml +@@ -76,4 +76,5 @@ + + + ++ + diff --git a/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.2.patch b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.2.patch new file mode 100644 index 00000000..d557e39f --- /dev/null +++ b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.2.patch @@ -0,0 +1,578 @@ +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrier.php b/vendor/magento/module-cron/Model/DeadlockRetrier.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrier.php +@@ -0,0 +1,39 @@ ++getTransactionLevel() !== 0) { ++ return $callback(); ++ } ++ ++ for ($retries = self::MAX_RETRIES - 1; $retries > 0; $retries--) { ++ try { ++ return $callback(); ++ } catch (DeadlockException $e) { ++ continue; ++ } ++ } ++ ++ return $callback(); ++ } ++} +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +@@ -0,0 +1,33 @@ ++timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); ++ $this->retrier = $retrier ?: ObjectManager::getInstance()->get(DeadlockRetrierInterface::class); + } + + /** +@@ -97,7 +105,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + public function setCronExpr($expr) + { + $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); +- if (sizeof($e) < 5 || sizeof($e) > 6) { ++ if (count($e) < 5 || count($e) > 6) { + throw new CronException(__('Invalid cron expression: %1', $expr)); + } + +@@ -168,7 +176,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + // handle modulus + if (strpos($expr, '/') !== false) { + $e = explode('/', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'match/modulus\': %1', $expr)); + } + if (!is_numeric($e[1])) { +@@ -187,7 +195,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } elseif (strpos($expr, '-') !== false) { + // handle range + $e = explode('-', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'from-to\' structure: %1', $expr)); + } + +@@ -251,21 +259,42 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Lock the cron job so no other scheduled instances run simultaneously. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. + * +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING +- * and no other jobs of the same code are currently in STATUS_RUNNING. + * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobUniqueStatusAtomic( +- $this->getId(), +- self::STATUS_RUNNING, +- self::STATUS_PENDING +- )) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_getResource(); ++ ++ // Change statuses from running to error for terminated jobs ++ $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->getConnection()->update( ++ $scheduleResource->getTable('cron_schedule'), ++ ['status' => self::STATUS_ERROR], ++ ['job_code = ?' => $this->getJobCode(), 'status = ?' => self::STATUS_RUNNING] ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ // Change status from pending to running for ran jobs ++ $result = $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->trySetJobStatusAtomic( ++ $this->getId(), ++ self::STATUS_RUNNING, ++ self::STATUS_PENDING ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ if ($result) { + $this->setStatus(self::STATUS_RUNNING); + return true; + } +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -9,17 +9,19 @@ + */ + namespace Magento\Cron\Observer; + ++use Magento\Cron\Model\Schedule; + use Magento\Framework\App\State; + use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; +-use Magento\Cron\Model\Schedule; + use Magento\Framework\Profiler\Driver\Standard\Stat; + use Magento\Framework\Profiler\Driver\Standard\StatFactory; ++use Magento\Cron\Model\DeadlockRetrierInterface; + + /** + * The observer for processing cron jobs. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.TooManyFields) + */ + class ProcessCronQueueObserver implements ObserverInterface + { +@@ -63,12 +65,17 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * How long to wait for cron group to become unlocked + */ +- const LOCK_TIMEOUT = 5; ++ const LOCK_TIMEOUT = 60; + + /** + * Static lock prefix for cron group locking + */ +- const LOCK_PREFIX = 'CRON_GROUP_'; ++ const LOCK_PREFIX = 'CRON_'; ++ ++ /** ++ * Max retries for acquire locks for cron jobs ++ */ ++ const MAX_RETRIES = 5; + + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection +@@ -145,6 +152,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private $statProfiler; + ++ /** ++ * @var \Magento\Framework\Event\ManagerInterface ++ */ ++ private $eventManager; ++ ++ /** ++ * @var DeadlockRetrierInterface ++ */ ++ private $retrier; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory +@@ -159,6 +176,8 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param State $state + * @param StatFactory $statFactory + * @param \Magento\Framework\Lock\LockManagerInterface $lockManager ++ * @param \Magento\Framework\Event\ManagerInterface $eventManager ++ * @param DeadlockRetrierInterface $retrier + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -174,7 +193,9 @@ class ProcessCronQueueObserver implements ObserverInterface + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\App\State $state, + StatFactory $statFactory, +- \Magento\Framework\Lock\LockManagerInterface $lockManager ++ \Magento\Framework\Lock\LockManagerInterface $lockManager, ++ \Magento\Framework\Event\ManagerInterface $eventManager, ++ DeadlockRetrierInterface $retrier + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -189,6 +210,8 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; ++ $this->eventManager = $eventManager; ++ $this->retrier = $retrier; + } + + /** +@@ -204,7 +227,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { +- + $currentTime = $this->dateTime->gmtTimestamp(); + $jobGroupsRoot = $this->_config->getJobs(); + // sort jobs groups to start from used in separated process +@@ -237,12 +259,12 @@ class ProcessCronQueueObserver implements ObserverInterface + + $this->lockGroup( + $groupId, +- function ($groupId) use ($currentTime, $jobsRoot) { ++ function ($groupId) use ($currentTime) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); +- $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + ); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -258,7 +280,6 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function lockGroup($groupId, callable $callback) + { +- + if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { + $this->logger->warning( + sprintf( +@@ -293,36 +314,50 @@ class ProcessCronQueueObserver implements ObserverInterface + $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; + if ($scheduledTime < $currentTime - $scheduleLifetime) { + $schedule->setStatus(Schedule::STATUS_MISSED); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); + } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode)); + } + $model = $this->_objectManager->create($jobConfig['instance']); + $callback = [$model, $jobConfig['method']]; + if (!is_callable($callback)) { + $schedule->setStatus(Schedule::STATUS_ERROR); ++ // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception( + sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())); ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + + $this->startProfiling(); ++ $this->eventManager->dispatch('cron_job_run', ['job_name' => 'cron/' . $groupId . '/' . $jobCode]); ++ + try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); ++ //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array($callback, [$schedule]); + } catch (\Throwable $e) { + $schedule->setStatus(Schedule::STATUS_ERROR); +- $this->logger->error(sprintf( +- 'Cron Job %s has an error: %s. Statistics: %s', +- $jobCode, +- $e->getMessage(), +- $this->getProfilingStat() +- )); ++ $this->logger->error( ++ sprintf( ++ 'Cron Job %s has an error: %s. Statistics: %s', ++ $jobCode, ++ $e->getMessage(), ++ $this->getProfilingStat() ++ ) ++ ); + if (!$e instanceof \Exception) { + $e = new \RuntimeException( + 'Error when running a cron job', +@@ -335,16 +370,22 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->stopProfiling(); + } + +- $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( +- '%Y-%m-%d %H:%M:%S', +- $this->dateTime->gmtTimestamp() +- )); ++ $schedule->setStatus( ++ Schedule::STATUS_SUCCESS ++ )->setFinishedAt( ++ strftime( ++ '%Y-%m-%d %H:%M:%S', ++ $this->dateTime->gmtTimestamp() ++ ) ++ ); + +- $this->logger->info(sprintf( +- 'Cron Job %s is successfully finished. Statistics: %s', +- $jobCode, +- $this->getProfilingStat() +- )); ++ $this->logger->info( ++ sprintf( ++ 'Cron Job %s is successfully finished. Statistics: %s', ++ $jobCode, ++ $this->getProfilingStat() ++ ) ++ ); + } + + /** +@@ -500,16 +541,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ]; + + $jobs = $this->_config->getJobs()[$groupId]; +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { +- $count += $connection->delete( +- $scheduleResource->getMainTable(), ++ $count += $this->cleanup( + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), +- 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ 'created_at < ?' => $this->_scheduleFactory ++ ->create() ++ ->getResource() ++ ->getConnection() ++ ->formatDate($currentTime - $time) + ] + ); + } +@@ -623,9 +665,7 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + if (count($jobsToCleanup) > 0) { +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $count = $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $count = $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, +@@ -666,15 +706,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function cleanupScheduleMismatches() + { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ +- 'status = ?' => Schedule::STATUS_PENDING, +- 'job_code = ?' => $jobCode, +- 'scheduled_at in (?)' => $scheduledAtList, +- ]); ++ $this->cleanup( ++ [ ++ 'status = ?' => Schedule::STATUS_PENDING, ++ 'job_code = ?' => $jobCode, ++ 'scheduled_at in (?)' => $scheduledAtList, ++ ] ++ ); + } ++ + return $this; + } + +@@ -716,7 +757,7 @@ class ProcessCronQueueObserver implements ObserverInterface + { + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); +- /** @var \Magento\Cron\Model\Schedule $schedule */ ++ /** @var Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run +@@ -732,17 +773,48 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $this->processError($schedule, $e); +- } ++ $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } +- $schedule->save(); ++ ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); ++ } ++ } ++ ++ /** ++ * Try to acquire lock for cron job and try to run this job. ++ * ++ * @param int $scheduledTime ++ * @param int $currentTime ++ * @param string[] $jobConfig ++ * @param Schedule $schedule ++ * @param string $groupId ++ */ ++ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) ++ { ++ // use sha1 to limit length ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); ++ ++ try { ++ for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { ++ if ($this->lockManager->lock($lockName, 0) && $schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ break; ++ } ++ $this->logger->warning("Could not acquire lock for cron job: {$schedule->getJobCode()}"); ++ } ++ } catch (\Exception $e) { ++ $this->processError($schedule, $e); ++ } finally { ++ $this->lockManager->unlock($lockName); + } + } + +@@ -753,7 +825,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Exception $exception + * @return void + */ +- private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) ++ private function processError(Schedule $schedule, \Exception $exception) + { + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +@@ -765,4 +837,26 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->logger->info($schedule->getMessages()); + } + } ++ ++ /** ++ * Clean up schedule ++ * ++ * @param mixed $where ++ * @return int ++ */ ++ private function cleanup($where = ''): int ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ ++ return (int) $this->retrier->execute( ++ function () use ($scheduleResource, $where) { ++ return $scheduleResource->getConnection()->delete( ++ $scheduleResource->getTable('cron_schedule'), ++ $where ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ } + } +diff -Naur a/vendor/magento/module-cron/etc/di.xml b/vendor/magento/module-cron/etc/di.xml +--- a/vendor/magento/module-cron/etc/di.xml ++++ b/vendor/magento/module-cron/etc/di.xml +@@ -76,4 +76,5 @@ + + + ++ + diff --git a/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.3.patch b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.3.patch new file mode 100644 index 00000000..d0979264 --- /dev/null +++ b/patches/MAGECLOUD-4530__fix_cron_deadlocks_and_improve_job_locking__2.3.3.patch @@ -0,0 +1,527 @@ +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrier.php b/vendor/magento/module-cron/Model/DeadlockRetrier.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrier.php +@@ -0,0 +1,39 @@ ++getTransactionLevel() !== 0) { ++ return $callback(); ++ } ++ ++ for ($retries = self::MAX_RETRIES - 1; $retries > 0; $retries--) { ++ try { ++ return $callback(); ++ } catch (DeadlockException $e) { ++ continue; ++ } ++ } ++ ++ return $callback(); ++ } ++} +diff -Naur a/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-cron/Model/DeadlockRetrierInterface.php +@@ -0,0 +1,33 @@ ++timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); ++ $this->retrier = $retrier ?: ObjectManager::getInstance()->get(DeadlockRetrierInterface::class); + } + + /** +@@ -97,7 +105,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + public function setCronExpr($expr) + { + $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); +- if (sizeof($e) < 5 || sizeof($e) > 6) { ++ if (count($e) < 5 || count($e) > 6) { + throw new CronException(__('Invalid cron expression: %1', $expr)); + } + +@@ -168,7 +176,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + // handle modulus + if (strpos($expr, '/') !== false) { + $e = explode('/', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'match/modulus\': %1', $expr)); + } + if (!is_numeric($e[1])) { +@@ -187,7 +195,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } elseif (strpos($expr, '-') !== false) { + // handle range + $e = explode('-', $expr); +- if (sizeof($e) !== 2) { ++ if (count($e) !== 2) { + throw new CronException(__('Invalid cron expression, expecting \'from-to\' structure: %1', $expr)); + } + +@@ -251,21 +259,42 @@ class Schedule extends \Magento\Framework\Model\AbstractModel + } + + /** +- * Lock the cron job so no other scheduled instances run simultaneously. ++ * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING. + * +- * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING +- * and no other jobs of the same code are currently in STATUS_RUNNING. + * Returns true if status was changed and false otherwise. + * + * @return boolean + */ + public function tryLockJob() + { +- if ($this->_getResource()->trySetJobUniqueStatusAtomic( +- $this->getId(), +- self::STATUS_RUNNING, +- self::STATUS_PENDING +- )) { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_getResource(); ++ ++ // Change statuses from running to error for terminated jobs ++ $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->getConnection()->update( ++ $scheduleResource->getTable('cron_schedule'), ++ ['status' => self::STATUS_ERROR], ++ ['job_code = ?' => $this->getJobCode(), 'status = ?' => self::STATUS_RUNNING] ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ // Change status from pending to running for ran jobs ++ $result = $this->retrier->execute( ++ function () use ($scheduleResource) { ++ return $scheduleResource->trySetJobStatusAtomic( ++ $this->getId(), ++ self::STATUS_RUNNING, ++ self::STATUS_PENDING ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ ++ if ($result) { + $this->setStatus(self::STATUS_RUNNING); + return true; + } +diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -3,6 +3,7 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + /** + * Handling cron jobs + */ +@@ -14,11 +15,13 @@ use Magento\Framework\Console\Cli; + use Magento\Framework\Event\ObserverInterface; + use Magento\Framework\Profiler\Driver\Standard\Stat; + use Magento\Framework\Profiler\Driver\Standard\StatFactory; ++use Magento\Cron\Model\DeadlockRetrierInterface; + + /** + * The observer for processing cron jobs. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.TooManyFields) + */ + class ProcessCronQueueObserver implements ObserverInterface + { +@@ -62,12 +65,17 @@ class ProcessCronQueueObserver implements ObserverInterface + /** + * How long to wait for cron group to become unlocked + */ +- const LOCK_TIMEOUT = 5; ++ const LOCK_TIMEOUT = 60; + + /** + * Static lock prefix for cron group locking + */ +- const LOCK_PREFIX = 'CRON_GROUP_'; ++ const LOCK_PREFIX = 'CRON_'; ++ ++ /** ++ * Max retries for acquire locks for cron jobs ++ */ ++ const MAX_RETRIES = 5; + + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection +@@ -144,6 +152,16 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private $statProfiler; + ++ /** ++ * @var \Magento\Framework\Event\ManagerInterface ++ */ ++ private $eventManager; ++ ++ /** ++ * @var DeadlockRetrierInterface ++ */ ++ private $retrier; ++ + /** + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory +@@ -158,6 +176,8 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param State $state + * @param StatFactory $statFactory + * @param \Magento\Framework\Lock\LockManagerInterface $lockManager ++ * @param \Magento\Framework\Event\ManagerInterface $eventManager ++ * @param DeadlockRetrierInterface $retrier + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( +@@ -173,7 +193,9 @@ class ProcessCronQueueObserver implements ObserverInterface + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\App\State $state, + StatFactory $statFactory, +- \Magento\Framework\Lock\LockManagerInterface $lockManager ++ \Magento\Framework\Lock\LockManagerInterface $lockManager, ++ \Magento\Framework\Event\ManagerInterface $eventManager, ++ DeadlockRetrierInterface $retrier + ) { + $this->_objectManager = $objectManager; + $this->_scheduleFactory = $scheduleFactory; +@@ -188,6 +210,8 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; ++ $this->eventManager = $eventManager; ++ $this->retrier = $retrier; + } + + /** +@@ -235,12 +259,12 @@ class ProcessCronQueueObserver implements ObserverInterface + + $this->lockGroup( + $groupId, +- function ($groupId) use ($currentTime, $jobsRoot) { ++ function ($groupId) use ($currentTime) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); +- $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + ); ++ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); + } + } + +@@ -309,9 +333,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ); + } + +- $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); ++ $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp())); ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); + + $this->startProfiling(); ++ $this->eventManager->dispatch('cron_job_run', ['job_name' => 'cron/' . $groupId . '/' . $jobCode]); ++ + try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + //phpcs:ignore Magento2.Functions.DiscouragedFunction +@@ -404,28 +436,6 @@ class ProcessCronQueueObserver implements ObserverInterface + return $pendingJobs; + } + +- /** +- * Return job collection from database with status 'pending', 'running' or 'success' +- * +- * @param string $groupId +- * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection +- */ +- private function getNonExitedSchedules($groupId) +- { +- $jobs = $this->_config->getJobs(); +- $pendingJobs = $this->_scheduleFactory->create()->getCollection(); +- $pendingJobs->addFieldToFilter( +- 'status', +- [ +- 'in' => [ +- Schedule::STATUS_PENDING, Schedule::STATUS_RUNNING, Schedule::STATUS_SUCCESS +- ] +- ] +- ); +- $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); +- return $pendingJobs; +- } +- + /** + * Generate cron schedule + * +@@ -457,7 +467,7 @@ class ProcessCronQueueObserver implements ObserverInterface + null + ); + +- $schedules = $this->getNonExitedSchedules($groupId); ++ $schedules = $this->getPendingSchedules($groupId); + $exists = []; + /** @var Schedule $schedule */ + foreach ($schedules as $schedule) { +@@ -531,16 +541,17 @@ class ProcessCronQueueObserver implements ObserverInterface + ]; + + $jobs = $this->_config->getJobs()[$groupId]; +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { +- $count += $connection->delete( +- $scheduleResource->getMainTable(), ++ $count += $this->cleanup( + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), +- 'created_at < ?' => $connection->formatDate($currentTime - $time) ++ 'created_at < ?' => $this->_scheduleFactory ++ ->create() ++ ->getResource() ++ ->getConnection() ++ ->formatDate($currentTime - $time) + ] + ); + } +@@ -654,9 +665,7 @@ class ProcessCronQueueObserver implements ObserverInterface + } + + if (count($jobsToCleanup) > 0) { +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); +- $count = $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $count = $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, +@@ -697,11 +706,8 @@ class ProcessCronQueueObserver implements ObserverInterface + */ + private function cleanupScheduleMismatches() + { +- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ +- $scheduleResource = $this->_scheduleFactory->create()->getResource(); + foreach ($this->invalid as $jobCode => $scheduledAtList) { +- $scheduleResource->getConnection()->delete( +- $scheduleResource->getMainTable(), ++ $this->cleanup( + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code = ?' => $jobCode, +@@ -709,6 +715,7 @@ class ProcessCronQueueObserver implements ObserverInterface + ] + ); + } ++ + return $this; + } + +@@ -750,7 +757,7 @@ class ProcessCronQueueObserver implements ObserverInterface + { + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); +- /** @var \Magento\Cron\Model\Schedule $schedule */ ++ /** @var Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run +@@ -766,17 +773,48 @@ class ProcessCronQueueObserver implements ObserverInterface + continue; + } + +- try { +- if ($schedule->tryLockJob()) { +- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); +- } +- } catch (\Exception $e) { +- $this->processError($schedule, $e); +- } ++ $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } +- $schedule->save(); ++ ++ $this->retrier->execute( ++ function () use ($schedule) { ++ $schedule->save(); ++ }, ++ $schedule->getResource()->getConnection() ++ ); ++ } ++ } ++ ++ /** ++ * Try to acquire lock for cron job and try to run this job. ++ * ++ * @param int $scheduledTime ++ * @param int $currentTime ++ * @param string[] $jobConfig ++ * @param Schedule $schedule ++ * @param string $groupId ++ */ ++ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) ++ { ++ // use sha1 to limit length ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); ++ ++ try { ++ for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { ++ if ($this->lockManager->lock($lockName, 0) && $schedule->tryLockJob()) { ++ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); ++ break; ++ } ++ $this->logger->warning("Could not acquire lock for cron job: {$schedule->getJobCode()}"); ++ } ++ } catch (\Exception $e) { ++ $this->processError($schedule, $e); ++ } finally { ++ $this->lockManager->unlock($lockName); + } + } + +@@ -787,7 +825,7 @@ class ProcessCronQueueObserver implements ObserverInterface + * @param \Exception $exception + * @return void + */ +- private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) ++ private function processError(Schedule $schedule, \Exception $exception) + { + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { +@@ -799,4 +837,26 @@ class ProcessCronQueueObserver implements ObserverInterface + $this->logger->info($schedule->getMessages()); + } + } ++ ++ /** ++ * Clean up schedule ++ * ++ * @param mixed $where ++ * @return int ++ */ ++ private function cleanup($where = ''): int ++ { ++ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ ++ $scheduleResource = $this->_scheduleFactory->create()->getResource(); ++ ++ return (int) $this->retrier->execute( ++ function () use ($scheduleResource, $where) { ++ return $scheduleResource->getConnection()->delete( ++ $scheduleResource->getTable('cron_schedule'), ++ $where ++ ); ++ }, ++ $scheduleResource->getConnection() ++ ); ++ } + } +diff -Naur a/vendor/magento/module-cron/etc/di.xml b/vendor/magento/module-cron/etc/di.xml +--- a/vendor/magento/module-cron/etc/di.xml ++++ b/vendor/magento/module-cron/etc/di.xml +@@ -79,4 +79,5 @@ + + + ++ + From 7a1bb077b617118f89962f1616edb0441fd6b589 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak Date: Fri, 24 Jan 2020 14:39:25 -0600 Subject: [PATCH 021/157] Bump Composer version to 1.0.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4f9637eb..e72ec458 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.0", + "version": "1.0.1", "license": "OSL-3.0", "require": { "php": "^7.0", From 02222f944c771b8e49d9d14dab31b4044261cf3a Mon Sep 17 00:00:00 2001 From: Ben Batschelet Date: Fri, 31 Jan 2020 08:28:15 -0600 Subject: [PATCH 022/157] MAGECLOUD-5127: Update Constraints for Magento 2.4 (#19) --- composer.json | 4 +--- config/services.xml | 1 + src/Shell/PackageNotFoundException.php | 17 +++++++++++++++ src/Shell/ProcessFactory.php | 29 ++++++++++++++++++++++++-- 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 src/Shell/PackageNotFoundException.php diff --git a/composer.json b/composer.json index fbf184e0..04509022 100644 --- a/composer.json +++ b/composer.json @@ -8,14 +8,12 @@ "php": "^7.0", "ext-json": "*", "composer/composer": "@stable", + "composer/semver": "^1.5", "symfony/config": "^3.3||^4.3", "symfony/console": "^2.6||^4.0", "symfony/dependency-injection": "^3.3||^4.3", "symfony/process": "^2.1||^4.1" }, - "conflict": { - "symfony/process": "^4.2" - }, "require-dev": { "phpmd/phpmd": "@stable", "phpunit/phpunit": "^6.2", diff --git a/config/services.xml b/config/services.xml index 24ffbcc8..763a87c9 100644 --- a/config/services.xml +++ b/config/services.xml @@ -13,5 +13,6 @@ + diff --git a/src/Shell/PackageNotFoundException.php b/src/Shell/PackageNotFoundException.php new file mode 100644 index 00000000..6a4c5119 --- /dev/null +++ b/src/Shell/PackageNotFoundException.php @@ -0,0 +1,17 @@ +directoryList = $directoryList; + $this->repository = $composer->getLocker()->getLockedRepository(); } /** @@ -37,8 +48,22 @@ public function __construct(DirectoryList $directoryList) public function create(array $cmd): Process { return new Process( - implode(' ', $cmd), + $this->processSupportsArrayParam() ? $cmd : implode(' ', $cmd), $this->directoryList->getMagentoRoot() ); } + + /** + * Test if symfony/process is current enough to support an array for its first parameter. + */ + private function processSupportsArrayParam(): bool + { + $package = $this->repository->findPackage('symfony/process', '*'); + + if ($package === null) { + throw new PackageNotFoundException('Could not find symfony/process package.'); + } + + return Comparator::greaterThanOrEqualTo($package->getVersion(), self::ARRAY_PARAM_MIN_VERSION); + } } From f8f827c2a1337aa4bfb29b181d6aa662b26a482a Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Wed, 12 Feb 2020 15:18:11 -0600 Subject: [PATCH 023/157] MC-31387: Paypal issue with region on 2.3.4 --- patches.json | 3 + ..._fix_paypal_issue_with_region__2.3.4.patch | 67 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 patches/MC-31387__fix_paypal_issue_with_region__2.3.4.patch diff --git a/patches.json b/patches.json index a64fce05..37af3ae7 100644 --- a/patches.json +++ b/patches.json @@ -238,6 +238,9 @@ }, "Fix for multi-site configuration issue": { "2.2.4": "MAGETWO-92926__fix_for_multi-site_configuration_issue__2.2.4.patch" + }, + "Fix PayPal issue with region": { + "2.3.4": "MC-31387__fix_paypal_issue_with_region__2.3.4.patch" } }, "monolog/monolog": { diff --git a/patches/MC-31387__fix_paypal_issue_with_region__2.3.4.patch b/patches/MC-31387__fix_paypal_issue_with_region__2.3.4.patch new file mode 100644 index 00000000..2e7849c2 --- /dev/null +++ b/patches/MC-31387__fix_paypal_issue_with_region__2.3.4.patch @@ -0,0 +1,67 @@ +diff -Nuar a/vendor/magento/module-paypal/Model/Api/Nvp.php b/vendor/magento/module-paypal/Model/Api/Nvp.php +--- a/vendor/magento/module-paypal/Model/Api/Nvp.php ++++ b/vendor/magento/module-paypal/Model/Api/Nvp.php +@@ -1512,17 +1512,17 @@ + } + // attempt to fetch region_id from directory + if ($address->getCountryId() && $address->getRegion()) { +- $regions = $this->_countryFactory->create()->loadByCode( +- $address->getCountryId() +- )->getRegionCollection()->addRegionCodeOrNameFilter( +- $address->getRegion() +- )->setPageSize( +- 1 +- ); ++ $regions = $this->_countryFactory->create() ++ ->getRegionCollection() ++ ->addCountryFilter($address->getCountryId()) ++ ->addRegionCodeOrNameFilter($address->getRegion()) ++ ->setPageSize(1); + $regionItems = $regions->getItems(); +- $region = array_shift($regionItems); +- $address->setRegionId($region->getId()); +- $address->setExportedKeys(array_merge($address->getExportedKeys(), ['region_id'])); ++ if (count($regionItems)) { ++ $region = array_shift($regionItems); ++ $address->setRegionId($region->getId()); ++ $address->setExportedKeys(array_merge($address->getExportedKeys(), ['region_id'])); ++ } + } + } + +@@ -1624,7 +1624,7 @@ + case 'year': + return 'Year'; + default: +- break; ++ return ''; + } + } + +@@ -1653,7 +1653,7 @@ + case 'active': + return 'Active'; + default: +- break; ++ return ''; + } + } + +@@ -1694,7 +1694,7 @@ + case 'Voided': + return \Magento\Paypal\Model\Info::PAYMENTSTATUS_VOIDED; + default: +- break; ++ return null; + } + } + +@@ -1712,7 +1712,7 @@ + case \Magento\Paypal\Model\Pro::PAYMENT_REVIEW_DENY: + return 'Deny'; + default: +- break; ++ return null; + } + } + From 3a85f8de93fb3ecbd0f1dcdb4b579a808117dd28 Mon Sep 17 00:00:00 2001 From: Ben Batschelet Date: Thu, 13 Feb 2020 15:26:50 -0600 Subject: [PATCH 024/157] Remove conflict from composer.json --- composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/composer.json b/composer.json index d49b77bb..84c3baf7 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,6 @@ "symfony/dependency-injection": "^3.3||^4.3", "symfony/process": "^2.1||^4.1" }, - "conflict": { - "symfony/process": "^4.2" - }, "require-dev": { "phpmd/phpmd": "@stable", "phpunit/phpunit": "^6.2", From 0630552b374eb089737db28f690a0b13c73762f2 Mon Sep 17 00:00:00 2001 From: PhantomDraven Date: Tue, 18 Feb 2020 16:58:30 +0100 Subject: [PATCH 025/157] Fix syntax error on composer.json of Magento_Amqp (#25) --- ...O-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch | 2 +- ...O-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch index feebf9d6..194df616 100644 --- a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch +++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.1.patch @@ -234,7 +234,7 @@ diff -Nuar a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-am "php": "~7.1.3||~7.2.0" }, + "suggest": { -+ "magento/module-asynchronous-operations": "*", ++ "magento/module-asynchronous-operations": "*" + }, "type": "magento2-module", "license": [ diff --git a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch index 99257df2..1cc4a23c 100644 --- a/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch +++ b/patches/MAGETWO-99902__pass_store_view_scope_in_async_web_api__2.3.2.patch @@ -234,7 +234,7 @@ diff -Nuar a/vendor/magento/module-amqp/composer.json b/vendor/magento/module-am "php": "~7.1.3||~7.2.0" }, + "suggest": { -+ "magento/module-asynchronous-operations": "*", ++ "magento/module-asynchronous-operations": "*" + }, "type": "magento2-module", "license": [ From c5bf3397f91956f19e5d3bdf75f15a9ed7e07ad2 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak Date: Wed, 19 Feb 2020 15:27:02 -0600 Subject: [PATCH 026/157] MAGECLOUD-5359: Patches 1.0.2 fail on magento version 2.1.4 with php 7.0 (#26) --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 84c3baf7..059f2462 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.1", + "version": "1.0.2", "license": "OSL-3.0", "require": { "php": "^7.0", @@ -10,7 +10,7 @@ "composer/composer": "@stable", "composer/semver": "^1.5", "symfony/config": "^3.3||^4.4", - "symfony/console": "^2.8||^4.0", + "symfony/console": "^2.6||^4.0", "symfony/dependency-injection": "^3.3||^4.3", "symfony/process": "^2.1||^4.1" }, From cde33df763995510e466fdf5eb88144a8a477c62 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak Date: Mon, 24 Feb 2020 10:34:09 -0600 Subject: [PATCH 027/157] MAGECLOUD-5323: Update Cloud projects with proper licenses (#28) --- .github/CODE_OF_CONDUCT.md | 46 +++++++++++++++++++ .github/CONTRIBUTING.md | 39 ++++++++++++++++ .../bug_report.md} | 8 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 ++++++++ 4 files changed, 112 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md rename .github/{ISSUE_TEMPLATE.md => ISSUE_TEMPLATE/bug_report.md} (89%) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..4e82725a --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at engcom@magento.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..0a308aba --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing to Magento Cloud Patches code + +Use the GitHub fork & pull model contribution model to submit your code contributions to the Magento Cloud Patches codebase. +In this contribution model, you maintain your own [fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/working-with-forks) of the Magento Cloud Patches repository, and create a [pull request](https://help.github.com/articles/about-pull-requests/) to submit your proposed changes to the base repository. For details on the fork & pull contribution model, see the [Beginners guide](https://github.com/magento/magento2/wiki/Getting-Started). + +Contributions can take the form of new features, changes to existing features, tests, bug fixes, or optimizations. You can also contribute new or updated documentation. + +The Magento Cloud development team and community maintainers review all issues and contributions submitted by the developer community in first in, first out order (FIFO). During the review process, reviewers might notify a contributor to request clarification on the proposed changes. + +## Prerequisites + +You must have a [GitHub account](https://help.github.com/en/github/getting-started-with-github/signing-up-for-a-new-github-account) with [two-factor authentication](https://help.github.com/en/github/authenticating-to-github/configuring-two-factor-authentication) enabled to contribute to Magento repositories. We also recommend creating a [personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) to use when interacting with GitHub from scripts or from the command line. + +## Contribution requirements + +1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.3/coding-standards/bk-coding-standards.html). +2. When you submit a Pull request (PR), write a meaningful description to explain the purpose of your contribution. Comprehensive descriptions increase the chances that a pull request can be merged quickly, without requests for additional clarification. See the [Magento Cloud Patches Pull Request Template](https://github.com/magento/magento-cloud-patches/blob/develop/.github/PULL_REQUEST_TEMPLATE.md) for more information. +3. Commits must be accompanied by meaningful commit messages. +4. If your PR includes bug fixes, provide a step-by-step description of how to reproduce the bug in the pull request description. +3. If your PR includes new logic or new features, you must also submit the following information along with the pull request + * Unit/integration test coverage + * Proposed documentation updates: Submit developer documentation contributions to the [Magento DevDocs repository](https://github.com/magento/devdocs/blob/master/.github/CONTRIBUTING.md). Submit updates to Magento user documentation to the [Magento Merchant documentation repository](https://github.com/magento/merchdocs/blob/master/.github/CONTRIBUTING.md). +4. For larger features or changes, [open an issue](https://github.com/magento/magento-cloud-patches/issues/new) to discuss the proposed changes prior to development. Discussing the updates in advance can prevent duplicate or unnecessary effort and allow other contributors to provide input. + +## Contribution process +1. Search current [listed issues](https://github.com/magento/magento-cloud-patches/issues) (open or closed) for similar proposals of intended contribution before starting work on a new contribution. +2. Review and sign the [Contributor License Agreement (CLA)](https://opensource.adobe.com/cla.html) if this is your first time contributing. You only need to sign the CLA once. +3. Create and test your work. +4. Fork the Cloud Patches repository according to the [Fork A Repository instructions](https://github.com/magento/magento2/wiki/Forking-and-Branching) and when you are ready to send us a pull request – follow the [Create A Pull Request instructions](https://github.com/magento/magento2/wiki/Working-Issues-and-PRs#submitting-prs). +5. After you submit the pull request, the Magento Cloud development team will review the contribution and collaborate with you as needed to incorporate your proposed changes. + +## Code of Conduct + +This project is released with a Contributor Code of Conduct. We expect you to agree to its terms when participating in this project. +The full text is available in the repository [Wiki](https://github.com/magento/magento2/wiki/Magento-Code-of-Conduct). + +## Connecting with Community! + +Need to find a project? Check out the [Slack Channels](https://github.com/magento/magento2/wiki/Slack-Channels) (with listed project info) and the [Magento Community Portal](https://opensource.magento.com/). diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 89% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/bug_report.md index 12ad4e45..f0634f46 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,3 +1,11 @@ +--- +name: Bug report +about: Technical issue with the Magento Cloud Patches +title: '' +labels: '' +assignees: '' +--- + +Add link to Magento DevDocs PR or Issue, if needed. + ### Contribution checklist - [ ] Pull request has a meaningful description of its purpose + - [ ] Pull request introduces user-facing changes and includes meaningful updates for any required release notes and documentation changes - [ ] All commits are accompanied by meaningful commit messages - [ ] All new or changed code is covered with unit/integration tests (if applicable) - - [ ] All automated tests passed successfully (all builds on Travis CI are green) From 272b7e38cbd6826f035039fd19604c50446cccb8 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak Date: Thu, 18 Jun 2020 09:05:56 -0500 Subject: [PATCH 044/157] Bump version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b4984403..8110ba29 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.4", + "version": "1.0.5", "license": "OSL-3.0", "require": { "php": "^7.0", From db5ca6e70ac8fc4e5b7cd58cc384184dff29d645 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Fri, 10 Jul 2020 15:25:08 -0500 Subject: [PATCH 045/157] MCLOUD-6423: MC-34580: Magento Quality Patches (#55) --- .travis.yml | 1 + bin/magento-patches | 10 + codeception.dist.yml | 1 + composer.json | 8 +- config/services.xml | 66 +++- patches.json | 16 +- ...e_issues_with_cron_schedule__2.1.10.patch} | 0 ...__zendframework1_use_TLS_1.2__2.1.4.patch} | 0 ...3__fix_monolog_slack_handler__2.1.x.patch} | 0 ...configure_max_execution_time__2.2.0.patch} | 0 ...configure_max_execution_time__2.3.1.patch} | 0 ...0__fix_amazon_payment_module__2.2.6.patch} | 0 ...029__add_processor_interface__2.1.x.patch} | 0 ...nfig_after_reading_from_file__2.3.3.patch} | 0 src/App/Container.php | 7 +- src/App/Logger.php | 61 ++++ src/App/Logger/LineFormatterFactory.php | 24 ++ src/App/RuntimeException.php | 15 + src/Application.php | 2 + src/Command/AbstractCommand.php | 26 ++ src/Command/Apply.php | 122 +++++-- src/Command/Patch/Manager.php | 163 --------- .../Process/Action/ActionInterface.php | 29 ++ src/Command/Process/Action/ActionPool.php | 52 +++ .../Process/Action/ApplyOptionalAction.php | 195 +++++++++++ .../Process/Action/ConfirmRequiredAction.php | 94 ++++++ .../Action/ProcessDeprecatedAction.php | 181 ++++++++++ src/Command/Process/Action/RevertAction.php | 188 +++++++++++ .../Process/Action/ReviewAppliedAction.php | 84 +++++ src/Command/Process/ApplyLocal.php | 105 ++++++ src/Command/Process/ApplyOptional.php | 68 ++++ src/Command/Process/ApplyRequired.php | 90 +++++ src/Command/Process/ProcessInterface.php | 28 ++ src/Command/Process/Renderer.php | 204 ++++++++++++ src/Command/Process/Revert.php | 74 +++++ src/Command/Process/ShowStatus.php | 136 ++++++++ src/Command/Revert.php | 129 ++++++++ src/Command/Status.php | 88 +++++ src/Composer/MagentoVersion.php | 83 +++++ src/Composer/Package.php | 44 +++ src/Console/ConfirmationQuestionFactory.php | 29 ++ src/Console/TableFactory.php | 28 ++ src/Filesystem/FileList.php | 16 + ...dException.php => FileSystemException.php} | 4 +- src/Filesystem/Filesystem.php | 58 +++- src/Patch/AggregatedPatchFactory.php | 162 +++++++++ src/Patch/Aggregator.php | 51 +++ src/Patch/Applier.php | 151 +++------ src/Patch/Collector/CloudCollector.php | 158 +++++++++ src/Patch/Collector/CollectorException.php | 17 + src/Patch/Collector/LocalCollector.php | 61 ++++ src/Patch/Collector/QualityCollector.php | 279 ++++++++++++++++ src/Patch/Data/AggregatedPatch.php | 157 +++++++++ src/Patch/Data/AggregatedPatchInterface.php | 72 ++++ src/Patch/Data/Patch.php | 206 ++++++++++++ src/Patch/Data/PatchInterface.php | 108 ++++++ src/Patch/Environment.php | 31 ++ src/Patch/FilterFactory.php | 46 +++ src/Patch/PatchBuilder.php | 266 +++++++++++++++ src/Patch/PatchIntegrityException.php | 17 + src/Patch/Pool/LocalPool.php | 41 +++ src/Patch/Pool/OptionalPool.php | 212 ++++++++++++ src/Patch/Pool/PatchNotFoundException.php | 17 + src/Patch/Pool/RequiredPool.php | 43 +++ src/Patch/RevertValidator.php | 71 ++++ src/Patch/SourceProvider.php | 142 ++++++++ src/Patch/SourceProviderException.php | 17 + src/Patch/Status/LocalResolver.php | 68 ++++ src/Patch/Status/OptionalResolver.php | 203 ++++++++++++ src/Patch/Status/ResolverInterface.php | 22 ++ src/Patch/Status/StatusPool.php | 91 +++++ .../Status/StatusResolverException.php} | 6 +- src/Shell/ProcessFactory.php | 12 +- .../Functional/Acceptance/AbstractCest.php | 14 + .../Functional/Acceptance/AcceptanceCest.php | 5 +- src/Test/Unit/Command/ApplyTest.php | 171 +++++++++- src/Test/Unit/Command/Patch/ManagerTest.php | 209 ------------ .../Action/ApplyOptionalActionTest.php | 270 +++++++++++++++ .../Action/ConfirmRequiredActionTest.php | 192 +++++++++++ .../Action/ProcessDeprecatedActionTest.php | 289 ++++++++++++++++ .../Process/Action/RevertActionTest.php | 259 +++++++++++++++ .../Action/ReviewAppliedActionTest.php | 145 ++++++++ .../Unit/Command/Process/ApplyLocalTest.php | 173 ++++++++++ .../Command/Process/ApplyOptionalTest.php | 116 +++++++ .../Command/Process/ApplyRequiredTest.php | 150 +++++++++ .../Unit/Command/Process/RendererTest.php | 198 +++++++++++ src/Test/Unit/Command/Process/RevertTest.php | 128 ++++++++ .../Unit/Command/Process/ShowStatusTest.php | 155 +++++++++ src/Test/Unit/Command/RevertTest.php | 152 +++++++++ src/Test/Unit/Command/StatusTest.php | 119 +++++++ src/Test/Unit/Composer/MagentoVersionTest.php | 90 +++++ src/Test/Unit/Filesystem/FileListTest.php | 19 ++ .../Unit/Patch/AggregatedPatchFactoryTest.php | 126 +++++++ src/Test/Unit/Patch/AggregatorTest.php | 88 +++++ src/Test/Unit/Patch/ApplierTest.php | 296 ++++++++--------- .../Patch/Collector/CloudCollectorTest.php | 299 +++++++++++++++++ .../Collector/Fixture/cloud_config_valid.php | 25 ++ .../Fixture}/m2-hotfixes/patch1.patch | 0 .../Fixture}/m2-hotfixes/patch2.patch | 0 .../Collector/Fixture}/m2-hotfixes/readme.md | 0 .../Fixture/quality_config_invalid.php | 22 ++ .../Fixture/quality_config_valid.php | 43 +++ .../Patch/Collector/LocalCollectorTest.php | 88 +++++ .../Patch/Collector/QualityCollectorTest.php | 226 +++++++++++++ src/Test/Unit/Patch/EnvironmentTest.php | 31 ++ src/Test/Unit/Patch/FilterFactoryTest.php | 88 +++++ .../Fixture/MC-1__testfixture__1.1.patch | 21 ++ src/Test/Unit/Patch/PatchBuilderTest.php | 135 ++++++++ src/Test/Unit/Patch/Pool/LocalPoolTest.php | 55 ++++ src/Test/Unit/Patch/Pool/OptionalPoolTest.php | 310 ++++++++++++++++++ src/Test/Unit/Patch/Pool/RequiredPoolTest.php | 54 +++ src/Test/Unit/Patch/RevertValidatorTest.php | 120 +++++++ src/Test/Unit/Patch/SourceProviderTest.php | 166 ++++++++++ .../Unit/Patch/Status/LocalResolverTest.php | 132 ++++++++ .../Patch/Status/OptionalResolverTest.php | 256 +++++++++++++++ src/Test/Unit/Patch/Status/StatusPoolTest.php | 54 +++ 116 files changed, 10064 insertions(+), 681 deletions(-) create mode 100755 bin/magento-patches rename patches/{MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch => MAGECLOUD-2427__resolve_issues_with_cron_schedule__2.1.10.patch} (100%) rename patches/{MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch => MAGECLOUD-2521__zendframework1_use_TLS_1.2__2.1.4.patch} (100%) rename patches/{MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch => MAGECLOUD-2793__fix_monolog_slack_handler__2.1.x.patch} (100%) rename patches/{MAGECLOUD-2822__configure_max_execution_time.patch => MAGECLOUD-2822__configure_max_execution_time__2.2.0.patch} (100%) rename patches/{MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch => MAGECLOUD-2822__configure_max_execution_time__2.3.1.patch} (100%) rename patches/{MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch => MAGECLOUD-2850__fix_amazon_payment_module__2.2.6.patch} (100%) rename patches/{MAGECLOUD-6029__add_processor_interface_2.1.x.patch => MAGECLOUD-6029__add_processor_interface__2.1.x.patch} (100%) rename patches/{MCLOUD-5650__hold_deployment_config_after_reading_from_file.patch => MCLOUD-5650__hold_deployment_config_after_reading_from_file__2.3.3.patch} (100%) create mode 100644 src/App/Logger.php create mode 100644 src/App/Logger/LineFormatterFactory.php create mode 100644 src/App/RuntimeException.php create mode 100644 src/Command/AbstractCommand.php delete mode 100644 src/Command/Patch/Manager.php create mode 100644 src/Command/Process/Action/ActionInterface.php create mode 100644 src/Command/Process/Action/ActionPool.php create mode 100644 src/Command/Process/Action/ApplyOptionalAction.php create mode 100644 src/Command/Process/Action/ConfirmRequiredAction.php create mode 100644 src/Command/Process/Action/ProcessDeprecatedAction.php create mode 100644 src/Command/Process/Action/RevertAction.php create mode 100644 src/Command/Process/Action/ReviewAppliedAction.php create mode 100644 src/Command/Process/ApplyLocal.php create mode 100644 src/Command/Process/ApplyOptional.php create mode 100644 src/Command/Process/ApplyRequired.php create mode 100644 src/Command/Process/ProcessInterface.php create mode 100644 src/Command/Process/Renderer.php create mode 100644 src/Command/Process/Revert.php create mode 100644 src/Command/Process/ShowStatus.php create mode 100644 src/Command/Revert.php create mode 100644 src/Command/Status.php create mode 100644 src/Composer/MagentoVersion.php create mode 100644 src/Composer/Package.php create mode 100644 src/Console/ConfirmationQuestionFactory.php create mode 100644 src/Console/TableFactory.php rename src/Filesystem/{FileNotFoundException.php => FileSystemException.php} (72%) create mode 100644 src/Patch/AggregatedPatchFactory.php create mode 100644 src/Patch/Aggregator.php create mode 100644 src/Patch/Collector/CloudCollector.php create mode 100644 src/Patch/Collector/CollectorException.php create mode 100644 src/Patch/Collector/LocalCollector.php create mode 100644 src/Patch/Collector/QualityCollector.php create mode 100644 src/Patch/Data/AggregatedPatch.php create mode 100644 src/Patch/Data/AggregatedPatchInterface.php create mode 100644 src/Patch/Data/Patch.php create mode 100644 src/Patch/Data/PatchInterface.php create mode 100644 src/Patch/Environment.php create mode 100644 src/Patch/FilterFactory.php create mode 100644 src/Patch/PatchBuilder.php create mode 100644 src/Patch/PatchIntegrityException.php create mode 100644 src/Patch/Pool/LocalPool.php create mode 100644 src/Patch/Pool/OptionalPool.php create mode 100644 src/Patch/Pool/PatchNotFoundException.php create mode 100644 src/Patch/Pool/RequiredPool.php create mode 100644 src/Patch/RevertValidator.php create mode 100644 src/Patch/SourceProvider.php create mode 100644 src/Patch/SourceProviderException.php create mode 100644 src/Patch/Status/LocalResolver.php create mode 100644 src/Patch/Status/OptionalResolver.php create mode 100644 src/Patch/Status/ResolverInterface.php create mode 100644 src/Patch/Status/StatusPool.php rename src/{Command/Patch/ManagerException.php => Patch/Status/StatusResolverException.php} (58%) delete mode 100644 src/Test/Unit/Command/Patch/ManagerTest.php create mode 100644 src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php create mode 100644 src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php create mode 100644 src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php create mode 100644 src/Test/Unit/Command/Process/Action/RevertActionTest.php create mode 100644 src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php create mode 100644 src/Test/Unit/Command/Process/ApplyLocalTest.php create mode 100644 src/Test/Unit/Command/Process/ApplyOptionalTest.php create mode 100644 src/Test/Unit/Command/Process/ApplyRequiredTest.php create mode 100644 src/Test/Unit/Command/Process/RendererTest.php create mode 100644 src/Test/Unit/Command/Process/RevertTest.php create mode 100644 src/Test/Unit/Command/Process/ShowStatusTest.php create mode 100644 src/Test/Unit/Command/RevertTest.php create mode 100644 src/Test/Unit/Command/StatusTest.php create mode 100644 src/Test/Unit/Composer/MagentoVersionTest.php create mode 100644 src/Test/Unit/Patch/AggregatedPatchFactoryTest.php create mode 100644 src/Test/Unit/Patch/AggregatorTest.php create mode 100644 src/Test/Unit/Patch/Collector/CloudCollectorTest.php create mode 100644 src/Test/Unit/Patch/Collector/Fixture/cloud_config_valid.php rename src/Test/Unit/{Command/Patch/_files => Patch/Collector/Fixture}/m2-hotfixes/patch1.patch (100%) rename src/Test/Unit/{Command/Patch/_files => Patch/Collector/Fixture}/m2-hotfixes/patch2.patch (100%) rename src/Test/Unit/{Command/Patch/_files => Patch/Collector/Fixture}/m2-hotfixes/readme.md (100%) create mode 100644 src/Test/Unit/Patch/Collector/Fixture/quality_config_invalid.php create mode 100644 src/Test/Unit/Patch/Collector/Fixture/quality_config_valid.php create mode 100644 src/Test/Unit/Patch/Collector/LocalCollectorTest.php create mode 100644 src/Test/Unit/Patch/Collector/QualityCollectorTest.php create mode 100644 src/Test/Unit/Patch/EnvironmentTest.php create mode 100644 src/Test/Unit/Patch/FilterFactoryTest.php create mode 100644 src/Test/Unit/Patch/Fixture/MC-1__testfixture__1.1.patch create mode 100644 src/Test/Unit/Patch/PatchBuilderTest.php create mode 100644 src/Test/Unit/Patch/Pool/LocalPoolTest.php create mode 100644 src/Test/Unit/Patch/Pool/OptionalPoolTest.php create mode 100644 src/Test/Unit/Patch/Pool/RequiredPoolTest.php create mode 100644 src/Test/Unit/Patch/RevertValidatorTest.php create mode 100644 src/Test/Unit/Patch/SourceProviderTest.php create mode 100644 src/Test/Unit/Patch/Status/LocalResolverTest.php create mode 100644 src/Test/Unit/Patch/Status/OptionalResolverTest.php create mode 100644 src/Test/Unit/Patch/Status/StatusPoolTest.php diff --git a/.travis.yml b/.travis.yml index 55fb2abf..890b222f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,6 +59,7 @@ install: - composer config http-basic.repo.magento.com ${REPO_USERNAME_CE} ${REPO_PASSWORD_CE} - composer config github-oauth.github.com ${GITHUB_TOKEN} - if [ -n "${MCD_VERSION}" ] && [ $TRAVIS_PHP_VERSION != "7.0" ]; then composer config repositories.mcd git git@github.com:magento/magento-cloud-docker.git && composer require "magento/magento-cloud-docker:${MCD_VERSION}" --no-update; fi; + - if [ -n "${MQP_VERSION}" ]; then composer config repositories.mqp git git@github.com:magento/quality-patches.git && composer require "magento/quality-patches:${MQP_VERSION}" --no-update; fi; - composer update -n --no-suggest before_script: diff --git a/bin/magento-patches b/bin/magento-patches new file mode 100755 index 00000000..99d5e7dc --- /dev/null +++ b/bin/magento-patches @@ -0,0 +1,10 @@ +#!/usr/bin/env php +run(); diff --git a/codeception.dist.yml b/codeception.dist.yml index 710cb349..2badeb23 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -20,6 +20,7 @@ modules: mcd_repo: "https://github.com/magento/magento-cloud-docker.git" mcc_repo: "https://github.com/magento/magento-cloud-components.git" mcp_repo: "https://github.com/magento/magento-cloud-patches.git" + mqp_repo: "https://github.com/magento/quality-patches.git" composer_magento_username: "%REPO_USERNAME%" composer_magento_password: "%REPO_PASSWORD%" composer_github_token: "%GITHUB_TOKEN%" diff --git a/composer.json b/composer.json index 8110ba29..49d6fab5 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,10 @@ "symfony/config": "^3.3||^4.4", "symfony/console": "^2.6||^4.0", "symfony/dependency-injection": "^3.3||^4.3", - "symfony/process": "^2.1||^4.1" + "symfony/process": "^2.1||^4.1", + "symfony/proxy-manager-bridge": "^3.3||^4.3", + "monolog/monolog": "^1.16", + "magento/quality-patches": "^1.0.0" }, "require-dev": { "codeception/codeception": "^2.5.3", @@ -22,7 +25,8 @@ "squizlabs/php_codesniffer": "^3.0" }, "bin": [ - "bin/ece-patches" + "bin/ece-patches", + "bin/magento-patches" ], "autoload": { "psr-4": { diff --git a/config/services.xml b/config/services.xml index 763a87c9..958607b9 100644 --- a/config/services.xml +++ b/config/services.xml @@ -6,13 +6,75 @@ + + + - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches.json b/patches.json index 4f769276..7ec44660 100644 --- a/patches.json +++ b/patches.json @@ -116,7 +116,7 @@ ">=2.3.1 <2.3.3": "MAGECLOUD-3913__fix_problems_with_consumer_runners_on_cloud_clusters__2.3.1.patch" }, "Resolve Issues with Cron Schedule": { - "2.1.10 - 2.1.14 || 2.2.2 - 2.2.5": "MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch" + "2.1.10 - 2.1.14 || 2.2.2 - 2.2.5": "MAGECLOUD-2427__resolve_issues_with_cron_schedule__2.1.10.patch" }, "Fix timezone parsing for Cron": { "2.1.4": "MAGECLOUD-2602__fix_timezone_parsing_for_cron__2.1.4.patch", @@ -135,7 +135,7 @@ "2.1.4 - 2.2.5": "MAGECLOUD-2445__do_not_run_cron_when_it_is_disabled__2.1.4.patch" }, "Zendframework1 should use TLS 1.2": { - ">=2.1.4 <2.3": "MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch" + ">=2.1.4 <2.3": "MAGECLOUD-2521__zendframework1_use_TLS_1.2__2.1.4.patch" }, "The recursion detected error during deployment": { "2.2.0 - 2.2.6": "MAGECLOUD-2173__the_recursion_error_during_deployment__2.2.0.patch" @@ -145,15 +145,15 @@ "2.2.6": "MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.6.patch" }, "Fix for DI compilation with Amazon_Payment module": { - "2.2.6": "MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch" + "2.2.6": "MAGECLOUD-2850__fix_amazon_payment_module__2.2.6.patch" }, "Add the possibility to install Magento without admin creation" : { "2.1.4 - 2.2.1": "MAGECLOUD-2573__installation_without_admin_creation__2.1.4.patch", "2.2.2 - 2.2.7": "MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch" }, "Add the possibility to configure max execution time during static content deployment": { - "2.2.0 - 2.2.8 || 2.3.0": "MAGECLOUD-2822__configure_max_execution_time.patch", - "2.3.1": "MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch" + "2.2.0 - 2.2.8 || 2.3.0": "MAGECLOUD-2822__configure_max_execution_time__2.2.0.patch", + "2.3.1": "MAGECLOUD-2822__configure_max_execution_time__2.3.1.patch" }, "Suppress PDO warnings to work around PHP bugs #63812, #74401": { "2.1.4": "MAGECLOUD-2820__implement_isolated_connections_mechanism__2.1.4.patch", @@ -232,7 +232,7 @@ ">=2.3.5 <2.4.0": "MAGECLOUD-5069__fpc_is_getting_disabled_during_deployments__2.3.5.patch" }, "Hold deployment config after reading from file": { - ">=2.3.3 <2.3.6": "MCLOUD-5650__hold_deployment_config_after_reading_from_file.patch" + ">=2.3.3 <2.3.6": "MCLOUD-5650__hold_deployment_config_after_reading_from_file__2.3.3.patch" }, "Pagination Not working - product_list_limit=all": { ">=2.3.2 <2.3.4": "MCLOUD-5684__pagination_not_working_product_list_limit_all__2.3.2.patch", @@ -274,8 +274,8 @@ }, "monolog/monolog": { "Fix monolog Slack Handler bug for magento 2.1.x": { - "1.16.0": "MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch", - ">=1.16.0 <1.24.0": "MAGECLOUD-6029__add_processor_interface_2.1.x.patch" + "1.16.0": "MAGECLOUD-2793__fix_monolog_slack_handler__2.1.x.patch", + ">=1.16.0 <1.24.0": "MAGECLOUD-6029__add_processor_interface__2.1.x.patch" } }, "colinmollenhour/cache-backend-redis": { diff --git a/patches/MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch b/patches/MAGECLOUD-2427__resolve_issues_with_cron_schedule__2.1.10.patch similarity index 100% rename from patches/MAGECLOUD-2427__resolve_issues_with_cron_schedule.patch rename to patches/MAGECLOUD-2427__resolve_issues_with_cron_schedule__2.1.10.patch diff --git a/patches/MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch b/patches/MAGECLOUD-2521__zendframework1_use_TLS_1.2__2.1.4.patch similarity index 100% rename from patches/MAGECLOUD-2521__zendframework1_use_TLS_1.2.patch rename to patches/MAGECLOUD-2521__zendframework1_use_TLS_1.2__2.1.4.patch diff --git a/patches/MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch b/patches/MAGECLOUD-2793__fix_monolog_slack_handler__2.1.x.patch similarity index 100% rename from patches/MAGECLOUD-2793__fix_monolog_slack_handler_2.1.x.patch rename to patches/MAGECLOUD-2793__fix_monolog_slack_handler__2.1.x.patch diff --git a/patches/MAGECLOUD-2822__configure_max_execution_time.patch b/patches/MAGECLOUD-2822__configure_max_execution_time__2.2.0.patch similarity index 100% rename from patches/MAGECLOUD-2822__configure_max_execution_time.patch rename to patches/MAGECLOUD-2822__configure_max_execution_time__2.2.0.patch diff --git a/patches/MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch b/patches/MAGECLOUD-2822__configure_max_execution_time__2.3.1.patch similarity index 100% rename from patches/MAGECLOUD-2822__configure_max_execution_time_2.3.1.patch rename to patches/MAGECLOUD-2822__configure_max_execution_time__2.3.1.patch diff --git a/patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch b/patches/MAGECLOUD-2850__fix_amazon_payment_module__2.2.6.patch similarity index 100% rename from patches/MAGECLOUD-2850_fix_amazon_payment_module__2.2.6.patch rename to patches/MAGECLOUD-2850__fix_amazon_payment_module__2.2.6.patch diff --git a/patches/MAGECLOUD-6029__add_processor_interface_2.1.x.patch b/patches/MAGECLOUD-6029__add_processor_interface__2.1.x.patch similarity index 100% rename from patches/MAGECLOUD-6029__add_processor_interface_2.1.x.patch rename to patches/MAGECLOUD-6029__add_processor_interface__2.1.x.patch diff --git a/patches/MCLOUD-5650__hold_deployment_config_after_reading_from_file.patch b/patches/MCLOUD-5650__hold_deployment_config_after_reading_from_file__2.3.3.patch similarity index 100% rename from patches/MCLOUD-5650__hold_deployment_config_after_reading_from_file.patch rename to patches/MCLOUD-5650__hold_deployment_config_after_reading_from_file__2.3.3.patch diff --git a/src/App/Container.php b/src/App/Container.php index 3620b926..65c0392c 100644 --- a/src/App/Container.php +++ b/src/App/Container.php @@ -10,6 +10,7 @@ use Composer; use Magento\CloudPatches\Filesystem\DirectoryList; use Psr\Container\ContainerInterface; +use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -37,6 +38,8 @@ class Container implements ContainerInterface public function __construct(string $basePath, string $magentoBasePath) { $containerBuilder = new ContainerBuilder(); + $containerBuilder->setProxyInstantiator(new RuntimeInstantiator()); + $containerBuilder->set('container', $containerBuilder); $containerBuilder->setDefinition('container', new Definition(__CLASS__)) ->setArguments([$basePath, $magentoBasePath]); @@ -72,7 +75,9 @@ private function createComposerInstance(DirectoryList $directoryList): Composer\ new Composer\IO\BufferIO(), $composerFile, false, - $directoryList->getMagentoRoot() + is_dir($directoryList->getMagentoRoot()) + ? $directoryList->getMagentoRoot() + : $directoryList->getRoot() ); return $composer; diff --git a/src/App/Logger.php b/src/App/Logger.php new file mode 100644 index 00000000..af7f9882 --- /dev/null +++ b/src/App/Logger.php @@ -0,0 +1,61 @@ +getPatchLog(); + $logDir = $filesystem->getDirectory($logPath); + $filesystem->createDirectory($logDir); + + if ($filesystem->isWritable($logDir)) { + try { + $handlerInstance = new StreamHandler($logPath, Logger::DEBUG); + $formatter = $lineFormatterFactory->create(); + $handlerInstance->setFormatter($formatter); + $handlers[] = $handlerInstance; + } catch (\Exception $e) { + $handlers[] = new NullHandler(); + } + } else { + $handlers[] = new NullHandler(); + } + + parent::__construct('default', $handlers); + } + + /** + * @inheritDoc + */ + public function info($message, array $context = []) + { + $message = strip_tags($message); + + parent::info($message, $context); + } +} diff --git a/src/App/Logger/LineFormatterFactory.php b/src/App/Logger/LineFormatterFactory.php new file mode 100644 index 00000000..aa571547 --- /dev/null +++ b/src/App/Logger/LineFormatterFactory.php @@ -0,0 +1,24 @@ +container->get(Command\Apply::class), + $this->container->get(Command\Revert::class), + $this->container->get(Command\Status::class) ]); } } diff --git a/src/Command/AbstractCommand.php b/src/Command/AbstractCommand.php new file mode 100644 index 00000000..06b5017b --- /dev/null +++ b/src/Command/AbstractCommand.php @@ -0,0 +1,26 @@ +manager = $manager; + private $applyOptional; + + /** + * @var ApplyRequired + */ + private $applyRequired; + + /** + * @var ApplyLocal + */ + private $applyLocal; + + /** + * @var Environment + */ + private $environment; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var MagentoVersion + */ + private $magentoVersion; + + /** + * @param ApplyRequired $applyRequired + * @param ApplyOptional $applyOptional + * @param ApplyLocal $applyLocal + * @param Environment $environment + * @param LoggerInterface $logger + * @param MagentoVersion $magentoVersion + */ + public function __construct( + ApplyRequired $applyRequired, + ApplyOptional $applyOptional, + ApplyLocal $applyLocal, + Environment $environment, + LoggerInterface $logger, + MagentoVersion $magentoVersion + ) { + $this->applyRequired = $applyRequired; + $this->applyOptional = $applyOptional; + $this->applyLocal = $applyLocal; + $this->environment = $environment; + $this->logger = $logger; + $this->magentoVersion = $magentoVersion; parent::__construct(self::NAME); } @@ -46,7 +102,11 @@ protected function configure() { $this->setName(self::NAME) ->setDescription('Apply patches') - ->addOption( + ->addArgument( + self::ARG_QUALITY_PATCHES, + InputArgument::IS_ARRAY, + 'List of quality patches to apply' + )->addOption( self::OPT_GIT_INSTALLATION, null, InputOption::VALUE_OPTIONAL, @@ -58,14 +118,38 @@ protected function configure() } /** - * {@inheritDoc} - * - * @throws ManagerException - * @throws ApplierException + * @inheritDoc */ public function execute(InputInterface $input, OutputInterface $output) { - $this->manager->applyComposerPatches($input, $output); - $this->manager->applyHotFixes($input, $output); + $deployedFromGit = $input->getOption(Apply::OPT_GIT_INSTALLATION); + if ($deployedFromGit) { + $output->writeln('Git-based installation. Skipping patches applying.'); + + return self::RETURN_SUCCESS; + } + + $this->logger->notice($this->magentoVersion->get()); + + try { + if ($this->environment->isCloud()) { + $this->applyRequired->run($input, $output); + $this->applyOptional->run($input, $output); + $this->applyLocal->run($input, $output); + } else { + $this->applyOptional->run($input, $output); + } + } catch (RuntimeException $e) { + $output->writeln('' . $e->getMessage() . ''); + $this->logger->error($e->getMessage()); + + return self::RETURN_FAILURE; + } catch (\Exception $e) { + $this->logger->critical($e); + + throw $e; + } + + return self::RETURN_SUCCESS; } } diff --git a/src/Command/Patch/Manager.php b/src/Command/Patch/Manager.php deleted file mode 100644 index d9434002..00000000 --- a/src/Command/Patch/Manager.php +++ /dev/null @@ -1,163 +0,0 @@ -applier = $applier; - $this->filesystem = $filesystem; - $this->fileList = $fileList; - $this->directoryList = $directoryList; - } - - /** - * Applies patches from composer.json file. - * Patches are applying from top to bottom of config list. - * - * ``` - * "colinmollenhour/credis" : { - * "Fix Redis issue": { - * "1.6": "patches/redis-pipeline.patch" - * } - * } - * - * Each patch must have corresponding constraint of target package, - * in one of the following format: - * - 1.6 - * - 1.6.* - * - ^1.6 - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @throws ManagerException - * @throws ApplierException - */ - public function applyComposerPatches(InputInterface $input, OutputInterface $output) - { - try { - $content = $this->filesystem->get($this->fileList->getPatches()); - } catch (FileNotFoundException $exception) { - throw new ManagerException($exception->getMessage(), $exception->getCode(), $exception); - } - - $patches = json_decode($content, true); - - if (!$patches) { - $output->writeln('Composer patches not found'); - - return; - } - - $deployedFromGit = $input->getOption(Apply::OPT_GIT_INSTALLATION); - - foreach ($patches as $packageName => $patchesInfo) { - foreach ($patchesInfo as $patchName => $packageInfo) { - if (!is_array($packageInfo)) { - throw new ManagerException('Wrong patch constraints'); - } - - foreach ($packageInfo as $constraint => $path) { - $message = $this->applier->apply( - (string)$path, - (string)$patchName, - (string)$packageName, - (string)$constraint, - (bool)$deployedFromGit - ); - - if (null !== $message) { - $output->writeln($message); - } - } - } - } - } - - /** - * Applies patches from root directory m2-hotfixes. - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @throws ApplierException - */ - public function applyHotFixes(InputInterface $input, OutputInterface $output) - { - $hotFixesDir = $this->directoryList->getMagentoRoot() . '/' . static::HOT_FIXES_DIR; - - if (!$this->filesystem->isDirectory($hotFixesDir)) { - $output->writeln('Hot-fixes directory was not found. Skipping'); - - return; - } - - $files = glob($hotFixesDir . '/*.patch'); - sort($files); - - $deployedFromGit = $input->getOption(Apply::OPT_GIT_INSTALLATION); - - $output->writeln('Applying hot-fixes'); - - foreach ($files as $file) { - $output->writeln( - $this->applier->applyFile($file, (bool)$deployedFromGit) - ); - } - } -} diff --git a/src/Command/Process/Action/ActionInterface.php b/src/Command/Process/Action/ActionInterface.php new file mode 100644 index 00000000..005c8d95 --- /dev/null +++ b/src/Command/Process/Action/ActionInterface.php @@ -0,0 +1,29 @@ +actions = $actions; + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output, array $patchFilter) + { + foreach ($this->actions as $action) { + $action->execute($input, $output, $patchFilter); + } + } +} diff --git a/src/Command/Process/Action/ApplyOptionalAction.php b/src/Command/Process/Action/ApplyOptionalAction.php new file mode 100644 index 00000000..5bc6a95f --- /dev/null +++ b/src/Command/Process/Action/ApplyOptionalAction.php @@ -0,0 +1,195 @@ +applier = $applier; + $this->optionalPool = $optionalPool; + $this->statusPool = $statusPool; + $this->renderer = $renderer; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output, array $patchFilter) + { + $patches = $this->getPatchList($patchFilter); + + $appliedPatches = []; + foreach ($patches as $patch) { + if ($this->statusPool->isApplied($patch->getId())) { + $this->printPatchWasApplied($output, $patch); + + continue; + } + + try { + $message = $this->applier->apply($patch->getPath(), $patch->getId()); + $this->renderer->printPatchInfo($output, $patch, $message); + $this->logger->info($message, ['file' => $patch->getPath()]); + array_push($appliedPatches, $patch); + } catch (ApplierException $exception) { + $this->printPatchApplyingFailed($output, $patch, $exception->getMessage()); + $this->rollback($output, $appliedPatches); + + throw new RuntimeException( + 'Applying optional patches ' . implode(' ', $patchFilter) . ' failed.', + $exception->getCode() + ); + } + } + } + + /** + * Prints and logs 'patch was applied' message. + * + * @param OutputInterface $output + * @param PatchInterface $patch + * + * @return void + */ + private function printPatchWasApplied(OutputInterface $output, PatchInterface $patch) + { + $message = sprintf( + 'Patch %s (%s) was already applied', + $patch->getId(), + $patch->getFilename() + ); + + $output->writeln($message . PHP_EOL); + $this->logger->info($message); + } + + /** + * Prints and logs 'applying patch failed' message. + * + * @param OutputInterface $output + * @param PatchInterface $patch + * @param string $errorOutput + * + * @return void + */ + private function printPatchApplyingFailed(OutputInterface $output, PatchInterface $patch, string $errorOutput) + { + $errorMessage = sprintf( + 'Applying patch %s (%s) failed.%s', + $patch->getId(), + $patch->getPath(), + $this->renderer->formatErrorOutput($errorOutput) + ); + + $output->writeln('' . $errorMessage . '' . PHP_EOL); + $this->logger->error($errorMessage); + } + + /** + * Returns a list of patches according to the filter. + * + * @param array $patchFilter + * + * @return PatchInterface[] + * @throws RuntimeException + */ + private function getPatchList(array $patchFilter): array + { + if (empty($patchFilter)) { + return array_filter( + $this->optionalPool->getOptionalAll(), + function ($patch) { + return !$patch->isDeprecated(); + } + ); + } + + try { + return $this->optionalPool->getList($patchFilter); + } catch (PatchNotFoundException $e) { + throw new RuntimeException($e->getMessage(), $e->getCode()); + } + } + + /** + * Rollback applied patches. + * + * @param OutputInterface $output + * @param PatchInterface[] $appliedPatches + * + * @return void + */ + private function rollback(OutputInterface $output, array $appliedPatches) + { + $this->logger->info('Start rollback'); + + foreach (array_reverse($appliedPatches) as $appliedPatch) { + $message = $this->applier->revert($appliedPatch->getPath(), $appliedPatch->getId()); + $this->renderer->printPatchInfo($output, $appliedPatch, $message); + $this->logger->info($message, ['file' => $appliedPatch->getPath()]); + } + + $this->logger->info('End rollback'); + } +} diff --git a/src/Command/Process/Action/ConfirmRequiredAction.php b/src/Command/Process/Action/ConfirmRequiredAction.php new file mode 100644 index 00000000..a8d74623 --- /dev/null +++ b/src/Command/Process/Action/ConfirmRequiredAction.php @@ -0,0 +1,94 @@ +optionalPool = $optionalPool; + $this->statusPool = $statusPool; + $this->aggregator = $aggregator; + $this->renderer = $renderer; + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output, array $patchFilter) + { + try { + $requiredNotAppliedPatches = array_filter( + $this->optionalPool->getAdditionalRequiredPatches($patchFilter), + function ($patch) { + return !$this->statusPool->isApplied($patch->getId()); + } + ); + } catch (PatchNotFoundException $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + + if ($requiredNotAppliedPatches) { + $output->writeln( + 'Next patches are required by ' . implode(' ', $patchFilter) . ':' . PHP_EOL + ); + + $aggregatedPatches = $this->aggregator->aggregate($requiredNotAppliedPatches); + $this->renderer->printTable($output, $aggregatedPatches); + + $question = 'Do you want to proceed with applying these patches?'; + if (!$this->renderer->printQuestion($input, $output, $question)) { + throw new RuntimeException( + implode(' ', $patchFilter) . ' can\'t be applied without required patches' + ); + } + } + } +} diff --git a/src/Command/Process/Action/ProcessDeprecatedAction.php b/src/Command/Process/Action/ProcessDeprecatedAction.php new file mode 100644 index 00000000..9341bda6 --- /dev/null +++ b/src/Command/Process/Action/ProcessDeprecatedAction.php @@ -0,0 +1,181 @@ +optionalPool = $optionalPool; + $this->statusPool = $statusPool; + $this->revert = $revert; + $this->aggregator = $aggregator; + $this->renderer = $renderer; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output, array $patchFilter) + { + if (empty($patchFilter)) { + return; + } + + try { + $patches = $this->aggregator->aggregate($this->optionalPool->getList($patchFilter)); + } catch (PatchNotFoundException $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + + foreach ($patches as $patch) { + $this->processDeprecation($patch, $output, $input); + $this->processReplacement($patch, $output, $input); + } + } + + /** + * Check if patch is deprecated. + * + * @param AggregatedPatchInterface $patch + * @param OutputInterface $output + * @param InputInterface $input + * @return void + * @throws RuntimeException + */ + private function processDeprecation( + AggregatedPatchInterface $patch, + OutputInterface $output, + InputInterface $input + ) { + if ($patch->isDeprecated()) { + $this->printDeprecatedWarning($output, $patch); + $question = 'Do you want to continue?'; + if (!$this->renderer->printQuestion($input, $output, $question)) { + throw new RuntimeException('Patch applying process was terminated'); + } + } + } + + /** + * Check if patch must replace some other patch. + * + * @param AggregatedPatchInterface $patch + * @param OutputInterface $output + * @param InputInterface $input + * + * @return void + * @throws RuntimeException + */ + private function processReplacement( + AggregatedPatchInterface $patch, + OutputInterface $output, + InputInterface $input + ) { + $requireRevertAndReplace = array_filter( + $this->optionalPool->getReplacedBy($patch->getId()), + function ($patchId) { + return $this->statusPool->isApplied($patchId); + } + ); + + if ($requireRevertAndReplace) { + $ids = implode(' ', $requireRevertAndReplace); + $warning = sprintf('%s should be reverted and replaced with %s', $ids, $patch->getId()); + $output->writeln('' . $warning . ''); + $this->logger->warning($warning); + + $question = 'Do you want to proceed with reverting?'; + if (!$this->renderer->printQuestion($input, $output, $question)) { + $errorMessage = sprintf('%s can\'t be applied without reverting %s', $patch->getId(), $ids); + + throw new RuntimeException($errorMessage); + } + + $this->revert->execute($input, $output, $requireRevertAndReplace); + } + } + + /** + * Prints warning message about using deprecated patch. + * + * @param OutputInterface $output + * @param AggregatedPatchInterface $patch + * @return void + */ + private function printDeprecatedWarning(OutputInterface $output, AggregatedPatchInterface $patch) + { + $message = sprintf( + 'Warning! Deprecated patch %s is going to be applied.%s', + $patch->getId(), + $patch->getReplacedWith() ? ' Please, replace it with ' . $patch->getReplacedWith() : '' + ); + $output->writeln('' . $message . ''); + $this->logger->warning($message); + } +} diff --git a/src/Command/Process/Action/RevertAction.php b/src/Command/Process/Action/RevertAction.php new file mode 100644 index 00000000..5c472596 --- /dev/null +++ b/src/Command/Process/Action/RevertAction.php @@ -0,0 +1,188 @@ +applier = $applier; + $this->revertValidator = $revertValidator; + $this->optionalPool = $optionalPool; + $this->statusPool = $statusPool; + $this->renderer = $renderer; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output, array $patchFilter) + { + $this->revertValidator->validate($patchFilter); + + try { + $patches = array_reverse($this->optionalPool->getList($patchFilter, false)); + } catch (PatchNotFoundException $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + + if (empty($patchFilter)) { + $patches = array_filter( + $patches, + function ($patch) { + return $this->statusPool->isApplied($patch->getId()); + } + ); + } + + if (!$this->revert($patches, $output)) { + throw new RuntimeException( + 'Revert operation for ' . implode(' ', $patchFilter) . ' finished with errors.' + ); + } + } + + /** + * Reverts patches. + * + * @param PatchInterface[] $patches + * @param OutputInterface $output + * + * @return bool + */ + private function revert(array $patches, OutputInterface $output): bool + { + $isSuccess = true; + foreach ($patches as $patch) { + if ($this->statusPool->isNotApplied($patch->getId())) { + $this->printPatchIsNotApplied($output, $patch); + + continue; + } + + try { + $message = $this->applier->revert($patch->getPath(), $patch->getId()); + $this->logger->info($message, ['file' => $patch->getPath()]); + $this->renderer->printPatchInfo($output, $patch, $message); + } catch (ApplierException $exception) { + $this->printPatchRevertingFailed($output, $patch, $exception->getMessage()); + $isSuccess = false; + } + } + + return $isSuccess; + } + + /** + * Prints and logs 'patch is not applied' message. + * + * @param OutputInterface $output + * @param PatchInterface $patch + * + * @return void + */ + private function printPatchIsNotApplied(OutputInterface $output, PatchInterface $patch) + { + $output->writeln( + sprintf( + 'Patch %s (%s) is not applied', + $patch->getId(), + $patch->getFilename() + ) + ); + $this->logger->info( + 'Patch ' . $patch->getId() .' is not applied', + ['file' => $patch->getPath()] + ); + } + + /** + * Prints and logs 'reverting patch failed' message. + * + * @param OutputInterface $output + * @param PatchInterface $patch + * @param string $errorOutput + * + * @return void + */ + private function printPatchRevertingFailed(OutputInterface $output, PatchInterface $patch, string $errorOutput) + { + $errorMessage = sprintf( + 'Reverting patch %s (%s) failed.%s', + $patch->getId(), + $patch->getPath(), + $this->renderer->formatErrorOutput($errorOutput) + ); + + $this->logger->error($errorMessage); + $output->writeln('' . $errorMessage . ''); + } +} diff --git a/src/Command/Process/Action/ReviewAppliedAction.php b/src/Command/Process/Action/ReviewAppliedAction.php new file mode 100644 index 00000000..a4ec579b --- /dev/null +++ b/src/Command/Process/Action/ReviewAppliedAction.php @@ -0,0 +1,84 @@ +optionalPool = $optionalPool; + $this->statusPool = $statusPool; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output, array $patchFilter) + { + $appliedOptionalPatches = array_filter( + $this->optionalPool->getOptionalAll(), + function ($patch) { + return $this->statusPool->isApplied($patch->getId()); + } + ); + + $ids = array_map( + function ($patch) { + return $patch->getId(); + }, + $appliedOptionalPatches + ); + $totalNumber = count(array_unique(array_merge($ids, $patchFilter))); + if ($totalNumber >= self::UPGRADE_THRESHOLD) { + $message = 'Warning for those on a previous minor line! Magento recommends installing a limited'. + ' number of quality patches to ensure a smooth upgrade to the latest line. Please begin planning'. + ' an upgrade to the latest release line.'; + + $output->writeln('' . $message . ''); + $this->logger->warning($message); + } + } +} diff --git a/src/Command/Process/ApplyLocal.php b/src/Command/Process/ApplyLocal.php new file mode 100644 index 00000000..e1418cf3 --- /dev/null +++ b/src/Command/Process/ApplyLocal.php @@ -0,0 +1,105 @@ +applier = $applier; + $this->localPool = $localPatchPool; + $this->renderer = $renderer; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function run(InputInterface $input, OutputInterface $output) + { + $patches = $this->localPool->getList(); + if (empty($patches)) { + $this->printInfo($output, 'Hot-fixes were not found. Skipping'); + + return; + } + + $this->logger->notice('Start of applying hot-fixes'); + + $output->writeln('Applying hot-fixes'); + foreach ($patches as $patch) { + try { + $message = $this->applier->apply($patch->getPath(), $patch->getTitle()); + $this->printInfo($output, $message); + } catch (ApplierException $exception) { + $errorMessage = sprintf( + 'Applying patch %s failed.%s', + $patch->getPath(), + $this->renderer->formatErrorOutput($exception->getMessage()) + ); + + throw new RuntimeException($errorMessage, $exception->getCode()); + } + } + + $this->logger->notice('End of applying hot-fixes'); + } + + /** + * Prints and logs info message. + * + * @param OutputInterface $output + * @param string $message + */ + private function printInfo(OutputInterface $output, string $message) + { + $output->writeln('' . $message . ''); + $this->logger->info($message); + } +} diff --git a/src/Command/Process/ApplyOptional.php b/src/Command/Process/ApplyOptional.php new file mode 100644 index 00000000..d4b8ce17 --- /dev/null +++ b/src/Command/Process/ApplyOptional.php @@ -0,0 +1,68 @@ +filterFactory = $filterFactory; + $this->actionPool = $actionPool; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function run(InputInterface $input, OutputInterface $output) + { + $argPatches = $input->getArgument(Apply::ARG_QUALITY_PATCHES); + $patchFilter = $this->filterFactory->createApplyFilter($argPatches); + if ($patchFilter === null) { + return; + } + + $this->logger->notice('Start of applying optional patches'); + $this->logger->info('Command argument: ' . implode(' ', $argPatches)); + $this->actionPool->execute($input, $output, $patchFilter); + $this->logger->notice('End of applying optional patches'); + } +} diff --git a/src/Command/Process/ApplyRequired.php b/src/Command/Process/ApplyRequired.php new file mode 100644 index 00000000..51f057d6 --- /dev/null +++ b/src/Command/Process/ApplyRequired.php @@ -0,0 +1,90 @@ +applier = $applier; + $this->requiredPool = $requiredPool; + $this->renderer = $renderer; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function run(InputInterface $input, OutputInterface $output) + { + $this->logger->notice('Start of applying required patches'); + + $patches = $this->requiredPool->getList(); + foreach ($patches as $patch) { + try { + $message = $this->applier->apply($patch->getPath(), $patch->getId()); + $this->renderer->printPatchInfo($output, $patch, $message); + $this->logger->info($message, ['file' => $patch->getPath()]); + } catch (ApplierException $exception) { + $errorMessage = sprintf( + 'Applying patch %s %s failed.%s', + $patch->getId(), + $patch->getPath(), + $this->renderer->formatErrorOutput($exception->getMessage()) + ); + + throw new RuntimeException($errorMessage, $exception->getCode()); + } + } + + $this->logger->notice('End of applying required patches'); + } +} diff --git a/src/Command/Process/ProcessInterface.php b/src/Command/Process/ProcessInterface.php new file mode 100644 index 00000000..b1a1dde8 --- /dev/null +++ b/src/Command/Process/ProcessInterface.php @@ -0,0 +1,28 @@ +tableFactory = $tableFactory; + $this->statusPool = $statusPool; + $this->questionHelper = $questionHelper; + $this->confirmationQuestionFactory = $confirmationQuestionFactory; + } + + /** + * Renders patches list as a table. + * + * @param OutputInterface $output + * @param AggregatedPatchInterface[] $patchList + * @return void + */ + public function printTable(OutputInterface $output, array $patchList) + { + $table = $this->tableFactory->create($output); + $table->setHeaders([self::ID, self::TITLE, self::TYPE, self::STATUS, self::DETAILS]); + $table->setStyle('box-double'); + $table->setColumnMaxWidth(1, 50); + + $rows = []; + foreach ($patchList as $patch) { + $rows[] = $this->createRow($patch); + $rows[] = new TableSeparator(); + } + array_pop($rows); + + $table->addRows($rows); + $table->render(); + } + + /** + * Print patch info. + * + * @param OutputInterface $output + * @param PatchInterface $patch + * @param string $prependedMessage + * @return void + */ + public function printPatchInfo( + OutputInterface $output, + PatchInterface $patch, + string $prependedMessage = '' + ) { + $info = [ + sprintf('Id: %s', $patch->getId()), + sprintf('Title: %s', $patch->getTitle()), + sprintf('File: %s', $patch->getFilename()), + sprintf( + 'Affected components: %s', + implode(' ', $patch->getAffectedComponents()) + ), + ]; + + if ($patch->getRequire()) { + $info[] = sprintf( + 'Require: %s', + implode(' ', $patch->getRequire()) + ); + } + + if ($patch->isDeprecated()) { + $info[] = sprintf( + 'Patch is deprecated!%s', + $patch->getReplacedWith() ? ' Please, replace it with ' . $patch->getReplacedWith() : '' + ); + } + + if ($prependedMessage) { + array_unshift($info, '' . $prependedMessage . ''); + } + $output->writeln($info); + $output->writeln(''); + } + + /** + * Format error output. + * + * @param string $errorOutput + * @return string + */ + public function formatErrorOutput(string $errorOutput): string + { + if (preg_match('#^.*?Error Output:(?.*?)$#is', $errorOutput, $matches)) { + $errorOutput = PHP_EOL . 'Error Output:' . $matches['errors']; + } + + return $errorOutput; + } + + /** + * Asks a confirmation question to the user. + * + * @param InputInterface $input + * @param OutputInterface $output + * @param string $question + * @return bool + */ + public function printQuestion(InputInterface $input, OutputInterface $output, string $question): bool + { + $question = $this->confirmationQuestionFactory->create( + '' . $question . ' [y/N] ' + ); + + return (bool)$this->questionHelper->ask($input, $output, $question); + } + + /** + * Creates table row. + * + * @param AggregatedPatchInterface $patch + * @return array + */ + private function createRow(AggregatedPatchInterface $patch): array + { + $glue = PHP_EOL . ' - '; + $details = ''; + if ($patch->getReplacedWith()) { + $details .= 'Recommended replacement: ' . $patch->getReplacedWith() . PHP_EOL . ''; + } + if ($patch->getRequire()) { + $details .= 'Required patches:' . + '' . $glue . implode($glue, $patch->getRequire()) . PHP_EOL . ''; + } + if ($patch->getAffectedComponents()) { + $details .= 'Affected components:' . $glue . implode($glue, $patch->getAffectedComponents()); + } + $id = $patch->getType() === PatchInterface::TYPE_CUSTOM ? 'N/A' : $patch->getId(); + + return [ + self::ID => '' . $id . '', + self::TITLE => $patch->getTitle(), + self::TYPE => $patch->isDeprecated() ? 'DEPRECATED' : $patch->getType(), + self::STATUS => $this->statusPool->get($patch->getId()), + self::DETAILS => $details + ]; + } +} diff --git a/src/Command/Process/Revert.php b/src/Command/Process/Revert.php new file mode 100644 index 00000000..1c92b01c --- /dev/null +++ b/src/Command/Process/Revert.php @@ -0,0 +1,74 @@ +filterFactory = $filterFactory; + $this->revertAction = $revertAction; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function run(InputInterface $input, OutputInterface $output) + { + $argPatches = $input->getArgument(RevertCommand::ARG_QUALITY_PATCHES); + $optAll = $input->getOption(RevertCommand::OPT_ALL); + $patchFilter = $this->filterFactory->createRevertFilter($optAll, $argPatches); + + if ($patchFilter === null) { + return; + } + $this->logger->notice('Start of reverting optional patches'); + + $this->logger->info('Command argument: ' . implode(' ', $argPatches)); + $this->logger->info('Command option: ' . $optAll ? '--all' : ''); + $this->revertAction->execute($input, $output, $patchFilter); + + $this->logger->notice('End of reverting optional patches'); + } +} diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php new file mode 100644 index 00000000..181366ef --- /dev/null +++ b/src/Command/Process/ShowStatus.php @@ -0,0 +1,136 @@ +aggregator = $aggregator; + $this->optionalPool = $optionalPool; + $this->localPool = $localPool; + $this->statusPool = $statusPool; + $this->reviewAppliedAction = $reviewAppliedAction; + $this->renderer = $renderer; + } + + /** + * @inheritDoc + */ + public function run(InputInterface $input, OutputInterface $output) + { + $this->printDetailsInfo($output); + + $this->reviewAppliedAction->execute($input, $output, []); + + $patches = $this->aggregator->aggregate( + array_merge($this->optionalPool->getList(), $this->localPool->getList()) + ); + foreach ($patches as $patch) { + if ($patch->isDeprecated() && $this->statusPool->isApplied($patch->getId())) { + $this->printDeprecatedWarning($output, $patch); + } + } + + $patches = array_filter( + $patches, + function ($patch) { + return !$patch->isDeprecated() || $this->statusPool->isApplied($patch->getId()); + } + ); + $this->renderer->printTable($output, $patches); + } + + /** + * Prints information where to find more details about patches. + * + * @param OutputInterface $output + * @return void + */ + private function printDetailsInfo(OutputInterface $output) + { + $output->writeln( + 'More detailed information about patches you can find on ' . + 'https://support.magento.com' + ); + } + + /** + * Prints warning message about applied deprecated patch. + * + * @param OutputInterface $output + * @param AggregatedPatchInterface $patch + * @return void + */ + private function printDeprecatedWarning(OutputInterface $output, AggregatedPatchInterface $patch) + { + $message = sprintf( + 'Deprecated patch %s is currently applied. Please, consider to revert it%s', + $patch->getId(), + $patch->getReplacedWith() ? ' and replace with ' . $patch->getReplacedWith() : '.' + ); + $output->writeln($message); + } +} diff --git a/src/Command/Revert.php b/src/Command/Revert.php new file mode 100644 index 00000000..2b971cde --- /dev/null +++ b/src/Command/Revert.php @@ -0,0 +1,129 @@ +revert = $revert; + $this->environment = $environment; + $this->logger = $logger; + $this->magentoVersion = $magentoVersion; + + parent::__construct(self::NAME); + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName(self::NAME) + ->setDescription('Revert patches') + ->addArgument( + self::ARG_QUALITY_PATCHES, + InputArgument::IS_ARRAY, + 'List of quality patches to revert' + )->addOption( + self::OPT_ALL, + 'a', + InputOption::VALUE_NONE, + 'Revert all patches' + ); + + parent::configure(); + } + + /** + * {@inheritDoc} + */ + public function execute(InputInterface $input, OutputInterface $output) + { + if ($this->environment->isCloud()) { + $output->writeln('Revert command is unavailable on Magento Cloud'); + + return self::RETURN_FAILURE; + } + + $this->logger->notice($this->magentoVersion->get()); + + try { + $this->revert->run($input, $output); + } catch (RuntimeException $e) { + $output->writeln('' . $e->getMessage() . ''); + $this->logger->error($e->getMessage()); + + return self::RETURN_FAILURE; + } catch (\Exception $e) { + $this->logger->critical($e); + + throw $e; + } + + return self::RETURN_SUCCESS; + } +} diff --git a/src/Command/Status.php b/src/Command/Status.php new file mode 100644 index 00000000..8054de1d --- /dev/null +++ b/src/Command/Status.php @@ -0,0 +1,88 @@ +showStatus = $showStatus; + $this->logger = $logger; + $this->magentoVersion = $magentoVersion; + + parent::__construct(self::NAME); + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName(self::NAME) + ->setDescription('Shows status of patches'); + + parent::configure(); + } + + /** + * {@inheritDoc} + */ + public function execute(InputInterface $input, OutputInterface $output) + { + try { + $this->showStatus->run($input, $output); + $output->writeln('' . $this->magentoVersion->get() . ''); + } catch (RuntimeException $e) { + $output->writeln('' . $e->getMessage() . ''); + $this->logger->error($e->getMessage()); + + return self::RETURN_FAILURE; + } catch (\Exception $e) { + $this->logger->critical($e); + + throw $e; + } + + return self::RETURN_SUCCESS; + } +} diff --git a/src/Composer/MagentoVersion.php b/src/Composer/MagentoVersion.php new file mode 100644 index 00000000..6495f57e --- /dev/null +++ b/src/Composer/MagentoVersion.php @@ -0,0 +1,83 @@ + 'B2B Edition', + 'magento/magento2-ee-base' => 'Enterprise Edition' + ]; + + /** + * @param Composer\Composer $composer + */ + public function __construct( + Composer\Composer $composer + ) { + $this->repository = $composer->getRepositoryManager()->getLocalRepository(); + } + + /** + * Returns Magento version and edition. + * + * @return string + */ + public function get(): string + { + if (null !== $this->version) { + return $this->version; + } + + $this->version = 'Magento 2 is not installed'; + $basePackage = $this->repository->findPackage('magento/magento2-base', '*'); + if ($basePackage instanceof PackageInterface) { + $version = $basePackage->getVersion(); + $edition = $this->getEdition(); + $this->version = 'Magento 2 ' . $edition . ', version ' . $version; + } + + return $this->version; + } + + /** + * Returns Magento edition. + * + * @return string + */ + private function getEdition(): string + { + foreach ($this->editionMap as $package => $edition) { + if ($this->repository->findPackage($package, '*') instanceof PackageInterface) { + return $edition; + } + } + + return 'Community Edition'; + } +} diff --git a/src/Composer/Package.php b/src/Composer/Package.php new file mode 100644 index 00000000..cac69821 --- /dev/null +++ b/src/Composer/Package.php @@ -0,0 +1,44 @@ +repository = $composer->getRepositoryManager()->getLocalRepository(); + } + + /** + * Checks whether package with specific constraint exists in the system. + * + * @param string $packageName + * @param string $packageConstraint + * @return bool True if patch with provided constraint exists, false otherwise. + */ + public function matchConstraint(string $packageName, string $packageConstraint): bool + { + return $this->repository->findPackage($packageName, $packageConstraint) instanceof PackageInterface; + } +} diff --git a/src/Console/ConfirmationQuestionFactory.php b/src/Console/ConfirmationQuestionFactory.php new file mode 100644 index 00000000..1f640c9f --- /dev/null +++ b/src/Console/ConfirmationQuestionFactory.php @@ -0,0 +1,29 @@ +directoryList->getRoot() . '/patches.json'; } + + /** + * @return string + */ + public function getPatchLog(): string + { + return $this->directoryList->getMagentoRoot() . '/var/log/patch.log'; + } + + /** + * @return string + */ + public function getInitPatchLog(): string + { + return $this->directoryList->getMagentoRoot() . '/init/var/log/patch.log'; + } } diff --git a/src/Filesystem/FileNotFoundException.php b/src/Filesystem/FileSystemException.php similarity index 72% rename from src/Filesystem/FileNotFoundException.php rename to src/Filesystem/FileSystemException.php index af9e0621..b9f9f2b7 100644 --- a/src/Filesystem/FileNotFoundException.php +++ b/src/Filesystem/FileSystemException.php @@ -10,8 +10,8 @@ use Magento\CloudPatches\App\GenericException; /** - * Exception if file can not be found + * Filesystem operations exception. */ -class FileNotFoundException extends GenericException +class FileSystemException extends GenericException { } diff --git a/src/Filesystem/Filesystem.php b/src/Filesystem/Filesystem.php index 6d53f82e..f20780f2 100644 --- a/src/Filesystem/Filesystem.php +++ b/src/Filesystem/Filesystem.php @@ -41,16 +41,17 @@ public function exists($path): bool * * @param string $path * @return string - * - * @throws FileNotFoundException + * @throws FileSystemException */ public function get(string $path): string { - if ($this->isFile($path)) { - return file_get_contents($path); + clearstatcache(); + $result = @file_get_contents($path); + if (false === $result) { + throw new FileSystemException('Cannot read contents from file "' . $path . '"'); } - throw new FileNotFoundException("File does not exist at path {$path}"); + return $result; } /** @@ -63,4 +64,51 @@ public function isFile(string $file): bool { return is_file($file); } + + /** + * Determine if directory is writable. + * + * @param string $directory + * @return bool + */ + public function isWritable(string $directory): bool + { + return is_writable($directory); + } + + /** + * Returns directory component of path. + * + * @param string $path + * @return string + */ + public function getDirectory(string $path): string + { + return dirname($path); + } + + /** + * Creates directory. + * + * @param string $path + * @param int $mode + * @param bool $recursive + * @return bool + */ + public function createDirectory($path, $mode = 0755, $recursive = true): bool + { + return @mkdir($path, $mode, $recursive); + } + + /** + * Copy source into destination. + * + * @param string $source + * @param string $destination + * @return bool + */ + public function copy(string $source, string $destination): bool + { + return copy($source, $destination); + } } diff --git a/src/Patch/AggregatedPatchFactory.php b/src/Patch/AggregatedPatchFactory.php new file mode 100644 index 00000000..fbcbea6d --- /dev/null +++ b/src/Patch/AggregatedPatchFactory.php @@ -0,0 +1,162 @@ +getId($items); + $type = $this->getType($items); + $title = $this->getTitle($items); + $components = $this->getAffectedComponents($items); + $require = $this->getRequire($items); + $replacedWith = $this->getReplacedWith($items); + $isDeprecated = $this->isDeprecated($items); + + return new AggregatedPatch( + $id, + $type, + $title, + $components, + $require, + $replacedWith, + $isDeprecated, + $items + ); + } + + /** + * Returns aggregated patch unique identifier. + * + * @param PatchInterface[] $patches + * @return string + */ + private function getId(array $patches): string + { + $patch = reset($patches); + + return $patch->getId(); + } + + /** + * Returns aggregated patch type. + * + * @param PatchInterface[] $patches + * @return string + */ + private function getType(array $patches): string + { + $patch = reset($patches); + + return $patch->getType(); + } + + /** + * Returns aggregated patch title. + * + * @param PatchInterface[] $patches + * @return string + */ + private function getTitle(array $patches): string + { + $patch = end($patches); + + return $patch->getTitle(); + } + + /** + * Returns aggregated list of affected components. + * + * @param PatchInterface[] $patches + * @return string[] + */ + private function getAffectedComponents(array $patches): array + { + $result = array_map( + function (PatchInterface $patch) { + return $patch->getAffectedComponents(); + }, + $patches + ); + $result = array_unique(array_merge([], ...$result)); + sort($result); + + return $result; + } + + /** + * Returns aggregated required patches. + * + * @param PatchInterface[] $patches + * @return string[] + */ + private function getRequire(array $patches): array + { + $result = array_map( + function (PatchInterface $patch) { + return $patch->getRequire(); + }, + $patches + ); + $result = array_unique(array_merge([], ...$result)); + + return $result; + } + + /** + * ID of the patch, which is recommended to replace the current patch. + * + * @param PatchInterface[] $patches + * @return string + */ + private function getReplacedWith(array $patches): string + { + $result = ''; + foreach ($patches as $patch) { + if ($patch->getReplacedWith()) { + $result = $patch->getReplacedWith(); + } + } + + return $result; + } + + /** + * Is patch deprecated. + * + * @param PatchInterface[] $patches + * @return bool + */ + private function isDeprecated(array $patches): bool + { + foreach ($patches as $patch) { + if ($patch->isDeprecated()) { + return true; + } + } + + return false; + } +} diff --git a/src/Patch/Aggregator.php b/src/Patch/Aggregator.php new file mode 100644 index 00000000..2f0d6829 --- /dev/null +++ b/src/Patch/Aggregator.php @@ -0,0 +1,51 @@ +aggregatedPatchFactory = $aggregatedPatchFactory; + } + + /** + * Returns collection of aggregated patches. + * + * @param PatchInterface[] $patches + * @return AggregatedPatchInterface[] + */ + public function aggregate(array $patches): array + { + $patchGroups = []; + foreach ($patches as $patch) { + $patchGroups[$patch->getId()][] = $patch; + } + + $result = []; + foreach ($patchGroups as $patchGroup) { + $result[] = $this->aggregatedPatchFactory->create($patchGroup); + } + + return $result; + } +} diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php index d7e1d2c4..568cbf9a 100644 --- a/src/Patch/Applier.php +++ b/src/Patch/Applier.php @@ -7,164 +7,107 @@ namespace Magento\CloudPatches\Patch; -use Composer; -use Magento\CloudPatches\Filesystem\DirectoryList; -use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Patch\Status\StatusPool; use Magento\CloudPatches\Shell\ProcessFactory; use Symfony\Component\Process\Exception\ProcessFailedException; /** - * Provides apply methods for patches. + * Applies and reverts patches. */ class Applier { - /** - * @var Composer\Repository\RepositoryInterface - */ - private $repository; - /** * @var ProcessFactory */ private $processFactory; /** - * @var DirectoryList - */ - private $directoryList; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @param Composer\Composer $composer * @param ProcessFactory $processFactory - * @param DirectoryList $directoryList - * @param Filesystem $filesystem */ public function __construct( - Composer\Composer $composer, - ProcessFactory $processFactory, - DirectoryList $directoryList, - Filesystem $filesystem + ProcessFactory $processFactory ) { - $this->repository = $composer->getRepositoryManager()->getLocalRepository(); $this->processFactory = $processFactory; - $this->directoryList = $directoryList; - $this->filesystem = $filesystem; } /** + * General apply processing. + * * @param string $path - * @param bool $deployedFromGit + * @param string $id * @return string * * @throws ApplierException */ - public function applyFile(string $path, bool $deployedFromGit): string + public function apply(string $path, string $id): string { - return $this->processApply($path, $path, $deployedFromGit); - } - - /** - * Applies patch, using 'git apply' command. - * - * If the patch fails to apply, checks if it has already been applied which is considered ok. - * - * @param string $path Path to patch - * @param string $name Name of patch - * @param string $packageName Name of package to be patched - * @param string $constraint Specific constraint of package to be fixed - * @param bool $deployedFromGit - * @return string|null - * - * @throws ApplierException - */ - public function apply( - string $path, - string $name, - string $packageName, - string $constraint, - bool $deployedFromGit - ) { - $fullName = sprintf( - '%s %s', - sprintf('%s (%s)', $name, $path), - $constraint - ); - - if ($packageName && !$this->matchConstraint($packageName, $constraint)) { - return null; - } + try { + $this->processFactory->create(['git', 'apply', $path]) + ->mustRun(); + } catch (ProcessFailedException $exception) { + try { + $this->processFactory->create(['git', 'apply', '--check', '--reverse', $path]) + ->mustRun(); + } catch (ProcessFailedException $reverseException) { + throw new ApplierException($exception->getMessage(), $exception->getCode()); + } - /** - * Support for relative paths. - */ - if (!$this->filesystem->exists($path)) { - $path = $this->directoryList->getPatches() . '/' . $path; + return sprintf('Patch %s was already applied', $id); } - return $this->processApply($path, $fullName, $deployedFromGit); + return sprintf('Patch %s has been applied', $id); } /** - * General apply processing. + * General revert processing. * * @param string $path - * @param string $fullName - * @param bool $deployedFromGit + * @param string $id * @return string * * @throws ApplierException */ - private function processApply(string $path, string $fullName, bool $deployedFromGit): string + public function revert(string $path, string $id): string { try { - $this->processFactory->create(['git', 'apply', $path]) + $this->processFactory->create(['git', 'apply', '--reverse', $path]) ->mustRun(); } catch (ProcessFailedException $exception) { - if ($deployedFromGit) { - return sprintf( - 'Patch "%s" was not applied. (%s)', - $fullName, - $exception->getMessage() - ); - } - try { - $this->processFactory->create(['git', 'apply', '--check', '--reverse', $path]) + $this->processFactory->create(['git', 'apply', '--check', $path]) ->mustRun(); - } catch (ProcessFailedException $reverseException) { - throw new ApplierException( - $reverseException->getMessage(), - $reverseException->getCode(), - $reverseException - ); + } catch (ProcessFailedException $applyException) { + throw new ApplierException($exception->getMessage(), $exception->getCode()); } - return sprintf( - 'Patch "%s" was already applied', - $fullName - ); + return sprintf('Patch %s wasn\'t applied', $id); } - return sprintf( - 'Patch "%s" applied', - $fullName - ); + return sprintf('Patch %s has been reverted', $id); } /** - * Checks whether package with specific constraint exists in the system. + * Checks patch status. * - * @param string $packageName - * @param string $constraint - * @return bool True if patch with provided constraint exists, false otherwise. + * @param string $patchContent + * @return string */ - private function matchConstraint(string $packageName, string $constraint): bool + public function status(string $patchContent): string { - return $this->repository->findPackage($packageName, $constraint) instanceof Composer\Package\PackageInterface; + try { + $this->processFactory->create(['git', 'apply', '--check'], $patchContent) + ->mustRun(); + } catch (ProcessFailedException $exception) { + try { + $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $patchContent) + ->mustRun(); + } catch (ProcessFailedException $reverseException) { + return StatusPool::NA; + } + + return StatusPool::APPLIED; + } + + return StatusPool::NOT_APPLIED; } } diff --git a/src/Patch/Collector/CloudCollector.php b/src/Patch/Collector/CloudCollector.php new file mode 100644 index 00000000..07b18f39 --- /dev/null +++ b/src/Patch/Collector/CloudCollector.php @@ -0,0 +1,158 @@ +sourceProvider = $sourceProvider; + $this->package = $package; + $this->directoryList = $directoryList; + $this->environment = $environment; + $this->patchBuilder = $patchBuilder; + } + + /** + * Collects quality patches. + * + * @return PatchInterface[] + * @throws CollectorException + */ + public function collect(): array + { + try { + $config = $this->sourceProvider->getCloudPatches(); + } catch (SourceProviderException $e) { + throw new CollectorException($e->getMessage(), $e->getCode(), $e); + } + + $result = []; + foreach ($config as $packageName => $packagePatches) { + foreach ($packagePatches as $patchTitle => $patchConfiguration) { + $this->validatePatchConfiguration($patchConfiguration, $patchTitle); + foreach ($patchConfiguration as $packageConstraint => $patchData) { + $patchFile = $patchData; + $patchId = $this->getPatchId($patchFile); + if ($this->package->matchConstraint($packageName, $packageConstraint)) { + try { + $patchPath = $this->directoryList->getPatches() . '/' . $patchFile; + $patchType = $this->environment->isCloud() + ? PatchInterface::TYPE_REQUIRED : PatchInterface::TYPE_OPTIONAL; + + $this->patchBuilder->setId($patchId); + $this->patchBuilder->setTitle($patchTitle); + $this->patchBuilder->setFilename($patchFile); + $this->patchBuilder->setPath($patchPath); + $this->patchBuilder->setType($patchType); + $this->patchBuilder->setPackageName($packageName); + $this->patchBuilder->setPackageConstraint($packageConstraint); + + $result[] = $this->patchBuilder->build(); + } catch (PatchIntegrityException $e) { + throw new CollectorException($e->getMessage(), $e->getCode(), $e); + } + } + } + } + } + + return $result; + } + + /** + * Validates patch configuration. + * + * @param array|string $configuration + * @param string $title + * @return void + * @throws CollectorException + */ + private function validatePatchConfiguration($configuration, string $title) + { + if (!is_array($configuration) || empty($configuration)) { + throw new CollectorException( + "Patch '{$title}' has invalid configuration. Should be not empty array." + ); + } + } + + /** + * Extract patchId from filename. + * + * @param string $patchFile + * @return string + * @throws CollectorException + */ + private function getPatchId(string $patchFile): string + { + $result = preg_match( + '#(?.*?)__(?.*?)__(?.*?)\.patch#', + $patchFile, + $patch + ); + if (!$result) { + throw new CollectorException( + "The patch filename '{$patchFile}' has invalid format." . PHP_EOL . + "Correct format: ____<PACKAGE_VERSION>.patch". PHP_EOL . + "Example: MAGECLOUD-2899__fix_redis_slave_configuration__2.3.0.patch" + ); + } + + return $patch['id']; + } +} diff --git a/src/Patch/Collector/CollectorException.php b/src/Patch/Collector/CollectorException.php new file mode 100644 index 00000000..1bb9fc3d --- /dev/null +++ b/src/Patch/Collector/CollectorException.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +use Magento\CloudPatches\App\GenericException; + +/** + * Exception if there are some troubles with collecting patches from source. + */ +class CollectorException extends GenericException +{ +} diff --git a/src/Patch/Collector/LocalCollector.php b/src/Patch/Collector/LocalCollector.php new file mode 100644 index 00000000..2da4c494 --- /dev/null +++ b/src/Patch/Collector/LocalCollector.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\PatchBuilder; +use Magento\CloudPatches\Patch\SourceProvider; + +/** + * Collects local patches. + */ +class LocalCollector +{ + /** + * @var SourceProvider + */ + private $sourceProvider; + + /** + * @var PatchBuilder + */ + private $patchBuilder; + + /** + * @param SourceProvider $sourceProvider + * @param PatchBuilder $patchBuilder + */ + public function __construct( + SourceProvider $sourceProvider, + PatchBuilder $patchBuilder + ) { + $this->sourceProvider = $sourceProvider; + $this->patchBuilder = $patchBuilder; + } + + /** + * Collects local patches. + * + * @return PatchInterface[] + */ + public function collect(): array + { + $files = $this->sourceProvider->getLocalPatches(); + $result = []; + foreach ($files as $file) { + $this->patchBuilder->setId(md5($file)); + $this->patchBuilder->setTitle('../' . SourceProvider::HOT_FIXES_DIR . '/' . basename($file)); + $this->patchBuilder->setFilename(basename($file)); + $this->patchBuilder->setPath($file); + $this->patchBuilder->setType(PatchInterface::TYPE_CUSTOM); + $result[] = $this->patchBuilder->build(); + } + + return $result; + } +} diff --git a/src/Patch/Collector/QualityCollector.php b/src/Patch/Collector/QualityCollector.php new file mode 100644 index 00000000..61c59918 --- /dev/null +++ b/src/Patch/Collector/QualityCollector.php @@ -0,0 +1,279 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Composer\Package; +use Magento\CloudPatches\Patch\PatchBuilder; +use Magento\CloudPatches\Patch\PatchIntegrityException; +use Magento\CloudPatches\Patch\SourceProvider; +use Magento\CloudPatches\Patch\SourceProviderException; +use Magento\QualityPatches\Info as QualityPatchesInfo; + +/** + * Collects patches. + */ +class QualityCollector +{ + /** + * Configuration JSON property. + * + * Contains patch filename, type string. + */ + const PROP_FILE = 'file'; + + /** + * Configuration JSON property. + * + * Contains required patch ids, type array. + */ + const PROP_REQUIRE = 'require'; + + /** + * Configuration JSON property. + * + * Contains patch id that current patch replaced with, type string. + */ + const PROP_REPLACED_WITH = 'replaced-with'; + + /** + * Configuration JSON property. + * + * Defines whether patch is deprecated, type boolean. + */ + const PROP_DEPRECATED = 'deprecated'; + + /** + * @var SourceProvider + */ + private $sourceProvider; + + /** + * @var Package + */ + private $package; + + /** + * @var QualityPatchesInfo + */ + private $qualityPatchesInfo; + + /** + * @var array|null + */ + private $config = null; + + /** + * @var PatchBuilder + */ + private $patchBuilder; + + /** + * @param SourceProvider $sourceProvider + * @param Package $package + * @param QualityPatchesInfo $qualityPatchesInfo + * @param PatchBuilder $patchBuilder + */ + public function __construct( + SourceProvider $sourceProvider, + Package $package, + QualityPatchesInfo $qualityPatchesInfo, + PatchBuilder $patchBuilder + ) { + $this->sourceProvider = $sourceProvider; + $this->package = $package; + $this->qualityPatchesInfo = $qualityPatchesInfo; + $this->patchBuilder = $patchBuilder; + } + + /** + * Collects quality patches. + * + * @return PatchInterface[] + * + * @throws CollectorException + */ + public function collect() + { + $result = []; + foreach ($this->getConfig() as $patchId => $patchGeneralConfig) { + foreach ($patchGeneralConfig as $packageName => $packageConfiguration) { + foreach ($packageConfiguration as $patchTitle => $patchInfo) { + foreach ($patchInfo as $packageConstraint => $patchData) { + $patchFile = $patchData[static::PROP_FILE]; + $patchRequire = $patchData[static::PROP_REQUIRE] ?? []; + $patchReplacedWith = $patchData[static::PROP_REPLACED_WITH] ?? ''; + $patchDeprecated = $patchData[static::PROP_DEPRECATED] ?? (bool)$patchReplacedWith; + + if ($this->package->matchConstraint($packageName, $packageConstraint)) { + $result[] = $this->createPatch( + $patchId, + $patchTitle, + $patchFile, + $packageName, + $packageConstraint, + $patchRequire, + $patchReplacedWith, + $patchDeprecated + ); + } + } + } + } + } + + return $result; + } + + /** + * Validates patch configuration. + * + * @param array $config + * + * @return void + * @throws CollectorException + */ + private function validateConfiguration(array $config) + { + foreach ($config as $patchId => $patchGeneralConfig) { + $errors = []; + foreach ($patchGeneralConfig as $packageConfiguration) { + foreach ($packageConfiguration as $patchInfo) { + foreach ($patchInfo as $packageConstraint => $patchData) { + $errors = $this->validateProperties($patchData, $packageConstraint, $errors); + } + } + } + + if (!empty($errors)) { + array_unshift($errors, "Patch {$patchId} has invalid configuration:"); + + throw new CollectorException(implode(PHP_EOL . ' - ', $errors)); + } + } + } + + /** + * Returns patches config. + * + * @return array + * @throws CollectorException + */ + private function getConfig(): array + { + if ($this->config === null) { + try { + $this->config = $this->sourceProvider->getQualityPatches(); + } catch (SourceProviderException $e) { + throw new CollectorException($e->getMessage(), $e->getCode(), $e); + } + $this->validateConfiguration($this->config); + } + + return $this->config; + } + + /** + * Creates patch. + * + * @param string $patchId + * @param string $patchTitle + * @param string $patchFile + * @param string $packageName + * @param string $packageConstraint + * @param array $patchRequire + * @param string $patchReplacedWith + * @param bool $patchDeprecated + * + * @return PatchInterface + * @throws CollectorException + */ + private function createPatch( + string $patchId, + string $patchTitle, + string $patchFile, + string $packageName, + string $packageConstraint, + array $patchRequire, + string $patchReplacedWith, + bool $patchDeprecated + ): PatchInterface { + try { + $patchPath = $this->qualityPatchesInfo->getPatchesDirectory() . '/' . $patchFile; + $this->patchBuilder->setId($patchId); + $this->patchBuilder->setTitle($patchTitle); + $this->patchBuilder->setFilename($patchFile); + $this->patchBuilder->setPath($patchPath); + $this->patchBuilder->setType(PatchInterface::TYPE_OPTIONAL); + $this->patchBuilder->setPackageName($packageName); + $this->patchBuilder->setPackageConstraint($packageConstraint); + $this->patchBuilder->setRequire($patchRequire); + $this->patchBuilder->setReplacedWith($patchReplacedWith); + $this->patchBuilder->setDeprecated($patchDeprecated); + $patch = $this->patchBuilder->build(); + } catch (PatchIntegrityException $e) { + throw new CollectorException($e->getMessage(), $e->getCode(), $e); + } + + return $patch; + } + + /** + * Validates properties. + * + * @param array $patchData + * @param string $packageConstraint + * @param string[] $errors + * @return array + */ + private function validateProperties( + array $patchData, + string $packageConstraint, + array $errors + ): array { + if (!isset($patchData[static::PROP_FILE])) { + $errors[] = sprintf( + "Property '%s' is not found in '%s'", + static::PROP_FILE, + $packageConstraint + ); + } + + if (isset($patchData[static::PROP_REQUIRE]) && + !is_array($patchData[static::PROP_REQUIRE]) + ) { + $errors[] = sprintf( + "Property '%s' from '%s' should have an array type", + static::PROP_REQUIRE, + $packageConstraint + ); + } + + if (isset($patchData[static::PROP_REPLACED_WITH]) && + !is_string($patchData[static::PROP_REPLACED_WITH]) + ) { + $errors[] = sprintf( + "Property '%s' from '%s' should have a string type", + static::PROP_REPLACED_WITH, + $packageConstraint + ); + } + + if (isset($patchData[static::PROP_DEPRECATED]) && + !is_bool($patchData[static::PROP_DEPRECATED]) + ) { + $errors[] = sprintf( + "Property '%s' from '%s' should have a boolean type", + static::PROP_DEPRECATED, + $packageConstraint + ); + } + + return $errors; + } +} diff --git a/src/Patch/Data/AggregatedPatch.php b/src/Patch/Data/AggregatedPatch.php new file mode 100644 index 00000000..3a379a3c --- /dev/null +++ b/src/Patch/Data/AggregatedPatch.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Data; + +/** + * Aggregated patch data class. + */ +class AggregatedPatch implements AggregatedPatchInterface +{ + /** + * @var string + */ + private $id; + + /** + * @var string + */ + private $type; + + /** + * @var string + */ + private $title; + + /** + * @var array + */ + private $affectedComponents; + + /** + * @var array + */ + private $require; + + /** + * @var string + */ + private $replacedWith; + + /** + * @var boolean + */ + private $isDeprecated; + + /** + * @var array + */ + private $items; + + /** + * @param string $id + * @param string $type + * @param string $title + * @param string[] $affectedComponents + * @param string[] $require + * @param string $replacedWith + * @param bool $isDeprecated + * @param PatchInterface[] $items + */ + public function __construct( + string $id, + string $type, + string $title, + array $affectedComponents, + array $require, + string $replacedWith, + bool $isDeprecated, + array $items + ) { + + $this->id = $id; + $this->type = $type; + $this->title = $title; + $this->affectedComponents = $affectedComponents; + $this->require = $require; + $this->replacedWith = $replacedWith; + $this->isDeprecated = $isDeprecated; + $this->items = $items; + } + + /** + * @return string + */ + public function __toString() + { + return $this->id; + } + + /** + * @inheritDoc + */ + public function getId(): string + { + return $this->id; + } + + /** + * @inheritDoc + */ + public function getType(): string + { + return $this->type; + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @inheritDoc + */ + public function getAffectedComponents(): array + { + return $this->affectedComponents; + } + + /** + * @inheritDoc + */ + public function getRequire(): array + { + return $this->require; + } + + /** + * @inheritDoc + */ + public function getReplacedWith(): string + { + return $this->replacedWith; + } + + /** + * @inheritDoc + */ + public function isDeprecated(): bool + { + return $this->isDeprecated; + } + + /** + * @inheritDoc + */ + public function getItems(): array + { + return $this->items; + } +} diff --git a/src/Patch/Data/AggregatedPatchInterface.php b/src/Patch/Data/AggregatedPatchInterface.php new file mode 100644 index 00000000..1c5a81b4 --- /dev/null +++ b/src/Patch/Data/AggregatedPatchInterface.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Data; + +/** + * Aggregated patch data interface. + */ +interface AggregatedPatchInterface +{ + /** + * Aggregated patch ID + * + * Patch unique identifier. + * + * @return string + */ + public function getId(): string; + + /** + * Aggregated patch type. + * + * @return string + */ + public function getType(): string; + + /** + * Aggregated patch title. + * + * @return string + */ + public function getTitle(): string; + + /** + * List of affected components. + * + * @return string[] + */ + public function getAffectedComponents(): array; + + /** + * Required patches. + * + * @return string[] + */ + public function getRequire(): array; + + /** + * ID of the patch, which is recommended to replace the current patch. + * + * @return string + */ + public function getReplacedWith(): string; + + /** + * Is patch deprecated. + * + * @return bool + */ + public function isDeprecated(): bool; + + /** + * Patch items. + * + * @return PatchInterface[] + */ + public function getItems(): array; +} diff --git a/src/Patch/Data/Patch.php b/src/Patch/Data/Patch.php new file mode 100644 index 00000000..ffccef19 --- /dev/null +++ b/src/Patch/Data/Patch.php @@ -0,0 +1,206 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Data; + +/** + * Patch data class. + */ +class Patch implements PatchInterface +{ + /** + * @var string + */ + private $id; + + /** + * @var string + */ + private $type; + + /** + * @var string + */ + private $title; + + /** + * @var string + */ + private $filename; + + /** + * @var string + */ + private $path; + + /** + * @var string + */ + private $packageName; + + /** + * @var string + */ + private $packageConstraint; + + /** + * @var array + */ + private $affectedComponents; + + /** + * @var array + */ + private $require; + /** + * @var string + */ + private $replacedWith; + + /** + * @var boolean + */ + private $isDeprecated; + + /** + * @param string $id + * @param string $type + * @param string $title + * @param string $filename + * @param string $path + * @param string $packageName + * @param string $packageConstraint + * @param string[] $affectedComponents + * @param string[] $require + * @param string $replacedWith + * @param bool $isDeprecated + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + string $id, + string $type, + string $title, + string $filename, + string $path, + string $packageName, + string $packageConstraint, + array $affectedComponents, + array $require, + string $replacedWith, + bool $isDeprecated + ) { + + $this->id = $id; + $this->type = $type; + $this->title = $title; + $this->filename = $filename; + $this->path = $path; + $this->packageName = $packageName; + $this->packageConstraint = $packageConstraint; + $this->affectedComponents = $affectedComponents; + $this->require = $require; + $this->replacedWith = $replacedWith; + $this->isDeprecated = $isDeprecated; + } + + /** + * @return string + */ + public function __toString() + { + return $this->id . $this->path; + } + + /** + * @inheritDoc + */ + public function getId(): string + { + return $this->id; + } + + /** + * @inheritDoc + */ + public function getType(): string + { + return $this->type; + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @inheritDoc + */ + public function getFilename(): string + { + return $this->filename; + } + + /** + * @inheritDoc + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @inheritDoc + */ + public function getPackageName(): string + { + return $this->packageName; + } + + /** + * @inheritDoc + */ + public function getPackageConstraint(): string + { + return $this->packageConstraint; + } + + /** + * @inheritDoc + */ + public function getAffectedComponents(): array + { + return $this->affectedComponents; + } + + /** + * @inheritDoc + */ + public function getRequire(): array + { + return $this->require; + } + + /** + * @inheritDoc + */ + public function getReplacedWith(): string + { + return $this->replacedWith; + } + + /** + * @inheritDoc + */ + public function isDeprecated(): bool + { + return $this->isDeprecated; + } +} diff --git a/src/Patch/Data/PatchInterface.php b/src/Patch/Data/PatchInterface.php new file mode 100644 index 00000000..a9df332b --- /dev/null +++ b/src/Patch/Data/PatchInterface.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Data; + +/** + * Patch data interface. + */ +interface PatchInterface +{ + /** + * Patch is required (Cloud patches on Cloud). + */ + const TYPE_REQUIRED = 'Required'; + + /** + * Patch is optional. + */ + const TYPE_OPTIONAL = 'Optional'; + + /** + * Patch is client specific (m2-hotfixes on Cloud). + */ + const TYPE_CUSTOM = 'Custom'; + + /** + * Patch ID + * + * Used Jira issue number as patch unique identifier. + * + * @return string + */ + public function getId(): string; + + /** + * Patch type. + * + * @return string + */ + public function getType(): string; + + /** + * Short patch description. + * + * @return string + */ + public function getTitle(): string; + + /** + * Patch filename. + * + * @return string + */ + public function getFilename(): string; + + /** + * Patch path. + * + * @return string + */ + public function getPath(): string; + + /** + * Name of the composer package associated with patch. + * + * @return string + */ + public function getPackageName(): string; + + /** + * Version constraint of the composer package associated with patch. + * + * @return string + */ + public function getPackageConstraint(): string; + + /** + * List of affected components. + * + * @return string[] + */ + public function getAffectedComponents(): array; + + /** + * Required patches. + * + * @return string[] + */ + public function getRequire(): array; + + /** + * Id of patch that current patch was replaced. + * + * @return string + */ + public function getReplacedWith(): string; + + /** + * Is patch deprecated. + * + * @return bool + */ + public function isDeprecated(): bool; +} diff --git a/src/Patch/Environment.php b/src/Patch/Environment.php new file mode 100644 index 00000000..2f520734 --- /dev/null +++ b/src/Patch/Environment.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +/** + * Environment configuration. + */ +class Environment +{ + /** + * Variable to define a Cloud environment. + */ + const ENV_VAR_CLOUD = 'MAGENTO_CLOUD_PROJECT'; + + /** + * Checks if it's Cloud environment. + * + * @return bool + */ + public function isCloud() + { + $result = $_ENV[self::ENV_VAR_CLOUD] ?? getenv(self::ENV_VAR_CLOUD); + + return (bool)$result; + } +} diff --git a/src/Patch/FilterFactory.php b/src/Patch/FilterFactory.php new file mode 100644 index 00000000..1aa4eec6 --- /dev/null +++ b/src/Patch/FilterFactory.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +/** + * Creates patch filter. + */ +class FilterFactory +{ + /** + * Returns patch apply filter. + * + * @param string[] $argPatches 'List of patches' input argument. + * @return string[]|null + */ + public function createApplyFilter(array $argPatches) + { + $firstArgument = reset($argPatches); + if ($firstArgument === '*') { + return []; + } + + return $argPatches ?: null; + } + + /** + * Returns patch revert filter. + * + * @param bool $optAll 'All patches' input option. + * @param string[] $argPatches 'List of patches' input argument. + * @return string[]|null + */ + public function createRevertFilter(bool $optAll, array $argPatches) + { + if ($optAll) { + return []; + } + + return $argPatches ?: null; + } +} diff --git a/src/Patch/PatchBuilder.php b/src/Patch/PatchBuilder.php new file mode 100644 index 00000000..fd87e557 --- /dev/null +++ b/src/Patch/PatchBuilder.php @@ -0,0 +1,266 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Data\PatchInterface; + +/** + * Builder for Patch. + * + * @see Patch + */ +class PatchBuilder +{ + /** + * @var string + */ + private $id; + + /** + * @var string + */ + private $type; + + /** + * @var string + */ + private $title; + + /** + * @var string + */ + private $filename; + + /** + * @var string + */ + private $path; + + /** + * @var string + */ + private $packageName = ''; + + /** + * @var string + */ + private $packageConstraint = ''; + + /** + * @var string[] + */ + private $require = []; + + /** + * @var string + */ + private $replacedWith = ''; + + /** + * @var boolean + */ + private $deprecated = false; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem) + { + $this->filesystem = $filesystem; + } + + /** + * Sets patch id. + * + * @param string $id + */ + public function setId(string $id) + { + $this->id = $id; + } + + /** + * Sets patch type. + * + * @param string $type + * @return void + */ + public function setType(string $type) + { + $this->type = $type; + } + + /** + * Sets patch title. + * + * @param string $title + * + * @return void + */ + public function setTitle(string $title) + { + $this->title = $title; + } + + /** + * Sets patch filename. + * + * @param string $filename + * @return void + */ + public function setFilename(string $filename) + { + $this->filename = $filename; + } + + /** + * Sets patch path. + * + * @param string $path + * @return void + */ + public function setPath(string $path) + { + $this->path = $path; + } + + /** + * Sets package name. + * + * @param string $packageName + * @return void + */ + public function setPackageName(string $packageName) + { + $this->packageName = $packageName; + } + + /** + * Sets package constraint. + * + * @param string $packageConstraint + * @return void + */ + public function setPackageConstraint(string $packageConstraint) + { + $this->packageConstraint = $packageConstraint; + } + + /** + * Sets patch require. + * + * @param string[] $require + * @return void + */ + public function setRequire(array $require) + { + $this->require = $require; + } + + /** + * Sets patch replacedWith. + * + * @param string $replacedWith + * @return void + */ + public function setReplacedWith(string $replacedWith) + { + $this->replacedWith = $replacedWith; + } + + /** + * Sets if patch is deprecated. + * + * @param bool $deprecated + * @return void + */ + public function setDeprecated(bool $deprecated) + { + $this->deprecated = $deprecated; + } + + /** + * Builds patch data object. + * + * @return PatchInterface + * @throws PatchIntegrityException + */ + public function build() + { + $id = strtoupper($this->id); + $components = $this->getAffectedComponents($this->path); + + return new Patch( + $id, + $this->type, + $this->title, + $this->filename, + $this->path, + $this->packageName, + $this->packageConstraint, + $components, + $this->require, + $this->replacedWith, + $this->deprecated + ); + } + + /** + * Returns list of affected components. + * + * @param string $path + * @return array + * @throws PatchIntegrityException + */ + private function getAffectedComponents(string $path): array + { + try { + $content = $this->filesystem->get($path); + } catch (FileSystemException $e) { + throw new PatchIntegrityException($e->getMessage(), $e->getCode(), $e); + } + + $result = []; + if (preg_match_all( + '#^.* [ab]/vendor/(?<vendor>.*?)/(?<component>.*?)/.*$#mi', + $content, + $matches, + PREG_SET_ORDER + )) { + foreach ($matches as $match) { + $result[] = $match['vendor'] . '/' . $match['component']; + } + } + + if (preg_match_all( + '#^.* [ab]/(?<folder>.*?)/(?<subfolder>.*?)[/ ].*$#mi', + $content, + $matches, + PREG_SET_ORDER + )) { + foreach ($matches as $match) { + if ($match['folder'] !== 'vendor') { + $result[] = $match['folder'] . '/' . $match['subfolder']; + } + } + } + + $result = array_unique($result); + sort($result); + + return $result; + } +} diff --git a/src/Patch/PatchIntegrityException.php b/src/Patch/PatchIntegrityException.php new file mode 100644 index 00000000..e6050763 --- /dev/null +++ b/src/Patch/PatchIntegrityException.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\App\GenericException; + +/** + * Exception if patch contains invalid data (like invalid source file or not available patches in require section). + */ +class PatchIntegrityException extends GenericException +{ +} diff --git a/src/Patch/Pool/LocalPool.php b/src/Patch/Pool/LocalPool.php new file mode 100644 index 00000000..72d25590 --- /dev/null +++ b/src/Patch/Pool/LocalPool.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Pool; + +use Magento\CloudPatches\Patch\Collector\LocalCollector; +use Magento\CloudPatches\Patch\Data\PatchInterface; + +/** + * Contains local patches. + */ +class LocalPool +{ + /** + * @var PatchInterface[] + */ + private $items; + + /** + * @param LocalCollector $localCollector + */ + public function __construct( + LocalCollector $localCollector + ) { + $this->items = $localCollector->collect(); + } + + /** + * Returns list of patches. + * + * @return PatchInterface[] + */ + public function getList() + { + return $this->items; + } +} diff --git a/src/Patch/Pool/OptionalPool.php b/src/Patch/Pool/OptionalPool.php new file mode 100644 index 00000000..f26a1b7f --- /dev/null +++ b/src/Patch/Pool/OptionalPool.php @@ -0,0 +1,212 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Pool; + +use Magento\CloudPatches\Patch\Collector\CollectorException; +use Magento\CloudPatches\Patch\Collector\QualityCollector; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\PatchIntegrityException; + +/** + * Contains all optional patches. + */ +class OptionalPool +{ + /** + * @var PatchInterface[] + */ + private $items; + + /** + * @param RequiredPool $requiredPool + * @param QualityCollector $qualityCollector + * @throws CollectorException + */ + public function __construct( + RequiredPool $requiredPool, + QualityCollector $qualityCollector + ) { + $this->items = array_merge( + $requiredPool->getList(), + $qualityCollector->collect() + ); + } + + /** + * Returns list of patches. + * + * @param string[] $filter + * @param bool $useRequire + * + * @return PatchInterface[] + * @throws PatchIntegrityException + * @throws PatchNotFoundException + */ + public function getList($filter = [], $useRequire = true) + { + if (empty($filter)) { + return $this->items; + } + + $result = []; + foreach ($this->items as $patch) { + if (in_array($patch->getId(), $filter)) { + if ($patch->getRequire() && $useRequire) { + $result[] = $this->getRequireList($patch->getRequire()); + } + $result[] = [$patch]; + } + } + + $result = $result ? array_unique(array_merge(...$result)) : []; + $this->validateSearchResult($filter, $result); + + return $result; + } + + /** + * Returns list of all optional patches. + * + * @return PatchInterface[] + * @throws PatchIntegrityException + */ + public function getOptionalAll() + { + $items = array_filter( + $this->items, + function ($patch) { + return $patch->getType() === PatchInterface::TYPE_OPTIONAL; + } + ); + + $result = []; + foreach ($items as $patch) { + if ($patch->getRequire()) { + $result[] = $this->getRequireList($patch->getRequire()); + } + $result[] = [$patch]; + } + $result = $result ? array_unique(array_merge(...$result)) : []; + + return $result; + } + + /** + * Returns patch ids dependent on provided patch if any. + * + * @param string $patchId + * @return string[] + */ + public function getDependentOn($patchId) + { + if (!$patchId) { + return []; + } + + $result = []; + foreach ($this->items as $patch) { + if (in_array($patchId, $patch->getRequire())) { + $result[] = $this->getDependentOn($patch->getId()); + $result[] = [$patch->getId()]; + } + } + + $result = array_unique(array_merge([], ...$result)); + + return $result; + } + + /** + * Returns required patches which are not included in patch filter. + * + * @param string[] $filter + * + * @return PatchInterface[] + * @throws PatchNotFoundException + * @throws PatchIntegrityException + */ + public function getAdditionalRequiredPatches($filter) + { + if (empty($filter)) { + return []; + } + $patches = $this->getList($filter); + + return array_filter( + $patches, + function ($patch) use ($filter) { + return !in_array($patch->getId(), $filter); + } + ); + } + + /** + * Returns patch ids replaced by provided patch if any. + * + * @param string $patchId + * @return string[] + */ + public function getReplacedBy($patchId) + { + if (!$patchId) { + return []; + } + + $result = []; + foreach ($this->items as $patch) { + if ($patchId === $patch->getReplacedWith()) { + $result[] = $patch->getId(); + } + } + + return array_unique($result); + } + + /** + * Validates search result. + * + * @param string[] $filter + * @param PatchInterface[] $searchResult + * + * @return void + * @throws PatchNotFoundException + */ + private function validateSearchResult(array $filter, array $searchResult) + { + $resultIds = array_map( + function (PatchInterface $patch) { + return $patch->getId(); + }, + $searchResult + ); + + $diff = array_diff($filter, $resultIds); + if (count($diff) > 0) { + throw new PatchNotFoundException( + 'Next patches weren\'t found: ' . implode($diff, ' ') . '. ' . + 'Please, check with "status" command availability of these patches for the current Magento version.' + ); + } + } + + /** + * Returns a list of patches for 'require' configuration option. + * + * @param array $require + * @return PatchInterface[] + * @throws PatchIntegrityException + */ + private function getRequireList(array $require): array + { + try { + return $this->getList($require); + } catch (PatchNotFoundException $e) { + throw new PatchIntegrityException('Configuration error - ' . $e->getMessage(), $e->getCode()); + } + } +} diff --git a/src/Patch/Pool/PatchNotFoundException.php b/src/Patch/Pool/PatchNotFoundException.php new file mode 100644 index 00000000..4d54232c --- /dev/null +++ b/src/Patch/Pool/PatchNotFoundException.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Pool; + +use Magento\CloudPatches\App\GenericException; + +/** + * Exception if a patch could not be found in a pool. + */ +class PatchNotFoundException extends GenericException +{ +} diff --git a/src/Patch/Pool/RequiredPool.php b/src/Patch/Pool/RequiredPool.php new file mode 100644 index 00000000..3c2f2ddb --- /dev/null +++ b/src/Patch/Pool/RequiredPool.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Pool; + +use Magento\CloudPatches\Patch\Collector\CloudCollector; +use Magento\CloudPatches\Patch\Collector\CollectorException; +use Magento\CloudPatches\Patch\Data\PatchInterface; + +/** + * Contains required patches. + */ +class RequiredPool +{ + /** + * @var PatchInterface[] + */ + private $items; + + /** + * @param CloudCollector $cloudCollector + * @throws CollectorException + */ + public function __construct( + CloudCollector $cloudCollector + ) { + $this->items = $cloudCollector->collect(); + } + + /** + * Returns list of patches. + * + * @return PatchInterface[] + */ + public function getList() + { + return $this->items; + } +} diff --git a/src/Patch/RevertValidator.php b/src/Patch/RevertValidator.php new file mode 100644 index 00000000..6c19e27f --- /dev/null +++ b/src/Patch/RevertValidator.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Status\StatusPool; + +/** + * Validates that patches can be reverted. + */ +class RevertValidator +{ + /** + * @var OptionalPool + */ + private $optionalPool; + + /** + * @var StatusPool + */ + private $statusPool; + + /** + * @param OptionalPool $optionalPool + * @param StatusPool $statusPool + */ + public function __construct( + OptionalPool $optionalPool, + StatusPool $statusPool + ) { + $this->optionalPool = $optionalPool; + $this->statusPool = $statusPool; + } + + /** + * Verifies that there are no applied dependent patches. + * + * @param string[] $patchFilter + * @return void + * @throws RuntimeException + */ + public function validate(array $patchFilter) + { + $errors = []; + foreach ($patchFilter as $patchId) { + $appliedDependents = array_filter( + $this->optionalPool->getDependentOn($patchId), + function ($patchId) { + return $this->statusPool->isApplied($patchId); + } + ); + if (array_diff($appliedDependents, $patchFilter)) { + $errors[] = sprintf( + 'Patch %1$s is a dependency for %2$s. Please, revert %2$s first', + $patchId, + implode(' ', $appliedDependents) + ); + } + } + + if ($errors) { + throw new RuntimeException(implode(PHP_EOL, $errors)); + } + } +} diff --git a/src/Patch/SourceProvider.php b/src/Patch/SourceProvider.php new file mode 100644 index 00000000..a8373183 --- /dev/null +++ b/src/Patch/SourceProvider.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\Filesystem\DirectoryList; +use Magento\CloudPatches\Filesystem\FileList; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\QualityPatches\Info as QualityPatchesInfo; +use Magento\CloudPatches\Filesystem\Filesystem; + +/** + * Patches config provider. + */ +class SourceProvider +{ + /** + * Directory for hot-fixes. + */ + const HOT_FIXES_DIR = 'm2-hotfixes'; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var FileList + */ + private $fileList; + + /** + * @var DirectoryList + */ + private $directoryList; + + /** + * @var QualityPatchesInfo + */ + private $qualityPatchesInfo; + + /** + * @param Filesystem $filesystem + * @param FileList $fileList + * @param DirectoryList $directoryList + * @param QualityPatchesInfo $qualityPatchesInfo + */ + public function __construct( + Filesystem $filesystem, + FileList $fileList, + DirectoryList $directoryList, + QualityPatchesInfo $qualityPatchesInfo + ) { + $this->filesystem = $filesystem; + $this->fileList = $fileList; + $this->directoryList = $directoryList; + $this->qualityPatchesInfo = $qualityPatchesInfo; + } + + /** + * Returns configuration of Cloud patches. + * + * Example of configuration: + * ``` + * "colinmollenhour/credis" : { + * "Fix Redis issue": { + * "1.6": "MAGETWO-67097__fix_credis_pipeline_bug.patch" + * } + * } + * + * Each patch must have corresponding version constraint of target composer package. + * @see https://getcomposer.org/doc/articles/versions.md + * + * @return array + * @throws SourceProviderException + */ + public function getCloudPatches(): array + { + $configPath = $this->fileList->getPatches(); + + return $this->readConfiguration($configPath); + } + + /** + * Returns configuration of Quality patches. + * + * @return array + * @throws SourceProviderException + */ + public function getQualityPatches(): array + { + $configSupportPath = $this->qualityPatchesInfo->getPatchesConfig(); + + return $this->readConfiguration($configSupportPath); + } + + /** + * Returns list of local patches from m2-hotfixes directory. + * + * @return array + */ + public function getLocalPatches(): array + { + $hotFixesDir = $this->directoryList->getMagentoRoot() . '/' . static::HOT_FIXES_DIR; + $files = glob($hotFixesDir . '/*.patch'); + if ($files) { + sort($files); + } + + return $files ?: []; + } + + /** + * Return patch configuration. + * + * @param string $configPath + * + * @return array + * @throws SourceProviderException + */ + private function readConfiguration(string $configPath): array + { + try { + $content = $this->filesystem->get($configPath); + } catch (FileSystemException $e) { + throw new SourceProviderException($e->getMessage(), $e->getCode(), $e); + } + + $result = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new SourceProviderException( + "Unable to unserialize patches configuration '{$configPath}'. Error: " . json_last_error_msg() + ); + } + + return $result; + } +} diff --git a/src/Patch/SourceProviderException.php b/src/Patch/SourceProviderException.php new file mode 100644 index 00000000..2b5906e0 --- /dev/null +++ b/src/Patch/SourceProviderException.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\App\GenericException; + +/** + * Exception if there are some troubles with reading patch source configuration. + */ +class SourceProviderException extends GenericException +{ +} diff --git a/src/Patch/Status/LocalResolver.php b/src/Patch/Status/LocalResolver.php new file mode 100644 index 00000000..d82f29b7 --- /dev/null +++ b/src/Patch/Status/LocalResolver.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Status; + +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\CloudPatches\Patch\Pool\LocalPool; +use Magento\CloudPatches\Patch\Applier; + +/** + * Resolves statuses of local patches. + */ +class LocalResolver implements ResolverInterface +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var Applier + */ + private $applier; + + /** + * @var LocalPool + */ + private $localPool; + + /** + * @param Filesystem $filesystem + * @param Applier $applier + * @param LocalPool $localPool + */ + public function __construct( + Filesystem $filesystem, + Applier $applier, + LocalPool $localPool + ) { + $this->filesystem = $filesystem; + $this->applier = $applier; + $this->localPool = $localPool; + } + + /** + * @inheritDoc + */ + public function resolve(): array + { + $result = []; + foreach ($this->localPool->getList() as $patch) { + try { + $content = $this->filesystem->get($patch->getPath()); + } catch (FileSystemException $e) { + throw new StatusResolverException($e->getMessage(), $e->getCode(), $e); + } + + $result[$patch->getId()] = $this->applier->status($content); + } + + return $result; + } +} diff --git a/src/Patch/Status/OptionalResolver.php b/src/Patch/Status/OptionalResolver.php new file mode 100644 index 00000000..9f6a990f --- /dev/null +++ b/src/Patch/Status/OptionalResolver.php @@ -0,0 +1,203 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Status; + +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Aggregator; +use Magento\CloudPatches\Patch\Applier; + +/** + * Resolves statuses of quality patches. + */ +class OptionalResolver implements ResolverInterface +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var Applier + */ + private $applier; + + /** + * @var Aggregator + */ + private $aggregator; + + /** + * @var OptionalPool + */ + private $optionalPool; + + /** + * @param Filesystem $filesystem + * @param Applier $applier + * @param Aggregator $aggregator + * @param OptionalPool $optionalPool + */ + public function __construct( + Filesystem $filesystem, + Applier $applier, + Aggregator $aggregator, + OptionalPool $optionalPool + ) { + $this->filesystem = $filesystem; + $this->applier = $applier; + $this->aggregator = $aggregator; + $this->optionalPool = $optionalPool; + } + + /** + * @inheritDoc + */ + public function resolve(): array + { + $patchList = $this->optionalPool->getList(); + $aggregatedPatches = $this->aggregator->aggregate($patchList); + + $statuses = [ + StatusPool::NA => [], + StatusPool::NOT_APPLIED => [], + StatusPool::APPLIED => [] + ]; + + $statuses = $this->analyzeIndependentPatches($aggregatedPatches, $statuses); + $statuses = $this->analyzeDependentPatches($aggregatedPatches, $statuses); + $statuses = $this->analyzeDependenciesOfAppliedPatches($aggregatedPatches, $statuses); + + $result = []; + foreach ($statuses as $status => $ids) { + foreach ($ids as $id) { + $result[$id] = $status; + } + } + + return $result; + } + + /** + * Analyzes patches without dependencies. + * + * @param AggregatedPatchInterface[] $aggregatedPatches + * @param array $statuses + * + * @return array + * @throws StatusResolverException + */ + private function analyzeIndependentPatches(array $aggregatedPatches, array $statuses) + { + $independentPatches = array_filter( + $aggregatedPatches, + function (AggregatedPatchInterface $patch) { + return empty($patch->getRequire()); + } + ); + foreach ($independentPatches as $aggregatedPatch) { + $content = $this->getContent($aggregatedPatch->getItems()); + $status = $this->applier->status($content); + $statuses[$status][] = $aggregatedPatch->getId(); + } + + return $statuses; + } + + /** + * Analyzes patches with dependencies. + * + * @param AggregatedPatchInterface[] $aggregatedPatches + * @param array $statuses + * + * @return array + * @throws StatusResolverException + */ + private function analyzeDependentPatches( + array $aggregatedPatches, + array $statuses + ) { + $dependentPatches = array_filter( + $aggregatedPatches, + function (AggregatedPatchInterface $patch) { + return $patch->getRequire(); + } + ); + + foreach ($dependentPatches as $dependentPatch) { + // filter patches that have Applied or N/A status + $requiredPatches = array_filter( + $this->optionalPool->getList([$dependentPatch->getId()]), + function (PatchInterface $patch) use ($statuses, $dependentPatch) { + return $patch->getId() === $dependentPatch->getId() || + in_array($patch->getId(), $statuses[StatusPool::NOT_APPLIED]); + } + ); + $content = $this->getContent($requiredPatches); + $status = $this->applier->status($content); + $statuses[$status][] = $dependentPatch->getId(); + } + + return $statuses; + } + + /** + * Analyzes dependencies of applied patches. + * + * If the dependent patch was applied we assume that required patch was applied as well. + * + * @param AggregatedPatchInterface[] $aggregatedPatches + * @param array $statuses + * @return array + */ + private function analyzeDependenciesOfAppliedPatches(array $aggregatedPatches, array $statuses) + { + $undefinedPatches = array_reverse($statuses[StatusPool::NA]); + foreach ($undefinedPatches as $patchId) { + foreach ($aggregatedPatches as $aggregatedPatch) { + $aggregatedRequire = $aggregatedPatch->getRequire(); + if (in_array($patchId, $aggregatedRequire) && + in_array($aggregatedPatch->getId(), $statuses[StatusPool::APPLIED]) + ) { + array_push($statuses[StatusPool::APPLIED], $patchId); + $statuses[StatusPool::NA] = array_diff( + $statuses[StatusPool::NA], + [$patchId] + ); + } + } + } + + return $statuses; + } + + /** + * Returns aggregated patch content. + * + * @param PatchInterface[] $patches + * + * @return string + * @throws StatusResolverException + */ + private function getContent(array $patches) + { + $result = ''; + foreach ($patches as $patch) { + try { + $result .= $this->filesystem->get($patch->getPath()); + } catch (FileSystemException $e) { + throw new StatusResolverException($e->getMessage(), $e->getCode(), $e); + } + } + + return $result; + } +} diff --git a/src/Patch/Status/ResolverInterface.php b/src/Patch/Status/ResolverInterface.php new file mode 100644 index 00000000..db88d5b7 --- /dev/null +++ b/src/Patch/Status/ResolverInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Status; + +/** + * Patch status resolver interface. + */ +interface ResolverInterface +{ + /** + * Resolves patch statuses. + * + * @return string[] + * @throws StatusResolverException + */ + public function resolve(): array; +} diff --git a/src/Patch/Status/StatusPool.php b/src/Patch/Status/StatusPool.php new file mode 100644 index 00000000..400fac11 --- /dev/null +++ b/src/Patch/Status/StatusPool.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Status; + +use InvalidArgumentException; + +/** + * Contains statuses of patches. + */ +class StatusPool +{ + /** + * Patch was applied. + */ + const APPLIED = 'Applied'; + + /** + * Status of patch can't be identified. There are some conflicts caused by other patches. + */ + const NA = 'N/A'; + + /** + * Patch is ready to apply. + */ + const NOT_APPLIED = 'Not applied'; + + /** + * @var array + */ + private $result; + + /** + * @param ResolverInterface[] $resolvers + */ + public function __construct( + array $resolvers + ) { + $result = []; + foreach ($resolvers as $resolver) { + if (!$resolver instanceof ResolverInterface) { + throw new InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + ResolverInterface::class, + get_class($resolver) + ) + ); + } + $result[] = $resolver->resolve(); + } + $this->result = array_merge([], ...$result); + } + + /** + * Returns status of patch. + * + * @param string $id + * @return string + */ + public function get($id) + { + return $this->result[$id] ?? self::NA; + } + + /** + * Checks if patch is applied. + * + * @param string $id + * @return bool + */ + public function isApplied($id) + { + return $this->result[$id] === self::APPLIED; + } + + /** + * Checks if patch is not applied. + * + * @param string $id + * @return bool + */ + public function isNotApplied($id) + { + return $this->result[$id] === self::NOT_APPLIED; + } +} diff --git a/src/Command/Patch/ManagerException.php b/src/Patch/Status/StatusResolverException.php similarity index 58% rename from src/Command/Patch/ManagerException.php rename to src/Patch/Status/StatusResolverException.php index 81aaae68..baa3d4f5 100644 --- a/src/Command/Patch/ManagerException.php +++ b/src/Patch/Status/StatusResolverException.php @@ -5,13 +5,13 @@ */ declare(strict_types=1); -namespace Magento\CloudPatches\Command\Patch; +namespace Magento\CloudPatches\Patch\Status; use Magento\CloudPatches\App\GenericException; /** - * Error for wrong format of constraints. + * Generic status resolver exception. */ -class ManagerException extends GenericException +class StatusResolverException extends GenericException { } diff --git a/src/Shell/ProcessFactory.php b/src/Shell/ProcessFactory.php index a57d50e2..5184bea2 100644 --- a/src/Shell/ProcessFactory.php +++ b/src/Shell/ProcessFactory.php @@ -42,19 +42,27 @@ public function __construct(DirectoryList $directoryList, Composer $composer) } /** + * Creates process. + * * @param array $cmd + * @param string|null $input * @return Process + * @throws PackageNotFoundException */ - public function create(array $cmd): Process + public function create(array $cmd, string $input = null): Process { return new Process( $this->processSupportsArrayParam() ? $cmd : implode(' ', $cmd), - $this->directoryList->getMagentoRoot() + $this->directoryList->getMagentoRoot(), + null, + $input ); } /** * Test if symfony/process is current enough to support an array for its first parameter. + * + * @throws PackageNotFoundException */ private function processSupportsArrayParam(): bool { diff --git a/src/Test/Functional/Acceptance/AbstractCest.php b/src/Test/Functional/Acceptance/AbstractCest.php index 2ae5d57b..a5c9eeab 100644 --- a/src/Test/Functional/Acceptance/AbstractCest.php +++ b/src/Test/Functional/Acceptance/AbstractCest.php @@ -37,11 +37,16 @@ protected function prepareTemplate(\CliTester $I, string $templateVersion): void $I->createArtifactCurrentTestedCode('patches', '1.0.99'); $I->addArtifactsRepoToComposer(); $I->addEceDockerGitRepoToComposer(); + $I->addQualityPatchesGitRepoToComposer(); $I->addDependencyToComposer('magento/magento-cloud-patches', '1.0.99'); $I->addDependencyToComposer( 'magento/magento-cloud-docker', $I->getDependencyVersion('magento/magento-cloud-docker') ); + $I->addDependencyToComposer( + 'magento/quality-patches', + $I->getDependencyVersion('magento/quality-patches') + ); if ($this->edition === 'CE') { $version = $this->getVersionRangeForMagento($I); @@ -53,6 +58,15 @@ protected function prepareTemplate(\CliTester $I, string $templateVersion): void $I->composerUpdate(); } + /** + * @param array $data + * @return string + */ + protected function convertEnvFromArrayToJson(array $data): string + { + return addslashes(json_encode($data)); + } + /** * @param \CliTester $I */ diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 15de4166..6e3ae99e 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -32,7 +32,10 @@ protected function getVersionRangeForMagento(\CliTester $I): string public function testPatches(\CliTester $I, \Codeception\Example $data): void { $this->prepareTemplate($I, $data['templateVersion']); - $I->assertTrue($I->runEceDockerCommand('build:compose --mode=production')); + $I->runEceDockerCommand(sprintf( + 'build:compose --mode=production --env-vars="%s"', + $this->convertEnvFromArrayToJson(['MAGENTO_CLOUD_PROJECT' => 'travis-testing']) + )); $I->assertTrue($I->runDockerComposeCommand('run build cloud-build')); $I->assertTrue($I->startEnvironment()); $I->assertTrue($I->runDockerComposeCommand('run deploy cloud-deploy')); diff --git a/src/Test/Unit/Command/ApplyTest.php b/src/Test/Unit/Command/ApplyTest.php index c751f658..85f35789 100644 --- a/src/Test/Unit/Command/ApplyTest.php +++ b/src/Test/Unit/Command/ApplyTest.php @@ -7,12 +7,17 @@ namespace Magento\CloudPatches\Test\Unit\Command; +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\AbstractCommand; use Magento\CloudPatches\Command\Apply; -use Magento\CloudPatches\Command\Patch\Manager; -use Magento\CloudPatches\Command\Patch\ManagerException; -use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Command\Process\ApplyLocal; +use Magento\CloudPatches\Command\Process\ApplyOptional; +use Magento\CloudPatches\Command\Process\ApplyRequired; +use Magento\CloudPatches\Composer\MagentoVersion; +use Magento\CloudPatches\Patch\Environment; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -27,38 +32,176 @@ class ApplyTest extends TestCase private $command; /** - * @var Manager|MockObject + * @var ApplyLocal|MockObject */ - private $managerMock; + private $applyLocal; + + /** + * @var ApplyOptional|MockObject + */ + private $applyOptional; + + /** + * @var ApplyRequired|MockObject + */ + private $applyRequired; + + /** + * @var Environment|MockObject + */ + private $environment; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var MagentoVersion|MockObject + */ + private $magentoVersion; /** * @inheritDoc */ protected function setUp() { - $this->managerMock = $this->createMock(Manager::class); + $this->applyLocal = $this->createMock(ApplyLocal::class); + $this->applyOptional = $this->createMock(ApplyOptional::class); + $this->applyRequired = $this->createMock(ApplyRequired::class); + $this->environment = $this->createMock(Environment::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->magentoVersion = $this->createMock(MagentoVersion::class); $this->command = new Apply( - $this->managerMock + $this->applyRequired, + $this->applyOptional, + $this->applyLocal, + $this->environment, + $this->logger, + $this->magentoVersion + ); + } + + /** + * Tests successful command execution on Cloud environment. + */ + public function testCloudEnvironmentSuccess() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->environment->method('isCloud') + ->willReturn(true); + + $this->applyLocal->expects($this->once()) + ->method('run'); + $this->applyOptional->expects($this->once()) + ->method('run'); + $this->applyRequired->expects($this->once()) + ->method('run'); + + $this->assertEquals( + AbstractCommand::RETURN_SUCCESS, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests successful command execution on OnPrem environment. + */ + public function testOnPremEnvironmentSuccess() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->environment->method('isCloud') + ->willReturn(false); + + $this->applyLocal->expects($this->never()) + ->method('run'); + $this->applyOptional->expects($this->once()) + ->method('run'); + $this->applyRequired->expects($this->never()) + ->method('run'); + + $this->assertEquals( + AbstractCommand::RETURN_SUCCESS, + $this->command->execute($inputMock, $outputMock) ); } /** - * @throws ManagerException - * @throws ApplierException + * Tests when runtime error happens during command execution. */ - public function testExecute() + public function testRuntimeError() { /** @var InputInterface|MockObject $inputMock */ $inputMock = $this->getMockForAbstractClass(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - $this->managerMock->expects($this->once()) - ->method('applyComposerPatches'); - $this->managerMock->expects($this->once()) - ->method('applyHotFixes'); + $this->applyOptional->expects($this->once()) + ->method('run') + ->willThrowException(new RuntimeException('Error!')); + $this->logger->expects($this->once()) + ->method('error'); + $this->assertEquals( + AbstractCommand::RETURN_FAILURE, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when critical error happens during command execution. + */ + public function testCriticalError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->applyOptional->expects($this->once()) + ->method('run') + ->willThrowException(new \InvalidArgumentException('Critical error!')); + $this->logger->expects($this->once()) + ->method('critical'); + + $this->expectException(\InvalidArgumentException::class); $this->command->execute($inputMock, $outputMock); } + + /** + * Tests when Magento is installed from Git. + */ + public function testGitBasedInstallation() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $inputMock->expects($this->once()) + ->method('getOption') + ->with(Apply::OPT_GIT_INSTALLATION) + ->willReturn(1); + + $this->applyLocal->expects($this->never()) + ->method('run'); + $this->applyOptional->expects($this->never()) + ->method('run'); + $this->applyRequired->expects($this->never()) + ->method('run'); + + $this->assertEquals( + AbstractCommand::RETURN_SUCCESS, + $this->command->execute($inputMock, $outputMock) + ); + } } diff --git a/src/Test/Unit/Command/Patch/ManagerTest.php b/src/Test/Unit/Command/Patch/ManagerTest.php deleted file mode 100644 index 7028f129..00000000 --- a/src/Test/Unit/Command/Patch/ManagerTest.php +++ /dev/null @@ -1,209 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Test\Unit\Command\Patch; - -use Composer\Package\RootPackageInterface; -use Magento\CloudPatches\Command\Apply; -use Magento\CloudPatches\Command\Patch\Manager; -use Magento\CloudPatches\Command\Patch\ManagerException; -use Magento\CloudPatches\Filesystem\DirectoryList; -use Magento\CloudPatches\Filesystem\FileList; -use Magento\CloudPatches\Filesystem\FileNotFoundException; -use Magento\CloudPatches\Filesystem\Filesystem; -use Magento\CloudPatches\Patch\Applier; -use Magento\CloudPatches\Patch\ApplierException; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * @inheritdoc - */ -class ManagerTest extends TestCase -{ - /** - * @var Manager - */ - private $manager; - - /** - * @var Applier|MockObject - */ - private $applierMock; - - /** - * @var RootPackageInterface|MockObject - */ - private $composerPackageMock; - - /** - * @var Filesystem|MockObject - */ - private $filesystemMock; - - /** - * @var FileList|MockObject - */ - private $fileListMock; - - /** - * @var DirectoryList|MockObject - */ - private $directoryListMock; - - /** - * @inheritdoc - */ - protected function setUp() - { - $this->applierMock = $this->createMock(Applier::class); - $this->composerPackageMock = $this->getMockForAbstractClass(RootPackageInterface::class); - $this->filesystemMock = $this->createMock(Filesystem::class); - $this->directoryListMock = $this->createMock(DirectoryList::class); - $this->fileListMock = $this->createMock(FileList::class); - - $this->manager = new Manager( - $this->applierMock, - $this->filesystemMock, - $this->fileListMock, - $this->directoryListMock - ); - } - - /** - * @throws ApplierException - * @throws ManagerException - */ - public function testApplyComposerPatches() - { - $this->filesystemMock->expects($this->once()) - ->method('get') - ->willReturn(json_encode( - [ - 'package1' => [ - 'patchName1' => [ - '100' => 'patchPath1', - ], - ], - 'package2' => [ - 'patchName2' => [ - '101.*' => 'patchPath2', - ], - 'patchName3' => [ - '102.*' => 'patchPath3', - ], - ] - ] - )); - $this->applierMock->expects($this->exactly(3)) - ->method('apply') - ->withConsecutive( - ['patchPath1', 'patchName1', 'package1', '100'], - ['patchPath2', 'patchName2', 'package2', '101.*'], - ['patchPath3', 'patchName3', 'package2', '102.*'] - ); - - /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); - /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - - $inputMock->method('getOption') - ->with(Apply::OPT_GIT_INSTALLATION) - ->willReturn(false); - - $this->manager->applyComposerPatches($inputMock, $outputMock); - } - - /** - * @expectedException \Magento\CloudPatches\Command\Patch\ManagerException - * @expectedExceptionMessage Not Found - * - * @throws ApplierException - * @throws ManagerException - */ - public function testApplyComposerPatchesWithFSException() - { - $this->filesystemMock->expects($this->once()) - ->method('get') - ->willThrowException(new FileNotFoundException('Not Found')); - - /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); - /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - - $inputMock->method('getOption') - ->with(Apply::OPT_GIT_INSTALLATION) - ->willReturn('0'); - - $this->manager->applyComposerPatches($inputMock, $outputMock); - } - - /** - * @throws ApplierException - */ - public function testExecuteApplyHotFixes() - { - $this->directoryListMock->expects($this->any()) - ->method('getMagentoRoot') - ->willReturn(__DIR__ . '/_files'); - $this->filesystemMock->expects($this->once()) - ->method('isDirectory') - ->willReturn(true); - $this->applierMock->expects($this->exactly(2)) - ->method('applyFile') - ->willReturnMap([ - [__DIR__ . '/_files/' . Manager::HOT_FIXES_DIR . '/patch1.patch', true, 'Patch 1 applied'], - [__DIR__ . '/_files/' . Manager::HOT_FIXES_DIR . '/patch2.patch', true, 'Patch 2 applied'] - ]); - - /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); - $inputMock->expects($this->once()) - ->method('getOption') - ->with(Apply::OPT_GIT_INSTALLATION) - ->willReturn('1'); - /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - $outputMock->expects($this->exactly(3)) - ->method('writeln') - ->withConsecutive( - ['Applying hot-fixes', 0], - ['Patch 1 applied', 0], - ['Patch 2 applied', 0] - ); - - $this->manager->applyHotFixes($inputMock, $outputMock); - } - - /** - * @throws ApplierException - */ - public function testExecuteApplyHotFixesNotFound() - { - $this->directoryListMock->expects($this->any()) - ->method('getMagentoRoot') - ->willReturn(__DIR__ . '/_files'); - $this->filesystemMock->expects($this->once()) - ->method('isDirectory') - ->willReturn(false); - /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); - /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - $outputMock->expects($this->once()) - ->method('writeln') - ->withConsecutive( - ['Hot-fixes directory was not found. Skipping', 0] - ); - - $this->manager->applyHotFixes($inputMock, $outputMock); - } -} diff --git a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php new file mode 100644 index 00000000..5a264a64 --- /dev/null +++ b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php @@ -0,0 +1,270 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process\Action; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\ApplyOptionalAction; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ApplyOptionalActionTest extends TestCase +{ + /** + * @var ApplyOptionalAction + */ + private $action; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @var StatusPool|MockObject + */ + private $statusPool; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->applier = $this->createMock(Applier::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->statusPool = $this->createMock(StatusPool::class); + $this->optionalPool = $this->createMock(OptionalPool::class); + $this->renderer = $this->createMock(Renderer::class); + + $this->action = new ApplyOptionalAction( + $this->applier, + $this->optionalPool, + $this->statusPool, + $this->renderer, + $this->logger + ); + } + + /** + * Tests successful optional patches applying. + * + * Case: applying 3 optional non-deprecated patches that wasn't applied previously. + */ + public function testExecuteSuccessful() + { + $patchFilter = ['MC-11111', 'MC-22222', 'MC-33333']; + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $patch2 = $this->createPatch('/path/patch2.patch', 'MC-22222'); + $patch3 = $this->createPatch('/path/patch3.patch', 'MC-33333'); + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-11111', false], + ['MC-22222', false], + ['MC-33333', false], + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willReturn([$patch1, $patch2, $patch3]); + + $this->applier->method('apply') + ->willReturnMap([ + [$patch1->getPath(), $patch1->getId(), 'Patch ' . $patch1->getId() .' has been applied'], + [$patch2->getPath(), $patch2->getId(), 'Patch ' . $patch2->getId() .' has been applied'], + [$patch3->getPath(), $patch3->getId(), 'Patch ' . $patch3->getId() .' has been applied'], + ]); + + $this->renderer->expects($this->exactly(3)) + ->method('printPatchInfo') + ->withConsecutive( + [$outputMock, $patch1, 'Patch ' . $patch1->getId() .' has been applied'], + [$outputMock, $patch2, 'Patch ' . $patch2->getId() .' has been applied'], + [$outputMock, $patch3, 'Patch ' . $patch3->getId() .' has been applied'] + ); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests successful optional patches applying. + * + * Case: applying optional patch that was applied previously. + */ + public function testApplyAlreadyAppliedPatch() + { + $patchFilter = ['MC-11111']; + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-11111', true] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willReturn([$patch1]); + + $this->applier->expects($this->never()) + ->method('apply'); + $this->renderer->expects($this->never()) + ->method('printPatchInfo'); + + $outputMock->expects($this->once()) + ->method('writeln') + ->withConsecutive( + [ + $this->stringContains( + 'Patch ' . $patch1->getId() .' (' . $patch1->getFilename() . ') was already applied' + ) + ] + ); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests successful optional patches applying. + * + * Case: patch filter is empty (should apply all patches from the pool). Pool contains deprecated patch that + * shouldn't be applied. + */ + public function testApplyingAllPatchesAndSkipDeprecated() + { + $patchFilter = []; + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111', false); + $patch2 = $this->createPatch('/path/patch2.patch', 'MC-22222', true); + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-11111', false], + ['MC-22222', false] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getOptionalAll') + ->willReturn([$patch1, $patch2]); + + $this->applier->method('apply') + ->willReturnMap([ + [$patch1->getPath(), $patch1->getId(), 'Patch ' . $patch1->getId() .' has been applied'] + ]); + + $this->renderer->expects($this->once()) + ->method('printPatchInfo') + ->withConsecutive( + [$outputMock, $patch1, 'Patch ' . $patch1->getId() .' has been applied'] + ); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests optional patches applying with exception. + * + * Case: first patch is applied successfully, exception is thrown during applying second patch, + * rollback starts, first patch should be reverted. + * + * @throws RuntimeException + */ + public function testApplyWithException() + { + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $patch2 = $this->createPatch('/path/patch2.patch', 'MC-22222'); + $patchFilter = [$patch1->getId(), $patch2->getId()]; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->method('getList') + ->willReturn([$patch1, $patch2]); + + $this->applier->method('apply') + ->willReturnMap([ + [$patch1->getPath(), $patch1->getId()], + [$patch2->getPath(), $patch2->getId()] + ])->willReturnCallback( + function ($path, $id) { + if ($id === 'MC-22222') { + throw new ApplierException('Error'); + } + + return "Patch {$path} {$id} has been applied"; + } + ); + + $this->renderer->expects($this->once()) + ->method('formatErrorOutput') + ->with('Error'); + + $this->applier->expects($this->once()) + ->method('revert') + ->withConsecutive([$patch1->getPath(), $patch1->getId()]); + + $this->expectException(RuntimeException::class); + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Creates patch mock. + * + * @param string $path + * @param string $id + * @param bool $isDeprecated + * + * @return PatchInterface|MockObject + */ + private function createPatch(string $path, string $id, bool $isDeprecated = false) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getPath')->willReturn($path); + $patch->method('getFilename')->willReturn('filename.patch'); + $patch->method('getId')->willReturn($id); + $patch->method('isDeprecated')->willReturn($isDeprecated); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php b/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php new file mode 100644 index 00000000..1424fafb --- /dev/null +++ b/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php @@ -0,0 +1,192 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process\Action; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\ConfirmRequiredAction; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Aggregator; +use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Pool\PatchNotFoundException; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ConfirmRequiredActionTest extends TestCase +{ + /** + * @var ConfirmRequiredAction + */ + private $action; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @var StatusPool|MockObject + */ + private $statusPool; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @var Aggregator|MockObject + */ + private $aggregator; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->statusPool = $this->createMock(StatusPool::class); + $this->optionalPool = $this->createMock(OptionalPool::class); + $this->renderer = $this->createMock(Renderer::class); + $this->aggregator = $this->createMock(Aggregator::class); + + $this->action = new ConfirmRequiredAction( + $this->optionalPool, + $this->statusPool, + $this->aggregator, + $this->renderer + ); + } + + /** + * Tests asking confirmation for not applied patches. + */ + public function testAskConfirmationForNotAppliedPatches() + { + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $patch2 = $this->createPatch('/path/patch2.patch', 'MC-22222'); + $patch3 = $this->createPatch('/path/patch3.patch', 'MC-33333'); + $patchFilter = [$patch1->getId(), $patch2->getId(), $patch3->getId()]; + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-11111', false], + ['MC-22222', false], + ['MC-33333', true] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getAdditionalRequiredPatches') + ->withConsecutive([$patchFilter]) + ->willReturn([$patch1, $patch2, $patch3]); + + $aggregatedPatch = $this->getMockForAbstractClass(AggregatedPatchInterface::class); + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->with([$patch1, $patch2]) + ->willReturn([$aggregatedPatch]); + + $this->renderer->expects($this->once()) + ->method('printTable') + ->withConsecutive([$outputMock, [$aggregatedPatch]]); + + $this->renderer->expects($this->once()) + ->method('printQuestion') + ->willReturn(true); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests exception when patch from filter is not found. + */ + public function testPatchNotFoundException() + { + $patchFilter = ['unknown id']; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getAdditionalRequiredPatches') + ->withConsecutive([$patchFilter]) + ->willThrowException(new PatchNotFoundException('')); + + $this->expectException(RuntimeException::class); + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests exception when user refused to confirm applying additional patches. + */ + public function testConfirmationRejected() + { + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $patchFilter = [$patch1->getId()]; + $this->statusPool->method('isApplied') + ->willReturnMap([ + [$patch1->getId(), false] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getAdditionalRequiredPatches') + ->withConsecutive([$patchFilter]) + ->willReturn([$patch1]); + + $aggregatedPatch = $this->getMockForAbstractClass(AggregatedPatchInterface::class); + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->with([$patch1]) + ->willReturn([$aggregatedPatch]); + + $this->renderer->expects($this->once()) + ->method('printTable') + ->withConsecutive([$outputMock, [$aggregatedPatch]]); + + $this->renderer->expects($this->once()) + ->method('printQuestion') + ->willReturn(false); + + $this->expectException(RuntimeException::class); + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Creates patch mock. + * + * @param string $path + * @param string $id + * @param bool $isDeprecated + * + * @return PatchInterface|MockObject + */ + private function createPatch(string $path, string $id, bool $isDeprecated = false) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getPath')->willReturn($path); + $patch->method('getFilename')->willReturn('filename.patch'); + $patch->method('getId')->willReturn($id); + $patch->method('isDeprecated')->willReturn($isDeprecated); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php b/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php new file mode 100644 index 00000000..68cef97e --- /dev/null +++ b/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php @@ -0,0 +1,289 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process\Action; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\ProcessDeprecatedAction; +use Magento\CloudPatches\Command\Process\Action\RevertAction; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Aggregator; +use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ProcessDeprecatedActionTest extends TestCase +{ + /** + * @var ProcessDeprecatedAction + */ + private $action; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @var StatusPool|MockObject + */ + private $statusPool; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @var Aggregator|MockObject + */ + private $aggregator; + + /** + * @var RevertAction|MockObject + */ + private $revertAction; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->optionalPool = $this->createMock(OptionalPool::class); + $this->statusPool = $this->createMock(StatusPool::class); + $this->renderer = $this->createMock(Renderer::class); + $this->aggregator = $this->createMock(Aggregator::class); + $this->revertAction = $this->createMock(RevertAction::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + + $this->action = new ProcessDeprecatedAction( + $this->optionalPool, + $this->statusPool, + $this->revertAction, + $this->aggregator, + $this->renderer, + $this->logger + ); + } + + /** + * Tests successful processing patch list with deprecated patches. + */ + public function testProcessDeprecationSuccessful() + { + $patch1 = $this->createPatch('MC-11111', true, 'MC-22222'); + $patchFilter = [$patch1->getId()]; + $expectedMessage = sprintf( + 'Warning! Deprecated patch %s is going to be applied. Please, replace it with %s', + $patch1->getId(), + $patch1->getReplacedWith() + ); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $patchMock = $this->getMockForAbstractClass(PatchInterface::class); + + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willReturn([$patchMock]); + $this->optionalPool->expects($this->once()) + ->method('getReplacedBy') + ->withConsecutive([$patch1->getId()]) + ->willReturn([]); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1]); + + $outputMock->expects($this->once()) + ->method('writeln') + ->withConsecutive([$this->stringContains($expectedMessage)]); + + $this->renderer->expects($this->once()) + ->method('printQuestion') + ->willReturn(true); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests a case when user rejected to apply deprecated patches. + */ + public function testProcessDeprecationException() + { + $patch1 = $this->createPatch('MC-11111', true); + $patchFilter = [$patch1->getId()]; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $patchMock = $this->getMockForAbstractClass(PatchInterface::class); + + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willReturn([$patchMock]); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1]); + + $this->renderer->expects($this->once()) + ->method('printQuestion') + ->willReturn(false); + + $this->expectException(RuntimeException::class); + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests successful processing patch list with applied patches that require replacement. + */ + public function testProcessReplacementSuccessful() + { + $requireReplacement = ['MC-22222', 'MC-33333']; + $patch1 = $this->createPatch('MC-11111', false); + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-22222', true], + ['MC-33333', true] + ]); + $patchFilter = [$patch1->getId()]; + $expectedMessage = sprintf( + '%s should be reverted and replaced with %s', + implode(' ', $requireReplacement), + $patch1->getId() + ); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $patchMock = $this->getMockForAbstractClass(PatchInterface::class); + + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willReturn([$patchMock]); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1]); + + $this->optionalPool->expects($this->once()) + ->method('getReplacedBy') + ->withConsecutive([$patch1->getId()]) + ->willReturn($requireReplacement); + + $outputMock->expects($this->once()) + ->method('writeln') + ->withConsecutive([$this->stringContains($expectedMessage)]); + + $this->renderer->expects($this->once()) + ->method('printQuestion') + ->willReturn(true); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests a case when user rejected to revert deprecated patches before applying a new one. + */ + public function testProcessReplacementException() + { + $requireReplacement = ['MC-22222', 'MC-33333']; + $patch1 = $this->createPatch('MC-11111', false); + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-22222', true], + ['MC-33333', true] + ]); + $patchFilter = [$patch1->getId()]; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $patchMock = $this->getMockForAbstractClass(PatchInterface::class); + + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willReturn([$patchMock]); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1]); + + $this->optionalPool->expects($this->once()) + ->method('getReplacedBy') + ->withConsecutive([$patch1->getId()]) + ->willReturn($requireReplacement); + + $this->renderer->expects($this->once()) + ->method('printQuestion') + ->willReturn(false); + + $this->expectException(RuntimeException::class); + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests with empty patch filter. + * + * Don't need to check patches for deprecation and replacement. + */ + public function testWithEmptyPatchFilter() + { + $patchFilter = []; + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->aggregator->expects($this->never()) + ->method('aggregate'); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Creates patch mock. + * + * @param string $id + * @param bool $isDeprecated + * @param string $replacedWith + * @return AggregatedPatchInterface|MockObject + */ + private function createPatch(string $id, bool $isDeprecated = false, string $replacedWith = '') + { + $patch = $this->getMockForAbstractClass(AggregatedPatchInterface::class); + $patch->method('getId')->willReturn($id); + $patch->method('isDeprecated')->willReturn($isDeprecated); + $patch->method('getReplacedWith')->willReturn($replacedWith); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/Action/RevertActionTest.php b/src/Test/Unit/Command/Process/Action/RevertActionTest.php new file mode 100644 index 00000000..4c0e3c56 --- /dev/null +++ b/src/Test/Unit/Command/Process/Action/RevertActionTest.php @@ -0,0 +1,259 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process\Action; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\RevertAction; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Pool\PatchNotFoundException; +use Magento\CloudPatches\Patch\RevertValidator; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class RevertActionTest extends TestCase +{ + /** + * @var RevertAction + */ + private $action; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @var StatusPool|MockObject + */ + private $statusPool; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @var RevertValidator|MockObject + */ + private $revertValidator; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->applier = $this->createMock(Applier::class); + $this->revertValidator = $this->createMock(RevertValidator::class); + $this->statusPool = $this->createMock(StatusPool::class); + $this->optionalPool = $this->createMock(OptionalPool::class); + $this->renderer = $this->createMock(Renderer::class); + $this->revertAction = $this->createMock(RevertAction::class); + /** @var \Psr\Log\LoggerInterface|MockObject $logger */ + $logger = $this->getMockForAbstractClass('\Psr\Log\LoggerInterface'); + + $this->action = new RevertAction( + $this->applier, + $this->revertValidator, + $this->optionalPool, + $this->statusPool, + $this->renderer, + $logger + ); + } + + /** + * Tests successful patches reverting. + * + * Case: reverting 2 applied patches. Verifies that patches are reverted in reverse order. + */ + public function testExecuteSuccessful() + { + $patchFilter = ['MC-11111', 'MC-22222']; + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $patch2 = $this->createPatch('/path/patch2.patch', 'MC-22222'); + $this->statusPool->method('isNotApplied') + ->willReturnMap([ + ['MC-11111', false], + ['MC-22222', false] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter, false]) + ->willReturn([$patch1, $patch2]); + + $this->applier->method('revert') + ->willReturnMap([ + [$patch1->getPath(), $patch1->getId(), 'Patch ' . $patch1->getId() .' has been reverted'], + [$patch2->getPath(), $patch2->getId(), 'Patch ' . $patch2->getId() .' has been reverted'] + ]); + + $this->renderer->expects($this->exactly(2)) + ->method('printPatchInfo') + ->withConsecutive( + [$outputMock, $patch2, 'Patch ' . $patch2->getId() .' has been reverted'], + [$outputMock, $patch1, 'Patch ' . $patch1->getId() .' has been reverted'] + ); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests successful patches reverting. + * + * Case: reverting patch that was not applied previously. + */ + public function testRevertNotAppliedPatch() + { + $patchFilter = ['MC-11111']; + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $this->statusPool->method('isNotApplied') + ->willReturnMap([ + ['MC-11111', true] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willReturn([$patch1]); + + $this->applier->expects($this->never()) + ->method('revert'); + $this->renderer->expects($this->never()) + ->method('printPatchInfo'); + + $outputMock->expects($this->once()) + ->method('writeln') + ->withConsecutive( + [ + $this->stringContains( + 'Patch ' . $patch1->getId() . ' (' . $patch1->getFilename() . ') is not applied' + ) + ] + ); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests patch revert with exception. + * + * @throws RuntimeException + */ + public function testRevertWithException() + { + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $patchFilter = [$patch1->getId()]; + $errorMessage = sprintf('Reverting patch %s (%s) failed.', $patch1->getId(), $patch1->getPath()); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->method('getList') + ->willReturn([$patch1]); + + $this->applier->method('revert') + ->willThrowException(new ApplierException('Error')); + + $this->renderer->expects($this->once()) + ->method('formatErrorOutput') + ->with('Error'); + $outputMock->expects($this->once()) + ->method('writeln') + ->withConsecutive( + [$this->stringContains($errorMessage)] + ); + + $this->expectException(RuntimeException::class); + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests exception when patch from filter is not found. + */ + public function testPatchNotFoundException() + { + $patchFilter = ['unknown id']; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willThrowException(new PatchNotFoundException('')); + + $this->expectException(RuntimeException::class); + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests exception when revert patch validation fails. + */ + public function testValidationFailedException() + { + $patchFilter = ['MC-11111']; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->revertValidator->expects($this->once()) + ->method('validate') + ->withConsecutive([$patchFilter]) + ->willThrowException(new RuntimeException('Error')); + $this->optionalPool->expects($this->never()) + ->method('getList'); + + $this->expectException(RuntimeException::class); + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Creates patch mock. + * + * @param string $path + * @param string $id + * + * @return PatchInterface|MockObject + */ + private function createPatch(string $path, string $id) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getPath')->willReturn($path); + $patch->method('getId')->willReturn($id); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php b/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php new file mode 100644 index 00000000..3d7b2354 --- /dev/null +++ b/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process\Action; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\RevertAction; +use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Pool\PatchNotFoundException; +use Magento\CloudPatches\Patch\RevertValidator; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ReviewAppliedActionTest extends TestCase +{ + /** + * @var ReviewAppliedAction + */ + private $action; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var StatusPool|MockObject + */ + private $statusPool; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->statusPool = $this->createMock(StatusPool::class); + $this->optionalPool = $this->createMock(OptionalPool::class); + + $this->action = new ReviewAppliedAction( + $this->optionalPool, + $this->statusPool, + $this->logger + ); + } + + /** + * Tests that warning message is shown when number of patches (filter + already applied) exceeds limit. + */ + public function testAppliedPatchesExceedsLimit() + { + $filterSize = round(ReviewAppliedAction::UPGRADE_THRESHOLD / 2); + $patchFilter = []; + for ($i = 1; $i <= $filterSize; $i++) { + $patchFilter[] = 'MC-' . $i; + } + + $appliedPatches = []; + for ($i = 1; $i <= (ReviewAppliedAction::UPGRADE_THRESHOLD - $filterSize); $i++) { + $appliedPatches[] = $this->createPatch('MDVA-' . $i); + } + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->statusPool->method('isApplied') + ->willReturn(true); + + $this->optionalPool->expects($this->once()) + ->method('getOptionalAll') + ->willReturn($appliedPatches); + + $outputMock->expects($this->once()) + ->method('writeln') + ->withConsecutive([$this->stringContains('error')]); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + + /** + * Tests that warning message is not shown when number of applied patches doesn't exceed the limit. + */ + public function testAppliedPatchesNotExceedLimit() + { + $appliedPatches = []; + for ($i = 1; $i < ReviewAppliedAction::UPGRADE_THRESHOLD; $i++) { + $appliedPatches[] = $this->createPatch('MDVA-' . $i); + } + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->statusPool->method('isApplied') + ->willReturn(true); + + $this->optionalPool->expects($this->once()) + ->method('getOptionalAll') + ->willReturn($appliedPatches); + + $outputMock->expects($this->never()) + ->method('writeln'); + + $this->action->execute($inputMock, $outputMock, []); + } + + /** + * Creates patch mock. + * + * @param string $id + * + * @return PatchInterface|MockObject + */ + private function createPatch(string $id) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getId')->willReturn($id); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/ApplyLocalTest.php b/src/Test/Unit/Command/Process/ApplyLocalTest.php new file mode 100644 index 00000000..7396710e --- /dev/null +++ b/src/Test/Unit/Command/Process/ApplyLocalTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\ApplyLocal; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\LocalPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ApplyLocalTest extends TestCase +{ + /** + * @var ApplyLocal + */ + private $manager; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var LocalPool|MockObject + */ + private $localPool; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->applier = $this->createMock(Applier::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->localPool = $this->createMock(LocalPool::class); + $this->renderer = $this->createMock(Renderer::class); + + $this->manager = new ApplyLocal( + $this->applier, + $this->localPool, + $this->renderer, + $this->logger + ); + } + + /** + * Tests case when there are no local patches in m2-hotfix directory. + * + * @throws RuntimeException + */ + public function testExecuteLocalPatchesNotFound() + { + $expectedMessage = '<info>Hot-fixes were not found. Skipping</info>'; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->localPool->method('getList') + ->willReturn([]); + $outputMock->expects($this->once()) + ->method('writeln') + ->with($expectedMessage); + + $this->manager->run($inputMock, $outputMock); + } + + /** + * Tests successful local patches applying. + * + * @throws RuntimeException + */ + public function testApplySuccessful() + { + $patch1 = $this->createPatch('/path/patch1.patch', '../m2-hotfixes/patch1.patch'); + $patch2 = $this->createPatch('/path/patch2.patch', '../m2-hotfixes/patch2.patch'); + $patch3 = $this->createPatch('/path/patch3.patch', '../m2-hotfixes/patch3.patch'); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->localPool->method('getList') + ->willReturn([$patch1, $patch2, $patch3]); + + $this->applier->method('apply') + ->willReturnMap([ + [$patch1->getPath(), $patch1->getTitle(), 'Patch ' . $patch1->getTitle() .' has been applied'], + [$patch2->getPath(), $patch2->getTitle(), 'Patch ' . $patch2->getTitle() .' has been applied'], + [$patch3->getPath(), $patch3->getTitle(), 'Patch ' . $patch3->getTitle() .' has been applied'], + ]); + + $outputMock->expects($this->exactly(4)) + ->method('writeln') + ->withConsecutive( + [$this->anything()], + [$this->stringContains('Patch ' . $patch1->getTitle() .' has been applied')], + [$this->stringContains('Patch ' . $patch2->getTitle() .' has been applied')], + [$this->stringContains('Patch ' . $patch3->getTitle() .' has been applied')] + ); + + $this->manager->run($inputMock, $outputMock); + } + + /** + * Tests local patches applying with exception. + * + * @throws RuntimeException + */ + public function testApplyWithException() + { + $patch = $this->createPatch('/path/patch.patch', '../m2-hotfixes/patch.patch'); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->localPool->method('getList') + ->willReturn([$patch]); + + $this->applier->method('apply') + ->withConsecutive([$patch->getPath(), $patch->getTitle()]) + ->willThrowException(new ApplierException('Error')); + $this->renderer->expects($this->once()) + ->method('formatErrorOutput') + ->with('Error'); + + $this->expectException(RuntimeException::class); + $this->manager->run($inputMock, $outputMock); + } + + /** + * Creates patch mock. + * + * @param string $path + * @param string $title + * + * @return PatchInterface|MockObject + */ + private function createPatch(string $path, string $title) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getPath')->willReturn($path); + $patch->method('getTitle')->willReturn($title); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/ApplyOptionalTest.php new file mode 100644 index 00000000..38ebba36 --- /dev/null +++ b/src/Test/Unit/Command/Process/ApplyOptionalTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Apply; +use Magento\CloudPatches\Command\Process\Action\ActionPool; +use Magento\CloudPatches\Command\Process\ApplyOptional; +use Magento\CloudPatches\Patch\FilterFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ApplyOptionalTest extends TestCase +{ + /** + * @var ApplyOptional + */ + private $manager; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var ActionPool|MockObject + */ + private $actionPool; + + /** + * @var FilterFactory|MockObject + */ + private $filterFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->filterFactory = $this->createMock(FilterFactory::class); + $this->actionPool = $this->createMock(ActionPool::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + + $this->manager = new ApplyOptional( + $this->filterFactory, + $this->actionPool, + $this->logger + ); + } + + /** + * Tests successful optional patches applying. + * + * @throws RuntimeException + */ + public function testApplyWithPatchArgumentProvided() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $cliPatchArgument = ['MC-1111', 'MC-22222']; + $inputMock->expects($this->once()) + ->method('getArgument') + ->with(Apply::ARG_QUALITY_PATCHES) + ->willReturn($cliPatchArgument); + $this->filterFactory->method('createApplyFilter') + ->with($cliPatchArgument) + ->willReturn($cliPatchArgument); + + $this->actionPool->expects($this->once()) + ->method('execute') + ->withConsecutive([$inputMock, $outputMock, $cliPatchArgument]); + + $this->manager->run($inputMock, $outputMock); + } + + /** + * Tests optional patches applying when CLI patch argument is empty. + * + * @throws RuntimeException + */ + public function testApplyWithEmptyPatchArgument() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $cliPatchArgument = []; + $inputMock->expects($this->once()) + ->method('getArgument') + ->with(Apply::ARG_QUALITY_PATCHES) + ->willReturn($cliPatchArgument); + $this->filterFactory->method('createApplyFilter') + ->with($cliPatchArgument) + ->willReturn(null); + + $this->actionPool->expects($this->never()) + ->method('execute'); + + $this->manager->run($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Command/Process/ApplyRequiredTest.php b/src/Test/Unit/Command/Process/ApplyRequiredTest.php new file mode 100644 index 00000000..04562de8 --- /dev/null +++ b/src/Test/Unit/Command/Process/ApplyRequiredTest.php @@ -0,0 +1,150 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\ApplyRequired; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\RequiredPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ApplyRequiredTest extends TestCase +{ + /** + * @var ApplyRequired + */ + private $manager; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var RequiredPool|MockObject + */ + private $requiredPool; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->applier = $this->createMock(Applier::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->requiredPool = $this->createMock(RequiredPool::class); + $this->renderer = $this->createMock(Renderer::class); + + $this->manager = new ApplyRequired( + $this->applier, + $this->requiredPool, + $this->renderer, + $this->logger + ); + } + + /** + * Tests successful required patches applying. + * + * @throws RuntimeException + */ + public function testApplySuccessful() + { + $patch1 = $this->createPatch('/path/patch1.patch', 'MC-11111'); + $patch2 = $this->createPatch('/path/patch2.patch', 'MC-22222'); + $patch3 = $this->createPatch('/path/patch3.patch', 'MC-33333'); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->requiredPool->method('getList') + ->willReturn([$patch1, $patch2, $patch3]); + + $this->applier->method('apply') + ->willReturnMap([ + [$patch1->getPath(), $patch1->getId(), 'Patch ' . $patch1->getId() .' has been applied'], + [$patch2->getPath(), $patch2->getId(), 'Patch ' . $patch2->getId() .' has been applied'], + [$patch3->getPath(), $patch3->getId(), 'Patch ' . $patch3->getId() .' has been applied'], + ]); + + $this->renderer->expects($this->exactly(3)) + ->method('printPatchInfo') + ->withConsecutive( + [$outputMock, $patch1, 'Patch ' . $patch1->getId() .' has been applied'], + [$outputMock, $patch2, 'Patch ' . $patch2->getId() .' has been applied'], + [$outputMock, $patch3, 'Patch ' . $patch3->getId() .' has been applied'] + ); + + $this->manager->run($inputMock, $outputMock); + } + + /** + * Tests required patches applying with exception. + * + * @throws RuntimeException + */ + public function testApplyWithException() + { + $patch = $this->createPatch('/path/patch.patch', 'MC-11111'); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->requiredPool->method('getList') + ->willReturn([$patch]); + + $this->applier->method('apply') + ->withConsecutive([$patch->getPath(), $patch->getId()]) + ->willThrowException(new ApplierException('Error')); + $this->renderer->expects($this->once()) + ->method('formatErrorOutput') + ->with('Error'); + + $this->expectException(RuntimeException::class); + $this->manager->run($inputMock, $outputMock); + } + + /** + * Creates patch mock. + * + * @param string $path + * @param string $id + * + * @return PatchInterface|MockObject + */ + private function createPatch(string $path, string $id) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getPath')->willReturn($path); + $patch->method('getId')->willReturn($id); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/RendererTest.php b/src/Test/Unit/Command/Process/RendererTest.php new file mode 100644 index 00000000..41c07539 --- /dev/null +++ b/src/Test/Unit/Command/Process/RendererTest.php @@ -0,0 +1,198 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process; + +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Console\ConfirmationQuestionFactory; +use Magento\CloudPatches\Console\TableFactory; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class RendererTest extends TestCase +{ + const PATCH_ID = 'MC-11111'; + + const PATCH_TITLE = 'Patch title'; + + const PATCH_FILENAME = 'MC-11111__patch_title__2.2.5.patch'; + + /** + * Test components. + * + * @var string[] + */ + private $affectedComponents = ['magento/framework', 'magento/module-elasticsearch']; + + /** + * @var Renderer + */ + private $renderer; + + /** + * @inheritdoc + */ + protected function setUp() + { + /** @var StatusPool|MockObject $statusPool */ + $statusPool = $this->createMock(StatusPool::class); + /** @var TableFactory|MockObject $tableFactory */ + $tableFactory = $this->createMock(TableFactory::class); + /** @var QuestionHelper|MockObject $questionHelper */ + $questionHelper = $this->createMock(QuestionHelper::class); + /** @var ConfirmationQuestionFactory|MockObject $confirmationQuestionFactory */ + $confirmationQuestionFactory = $this->createMock(ConfirmationQuestionFactory::class); + + $this->renderer = new Renderer( + $tableFactory, + $statusPool, + $questionHelper, + $confirmationQuestionFactory + ); + } + + /** + * Tests patch info output. + * + * @param PatchInterface $patch + * @param string $prependedMessage + * @param array $expectedArray + * @dataProvider printPatchInfoDataProvider + */ + public function testPrintPatchInfo(PatchInterface $patch, string $prependedMessage, array $expectedArray) + { + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock->expects($this->atLeastOnce()) + ->method('writeln') + ->withConsecutive([$expectedArray]); + + $this->renderer->printPatchInfo($outputMock, $patch, $prependedMessage); + } + + /** + * @return array[] + */ + public function printPatchInfoDataProvider(): array + { + return [ + [ + 'patch' => $this->createPatch(false), + 'prependedMessage' => '', + 'expectedArray' => [ + '<comment>Id:</comment> ' . self::PATCH_ID, + '<comment>Title:</comment> ' . self::PATCH_TITLE, + '<comment>File:</comment> ' . self::PATCH_FILENAME, + '<comment>Affected components:</comment> ' . implode(' ', $this->affectedComponents) + ] + ], + [ + 'patch' => $this->createPatch(true), + 'prependedMessage' => 'Prepended message', + 'expectedArray' => [ + '<info>Prepended message</info>', + '<comment>Id:</comment> ' . self::PATCH_ID, + '<comment>Title:</comment> ' . self::PATCH_TITLE, + '<comment>File:</comment> ' . self::PATCH_FILENAME, + '<comment>Affected components:</comment> ' . implode(' ', $this->affectedComponents), + '<error>Patch is deprecated!</error>' + ] + ], + [ + 'patch' => $this->createPatch(true, 'MC-22222', ['MC-33333', 'MC-44444']), + 'prependedMessage' => 'Prepended message', + 'expectedArray' => [ + '<info>Prepended message</info>', + '<comment>Id:</comment> ' . self::PATCH_ID, + '<comment>Title:</comment> ' . self::PATCH_TITLE, + '<comment>File:</comment> ' . self::PATCH_FILENAME, + '<comment>Affected components:</comment> ' . implode(' ', $this->affectedComponents), + '<comment>Require:</comment> MC-33333 MC-44444', + '<error>Patch is deprecated!</error> Please, replace it with MC-22222' + ] + ] + ]; + } + + /** + * Tests error output formatting. + * + * @param string $errorOutput + * @param string $expectedOutput + * @dataProvider formatErrorOutputDataProvider + */ + public function testFormatErrorOutput(string $errorOutput, string $expectedOutput) + { + $this->assertEquals($expectedOutput, $this->renderer->formatErrorOutput($errorOutput)); + } + + /** + * @return array + */ + public function formatErrorOutputDataProvider(): array + { + return [ + [ + 'error' => 'The command "\'git\' \'apply\' \'/path/to/patch/MC-1111_test_patch_1.1.1_ce.patch\'" failed. + +Exit Code: 1(General error) + +Working directory: /path/to/patch + +Output: +================ + + +Error Output: +================ +error: patch failed: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php:23 +error: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php: patch does not apply', + + 'expectedOutput' => ' +Error Output: +================ +error: patch failed: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php:23 +error: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php: patch does not apply' + ], + [ + 'error' => 'Some other output', + 'expectedOutput' => 'Some other output' + ], + ]; + } + + /** + * Creates patch mock. + * + * @param bool $isDeprecated + * @param string $replacedWith + * @param array $require + * + * @return PatchInterface|MockObject + */ + private function createPatch(bool $isDeprecated, string $replacedWith = '', array $require = []) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + + $patch->method('getId')->willReturn(self::PATCH_ID); + $patch->method('getTitle')->willReturn(self::PATCH_TITLE); + $patch->method('getFilename')->willReturn(self::PATCH_FILENAME); + $patch->method('getAffectedComponents')->willReturn($this->affectedComponents); + $patch->method('isDeprecated')->willReturn($isDeprecated); + $patch->method('getReplacedWith')->willReturn($replacedWith); + $patch->method('getRequire')->willReturn($require); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/RevertTest.php b/src/Test/Unit/Command/Process/RevertTest.php new file mode 100644 index 00000000..c0a20675 --- /dev/null +++ b/src/Test/Unit/Command/Process/RevertTest.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\RevertAction; +use Magento\CloudPatches\Command\Process\Revert; +use Magento\CloudPatches\Command\Revert as RevertCommand; +use Magento\CloudPatches\Patch\FilterFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class RevertTest extends TestCase +{ + /** + * @var Revert + */ + private $manager; + + /** + * @var RevertAction|MockObject + */ + private $revertAction; + + /** + * @var FilterFactory|MockObject + */ + private $filterFactory; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->filterFactory = $this->createMock(FilterFactory::class); + $this->revertAction = $this->createMock(RevertAction::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + + $this->manager = new Revert( + $this->filterFactory, + $this->revertAction, + $this->logger + ); + } + + /** + * Tests optional patches reverting when CLI patch argument provided. + * + * @throws RuntimeException + */ + public function testRevertWithPatchArgumentProvided() + { + $cliPatchArgument = ['MC-1111', 'MC-22222']; + $cliOptAll = false; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $inputMock->expects($this->once()) + ->method('getArgument') + ->with(RevertCommand::ARG_QUALITY_PATCHES) + ->willReturn($cliPatchArgument); + $inputMock->expects($this->once()) + ->method('getOption') + ->with(RevertCommand::OPT_ALL) + ->willReturn($cliOptAll); + $this->filterFactory->method('createRevertFilter') + ->withConsecutive([$cliOptAll, $cliPatchArgument]) + ->willReturn($cliPatchArgument); + + $this->revertAction->expects($this->once()) + ->method('execute') + ->withConsecutive([$inputMock, $outputMock, $cliPatchArgument]); + + $this->manager->run($inputMock, $outputMock); + } + + /** + * Tests optional patches reverting when CLI patch argument is empty. + * + * @throws RuntimeException + */ + public function testRevertWithEmptyPatchArgument() + { + $cliPatchArgument = []; + $cliOptAll = false; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $inputMock->expects($this->once()) + ->method('getArgument') + ->with(RevertCommand::ARG_QUALITY_PATCHES) + ->willReturn($cliPatchArgument); + $inputMock->expects($this->once()) + ->method('getOption') + ->with(RevertCommand::OPT_ALL) + ->willReturn($cliOptAll); + $this->filterFactory->method('createRevertFilter') + ->withConsecutive([$cliOptAll, $cliPatchArgument]) + ->willReturn(null); + + $this->revertAction->expects($this->never()) + ->method('execute'); + + $this->manager->run($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Command/Process/ShowStatusTest.php b/src/Test/Unit/Command/Process/ShowStatusTest.php new file mode 100644 index 00000000..a2dcfd86 --- /dev/null +++ b/src/Test/Unit/Command/Process/ShowStatusTest.php @@ -0,0 +1,155 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process; + +use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Command\Process\ShowStatus; +use Magento\CloudPatches\Patch\Aggregator; +use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\LocalPool; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ShowStatusTest extends TestCase +{ + /** + * @var ShowStatus + */ + private $manager; + + /** + * @var ReviewAppliedAction|MockObject + */ + private $reviewAppliedAction; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @var Aggregator|MockObject + */ + private $aggregator; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @var StatusPool|MockObject + */ + private $statusPool; + + /** + * @var LocalPool|MockObject + */ + private $localPool; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->aggregator = $this->createMock(Aggregator::class); + $this->optionalPool = $this->createMock(OptionalPool::class); + $this->localPool = $this->createMock(LocalPool::class); + $this->statusPool = $this->createMock(StatusPool::class); + $this->reviewAppliedAction = $this->createMock(ReviewAppliedAction::class); + $this->renderer = $this->createMock(Renderer::class); + + $this->manager = new ShowStatus( + $this->aggregator, + $this->optionalPool, + $this->localPool, + $this->statusPool, + $this->reviewAppliedAction, + $this->renderer + ); + } + + /** + * Tests show status. + * + * Patch 1 - deprecated, applied - show warning message, show patch in the table; + * Patch 2 - not deprecated, not applied - no warning message, show patch in the table; + * Patch 3 - deprecated, not applied - no warning message, don't show patch in the table; + */ + public function testShowStatus() + { + $patch1 = $this->createPatch('patch1', true); + $patch2 = $this->createPatch('patch2', false); + $patch3 = $this->createPatch('patch3', true); + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['patch1', true], + ['patch2', false], + ['patch3', false], + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $patchMock = $this->getMockForAbstractClass(PatchInterface::class); + + $this->reviewAppliedAction->expects($this->once()) + ->method('execute') + ->withConsecutive([$inputMock, $outputMock, []]); + $this->optionalPool->method('getList') + ->willReturn([$patchMock]); + $this->localPool->method('getList') + ->willReturn([$patchMock]); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1, $patch2, $patch3]); + + // Show warning message about patch deprecation + $outputMock->expects($this->exactly(2)) + ->method('writeln') + ->withConsecutive( + [$this->anything()], + [$this->stringContains('Deprecated patch ' . $patch1->getId() . ' is currently applied')] + ); + + // Show patches in the table + $this->renderer->expects($this->once()) + ->method('printTable') + ->withConsecutive([$outputMock, [$patch1, $patch2]]); + + $this->manager->run($inputMock, $outputMock); + } + + /** + * Creates patch mock. + * + * @param string $id + * @param bool $isDeprecated + * + * @return AggregatedPatchInterface|MockObject + */ + private function createPatch(string $id, bool $isDeprecated) + { + $patch = $this->getMockForAbstractClass(AggregatedPatchInterface::class); + $patch->method('getId')->willReturn($id); + $patch->method('isDeprecated')->willReturn($isDeprecated); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/RevertTest.php b/src/Test/Unit/Command/RevertTest.php new file mode 100644 index 00000000..5f8ea6a1 --- /dev/null +++ b/src/Test/Unit/Command/RevertTest.php @@ -0,0 +1,152 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\AbstractCommand; +use Magento\CloudPatches\Command\Process\Revert as RevertProcess; +use Magento\CloudPatches\Command\Revert; +use Magento\CloudPatches\Composer\MagentoVersion; +use Magento\CloudPatches\Patch\Environment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritDoc + */ +class RevertTest extends TestCase +{ + /** + * @var Revert + */ + private $command; + + /** + * @var RevertProcess|MockObject + */ + private $revert; + + /** + * @var Environment|MockObject + */ + private $environment; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->revert = $this->createMock(RevertProcess::class); + $this->environment = $this->createMock(Environment::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + + /** @var MagentoVersion|MockObject $magentoVersion */ + $magentoVersion = $this->createMock(MagentoVersion::class); + + $this->command = new Revert( + $this->revert, + $this->environment, + $this->logger, + $magentoVersion + ); + } + + /** + * Tests that command is not available on Cloud environment. + */ + public function testCloudEnvironmentNotAvailable() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->environment->method('isCloud') + ->willReturn(true); + + $this->revert->expects($this->never()) + ->method('run'); + + $this->assertEquals( + AbstractCommand::RETURN_FAILURE, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests successful command execution on OnPrem environment. + */ + public function testOnPremEnvironmentSuccess() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->environment->method('isCloud') + ->willReturn(false); + + $this->revert->expects($this->once()) + ->method('run'); + + $this->assertEquals( + AbstractCommand::RETURN_SUCCESS, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when runtime error happens during command execution. + */ + public function testRuntimeError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->revert->expects($this->once()) + ->method('run') + ->willThrowException(new RuntimeException('Error!')); + $this->logger->expects($this->once()) + ->method('error'); + + $this->assertEquals( + AbstractCommand::RETURN_FAILURE, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when critical error happens during command execution. + */ + public function testCriticalError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->revert->expects($this->once()) + ->method('run') + ->willThrowException(new \InvalidArgumentException('Critical error!')); + $this->logger->expects($this->once()) + ->method('critical'); + + $this->expectException(\InvalidArgumentException::class); + $this->command->execute($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Command/StatusTest.php b/src/Test/Unit/Command/StatusTest.php new file mode 100644 index 00000000..31094acd --- /dev/null +++ b/src/Test/Unit/Command/StatusTest.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\AbstractCommand; +use Magento\CloudPatches\Command\Apply; +use Magento\CloudPatches\Command\Process\ShowStatus; +use Magento\CloudPatches\Command\Status; +use Magento\CloudPatches\Composer\MagentoVersion; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritDoc + */ +class StatusTest extends TestCase +{ + /** + * @var Apply + */ + private $command; + + /** + * @var ShowStatus|MockObject + */ + private $showStatus; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->showStatus = $this->createMock(ShowStatus::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + /** @var MagentoVersion|MockObject $magentoVersion */ + $magentoVersion = $this->createMock(MagentoVersion::class); + + $this->command = new Status( + $this->showStatus, + $this->logger, + $magentoVersion + ); + } + + /** + * Tests successful command execution. + */ + public function testExecuteSuccess() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->showStatus->expects($this->once()) + ->method('run'); + + $this->assertEquals( + AbstractCommand::RETURN_SUCCESS, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when runtime error happens during command execution. + */ + public function testRuntimeError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->showStatus->expects($this->once()) + ->method('run') + ->willThrowException(new RuntimeException('Error!')); + $this->logger->expects($this->once()) + ->method('error'); + + $this->assertEquals( + AbstractCommand::RETURN_FAILURE, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when critical error happens during command execution. + */ + public function testCriticalError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->showStatus->expects($this->once()) + ->method('run') + ->willThrowException(new \InvalidArgumentException('Critical error!')); + $this->logger->expects($this->once()) + ->method('critical'); + + $this->expectException(\InvalidArgumentException::class); + $this->command->execute($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Composer/MagentoVersionTest.php b/src/Test/Unit/Composer/MagentoVersionTest.php new file mode 100644 index 00000000..181fd7fa --- /dev/null +++ b/src/Test/Unit/Composer/MagentoVersionTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Composer; + +use Composer\Composer; +use Composer\Package\PackageInterface; +use Composer\Repository\RepositoryManager; +use Composer\Repository\WritableRepositoryInterface; +use Magento\CloudPatches\Composer\MagentoVersion; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class MagentoVersionTest extends TestCase +{ + const VERSION = '2.3.5'; + + /** + * @var WritableRepositoryInterface|MockObject + */ + private $repository; + + /** + * @var MagentoVersion + */ + private $magentoVersion; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->repository = $this->getMockForAbstractClass(WritableRepositoryInterface::class); + $repositoryManager = $this->createMock(RepositoryManager::class); + $repositoryManager->method('getLocalRepository') + ->willReturn($this->repository); + + /** @var Composer $composer */ + $composer = $this->createMock(Composer::class); + $composer->method('getRepositoryManager') + ->willReturn($repositoryManager); + + $this->magentoVersion = new MagentoVersion($composer); + } + + /** + * Tests retrieving Magento version and edition. + * + * @param bool $ce + * @param bool $ee + * @param bool $b2b + * @param string $expectedResult + * + * @dataProvider getDataProvider + */ + public function testGet(bool $ce, bool $ee, bool $b2b, string $expectedResult) + { + $package = $this->getMockForAbstractClass(PackageInterface::class); + $package->method('getVersion') + ->willReturn(self::VERSION); + $this->repository->method('findPackage') + ->willReturnMap([ + ['magento/magento2-base', '*', $ce ? $package : null], + ['magento/magento2-ee-base', '*', $ee ? $package : null], + ['magento/magento2-b2b-base', '*', $b2b ? $package : null], + ]); + + $this->assertEquals($expectedResult, $this->magentoVersion->get()); + } + + /** + * @return array + */ + public function getDataProvider(): array + { + return [ + ['CE' => false, 'EE' => false, 'B2B' => false, 'Magento 2 is not installed'], + ['CE' => true, 'EE' => true, 'B2B' => false, 'Magento 2 Enterprise Edition, version ' . self::VERSION], + ['CE' => true, 'EE' => false, 'B2B' => true, 'Magento 2 B2B Edition, version ' . self::VERSION], + ['CE' => true, 'EE' => false, 'B2B' => false, 'Magento 2 Community Edition, version ' . self::VERSION], + ]; + } +} diff --git a/src/Test/Unit/Filesystem/FileListTest.php b/src/Test/Unit/Filesystem/FileListTest.php index 5d3f8b93..194ab014 100644 --- a/src/Test/Unit/Filesystem/FileListTest.php +++ b/src/Test/Unit/Filesystem/FileListTest.php @@ -36,6 +36,9 @@ protected function setUp() $this->directoryListMock->method('getRoot') ->willReturn('root'); + $this->directoryListMock->method('getMagentoRoot') + ->willReturn('magento_root'); + $this->fileList = new FileList( $this->directoryListMock ); @@ -48,4 +51,20 @@ public function testGetPatches() $this->fileList->getPatches() ); } + + public function testGetPatchLog() + { + $this->assertSame( + 'magento_root/var/log/patch.log', + $this->fileList->getPatchLog() + ); + } + + public function testGetInitPatchLog() + { + $this->assertSame( + 'magento_root/init/var/log/patch.log', + $this->fileList->getInitPatchLog() + ); + } } diff --git a/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php b/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php new file mode 100644 index 00000000..032f9e61 --- /dev/null +++ b/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\Patch\AggregatedPatchFactory; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class AggregatedPatchFactoryTest extends TestCase +{ + /** + * @var AggregatedPatchFactory + */ + private $aggregatedPatchFactory; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->aggregatedPatchFactory = new AggregatedPatchFactory(); + } + + /** + * Tests creating aggregated patch. + * + * @param PatchInterface[] $patches + * @param array $expectedResult + * @dataProvider createDataProvider + */ + public function testCreate(array $patches, array $expectedResult) + { + $aggregatedPatch = $this->aggregatedPatchFactory->create($patches); + + $this->assertEquals($aggregatedPatch->getId(), $expectedResult['id']); + $this->assertEquals($aggregatedPatch->getTitle(), $expectedResult['title']); + $this->assertEquals($aggregatedPatch->getType(), $expectedResult['type']); + $this->assertEquals($aggregatedPatch->getAffectedComponents(), $expectedResult['affected_components']); + $this->assertEquals($aggregatedPatch->getRequire(), $expectedResult['require']); + $this->assertEquals($aggregatedPatch->getReplacedWith(), $expectedResult['replaced_with']); + $this->assertEquals($aggregatedPatch->isDeprecated(), $expectedResult['is_deprecated']); + } + + /** + * @return array + */ + public function createDataProvider(): array + { + return [ + [ + 'patches' => [ + $this->createPatch( + 'MC-1', + 'Title patch MC-1 CE', + 'Optional', + ['magento-module1', 'magento-module2'], + ['MC-2'], + 'MC-3', + true + ), + $this->createPatch( + 'MC-1', + 'Title patch MC-1 EE', + 'Optional', + ['magento-module3'], + ['MC-3'], + 'MC-4', + false + ) + ], + 'expectedResult' => [ + 'id' => 'MC-1', + 'title' => 'Title patch MC-1 EE', + 'type' => 'Optional', + 'affected_components' => ['magento-module1', 'magento-module2', 'magento-module3'], + 'require' => ['MC-2', 'MC-3'], + 'replaced_with' => 'MC-4', + 'is_deprecated' => true + ] + ], + ]; + } + + /** + * Creates patch mock. + * + * @param string $id + * @param string $title + * @param string $type + * @param array $affectedComponents + * @param array $require + * @param string $replacedWith + * @param bool $isDeprecated + * @return Patch|MockObject + */ + private function createPatch( + string $id, + string $title, + string $type, + array $affectedComponents, + array $require, + string $replacedWith, + bool $isDeprecated + ) { + $patch = $this->createMock(Patch::class); + $patch->method('getId')->willReturn($id); + $patch->method('getTitle')->willReturn($title); + $patch->method('getType')->willReturn($type); + $patch->method('getAffectedComponents')->willReturn($affectedComponents); + $patch->method('getRequire')->willReturn($require); + $patch->method('getReplacedWith')->willReturn($replacedWith); + $patch->method('isDeprecated')->willReturn($isDeprecated); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/AggregatorTest.php b/src/Test/Unit/Patch/AggregatorTest.php new file mode 100644 index 00000000..5427dca2 --- /dev/null +++ b/src/Test/Unit/Patch/AggregatorTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\Patch\AggregatedPatchFactory; +use Magento\CloudPatches\Patch\Aggregator; +use Magento\CloudPatches\Patch\Data\Patch; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class AggregatorTest extends TestCase +{ + /** + * @var Aggregator + */ + private $aggregator; + + /** + * @var AggregatedPatchFactory|MockObject + */ + private $aggregatedPatchFactory; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->aggregatedPatchFactory = $this->createMock(AggregatedPatchFactory::class); + $this->aggregator = new Aggregator($this->aggregatedPatchFactory); + } + + /** + * Tests patch aggregation. + */ + public function testAggregate() + { + $patch1CE = $this->createPatch('MC-1', 'Patch1 CE'); + $patch1EE = $this->createPatch('MC-1', 'Patch1 EE'); + $patch1B2B = $this->createPatch('MC-1', 'Patch1 B2B'); + $patch2CE = $this->createPatch('MC-2', 'Patch2 CE'); + $patch2EE = $this->createPatch('MC-2', 'Patch2 EE'); + $patch3 = $this->createPatch('MC-3', 'Patch3'); + + $this->aggregatedPatchFactory->expects($this->exactly(3)) + ->method('create') + ->withConsecutive( + [[$patch1CE, $patch1EE, $patch1B2B]], + [[$patch2CE, $patch2EE]], + [[$patch3]] + ); + + $this->assertTrue( + is_array( + $this->aggregator->aggregate( + [$patch1CE, $patch1EE, $patch1B2B, $patch2CE, $patch2EE, $patch3] + ) + ) + ); + } + + /** + * Creates patch mock. + * + * @param string $id + * @param string $title + * @return Patch|MockObject + */ + private function createPatch(string $id, string $title) + { + $patch = $this->createMock(Patch::class); + $patch->method('getId')->willReturn($id); + $patch->method('getTitle')->willReturn($title); + + // To make mock object unique for assertions and array operations. + $patch->id = microtime(); + $patch->method('__toString')->willReturn($patch->id); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php index cd2171b6..f2efb35a 100644 --- a/src/Test/Unit/Patch/ApplierTest.php +++ b/src/Test/Unit/Patch/ApplierTest.php @@ -7,14 +7,9 @@ namespace Magento\CloudPatches\Test\Unit\Patch; -use Composer\Composer; -use Composer\Package\PackageInterface; -use Composer\Repository\RepositoryInterface; -use Composer\Repository\RepositoryManager; -use Magento\CloudPatches\Filesystem\DirectoryList; -use Magento\CloudPatches\Filesystem\Filesystem; use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Status\StatusPool; use Magento\CloudPatches\Shell\ProcessFactory; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -31,236 +26,245 @@ class ApplierTest extends TestCase */ private $applier; - /** - * @var Composer|MockObject - */ - private $composerMock; - /** * @var ProcessFactory|MockObject */ - private $processFactoryMock; - - /** - * @var RepositoryInterface|MockObject - */ - private $localRepositoryMock; - - /** - * @var DirectoryList|MockObject - */ - private $directoryListMock; - - /** - * @var Filesystem|MockObject - */ - private $filesystemMock; + private $processFactory; /** * @inheritDoc */ protected function setUp() { - $this->composerMock = $this->createMock(Composer::class); - $this->localRepositoryMock = $this->getMockForAbstractClass(RepositoryInterface::class); - $this->directoryListMock = $this->createMock(DirectoryList::class); - $this->filesystemMock = $this->createMock(Filesystem::class); - $this->processFactoryMock = $this->createMock(ProcessFactory::class); - - $repositoryManagerMock = $this->createMock(RepositoryManager::class); - - $repositoryManagerMock->expects($this->once()) - ->method('getLocalRepository') - ->willReturn($this->localRepositoryMock); - $this->composerMock->expects($this->once()) - ->method('getRepositoryManager') - ->willReturn($repositoryManagerMock); - - $this->applier = new Applier( - $this->composerMock, - $this->processFactoryMock, - $this->directoryListMock, - $this->filesystemMock - ); + $this->processFactory = $this->createMock(ProcessFactory::class); + + $this->applier = new Applier($this->processFactory); } /** - * @param string $path - * @param string $name - * @param string $packageName - * @param string $constraint - * @param string $expectedLog - * @dataProvider applyDataProvider + * Tests apply operation, case when patch applied successfully. * * @throws ApplierException */ - public function testApply(string $path, string $name, string $packageName, string $constraint, string $expectedLog) + public function testApply() { - $this->filesystemMock->expects($this->once()) - ->method('exists') - ->with($path) - ->willReturn(true); - $this->localRepositoryMock->method('findPackage') - ->with($packageName, $constraint) - ->willReturn($this->getMockForAbstractClass(PackageInterface::class)); + $path = 'path/to/patch'; + $patchId = 'MC-11111'; + $expectedMessage = 'Patch ' . $patchId . ' has been applied'; $processMock = $this->createMock(Process::class); - $this->processFactoryMock->expects($this->once()) + $this->processFactory->expects($this->once()) ->method('create') ->with(['git', 'apply', $path]) ->willReturn($processMock); $processMock->expects($this->once()) ->method('mustRun'); - $this->assertSame( - $expectedLog, - $this->applier->apply($path, $name, $packageName, $constraint, false) - ); + $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); } /** - * @return array + * Tests apply operation, case when applying patch fails. */ - public function applyDataProvider(): array + public function testApplyFailed() { - return [ - ['path/to/patch', 'patchName', 'packageName', '1.0', 'Patch "patchName (path/to/patch) 1.0" applied'] - ]; + $path = 'path/to/patch'; + $patchId = 'MC-11111'; + + /** @var Process|MockObject $result */ + $processMock = $this->createMock(Process::class); + $processMock->method('mustRun') + ->willThrowException(new ProcessFailedException($processMock)); + + $this->processFactory->expects($this->exactly(2)) + ->method('create') + ->willReturn($processMock); + + $this->expectException(ApplierException::class); + $this->applier->apply($path, $patchId); } /** - * @param string $path - * @param string $expectedLog - * @dataProvider applyFileDataProvider + * Tests apply operation, case when patch was already applied. * * @throws ApplierException */ - public function testApplyFile(string $path, string $expectedLog) + public function testApplyPatchAlreadyApplied() + { + $path = 'path/to/patch'; + $patchId = 'MC-11111'; + $expectedMessage = 'Patch ' . $patchId . ' was already applied'; + + $this->processFactory->expects($this->exactly(2)) + ->method('create') + ->willReturnMap([ + [['git', 'apply', $path]], + [['git', 'apply', $path, '--check', '--reverse']] + ])->willReturnCallback([$this, 'shellApplyRevertCallback']); + + $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); + } + + /** + * Callback for 'apply' and 'revert' operations. + * + * @param array $command + * @return Process + * + * @throws ProcessFailedException when the command isn't a reverse + */ + public function shellApplyRevertCallback(array $command): Process { + if (in_array('--reverse', $command, true) && in_array('--check', $command, true) || + !in_array('--reverse', $command, true) && in_array('--check', $command, true) + ) { + // Command was the reverse check, it's all good. + /** @var Process|MockObject $result */ + $result = $this->createMock(Process::class); + $result->expects($this->once()) + ->method('mustRun'); + + return $result; + } + + /** @var Process|MockObject $result */ + $result = $this->createMock(Process::class); + $result->expects($this->once()) + ->method('mustRun') + ->willThrowException(new ProcessFailedException($result)); + + return $result; + } + + /** + * Tests revert operation, case when patch reverted successfully. + * + * @throws ApplierException + */ + public function testRevert() + { + $path = 'path/to/patch'; + $patchId = 'MC-11111'; + $expectedMessage = 'Patch ' . $patchId . ' has been reverted'; + $processMock = $this->createMock(Process::class); - $this->processFactoryMock->expects($this->once()) + $this->processFactory->expects($this->once()) ->method('create') - ->with(['git', 'apply', $path]) + ->with(['git', 'apply', '--reverse', $path]) ->willReturn($processMock); $processMock->expects($this->once()) ->method('mustRun'); - $this->assertSame( - $expectedLog, - $this->applier->applyFile($path, false) - ); + $this->assertSame($expectedMessage, $this->applier->revert($path, $patchId)); } /** - * @return array + * Tests revert operation, case when patch revert fails. */ - public function applyFileDataProvider(): array + public function testRevertFailed() { - return [ - ['path/to/patch2', 'Patch "path/to/patch2" applied'], - ]; + $path = 'path/to/patch'; + $patchId = 'MC-11111'; + + /** @var Process|MockObject $result */ + $processMock = $this->createMock(Process::class); + $processMock->method('mustRun') + ->willThrowException(new ProcessFailedException($processMock)); + + $this->processFactory->expects($this->exactly(2)) + ->method('create') + ->willReturn($processMock); + + $this->expectException(ApplierException::class); + $this->applier->revert($path, $patchId); } /** + * Tests revert operation, case when patch wasn't applied. + * * @throws ApplierException */ - public function testApplyPathNotExists() + public function testRevertPatchWasntApplied() { $path = 'path/to/patch'; - $name = 'patchName'; - $packageName = 'packageName'; - $constraint = '1.0'; - - $this->filesystemMock->expects($this->once()) - ->method('exists') - ->with($path) - ->willReturn(false); - $this->localRepositoryMock->expects($this->once()) - ->method('findPackage') - ->with($packageName, $constraint) - ->willReturn($this->getMockForAbstractClass(PackageInterface::class)); - $this->directoryListMock->expects($this->once()) - ->method('getPatches') - ->willReturn('root'); + $patchId = 'MC-11111'; + $expectedMessage = 'Patch ' . $patchId . ' wasn\'t applied'; + $this->processFactory->expects($this->exactly(2)) + ->method('create') + ->willReturnMap([ + [['git', 'apply', $path]], + [['git', 'apply', $path, '--check']] + ])->willReturnCallback([$this, 'shellApplyRevertCallback']); + + $this->assertSame($expectedMessage, $this->applier->revert($path, $patchId)); + } + + /** + * Tests status operation, case when patch is not applied. + */ + public function testStatusNotApplied() + { + $patchContent = 'patch content'; $processMock = $this->createMock(Process::class); - $this->processFactoryMock->expects($this->once()) + $this->processFactory->expects($this->once()) ->method('create') - ->with(['git', 'apply', 'root/path/to/patch']) + ->withConsecutive([['git', 'apply', '--check'], $patchContent]) ->willReturn($processMock); $processMock->expects($this->once()) ->method('mustRun'); - $this->applier->apply($path, $name, $packageName, $constraint, false); + $this->assertSame(StatusPool::NOT_APPLIED, $this->applier->status($patchContent)); } /** - * @throws ApplierException + * Tests status operation, case when patch status can't be defined. */ - public function testApplyPathNotExistsAndNotMatchedConstraints() + public function testStatusNotAvailable() { - $path = 'path/to/patch'; - $name = 'patchName'; - $packageName = 'packageName'; - $constraint = '1.0'; - - $this->localRepositoryMock->expects($this->once()) - ->method('findPackage') - ->with($packageName, $constraint) - ->willReturn(null); + $patchContent = 'patch content'; + /** @var Process|MockObject $result */ $processMock = $this->createMock(Process::class); + $processMock->method('mustRun') + ->willThrowException(new ProcessFailedException($processMock)); - $this->processFactoryMock->expects($this->never()) + $this->processFactory->expects($this->exactly(2)) ->method('create') - ->with(['git', 'apply', 'root/path/to/patch']) ->willReturn($processMock); - $this->applier->apply($path, $name, $packageName, $constraint, false); + $this->assertSame(StatusPool::NA, $this->applier->status($patchContent)); } /** - * @throws ApplierException + * Tests status operation, case when patch is applied. */ - public function testApplyPatchAlreadyApplied() + public function testStatusApplied() { - $path = 'path/to/patch'; - $name = 'patchName'; - $packageName = 'packageName'; - $constraint = '1.0'; - - $this->filesystemMock->expects($this->once()) - ->method('exists') - ->with($path) - ->willReturn(true); - $this->localRepositoryMock->expects($this->once()) - ->method('findPackage') - ->with($packageName, $constraint) - ->willReturn($this->getMockForAbstractClass(PackageInterface::class)); - - $this->processFactoryMock->expects($this->exactly(2)) + $patchContent = 'patch content'; + + $this->processFactory->expects($this->exactly(2)) ->method('create') ->willReturnMap([ - [['git', 'apply', 'path/to/patch']], - [['git', 'apply', 'path/to/patch', '--revert']] - ])->willReturnCallback([$this, 'shellMockReverseCallback']); - - $this->assertSame( - 'Patch "patchName (path/to/patch) 1.0" was already applied', - $this->applier->apply($path, $name, $packageName, $constraint, false) - ); + [['git', 'apply', '--check']], + [['git', 'apply', '--check', '--reverse']] + ])->willReturnCallback([$this, 'shellStatusCallback']); + + $this->assertSame(StatusPool::APPLIED, $this->applier->status($patchContent)); } /** + * Callback for 'status' operations. + * * @param array $command * @return Process * * @throws ProcessFailedException when the command isn't a reverse */ - public function shellMockReverseCallback(array $command): Process + public function shellStatusCallback(array $command): Process { if (in_array('--reverse', $command, true) && in_array('--check', $command, true)) { // Command was the reverse check, it's all good. diff --git a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php new file mode 100644 index 00000000..55e65696 --- /dev/null +++ b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php @@ -0,0 +1,299 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Collector; + +use Magento\CloudPatches\Composer\Package; +use Magento\CloudPatches\Filesystem\DirectoryList; +use Magento\CloudPatches\Patch\Collector\CloudCollector; +use Magento\CloudPatches\Patch\Collector\CollectorException; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Environment; +use Magento\CloudPatches\Patch\PatchBuilder; +use Magento\CloudPatches\Patch\PatchIntegrityException; +use Magento\CloudPatches\Patch\SourceProvider; +use Magento\CloudPatches\Patch\SourceProviderException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class CloudCollectorTest extends TestCase +{ + const CLOUD_PATCH_DIR = 'cloud/patch/dir'; + + /** + * @var CloudCollector + */ + private $collector; + + /** + * @var PatchBuilder|MockObject + */ + private $patchBuilder; + + /** + * @var SourceProvider|MockObject + */ + private $sourceProvider; + + /** + * @var Package|MockObject + */ + private $package; + + /** + * @var Environment|MockObject + */ + private $environment; + + /** + * @var DirectoryList|MockObject + */ + private $directoryList; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->sourceProvider = $this->createMock(SourceProvider::class); + $this->package = $this->createMock(Package::class); + $this->environment = $this->createMock(Environment::class); + $this->directoryList = $this->createMock(DirectoryList::class); + $this->patchBuilder = $this->createMock(PatchBuilder::class); + + $this->collector = new CloudCollector( + $this->sourceProvider, + $this->package, + $this->directoryList, + $this->environment, + $this->patchBuilder + ); + } + + /** + * Tests collecting patches - valid configuration + * + * @param bool $isCloud + * @param string $expectedType + * @dataProvider collectDataProvider + */ + public function testCollectSuccessful(bool $isCloud, string $expectedType) + { + $validConfig = require __DIR__ . '/Fixture/cloud_config_valid.php'; + $this->sourceProvider->expects($this->once()) + ->method('getCloudPatches') + ->willReturn($validConfig); + $this->directoryList->method('getPatches') + ->willReturn(self::CLOUD_PATCH_DIR); + $this->environment->method('isCloud') + ->willReturn($isCloud); + + $this->package->method('matchConstraint') + ->willReturnMap([ + ['magento/magento2-base', '2.1.4 - 2.1.14', false], + ['magento/magento2-base', '2.2.0 - 2.2.5', true], + ['magento/magento2-ee-base', '2.2.0 - 2.2.5', true], + ]); + + $this->patchBuilder->expects($this->exactly(3)) + ->method('setId') + ->withConsecutive(['MDVA-2470'], ['MDVA-2470'], ['MAGECLOUD-2033']); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setTitle') + ->withConsecutive( + ['Fix asset locker race condition when using Redis'], + ['Fix asset locker race condition when using Redis EE'], + ['Allow DB dumps done with the support module to complete'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setFilename') + ->withConsecutive( + ['MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'], + ['MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'], + ['MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setPath') + ->withConsecutive( + [self::CLOUD_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'], + [self::CLOUD_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'], + [self::CLOUD_PATCH_DIR . '/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setType') + ->withConsecutive( + [$expectedType], + [$expectedType], + [$expectedType] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setPackageName') + ->withConsecutive( + ['magento/magento2-base'], + ['magento/magento2-ee-base'], + ['magento/magento2-ee-base'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setPackageConstraint') + ->withConsecutive( + ['2.2.0 - 2.2.5'], + ['2.2.0 - 2.2.5'], + ['2.2.0 - 2.2.5'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('build') + ->willReturn($this->createMock(Patch::class)); + + $this->assertTrue(is_array($this->collector->collect())); + } + + /** + * @return array + */ + public function collectDataProvider(): array + { + return [ + ['isCloud' => false, 'expectedType' => PatchInterface::TYPE_OPTIONAL], + ['isCloud' => true, 'expectedType' => PatchInterface::TYPE_REQUIRED] + ]; + } + + /** + * Tests collecting patches - invalid configuration, patch filename + * + * @param array $invalidConfig + * @dataProvider invalidPatchFilenameDataProvider + */ + public function testInvalidConfigurationPatchFilename(array $invalidConfig) + { + $this->sourceProvider->expects($this->once()) + ->method('getCloudPatches') + ->willReturn($invalidConfig); + + $this->package->expects($this->never()) + ->method('matchConstraint'); + + $this->expectException(CollectorException::class); + $this->collector->collect(); + } + + /** + * @return array + */ + public function invalidPatchFilenameDataProvider(): array + { + return [ + [$this->createConfig('fix_asset_locking_race_condition__2.1.4.patch')], + [$this->createConfig('MDVA-2470__fix_asset_locking_race_condition.patch')], + [$this->createConfig('MDVA-2470_fix_asset_locking_race_condition__2.1.4.patch')], + [$this->createConfig('MDVA-2470__fix_asset_locking_race_condition_2.1.4.patch')], + ]; + } + + /** + * Returns config. + * + * @param string $filename + * @return array + */ + private function createConfig(string $filename): array + { + return [ + 'magento/magento2-base' => [ + 'Fix asset locker race condition when using Redis' => [ + '2.1.4 - 2.1.14' => $filename + ] + ] + ]; + } + + /** + * Tests collecting patches - invalid configuration, patch title section + * + * @param array $config + * @dataProvider invalidTitleSectionDataProvider + */ + public function testInvalidConfigurationTitleSection(array $config) + { + $this->sourceProvider->expects($this->once()) + ->method('getCloudPatches') + ->willReturn($config); + + $this->patchBuilder->expects($this->never()) + ->method('build'); + + $this->expectException(CollectorException::class); + $this->collector->collect(); + } + + /** + * @return array + */ + public function invalidTitleSectionDataProvider(): array + { + return [ + [ + [ + 'magento/magento2-base' => [ + 'Fix asset locker race condition when using Redis' => [], + ] + ] + ], + [ + [ + 'magento/magento2-base' => [ + 'Fix asset locker race condition when using Redis' => 'String instead of array', + ] + ] + ] + ]; + } + + /** + * Tests case when patch factory can't create a patch for some reason. + */ + public function testPatchIntegrityException() + { + $validConfig = require __DIR__ . '/Fixture/cloud_config_valid.php'; + $this->sourceProvider->expects($this->once()) + ->method('getCloudPatches') + ->willReturn($validConfig); + + $this->package->method('matchConstraint') + ->willReturnMap([ + ['magento/magento2-base', '2.1.4 - 2.1.14', false], + ['magento/magento2-base', '2.2.0 - 2.2.5', true], + ['magento/magento2-ee-base', '2.2.0 - 2.2.5', true], + ]); + + $this->patchBuilder->method('build') + ->willThrowException(new PatchIntegrityException('')); + + $this->expectException(CollectorException::class); + $this->collector->collect(); + } + + /** + * Tests case when configuration can't be retrieved from source. + */ + public function testSourceProviderException() + { + $this->sourceProvider->expects($this->once()) + ->method('getCloudPatches') + ->willThrowException(new SourceProviderException('')); + + $this->patchBuilder->expects($this->never()) + ->method('build'); + + $this->expectException(CollectorException::class); + $this->collector->collect(); + } +} diff --git a/src/Test/Unit/Patch/Collector/Fixture/cloud_config_valid.php b/src/Test/Unit/Patch/Collector/Fixture/cloud_config_valid.php new file mode 100644 index 00000000..e1523ff4 --- /dev/null +++ b/src/Test/Unit/Patch/Collector/Fixture/cloud_config_valid.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector\Fixture; + +return [ + 'magento/magento2-base' => [ + 'Fix asset locker race condition when using Redis' => [ + '2.1.4 - 2.1.14' => 'MDVA-2470__fix_asset_locking_race_condition__2.1.4.patch', + '2.2.0 - 2.2.5' => 'MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch' + ], + ], + 'magento/magento2-ee-base' => [ + 'Fix asset locker race condition when using Redis EE' => [ + '2.2.0 - 2.2.5' => 'MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch' + ], + 'Allow DB dumps done with the support module to complete' => [ + '2.2.0 - 2.2.5' => 'MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch' + ] + ] +]; diff --git a/src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch1.patch b/src/Test/Unit/Patch/Collector/Fixture/m2-hotfixes/patch1.patch similarity index 100% rename from src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch1.patch rename to src/Test/Unit/Patch/Collector/Fixture/m2-hotfixes/patch1.patch diff --git a/src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch2.patch b/src/Test/Unit/Patch/Collector/Fixture/m2-hotfixes/patch2.patch similarity index 100% rename from src/Test/Unit/Command/Patch/_files/m2-hotfixes/patch2.patch rename to src/Test/Unit/Patch/Collector/Fixture/m2-hotfixes/patch2.patch diff --git a/src/Test/Unit/Command/Patch/_files/m2-hotfixes/readme.md b/src/Test/Unit/Patch/Collector/Fixture/m2-hotfixes/readme.md similarity index 100% rename from src/Test/Unit/Command/Patch/_files/m2-hotfixes/readme.md rename to src/Test/Unit/Patch/Collector/Fixture/m2-hotfixes/readme.md diff --git a/src/Test/Unit/Patch/Collector/Fixture/quality_config_invalid.php b/src/Test/Unit/Patch/Collector/Fixture/quality_config_invalid.php new file mode 100644 index 00000000..5d2f2282 --- /dev/null +++ b/src/Test/Unit/Patch/Collector/Fixture/quality_config_invalid.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector\Fixture; + +return [ + 'MDVA-2033' => [ + 'magento/magento2-ee-base' => [ + 'Allow DB dumps done with the support module to complete' => [ + '2.2.0 - 2.2.5' => [ + 'require' => 'MC-11111 MC-22222', + 'replaced-with' => ['MC-33333'], + 'deprecated' => 1 + ] + ], + ] + ], +]; diff --git a/src/Test/Unit/Patch/Collector/Fixture/quality_config_valid.php b/src/Test/Unit/Patch/Collector/Fixture/quality_config_valid.php new file mode 100644 index 00000000..23345411 --- /dev/null +++ b/src/Test/Unit/Patch/Collector/Fixture/quality_config_valid.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector\Fixture; + +return [ + 'MDVA-2470' => [ + 'magento/magento2-base' => [ + 'Fix asset locker race condition when using Redis' => [ + '2.1.4 - 2.1.14' => [ + 'file' => 'MDVA-2470__fix_asset_locking_race_condition__2.1.4.patch' + ], + '2.2.0 - 2.2.5' => [ + 'file' => 'MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch' + ] + ], + ], + 'magento/magento2-ee-base' => [ + 'Fix asset locker race condition when using Redis EE' => [ + '2.2.0 - 2.2.5' => [ + 'file' => 'MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch' + ] + ], + ] + + ], + 'MDVA-2033' => [ + 'magento/magento2-ee-base' => [ + 'Allow DB dumps done with the support module to complete' => [ + '2.2.0 - 2.2.5' => [ + 'file' => 'MDVA-2033__prevent_deadlock_during_db_dump__2.2.0.patch', + 'require' => ['MC-11111', 'MC-22222'], + 'replaced-with' => 'MC-33333', + 'deprecated' => true + ] + ], + ] + ], +]; diff --git a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php new file mode 100644 index 00000000..cd297c5e --- /dev/null +++ b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Collector; + +use Magento\CloudPatches\Patch\Collector\LocalCollector; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\PatchBuilder; +use Magento\CloudPatches\Patch\SourceProvider; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class LocalCollectorTest extends TestCase +{ + /** + * @var LocalCollector + */ + private $collector; + + /** + * @var PatchBuilder|MockObject + */ + private $patchBuilder; + + /** + * @var SourceProvider|MockObject + */ + private $sourceProvider; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->patchBuilder = $this->createMock(PatchBuilder::class); + $this->sourceProvider = $this->createMock(SourceProvider::class); + + $this->collector = new LocalCollector( + $this->sourceProvider, + $this->patchBuilder + ); + } + + /** + * Tests collecting local patches. + */ + public function testCollect() + { + $file1 = __DIR__ . SourceProvider::HOT_FIXES_DIR . '/patch1.patch'; + $file2 = __DIR__ . SourceProvider::HOT_FIXES_DIR . '/patch2.patch'; + + $this->sourceProvider->expects($this->once()) + ->method('getLocalPatches') + ->willReturn([$file1, $file2]); + + $this->patchBuilder->expects($this->exactly(2)) + ->method('setId') + ->withConsecutive([md5($file1)], [md5($file2)]); + $this->patchBuilder->expects($this->exactly(2)) + ->method('setTitle') + ->withConsecutive( + ['../' . SourceProvider::HOT_FIXES_DIR . '/patch1.patch'], + ['../' . SourceProvider::HOT_FIXES_DIR . '/patch2.patch'] + ); + $this->patchBuilder->expects($this->exactly(2)) + ->method('setFilename') + ->withConsecutive(['patch1.patch'], ['patch2.patch']); + $this->patchBuilder->expects($this->exactly(2)) + ->method('setPath') + ->withConsecutive([$file1], [$file2]); + $this->patchBuilder->expects($this->exactly(2)) + ->method('setType') + ->withConsecutive([PatchInterface::TYPE_CUSTOM], [PatchInterface::TYPE_CUSTOM]); + $this->patchBuilder->expects($this->exactly(2)) + ->method('build') + ->willReturn($this->createMock(Patch::class)); + + $this->assertTrue(is_array($this->collector->collect())); + } +} diff --git a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php new file mode 100644 index 00000000..5d4bdf1c --- /dev/null +++ b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php @@ -0,0 +1,226 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Collector; + +use Magento\CloudPatches\Composer\Package; +use Magento\CloudPatches\Patch\Collector\CollectorException; +use Magento\CloudPatches\Patch\Collector\QualityCollector; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\PatchBuilder; +use Magento\CloudPatches\Patch\PatchIntegrityException; +use Magento\CloudPatches\Patch\SourceProvider; +use Magento\CloudPatches\Patch\SourceProviderException; +use Magento\QualityPatches\Info as QualityPatchesInfo; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class QualityCollectorTest extends TestCase +{ + const QUALITY_PATCH_DIR = 'quality/patch/dir'; + + /** + * @var QualityCollector + */ + private $collector; + + /** + * @var PatchBuilder|MockObject + */ + private $patchBuilder; + + /** + * @var SourceProvider|MockObject + */ + private $sourceProvider; + + /** + * @var Package|MockObject + */ + private $package; + + /** + * @var QualityPatchesInfo|MockObject + */ + private $qualityPatchesInfo; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->sourceProvider = $this->createMock(SourceProvider::class); + $this->package = $this->createMock(Package::class); + $this->qualityPatchesInfo = $this->createMock(QualityPatchesInfo::class); + $this->patchBuilder = $this->createMock(PatchBuilder::class); + + $this->collector = new QualityCollector( + $this->sourceProvider, + $this->package, + $this->qualityPatchesInfo, + $this->patchBuilder + ); + } + + /** + * Tests collecting patches - valid configuration + */ + public function testCollectSuccessful() + { + $validConfig = require __DIR__ . '/Fixture/quality_config_valid.php'; + $this->sourceProvider->expects($this->once()) + ->method('getQualityPatches') + ->willReturn($validConfig); + $this->qualityPatchesInfo->method('getPatchesDirectory') + ->willReturn(self::QUALITY_PATCH_DIR); + + $this->package->method('matchConstraint') + ->willReturnMap([ + ['magento/magento2-base', '2.1.4 - 2.1.14', false], + ['magento/magento2-base', '2.2.0 - 2.2.5', true], + ['magento/magento2-ee-base', '2.2.0 - 2.2.5', true], + ]); + + $this->package->method('matchConstraint') + ->willReturnMap([ + ['magento/magento2-base', '2.1.4 - 2.1.14', false], + ['magento/magento2-base', '2.2.0 - 2.2.5', true], + ['magento/magento2-ee-base', '2.2.0 - 2.2.5', true], + ]); + + $this->patchBuilder->expects($this->exactly(3)) + ->method('setId') + ->withConsecutive(['MDVA-2470'], ['MDVA-2470'], ['MDVA-2033']); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setTitle') + ->withConsecutive( + ['Fix asset locker race condition when using Redis'], + ['Fix asset locker race condition when using Redis EE'], + ['Allow DB dumps done with the support module to complete'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setFilename') + ->withConsecutive( + ['MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'], + ['MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'], + ['MDVA-2033__prevent_deadlock_during_db_dump__2.2.0.patch'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setPath') + ->withConsecutive( + [self::QUALITY_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'], + [self::QUALITY_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'], + [self::QUALITY_PATCH_DIR . '/MDVA-2033__prevent_deadlock_during_db_dump__2.2.0.patch'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setType') + ->withConsecutive( + [PatchInterface::TYPE_OPTIONAL], + [PatchInterface::TYPE_OPTIONAL], + [PatchInterface::TYPE_OPTIONAL] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setPackageName') + ->withConsecutive( + ['magento/magento2-base'], + ['magento/magento2-ee-base'], + ['magento/magento2-ee-base'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setPackageConstraint') + ->withConsecutive( + ['2.2.0 - 2.2.5'], + ['2.2.0 - 2.2.5'], + ['2.2.0 - 2.2.5'] + ); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setRequire') + ->withConsecutive([[]], [[]], [['MC-11111', 'MC-22222']]); + + $this->patchBuilder->expects($this->exactly(3)) + ->method('setReplacedWith') + ->withConsecutive([''], [''], ['MC-33333']); + $this->patchBuilder->expects($this->exactly(3)) + ->method('setDeprecated') + ->withConsecutive([false], [false], [true]); + $this->patchBuilder->expects($this->exactly(3)) + ->method('build') + ->willReturn($this->createMock(Patch::class)); + + $this->assertTrue(is_array($this->collector->collect())); + } + + /** + * Tests collecting patches - invalid configuration + */ + public function testInvalidConfiguration() + { + $config = require __DIR__ . '/Fixture/quality_config_invalid.php'; + + $expectedExceptionMessage = 'Patch MDVA-2033 has invalid configuration:' . + PHP_EOL . ' - Property \'file\' is not found in \'2.2.0 - 2.2.5\'' . + PHP_EOL . ' - Property \'require\' from \'2.2.0 - 2.2.5\' should have an array type' . + PHP_EOL . ' - Property \'replaced-with\' from \'2.2.0 - 2.2.5\' should have a string type' . + PHP_EOL . ' - Property \'deprecated\' from \'2.2.0 - 2.2.5\' should have a boolean type'; + + $this->sourceProvider->expects($this->once()) + ->method('getQualityPatches') + ->willReturn($config); + + $this->patchBuilder->expects($this->never()) + ->method('build'); + + $this->expectException(CollectorException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->collector->collect(); + } + + /** + * Tests case when patch factory can't create a patch for some reason. + */ + public function testPatchIntegrityException() + { + $validConfig = require __DIR__ . '/Fixture/quality_config_valid.php'; + $this->sourceProvider->expects($this->once()) + ->method('getQualityPatches') + ->willReturn($validConfig); + + $this->package->method('matchConstraint') + ->willReturnMap([ + ['magento/magento2-base', '2.1.4 - 2.1.14', false], + ['magento/magento2-base', '2.2.0 - 2.2.5', true], + ['magento/magento2-ee-base', '2.2.0 - 2.2.5', true], + ]); + + $this->patchBuilder->method('build') + ->willThrowException(new PatchIntegrityException('')); + + $this->expectException(CollectorException::class); + $this->collector->collect(); + } + + /** + * Tests case when configuration can't be retrieved from source. + */ + public function testSourceProviderException() + { + $this->sourceProvider->expects($this->once()) + ->method('getQualityPatches') + ->willThrowException(new SourceProviderException('')); + + $this->patchBuilder->expects($this->never()) + ->method('build'); + + $this->expectException(CollectorException::class); + $this->collector->collect(); + } +} diff --git a/src/Test/Unit/Patch/EnvironmentTest.php b/src/Test/Unit/Patch/EnvironmentTest.php new file mode 100644 index 00000000..21741aff --- /dev/null +++ b/src/Test/Unit/Patch/EnvironmentTest.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\Patch\Environment; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class EnvironmentTest extends TestCase +{ + /** + * Tests environment. + */ + public function testIsCloud() + { + $environment = new Environment(); + + $_ENV[Environment::ENV_VAR_CLOUD] = ''; + $this->assertFalse($environment->isCloud()); + + $_ENV[Environment::ENV_VAR_CLOUD] = '123'; + $this->assertTrue($environment->isCloud()); + } +} diff --git a/src/Test/Unit/Patch/FilterFactoryTest.php b/src/Test/Unit/Patch/FilterFactoryTest.php new file mode 100644 index 00000000..db9f2bd8 --- /dev/null +++ b/src/Test/Unit/Patch/FilterFactoryTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\Patch\FilterFactory; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class FilterFactoryTest extends TestCase +{ + /** + * @var FilterFactory + */ + private $filterFactory; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->filterFactory = new FilterFactory(); + } + + /** + * Tests creating 'apply' filter. + * + * @param array $inputArgument + * @param array|null $expectedValue + * @dataProvider createApplyFilterDataProvider + */ + public function testCreateApplyFilter(array $inputArgument, $expectedValue) + { + $this->assertEquals( + $expectedValue, + $this->filterFactory->createApplyFilter($inputArgument) + ); + } + + /** + * @return array + */ + public function createApplyFilterDataProvider(): array + { + return [ + ['inputArgument' => [], 'expectedValue' => null], + ['inputArgument' => ['*'], 'expectedValue' => []], + ['inputArgument' => ['*', 'MC-1'], 'expectedValue' => []], + ['inputArgument' => ['MC-1', 'MC-2'], 'expectedValue' => ['MC-1', 'MC-2']], + ]; + } + + /** + * Tests creating 'apply' filter. + * + * @param array $inputArgument + * @param bool $optAll + * @param array|null $expectedValue + * @dataProvider createRevertFilterDataProvider + */ + public function testCreateRevertFilter(array $inputArgument, bool $optAll, $expectedValue) + { + $this->assertEquals( + $expectedValue, + $this->filterFactory->createRevertFilter($optAll, $inputArgument) + ); + } + + /** + * @return array + */ + public function createRevertFilterDataProvider(): array + { + return [ + ['inputArgument' => [], 'optAll' => false, 'expectedValue' => null], + ['inputArgument' => ['*'], 'optAll' => false, 'expectedValue' => ['*']], + ['inputArgument' => ['MC-1', 'MC-2'], 'optAll' => false, 'expectedValue' => ['MC-1', 'MC-2']], + ['inputArgument' => [], 'optAll' => true, 'expectedValue' => []], + ['inputArgument' => ['MC-1', 'MC-2'], 'optAll' => true, 'expectedValue' => []] + ]; + } +} diff --git a/src/Test/Unit/Patch/Fixture/MC-1__testfixture__1.1.patch b/src/Test/Unit/Patch/Fixture/MC-1__testfixture__1.1.patch new file mode 100644 index 00000000..7ba62531 --- /dev/null +++ b/src/Test/Unit/Patch/Fixture/MC-1__testfixture__1.1.patch @@ -0,0 +1,21 @@ +diff -Nuar a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +--- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +@@ -77,6 +77,7 @@ class UpgradeCommand extends AbstractSetupCommand + protected function execute(InputInterface $input, OutputInterface $output) + { + +diff -Nuar a/vendor/magento/module-email/Model/Transport.php b/vendor/magento/module-email/Model/Transport.php +--- a/vendor/magento/module-email/Model/Transport.php ++++ b/vendor/magento/module-email/Model/Transport.php +@@ -9,7 +9,6 @@ namespace Magento\Email\Model; + + use Magento\Framework\App\Config\ScopeConfigInterface; + use Magento\Framework\Exception\MailException; + +diff -Nuar a/vendor/magento/framework/Mail/EmailMessage.php b/vendor/magento/framework/Mail/EmailMessage.php +--- a/vendor/magento/framework/Mail/EmailMessage.php ++++ b/vendor/magento/framework/Mail/EmailMessage.php +@@ -10,19 +10,13 @@ namespace Magento\Framework\Mail; + use Magento\Framework\Mail\Exception\InvalidArgumentException; + use Zend\Mail\Address as ZendAddress; diff --git a/src/Test/Unit/Patch/PatchBuilderTest.php b/src/Test/Unit/Patch/PatchBuilderTest.php new file mode 100644 index 00000000..de7abbef --- /dev/null +++ b/src/Test/Unit/Patch/PatchBuilderTest.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\PatchBuilder; +use Magento\CloudPatches\Patch\PatchIntegrityException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class PatchBuilderTest extends TestCase +{ + const PATCH_FILENAME = 'filename.patch'; + + /** + * @var PatchBuilder + */ + private $patchBuilder; + + /** + * @var Filesystem|MockObject + */ + private $filesystem; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->filesystem = $this->createMock(Filesystem::class); + + $this->patchBuilder = new PatchBuilder($this->filesystem); + } + + /** + * Tests patch creation. + */ + public function testBuild() + { + $patchData = [ + 'id' => 'mc-1', + 'title' => 'Title patch MC-1', + 'filename' => self::PATCH_FILENAME, + 'path' => 'patch/dir/' . self::PATCH_FILENAME, + 'type' => 'patch type', + 'packageName' => 'magento/magento-base', + 'packageConstraint' => '2.3.5', + 'require' => ['MC-2'], + 'replacedWith' => 'MC-3', + 'deprecated' => true + ]; + $patchContent = file_get_contents(__DIR__ . '/Fixture/MC-1__testfixture__1.1.patch'); + + $this->filesystem->method('get') + ->willReturn($patchContent); + + $patch = $this->buildPatch($patchData); + + $this->assertEquals($patch->getType(), $patchData['type']); + $this->assertEquals($patch->getPath(), $patchData['path']); + $this->assertEquals($patch->getId(), strtoupper($patchData['id'])); + $this->assertEquals($patch->getTitle(), $patchData['title']); + $this->assertEquals($patch->getFilename(), $patchData['filename']); + $this->assertEquals($patch->getPackageName(), $patchData['packageName']); + $this->assertEquals($patch->getPackageConstraint(), $patchData['packageConstraint']); + $this->assertEquals($patch->getRequire(), $patchData['require']); + $this->assertEquals($patch->getReplacedWith(), $patchData['replacedWith']); + $this->assertEquals($patch->isDeprecated(), $patchData['deprecated']); + $this->assertEquals( + ['magento/framework', 'magento/module-email', 'setup/src'], + $patch->getAffectedComponents() + ); + } + + /** + * Tests a case when patch content can't be received. + * + * @throws PatchIntegrityException + */ + public function testBuildWithException() + { + $patchData = [ + 'id' => 'mc-1', + 'title' => 'Title patch MC-1', + 'filename' => self::PATCH_FILENAME, + 'path' => 'patch/dir/' . self::PATCH_FILENAME, + 'type' => 'patch type', + 'packageName' => 'magento/magento-base', + 'packageConstraint' => '2.3.5', + 'require' => ['MC-2'], + 'replacedWith' => 'MC-3', + 'deprecated' => true + ]; + + $this->filesystem->method('get') + ->willThrowException(new FileSystemException('')); + + $this->expectException(PatchIntegrityException::class); + $this->buildPatch($patchData); + } + + /** + * Builds a patch. + * + * @param array $patchData + * + * @return PatchInterface + * @throws PatchIntegrityException + */ + private function buildPatch(array $patchData): PatchInterface + { + $this->patchBuilder->setId($patchData['id']); + $this->patchBuilder->setTitle($patchData['title']); + $this->patchBuilder->setFilename($patchData['filename']); + $this->patchBuilder->setPath($patchData['path']); + $this->patchBuilder->setType($patchData['type']); + $this->patchBuilder->setPackageName($patchData['packageName']); + $this->patchBuilder->setPackageConstraint($patchData['packageConstraint']); + $this->patchBuilder->setRequire($patchData['require']); + $this->patchBuilder->setReplacedWith($patchData['replacedWith']); + $this->patchBuilder->setDeprecated($patchData['deprecated']); + + return $this->patchBuilder->build(); + } +} diff --git a/src/Test/Unit/Patch/Pool/LocalPoolTest.php b/src/Test/Unit/Patch/Pool/LocalPoolTest.php new file mode 100644 index 00000000..2439260a --- /dev/null +++ b/src/Test/Unit/Patch/Pool/LocalPoolTest.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Pool; + +use Magento\CloudPatches\Patch\Collector\CloudCollector; +use Magento\CloudPatches\Patch\Collector\LocalCollector; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Pool\LocalPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class LocalPoolTest extends TestCase +{ + /** + * Tests retrieving patches. + */ + public function testGetList() + { + $patch1 = $this->createPatch('HotFix-1'); + $patch2 = $this->createPatch('HotFix-2'); + $patch3 = $this->createPatch('HotFix-3'); + + /** @var LocalCollector|MockObject $localCollector */ + $localCollector = $this->createMock(LocalCollector::class); + $localCollector->expects($this->once()) + ->method('collect') + ->willReturn([$patch1, $patch2, $patch3]); + + $pool = new LocalPool($localCollector); + + $this->assertEquals([$patch1, $patch2, $patch3], $pool->getList()); + } + + /** + * Creates patch mock. + * + * @param string $id + * @return Patch|MockObject + */ + private function createPatch(string $id) + { + $patch = $this->createMock(Patch::class); + $patch->method('getId')->willReturn($id); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php new file mode 100644 index 00000000..53c711e7 --- /dev/null +++ b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php @@ -0,0 +1,310 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Pool; + +use Magento\CloudPatches\Patch\Collector\CollectorException; +use Magento\CloudPatches\Patch\Collector\QualityCollector; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\PatchIntegrityException; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Pool\PatchNotFoundException; +use Magento\CloudPatches\Patch\Pool\RequiredPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class OptionalPoolTest extends TestCase +{ + /** + * @var RequiredPool|MockObject + */ + private $requiredPool; + + /** + * @var QualityCollector|MockObject + */ + private $qualityCollector; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->requiredPool = $this->createMock(RequiredPool::class); + $this->qualityCollector = $this->createMock(QualityCollector::class); + } + + /** + * Tests retrieving patches. + * + * @param string[] $filter + * @param bool $useRequire + * @param PatchInterface[] $cloudPatches + * @param PatchInterface[] $qualityPatches + * @param PatchInterface[] $expectedResult + * + * @dataProvider getListFilterDataProvider + */ + public function testGetList( + array $filter, + bool $useRequire, + array $cloudPatches, + array $qualityPatches, + array $expectedResult + ) { + $pool = $this->createPool($cloudPatches, $qualityPatches); + + $this->assertEquals($expectedResult, array_values($pool->getList($filter, $useRequire))); + } + + /** + * @return array + */ + public function getListFilterDataProvider(): array + { + return [ + $this->caseReturnAllWithEmptyFilter(), + $this->caseReturnPatchWithoutRequired(), + $this->caseReturnPatchWithRequired(), + $this->caseReturnPatchListUnique() + ]; + } + + /** + * Tests case when patch is not found in a pool. + */ + public function testGetListPatchNotFound() + { + $filter = ['MC-3']; + $patch1 = $this->createPatch('MC-1'); + $patch2 = $this->createPatch('MC-2'); + + $pool = $this->createPool([$patch1, $patch2]); + + $this->expectException(PatchNotFoundException::class); + $pool->getList($filter); + } + + /** + * Tests case when patch from 'require' configuration attribute is not found in a pool. + */ + public function testGetListRequiredPatchNotFound() + { + $filter = ['MC-1']; + $patch = $this->createPatch('MC-1', ['MC-not-exists']); + + $pool = $this->createPool([$patch]); + + $this->expectException(PatchIntegrityException::class); + $pool->getList($filter); + } + + /** + * Tests retrieving only optional patches. + */ + public function testGetOptionalAll() + { + $requiredPatch1 = $this->createPatch('MCLOUD-1'); + $requiredPatch2 = $this->createPatch('MCLOUD-2'); + $optionalPatch1 = $this->createPatch('MC-1'); + $optionalPatch2 = $this->createPatch('MC-2'); + $optionalPatch1->method('getType') + ->willReturn(PatchInterface::TYPE_OPTIONAL); + $optionalPatch2->method('getType') + ->willReturn(PatchInterface::TYPE_OPTIONAL); + + $pool = $this->createPool([$requiredPatch1, $requiredPatch2], [$optionalPatch1, $optionalPatch2]); + + $this->assertEquals([$optionalPatch1, $optionalPatch2], $pool->getOptionalAll()); + } + + /** + * Tests retrieving patch ids dependent on provided patch if any. + */ + public function testGetDependentOn() + { + $patch1 = $this->createPatch('MC-1'); + $patch2 = $this->createPatch('MC-2', ['MC-1']); + $patch3 = $this->createPatch('MC-3', ['MC-1']); + + $pool = $this->createPool([$patch1, $patch2, $patch3]); + + $this->assertEquals(['MC-2', 'MC-3'], $pool->getDependentOn('MC-1')); + } + + /** + * Tests retrieving additional required patches which are not included in patch filter. + */ + public function testGetAdditionalRequiredPatches() + { + $filter = ['MC-4', 'MC-1']; + $patch1 = $this->createPatch('MC-1'); + $patch2 = $this->createPatch('MC-2', ['MC-1']); + $patch3 = $this->createPatch('MC-3', ['MC-2']); + $patch4 = $this->createPatch('MC-4', ['MC-3']); + + $pool = $this->createPool([$patch1, $patch2, $patch3, $patch4]); + + $this->assertEquals( + [$patch2, $patch3], + array_values($pool->getAdditionalRequiredPatches($filter)) + ); + } + + /** + * Tests retrieving patch ids replaced by provided patch if any. + */ + public function testGetReplacedBy() + { + $patchForReplaceId = 'MC-4'; + $patch1 = $this->createPatch('MC-1'); + $patch2 = $this->createPatch('MC-2', ['MC-1'], $patchForReplaceId); + $patch3 = $this->createPatch('MC-3', [], $patchForReplaceId); + $patch4 = $this->createPatch('MC-4'); + + $pool = $this->createPool([$patch1, $patch2, $patch3, $patch4]); + + $this->assertEquals( + [$patch2->getId(), $patch3->getId()], + array_values($pool->getReplacedBy($patchForReplaceId)) + ); + } + + /** + * Filter is empty, Cloud + Quality patches expected to return. + * + * @return array[] + */ + private function caseReturnAllWithEmptyFilter(): array + { + $cloudPatch1 = $this->createPatch('MCLOUD-1'); + $cloudPatch2 = $this->createPatch('MCLOUD-2'); + $qualityPatch1 = $this->createPatch('MC-1'); + $qualityPatch2 = $this->createPatch('MC-2'); + + return [ + 'filter' => [], + 'useRequire' => false, + 'cloudPatches' => [$cloudPatch1, $cloudPatch2], + 'qualityPatches' => [$qualityPatch1, $qualityPatch2], + 'expectedResult' => [$cloudPatch1, $cloudPatch2, $qualityPatch1, $qualityPatch2] + ]; + } + + /** + * Filter is not empty, expected to return requested patch without required. + * + * @return array[] + */ + private function caseReturnPatchWithoutRequired(): array + { + $cloudPatch1 = $this->createPatch('MCLOUD-1'); + $cloudPatch2 = $this->createPatch('MCLOUD-2'); + $qualityPatch1 = $this->createPatch('MC-1'); + $qualityPatch2 = $this->createPatch('MC-2', ['MC-1']); + + return [ + 'filter' => ['MC-2'], + 'useRequire' => false, + 'cloudPatches' => [$cloudPatch1, $cloudPatch2], + 'qualityPatches' => [$qualityPatch1, $qualityPatch2], + 'expectedResult' => [$qualityPatch2] + ]; + } + + /** + * Expected to return requested patch with required. + * + * @return array[] + */ + private function caseReturnPatchWithRequired(): array + { + $cloudPatch1 = $this->createPatch('MCLOUD-1'); + $cloudPatch2 = $this->createPatch('MCLOUD-2'); + $qualityPatch1 = $this->createPatch('MC-1'); + $qualityPatch2 = $this->createPatch('MC-2', ['MCLOUD-2']); + + return [ + 'filter' => ['MC-2'], + 'useRequire' => true, + 'cloudPatches' => [$cloudPatch1, $cloudPatch2], + 'qualityPatches' => [$qualityPatch1, $qualityPatch2], + 'expectedResult' => [$cloudPatch2, $qualityPatch2] + ]; + } + + /** + * Expected to return result without duplicates. + * + * @return array[] + */ + private function caseReturnPatchListUnique(): array + { + $cloudPatch1 = $this->createPatch('MCLOUD-1'); + $cloudPatch2 = $this->createPatch('MCLOUD-2'); + $qualityPatch1 = $this->createPatch('MC-1'); + $qualityPatch2 = $this->createPatch('MC-2', ['MCLOUD-2']); + + return [ + 'filter' => ['MCLOUD-2', 'MC-2'], + 'useRequire' => true, + 'cloudPatches' => [$cloudPatch1, $cloudPatch2], + 'qualityPatches' => [$qualityPatch1, $qualityPatch2], + 'expectedResult' => [$cloudPatch2, $qualityPatch2] + ]; + } + + /** + * Creates patch mock. + * + * @param string $id + * @param array $require + * @param string $replacedWith + * @return Patch|MockObject + */ + private function createPatch(string $id, array $require = [], string $replacedWith = '') + { + $patch = $this->createMock(Patch::class); + $patch->method('getId')->willReturn($id); + $patch->method('getRequire')->willReturn($require); + $patch->method('getReplacedWith')->willReturn($replacedWith); + + // To make mock object unique for assertions and array operations. + $patch->id = microtime(); + $patch->method('__toString')->willReturn($patch->id); + + return $patch; + } + + /** + * Creates optional pool. + * + * @param PatchInterface[] $cloudPatches + * @param PatchInterface[] $qualityPatches + * + * @return OptionalPool + * @throws CollectorException + */ + private function createPool(array $cloudPatches = [], array $qualityPatches = []): OptionalPool + { + $this->requiredPool->expects($this->once()) + ->method('getList') + ->willReturn($cloudPatches); + + $this->qualityCollector->expects($this->once()) + ->method('collect') + ->willReturn($qualityPatches); + + $pool = new OptionalPool($this->requiredPool, $this->qualityCollector); + + return $pool; + } +} diff --git a/src/Test/Unit/Patch/Pool/RequiredPoolTest.php b/src/Test/Unit/Patch/Pool/RequiredPoolTest.php new file mode 100644 index 00000000..f3f92d9e --- /dev/null +++ b/src/Test/Unit/Patch/Pool/RequiredPoolTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Pool; + +use Magento\CloudPatches\Patch\Collector\CloudCollector; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Pool\RequiredPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class RequiredPoolTest extends TestCase +{ + /** + * Tests retrieving patches. + */ + public function testGetList() + { + $patch1 = $this->createPatch('MCLOUD-1'); + $patch2 = $this->createPatch('MCLOUD-2'); + $patch3 = $this->createPatch('MCLOUD-3'); + + /** @var CloudCollector|MockObject $cloudCollector */ + $cloudCollector = $this->createMock(CloudCollector::class); + $cloudCollector->expects($this->once()) + ->method('collect') + ->willReturn([$patch1, $patch2, $patch3]); + + $pool = new RequiredPool($cloudCollector); + + $this->assertEquals([$patch1, $patch2, $patch3], $pool->getList()); + } + + /** + * Creates patch mock. + * + * @param string $id + * @return Patch|MockObject + */ + private function createPatch(string $id) + { + $patch = $this->createMock(Patch::class); + $patch->method('getId')->willReturn($id); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/RevertValidatorTest.php b/src/Test/Unit/Patch/RevertValidatorTest.php new file mode 100644 index 00000000..f39dc7fb --- /dev/null +++ b/src/Test/Unit/Patch/RevertValidatorTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\RevertValidator; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class RevertValidatorTest extends TestCase +{ + /** + * @var RevertValidator + */ + private $revertValidator; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @var StatusPool|MockObject + */ + private $statusPool; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->optionalPool = $this->createMock(OptionalPool::class); + $this->statusPool = $this->createMock(StatusPool::class); + + $this->revertValidator = new RevertValidator( + $this->optionalPool, + $this->statusPool + ); + } + + /** + * Tests validation fails. + * + * Case when patch has applied dependent patches. + */ + public function testValidateWithAppliedDependents() + { + $patchFilter = ['MC-1']; + + $this->optionalPool->method('getDependentOn') + ->with('MC-1') + ->willReturn(['MC-2', 'MC-3']); + + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-2', true], + ['MC-3', true], + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Patch MC-1 is a dependency for MC-2 MC-3. Please, revert MC-2 MC-3 first'); + + $this->revertValidator->validate($patchFilter); + } + + /** + * Tests validation success. + * + * Case when dependent patches are not applied. + * + * @doesNotPerformAssertions + */ + public function testValidateWithNotAppliedDependents() + { + $patchFilter = ['MC-1']; + + $this->optionalPool->method('getDependentOn') + ->with('MC-1') + ->willReturn(['MC-2', 'MC-3']); + + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-2', false], + ['MC-3', false], + ]); + + $this->revertValidator->validate($patchFilter); + } + + /** + * Tests validation success. + * + * Case when there are no dependent patches. + * + * @doesNotPerformAssertions + */ + public function testValidateWithNoDependents() + { + $patchFilter = ['MC-1']; + + $this->optionalPool->method('getDependentOn') + ->with('MC-1') + ->willReturn([]); + + $this->statusPool->expects($this->never()) + ->method('isApplied'); + + $this->revertValidator->validate($patchFilter); + } +} diff --git a/src/Test/Unit/Patch/SourceProviderTest.php b/src/Test/Unit/Patch/SourceProviderTest.php new file mode 100644 index 00000000..f4ad4534 --- /dev/null +++ b/src/Test/Unit/Patch/SourceProviderTest.php @@ -0,0 +1,166 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\Filesystem\DirectoryList; +use Magento\CloudPatches\Filesystem\FileList; +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\CloudPatches\Patch\SourceProvider; +use Magento\CloudPatches\Patch\SourceProviderException; +use Magento\QualityPatches\Info as QualityPatchesInfo; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class SourceProviderTest extends TestCase +{ + /** + * @var SourceProvider + */ + private $sourceProvider; + + /** + * @var Filesystem|MockObject + */ + private $filesystem; + + /** + * @var DirectoryList|MockObject + */ + private $directoryList; + + /** + * @var QualityPatchesInfo|MockObject + */ + private $qualityPatchesInfo; + + /** + * @var FileList|MockObject + */ + private $filelist; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->filesystem = $this->createMock(Filesystem::class); + $this->filelist = $this->createMock(FileList::class); + $this->directoryList = $this->createMock(DirectoryList::class); + $this->qualityPatchesInfo = $this->createMock(QualityPatchesInfo::class); + + $this->sourceProvider = new SourceProvider( + $this->filesystem, + $this->filelist, + $this->directoryList, + $this->qualityPatchesInfo + ); + } + + /** + * Tests retrieving Cloud patch configuration. + */ + public function testGetCloudPatches() + { + $configPath = '/cloud/patches.json'; + $configSource = require __DIR__ . '/Collector/Fixture/cloud_config_valid.php'; + $jsonConfig = json_encode($configSource); + + $this->filelist->expects($this->once()) + ->method('getPatches') + ->willReturn($configPath); + + $this->filesystem->expects($this->once()) + ->method('get') + ->with($configPath) + ->willReturn($jsonConfig); + + $this->assertEquals($configSource, $this->sourceProvider->getCloudPatches()); + } + + /** + * Tests retrieving Quality patch configuration. + */ + public function testGetQualityPatches() + { + $configPath = '/quality/patches.json'; + $configSource = require __DIR__ . '/Collector/Fixture/quality_config_valid.php'; + $jsonConfig = json_encode($configSource); + + $this->qualityPatchesInfo->expects($this->once()) + ->method('getPatchesConfig') + ->willReturn($configPath); + + $this->filesystem->expects($this->once()) + ->method('get') + ->with($configPath) + ->willReturn($jsonConfig); + + $this->assertEquals($configSource, $this->sourceProvider->getQualityPatches()); + } + + /** + * Tests retrieving Local patch configuration. + */ + public function testGetLocalPatches() + { + $this->directoryList->method('getMagentoRoot') + ->willReturn(__DIR__ . '/Collector/Fixture'); + + $expectedResult = [ + __DIR__ . '/Collector/Fixture/' . SourceProvider::HOT_FIXES_DIR . '/patch1.patch', + __DIR__ . '/Collector/Fixture/' . SourceProvider::HOT_FIXES_DIR . '/patch2.patch' + ]; + + $this->assertEquals($expectedResult, $this->sourceProvider->getLocalPatches()); + } + + /** + * Tests retrieving Quality patch configuration with filesystem exception. + */ + public function testGetQualityPatchesFilesystemException() + { + $configPath = '/quality/patches.json'; + + $this->qualityPatchesInfo->expects($this->once()) + ->method('getPatchesConfig') + ->willReturn($configPath); + + $this->filesystem->expects($this->once()) + ->method('get') + ->willThrowException(new FileSystemException('')); + + $this->expectException(SourceProviderException::class); + $this->sourceProvider->getQualityPatches(); + } + + /** + * Tests retrieving Quality patch configuration with json decoding exception. + */ + public function testGetQualityPatchesJsonException() + { + $configPath = '/quality/patches.json'; + + $this->qualityPatchesInfo->expects($this->once()) + ->method('getPatchesConfig') + ->willReturn($configPath); + + $this->filesystem->expects($this->once()) + ->method('get') + ->with($configPath) + ->willReturn('Invalid config format'); + + $this->expectException(SourceProviderException::class); + $this->expectExceptionMessageRegExp('/Unable to unserialize patches configuration/'); + + $this->sourceProvider->getQualityPatches(); + } +} diff --git a/src/Test/Unit/Patch/Status/LocalResolverTest.php b/src/Test/Unit/Patch/Status/LocalResolverTest.php new file mode 100644 index 00000000..cf0c8247 --- /dev/null +++ b/src/Test/Unit/Patch/Status/LocalResolverTest.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Status; + +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Pool\LocalPool; +use Magento\CloudPatches\Patch\Status\LocalResolver; +use Magento\CloudPatches\Patch\Status\OptionalResolver; +use Magento\CloudPatches\Patch\Status\StatusPool; +use Magento\CloudPatches\Patch\Status\StatusResolverException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class LocalResolverTest extends TestCase +{ + /** + * @var Filesystem|MockObject + */ + private $filesystem; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var LocalPool|MockObject + */ + private $localPool; + + /** + * @var OptionalResolver + */ + private $resolver; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->filesystem = $this->createMock(Filesystem::class); + $this->applier = $this->createMock(Applier::class); + $this->localPool = $this->createMock(LocalPool::class); + + $this->resolver = new LocalResolver( + $this->filesystem, + $this->applier, + $this->localPool + ); + } + + /** + * Tests resolving patch statuses. + */ + public function testResolve() + { + $patch1 = $this->createPatch('/path/patch1.patch'); + $patch2 = $this->createPatch('/path/patch2.patch'); + $patch3 = $this->createPatch('/path/patch3.patch'); + + $this->localPool->expects($this->once()) + ->method('getList') + ->willReturn([$patch1, $patch2, $patch3]); + + $this->filesystem->method('get') + ->willReturnMap([ + [$patch1->getPath(), 'Content '. $patch1->getId()], + [$patch2->getPath(), 'Content '. $patch2->getId()], + [$patch3->getPath(), 'Content '. $patch3->getId()] + ]); + + $this->applier->expects($this->exactly(3)) + ->method('status') + ->willReturnMap([ + ['Content ' . $patch1->getId(), StatusPool::NOT_APPLIED], + ['Content ' . $patch2->getId(), StatusPool::APPLIED], + ['Content ' . $patch3->getId(), StatusPool::NA] + ]); + + $expectedResult = [ + $patch1->getId() => StatusPool::NOT_APPLIED, + $patch2->getId() => StatusPool::APPLIED, + $patch3->getId() => StatusPool::NA + ]; + + $this->assertEquals($expectedResult, $this->resolver->resolve()); + } + + /** + * Tests a case when exception happens during reading patch content. + */ + public function testResolveWithException() + { + $patch = $this->createPatch('/path/patch.patch'); + + $this->localPool->expects($this->once()) + ->method('getList') + ->willReturn([$patch]); + + $this->filesystem->method('get') + ->willThrowException(new FileSystemException('')); + + $this->expectException(StatusResolverException::class); + $this->resolver->resolve(); + } + + /** + * Creates patch mock. + * + * @param string $path + * @return Patch|MockObject + */ + private function createPatch(string $path) + { + $patch = $this->createMock(Patch::class); + $patch->method('getId')->willReturn(md5($path)); + $patch->method('getPath')->willReturn($path); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/Status/OptionalResolverTest.php b/src/Test/Unit/Patch/Status/OptionalResolverTest.php new file mode 100644 index 00000000..9789bd29 --- /dev/null +++ b/src/Test/Unit/Patch/Status/OptionalResolverTest.php @@ -0,0 +1,256 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Status; + +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\CloudPatches\Patch\Aggregator; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\Data\AggregatedPatch; +use Magento\CloudPatches\Patch\Data\Patch; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\Status\OptionalResolver; +use Magento\CloudPatches\Patch\Status\StatusPool; +use Magento\CloudPatches\Patch\Status\StatusResolverException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class OptionalResolverTest extends TestCase +{ + /** + * @var Filesystem|MockObject + */ + private $filesystem; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var Aggregator|MockObject + */ + private $aggregator; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @var OptionalResolver + */ + private $resolver; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->filesystem = $this->createMock(Filesystem::class); + $this->applier = $this->createMock(Applier::class); + $this->aggregator = $this->createMock(Aggregator::class); + $this->optionalPool = $this->createMock(OptionalPool::class); + + $this->resolver = new OptionalResolver( + $this->filesystem, + $this->applier, + $this->aggregator, + $this->optionalPool + ); + } + + /** + * Tests resolving patch statuses for patches without dependencies. + */ + public function testResolveForIndependentPatches() + { + $patch1 = $this->createPatch('MC-1'); + $patch2 = $this->createPatch('MC-2'); + $patch3 = $this->createPatch('MC-3'); + + $patchMock = $this->getMockForAbstractClass(PatchInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getList') + ->willReturn([$patchMock]); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1, $patch2, $patch3]); + + $this->filesystem->method('get') + ->willReturnMap([ + ['path/' . $patch1->getId(), 'Content '. $patch1->getId()], + ['path/' . $patch2->getId(), 'Content '. $patch2->getId()], + ['path/' . $patch3->getId(), 'Content '. $patch3->getId()], + ]); + + $this->applier->expects($this->exactly(3)) + ->method('status') + ->willReturnMap([ + ['Content ' . $patch1->getId(), StatusPool::APPLIED], + ['Content ' . $patch2->getId(), StatusPool::NOT_APPLIED], + ['Content ' . $patch3->getId(), StatusPool::NA], + ]); + + $expectedResult = [ + $patch1->getId() => StatusPool::APPLIED, + $patch2->getId() => StatusPool::NOT_APPLIED, + $patch3->getId() => StatusPool::NA, + ]; + + $this->assertEquals($expectedResult, $this->resolver->resolve()); + } + + /** + * Tests resolving patch statuses for patches with dependencies. + * + * Status is defined using combined patch that contains all not applied dependencies. + */ + public function testResolveForDependentPatches() + { + $patch1 = $this->createPatch('MC-1'); + $patch2 = $this->createPatch('MC-2', ['MC-1']); + $patch3 = $this->createPatch('MC-3', ['MC-2']); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1, $patch2, $patch3]); + + $this->filesystem->method('get') + ->willReturnMap([ + ['path/' . $patch1->getId(), 'Content '. $patch1->getId()], + ['path/' . $patch2->getId(), 'Content '. $patch2->getId()], + ['path/' . $patch3->getId(), 'Content '. $patch3->getId()], + ]); + + $this->optionalPool->method('getList') + ->willReturnMap([ + [[], true, []], + [[$patch2->getId()], true, [$patch1->getItems()[0], $patch2->getItems()[0]]], + [[$patch3->getId()], true, [$patch1->getItems()[0], $patch2->getItems()[0], $patch3->getItems()[0]]], + ]); + + $contentPatch1 = 'Content ' . $patch1->getId(); + $contentPatch2 = 'Content ' . $patch2->getId(); + $contentPatch3 = 'Content ' . $patch3->getId(); + $this->applier->expects($this->exactly(3)) + ->method('status') + ->willReturnMap([ + [$contentPatch1, StatusPool::NOT_APPLIED], + [$contentPatch1 . $contentPatch2, StatusPool::NOT_APPLIED], + [$contentPatch1 . $contentPatch2 . $contentPatch3, StatusPool::APPLIED], + ]); + + $expectedResult = [ + $patch1->getId() => StatusPool::NOT_APPLIED, + $patch2->getId() => StatusPool::NOT_APPLIED, + $patch3->getId() => StatusPool::APPLIED, + ]; + + $this->assertEquals($expectedResult, $this->resolver->resolve()); + } + + /** + * Tests resolving patch statuses for conflicting cases, + * when status can be defined only after analysis of applied dependencies. + */ + public function testResolveForDependentPatchesWithConflicts() + { + $patch1 = $this->createPatch('MC-1'); + $patch2 = $this->createPatch('MC-2', ['MC-1']); + $patch3 = $this->createPatch('MC-3', ['MC-2']); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1, $patch2, $patch3]); + + $this->filesystem->method('get') + ->willReturnMap([ + ['path/' . $patch1->getId(), 'Content '. $patch1->getId()], + ['path/' . $patch2->getId(), 'Content '. $patch2->getId()], + ['path/' . $patch3->getId(), 'Content '. $patch3->getId()], + ]); + + $this->optionalPool->method('getList') + ->willReturnMap([ + [[], true, []], + [[$patch2->getId()], true, [$patch1->getItems()[0], $patch2->getItems()[0]]], + [[$patch3->getId()], true, [$patch1->getItems()[0], $patch2->getItems()[0], $patch3->getItems()[0]]], + ]); + + $contentPatch1 = 'Content ' . $patch1->getId(); + $contentPatch2 = 'Content ' . $patch2->getId(); + $contentPatch3 = 'Content ' . $patch3->getId(); + $this->applier->expects($this->exactly(3)) + ->method('status') + ->willReturnMap([ + [$contentPatch1, StatusPool::NA], + [$contentPatch2, StatusPool::NA], + [$contentPatch3, StatusPool::APPLIED], + ]); + + $expectedResult = [ + $patch1->getId() => StatusPool::APPLIED, + $patch2->getId() => StatusPool::APPLIED, + $patch3->getId() => StatusPool::APPLIED, + ]; + + $this->assertEquals($expectedResult, $this->resolver->resolve()); + } + + /** + * Tests a case when exception happens during reading patch content. + */ + public function testResolveWithException() + { + $patch1 = $this->createPatch('MC-1'); + + $patchMock = $this->getMockForAbstractClass(PatchInterface::class); + $this->optionalPool->expects($this->once()) + ->method('getList') + ->willReturn([$patchMock]); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1]); + + $this->filesystem->method('get') + ->willThrowException(new FileSystemException('')); + + $this->expectException(StatusResolverException::class); + $this->resolver->resolve(); + } + + /** + * Creates patch mock. + * + * @param string $id + * @param array $require + * @return AggregatedPatch|MockObject + */ + private function createPatch(string $id, array $require = []) + { + $patch = $this->createMock(Patch::class); + $patch->method('getId')->willReturn($id); + $patch->method('getPath')->willReturn('path/' . $id); + $aggregatedPatch = $this->createMock(AggregatedPatch::class); + $aggregatedPatch->method('getId')->willReturn($id); + $aggregatedPatch->method('getRequire')->willReturn($require); + $aggregatedPatch->method('getItems')->willReturn([$patch]); + + // To make mock object unique for assertions and array operations. + $aggregatedPatch->id = microtime(); + + return $aggregatedPatch; + } +} diff --git a/src/Test/Unit/Patch/Status/StatusPoolTest.php b/src/Test/Unit/Patch/Status/StatusPoolTest.php new file mode 100644 index 00000000..600481e6 --- /dev/null +++ b/src/Test/Unit/Patch/Status/StatusPoolTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Status; + +use Magento\CloudPatches\Patch\Status\ResolverInterface; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class StatusPoolTest extends TestCase +{ + /** + * Tests retrieving patch statuses. + */ + public function testStatusGet() + { + $result1 = ['MC-1' => StatusPool::APPLIED, 'MC-2' => StatusPool::NOT_APPLIED]; + $resolver1 = $this->getMockForAbstractClass(ResolverInterface::class); + $resolver1->method('resolve')->willReturn($result1); + + $result2 = ['MC-3' => StatusPool::APPLIED, 'MC-4' => StatusPool::NA]; + $resolver2 = $this->getMockForAbstractClass(ResolverInterface::class); + $resolver2->method('resolve')->willReturn($result2); + + $statusPool = new StatusPool([$resolver1, $resolver2]); + + $this->assertEquals(StatusPool::APPLIED, $statusPool->get('MC-1')); + $this->assertEquals(StatusPool::NOT_APPLIED, $statusPool->get('MC-2')); + $this->assertEquals(StatusPool::APPLIED, $statusPool->get('MC-3')); + $this->assertEquals(StatusPool::NA, $statusPool->get('MC-4')); + $this->assertEquals(StatusPool::NA, $statusPool->get('NotExistingId')); + $this->assertEquals(true, $statusPool->isNotApplied('MC-2')); + $this->assertEquals(true, $statusPool->isApplied('MC-3')); + $this->assertEquals(false, $statusPool->isNotApplied('MC-3')); + } + + /** + * Tests a case when exception happens after an instantiating status pool with the wrong resolver. + */ + public function testResolveWithException() + { + $invalidResolver = new \stdClass(); + + $this->expectException(\InvalidArgumentException::class); + new StatusPool([$invalidResolver]); + } +} From dc08373c62eb547906eda7551d6710e60fa1c445 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Mon, 20 Jul 2020 09:28:29 -0500 Subject: [PATCH 046/157] MCLOUD-6455: Increase test coverage for magento-cloud-patches and quality-patches (#58) --- .travis.yml | 10 ++++++ codeception.dist.yml | 1 + .../Functional/Acceptance/AbstractCest.php | 27 +++++++++++++--- .../Acceptance/Acceptance71CeCest.php | 2 +- .../Acceptance/Acceptance71CePart2Cest.php | 19 ++++++++++++ .../Acceptance/Acceptance71Cest.php | 10 +----- .../Acceptance/Acceptance71Part2Cest.php | 31 +++++++++++++++++++ .../Acceptance/Acceptance72Cest.php | 3 +- .../Functional/Acceptance/AcceptanceCest.php | 19 ++++++------ .../patches/.apply_quality_patches.env.yaml | 4 +++ tests/travis/functional-ce.sh | 2 +- tests/travis/functional-ee.sh | 2 +- 12 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 src/Test/Functional/Acceptance/Acceptance71CePart2Cest.php create mode 100644 src/Test/Functional/Acceptance/Acceptance71Part2Cest.php create mode 100644 tests/functional/_data/files/patches/.apply_quality_patches.env.yaml diff --git a/.travis.yml b/.travis.yml index 890b222f..9c62b41e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,6 +39,11 @@ jobs: php: '7.1' env: - TEST_SUITE=functional-ee + - PART_INDEX=1 + - php: '7.1' + env: + - TEST_SUITE=functional-ee + - PART_INDEX=2 - php: '7.2' env: - TEST_SUITE=functional-ee @@ -48,6 +53,11 @@ jobs: - php: '7.1' env: - TEST_SUITE=functional-ce + - PART_INDEX=1 + - php: '7.1' + env: + - TEST_SUITE=functional-ce + - PART_INDEX=2 - php: '7.2' env: - TEST_SUITE=functional-ce diff --git a/codeception.dist.yml b/codeception.dist.yml index 2badeb23..fe81096b 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -21,6 +21,7 @@ modules: mcc_repo: "https://github.com/magento/magento-cloud-components.git" mcp_repo: "https://github.com/magento/magento-cloud-patches.git" mqp_repo: "https://github.com/magento/quality-patches.git" + ece_tools_repo: "https://github.com/magento/ece-tools.git" composer_magento_username: "%REPO_USERNAME%" composer_magento_password: "%REPO_PASSWORD%" composer_github_token: "%GITHUB_TOKEN%" diff --git a/src/Test/Functional/Acceptance/AbstractCest.php b/src/Test/Functional/Acceptance/AbstractCest.php index a5c9eeab..64f489b9 100644 --- a/src/Test/Functional/Acceptance/AbstractCest.php +++ b/src/Test/Functional/Acceptance/AbstractCest.php @@ -28,8 +28,9 @@ public function _before(\CliTester $I): void /** * @param \CliTester $I * @param string $templateVersion + * @param string $magentoVersion */ - protected function prepareTemplate(\CliTester $I, string $templateVersion): void + protected function prepareTemplate(\CliTester $I, string $templateVersion, string $magentoVersion = null): void { $I->cloneTemplateToWorkDir($templateVersion); $I->createAuthJson(); @@ -38,6 +39,7 @@ protected function prepareTemplate(\CliTester $I, string $templateVersion): void $I->addArtifactsRepoToComposer(); $I->addEceDockerGitRepoToComposer(); $I->addQualityPatchesGitRepoToComposer(); + $I->addEceToolsGitRepoToComposer(); $I->addDependencyToComposer('magento/magento-cloud-patches', '1.0.99'); $I->addDependencyToComposer( 'magento/magento-cloud-docker', @@ -48,11 +50,15 @@ protected function prepareTemplate(\CliTester $I, string $templateVersion): void $I->getDependencyVersion('magento/quality-patches') ); - if ($this->edition === 'CE') { - $version = $this->getVersionRangeForMagento($I); + $I->addDependencyToComposer('magento/ece-tools', 'dev-develop as 2002.1.99'); + + if ($this->edition === 'CE' || $magentoVersion) { + $version = $magentoVersion ?: $this->getVersionRangeForMagento($I); $I->removeDependencyFromComposer('magento/magento-cloud-metapackage'); - $I->addDependencyToComposer('magento/ece-tools', '^2002.1.0'); - $I->addDependencyToComposer('magento/product-community-edition', $version); + $I->addDependencyToComposer( + $this->edition === 'CE' ? 'magento/product-community-edition' : 'magento/product-enterprise-edition', + $version + ); } $I->composerUpdate(); @@ -67,6 +73,17 @@ protected function convertEnvFromArrayToJson(array $data): string return addslashes(json_encode($data)); } + /** + * @param \CliTester $I + * @return string + */ + protected function getVersionRangeForMagento(\CliTester $I): string + { + $composer = json_decode(file_get_contents($I->getWorkDirPath() . '/composer.json'), true); + + return $composer['require']['magento/magento-cloud-metapackage'] ?? ''; + } + /** * @param \CliTester $I */ diff --git a/src/Test/Functional/Acceptance/Acceptance71CeCest.php b/src/Test/Functional/Acceptance/Acceptance71CeCest.php index 26f2d775..11d68a04 100644 --- a/src/Test/Functional/Acceptance/Acceptance71CeCest.php +++ b/src/Test/Functional/Acceptance/Acceptance71CeCest.php @@ -8,7 +8,7 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php71ce + * @group php71cePart1 */ class Acceptance71CeCest extends Acceptance71Cest { diff --git a/src/Test/Functional/Acceptance/Acceptance71CePart2Cest.php b/src/Test/Functional/Acceptance/Acceptance71CePart2Cest.php new file mode 100644 index 00000000..9b9a8e2c --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance71CePart2Cest.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php71cePart2 + */ +class Acceptance71CePart2Cest extends Acceptance71Part2Cest +{ + /** + * @var string + */ + protected $edition = 'CE'; +} diff --git a/src/Test/Functional/Acceptance/Acceptance71Cest.php b/src/Test/Functional/Acceptance/Acceptance71Cest.php index 0feb63ca..d4acfcc5 100644 --- a/src/Test/Functional/Acceptance/Acceptance71Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance71Cest.php @@ -8,7 +8,7 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php71 + * @group php71Part1 */ class Acceptance71Cest extends AcceptanceCest { @@ -25,14 +25,6 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.2.1'], ['templateVersion' => '2.2.2'], ['templateVersion' => '2.2.3'], - ['templateVersion' => '2.2.4'], - ['templateVersion' => '2.2.5'], - ['templateVersion' => '2.2.6'], - ['templateVersion' => '2.2.7'], - ['templateVersion' => '2.2.8'], - ['templateVersion' => '2.2.9'], - ['templateVersion' => '2.2.10'], - ['templateVersion' => '2.2.11'], ]; } } diff --git a/src/Test/Functional/Acceptance/Acceptance71Part2Cest.php b/src/Test/Functional/Acceptance/Acceptance71Part2Cest.php new file mode 100644 index 00000000..87df8566 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance71Part2Cest.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php71Part2 + */ +class Acceptance71Part2Cest extends AcceptanceCest +{ + /** + * @return array + */ + protected function patchesDataProvider(): array + { + return [ + ['templateVersion' => '2.2.4'], + ['templateVersion' => '2.2.5'], + ['templateVersion' => '2.2.6'], + ['templateVersion' => '2.2.7'], + ['templateVersion' => '2.2.8'], + ['templateVersion' => '2.2.9'], + ['templateVersion' => '2.2.10'], + ['templateVersion' => '2.2.11'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/Acceptance72Cest.php b/src/Test/Functional/Acceptance/Acceptance72Cest.php index 0eb28f0b..956eba26 100644 --- a/src/Test/Functional/Acceptance/Acceptance72Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance72Cest.php @@ -20,7 +20,8 @@ protected function patchesDataProvider(): array return [ ['templateVersion' => '2.3.0'], ['templateVersion' => '2.3.1'], - ['templateVersion' => '2.3.2'], + ['templateVersion' => '2.3.2', 'magentoVersion' => '2.3.2'], + ['templateVersion' => '2.3.2', 'magentoVersion' => '2.3.2-p2'], ]; } } diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 6e3ae99e..f0fd0485 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -14,13 +14,10 @@ class AcceptanceCest extends AbstractCest { /** * @param \CliTester $I - * @return string */ - protected function getVersionRangeForMagento(\CliTester $I): string + public function _before(\CliTester $I): void { - $composer = json_decode(file_get_contents($I->getWorkDirPath() . '/composer.json'), true); - - return $composer['require']['magento/magento-cloud-metapackage'] ?? ''; + parent::_before($I); } /** @@ -31,7 +28,8 @@ protected function getVersionRangeForMagento(\CliTester $I): string */ public function testPatches(\CliTester $I, \Codeception\Example $data): void { - $this->prepareTemplate($I, $data['templateVersion']); + $this->prepareTemplate($I, $data['templateVersion'], $data['magentoVersion'] ?? null); + $I->copyFileToWorkDir('files/patches/.apply_quality_patches.env.yaml', '.magento.env.yaml'); $I->runEceDockerCommand(sprintf( 'build:compose --mode=production --env-vars="%s"', $this->convertEnvFromArrayToJson(['MAGENTO_CLOUD_PROJECT' => 'travis-testing']) @@ -51,9 +49,12 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void protected function patchesDataProvider(): array { return [ - ['templateVersion' => '2.3.3'], - ['templateVersion' => '2.3.4'], - ['templateVersion' => '2.3.5'], + ['templateVersion' => '2.3.3', 'magentoVersion' => '2.3.3'], + ['templateVersion' => '2.3.3', 'magentoVersion' => '2.3.3-p1'], + ['templateVersion' => '2.3.4', 'magentoVersion' => '2.3.4'], + ['templateVersion' => '2.3.4', 'magentoVersion' => '2.3.4-p2'], + ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5'], + ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5-p1'], ['templateVersion' => 'master'], ]; } diff --git a/tests/functional/_data/files/patches/.apply_quality_patches.env.yaml b/tests/functional/_data/files/patches/.apply_quality_patches.env.yaml new file mode 100644 index 00000000..93fc5e5e --- /dev/null +++ b/tests/functional/_data/files/patches/.apply_quality_patches.env.yaml @@ -0,0 +1,4 @@ +stage: + build: + QUALITY_PATCHES: + - '*' diff --git a/tests/travis/functional-ce.sh b/tests/travis/functional-ce.sh index 6eedc70a..ab0e3f7f 100755 --- a/tests/travis/functional-ce.sh +++ b/tests/travis/functional-ce.sh @@ -8,7 +8,7 @@ trap '>&2 echo Error: Command \`$BASH_COMMAND\` on line $LINENO failed with exit case $TRAVIS_PHP_VERSION in 7.1) - ./vendor/bin/codecept run -g php71ce --steps + ./vendor/bin/codecept run -g php71cePart$PART_INDEX --steps ;; 7.2) ./vendor/bin/codecept run -g php72ce --steps diff --git a/tests/travis/functional-ee.sh b/tests/travis/functional-ee.sh index a0d12a11..467b6f11 100755 --- a/tests/travis/functional-ee.sh +++ b/tests/travis/functional-ee.sh @@ -8,7 +8,7 @@ trap '>&2 echo Error: Command \`$BASH_COMMAND\` on line $LINENO failed with exit case $TRAVIS_PHP_VERSION in 7.1) - ./vendor/bin/codecept run -g php71 --steps + ./vendor/bin/codecept run -g php71cePart$PART_INDEX --steps ;; 7.2) ./vendor/bin/codecept run -g php72 --steps From 72451669ad3d6b7b47b15aeeda0067c24cd717a1 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Mon, 27 Jul 2020 11:26:59 -0500 Subject: [PATCH 047/157] Magento Quality Patches - improvement of dev experience (#61) --- bin/ece-patches | 3 +- composer.json | 3 +- config/services.xml | 26 +-- src/ApplicationEce.php | 48 +++++ src/Command/Apply.php | 68 +----- src/Command/Ece/Apply.php | 113 ++++++++++ src/Command/Ece/Revert.php | 93 +++++++++ .../Process/Action/ApplyOptionalAction.php | 62 +----- src/Command/Process/ApplyLocal.php | 34 ++- src/Command/Process/ApplyOptional.php | 4 +- src/Command/Process/ApplyRequired.php | 23 +- src/Command/Process/Ece/ApplyOptional.php | 77 +++++++ src/Command/Process/Ece/Revert.php | 156 ++++++++++++++ src/Command/Process/Renderer.php | 46 +++- src/Command/Process/Revert.php | 4 +- src/Command/Revert.php | 33 +-- src/Command/Status.php | 2 +- src/Composer/MagentoVersion.php | 49 ++++- src/Composer/Package.php | 13 +- src/Composer/QualityPackage.php | 56 +++++ src/Environment/Config.php | 82 ++++++++ src/Environment/ConfigReader.php | 70 +++++++ src/Filesystem/FileList.php | 4 +- src/Patch/Applier.php | 86 +++++++- src/Patch/Collector/CloudCollector.php | 14 +- src/Patch/Collector/LocalCollector.php | 5 +- src/Patch/Collector/QualityCollector.php | 14 +- src/Patch/Conflict/Analyzer.php | 188 +++++++++++++++++ src/Patch/Conflict/ApplyChecker.php | 79 +++++++ src/Patch/Conflict/Processor.php | 93 +++++++++ src/Patch/Environment.php | 31 --- src/Patch/GitConverter.php | 126 +++++++++++ src/Patch/Pool/OptionalPool.php | 43 ++++ src/Patch/RollbackProcessor.php | 68 ++++++ src/Patch/SourceProvider.php | 16 +- src/Test/Unit/Command/ApplyTest.php | 89 +------- src/Test/Unit/Command/Ece/ApplyTest.php | 142 +++++++++++++ src/Test/Unit/Command/Ece/RevertTest.php | 119 +++++++++++ .../Action/ApplyOptionalActionTest.php | 25 ++- .../Unit/Command/Process/ApplyLocalTest.php | 37 +++- .../Command/Process/ApplyOptionalTest.php | 12 +- .../Command/Process/ApplyRequiredTest.php | 21 +- .../Command/Process/Ece/ApplyOptionalTest.php | 121 +++++++++++ .../Unit/Command/Process/Ece/RevertTest.php | 196 ++++++++++++++++++ .../Unit/Command/Process/RendererTest.php | 3 - src/Test/Unit/Command/Process/RevertTest.php | 4 +- src/Test/Unit/Command/RevertTest.php | 37 +--- src/Test/Unit/Composer/MagentoVersionTest.php | 155 +++++++++++++- .../Unit/Environment/ConfigReaderTest.php | 94 +++++++++ src/Test/Unit/Environment/ConfigTest.php | 86 ++++++++ .../Unit/Environment/_file/.magento.env.yaml | 5 + src/Test/Unit/Filesystem/FileListTest.php | 8 - src/Test/Unit/Patch/ApplierTest.php | 81 +++++++- .../Patch/Collector/CloudCollectorTest.php | 12 +- .../Patch/Collector/LocalCollectorTest.php | 8 +- .../Patch/Collector/QualityCollectorTest.php | 12 +- src/Test/Unit/Patch/Conflict/AnalyzerTest.php | 182 ++++++++++++++++ .../Unit/Patch/Conflict/ApplyCheckerTest.php | 105 ++++++++++ .../Unit/Patch/Conflict/ProcessorTest.php | 138 ++++++++++++ src/Test/Unit/Patch/EnvironmentTest.php | 31 --- src/Test/Unit/Patch/GitConverterTest.php | 87 ++++++++ src/Test/Unit/Patch/Pool/OptionalPoolTest.php | 46 ++++ src/Test/Unit/Patch/RollbackProcessorTest.php | 103 +++++++++ src/Test/Unit/Patch/SourceProviderTest.php | 33 ++- 64 files changed, 3352 insertions(+), 472 deletions(-) create mode 100644 src/ApplicationEce.php create mode 100644 src/Command/Ece/Apply.php create mode 100644 src/Command/Ece/Revert.php create mode 100644 src/Command/Process/Ece/ApplyOptional.php create mode 100644 src/Command/Process/Ece/Revert.php create mode 100644 src/Composer/QualityPackage.php create mode 100644 src/Environment/Config.php create mode 100644 src/Environment/ConfigReader.php create mode 100644 src/Patch/Conflict/Analyzer.php create mode 100644 src/Patch/Conflict/ApplyChecker.php create mode 100644 src/Patch/Conflict/Processor.php delete mode 100644 src/Patch/Environment.php create mode 100755 src/Patch/GitConverter.php create mode 100644 src/Patch/RollbackProcessor.php create mode 100644 src/Test/Unit/Command/Ece/ApplyTest.php create mode 100644 src/Test/Unit/Command/Ece/RevertTest.php create mode 100644 src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php create mode 100644 src/Test/Unit/Command/Process/Ece/RevertTest.php create mode 100644 src/Test/Unit/Environment/ConfigReaderTest.php create mode 100644 src/Test/Unit/Environment/ConfigTest.php create mode 100644 src/Test/Unit/Environment/_file/.magento.env.yaml create mode 100644 src/Test/Unit/Patch/Conflict/AnalyzerTest.php create mode 100644 src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php create mode 100644 src/Test/Unit/Patch/Conflict/ProcessorTest.php delete mode 100644 src/Test/Unit/Patch/EnvironmentTest.php create mode 100644 src/Test/Unit/Patch/GitConverterTest.php create mode 100644 src/Test/Unit/Patch/RollbackProcessorTest.php diff --git a/bin/ece-patches b/bin/ece-patches index 99d5e7dc..3efc8718 100755 --- a/bin/ece-patches +++ b/bin/ece-patches @@ -4,7 +4,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +define('IS_CLOUD', true); $container = require __DIR__ . '/../bootstrap.php'; -$application = new Magento\CloudPatches\Application($container); +$application = new Magento\CloudPatches\ApplicationEce($container); $application->run(); diff --git a/composer.json b/composer.json index 49d6fab5..5c08720b 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.5", + "version": "1.0.6", "license": "OSL-3.0", "require": { "php": "^7.0", @@ -14,6 +14,7 @@ "symfony/dependency-injection": "^3.3||^4.3", "symfony/process": "^2.1||^4.1", "symfony/proxy-manager-bridge": "^3.3||^4.3", + "symfony/yaml": "^3.3||^4.0", "monolog/monolog": "^1.16", "magento/quality-patches": "^1.0.0" }, diff --git a/config/services.xml b/config/services.xml index 958607b9..52ca97c1 100644 --- a/config/services.xml +++ b/config/services.xml @@ -9,7 +9,6 @@ <service id="Psr\Log\LoggerInterface" alias="Magento\CloudPatches\App\Logger" /> <service id="Magento\CloudPatches\App\Container" autowire="false"/> <service id="Magento\CloudPatches\Filesystem\DirectoryList" autowire="false"/> - <service id="Magento\QualityPatches\Info"/> <service id="Symfony\Component\Console\Helper\QuestionHelper"/> <service id="Composer\Composer"/> <service id="Magento\CloudPatches\App\GenericException" autowire="false"/> @@ -28,11 +27,6 @@ <service id="Magento\CloudPatches\Patch\Pool\RequiredPool" lazy="true"/> <service id="Magento\CloudPatches\Patch\Pool\LocalPool" lazy="true"/> <service id="Magento\CloudPatches\Patch\Status\StatusPool" autowire="false"/> - <service id="statusOptionalPool" class="Magento\CloudPatches\Patch\Status\StatusPool" lazy="true"> - <argument key="$resolvers" type="collection"> - <argument type="service" id="Magento\CloudPatches\Patch\Status\OptionalResolver"/> - </argument> - </service> <service id="statusPool" class="Magento\CloudPatches\Patch\Status\StatusPool" lazy="true"> <argument key="$resolvers" type="collection"> <argument type="service" id="Magento\CloudPatches\Patch\Status\LocalResolver"/> @@ -42,26 +36,29 @@ <service id="Magento\CloudPatches\Command\Process\ShowStatus"> <argument key="$statusPool" type="service" id="statusPool"/> </service> + <service id="Magento\CloudPatches\Command\Process\Ece\Revert"> + <argument key="$statusPool" type="service" id="statusPool"/> + </service> <service id="Magento\CloudPatches\Command\Process\Action\ApplyOptionalAction"> - <argument key="$statusPool" type="service" id="statusOptionalPool"/> + <argument key="$statusPool" type="service" id="statusPool"/> </service> <service id="Magento\CloudPatches\Command\Process\Action\RevertAction"> - <argument key="$statusPool" type="service" id="statusOptionalPool"/> + <argument key="$statusPool" type="service" id="statusPool"/> </service> <service id="Magento\CloudPatches\Command\Process\Action\ConfirmRequiredAction"> - <argument key="$statusPool" type="service" id="statusOptionalPool"/> + <argument key="$statusPool" type="service" id="statusPool"/> </service> <service id="Magento\CloudPatches\Command\Process\Action\ProcessDeprecatedAction"> - <argument key="$statusPool" type="service" id="statusOptionalPool"/> + <argument key="$statusPool" type="service" id="statusPool"/> </service> <service id="Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction"> - <argument key="$statusPool" type="service" id="statusOptionalPool"/> + <argument key="$statusPool" type="service" id="statusPool"/> </service> <service id="Magento\CloudPatches\Patch\RevertValidator"> - <argument key="$statusPool" type="service" id="statusOptionalPool"/> + <argument key="$statusPool" type="service" id="statusPool"/> </service> <service id="Magento\CloudPatches\Command\Process\Renderer"> - <argument key="$statusPool" type="service" id="statusOptionalPool"/> + <argument key="$statusPool" type="service" id="statusPool"/> </service> <service id="Magento\CloudPatches\Command\Process\Action\ActionPool" autowire="false"/> <service id="ApplyOptionalActionPool" class="Magento\CloudPatches\Command\Process\Action\ActionPool"> @@ -75,6 +72,9 @@ <service id="Magento\CloudPatches\Command\Process\ApplyOptional"> <argument key="$actionPool" type="service" id="ApplyOptionalActionPool"/> </service> + <service id="Magento\CloudPatches\Command\Process\Ece\ApplyOptional"> + <argument key="$actionPool" type="service" id="ApplyOptionalActionPool"/> + </service> <service id="Magento\CloudPatches\Patch\PatchBuilder" shared="false"/> </services> </container> diff --git a/src/ApplicationEce.php b/src/ApplicationEce.php new file mode 100644 index 00000000..fc6e4c2e --- /dev/null +++ b/src/ApplicationEce.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches; + +use Composer\Composer; +use Magento\CloudPatches\Command; +use Psr\Container\ContainerInterface; + +/** + * @inheritdoc + */ +class ApplicationEce extends \Symfony\Component\Console\Application +{ + /** + * @var ContainerInterface + */ + private $container; + + /** + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + + parent::__construct( + $container->get(Composer::class)->getPackage()->getPrettyName(), + $container->get(Composer::class)->getPackage()->getPrettyVersion() + ); + } + + /** + * @inheritdoc + */ + protected function getDefaultCommands() + { + return array_merge(parent::getDefaultCommands(), [ + $this->container->get(Command\Ece\Apply::class), + $this->container->get(Command\Ece\Revert::class), + $this->container->get(Command\Status::class) + ]); + } +} diff --git a/src/Command/Apply.php b/src/Command/Apply.php index 3c681af6..ce77f79c 100644 --- a/src/Command/Apply.php +++ b/src/Command/Apply.php @@ -8,19 +8,15 @@ namespace Magento\CloudPatches\Command; use Magento\CloudPatches\App\RuntimeException; -use Magento\CloudPatches\Command\Process\ApplyLocal; use Magento\CloudPatches\Command\Process\ApplyOptional; -use Magento\CloudPatches\Command\Process\ApplyRequired; use Magento\CloudPatches\Composer\MagentoVersion; -use Magento\CloudPatches\Patch\Environment; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** - * Patch apply command. + * Patch apply command (OnPrem). */ class Apply extends AbstractCommand { @@ -30,35 +26,15 @@ class Apply extends AbstractCommand const NAME = 'apply'; /** - * Defines whether Magento is installed from Git. + * List of patches to apply. */ - const OPT_GIT_INSTALLATION = 'git-installation'; - - /** - * List of quality patches to apply. - */ - const ARG_QUALITY_PATCHES = 'quality-patches'; + const ARG_LIST_OF_PATCHES = 'list-of-patches'; /** * @var ApplyOptional */ private $applyOptional; - /** - * @var ApplyRequired - */ - private $applyRequired; - - /** - * @var ApplyLocal - */ - private $applyLocal; - - /** - * @var Environment - */ - private $environment; - /** * @var LoggerInterface */ @@ -70,25 +46,16 @@ class Apply extends AbstractCommand private $magentoVersion; /** - * @param ApplyRequired $applyRequired * @param ApplyOptional $applyOptional - * @param ApplyLocal $applyLocal - * @param Environment $environment * @param LoggerInterface $logger * @param MagentoVersion $magentoVersion */ public function __construct( - ApplyRequired $applyRequired, ApplyOptional $applyOptional, - ApplyLocal $applyLocal, - Environment $environment, LoggerInterface $logger, MagentoVersion $magentoVersion ) { - $this->applyRequired = $applyRequired; $this->applyOptional = $applyOptional; - $this->applyLocal = $applyLocal; - $this->environment = $environment; $this->logger = $logger; $this->magentoVersion = $magentoVersion; @@ -101,17 +68,11 @@ public function __construct( protected function configure() { $this->setName(self::NAME) - ->setDescription('Apply patches') + ->setDescription('Applies patches. The list of patches should pass as a command argument') ->addArgument( - self::ARG_QUALITY_PATCHES, - InputArgument::IS_ARRAY, - 'List of quality patches to apply' - )->addOption( - self::OPT_GIT_INSTALLATION, - null, - InputOption::VALUE_OPTIONAL, - 'Is git installation', - false + self::ARG_LIST_OF_PATCHES, + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'List of patches to apply' ); parent::configure(); @@ -122,23 +83,10 @@ protected function configure() */ public function execute(InputInterface $input, OutputInterface $output) { - $deployedFromGit = $input->getOption(Apply::OPT_GIT_INSTALLATION); - if ($deployedFromGit) { - $output->writeln('<info>Git-based installation. Skipping patches applying.</info>'); - - return self::RETURN_SUCCESS; - } - $this->logger->notice($this->magentoVersion->get()); try { - if ($this->environment->isCloud()) { - $this->applyRequired->run($input, $output); - $this->applyOptional->run($input, $output); - $this->applyLocal->run($input, $output); - } else { - $this->applyOptional->run($input, $output); - } + $this->applyOptional->run($input, $output); } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); $this->logger->error($e->getMessage()); diff --git a/src/Command/Ece/Apply.php b/src/Command/Ece/Apply.php new file mode 100644 index 00000000..6090a384 --- /dev/null +++ b/src/Command/Ece/Apply.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Command\Ece; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\AbstractCommand; +use Magento\CloudPatches\Command\Process\ApplyLocal; +use Magento\CloudPatches\Command\Process\Ece\ApplyOptional; +use Magento\CloudPatches\Command\Process\ApplyRequired; +use Magento\CloudPatches\Composer\MagentoVersion; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Patch apply command (Cloud). + */ +class Apply extends AbstractCommand +{ + /** + * Command name. + */ + const NAME = 'apply'; + + /** + * @var ApplyOptional + */ + private $applyOptional; + + /** + * @var ApplyRequired + */ + private $applyRequired; + + /** + * @var ApplyLocal + */ + private $applyLocal; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var MagentoVersion + */ + private $magentoVersion; + + /** + * @param ApplyRequired $applyRequired + * @param ApplyOptional $applyOptional + * @param ApplyLocal $applyLocal + * @param LoggerInterface $logger + * @param MagentoVersion $magentoVersion + */ + public function __construct( + ApplyRequired $applyRequired, + ApplyOptional $applyOptional, + ApplyLocal $applyLocal, + LoggerInterface $logger, + MagentoVersion $magentoVersion + ) { + $this->applyRequired = $applyRequired; + $this->applyOptional = $applyOptional; + $this->applyLocal = $applyLocal; + $this->logger = $logger; + $this->magentoVersion = $magentoVersion; + + parent::__construct(self::NAME); + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName(self::NAME) + ->setDescription('Applies patches (Magento Cloud only)'); + + parent::configure(); + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $this->logger->notice($this->magentoVersion->get()); + + try { + $this->applyRequired->run($input, $output); + $this->applyOptional->run($input, $output); + $this->applyLocal->run($input, $output); + } catch (RuntimeException $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + $this->logger->error($e->getMessage()); + + return self::RETURN_FAILURE; + } catch (\Exception $e) { + $this->logger->critical($e); + + throw $e; + } + + return self::RETURN_SUCCESS; + } +} diff --git a/src/Command/Ece/Revert.php b/src/Command/Ece/Revert.php new file mode 100644 index 00000000..bd8cde31 --- /dev/null +++ b/src/Command/Ece/Revert.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Command\Ece; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\AbstractCommand; +use Magento\CloudPatches\Command\Process\Ece\Revert as RevertProcess; +use Magento\CloudPatches\Composer\MagentoVersion; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Patch revert command (Cloud). + */ +class Revert extends AbstractCommand +{ + /** + * Command name. + */ + const NAME = 'revert'; + + /** + * @var RevertProcess + */ + private $revert; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var MagentoVersion + */ + private $magentoVersion; + + /** + * @param RevertProcess $revert + * @param LoggerInterface $logger + * @param MagentoVersion $magentoVersion + */ + public function __construct( + RevertProcess $revert, + LoggerInterface $logger, + MagentoVersion $magentoVersion + ) { + $this->revert = $revert; + $this->logger = $logger; + $this->magentoVersion = $magentoVersion; + + parent::__construct(self::NAME); + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName(self::NAME) + ->setDescription('Reverts patches (Magento Cloud only)'); + + parent::configure(); + } + + /** + * {@inheritDoc} + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $this->logger->notice($this->magentoVersion->get()); + + try { + $this->revert->run($input, $output); + } catch (RuntimeException $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + $this->logger->error($e->getMessage()); + + return self::RETURN_FAILURE; + } catch (\Exception $e) { + $this->logger->critical($e); + + throw $e; + } + + return self::RETURN_SUCCESS; + } +} diff --git a/src/Command/Process/Action/ApplyOptionalAction.php b/src/Command/Process/Action/ApplyOptionalAction.php index 5bc6a95f..461d6c32 100644 --- a/src/Command/Process/Action/ApplyOptionalAction.php +++ b/src/Command/Process/Action/ApplyOptionalAction.php @@ -9,6 +9,7 @@ use Magento\CloudPatches\App\RuntimeException; use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor; use Magento\CloudPatches\Patch\Pool\OptionalPool; use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; @@ -51,25 +52,33 @@ class ApplyOptionalAction implements ActionInterface */ private $logger; + /** + * @var ConflictProcessor + */ + private $conflictProcessor; + /** * @param Applier $applier * @param OptionalPool $optionalPool * @param StatusPool $statusPool * @param Renderer $renderer * @param LoggerInterface $logger + * @param ConflictProcessor $conflictProcessor */ public function __construct( Applier $applier, OptionalPool $optionalPool, StatusPool $statusPool, Renderer $renderer, - LoggerInterface $logger + LoggerInterface $logger, + ConflictProcessor $conflictProcessor ) { $this->applier = $applier; $this->optionalPool = $optionalPool; $this->statusPool = $statusPool; $this->renderer = $renderer; $this->logger = $logger; + $this->conflictProcessor = $conflictProcessor; } /** @@ -93,13 +102,7 @@ public function execute(InputInterface $input, OutputInterface $output, array $p $this->logger->info($message, ['file' => $patch->getPath()]); array_push($appliedPatches, $patch); } catch (ApplierException $exception) { - $this->printPatchApplyingFailed($output, $patch, $exception->getMessage()); - $this->rollback($output, $appliedPatches); - - throw new RuntimeException( - 'Applying optional patches ' . implode(' ', $patchFilter) . ' failed.', - $exception->getCode() - ); + $this->conflictProcessor->process($output, $patch, $appliedPatches, $exception->getMessage()); } } } @@ -124,28 +127,6 @@ private function printPatchWasApplied(OutputInterface $output, PatchInterface $p $this->logger->info($message); } - /** - * Prints and logs 'applying patch failed' message. - * - * @param OutputInterface $output - * @param PatchInterface $patch - * @param string $errorOutput - * - * @return void - */ - private function printPatchApplyingFailed(OutputInterface $output, PatchInterface $patch, string $errorOutput) - { - $errorMessage = sprintf( - 'Applying patch %s (%s) failed.%s', - $patch->getId(), - $patch->getPath(), - $this->renderer->formatErrorOutput($errorOutput) - ); - - $output->writeln('<error>' . $errorMessage . '</error>' . PHP_EOL); - $this->logger->error($errorMessage); - } - /** * Returns a list of patches according to the filter. * @@ -171,25 +152,4 @@ function ($patch) { throw new RuntimeException($e->getMessage(), $e->getCode()); } } - - /** - * Rollback applied patches. - * - * @param OutputInterface $output - * @param PatchInterface[] $appliedPatches - * - * @return void - */ - private function rollback(OutputInterface $output, array $appliedPatches) - { - $this->logger->info('Start rollback'); - - foreach (array_reverse($appliedPatches) as $appliedPatch) { - $message = $this->applier->revert($appliedPatch->getPath(), $appliedPatch->getId()); - $this->renderer->printPatchInfo($output, $appliedPatch, $message); - $this->logger->info($message, ['file' => $appliedPatch->getPath()]); - } - - $this->logger->info('End rollback'); - } } diff --git a/src/Command/Process/ApplyLocal.php b/src/Command/Process/ApplyLocal.php index e1418cf3..3ba8ebde 100644 --- a/src/Command/Process/ApplyLocal.php +++ b/src/Command/Process/ApplyLocal.php @@ -11,6 +11,7 @@ use Magento\CloudPatches\Patch\Pool\LocalPool; use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\RollbackProcessor; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -40,22 +41,30 @@ class ApplyLocal implements ProcessInterface */ private $logger; + /** + * @var RollbackProcessor + */ + private $rollbackProcessor; + /** * @param Applier $applier - * @param LocalPool $localPatchPool + * @param LocalPool $localPool * @param Renderer $renderer * @param LoggerInterface $logger + * @param RollbackProcessor $rollbackProcessor */ public function __construct( Applier $applier, - LocalPool $localPatchPool, + LocalPool $localPool, Renderer $renderer, - LoggerInterface $logger + LoggerInterface $logger, + RollbackProcessor $rollbackProcessor ) { $this->applier = $applier; - $this->localPool = $localPatchPool; + $this->localPool = $localPool; $this->renderer = $renderer; $this->logger = $logger; + $this->rollbackProcessor = $rollbackProcessor; } /** @@ -73,11 +82,16 @@ public function run(InputInterface $input, OutputInterface $output) $this->logger->notice('Start of applying hot-fixes'); $output->writeln('<info>Applying hot-fixes</info>'); + $appliedPatches = []; foreach ($patches as $patch) { try { $message = $this->applier->apply($patch->getPath(), $patch->getTitle()); $this->printInfo($output, $message); + array_push($appliedPatches, $patch); } catch (ApplierException $exception) { + $this->printError($output, 'Error: patch conflict happened'); + $messages = $this->rollbackProcessor->process($appliedPatches); + $output->writeln($messages); $errorMessage = sprintf( 'Applying patch %s failed.%s', $patch->getPath(), @@ -102,4 +116,16 @@ private function printInfo(OutputInterface $output, string $message) $output->writeln('<info>' . $message . '</info>'); $this->logger->info($message); } + + /** + * Prints and logs error message. + * + * @param OutputInterface $output + * @param string $message + */ + private function printError(OutputInterface $output, string $message) + { + $output->writeln('<error>' . $message . '</error>'); + $this->logger->error($message); + } } diff --git a/src/Command/Process/ApplyOptional.php b/src/Command/Process/ApplyOptional.php index d4b8ce17..92757360 100644 --- a/src/Command/Process/ApplyOptional.php +++ b/src/Command/Process/ApplyOptional.php @@ -15,7 +15,7 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * Applies optional patches (Cloud & OnPrem). + * Applies optional patches (OnPrem). */ class ApplyOptional implements ProcessInterface { @@ -54,7 +54,7 @@ public function __construct( */ public function run(InputInterface $input, OutputInterface $output) { - $argPatches = $input->getArgument(Apply::ARG_QUALITY_PATCHES); + $argPatches = $input->getArgument(Apply::ARG_LIST_OF_PATCHES); $patchFilter = $this->filterFactory->createApplyFilter($argPatches); if ($patchFilter === null) { return; diff --git a/src/Command/Process/ApplyRequired.php b/src/Command/Process/ApplyRequired.php index 51f057d6..f4d5eed2 100644 --- a/src/Command/Process/ApplyRequired.php +++ b/src/Command/Process/ApplyRequired.php @@ -7,7 +7,7 @@ namespace Magento\CloudPatches\Command\Process; -use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor; use Magento\CloudPatches\Patch\Pool\RequiredPool; use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; @@ -42,22 +42,30 @@ class ApplyRequired implements ProcessInterface */ private $logger; + /** + * @var ConflictProcessor + */ + private $conflictProcessor; + /** * @param Applier $applier * @param RequiredPool $requiredPool * @param Renderer $renderer * @param LoggerInterface $logger + * @param ConflictProcessor $conflictProcessor */ public function __construct( Applier $applier, RequiredPool $requiredPool, Renderer $renderer, - LoggerInterface $logger + LoggerInterface $logger, + ConflictProcessor $conflictProcessor ) { $this->applier = $applier; $this->requiredPool = $requiredPool; $this->renderer = $renderer; $this->logger = $logger; + $this->conflictProcessor = $conflictProcessor; } /** @@ -67,21 +75,16 @@ public function run(InputInterface $input, OutputInterface $output) { $this->logger->notice('Start of applying required patches'); + $appliedPatches = []; $patches = $this->requiredPool->getList(); foreach ($patches as $patch) { try { $message = $this->applier->apply($patch->getPath(), $patch->getId()); $this->renderer->printPatchInfo($output, $patch, $message); $this->logger->info($message, ['file' => $patch->getPath()]); + array_push($appliedPatches, $patch); } catch (ApplierException $exception) { - $errorMessage = sprintf( - '<error>Applying patch %s %s failed.%s</error>', - $patch->getId(), - $patch->getPath(), - $this->renderer->formatErrorOutput($exception->getMessage()) - ); - - throw new RuntimeException($errorMessage, $exception->getCode()); + $this->conflictProcessor->process($output, $patch, $appliedPatches, $exception->getMessage()); } } diff --git a/src/Command/Process/Ece/ApplyOptional.php b/src/Command/Process/Ece/ApplyOptional.php new file mode 100644 index 00000000..dc02f1d4 --- /dev/null +++ b/src/Command/Process/Ece/ApplyOptional.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Command\Process\Ece; + +use Magento\CloudPatches\Command\Process\Action\ActionPool; +use Magento\CloudPatches\Command\Process\ProcessInterface; +use Magento\CloudPatches\Environment\Config; +use Magento\CloudPatches\Patch\FilterFactory; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Applies optional patches (Cloud). + */ +class ApplyOptional implements ProcessInterface +{ + /** + * @var FilterFactory + */ + private $filterFactory; + + /** + * @var ActionPool + */ + private $actionPool; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Config + */ + private $config; + + /** + * @param FilterFactory $filterFactory + * @param ActionPool $actionPool + * @param LoggerInterface $logger + * @param Config $config + */ + public function __construct( + FilterFactory $filterFactory, + ActionPool $actionPool, + LoggerInterface $logger, + Config $config + ) { + $this->filterFactory = $filterFactory; + $this->actionPool = $actionPool; + $this->logger = $logger; + $this->config = $config; + } + + /** + * @inheritDoc + */ + public function run(InputInterface $input, OutputInterface $output) + { + $envQualityPatches = $this->config->getQualityPatches(); + $patchFilter = $this->filterFactory->createApplyFilter($envQualityPatches); + if ($patchFilter === null) { + return; + } + + $this->logger->notice('Start of applying optional patches'); + $this->logger->info('QUALITY_PATCHES env variable: ' . implode(' ', $envQualityPatches)); + $this->actionPool->execute($input, $output, $patchFilter); + $this->logger->notice('End of applying optional patches'); + } +} diff --git a/src/Command/Process/Ece/Revert.php b/src/Command/Process/Ece/Revert.php new file mode 100644 index 00000000..b2b93b4e --- /dev/null +++ b/src/Command/Process/Ece/Revert.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Command\Process\Ece; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\RevertAction; +use Magento\CloudPatches\Command\Process\ProcessInterface; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Pool\LocalPool; +use Magento\CloudPatches\Patch\Status\StatusPool; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Reverts all patches (Cloud). + */ +class Revert implements ProcessInterface +{ + /** + * @var RevertAction + */ + private $revertAction; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Applier + */ + private $applier; + + /** + * @var LocalPool + */ + private $localPool; + + /** + * @var Renderer + */ + private $renderer; + + /** + * @var StatusPool + */ + private $statusPool; + + /** + * @param RevertAction $revertAction + * @param LoggerInterface $logger + * @param Applier $applier + * @param LocalPool $localPool + * @param Renderer $renderer + * @param StatusPool $statusPool + */ + public function __construct( + RevertAction $revertAction, + LoggerInterface $logger, + Applier $applier, + LocalPool $localPool, + Renderer $renderer, + StatusPool $statusPool + ) { + $this->revertAction = $revertAction; + $this->logger = $logger; + $this->applier = $applier; + $this->localPool = $localPool; + $this->renderer = $renderer; + $this->statusPool = $statusPool; + } + + /** + * @inheritDoc + */ + public function run(InputInterface $input, OutputInterface $output) + { + $this->logger->notice('Start of reverting all patches'); + + $this->revertLocalPatches($output); + $this->revertAction->execute($input, $output, []); + + $this->logger->notice('End of reverting all patches'); + } + + /** + * Reverts local custom patches. + + * @param OutputInterface $output + * @return void + * @throws RuntimeException + */ + private function revertLocalPatches(OutputInterface $output) + { + $patches = array_filter( + $this->localPool->getList(), + function ($patch) { + return !$this->statusPool->isNotApplied($patch->getId()); + } + ); + + if (empty($patches)) { + return; + } + + $output->writeln('<info>Start of reverting hot-fixes</info>'); + + foreach (array_reverse($patches) as $patch) { + try { + $message = $this->applier->revert($patch->getPath(), $patch->getTitle()); + $this->printInfo($output, $message); + } catch (ApplierException $exception) { + $errorMessage = sprintf( + 'Reverting patch %s failed.%s', + $patch->getPath(), + $this->renderer->formatErrorOutput($exception->getMessage()) + ); + $this->printError($output, $errorMessage); + } + } + + $output->writeln('<info>End of reverting hot-fixes</info>'); + } + + /** + * Prints and logs info message. + * + * @param OutputInterface $output + * @param string $message + */ + private function printInfo(OutputInterface $output, string $message) + { + $output->writeln('<info>' . $message . '</info>'); + $this->logger->info($message); + } + + /** + * Prints and logs error message. + * + * @param OutputInterface $output + * @param string $message + */ + private function printError(OutputInterface $output, string $message) + { + $output->writeln('<error>' . $message . '</error>'); + $this->logger->error($message); + } +} diff --git a/src/Command/Process/Renderer.php b/src/Command/Process/Renderer.php index 764a86a6..319ce7b2 100644 --- a/src/Command/Process/Renderer.php +++ b/src/Command/Process/Renderer.php @@ -82,15 +82,17 @@ public function printTable(OutputInterface $output, array $patchList) $table = $this->tableFactory->create($output); $table->setHeaders([self::ID, self::TITLE, self::TYPE, self::STATUS, self::DETAILS]); $table->setStyle('box-double'); - $table->setColumnMaxWidth(1, 50); $rows = []; foreach ($patchList as $patch) { $rows[] = $this->createRow($patch); - $rows[] = new TableSeparator(); } - array_pop($rows); + usort($rows, function ($a, $b) { + return strcmp($a[self::STATUS], $b[self::STATUS]); + }); + + $rows = $this->addTableSeparator($rows); $table->addRows($rows); $table->render(); } @@ -109,7 +111,6 @@ public function printPatchInfo( string $prependedMessage = '' ) { $info = [ - sprintf('<comment>Id:</comment> %s', $patch->getId()), sprintf('<comment>Title:</comment> %s', $patch->getTitle()), sprintf('<comment>File:</comment> %s', $patch->getFilename()), sprintf( @@ -179,26 +180,53 @@ public function printQuestion(InputInterface $input, OutputInterface $output, st */ private function createRow(AggregatedPatchInterface $patch): array { - $glue = PHP_EOL . ' - '; $details = ''; if ($patch->getReplacedWith()) { - $details .= '<info>Recommended replacement: ' . $patch->getReplacedWith() . PHP_EOL . '</info>'; + $details .= '<info>Recommended replacement: ' . $patch->getReplacedWith() . '</info>' . PHP_EOL; } + if ($patch->getRequire()) { - $details .= 'Required patches:' . - '<comment>' . $glue . implode($glue, $patch->getRequire()) . PHP_EOL . '</comment>'; + $wrappedRequire = array_map( + function ($item) { + return sprintf('<comment> - %s</comment>', $item); + }, + $patch->getRequire() + ); + $details .= 'Required patches:' . PHP_EOL . implode(PHP_EOL, $wrappedRequire) . PHP_EOL; } + if ($patch->getAffectedComponents()) { + $glue = PHP_EOL . ' - '; $details .= 'Affected components:' . $glue . implode($glue, $patch->getAffectedComponents()); } + $id = $patch->getType() === PatchInterface::TYPE_CUSTOM ? 'N/A' : $patch->getId(); + $title = chunk_split($patch->getTitle(), 60, PHP_EOL); return [ self::ID => '<comment>' . $id . '</comment>', - self::TITLE => $patch->getTitle(), + self::TITLE => $title, self::TYPE => $patch->isDeprecated() ? '<error>DEPRECATED</error>' : $patch->getType(), self::STATUS => $this->statusPool->get($patch->getId()), self::DETAILS => $details ]; } + + /** + * Adds table separator. + * + * @param array $rowItems + * @return array + */ + private function addTableSeparator(array $rowItems): array + { + $result = []; + foreach ($rowItems as $row) { + $result[] = $row; + $result[] = new TableSeparator(); + } + array_pop($result); + + return $result; + } } diff --git a/src/Command/Process/Revert.php b/src/Command/Process/Revert.php index 1c92b01c..d9f07cff 100644 --- a/src/Command/Process/Revert.php +++ b/src/Command/Process/Revert.php @@ -15,7 +15,7 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * Reverts patches. + * Reverts patches (OnPrem). * * Patches are reverting from bottom to top of config list. */ @@ -56,7 +56,7 @@ public function __construct( */ public function run(InputInterface $input, OutputInterface $output) { - $argPatches = $input->getArgument(RevertCommand::ARG_QUALITY_PATCHES); + $argPatches = $input->getArgument(RevertCommand::ARG_LIST_OF_PATCHES); $optAll = $input->getOption(RevertCommand::OPT_ALL); $patchFilter = $this->filterFactory->createRevertFilter($optAll, $argPatches); diff --git a/src/Command/Revert.php b/src/Command/Revert.php index 2b971cde..8d6f99ed 100644 --- a/src/Command/Revert.php +++ b/src/Command/Revert.php @@ -10,7 +10,6 @@ use Magento\CloudPatches\App\RuntimeException; use Magento\CloudPatches\Command\Process\Revert as RevertProcess; use Magento\CloudPatches\Composer\MagentoVersion; -use Magento\CloudPatches\Patch\Environment; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -18,7 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * @inheritDoc + * Patch revert command (OnPrem). */ class Revert extends AbstractCommand { @@ -28,9 +27,9 @@ class Revert extends AbstractCommand const NAME = 'revert'; /** - * List of quality patches to revert. + * List of patches to revert. */ - const ARG_QUALITY_PATCHES = 'quality-patches'; + const ARG_LIST_OF_PATCHES = 'list-of-patches'; /** * Revert all patches. @@ -42,11 +41,6 @@ class Revert extends AbstractCommand */ private $revert; - /** - * @var Environment - */ - private $environment; - /** * @var LoggerInterface */ @@ -59,18 +53,15 @@ class Revert extends AbstractCommand /** * @param RevertProcess $revert - * @param Environment $environment * @param LoggerInterface $logger * @param MagentoVersion $magentoVersion */ public function __construct( RevertProcess $revert, - Environment $environment, LoggerInterface $logger, MagentoVersion $magentoVersion ) { $this->revert = $revert; - $this->environment = $environment; $this->logger = $logger; $this->magentoVersion = $magentoVersion; @@ -83,16 +74,18 @@ public function __construct( protected function configure() { $this->setName(self::NAME) - ->setDescription('Revert patches') - ->addArgument( - self::ARG_QUALITY_PATCHES, + ->setDescription( + 'Reverts patches. The list of patches should pass as a command argument' . + ' or use option --all to revert all patches' + )->addArgument( + self::ARG_LIST_OF_PATCHES, InputArgument::IS_ARRAY, - 'List of quality patches to revert' + 'List of patches to revert' )->addOption( self::OPT_ALL, 'a', InputOption::VALUE_NONE, - 'Revert all patches' + 'Reverts all patches' ); parent::configure(); @@ -103,12 +96,6 @@ protected function configure() */ public function execute(InputInterface $input, OutputInterface $output) { - if ($this->environment->isCloud()) { - $output->writeln('<error>Revert command is unavailable on Magento Cloud</error>'); - - return self::RETURN_FAILURE; - } - $this->logger->notice($this->magentoVersion->get()); try { diff --git a/src/Command/Status.php b/src/Command/Status.php index 8054de1d..857b2975 100644 --- a/src/Command/Status.php +++ b/src/Command/Status.php @@ -59,7 +59,7 @@ public function __construct( protected function configure() { $this->setName(self::NAME) - ->setDescription('Shows status of patches'); + ->setDescription('Shows the list of available patches and their statuses'); parent::configure(); } diff --git a/src/Composer/MagentoVersion.php b/src/Composer/MagentoVersion.php index 6495f57e..e7126a52 100644 --- a/src/Composer/MagentoVersion.php +++ b/src/Composer/MagentoVersion.php @@ -9,7 +9,9 @@ use Composer; use Composer\Package\PackageInterface; +use Composer\Package\RootPackageInterface; use Composer\Repository\RepositoryInterface; +use Composer\Semver\Semver; /** * Defines version of Magento. @@ -21,17 +23,32 @@ class MagentoVersion */ private $repository; + /** + * @var RootPackageInterface + */ + private $rootPackage; + /** * @var string */ private $version; /** - * @var string[] + * @var array */ private $editionMap = [ 'magento/magento2-b2b-base' => 'B2B Edition', - 'magento/magento2-ee-base' => 'Enterprise Edition' + 'magento/magento2-ee-base' => 'Enterprise Edition', + 'magento/magento2ee' => 'Enterprise Edition', + 'magento/magento2ce' => 'Community Edition' + ]; + + /** + * @var array + */ + private $gitToComposerMap = [ + 'magento/magento2ce' => ['magento/magento2-base'], + 'magento/magento2ee' => ['magento/magento2-base', 'magento/magento2-ee-base'] ]; /** @@ -40,6 +57,7 @@ class MagentoVersion public function __construct( Composer\Composer $composer ) { + $this->rootPackage = $composer->getPackage(); $this->repository = $composer->getRepositoryManager()->getLocalRepository(); } @@ -60,11 +78,38 @@ public function get(): string $version = $basePackage->getVersion(); $edition = $this->getEdition(); $this->version = 'Magento 2 ' . $edition . ', version ' . $version; + } elseif ($this->isGitBased()) { + $edition = $this->editionMap[$this->rootPackage->getName()]; + $this->version = 'Git-based: Magento 2 ' . $edition . ', version ' . $this->rootPackage->getVersion(); } return $this->version; } + /** + * Checks if it's git-based installation. + * + * @return boolean + */ + public function isGitBased(): bool + { + return isset($this->gitToComposerMap[$this->rootPackage->getName()]); + } + + /** + * Matches package on git-based Magento instance + * + * @param string $name + * @param string $constraint + * @return boolean + */ + public function matchPackageGit(string $name, string $constraint): bool + { + return $this->isGitBased() + && in_array($name, $this->gitToComposerMap[$this->rootPackage->getName()]) + && Semver::satisfies($this->rootPackage->getVersion(), $constraint); + } + /** * Returns Magento edition. * diff --git a/src/Composer/Package.php b/src/Composer/Package.php index cac69821..8f919d40 100644 --- a/src/Composer/Package.php +++ b/src/Composer/Package.php @@ -21,13 +21,21 @@ class Package */ private $repository; + /** + * @var MagentoVersion + */ + private $magentoVersion; + /** * @param Composer\Composer $composer + * @param MagentoVersion $magentoVersion */ public function __construct( - Composer\Composer $composer + Composer\Composer $composer, + MagentoVersion $magentoVersion ) { $this->repository = $composer->getRepositoryManager()->getLocalRepository(); + $this->magentoVersion = $magentoVersion; } /** @@ -39,6 +47,7 @@ public function __construct( */ public function matchConstraint(string $packageName, string $packageConstraint): bool { - return $this->repository->findPackage($packageName, $packageConstraint) instanceof PackageInterface; + return $this->magentoVersion->matchPackageGit($packageName, $packageConstraint) || + $this->repository->findPackage($packageName, $packageConstraint) instanceof PackageInterface; } } diff --git a/src/Composer/QualityPackage.php b/src/Composer/QualityPackage.php new file mode 100644 index 00000000..9cc28e93 --- /dev/null +++ b/src/Composer/QualityPackage.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Composer; + +/** + * Provides info from magento/quality-patches package. + */ +class QualityPackage +{ + /** + * @var string|null + */ + private $patchesDirectory; + + /** + * @var string|null + */ + private $patchesConfig; + + /** + * QualityPackage constructor + */ + public function __construct() + { + if (class_exists(\Magento\QualityPatches\Info::class)) { + $info = new \Magento\QualityPatches\Info(); + $this->patchesDirectory = $info->getPatchesDirectory(); + $this->patchesConfig = $info->getPatchesConfig(); + } + } + + /** + * Returns path to patches directory. + * + * @return string|null + */ + public function getPatchesDirectory() + { + return $this->patchesDirectory; + } + + /** + * Returns path to patches configuration file. + * + * @return string|null + */ + public function getPatchesConfig() + { + return $this->patchesConfig; + } +} diff --git a/src/Environment/Config.php b/src/Environment/Config.php new file mode 100644 index 00000000..0109d2d5 --- /dev/null +++ b/src/Environment/Config.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Environment; + +use Magento\CloudPatches\Filesystem\FileSystemException; + +/** + * Environment configuration. + */ +class Config +{ + /** + * Variable to define a Cloud environment. + */ + const ENV_VAR_CLOUD = 'MAGENTO_CLOUD_PROJECT'; + + /** + * Const is defined in ./bin/ece-patches + */ + const CONST_IS_CLOUD = 'IS_CLOUD'; + + /** + * Quality patches environment variable. + */ + const ENV_VAR_QUALITY_PATCHES = 'QUALITY_PATCHES'; + + /** + * @var ConfigReader + */ + private $configReader; + + /** + * @param ConfigReader $configReader + */ + public function __construct(ConfigReader $configReader) + { + $this->configReader = $configReader; + } + /** + * Checks if it's Cloud environment. + * + * @return bool + */ + public function isCloud(): bool + { + return (bool)$this->getEnv(self::ENV_VAR_CLOUD) || defined(self::CONST_IS_CLOUD); + } + + /** + * Returns quality patches env variable. + * + * @return array + * @throws FileSystemException + */ + public function getQualityPatches(): array + { + $result = $this->getEnv(self::ENV_VAR_QUALITY_PATCHES); + if ($result === false) { + $result = $this->configReader->read()['stage']['build'][self::ENV_VAR_QUALITY_PATCHES] ?? []; + } + + return $result ?: []; + } + + /** + * 'getEnv' method is an abstraction for _ENV and getenv. + * If _ENV is enabled in php.ini, use that. If not, fall back to use getenv. + * returns false if not found + * + * @param string $key + * @return array|string|int|null|bool + */ + private function getEnv(string $key) + { + return $_ENV[$key] ?? getenv($key); + } +} diff --git a/src/Environment/ConfigReader.php b/src/Environment/ConfigReader.php new file mode 100644 index 00000000..255f860f --- /dev/null +++ b/src/Environment/ConfigReader.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Environment; + +use Magento\CloudPatches\Filesystem\FileList; +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use Symfony\Component\Yaml\Yaml; +use Symfony\Component\Yaml\Exception\ParseException; + +/** + * Reads configuration from .magento.env.yaml configuration file. + */ +class ConfigReader +{ + /** + * @var FileList + */ + private $fileList; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * Cached configuration + * + * @var array|null + */ + private $config; + + /** + * @param FileList $fileList + * @param Filesystem $filesystem + */ + public function __construct(FileList $fileList, Filesystem $filesystem) + { + $this->fileList = $fileList; + $this->filesystem = $filesystem; + } + + /** + * Returns config. + * + * @return array + * @throws ParseException + * @throws FileSystemException + */ + public function read(): array + { + if ($this->config === null) { + $path = $this->fileList->getEnvConfig(); + + if (!$this->filesystem->exists($path)) { + $this->config = []; + } else { + $parseFlag = defined(Yaml::class . '::PARSE_CONSTANT') ? Yaml::PARSE_CONSTANT : 0; + $this->config = (array)Yaml::parse($this->filesystem->get($path), $parseFlag); + } + } + + return $this->config; + } +} diff --git a/src/Filesystem/FileList.php b/src/Filesystem/FileList.php index 07e07bd2..7c95b40c 100644 --- a/src/Filesystem/FileList.php +++ b/src/Filesystem/FileList.php @@ -44,8 +44,8 @@ public function getPatchLog(): string /** * @return string */ - public function getInitPatchLog(): string + public function getEnvConfig(): string { - return $this->directoryList->getMagentoRoot() . '/init/var/log/patch.log'; + return $this->directoryList->getMagentoRoot() . '/.magento.env.yaml'; } } diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php index 568cbf9a..e5f2d1ef 100644 --- a/src/Patch/Applier.php +++ b/src/Patch/Applier.php @@ -7,6 +7,8 @@ namespace Magento\CloudPatches\Patch; +use Magento\CloudPatches\Composer\MagentoVersion; +use Magento\CloudPatches\Filesystem\Filesystem; use Magento\CloudPatches\Patch\Status\StatusPool; use Magento\CloudPatches\Shell\ProcessFactory; use Symfony\Component\Process\Exception\ProcessFailedException; @@ -21,13 +23,37 @@ class Applier */ private $processFactory; + /** + * @var GitConverter + */ + private $gitConverter; + + /** + * @var MagentoVersion + */ + private $magentoVersion; + + /** + * @var Filesystem + */ + private $filesystem; + /** * @param ProcessFactory $processFactory + * @param GitConverter $gitConverter + * @param MagentoVersion $magentoVersion + * @param Filesystem $filesystem */ public function __construct( - ProcessFactory $processFactory + ProcessFactory $processFactory, + GitConverter $gitConverter, + MagentoVersion $magentoVersion, + Filesystem $filesystem ) { $this->processFactory = $processFactory; + $this->gitConverter = $gitConverter; + $this->magentoVersion = $magentoVersion; + $this->filesystem = $filesystem; } /** @@ -41,12 +67,13 @@ public function __construct( */ public function apply(string $path, string $id): string { + $content = $this->readContent($path); try { - $this->processFactory->create(['git', 'apply', $path]) + $this->processFactory->create(['git', 'apply'], $content) ->mustRun(); } catch (ProcessFailedException $exception) { try { - $this->processFactory->create(['git', 'apply', '--check', '--reverse', $path]) + $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $content) ->mustRun(); } catch (ProcessFailedException $reverseException) { throw new ApplierException($exception->getMessage(), $exception->getCode()); @@ -69,12 +96,13 @@ public function apply(string $path, string $id): string */ public function revert(string $path, string $id): string { + $content = $this->readContent($path); try { - $this->processFactory->create(['git', 'apply', '--reverse', $path]) + $this->processFactory->create(['git', 'apply', '--reverse'], $content) ->mustRun(); } catch (ProcessFailedException $exception) { try { - $this->processFactory->create(['git', 'apply', '--check', $path]) + $this->processFactory->create(['git', 'apply', '--check'], $content) ->mustRun(); } catch (ProcessFailedException $applyException) { throw new ApplierException($exception->getMessage(), $exception->getCode()); @@ -94,6 +122,7 @@ public function revert(string $path, string $id): string */ public function status(string $patchContent): string { + $patchContent = $this->prepareContent($patchContent); try { $this->processFactory->create(['git', 'apply', '--check'], $patchContent) ->mustRun(); @@ -110,4 +139,51 @@ public function status(string $patchContent): string return StatusPool::NOT_APPLIED; } + + /** + * Checks if the patch can be applied. + * + * @param string $patchContent + * @return boolean + */ + public function checkApply(string $patchContent): bool + { + $patchContent = $this->prepareContent($patchContent); + try { + $this->processFactory->create(['git', 'apply', '--check'], $patchContent) + ->mustRun(); + } catch (ProcessFailedException $exception) { + return false; + } + + return true; + } + + /** + * Returns patch content. + * + * @param string $path + * @return string + */ + private function readContent(string $path): string + { + $content = $this->filesystem->get($path); + + return $this->prepareContent($content); + } + + /** + * Prepares patch content. + * + * @param string $content + * @return string + */ + private function prepareContent(string $content): string + { + if ($this->magentoVersion->isGitBased()) { + $content = $this->gitConverter->convert($content); + } + + return $content; + } } diff --git a/src/Patch/Collector/CloudCollector.php b/src/Patch/Collector/CloudCollector.php index 07b18f39..eb818893 100644 --- a/src/Patch/Collector/CloudCollector.php +++ b/src/Patch/Collector/CloudCollector.php @@ -7,10 +7,10 @@ namespace Magento\CloudPatches\Patch\Collector; +use Magento\CloudPatches\Environment\Config; use Magento\CloudPatches\Filesystem\DirectoryList; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Composer\Package; -use Magento\CloudPatches\Patch\Environment; use Magento\CloudPatches\Patch\PatchBuilder; use Magento\CloudPatches\Patch\PatchIntegrityException; use Magento\CloudPatches\Patch\SourceProvider; @@ -37,9 +37,9 @@ class CloudCollector private $directoryList; /** - * @var Environment + * @var Config */ - private $environment; + private $envConfig; /** * @var PatchBuilder @@ -50,20 +50,20 @@ class CloudCollector * @param SourceProvider $sourceProvider * @param Package $package * @param DirectoryList $directoryList - * @param Environment $environment + * @param Config $envConfig * @param PatchBuilder $patchBuilder */ public function __construct( SourceProvider $sourceProvider, Package $package, DirectoryList $directoryList, - Environment $environment, + Config $envConfig, PatchBuilder $patchBuilder ) { $this->sourceProvider = $sourceProvider; $this->package = $package; $this->directoryList = $directoryList; - $this->environment = $environment; + $this->envConfig = $envConfig; $this->patchBuilder = $patchBuilder; } @@ -91,7 +91,7 @@ public function collect(): array if ($this->package->matchConstraint($packageName, $packageConstraint)) { try { $patchPath = $this->directoryList->getPatches() . '/' . $patchFile; - $patchType = $this->environment->isCloud() + $patchType = $this->envConfig->isCloud() ? PatchInterface::TYPE_REQUIRED : PatchInterface::TYPE_OPTIONAL; $this->patchBuilder->setId($patchId); diff --git a/src/Patch/Collector/LocalCollector.php b/src/Patch/Collector/LocalCollector.php index 2da4c494..8f2b35cf 100644 --- a/src/Patch/Collector/LocalCollector.php +++ b/src/Patch/Collector/LocalCollector.php @@ -48,8 +48,9 @@ public function collect(): array $files = $this->sourceProvider->getLocalPatches(); $result = []; foreach ($files as $file) { - $this->patchBuilder->setId(md5($file)); - $this->patchBuilder->setTitle('../' . SourceProvider::HOT_FIXES_DIR . '/' . basename($file)); + $shortPath = '../' . SourceProvider::HOT_FIXES_DIR . '/' . basename($file); + $this->patchBuilder->setId($shortPath); + $this->patchBuilder->setTitle($shortPath); $this->patchBuilder->setFilename(basename($file)); $this->patchBuilder->setPath($file); $this->patchBuilder->setType(PatchInterface::TYPE_CUSTOM); diff --git a/src/Patch/Collector/QualityCollector.php b/src/Patch/Collector/QualityCollector.php index 61c59918..1671119c 100644 --- a/src/Patch/Collector/QualityCollector.php +++ b/src/Patch/Collector/QualityCollector.php @@ -7,13 +7,13 @@ namespace Magento\CloudPatches\Patch\Collector; +use Magento\CloudPatches\Composer\QualityPackage; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Composer\Package; use Magento\CloudPatches\Patch\PatchBuilder; use Magento\CloudPatches\Patch\PatchIntegrityException; use Magento\CloudPatches\Patch\SourceProvider; use Magento\CloudPatches\Patch\SourceProviderException; -use Magento\QualityPatches\Info as QualityPatchesInfo; /** * Collects patches. @@ -59,9 +59,9 @@ class QualityCollector private $package; /** - * @var QualityPatchesInfo + * @var QualityPackage */ - private $qualityPatchesInfo; + private $qualityPackage; /** * @var array|null @@ -76,18 +76,18 @@ class QualityCollector /** * @param SourceProvider $sourceProvider * @param Package $package - * @param QualityPatchesInfo $qualityPatchesInfo + * @param QualityPackage $qualityPackage * @param PatchBuilder $patchBuilder */ public function __construct( SourceProvider $sourceProvider, Package $package, - QualityPatchesInfo $qualityPatchesInfo, + QualityPackage $qualityPackage, PatchBuilder $patchBuilder ) { $this->sourceProvider = $sourceProvider; $this->package = $package; - $this->qualityPatchesInfo = $qualityPatchesInfo; + $this->qualityPackage = $qualityPackage; $this->patchBuilder = $patchBuilder; } @@ -204,7 +204,7 @@ private function createPatch( bool $patchDeprecated ): PatchInterface { try { - $patchPath = $this->qualityPatchesInfo->getPatchesDirectory() . '/' . $patchFile; + $patchPath = $this->qualityPackage->getPatchesDirectory() . '/' . $patchFile; $this->patchBuilder->setId($patchId); $this->patchBuilder->setTitle($patchTitle); $this->patchBuilder->setFilename($patchFile); diff --git a/src/Patch/Conflict/Analyzer.php b/src/Patch/Conflict/Analyzer.php new file mode 100644 index 00000000..3862823e --- /dev/null +++ b/src/Patch/Conflict/Analyzer.php @@ -0,0 +1,188 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Conflict; + +use Magento\CloudPatches\Environment\Config; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\RollbackProcessor; + +/** + * Analyzes patch conflicts. + */ +class Analyzer +{ + /** + * @var OptionalPool + */ + private $optionalPool; + + /** + * @var Config + */ + private $envConfig; + + /** + * @var RollbackProcessor + */ + private $rollbackProcessor; + + /** + * @var ApplyChecker + */ + private $applyChecker; + + /** + * @param OptionalPool $optionalPool + * @param Config $envConfig + * @param RollbackProcessor $rollbackProcessor + * @param ApplyChecker $applyChecker + */ + public function __construct( + OptionalPool $optionalPool, + Config $envConfig, + RollbackProcessor $rollbackProcessor, + ApplyChecker $applyChecker + ) { + $this->optionalPool = $optionalPool; + $this->envConfig = $envConfig; + $this->rollbackProcessor = $rollbackProcessor; + $this->applyChecker = $applyChecker; + } + + /** + * Returns details about patch conflict. + * + * Identifies which particular patch(es) leads to conflict. + * Works only on Cloud since we need to have a clean Magento instance before analyzing. + * + * @param PatchInterface $failedPatch + * @param array $patchFilter + * @return string + */ + public function analyze(PatchInterface $failedPatch, array $patchFilter = []): string + { + if (!$this->envConfig->isCloud()) { + return ''; + } + + if ($failedPatch->getType() !== PatchInterface::TYPE_REQUIRED) { + $this->cleanupInstance(); + } + $id = $failedPatch->getId(); + + return $this->analyzeRequired($id) ?: $this->analyzeOptional($id, $patchFilter); + } + + /** + * Returns details about conflict with optional patches. + * + * @param string $failedPatchId + * @param array $patchFilter + * @return string + */ + private function analyzeOptional(string $failedPatchId, array $patchFilter = []): string + { + $optionalPatchIds = $patchFilter ?: $this->optionalPool->getIdsByType(PatchInterface::TYPE_OPTIONAL); + $ids = $this->getIncompatiblePatches($optionalPatchIds, $failedPatchId); + if ($ids) { + $errorMessage = sprintf( + 'Patch %s is not compatible with optional: %s', + $failedPatchId, + implode(' ', $ids) + ); + } + + return $errorMessage; + } + + /** + * Returns details about conflict with required patch. + * + * @param string $failedPatchId + * @return string + */ + private function analyzeRequired(string $failedPatchId): string + { + $requiredPatchIds = $this->optionalPool->getIdsByType(PatchInterface::TYPE_REQUIRED); + $poolToCompare = array_diff($requiredPatchIds, [$failedPatchId]); + if ($this->applyChecker->check(array_merge($poolToCompare, [$failedPatchId]))) { + return ''; + } + + while (count($poolToCompare)) { + $patchId = array_pop($poolToCompare); + if ($this->applyChecker->check(array_merge($poolToCompare, [$failedPatchId]))) { + return sprintf( + 'Patch %s is not compatible with required: %s', + $failedPatchId, + $patchId + ); + } + } + + if (!$this->applyChecker->check([$failedPatchId])) { + return 'Patch ' . $failedPatchId . ' can\'t be applied to clean Magento instance'; + } + + return ''; + } + + /** + * Returns ids of incompatible patches. + * + * @param string[] $patchesToCompare + * @param string $patchId + * @return array + */ + private function getIncompatiblePatches(array $patchesToCompare, string $patchId): array + { + $result = []; + $patchesToCompare = array_diff($patchesToCompare, [$patchId]); + foreach ($patchesToCompare as $compareId) { + if (!$this->applyChecker->check([$compareId, $patchId])) { + $result[] = $compareId; + } + } + + foreach ($result as $key => $patchId) { + $dependencies = $this->optionalPool->getDependencies($patchId); + if (array_intersect($result, $dependencies)) { + unset($result[$key]); + } + } + + return $result; + } + + /** + * Cleanup instance from applied patches. + * + * @return void + */ + private function cleanupInstance() + { + $requiredPatches = $this->getRequiredPatches(); + $this->rollbackProcessor->process($requiredPatches); + } + + /** + * Returns all patches of type 'Required'. + * + * @return PatchInterface[] + */ + private function getRequiredPatches(): array + { + return array_filter( + $this->optionalPool->getList(), + function ($patch) { + return $patch->getType() === PatchInterface::TYPE_REQUIRED; + } + ); + } +} diff --git a/src/Patch/Conflict/ApplyChecker.php b/src/Patch/Conflict/ApplyChecker.php new file mode 100644 index 00000000..a126fe60 --- /dev/null +++ b/src/Patch/Conflict/ApplyChecker.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Conflict; + +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; + +/** + * Checks if list of patches can be applied. + */ +class ApplyChecker +{ + /** + * @var Applier + */ + private $applier; + + /** + * @var OptionalPool + */ + private $optionalPool; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Applier $applier + * @param OptionalPool $optionalPool + * @param Filesystem $filesystem + */ + public function __construct( + Applier $applier, + OptionalPool $optionalPool, + Filesystem $filesystem + ) { + $this->applier = $applier; + $this->optionalPool = $optionalPool; + $this->filesystem = $filesystem; + } + + /** + * Returns true if listed patches with all dependencies can be applied to clean Magento instance. + * + * @param string[] $patchIds + * @return boolean + */ + public function check(array $patchIds): bool + { + $patchItems = $this->optionalPool->getList($patchIds); + $content = $this->getContent($patchItems); + + return $this->applier->checkApply($content); + } + + /** + * Returns aggregated patch content. + * + * @param PatchInterface[] $patches + * @return string + */ + private function getContent(array $patches): string + { + $result = ''; + foreach ($patches as $patch) { + $result .= $this->filesystem->get($patch->getPath()); + } + + return $result; + } +} diff --git a/src/Patch/Conflict/Processor.php b/src/Patch/Conflict/Processor.php new file mode 100644 index 00000000..29278995 --- /dev/null +++ b/src/Patch/Conflict/Processor.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Conflict; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Conflict\Analyzer as ConflictAnalyzer; +use Magento\CloudPatches\Patch\RollbackProcessor; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Process patch conflict. + */ +class Processor +{ + /** + * @var Renderer + */ + private $renderer; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ConflictAnalyzer + */ + private $conflictAnalyzer; + + /** + * @var RollbackProcessor + */ + private $rollbackProcessor; + + /** + * @param Renderer $renderer + * @param LoggerInterface $logger + * @param ConflictAnalyzer $conflictAnalyzer + * @param RollbackProcessor $rollbackProcessor + */ + public function __construct( + Renderer $renderer, + LoggerInterface $logger, + ConflictAnalyzer $conflictAnalyzer, + RollbackProcessor $rollbackProcessor + ) { + $this->renderer = $renderer; + $this->logger = $logger; + $this->conflictAnalyzer = $conflictAnalyzer; + $this->rollbackProcessor = $rollbackProcessor; + } + + /** + * Makes rollback of applied patches and provides conflict details. + * + * @param OutputInterface $output + * @param PatchInterface $patch + * @param array $appliedPatches + * @param string $exceptionMessage + * @throws RuntimeException + */ + public function process( + OutputInterface $output, + PatchInterface $patch, + array $appliedPatches, + string $exceptionMessage + ) { + $errorMessage = 'Error: patch conflict happened'; + $this->logger->error($errorMessage); + $output->writeln('<error>' . $errorMessage . '</error>'); + + $messages = $this->rollbackProcessor->process($appliedPatches); + $output->writeln($messages); + $conflictDetails = $this->conflictAnalyzer->analyze($patch); + $errorMessage = sprintf( + 'Applying patch %s (%s) failed.%s%s', + $patch->getId(), + $patch->getPath(), + $this->renderer->formatErrorOutput($exceptionMessage), + $conflictDetails ? PHP_EOL . $conflictDetails : '' + ); + + throw new RuntimeException($errorMessage); + } +} diff --git a/src/Patch/Environment.php b/src/Patch/Environment.php deleted file mode 100644 index 2f520734..00000000 --- a/src/Patch/Environment.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Patch; - -/** - * Environment configuration. - */ -class Environment -{ - /** - * Variable to define a Cloud environment. - */ - const ENV_VAR_CLOUD = 'MAGENTO_CLOUD_PROJECT'; - - /** - * Checks if it's Cloud environment. - * - * @return bool - */ - public function isCloud() - { - $result = $_ENV[self::ENV_VAR_CLOUD] ?? getenv(self::ENV_VAR_CLOUD); - - return (bool)$result; - } -} diff --git a/src/Patch/GitConverter.php b/src/Patch/GitConverter.php new file mode 100755 index 00000000..136afc84 --- /dev/null +++ b/src/Patch/GitConverter.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +/** + * Converts patch from composer-based format to git-based. + * + * @see https://github.com/magento-sparta/m2-convert-patch-for-composer-install/blob/master/convert-for-composer.php + */ +class GitConverter +{ + const MODULE = 'Module'; + const ADMINHTML_DESIGN = 'AdminhtmlDesign'; + const FRONTEND_DESIGN = 'FrontendDesign'; + const LIBRARY_AMPQ = 'LibraryAmpq'; + const LIBRARY_BULK = 'LibraryBulk'; + const LIBRARY_FOREIGN_KEY = 'LibraryForeignKey'; + const LIBRARY_MESSAGE_QUEUE = 'LibraryMessageQueue'; + const LIBRARY = 'Library'; + + /** + * @var string[] + */ + private $nonComposerPath = [ + self::MODULE => 'app/code/Magento/', + self::ADMINHTML_DESIGN => 'app/design/adminhtml/Magento/', + self::FRONTEND_DESIGN => 'app/design/frontend/Magento/', + self::LIBRARY_AMPQ => 'lib/internal/Magento/Framework/Amqp/', + self::LIBRARY_BULK => 'lib/internal/Magento/Framework/Bulk/', + self::LIBRARY_FOREIGN_KEY => 'lib/internal/Magento/Framework/ForeignKey/', + self::LIBRARY_MESSAGE_QUEUE => 'lib/internal/Magento/Framework/MessageQueue/', + self::LIBRARY => 'lib/internal/Magento/Framework/' + ]; + + /** + * @var string[] + */ + private $composerPath = [ + self::MODULE => 'vendor/magento/module-', + self::ADMINHTML_DESIGN => 'vendor/magento/theme-adminhtml-', + self::FRONTEND_DESIGN => 'vendor/magento/theme-frontend-', + self::LIBRARY_AMPQ => 'vendor/magento/framework-ampq/', + self::LIBRARY_BULK => 'vendor/magento/framework-bulk/', + self::LIBRARY_FOREIGN_KEY => 'vendor/magento/framework-foreign-key/', + self::LIBRARY_MESSAGE_QUEUE => 'vendor/magento/framework-message-queue/', + self::LIBRARY => 'vendor/magento/framework/' + ]; + + /** + * Converts patch content from composer-based to git-based. + * + * @param string $content + * @return string + */ + public function convert(string $content): string + { + foreach ($this->composerPath as $type => $path) { + $escapedPath = addcslashes($path, '/'); + $needProcess = $type !== self::FRONTEND_DESIGN && $type !== self::ADMINHTML_DESIGN; + + /** + * phpcs:disable + * Example: + * ( 1 ) ( 2 )( 3 ) ( 4 )( 5 ) + * diff --git a/vendor/magento/module-some-module/Some/Path/File.ext b/vendor/magento/module-some-module/Some/Path/File.ext + * + * ( 1 ) ()( 3 ) ()( 5 ) + * diff --git a/vendor/magento/framework-message-queue/Config.php b/vendor/magento/framework-message-queue/Config.php + * phpcs:enable + */ + $regex = '~(^diff -(?:.*?)\s+(?:a\/)?)' . $escapedPath . '([-\w]+\/)?([^\s]+\s+(?:b\/)?)' . + $escapedPath . '([-\w]+\/)?([^\s]+)$~m'; + $content = preg_replace_callback( + $regex, + function ($matches) use ($type, $needProcess) { + return $matches[1] . $this->nonComposerPath[$type] + . ($needProcess ? $this->dashedStringToCamelCase($matches[2]) : $matches[2]) + . $matches[3] . $this->nonComposerPath[$type] + . ($needProcess ? $this->dashedStringToCamelCase($matches[4]) : $matches[4]) + . $matches[5]; + }, + $content + ); + + // ( 1 ) ( 2 ) + // +++ b/vendor/magento/module-some-module... + $content = preg_replace_callback( + '~(^(?:---|\+\+\+|Index:)\s+(?:a\/|b\/)?)' . $escapedPath . '([-\w]+)~m', + function ($matches) use ($type, $needProcess) { + return $matches[1] . $this->nonComposerPath[$type] + . ($needProcess ? $this->dashedStringToCamelCase($matches[2]) : $matches[2]); + }, + $content + ); + + // ( 1 ) ( 2 ) + // rename from vendor/magento/module-some-module... + $content = preg_replace_callback( + '~(^rename\s+(?:from|to)\s+)' . $escapedPath . '([-\w]+)~m', + function ($matches) use ($type, $needProcess) { + return $matches[1] . $this->nonComposerPath[$type] + . ($needProcess ? $this->dashedStringToCamelCase($matches[2]) : $matches[2]); + }, + $content + ); + } + + return $content; + } + + /** + * Converts string to camel case. + * + * @param string $string + * @return string + */ + private function dashedStringToCamelCase(string $string): string + { + return str_replace('-', '', ucwords($string, '-')); + } +} diff --git a/src/Patch/Pool/OptionalPool.php b/src/Patch/Pool/OptionalPool.php index f26a1b7f..80c3cf0c 100644 --- a/src/Patch/Pool/OptionalPool.php +++ b/src/Patch/Pool/OptionalPool.php @@ -121,6 +121,24 @@ public function getDependentOn($patchId) return $result; } + /** + * Returns patch dependency ids. + * + * @param string $patchId + * @return string[] + */ + public function getDependencies($patchId) + { + $result = array_map( + function (PatchInterface $patch) { + return $patch->getId(); + }, + $this->getAdditionalRequiredPatches([$patchId]) + ); + + return array_unique($result); + } + /** * Returns required patches which are not included in patch filter. * @@ -167,6 +185,31 @@ public function getReplacedBy($patchId) return array_unique($result); } + /** + * Returns not deprecated patch ids by type. + * + * @param string $type + * @return string[] + */ + public function getIdsByType($type) + { + $items = array_filter( + $this->items, + function ($patch) use ($type) { + return !$patch->isDeprecated() && $patch->getType() === $type; + } + ); + + $result = array_map( + function (PatchInterface $patch) { + return $patch->getId(); + }, + $items + ); + + return array_unique($result); + } + /** * Validates search result. * diff --git a/src/Patch/RollbackProcessor.php b/src/Patch/RollbackProcessor.php new file mode 100644 index 00000000..f6cbfb13 --- /dev/null +++ b/src/Patch/RollbackProcessor.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Psr\Log\LoggerInterface; + +/** + * Rollback applied patches. + */ +class RollbackProcessor +{ + /** + * @var Applier + */ + private $applier; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Applier $applier + * @param LoggerInterface $logger + */ + public function __construct( + Applier $applier, + LoggerInterface $logger + ) { + $this->applier = $applier; + $this->logger = $logger; + } + + /** + * Rollback applied patches. + * + * @param PatchInterface[] $appliedPatches + * @return string[] + */ + public function process(array $appliedPatches): array + { + if (empty($appliedPatches)) { + return []; + } + + $message = 'Start of rollback'; + $this->logger->info($message); + $messages[] = $message; + + foreach (array_reverse($appliedPatches) as $appliedPatch) { + $message = $this->applier->revert($appliedPatch->getPath(), $appliedPatch->getId()); + $messages[] = $message; + $this->logger->info($message, ['file' => $appliedPatch->getPath()]); + } + + $message = 'End of rollback'; + $this->logger->info($message); + $messages[] = $message; + + return $messages; + } +} diff --git a/src/Patch/SourceProvider.php b/src/Patch/SourceProvider.php index a8373183..743fdbd4 100644 --- a/src/Patch/SourceProvider.php +++ b/src/Patch/SourceProvider.php @@ -7,10 +7,10 @@ namespace Magento\CloudPatches\Patch; +use Magento\CloudPatches\Composer\QualityPackage; use Magento\CloudPatches\Filesystem\DirectoryList; use Magento\CloudPatches\Filesystem\FileList; use Magento\CloudPatches\Filesystem\FileSystemException; -use Magento\QualityPatches\Info as QualityPatchesInfo; use Magento\CloudPatches\Filesystem\Filesystem; /** @@ -39,26 +39,26 @@ class SourceProvider private $directoryList; /** - * @var QualityPatchesInfo + * @var QualityPackage */ - private $qualityPatchesInfo; + private $qualityPackage; /** * @param Filesystem $filesystem * @param FileList $fileList * @param DirectoryList $directoryList - * @param QualityPatchesInfo $qualityPatchesInfo + * @param QualityPackage $qualityPackage */ public function __construct( Filesystem $filesystem, FileList $fileList, DirectoryList $directoryList, - QualityPatchesInfo $qualityPatchesInfo + QualityPackage $qualityPackage ) { $this->filesystem = $filesystem; $this->fileList = $fileList; $this->directoryList = $directoryList; - $this->qualityPatchesInfo = $qualityPatchesInfo; + $this->qualityPackage = $qualityPackage; } /** @@ -93,9 +93,9 @@ public function getCloudPatches(): array */ public function getQualityPatches(): array { - $configSupportPath = $this->qualityPatchesInfo->getPatchesConfig(); + $configPath = $this->qualityPackage->getPatchesConfig(); - return $this->readConfiguration($configSupportPath); + return $configPath ? $this->readConfiguration($configPath) : []; } /** diff --git a/src/Test/Unit/Command/ApplyTest.php b/src/Test/Unit/Command/ApplyTest.php index 85f35789..b88db0dc 100644 --- a/src/Test/Unit/Command/ApplyTest.php +++ b/src/Test/Unit/Command/ApplyTest.php @@ -10,11 +10,8 @@ use Magento\CloudPatches\App\RuntimeException; use Magento\CloudPatches\Command\AbstractCommand; use Magento\CloudPatches\Command\Apply; -use Magento\CloudPatches\Command\Process\ApplyLocal; use Magento\CloudPatches\Command\Process\ApplyOptional; -use Magento\CloudPatches\Command\Process\ApplyRequired; use Magento\CloudPatches\Composer\MagentoVersion; -use Magento\CloudPatches\Patch\Environment; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -31,26 +28,11 @@ class ApplyTest extends TestCase */ private $command; - /** - * @var ApplyLocal|MockObject - */ - private $applyLocal; - /** * @var ApplyOptional|MockObject */ private $applyOptional; - /** - * @var ApplyRequired|MockObject - */ - private $applyRequired; - - /** - * @var Environment|MockObject - */ - private $environment; - /** * @var LoggerInterface|MockObject */ @@ -66,68 +48,29 @@ class ApplyTest extends TestCase */ protected function setUp() { - $this->applyLocal = $this->createMock(ApplyLocal::class); $this->applyOptional = $this->createMock(ApplyOptional::class); - $this->applyRequired = $this->createMock(ApplyRequired::class); - $this->environment = $this->createMock(Environment::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); $this->magentoVersion = $this->createMock(MagentoVersion::class); $this->command = new Apply( - $this->applyRequired, $this->applyOptional, - $this->applyLocal, - $this->environment, $this->logger, $this->magentoVersion ); } /** - * Tests successful command execution on Cloud environment. + * Tests successful command execution. */ - public function testCloudEnvironmentSuccess() + public function testExecute() { /** @var InputInterface|MockObject $inputMock */ $inputMock = $this->getMockForAbstractClass(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - $this->environment->method('isCloud') - ->willReturn(true); - - $this->applyLocal->expects($this->once()) - ->method('run'); $this->applyOptional->expects($this->once()) ->method('run'); - $this->applyRequired->expects($this->once()) - ->method('run'); - - $this->assertEquals( - AbstractCommand::RETURN_SUCCESS, - $this->command->execute($inputMock, $outputMock) - ); - } - - /** - * Tests successful command execution on OnPrem environment. - */ - public function testOnPremEnvironmentSuccess() - { - /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); - /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - - $this->environment->method('isCloud') - ->willReturn(false); - - $this->applyLocal->expects($this->never()) - ->method('run'); - $this->applyOptional->expects($this->once()) - ->method('run'); - $this->applyRequired->expects($this->never()) - ->method('run'); $this->assertEquals( AbstractCommand::RETURN_SUCCESS, @@ -176,32 +119,4 @@ public function testCriticalError() $this->expectException(\InvalidArgumentException::class); $this->command->execute($inputMock, $outputMock); } - - /** - * Tests when Magento is installed from Git. - */ - public function testGitBasedInstallation() - { - /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); - /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - - $inputMock->expects($this->once()) - ->method('getOption') - ->with(Apply::OPT_GIT_INSTALLATION) - ->willReturn(1); - - $this->applyLocal->expects($this->never()) - ->method('run'); - $this->applyOptional->expects($this->never()) - ->method('run'); - $this->applyRequired->expects($this->never()) - ->method('run'); - - $this->assertEquals( - AbstractCommand::RETURN_SUCCESS, - $this->command->execute($inputMock, $outputMock) - ); - } } diff --git a/src/Test/Unit/Command/Ece/ApplyTest.php b/src/Test/Unit/Command/Ece/ApplyTest.php new file mode 100644 index 00000000..8b9906c0 --- /dev/null +++ b/src/Test/Unit/Command/Ece/ApplyTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Ece; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\AbstractCommand; +use Magento\CloudPatches\Command\Ece\Apply; +use Magento\CloudPatches\Command\Process\ApplyLocal; +use Magento\CloudPatches\Command\Process\Ece\ApplyOptional; +use Magento\CloudPatches\Command\Process\ApplyRequired; +use Magento\CloudPatches\Composer\MagentoVersion; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritDoc + */ +class ApplyTest extends TestCase +{ + /** + * @var Apply + */ + private $command; + + /** + * @var ApplyLocal|MockObject + */ + private $applyLocal; + + /** + * @var ApplyOptional|MockObject + */ + private $applyOptionalEce; + + /** + * @var ApplyRequired|MockObject + */ + private $applyRequired; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var MagentoVersion|MockObject + */ + private $magentoVersion; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->applyLocal = $this->createMock(ApplyLocal::class); + $this->applyOptionalEce = $this->createMock(ApplyOptional::class); + $this->applyRequired = $this->createMock(ApplyRequired::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->magentoVersion = $this->createMock(MagentoVersion::class); + + $this->command = new Apply( + $this->applyRequired, + $this->applyOptionalEce, + $this->applyLocal, + $this->logger, + $this->magentoVersion + ); + } + + /** + * Tests successful command execution - Cloud environment. + */ + public function testExecute() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->applyLocal->expects($this->once()) + ->method('run'); + $this->applyOptionalEce->expects($this->once()) + ->method('run'); + $this->applyRequired->expects($this->once()) + ->method('run'); + + $this->assertEquals( + AbstractCommand::RETURN_SUCCESS, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when runtime error happens during command execution. + */ + public function testRuntimeError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->applyOptionalEce->expects($this->once()) + ->method('run') + ->willThrowException(new RuntimeException('Error!')); + $this->logger->expects($this->once()) + ->method('error'); + + $this->assertEquals( + AbstractCommand::RETURN_FAILURE, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when critical error happens during command execution. + */ + public function testCriticalError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->applyOptionalEce->expects($this->once()) + ->method('run') + ->willThrowException(new \InvalidArgumentException('Critical error!')); + $this->logger->expects($this->once()) + ->method('critical'); + + $this->expectException(\InvalidArgumentException::class); + $this->command->execute($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Command/Ece/RevertTest.php b/src/Test/Unit/Command/Ece/RevertTest.php new file mode 100644 index 00000000..55f9075a --- /dev/null +++ b/src/Test/Unit/Command/Ece/RevertTest.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Ece; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\AbstractCommand; +use Magento\CloudPatches\Command\Process\Ece\Revert as RevertProcess; +use Magento\CloudPatches\Command\Ece\Revert; +use Magento\CloudPatches\Composer\MagentoVersion; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritDoc + */ +class RevertTest extends TestCase +{ + /** + * @var Revert + */ + private $command; + + /** + * @var RevertProcess|MockObject + */ + private $revertEce; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->revertEce = $this->createMock(RevertProcess::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + + /** @var MagentoVersion|MockObject $magentoVersion */ + $magentoVersion = $this->createMock(MagentoVersion::class); + + $this->command = new Revert( + $this->revertEce, + $this->logger, + $magentoVersion + ); + } + + /** + * Tests successful command execution. + */ + public function testRevertSuccess() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->revertEce->expects($this->once()) + ->method('run'); + + $this->assertEquals( + AbstractCommand::RETURN_SUCCESS, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when runtime error happens during command execution. + */ + public function testRuntimeError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->revertEce->expects($this->once()) + ->method('run') + ->willThrowException(new RuntimeException('Error!')); + $this->logger->expects($this->once()) + ->method('error'); + + $this->assertEquals( + AbstractCommand::RETURN_FAILURE, + $this->command->execute($inputMock, $outputMock) + ); + } + + /** + * Tests when critical error happens during command execution. + */ + public function testCriticalError() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->revertEce->expects($this->once()) + ->method('run') + ->willThrowException(new \InvalidArgumentException('Critical error!')); + $this->logger->expects($this->once()) + ->method('critical'); + + $this->expectException(\InvalidArgumentException::class); + $this->command->execute($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php index 5a264a64..dabaa7fc 100644 --- a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php @@ -12,6 +12,7 @@ use Magento\CloudPatches\Command\Process\Renderer; use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Patch\Pool\OptionalPool; use Magento\CloudPatches\Patch\Status\StatusPool; @@ -56,6 +57,11 @@ class ApplyOptionalActionTest extends TestCase */ private $optionalPool; + /** + * @var ConflictProcessor|MockObject + */ + private $conflictProcessor; + /** * @inheritdoc */ @@ -66,13 +72,15 @@ protected function setUp() $this->statusPool = $this->createMock(StatusPool::class); $this->optionalPool = $this->createMock(OptionalPool::class); $this->renderer = $this->createMock(Renderer::class); + $this->conflictProcessor = $this->createMock(ConflictProcessor::class); $this->action = new ApplyOptionalAction( $this->applier, $this->optionalPool, $this->statusPool, $this->renderer, - $this->logger + $this->logger, + $this->conflictProcessor ); } @@ -229,22 +237,21 @@ public function testApplyWithException() ])->willReturnCallback( function ($path, $id) { if ($id === 'MC-22222') { - throw new ApplierException('Error'); + throw new ApplierException('Applier error message'); } return "Patch {$path} {$id} has been applied"; } ); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->with('Error'); - - $this->applier->expects($this->once()) - ->method('revert') - ->withConsecutive([$patch1->getPath(), $patch1->getId()]); + $this->conflictProcessor->expects($this->once()) + ->method('process') + ->withConsecutive([$outputMock, $patch2, [$patch1], 'Applier error message']) + ->willThrowException(new RuntimeException('Error message')); $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Error message'); + $this->action->execute($inputMock, $outputMock, $patchFilter); } diff --git a/src/Test/Unit/Command/Process/ApplyLocalTest.php b/src/Test/Unit/Command/Process/ApplyLocalTest.php index 7396710e..cc6490c3 100644 --- a/src/Test/Unit/Command/Process/ApplyLocalTest.php +++ b/src/Test/Unit/Command/Process/ApplyLocalTest.php @@ -14,6 +14,7 @@ use Magento\CloudPatches\Patch\ApplierException; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Patch\Pool\LocalPool; +use Magento\CloudPatches\Patch\RollbackProcessor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -45,6 +46,11 @@ class ApplyLocalTest extends TestCase */ private $localPool; + /** + * @var RollbackProcessor|MockObject + */ + private $rollbackProcessor; + /** * @var Renderer|MockObject */ @@ -59,12 +65,14 @@ protected function setUp() $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); $this->localPool = $this->createMock(LocalPool::class); $this->renderer = $this->createMock(Renderer::class); + $this->rollbackProcessor = $this->createMock(RollbackProcessor::class); $this->manager = new ApplyLocal( $this->applier, $this->localPool, $this->renderer, - $this->logger + $this->logger, + $this->rollbackProcessor ); } @@ -134,21 +142,38 @@ public function testApplySuccessful() */ public function testApplyWithException() { - $patch = $this->createPatch('/path/patch.patch', '../m2-hotfixes/patch.patch'); + $patch1 = $this->createPatch('/path/patch1.patch', '../m2-hotfixes/patch1.patch'); + $patch2 = $this->createPatch('/path/patch2.patch', '../m2-hotfixes/patch2.patch'); + $rollbackMessages = ['Patch 1 has been reverted']; /** @var InputInterface|MockObject $inputMock */ $inputMock = $this->getMockForAbstractClass(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ $outputMock = $this->getMockForAbstractClass(OutputInterface::class); $this->localPool->method('getList') - ->willReturn([$patch]); + ->willReturn([$patch1, $patch2]); $this->applier->method('apply') - ->withConsecutive([$patch->getPath(), $patch->getTitle()]) - ->willThrowException(new ApplierException('Error')); + ->willReturnMap([ + [$patch1->getPath(), $patch1->getTitle()], + [$patch2->getPath(), $patch2->getTitle()] + ])->willReturnCallback( + function ($path, $title) { + if (strpos($title, 'patch2') !== false) { + throw new ApplierException('Applier error message'); + } + + return "Patch {$path} {$title} has been applied"; + } + ); + + $this->rollbackProcessor->expects($this->once()) + ->method('process') + ->withConsecutive([[$patch1]]) + ->willReturn($rollbackMessages); $this->renderer->expects($this->once()) ->method('formatErrorOutput') - ->with('Error'); + ->with('Applier error message'); $this->expectException(RuntimeException::class); $this->manager->run($inputMock, $outputMock); diff --git a/src/Test/Unit/Command/Process/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/ApplyOptionalTest.php index 38ebba36..a32b68c9 100644 --- a/src/Test/Unit/Command/Process/ApplyOptionalTest.php +++ b/src/Test/Unit/Command/Process/ApplyOptionalTest.php @@ -26,7 +26,7 @@ class ApplyOptionalTest extends TestCase /** * @var ApplyOptional */ - private $manager; + private $applyOptional; /** * @var LoggerInterface|MockObject @@ -52,7 +52,7 @@ protected function setUp() $this->actionPool = $this->createMock(ActionPool::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); - $this->manager = new ApplyOptional( + $this->applyOptional = new ApplyOptional( $this->filterFactory, $this->actionPool, $this->logger @@ -74,7 +74,7 @@ public function testApplyWithPatchArgumentProvided() $cliPatchArgument = ['MC-1111', 'MC-22222']; $inputMock->expects($this->once()) ->method('getArgument') - ->with(Apply::ARG_QUALITY_PATCHES) + ->with(Apply::ARG_LIST_OF_PATCHES) ->willReturn($cliPatchArgument); $this->filterFactory->method('createApplyFilter') ->with($cliPatchArgument) @@ -84,7 +84,7 @@ public function testApplyWithPatchArgumentProvided() ->method('execute') ->withConsecutive([$inputMock, $outputMock, $cliPatchArgument]); - $this->manager->run($inputMock, $outputMock); + $this->applyOptional->run($inputMock, $outputMock); } /** @@ -102,7 +102,7 @@ public function testApplyWithEmptyPatchArgument() $cliPatchArgument = []; $inputMock->expects($this->once()) ->method('getArgument') - ->with(Apply::ARG_QUALITY_PATCHES) + ->with(Apply::ARG_LIST_OF_PATCHES) ->willReturn($cliPatchArgument); $this->filterFactory->method('createApplyFilter') ->with($cliPatchArgument) @@ -111,6 +111,6 @@ public function testApplyWithEmptyPatchArgument() $this->actionPool->expects($this->never()) ->method('execute'); - $this->manager->run($inputMock, $outputMock); + $this->applyOptional->run($inputMock, $outputMock); } } diff --git a/src/Test/Unit/Command/Process/ApplyRequiredTest.php b/src/Test/Unit/Command/Process/ApplyRequiredTest.php index 04562de8..9675f5c2 100644 --- a/src/Test/Unit/Command/Process/ApplyRequiredTest.php +++ b/src/Test/Unit/Command/Process/ApplyRequiredTest.php @@ -12,6 +12,7 @@ use Magento\CloudPatches\Command\Process\Renderer; use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Patch\Pool\RequiredPool; use PHPUnit\Framework\MockObject\MockObject; @@ -50,6 +51,11 @@ class ApplyRequiredTest extends TestCase */ private $renderer; + /** + * @var ConflictProcessor|MockObject + */ + private $conflictProcessor; + /** * @inheritdoc */ @@ -59,12 +65,14 @@ protected function setUp() $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); $this->requiredPool = $this->createMock(RequiredPool::class); $this->renderer = $this->createMock(Renderer::class); + $this->conflictProcessor = $this->createMock(ConflictProcessor::class); $this->manager = new ApplyRequired( $this->applier, $this->requiredPool, $this->renderer, - $this->logger + $this->logger, + $this->conflictProcessor ); } @@ -122,12 +130,15 @@ public function testApplyWithException() $this->applier->method('apply') ->withConsecutive([$patch->getPath(), $patch->getId()]) - ->willThrowException(new ApplierException('Error')); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->with('Error'); + ->willThrowException(new ApplierException('Applier error message')); + $this->conflictProcessor->expects($this->once()) + ->method('process') + ->withConsecutive([$outputMock, $patch, [], 'Applier error message']) + ->willThrowException(new RuntimeException('Error message')); $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Error message'); + $this->manager->run($inputMock, $outputMock); } diff --git a/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php new file mode 100644 index 00000000..f269a989 --- /dev/null +++ b/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process\Ece; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\ActionPool; +use Magento\CloudPatches\Command\Process\Ece\ApplyOptional; +use Magento\CloudPatches\Environment\Config; +use Magento\CloudPatches\Patch\FilterFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class ApplyOptionalTest extends TestCase +{ + /** + * @var ApplyOptional + */ + private $applyOptionalEce; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var ActionPool|MockObject + */ + private $actionPool; + + /** + * @var FilterFactory|MockObject + */ + private $filterFactory; + + /** + * @var Config|MockObject + */ + private $config; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->filterFactory = $this->createMock(FilterFactory::class); + $this->actionPool = $this->createMock(ActionPool::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->config = $this->createMock(Config::class); + + $this->applyOptionalEce = new ApplyOptional( + $this->filterFactory, + $this->actionPool, + $this->logger, + $this->config + ); + } + + /** + * Tests successful optional patches applying. + * + * @throws RuntimeException + */ + public function testApplyWithPatchEnvVariableProvided() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $configQualityPatches = ['MC-1111', 'MC-22222']; + $this->config->expects($this->once()) + ->method('getQualityPatches') + ->willReturn($configQualityPatches); + $this->filterFactory->method('createApplyFilter') + ->with($configQualityPatches) + ->willReturn($configQualityPatches); + + $this->actionPool->expects($this->once()) + ->method('execute') + ->withConsecutive([$inputMock, $outputMock, $configQualityPatches]); + + $this->applyOptionalEce->run($inputMock, $outputMock); + } + + /** + * Tests optional patches applying when QUALITY_PATCHES env variable is empty. + * + * @throws RuntimeException + */ + public function testApplyWithEmptyPatchEnvVariable() + { + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $configQualityPatches = []; + $this->config->expects($this->once()) + ->method('getQualityPatches') + ->willReturn($configQualityPatches); + $this->filterFactory->method('createApplyFilter') + ->with($configQualityPatches) + ->willReturn(null); + + $this->actionPool->expects($this->never()) + ->method('execute'); + + $this->applyOptionalEce->run($inputMock, $outputMock); + } +} diff --git a/src/Test/Unit/Command/Process/Ece/RevertTest.php b/src/Test/Unit/Command/Process/Ece/RevertTest.php new file mode 100644 index 00000000..82dacb01 --- /dev/null +++ b/src/Test/Unit/Command/Process/Ece/RevertTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Command\Process\Ece; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Action\RevertAction; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Command\Process\Ece\Revert; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\LocalPool; +use Magento\CloudPatches\Patch\Status\StatusPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritdoc + */ +class RevertTest extends TestCase +{ + /** + * @var Revert + */ + private $revertEce; + + /** + * @var RevertAction|MockObject + */ + private $revertAction; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var LocalPool|MockObject + */ + private $localPool; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @var StatusPool|MockObject + */ + private $statusPool; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->revertAction = $this->createMock(RevertAction::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->applier = $this->createMock(Applier::class); + $this->localPool = $this->createMock(LocalPool::class); + $this->renderer = $this->createMock(Renderer::class); + $this->statusPool = $this->createMock(StatusPool::class); + + $this->revertEce = new Revert( + $this->revertAction, + $this->logger, + $this->applier, + $this->localPool, + $this->renderer, + $this->statusPool + ); + } + + /** + * Tests successful patches reverting. + * + * @throws RuntimeException + */ + public function testRevertSuccessful() + { + $patch1 = $this->createPatch('/path/patch1.patch', '../m2-hotfixes/patch1.patch'); + $patch2 = $this->createPatch('/path/patch2.patch', '../m2-hotfixes/patch2.patch'); + $patch3 = $this->createPatch('/path/patch3.patch', '../m2-hotfixes/patch3.patch'); + $this->statusPool->method('isNotApplied') + ->willReturnMap([ + ['../m2-hotfixes/patch1.patch', false], + ['../m2-hotfixes/patch2.patch', false], + ['../m2-hotfixes/patch3.patch', true] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->localPool->method('getList') + ->willReturn([$patch1, $patch2, $patch3]); + + $this->applier->method('revert') + ->willReturnMap([ + [$patch2->getPath(), $patch2->getTitle(), 'Patch ' . $patch2->getTitle() .' has been reverted'], + [$patch1->getPath(), $patch1->getTitle(), 'Patch ' . $patch1->getTitle() .' has been reverted'], + ]); + + $outputMock->expects($this->exactly(4)) + ->method('writeln') + ->withConsecutive( + [$this->anything()], + [$this->stringContains('Patch ' . $patch2->getTitle() .' has been reverted')], + [$this->stringContains('Patch ' . $patch1->getTitle() .' has been reverted')] + ); + + $this->revertAction->expects($this->once()) + ->method('execute') + ->withConsecutive([$inputMock, $outputMock, []]); + + $this->revertEce->run($inputMock, $outputMock); + } + + /** + * Tests patches reverting with exception. + * + * @throws RuntimeException + */ + public function testRevertWithError() + { + $patch1 = $this->createPatch('/path/patch1.patch', '../m2-hotfixes/patch1.patch'); + $patch2 = $this->createPatch('/path/patch2.patch', '../m2-hotfixes/patch2.patch'); + $this->statusPool->method('isNotApplied') + ->willReturnMap([ + ['../m2-hotfixes/patch1.patch', false], + ['../m2-hotfixes/patch2.patch', false] + ]); + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $this->localPool->method('getList') + ->willReturn([$patch1, $patch2]); + + $this->applier->method('revert') + ->willReturnMap([ + [$patch1->getPath(), $patch1->getTitle()], + [$patch2->getPath(), $patch2->getTitle()] + ])->willReturnCallback( + function ($path, $title) { + if (strpos($title, 'patch2') !== false) { + throw new ApplierException('Applier error message'); + } + + return "Patch {$path} {$title} has been reverted"; + } + ); + + $this->renderer->expects($this->once()) + ->method('formatErrorOutput') + ->with('Applier error message'); + + $this->revertAction->expects($this->once()) + ->method('execute') + ->withConsecutive([$inputMock, $outputMock, []]); + + $this->revertEce->run($inputMock, $outputMock); + } + + /** + * Creates patch mock. + * + * @param string $path + * @param string $title + * + * @return PatchInterface|MockObject + */ + private function createPatch(string $path, string $title) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getPath')->willReturn($path); + $patch->method('getTitle')->willReturn($title); + $patch->method('getId')->willReturn($title); + + return $patch; + } +} diff --git a/src/Test/Unit/Command/Process/RendererTest.php b/src/Test/Unit/Command/Process/RendererTest.php index 41c07539..f78e1612 100644 --- a/src/Test/Unit/Command/Process/RendererTest.php +++ b/src/Test/Unit/Command/Process/RendererTest.php @@ -91,7 +91,6 @@ public function printPatchInfoDataProvider(): array 'patch' => $this->createPatch(false), 'prependedMessage' => '', 'expectedArray' => [ - '<comment>Id:</comment> ' . self::PATCH_ID, '<comment>Title:</comment> ' . self::PATCH_TITLE, '<comment>File:</comment> ' . self::PATCH_FILENAME, '<comment>Affected components:</comment> ' . implode(' ', $this->affectedComponents) @@ -102,7 +101,6 @@ public function printPatchInfoDataProvider(): array 'prependedMessage' => 'Prepended message', 'expectedArray' => [ '<info>Prepended message</info>', - '<comment>Id:</comment> ' . self::PATCH_ID, '<comment>Title:</comment> ' . self::PATCH_TITLE, '<comment>File:</comment> ' . self::PATCH_FILENAME, '<comment>Affected components:</comment> ' . implode(' ', $this->affectedComponents), @@ -114,7 +112,6 @@ public function printPatchInfoDataProvider(): array 'prependedMessage' => 'Prepended message', 'expectedArray' => [ '<info>Prepended message</info>', - '<comment>Id:</comment> ' . self::PATCH_ID, '<comment>Title:</comment> ' . self::PATCH_TITLE, '<comment>File:</comment> ' . self::PATCH_FILENAME, '<comment>Affected components:</comment> ' . implode(' ', $this->affectedComponents), diff --git a/src/Test/Unit/Command/Process/RevertTest.php b/src/Test/Unit/Command/Process/RevertTest.php index c0a20675..37bb7a55 100644 --- a/src/Test/Unit/Command/Process/RevertTest.php +++ b/src/Test/Unit/Command/Process/RevertTest.php @@ -76,7 +76,7 @@ public function testRevertWithPatchArgumentProvided() $inputMock->expects($this->once()) ->method('getArgument') - ->with(RevertCommand::ARG_QUALITY_PATCHES) + ->with(RevertCommand::ARG_LIST_OF_PATCHES) ->willReturn($cliPatchArgument); $inputMock->expects($this->once()) ->method('getOption') @@ -110,7 +110,7 @@ public function testRevertWithEmptyPatchArgument() $inputMock->expects($this->once()) ->method('getArgument') - ->with(RevertCommand::ARG_QUALITY_PATCHES) + ->with(RevertCommand::ARG_LIST_OF_PATCHES) ->willReturn($cliPatchArgument); $inputMock->expects($this->once()) ->method('getOption') diff --git a/src/Test/Unit/Command/RevertTest.php b/src/Test/Unit/Command/RevertTest.php index 5f8ea6a1..f732ea8c 100644 --- a/src/Test/Unit/Command/RevertTest.php +++ b/src/Test/Unit/Command/RevertTest.php @@ -12,7 +12,6 @@ use Magento\CloudPatches\Command\Process\Revert as RevertProcess; use Magento\CloudPatches\Command\Revert; use Magento\CloudPatches\Composer\MagentoVersion; -use Magento\CloudPatches\Patch\Environment; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -34,11 +33,6 @@ class RevertTest extends TestCase */ private $revert; - /** - * @var Environment|MockObject - */ - private $environment; - /** * @var LoggerInterface|MockObject */ @@ -50,7 +44,6 @@ class RevertTest extends TestCase protected function setUp() { $this->revert = $this->createMock(RevertProcess::class); - $this->environment = $this->createMock(Environment::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); /** @var MagentoVersion|MockObject $magentoVersion */ @@ -58,47 +51,21 @@ protected function setUp() $this->command = new Revert( $this->revert, - $this->environment, $this->logger, $magentoVersion ); } /** - * Tests that command is not available on Cloud environment. + * Tests successful command execution. */ - public function testCloudEnvironmentNotAvailable() + public function testRevertSuccess() { /** @var InputInterface|MockObject $inputMock */ $inputMock = $this->getMockForAbstractClass(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - $this->environment->method('isCloud') - ->willReturn(true); - - $this->revert->expects($this->never()) - ->method('run'); - - $this->assertEquals( - AbstractCommand::RETURN_FAILURE, - $this->command->execute($inputMock, $outputMock) - ); - } - - /** - * Tests successful command execution on OnPrem environment. - */ - public function testOnPremEnvironmentSuccess() - { - /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); - /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - - $this->environment->method('isCloud') - ->willReturn(false); - $this->revert->expects($this->once()) ->method('run'); diff --git a/src/Test/Unit/Composer/MagentoVersionTest.php b/src/Test/Unit/Composer/MagentoVersionTest.php index 181fd7fa..615b679d 100644 --- a/src/Test/Unit/Composer/MagentoVersionTest.php +++ b/src/Test/Unit/Composer/MagentoVersionTest.php @@ -9,6 +9,7 @@ use Composer\Composer; use Composer\Package\PackageInterface; +use Composer\Package\RootPackageInterface; use Composer\Repository\RepositoryManager; use Composer\Repository\WritableRepositoryInterface; use Magento\CloudPatches\Composer\MagentoVersion; @@ -32,12 +33,18 @@ class MagentoVersionTest extends TestCase */ private $magentoVersion; + /** + * @var RootPackageInterface|MockObject + */ + private $rootPackage; + /** * @inheritDoc */ protected function setUp() { $this->repository = $this->getMockForAbstractClass(WritableRepositoryInterface::class); + $this->rootPackage = $this->getMockForAbstractClass(RootPackageInterface::class); $repositoryManager = $this->createMock(RepositoryManager::class); $repositoryManager->method('getLocalRepository') ->willReturn($this->repository); @@ -46,6 +53,8 @@ protected function setUp() $composer = $this->createMock(Composer::class); $composer->method('getRepositoryManager') ->willReturn($repositoryManager); + $composer->method('getPackage') + ->willReturn($this->rootPackage); $this->magentoVersion = new MagentoVersion($composer); } @@ -56,12 +65,18 @@ protected function setUp() * @param bool $ce * @param bool $ee * @param bool $b2b + * @param string $rootPackage * @param string $expectedResult * * @dataProvider getDataProvider */ - public function testGet(bool $ce, bool $ee, bool $b2b, string $expectedResult) + public function testGet(bool $ce, bool $ee, bool $b2b, string $rootPackage, string $expectedResult) { + $this->rootPackage->method('getName') + ->willReturn($rootPackage); + $this->rootPackage->method('getVersion') + ->willReturn(self::VERSION); + $package = $this->getMockForAbstractClass(PackageInterface::class); $package->method('getVersion') ->willReturn(self::VERSION); @@ -81,10 +96,140 @@ public function testGet(bool $ce, bool $ee, bool $b2b, string $expectedResult) public function getDataProvider(): array { return [ - ['CE' => false, 'EE' => false, 'B2B' => false, 'Magento 2 is not installed'], - ['CE' => true, 'EE' => true, 'B2B' => false, 'Magento 2 Enterprise Edition, version ' . self::VERSION], - ['CE' => true, 'EE' => false, 'B2B' => true, 'Magento 2 B2B Edition, version ' . self::VERSION], - ['CE' => true, 'EE' => false, 'B2B' => false, 'Magento 2 Community Edition, version ' . self::VERSION], + [ + 'CE' => false, + 'EE' => false, + 'B2B' => false, + 'gitPackage' => '', + 'Magento 2 is not installed' + ], + [ + 'CE' => true, + 'EE' => true, + 'B2B' => false, + 'gitPackage' => '', + 'Magento 2 Enterprise Edition, version ' . self::VERSION + ], + [ + 'CE' => true, + 'EE' => false, + 'B2B' => true, + 'gitPackage' => '', + 'Magento 2 B2B Edition, version ' . self::VERSION + ], + [ + 'CE' => true, + 'EE' => false, + 'B2B' => false, + 'gitPackage' => '', + 'Magento 2 Community Edition, version ' . self::VERSION + ], + [ + 'CE' => false, + 'EE' => false, + 'B2B' => false, + 'gitPackage' => 'magento/magento2ce', + 'Git-based: Magento 2 Community Edition, version ' . self::VERSION + ], + [ + 'CE' => false, + 'EE' => false, + 'B2B' => false, + 'gitPackage' => 'magento/magento2ee', + 'Git-based: Magento 2 Enterprise Edition, version ' . self::VERSION + ], + ]; + } + + /** + * Tests Magento git-version identifying . + * + * @param string $rootPackageName + * @param bool $expectedResult + * @dataProvider isGitBasedDataProvider + */ + public function testIsGitBased(string $rootPackageName, bool $expectedResult) + { + $this->rootPackage->method('getName') + ->willReturn($rootPackageName); + + $this->assertEquals($expectedResult, $this->magentoVersion->isGitBased()); + } + + /** + * @return array + */ + public function isGitBasedDataProvider(): array + { + return [ + ['rootPackageName' => 'magento/magento2ce', 'expectedResult' => true], + ['rootPackageName' => 'magento/magento2ee', 'expectedResult' => true], + ['rootPackageName' => 'magento/magento2-ce-base', 'expectedResult' => false] + ]; + } + + /** + * Tests package matching using composer root package. + * + * @param string $rootPackageName + * @param string $rootPackageVersion + * @param string $testPackageName + * @param string $testPackageVersion + * @param bool $expectedResult + * @dataProvider matchPackageGitProvider + */ + public function testMatchPackageGit( + string $rootPackageName, + string $rootPackageVersion, + string $testPackageName, + string $testPackageVersion, + bool $expectedResult + ) { + $this->rootPackage->method('getName') + ->willReturn($rootPackageName); + $this->rootPackage->method('getVersion') + ->willReturn($rootPackageVersion); + + $this->assertEquals( + $expectedResult, + $this->magentoVersion->matchPackageGit($testPackageName, $testPackageVersion) + ); + } + + /** + * @return array + */ + public function matchPackageGitProvider(): array + { + return [ + [ + 'magento/magento2ce', + '2.3.5', + 'magento/magento2-base', + '<=2.3.5 <2.3.6', + 'expectedResult' => true + ], + [ + 'magento/magento2ce', + '2.3.5', + 'magento/magento2-base', + '<2.3.5', + 'expectedResult' => false + ], + [ + 'magento/magento2ce', + '2.3.5', + 'magento/magento2-ee-base', + '<=2.3.5 <2.3.6', + 'expectedResult' => false + ], + [ + 'magento/magento2ee', + '2.3.5', + 'magento/magento2-ee-base', + '<=2.3.5 <2.3.6', + 'expectedResult' => true + ], ]; } } diff --git a/src/Test/Unit/Environment/ConfigReaderTest.php b/src/Test/Unit/Environment/ConfigReaderTest.php new file mode 100644 index 00000000..ddd0f5ad --- /dev/null +++ b/src/Test/Unit/Environment/ConfigReaderTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Environment; + +use Magento\CloudPatches\Environment\ConfigReader; +use Magento\CloudPatches\Filesystem\FileList; +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\FileSystemException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritdoc + */ +class ConfigReaderTest extends TestCase +{ + /** + * @var FileList|MockObject + */ + private $fileList; + + /** + * @var Filesystem|MockObject + */ + private $filesystem; + + /** + * @var ConfigReader + */ + private $configReader; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->fileList = $this->createMock(FileList::class); + $this->filesystem = $this->createPartialMock(Filesystem::class, ['exists']); + + $this->configReader = new ConfigReader( + $this->fileList, + $this->filesystem + ); + } + + /** + * @throws FileSystemException + */ + public function testRead() + { + $baseDir = __DIR__ . '/_file/'; + + $this->fileList->expects($this->once()) + ->method('getEnvConfig') + ->willReturn($baseDir . '/.magento.env.yaml'); + $this->filesystem->expects($this->once()) + ->method('exists') + ->willReturn(true); + + $this->configReader->read(); + $this->assertEquals( + [ + 'stage' => [ + 'build' => [ + 'QUALITY_PATCHES' => ['MC-1', 'MC-2'] + ] + ] + ], + $this->configReader->read() + ); + } + + /** + * @throws FileSystemException + */ + public function testReadNotExist() + { + $baseDir = __DIR__ . '/_file/'; + + $this->fileList->expects($this->once()) + ->method('getEnvConfig') + ->willReturn($baseDir . '/.magento.env.yaml'); + $this->filesystem->expects($this->once()) + ->method('exists') + ->willReturn(false); + + $this->assertEquals([], $this->configReader->read()); + } +} diff --git a/src/Test/Unit/Environment/ConfigTest.php b/src/Test/Unit/Environment/ConfigTest.php new file mode 100644 index 00000000..79d33297 --- /dev/null +++ b/src/Test/Unit/Environment/ConfigTest.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Environment; + +use Magento\CloudPatches\Environment\Config; +use Magento\CloudPatches\Environment\ConfigReader; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * @inheritDoc + */ +class ConfigTest extends TestCase +{ + /** + * @var ConfigReader|MockObject + */ + private $configReader; + + /** + * @var Config + */ + private $config; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->configReader = $this->createMock(ConfigReader::class); + + $this->config = new Config($this->configReader); + } + + /** + * Tests Cloud environment. + */ + public function testIsCloud() + { + $_ENV[Config::ENV_VAR_CLOUD] = ''; + $this->assertFalse($this->config->isCloud()); + + $_ENV[Config::ENV_VAR_CLOUD] = '123'; + $this->assertTrue($this->config->isCloud()); + } + + /** + * Tests retrieving QUALITY_PATCHES from env variable. + */ + public function testGetQualityPatchesEnv() + { + $_ENV[Config::ENV_VAR_QUALITY_PATCHES] = ['MC-1', 'MC-2']; + + $this->configReader->expects($this->never()) + ->method('read'); + + $this->assertEquals( + ['MC-1', 'MC-2'], + $this->config->getQualityPatches() + ); + } + + /** + * Tests retrieving QUALITY_PATCHES from env config. + */ + public function testGetQualityPatchesConfig() + { + unset($_ENV[Config::ENV_VAR_QUALITY_PATCHES]); + $this->assertArrayNotHasKey(Config::ENV_VAR_QUALITY_PATCHES, $_ENV); + + $config['stage']['build'][Config::ENV_VAR_QUALITY_PATCHES] = ['MC-1', 'MC-2']; + $this->configReader->expects($this->once()) + ->method('read') + ->willReturn($config); + + $this->assertEquals( + ['MC-1', 'MC-2'], + $this->config->getQualityPatches() + ); + } +} diff --git a/src/Test/Unit/Environment/_file/.magento.env.yaml b/src/Test/Unit/Environment/_file/.magento.env.yaml new file mode 100644 index 00000000..de70300e --- /dev/null +++ b/src/Test/Unit/Environment/_file/.magento.env.yaml @@ -0,0 +1,5 @@ +stage: + build: + QUALITY_PATCHES: + - MC-1 + - MC-2 diff --git a/src/Test/Unit/Filesystem/FileListTest.php b/src/Test/Unit/Filesystem/FileListTest.php index 194ab014..f0f019a7 100644 --- a/src/Test/Unit/Filesystem/FileListTest.php +++ b/src/Test/Unit/Filesystem/FileListTest.php @@ -59,12 +59,4 @@ public function testGetPatchLog() $this->fileList->getPatchLog() ); } - - public function testGetInitPatchLog() - { - $this->assertSame( - 'magento_root/init/var/log/patch.log', - $this->fileList->getInitPatchLog() - ); - } } diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php index f2efb35a..d62d0c0a 100644 --- a/src/Test/Unit/Patch/ApplierTest.php +++ b/src/Test/Unit/Patch/ApplierTest.php @@ -7,8 +7,11 @@ namespace Magento\CloudPatches\Test\Unit\Patch; +use Magento\CloudPatches\Composer\MagentoVersion; +use Magento\CloudPatches\Filesystem\Filesystem; use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; +use Magento\CloudPatches\Patch\GitConverter; use Magento\CloudPatches\Patch\Status\StatusPool; use Magento\CloudPatches\Shell\ProcessFactory; use PHPUnit\Framework\MockObject\MockObject; @@ -31,14 +34,37 @@ class ApplierTest extends TestCase */ private $processFactory; + /** + * @var GitConverter|MockObject + */ + private $gitConverter; + + /** + * @var MagentoVersion|MockObject + */ + private $magentoVersion; + + /** + * @var Filesystem|MockObject + */ + private $filesystem; + /** * @inheritDoc */ protected function setUp() { $this->processFactory = $this->createMock(ProcessFactory::class); - - $this->applier = new Applier($this->processFactory); + $this->gitConverter = $this->createMock(GitConverter::class); + $this->magentoVersion = $this->createMock(MagentoVersion::class); + $this->filesystem = $this->createMock(Filesystem::class); + + $this->applier = new Applier( + $this->processFactory, + $this->gitConverter, + $this->magentoVersion, + $this->filesystem + ); } /** @@ -51,12 +77,20 @@ public function testApply() $path = 'path/to/patch'; $patchId = 'MC-11111'; $expectedMessage = 'Patch ' . $patchId . ' has been applied'; - + $this->filesystem->expects($this->once()) + ->method('get') + ->willReturn('patchContent'); + $this->magentoVersion->expects($this->once()) + ->method('isGitBased') + ->willReturn(true); + $this->gitConverter->expects($this->once()) + ->method('convert') + ->willReturn('gitContent'); $processMock = $this->createMock(Process::class); $this->processFactory->expects($this->once()) ->method('create') - ->with(['git', 'apply', $path]) + ->withConsecutive([['git', 'apply'], 'gitContent']) ->willReturn($processMock); $processMock->expects($this->once()) ->method('mustRun'); @@ -96,11 +130,20 @@ public function testApplyPatchAlreadyApplied() $patchId = 'MC-11111'; $expectedMessage = 'Patch ' . $patchId . ' was already applied'; + $this->filesystem->expects($this->once()) + ->method('get') + ->willReturn('patchContent'); + $this->magentoVersion->expects($this->once()) + ->method('isGitBased') + ->willReturn(false); + $this->gitConverter->expects($this->never()) + ->method('convert'); + $this->processFactory->expects($this->exactly(2)) ->method('create') ->willReturnMap([ - [['git', 'apply', $path]], - [['git', 'apply', $path, '--check', '--reverse']] + [['git', 'apply'], 'patchContent'], + [['git', 'apply', '--check', '--reverse'], 'patchContent'] ])->willReturnCallback([$this, 'shellApplyRevertCallback']); $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); @@ -148,11 +191,21 @@ public function testRevert() $patchId = 'MC-11111'; $expectedMessage = 'Patch ' . $patchId . ' has been reverted'; + $this->filesystem->expects($this->once()) + ->method('get') + ->willReturn('patchContent'); + $this->magentoVersion->expects($this->once()) + ->method('isGitBased') + ->willReturn(true); + $this->gitConverter->expects($this->once()) + ->method('convert') + ->willReturn('gitContent'); + $processMock = $this->createMock(Process::class); $this->processFactory->expects($this->once()) ->method('create') - ->with(['git', 'apply', '--reverse', $path]) + ->withConsecutive([['git', 'apply', '--reverse'], 'gitContent']) ->willReturn($processMock); $processMock->expects($this->once()) ->method('mustRun'); @@ -190,13 +243,23 @@ public function testRevertPatchWasntApplied() { $path = 'path/to/patch'; $patchId = 'MC-11111'; + $patchContent = 'patch content'; $expectedMessage = 'Patch ' . $patchId . ' wasn\'t applied'; + $this->filesystem->expects($this->once()) + ->method('get') + ->willReturn($patchContent); + $this->magentoVersion->expects($this->once()) + ->method('isGitBased') + ->willReturn(false); + $this->gitConverter->expects($this->never()) + ->method('convert'); + $this->processFactory->expects($this->exactly(2)) ->method('create') ->willReturnMap([ - [['git', 'apply', $path]], - [['git', 'apply', $path, '--check']] + [['git', 'apply'], $patchContent], + [['git', 'apply', '--check'], $patchContent] ])->willReturnCallback([$this, 'shellApplyRevertCallback']); $this->assertSame($expectedMessage, $this->applier->revert($path, $patchId)); diff --git a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php index 55e65696..b83df61d 100644 --- a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php @@ -8,12 +8,12 @@ namespace Magento\CloudPatches\Test\Unit\Patch\Collector; use Magento\CloudPatches\Composer\Package; +use Magento\CloudPatches\Environment\Config; use Magento\CloudPatches\Filesystem\DirectoryList; use Magento\CloudPatches\Patch\Collector\CloudCollector; use Magento\CloudPatches\Patch\Collector\CollectorException; use Magento\CloudPatches\Patch\Data\Patch; use Magento\CloudPatches\Patch\Data\PatchInterface; -use Magento\CloudPatches\Patch\Environment; use Magento\CloudPatches\Patch\PatchBuilder; use Magento\CloudPatches\Patch\PatchIntegrityException; use Magento\CloudPatches\Patch\SourceProvider; @@ -49,9 +49,9 @@ class CloudCollectorTest extends TestCase private $package; /** - * @var Environment|MockObject + * @var Config|MockObject */ - private $environment; + private $envConfig; /** * @var DirectoryList|MockObject @@ -65,7 +65,7 @@ protected function setUp() { $this->sourceProvider = $this->createMock(SourceProvider::class); $this->package = $this->createMock(Package::class); - $this->environment = $this->createMock(Environment::class); + $this->envConfig = $this->createMock(Config::class); $this->directoryList = $this->createMock(DirectoryList::class); $this->patchBuilder = $this->createMock(PatchBuilder::class); @@ -73,7 +73,7 @@ protected function setUp() $this->sourceProvider, $this->package, $this->directoryList, - $this->environment, + $this->envConfig, $this->patchBuilder ); } @@ -93,7 +93,7 @@ public function testCollectSuccessful(bool $isCloud, string $expectedType) ->willReturn($validConfig); $this->directoryList->method('getPatches') ->willReturn(self::CLOUD_PATCH_DIR); - $this->environment->method('isCloud') + $this->envConfig->method('isCloud') ->willReturn($isCloud); $this->package->method('matchConstraint') diff --git a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php index cd297c5e..972436dd 100644 --- a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php @@ -56,6 +56,8 @@ public function testCollect() { $file1 = __DIR__ . SourceProvider::HOT_FIXES_DIR . '/patch1.patch'; $file2 = __DIR__ . SourceProvider::HOT_FIXES_DIR . '/patch2.patch'; + $shortPath1 = '../' . SourceProvider::HOT_FIXES_DIR . '/patch1.patch'; + $shortPath2 = '../' . SourceProvider::HOT_FIXES_DIR . '/patch2.patch'; $this->sourceProvider->expects($this->once()) ->method('getLocalPatches') @@ -63,12 +65,12 @@ public function testCollect() $this->patchBuilder->expects($this->exactly(2)) ->method('setId') - ->withConsecutive([md5($file1)], [md5($file2)]); + ->withConsecutive([$shortPath1], [$shortPath2]); $this->patchBuilder->expects($this->exactly(2)) ->method('setTitle') ->withConsecutive( - ['../' . SourceProvider::HOT_FIXES_DIR . '/patch1.patch'], - ['../' . SourceProvider::HOT_FIXES_DIR . '/patch2.patch'] + [$shortPath1], + [$shortPath2] ); $this->patchBuilder->expects($this->exactly(2)) ->method('setFilename') diff --git a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php index 5d4bdf1c..b9e38531 100644 --- a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php @@ -8,6 +8,7 @@ namespace Magento\CloudPatches\Test\Unit\Patch\Collector; use Magento\CloudPatches\Composer\Package; +use Magento\CloudPatches\Composer\QualityPackage; use Magento\CloudPatches\Patch\Collector\CollectorException; use Magento\CloudPatches\Patch\Collector\QualityCollector; use Magento\CloudPatches\Patch\Data\Patch; @@ -16,7 +17,6 @@ use Magento\CloudPatches\Patch\PatchIntegrityException; use Magento\CloudPatches\Patch\SourceProvider; use Magento\CloudPatches\Patch\SourceProviderException; -use Magento\QualityPatches\Info as QualityPatchesInfo; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -48,9 +48,9 @@ class QualityCollectorTest extends TestCase private $package; /** - * @var QualityPatchesInfo|MockObject + * @var QualityPackage|MockObject */ - private $qualityPatchesInfo; + private $qualityPackage; /** * @inheritDoc @@ -59,13 +59,13 @@ protected function setUp() { $this->sourceProvider = $this->createMock(SourceProvider::class); $this->package = $this->createMock(Package::class); - $this->qualityPatchesInfo = $this->createMock(QualityPatchesInfo::class); + $this->qualityPackage = $this->createMock(QualityPackage::class); $this->patchBuilder = $this->createMock(PatchBuilder::class); $this->collector = new QualityCollector( $this->sourceProvider, $this->package, - $this->qualityPatchesInfo, + $this->qualityPackage, $this->patchBuilder ); } @@ -79,7 +79,7 @@ public function testCollectSuccessful() $this->sourceProvider->expects($this->once()) ->method('getQualityPatches') ->willReturn($validConfig); - $this->qualityPatchesInfo->method('getPatchesDirectory') + $this->qualityPackage->method('getPatchesDirectory') ->willReturn(self::QUALITY_PATCH_DIR); $this->package->method('matchConstraint') diff --git a/src/Test/Unit/Patch/Conflict/AnalyzerTest.php b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php new file mode 100644 index 00000000..f518d8f9 --- /dev/null +++ b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php @@ -0,0 +1,182 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Conflict; + +use Magento\CloudPatches\Environment\Config; +use Magento\CloudPatches\Patch\Conflict\Analyzer as ConflictAnalyzer; +use Magento\CloudPatches\Patch\Conflict\ApplyChecker; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use Magento\CloudPatches\Patch\RollbackProcessor; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class AnalyzerTest extends TestCase +{ + /** + * @var RollbackProcessor|MockObject + */ + private $rollbackProcessor; + + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @var Config|MockObject + */ + private $config; + + /** + * @var ConflictAnalyzer + */ + private $conflictAnalyzer; + + /** + * @var ApplyChecker|MockObject + */ + private $applyChecker; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->optionalPool = $this->createMock(OptionalPool::class); + $this->config = $this->createMock(Config::class); + $this->rollbackProcessor = $this->createMock(RollbackProcessor::class); + $this->applyChecker = $this->createMock(ApplyChecker::class); + + $this->conflictAnalyzer = new ConflictAnalyzer( + $this->optionalPool, + $this->config, + $this->rollbackProcessor, + $this->applyChecker + ); + } + + /** + * Tests patch conflict analyzing. + * + * @param array $checkApplyMap + * @param string $expectedMessage + * @dataProvider analyzeDataProvider + */ + public function testAnalyze(array $checkApplyMap, string $expectedMessage) + { + $failedPatch = $this->createPatch('MC-1', 'path1', PatchInterface::TYPE_OPTIONAL); + $requiredPool = ['REQUIRED-1', 'REQUIRED-2']; + $optionalPool = ['OPTIONAL-1', 'OPTIONAL-2']; + + $this->config->expects($this->once()) + ->method('isCloud') + ->willReturn(true); + $this->optionalPool->expects($this->once()) + ->method('getList') + ->willReturn([]); + $this->optionalPool->method('getDependencies') + ->willReturn([]); + $this->rollbackProcessor->expects($this->once()) + ->method('process'); + + $this->optionalPool->expects($this->atLeastOnce()) + ->method('getIdsByType') + ->willReturnMap([ + [PatchInterface::TYPE_REQUIRED, $requiredPool], + [PatchInterface::TYPE_OPTIONAL, $optionalPool] + ]); + + $this->applyChecker->method('check') + ->willReturnMap($checkApplyMap); + + $this->assertEquals( + $expectedMessage, + $this->conflictAnalyzer->analyze($failedPatch, []) + ); + } + + /** + * @return array + */ + public function analyzeDataProvider(): array + { + return [ + [ + 'checkApplyMap' => [ + [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], true], + [['OPTIONAL-1', 'MC-1'], false], + [['OPTIONAL-2', 'MC-1'], false], + ], + 'expectedMessage' => 'Patch MC-1 is not compatible with optional: OPTIONAL-1 OPTIONAL-2' + ], + [ + 'checkApplyMap' => [ + [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], true], + [['OPTIONAL-1', 'MC-1'], true], + [['OPTIONAL-2', 'MC-1'], false], + ], + 'expectedMessage' => 'Patch MC-1 is not compatible with optional: OPTIONAL-2' + ], + [ + 'checkApplyMap' => [ + [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], false], + [['REQUIRED-2', 'MC-1'], false], + [['REQUIRED-1', 'MC-1'], true], + ], + 'expectedMessage' => 'Patch MC-1 is not compatible with required: REQUIRED-2' + ], + [ + 'checkApplyMap' => [ + [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], false], + [['REQUIRED-2', 'MC-1'], false], + [['REQUIRED-1', 'MC-1'], false], + [['MC-1'], false], + ], + 'expectedMessage' => 'Patch MC-1 can\'t be applied to clean Magento instance' + ], + ]; + } + + /** + * Tests with non-Cloud environment. + */ + public function testAnalyzeWithNonCloudEnv() + { + $patch = $this->createPatch('MC-1', 'path1'); + + $this->config->expects($this->once()) + ->method('isCloud') + ->willReturn(false); + $this->optionalPool->expects($this->never()) + ->method('getIdsByType'); + + $this->assertEmpty($this->conflictAnalyzer->analyze($patch)); + } + + /** + * Creates patch mock. + * + * @param string $id + * @param string $path + * @param string $type + * @return PatchInterface|MockObject + */ + private function createPatch(string $id, string $path, string $type = '') + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getId')->willReturn($id); + $patch->method('getPath')->willReturn($path); + $patch->method('getType')->willReturn($type); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php b/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php new file mode 100644 index 00000000..f59d651d --- /dev/null +++ b/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Conflict; + +use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\Conflict\ApplyChecker; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\Pool\OptionalPool; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class ApplyCheckerTest extends TestCase +{ + /** + * @var OptionalPool|MockObject + */ + private $optionalPool; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var Filesystem|MockObject + */ + private $filesystem; + + /** + * @var ApplyChecker + */ + private $applyChecker; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->optionalPool = $this->createMock(OptionalPool::class); + $this->filesystem = $this->createMock(Filesystem::class); + $this->applier = $this->createMock(Applier::class); + + $this->applyChecker = new ApplyChecker( + $this->applier, + $this->optionalPool, + $this->filesystem + ); + } + + /** + * Tests patch apply checker. + */ + public function testCheck() + { + $patchIds = ['MC-1', 'MC-2', 'MC-3']; + $patch1 = $this->createPatch('MC-1', 'path1'); + $patch2 = $this->createPatch('MC-2', 'path2'); + $patch3 = $this->createPatch('MC-3', 'path3'); + + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchIds]) + ->willReturn([$patch1, $patch2, $patch3]); + $this->filesystem->expects($this->exactly(3)) + ->method('get') + ->willReturnMap([ + [$patch1->getPath(), 'content1'], + [$patch2->getPath(), 'content2'], + [$patch3->getPath(), 'content3'], + ]); + $this->applier->expects($this->once()) + ->method('checkApply') + ->with('content1content2content3') + ->willReturn(true); + + $this->assertTrue( + $this->applyChecker->check($patchIds) + ); + } + + /** + * Creates patch mock. + * + * @param string $id + * @param string $path + * @return PatchInterface|MockObject + */ + private function createPatch(string $id, string $path) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getId')->willReturn($id); + $patch->method('getPath')->willReturn($path); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/Conflict/ProcessorTest.php b/src/Test/Unit/Patch/Conflict/ProcessorTest.php new file mode 100644 index 00000000..00569d60 --- /dev/null +++ b/src/Test/Unit/Patch/Conflict/ProcessorTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch\Conflict; + +use Magento\CloudPatches\App\RuntimeException; +use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Conflict\Analyzer as ConflictAnalyzer; +use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\RollbackProcessor; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @inheritDoc + */ +class ProcessorTest extends TestCase +{ + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var ConflictAnalyzer|MockObject + */ + private $conflictAnalyzer; + + /** + * @var RollbackProcessor|MockObject + */ + private $rollbackProcessor; + + /** + * @var Renderer|MockObject + */ + private $renderer; + + /** + * @var ConflictProcessor + */ + private $conflictProcessor; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->renderer = $this->createMock(Renderer::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->conflictAnalyzer = $this->createMock(ConflictAnalyzer::class); + $this->rollbackProcessor = $this->createMock(RollbackProcessor::class); + + $this->conflictProcessor = new ConflictProcessor( + $this->renderer, + $this->logger, + $this->conflictAnalyzer, + $this->rollbackProcessor + ); + } + + /** + * Tests patch conflict processing. + */ + public function testProcess() + { + $appliedPatch1 = $this->createPatch('MC-1', 'path1'); + $appliedPatch2 = $this->createPatch('MC-2', 'path2'); + $failedPatch = $this->createPatch('MC-3', 'path3'); + $exceptionMessage = 'exceptionMessage'; + $conflictDetails = 'Conflict details'; + $formattedOutput = 'formattedOutput'; + $rollbackMessages = ['Patch 1 has been reverted', 'Patch 2 has been reverted']; + + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $this->rollbackProcessor->expects($this->once()) + ->method('process') + ->withConsecutive([[$appliedPatch1, $appliedPatch2]]) + ->willReturn($rollbackMessages); + $this->conflictAnalyzer->expects($this->once()) + ->method('analyze') + ->withConsecutive([$failedPatch]) + ->willReturn($conflictDetails); + $this->renderer->expects($this->once()) + ->method('formatErrorOutput') + ->withConsecutive([$exceptionMessage]) + ->willReturn($formattedOutput); + $outputMock->expects($this->exactly(2)) + ->method('writeln') + ->withConsecutive( + [$this->stringContains('Error: patch conflict happened')], + [$rollbackMessages] + ); + + $expectedErrorMessage = sprintf( + 'Applying patch %s (%s) failed.%s%s', + $failedPatch->getId(), + $failedPatch->getPath(), + $formattedOutput, + PHP_EOL . $conflictDetails + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage($expectedErrorMessage); + + $this->conflictProcessor->process( + $outputMock, + $failedPatch, + [$appliedPatch1, $appliedPatch2], + $exceptionMessage + ); + } + + /** + * Creates patch mock. + * + * @param string $id + * @param string $path + * @return PatchInterface|MockObject + */ + private function createPatch(string $id, string $path) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getId')->willReturn($id); + $patch->method('getPath')->willReturn($path); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/EnvironmentTest.php b/src/Test/Unit/Patch/EnvironmentTest.php deleted file mode 100644 index 21741aff..00000000 --- a/src/Test/Unit/Patch/EnvironmentTest.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Test\Unit\Patch; - -use Magento\CloudPatches\Patch\Environment; -use PHPUnit\Framework\TestCase; - -/** - * @inheritDoc - */ -class EnvironmentTest extends TestCase -{ - /** - * Tests environment. - */ - public function testIsCloud() - { - $environment = new Environment(); - - $_ENV[Environment::ENV_VAR_CLOUD] = ''; - $this->assertFalse($environment->isCloud()); - - $_ENV[Environment::ENV_VAR_CLOUD] = '123'; - $this->assertTrue($environment->isCloud()); - } -} diff --git a/src/Test/Unit/Patch/GitConverterTest.php b/src/Test/Unit/Patch/GitConverterTest.php new file mode 100644 index 00000000..a250edf1 --- /dev/null +++ b/src/Test/Unit/Patch/GitConverterTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\Patch\GitConverter; +use PHPUnit\Framework\TestCase; + +/** + * @inheritDoc + */ +class GitConverterTest extends TestCase +{ + /** + * @var GitConverter + */ + private $gitConverter; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->gitConverter = new GitConverter(); + } + + /** + * Tests patch converting from composer-based to git-based. + * + * @param string $composerContent + * @param string $expectedContent + * @dataProvider convertDataProvider + */ + public function testConvert(string $composerContent, string $expectedContent) + { + $this->assertEquals( + $expectedContent, + $this->gitConverter->convert($composerContent) + ); + } + + /** + * phpcs:disable + * @return array + */ + public function convertDataProvider() + { + return [ + [ + 'composerContent' => 'diff -Naur a/vendor/magento/framework/View/Asset/File/FallbackContext.php b/vendor/magento/framework/View/Asset/File/FallbackContext.php +--- a/vendor/magento/framework/View/Asset/File/FallbackContext.php ++++ b/vendor/magento/framework/View/Asset/File/FallbackContext.php', + 'expectedContent' => 'diff -Naur a/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php b/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php +--- a/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php ++++ b/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php' + ], + + [ + 'composerContent' => 'diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml', + 'expectedContent' => 'diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml' + ], + + [ + 'composerContent' => 'diff --git a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php +--- a/vendor/magento/module-deploy/Process/Queue.php ++++ b/vendor/magento/module-deploy/Process/Queue.php', + 'expectedContent' => 'diff --git a/app/code/Magento/Deploy/Process/Queue.php b/app/code/Magento/Deploy/Process/Queue.php +--- a/app/code/Magento/Deploy/Process/Queue.php ++++ b/app/code/Magento/Deploy/Process/Queue.php' + ], + + [ + 'composerContent' => 'rename from vendor/magento/module-some-module', + 'expectedContent' => 'rename from app/code/Magento/SomeModule' + ] + ]; + } + /** phpcs:enable */ +} diff --git a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php index 53c711e7..58c68cca 100644 --- a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php +++ b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php @@ -140,6 +140,24 @@ public function testGetDependentOn() $this->assertEquals(['MC-2', 'MC-3'], $pool->getDependentOn('MC-1')); } + /** + * Tests retrieving ids of patch dependencies. + */ + public function testGetDependencies() + { + $patch1 = $this->createPatch('MC-1'); + $patch2 = $this->createPatch('MC-2', ['MC-1']); + $patch3 = $this->createPatch('MC-3', ['MC-2']); + $patch4 = $this->createPatch('MC-4', ['MC-3']); + + $pool = $this->createPool([$patch1, $patch2, $patch3, $patch4]); + + $this->assertEquals( + ['MC-1', 'MC-2', 'MC-3'], + array_values($pool->getDependencies('MC-4')) + ); + } + /** * Tests retrieving additional required patches which are not included in patch filter. */ @@ -178,6 +196,34 @@ public function testGetReplacedBy() ); } + /** + * Tests retrieving not deprecated patch ids by type. + */ + public function testGetIdsByType() + { + $patch1 = $this->createPatch('OPTIONAL-1'); + $patch1->method('getType')->willReturn(PatchInterface::TYPE_OPTIONAL); + $patch2 = $this->createPatch('OPTIONAL-2'); + $patch2->method('getType')->willReturn(PatchInterface::TYPE_OPTIONAL); + $patch2->method('isDeprecated')->willReturn(true); + $patch3 = $this->createPatch('REQUIRED-3'); + $patch3->method('getType')->willReturn(PatchInterface::TYPE_REQUIRED); + $patch4 = $this->createPatch('REQUIRED-4'); + $patch4->method('getType')->willReturn(PatchInterface::TYPE_REQUIRED); + + $pool = $this->createPool([$patch1, $patch2, $patch3, $patch4]); + + $this->assertEquals( + ['OPTIONAL-1'], + array_values($pool->getIdsByType(PatchInterface::TYPE_OPTIONAL)) + ); + + $this->assertEquals( + ['REQUIRED-3', 'REQUIRED-4'], + array_values($pool->getIdsByType(PatchInterface::TYPE_REQUIRED)) + ); + } + /** * Filter is empty, Cloud + Quality patches expected to return. * diff --git a/src/Test/Unit/Patch/RollbackProcessorTest.php b/src/Test/Unit/Patch/RollbackProcessorTest.php new file mode 100644 index 00000000..60a8cfde --- /dev/null +++ b/src/Test/Unit/Patch/RollbackProcessorTest.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Patch; + +use Magento\CloudPatches\Patch\Applier; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Patch\RollbackProcessor; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * @inheritDoc + */ +class RollbackProcessorTest extends TestCase +{ + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var Applier|MockObject + */ + private $applier; + + /** + * @var RollbackProcessor + */ + private $rollbackProcessor; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->applier = $this->createMock(Applier::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + + $this->rollbackProcessor = new RollbackProcessor( + $this->applier, + $this->logger + ); + } + + /** + * Tests patch conflict processing. + */ + public function testProcess() + { + $patch1 = $this->createPatch('MC-1', 'path1'); + $patch2 = $this->createPatch('MC-2', 'path2'); + $expectedMessages = [ + 'Start of rollback', + 'Patch MC-2 has been reverted', + 'Patch MC-1 has been reverted', + 'End of rollback' + ]; + + $this->applier->method('revert') + ->willReturnMap([ + [$patch1->getPath(), $patch1->getId(), 'Patch ' . $patch1->getId() .' has been reverted'], + [$patch2->getPath(), $patch2->getId(), 'Patch ' . $patch2->getId() .' has been reverted'], + ]); + + $this->assertEquals( + $expectedMessages, + $this->rollbackProcessor->process([$patch1, $patch2]) + ); + } + + /** + * Tests with empty passing argument. + */ + public function testProcessWithEmptyArray() + { + $this->applier->expects($this->never()) + ->method('revert'); + + $this->assertEmpty($this->rollbackProcessor->process([])); + } + + /** + * Creates patch mock. + * + * @param string $id + * @param string $path + * @return PatchInterface|MockObject + */ + private function createPatch(string $id, string $path) + { + $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch->method('getId')->willReturn($id); + $patch->method('getPath')->willReturn($path); + + return $patch; + } +} diff --git a/src/Test/Unit/Patch/SourceProviderTest.php b/src/Test/Unit/Patch/SourceProviderTest.php index f4ad4534..3ad8ca52 100644 --- a/src/Test/Unit/Patch/SourceProviderTest.php +++ b/src/Test/Unit/Patch/SourceProviderTest.php @@ -7,13 +7,13 @@ namespace Magento\CloudPatches\Test\Unit\Patch; +use Magento\CloudPatches\Composer\QualityPackage; use Magento\CloudPatches\Filesystem\DirectoryList; use Magento\CloudPatches\Filesystem\FileList; use Magento\CloudPatches\Filesystem\Filesystem; use Magento\CloudPatches\Filesystem\FileSystemException; use Magento\CloudPatches\Patch\SourceProvider; use Magento\CloudPatches\Patch\SourceProviderException; -use Magento\QualityPatches\Info as QualityPatchesInfo; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -38,9 +38,9 @@ class SourceProviderTest extends TestCase private $directoryList; /** - * @var QualityPatchesInfo|MockObject + * @var QualityPackage|MockObject */ - private $qualityPatchesInfo; + private $qualityPackage; /** * @var FileList|MockObject @@ -55,13 +55,13 @@ protected function setUp() $this->filesystem = $this->createMock(Filesystem::class); $this->filelist = $this->createMock(FileList::class); $this->directoryList = $this->createMock(DirectoryList::class); - $this->qualityPatchesInfo = $this->createMock(QualityPatchesInfo::class); + $this->qualityPackage = $this->createMock(QualityPackage::class); $this->sourceProvider = new SourceProvider( $this->filesystem, $this->filelist, $this->directoryList, - $this->qualityPatchesInfo + $this->qualityPackage ); } @@ -95,7 +95,7 @@ public function testGetQualityPatches() $configSource = require __DIR__ . '/Collector/Fixture/quality_config_valid.php'; $jsonConfig = json_encode($configSource); - $this->qualityPatchesInfo->expects($this->once()) + $this->qualityPackage->expects($this->once()) ->method('getPatchesConfig') ->willReturn($configPath); @@ -107,6 +107,23 @@ public function testGetQualityPatches() $this->assertEquals($configSource, $this->sourceProvider->getQualityPatches()); } + /** + * Tests retrieving Quality patch configuration when config path is null. + * + * Case when magento/quality-patches package is not installed. + */ + public function testGetQualityPatchesWithNullConfigPath() + { + $this->qualityPackage->expects($this->once()) + ->method('getPatchesConfig') + ->willReturn(null); + + $this->filesystem->expects($this->never()) + ->method('get'); + + $this->assertEquals([], $this->sourceProvider->getQualityPatches()); + } + /** * Tests retrieving Local patch configuration. */ @@ -130,7 +147,7 @@ public function testGetQualityPatchesFilesystemException() { $configPath = '/quality/patches.json'; - $this->qualityPatchesInfo->expects($this->once()) + $this->qualityPackage->expects($this->once()) ->method('getPatchesConfig') ->willReturn($configPath); @@ -149,7 +166,7 @@ public function testGetQualityPatchesJsonException() { $configPath = '/quality/patches.json'; - $this->qualityPatchesInfo->expects($this->once()) + $this->qualityPackage->expects($this->once()) ->method('getPatchesConfig') ->willReturn($configPath); From 7dbcd5594ba31ce16efdc7bcd1000255f0ce0f84 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Wed, 29 Jul 2020 12:59:17 -0500 Subject: [PATCH 048/157] MCLOUD-6139 - MCLOUD-6211: Redis improvement - Create patches for 2.3.0 - 2.3.5 (#52) --- patches.json | 18 +- ...add_zookeeper_and_flock_locks__2.3.0.patch | 147 +- ...add_zookeeper_and_flock_locks__2.3.1.patch | 1059 +++++++ ...11__redis_improvement_patches__2.3.0.patch | 2441 +++++++++++++++++ ...11__redis_improvement_patches__2.3.1.patch | 2060 ++++++++++++++ ...11__redis_improvement_patches__2.3.2.patch | 1486 ++++++++++ ...redis_improvement_patches__2.3.3-p1.patch} | 761 ++++- ...1__redis_improvement_patches__2.3.3.patch} | 875 ++++-- ...1__redis_improvement_patches__2.3.4.patch} | 622 +++-- ...11__redis_improvement_patches__2.3.5.patch | 382 +++ ...6139__improvement_flock_locks__2.3.2.patch | 19 + 11 files changed, 9264 insertions(+), 606 deletions(-) create mode 100644 patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.1.patch create mode 100644 patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch create mode 100644 patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch create mode 100644 patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch rename patches/{MDVA-26795__fix_performance_issue_in_cache_locking_mechanism__2.3.4.patch => MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch} (50%) rename patches/{MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3.patch => MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch} (69%) rename patches/{MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3-p1.patch => MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch} (86%) create mode 100644 patches/MCLOUD-6211__redis_improvement_patches__2.3.5.patch create mode 100644 patches/MCLOUD_6139__improvement_flock_locks__2.3.2.patch diff --git a/patches.json b/patches.json index 7ec44660..a8d786da 100644 --- a/patches.json +++ b/patches.json @@ -70,7 +70,9 @@ }, "Add Zookeeper and flock locks": { "2.2.5 - 2.2.8": "MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.2.5.patch", - "2.3.0 - 2.3.1": "MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch" + "2.3.0": "MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch", + "2.3.1": "MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.1.patch", + ">=2.3.2 <2.3.5": "MCLOUD_6139__improvement_flock_locks__2.3.2.patch" }, "Reduce memory usage when importing customers and addresses": { "2.1.4 - 2.1.10": "SET-36__fix_oom_during_customer_import__2.1.4.patch", @@ -245,13 +247,17 @@ "Fix load balancer issue": { ">=2.3.4 <2.3.6": "MCLOUD-5837__fix_filesystem_load_balancer_issue__2.3.4.patch" }, - "Cache Locking performance issue": { - "2.3.3": "MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3.patch", - ">=2.3.3-p1 <2.3.4": "MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3-p1.patch", - ">=2.3.4 <2.3.5": "MDVA-26795__fix_performance_issue_in_cache_locking_mechanism__2.3.4.patch" - }, "Large amount of Keys Block Cache": { ">=2.3.1 <2.3.5": "MDVA-22950__large_amount_of_keys_block_cache__2.3.1.patch" + }, + "Redis improvements": { + "2.3.0": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch", + "2.3.1": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch", + ">=2.3.2 <2.3.3": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch", + "2.3.3": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch", + ">=2.3.3-p1 <2.3.4": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch", + ">=2.3.4 <2.3.5": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch", + ">=2.3.5 <2.3.6": "MCLOUD-6211__redis_improvement_patches__2.3.5.patch" } }, "magento/module-paypal": { diff --git a/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch b/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch index f364ba3c..3ee0c700 100644 --- a/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch +++ b/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.0.patch @@ -10,10 +10,11 @@ diff -Naur a/app/etc/di.xml b/app/etc/di.xml <preference for="Magento\Framework\Api\AttributeTypeResolverInterface" type="Magento\Framework\Reflection\AttributeTypeResolver" /> <preference for="Magento\Framework\Api\Search\SearchResultInterface" type="Magento\Framework\Api\Search\SearchResult" /> <preference for="Magento\Framework\Api\Search\SearchCriteriaInterface" type="Magento\Framework\Api\Search\SearchCriteria"/> -diff -Naur a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php +diff --git a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php +new file mode 100644 --- /dev/null +++ b/vendor/magento/framework/Lock/Backend/FileLock.php -@@ -0,0 +1,194 @@ +@@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. @@ -107,6 +108,7 @@ diff -Naur a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento + + while (!$this->tryToLock($fileResource)) { + if (!$skipDeadline && $deadline <= microtime(true)) { ++ $this->tryToUnlock($fileResource); + $this->fileDriver->fileClose($fileResource); + return false; + } @@ -140,6 +142,7 @@ diff -Naur a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento + } else { + $result = true; + } ++ $this->tryToUnlock($fileResource); + $this->fileDriver->fileClose($fileResource); + } + } catch (FileSystemException $exception) { @@ -208,6 +211,7 @@ diff -Naur a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento + } + } +} + diff -Naur a/vendor/magento/framework/Lock/Backend/Zookeeper.php b/vendor/magento/framework/Lock/Backend/Zookeeper.php --- /dev/null +++ b/vendor/magento/framework/Lock/Backend/Zookeeper.php @@ -1053,3 +1057,142 @@ diff -Naur a/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php b/setup/sr + } + } +} +diff -Nuar a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/Lock/Backend/Cache.php +@@ -0,0 +1,134 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++declare(strict_types=1); ++ ++namespace Magento\Framework\Lock\Backend; ++ ++use Magento\Framework\Cache\FrontendInterface; ++ ++/** ++ * Implementation of the lock manager on the basis of the caching system. ++ */ ++class Cache implements \Magento\Framework\Lock\LockManagerInterface ++{ ++ /** ++ * Prefix for marking that key is locked or not. ++ */ ++ const LOCK_PREFIX = 'LOCKED_RECORD_INFO_'; ++ ++ /** ++ * @var FrontendInterface ++ */ ++ private $cache; ++ ++ /** ++ * Sign for locks, helps to avoid removing a lock that was created by another client ++ * ++ * @string ++ */ ++ private $lockSign; ++ ++ /** ++ * @param FrontendInterface $cache ++ */ ++ public function __construct(FrontendInterface $cache) ++ { ++ $this->cache = $cache; ++ $this->lockSign = $this->generateLockSign(); ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ if (empty($this->lockSign)) { ++ $this->lockSign = $this->generateLockSign(); ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false !== $data) { ++ return false; ++ } ++ ++ $timeout = $timeout <= 0 ? null : $timeout; ++ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if ($data === $this->lockSign) { ++ return true; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function unlock(string $name): bool ++ { ++ if (empty($this->lockSign)) { ++ return false; ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false === $data) { ++ return false; ++ } ++ ++ $removeResult = false; ++ if ($data === $this->lockSign) { ++ $removeResult = (bool)$this->cache->remove($this->getIdentifier($name)); ++ } ++ ++ return $removeResult; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function isLocked(string $name): bool ++ { ++ return (bool)$this->cache->test($this->getIdentifier($name)); ++ } ++ ++ /** ++ * Get cache locked identifier based on cache identifier. ++ * ++ * @param string $cacheIdentifier ++ * @return string ++ */ ++ private function getIdentifier(string $cacheIdentifier): string ++ { ++ return self::LOCK_PREFIX . $cacheIdentifier; ++ } ++ ++ /** ++ * Function that generates lock sign that helps to avoid removing a lock that was created by another client. ++ * ++ * @return string ++ */ ++ private function generateLockSign() ++ { ++ $sign = implode( ++ '-', ++ [ ++ \getmypid(), \crc32(\gethostname()) ++ ] ++ ); ++ ++ try { ++ $sign .= '-' . \bin2hex(\random_bytes(4)); ++ } catch (\Exception $e) { ++ $sign .= '-' . \uniqid('-uniqid-'); ++ } ++ ++ return $sign; ++ } ++} diff --git a/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.1.patch b/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.1.patch new file mode 100644 index 00000000..2ba83d8b --- /dev/null +++ b/patches/MAGECLOUD-3054__add_zookeeper_and_flock_locks__2.3.1.patch @@ -0,0 +1,1059 @@ +diff -Naur a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -38,7 +38,7 @@ + <preference for="Magento\Framework\Locale\ListsInterface" type="Magento\Framework\Locale\TranslatedLists" /> + <preference for="Magento\Framework\Locale\AvailableLocalesInterface" type="Magento\Framework\Locale\Deployed\Codes" /> + <preference for="Magento\Framework\Locale\OptionInterface" type="Magento\Framework\Locale\Deployed\Options" /> +- <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Backend\Database" /> ++ <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Proxy" /> + <preference for="Magento\Framework\Api\AttributeTypeResolverInterface" type="Magento\Framework\Reflection\AttributeTypeResolver" /> + <preference for="Magento\Framework\Api\Search\SearchResultInterface" type="Magento\Framework\Api\Search\SearchResult" /> + <preference for="Magento\Framework\Api\Search\SearchCriteriaInterface" type="Magento\Framework\Api\Search\SearchCriteria"/> +diff --git a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/Lock/Backend/FileLock.php +@@ -0,0 +1,196 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++declare(strict_types=1); ++ ++namespace Magento\Framework\Lock\Backend; ++ ++use Magento\Framework\Lock\LockManagerInterface; ++use Magento\Framework\Filesystem\Driver\File as FileDriver; ++use Magento\Framework\Exception\RuntimeException; ++use Magento\Framework\Exception\FileSystemException; ++use Magento\Framework\Phrase; ++ ++/** ++ * LockManager using the file system for locks ++ */ ++class FileLock implements LockManagerInterface ++{ ++ /** ++ * The file driver instance ++ * ++ * @var FileDriver ++ */ ++ private $fileDriver; ++ ++ /** ++ * The path to the locks storage folder ++ * ++ * @var string ++ */ ++ private $path; ++ ++ /** ++ * How many microseconds to wait before re-try to acquire a lock ++ * ++ * @var int ++ */ ++ private $sleepCycle = 100000; ++ ++ /** ++ * The mapping list of the path lock with the file resource ++ * ++ * @var array ++ */ ++ private $locks = []; ++ ++ /** ++ * @param FileDriver $fileDriver The file driver ++ * @param string $path The path to the locks storage folder ++ * @throws RuntimeException Throws RuntimeException if $path is empty ++ * or cannot create the directory for locks ++ */ ++ public function __construct(FileDriver $fileDriver, string $path) ++ { ++ if (!$path) { ++ throw new RuntimeException(new Phrase('The path needs to be a non-empty string.')); ++ } ++ ++ $this->fileDriver = $fileDriver; ++ $this->path = rtrim($path, '/') . '/'; ++ ++ try { ++ if (!$this->fileDriver->isExists($this->path)) { ++ $this->fileDriver->createDirectory($this->path); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException( ++ new Phrase('Cannot create the directory for locks: %1', [$this->path]), ++ $exception ++ ); ++ } ++ } ++ ++ /** ++ * Acquires a lock by name ++ * ++ * @param string $name The lock name ++ * @param int $timeout Timeout in seconds. A negative timeout value means infinite timeout ++ * @return bool Returns true if the lock is acquired, otherwise returns false ++ * @throws RuntimeException Throws RuntimeException if cannot acquires the lock because FS problems ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ try { ++ $lockFile = $this->getLockPath($name); ++ $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); ++ $skipDeadline = $timeout < 0; ++ $deadline = microtime(true) + $timeout; ++ ++ while (!$this->tryToLock($fileResource)) { ++ if (!$skipDeadline && $deadline <= microtime(true)) { ++ $this->tryToUnlock($fileResource); ++ $this->fileDriver->fileClose($fileResource); ++ return false; ++ } ++ usleep($this->sleepCycle); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException(new Phrase('Cannot acquire a lock.'), $exception); ++ } ++ ++ $this->locks[$lockFile] = $fileResource; ++ return true; ++ } ++ ++ /** ++ * Checks if a lock exists by name ++ * ++ * @param string $name The lock name ++ * @return bool Returns true if the lock exists, otherwise returns false ++ * @throws RuntimeException Throws RuntimeException if cannot check that the lock exists ++ */ ++ public function isLocked(string $name): bool ++ { ++ $lockFile = $this->getLockPath($name); ++ $result = false; ++ ++ try { ++ if ($this->fileDriver->isExists($lockFile)) { ++ $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); ++ if ($this->tryToLock($fileResource)) { ++ $result = false; ++ } else { ++ $result = true; ++ } ++ $this->tryToUnlock($fileResource); ++ $this->fileDriver->fileClose($fileResource); ++ } ++ } catch (FileSystemException $exception) { ++ throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); ++ } ++ ++ return $result; ++ } ++ ++ /** ++ * Remove the lock by name ++ * ++ * @param string $name The lock name ++ * @return bool If the lock is removed returns true, otherwise returns false ++ */ ++ public function unlock(string $name): bool ++ { ++ $lockFile = $this->getLockPath($name); ++ ++ if (isset($this->locks[$lockFile]) && $this->tryToUnlock($this->locks[$lockFile])) { ++ unset($this->locks[$lockFile]); ++ return true; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * Returns the full path to the lock file by name ++ * ++ * @param string $name The lock name ++ * @return string The path to the lock file ++ */ ++ private function getLockPath(string $name): string ++ { ++ return $this->path . $name; ++ } ++ ++ /** ++ * Tries to lock a file resource ++ * ++ * @param resource $resource The file resource ++ * @return bool If the lock is acquired returns true, otherwise returns false ++ */ ++ private function tryToLock($resource): bool ++ { ++ try { ++ return $this->fileDriver->fileLock($resource, LOCK_EX | LOCK_NB); ++ } catch (FileSystemException $exception) { ++ return false; ++ } ++ } ++ ++ /** ++ * Tries to unlock a file resource ++ * ++ * @param resource $resource The file resource ++ * @return bool If the lock is removed returns true, otherwise returns false ++ */ ++ private function tryToUnlock($resource): bool ++ { ++ try { ++ return $this->fileDriver->fileLock($resource, LOCK_UN | LOCK_NB); ++ } catch (FileSystemException $exception) { ++ return false; ++ } ++ } ++} + +diff -Naur a/vendor/magento/framework/Lock/Backend/Zookeeper.php b/vendor/magento/framework/Lock/Backend/Zookeeper.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/Backend/Zookeeper.php +@@ -0,0 +1,280 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++declare(strict_types=1); ++ ++namespace Magento\Framework\Lock\Backend; ++ ++use Magento\Framework\Lock\LockManagerInterface; ++use Magento\Framework\Exception\RuntimeException; ++use Magento\Framework\Phrase; ++ ++/** ++ * LockManager using the Zookeeper for locks ++ */ ++class Zookeeper implements LockManagerInterface ++{ ++ /** ++ * Zookeeper provider ++ * ++ * @var \Zookeeper ++ */ ++ private $zookeeper; ++ ++ /** ++ * The base path to locks in Zookeeper ++ * ++ * @var string ++ */ ++ private $path; ++ ++ /** ++ * The name of sequence nodes ++ * ++ * @var string ++ */ ++ private $lockName = 'lock-'; ++ ++ /** ++ * The host to connect to Zookeeper ++ * ++ * @var string ++ */ ++ private $host; ++ ++ /** ++ * How many seconds to wait before timing out on connections ++ * ++ * @var int ++ */ ++ private $connectionTimeout = 2; ++ ++ /** ++ * How many microseconds to wait before recheck connections or nodes ++ * ++ * @var int ++ */ ++ private $sleepCycle = 100000; ++ ++ /** ++ * The default permissions for Zookeeper nodes ++ * ++ * @var array ++ */ ++ private $acl = [['perms'=>\Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']]; ++ ++ /** ++ * The mapping list of the lock name with the full lock path ++ * ++ * @var array ++ */ ++ private $locks = []; ++ ++ /** ++ * The default path to storage locks ++ */ ++ const DEFAULT_PATH = '/magento/locks'; ++ ++ /** ++ * @param string $host The host to connect to Zookeeper ++ * @param string $path The base path to locks in Zookeeper ++ * @throws RuntimeException ++ */ ++ public function __construct(string $host, string $path = self::DEFAULT_PATH) ++ { ++ if (!$path) { ++ throw new RuntimeException( ++ new Phrase('The path needs to be a non-empty string.') ++ ); ++ } ++ ++ if (!$host) { ++ throw new RuntimeException( ++ new Phrase('The host needs to be a non-empty string.') ++ ); ++ } ++ ++ $this->host = $host; ++ $this->path = rtrim($path, '/') . '/'; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * You can see the lock algorithm by the link ++ * @link https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks ++ * ++ * @throws RuntimeException ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ $skipDeadline = $timeout < 0; ++ $lockPath = $this->getFullPathToLock($name); ++ $deadline = microtime(true) + $timeout; ++ ++ if (!$this->checkAndCreateParentNode($lockPath)) { ++ throw new RuntimeException(new Phrase('Failed creating the path %1', [$lockPath])); ++ } ++ ++ $lockKey = $this->getProvider() ++ ->create($lockPath, '1', $this->acl, \Zookeeper::EPHEMERAL | \Zookeeper::SEQUENCE); ++ ++ if (!$lockKey) { ++ throw new RuntimeException(new Phrase('Failed creating lock %1', [$lockPath])); ++ } ++ ++ while ($this->isAnyLock($lockKey, $this->getIndex($lockKey))) { ++ if (!$skipDeadline && $deadline <= microtime(true)) { ++ $this->getProvider()->delete($lockKey); ++ return false; ++ } ++ ++ usleep($this->sleepCycle); ++ } ++ ++ $this->locks[$name] = $lockKey; ++ ++ return true; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function unlock(string $name): bool ++ { ++ if (!isset($this->locks[$name])) { ++ return false; ++ } ++ ++ return $this->getProvider()->delete($this->locks[$name]); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function isLocked(string $name): bool ++ { ++ return $this->isAnyLock($this->getFullPathToLock($name)); ++ } ++ ++ /** ++ * Gets full path to lock by its name ++ * ++ * @param string $name ++ * @return string ++ */ ++ private function getFullPathToLock(string $name): string ++ { ++ return $this->path . $name . '/' . $this->lockName; ++ } ++ ++ /** ++ * Initiolizes and returns Zookeeper provider ++ * ++ * @return \Zookeeper ++ * @throws RuntimeException ++ */ ++ private function getProvider(): \Zookeeper ++ { ++ if (!$this->zookeeper) { ++ $this->zookeeper = new \Zookeeper($this->host); ++ } ++ ++ $deadline = microtime(true) + $this->connectionTimeout; ++ while ($this->zookeeper->getState() != \Zookeeper::CONNECTED_STATE) { ++ if ($deadline <= microtime(true)) { ++ throw new RuntimeException(new Phrase('Zookeeper connection timed out!')); ++ } ++ usleep($this->sleepCycle); ++ } ++ ++ return $this->zookeeper; ++ } ++ ++ /** ++ * Checks and creates base path recursively ++ * ++ * @param string $path ++ * @return bool ++ * @throws RuntimeException ++ */ ++ private function checkAndCreateParentNode(string $path): bool ++ { ++ $path = dirname($path); ++ if ($this->getProvider()->exists($path)) { ++ return true; ++ } ++ ++ if (!$this->checkAndCreateParentNode($path)) { ++ return false; ++ } ++ ++ if ($this->getProvider()->create($path, '1', $this->acl)) { ++ return true; ++ } ++ ++ return $this->getProvider()->exists($path); ++ } ++ ++ /** ++ * Gets int increment of lock key ++ * ++ * @param string $key ++ * @return int|null ++ */ ++ private function getIndex(string $key) ++ { ++ if (!preg_match('/' . $this->lockName . '([0-9]+)$/', $key, $matches)) { ++ return null; ++ } ++ ++ return intval($matches[1]); ++ } ++ ++ /** ++ * Checks if there is any sequence node under parent of $fullKey. ++ * ++ * At first checks that the $fullKey node is present, if not - returns false. ++ * If $indexKey is non-null and there is a smaller index than $indexKey then returns true, ++ * otherwise returns false. ++ * ++ * @param string $fullKey The full path without any sequence info ++ * @param int|null $indexKey The index to compare ++ * @return bool ++ * @throws RuntimeException ++ */ ++ private function isAnyLock(string $fullKey, int $indexKey = null): bool ++ { ++ $parent = dirname($fullKey); ++ ++ if (!$this->getProvider()->exists($parent)) { ++ return false; ++ } ++ ++ $children = $this->getProvider()->getChildren($parent); ++ ++ if (null === $indexKey && !empty($children)) { ++ return true; ++ } ++ ++ foreach ($children as $childKey) { ++ $childIndex = $this->getIndex($childKey); ++ ++ if (null === $childIndex) { ++ continue; ++ } ++ ++ if ($childIndex < $indexKey) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++} +diff -Naur a/vendor/magento/framework/Lock/LockBackendFactory.php b/vendor/magento/framework/Lock/LockBackendFactory.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/LockBackendFactory.php +@@ -0,0 +1,111 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++declare(strict_types=1); ++ ++namespace Magento\Framework\Lock; ++ ++use Magento\Framework\Phrase; ++use Magento\Framework\Exception\RuntimeException; ++use Magento\Framework\ObjectManagerInterface; ++use Magento\Framework\App\DeploymentConfig; ++use Magento\Framework\Lock\Backend\Database as DatabaseLock; ++use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; ++use Magento\Framework\Lock\Backend\Cache as CacheLock; ++use Magento\Framework\Lock\Backend\FileLock; ++ ++/** ++ * The factory to create object that implements LockManagerInterface ++ */ ++class LockBackendFactory ++{ ++ /** ++ * The Object Manager instance ++ * ++ * @var ObjectManagerInterface ++ */ ++ private $objectManager; ++ ++ /** ++ * The Application deployment configuration ++ * ++ * @var DeploymentConfig ++ */ ++ private $deploymentConfig; ++ ++ /** ++ * DB lock provider name ++ * ++ * @const string ++ */ ++ const LOCK_DB = 'db'; ++ ++ /** ++ * Zookeeper lock provider name ++ * ++ * @const string ++ */ ++ const LOCK_ZOOKEEPER = 'zookeeper'; ++ ++ /** ++ * Cache lock provider name ++ * ++ * @const string ++ */ ++ const LOCK_CACHE = 'cache'; ++ ++ /** ++ * File lock provider name ++ * ++ * @const string ++ */ ++ const LOCK_FILE = 'file'; ++ ++ /** ++ * The list of lock providers with mapping on classes ++ * ++ * @var array ++ */ ++ private $lockers = [ ++ self::LOCK_DB => DatabaseLock::class, ++ self::LOCK_ZOOKEEPER => ZookeeperLock::class, ++ self::LOCK_CACHE => CacheLock::class, ++ self::LOCK_FILE => FileLock::class, ++ ]; ++ ++ /** ++ * @param ObjectManagerInterface $objectManager The Object Manager instance ++ * @param DeploymentConfig $deploymentConfig The Application deployment configuration ++ */ ++ public function __construct( ++ ObjectManagerInterface $objectManager, ++ DeploymentConfig $deploymentConfig ++ ) { ++ $this->objectManager = $objectManager; ++ $this->deploymentConfig = $deploymentConfig; ++ } ++ ++ /** ++ * Creates an instance of LockManagerInterface using information from deployment config ++ * ++ * @return LockManagerInterface ++ * @throws RuntimeException ++ */ ++ public function create(): LockManagerInterface ++ { ++ $provider = $this->deploymentConfig->get('lock/provider', self::LOCK_DB); ++ $config = $this->deploymentConfig->get('lock/config', []); ++ ++ if (!isset($this->lockers[$provider])) { ++ throw new RuntimeException(new Phrase('Unknown locks provider: %1', [$provider])); ++ } ++ ++ if (self::LOCK_ZOOKEEPER === $provider && !extension_loaded(self::LOCK_ZOOKEEPER)) { ++ throw new RuntimeException(new Phrase('php extension Zookeeper is not installed.')); ++ } ++ ++ return $this->objectManager->create($this->lockers[$provider], $config); ++ } ++} +diff -Naur a/vendor/magento/framework/Lock/Proxy.php b/vendor/magento/framework/Lock/Proxy.php +--- /dev/null ++++ b/vendor/magento/framework/Lock/Proxy.php +@@ -0,0 +1,83 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++declare(strict_types=1); ++ ++namespace Magento\Framework\Lock; ++ ++use Magento\Framework\Exception\RuntimeException; ++ ++/** ++ * Proxy for LockManagers ++ */ ++class Proxy implements LockManagerInterface ++{ ++ /** ++ * The factory to create LockManagerInterface implementation ++ * ++ * @var LockBackendFactory ++ */ ++ private $factory; ++ ++ /** ++ * A LockManagerInterface implementation ++ * ++ * @var LockManagerInterface ++ */ ++ private $locker; ++ ++ /** ++ * @param LockBackendFactory $factory The factory to create LockManagerInterface implementation ++ */ ++ public function __construct(LockBackendFactory $factory) ++ { ++ $this->factory = $factory; ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function isLocked(string $name): bool ++ { ++ return $this->getLocker()->isLocked($name); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function lock(string $name, int $timeout = -1): bool ++ { ++ return $this->getLocker()->lock($name, $timeout); ++ } ++ ++ /** ++ * @inheritdoc ++ * ++ * @throws RuntimeException ++ */ ++ public function unlock(string $name): bool ++ { ++ return $this->getLocker()->unlock($name); ++ } ++ ++ /** ++ * Gets LockManagerInterface implementation using Factory ++ * ++ * @return LockManagerInterface ++ * @throws RuntimeException ++ */ ++ private function getLocker(): LockManagerInterface ++ { ++ if (!$this->locker) { ++ $this->locker = $this->factory->create(); ++ } ++ ++ return $this->locker; ++ } ++} +diff -Naur a/setup/src/Magento/Setup/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList.php +@@ -50,7 +50,8 @@ class ConfigOptionsList implements ConfigOptionsListInterface + private $configOptionsListClasses = [ + \Magento\Setup\Model\ConfigOptionsList\Session::class, + \Magento\Setup\Model\ConfigOptionsList\Cache::class, +- \Magento\Setup\Model\ConfigOptionsList\PageCache::class ++ \Magento\Setup\Model\ConfigOptionsList\PageCache::class, ++ \Magento\Setup\Model\ConfigOptionsList\Lock::class, + ]; + + /** +diff -Naur a/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php +--- /dev/null ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php +@@ -0,0 +1,342 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++declare(strict_types=1); ++ ++namespace Magento\Setup\Model\ConfigOptionsList; ++ ++use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; ++use Magento\Framework\Lock\LockBackendFactory; ++use Magento\Framework\App\DeploymentConfig; ++use Magento\Framework\Config\Data\ConfigData; ++use Magento\Framework\Config\File\ConfigFilePool; ++use Magento\Framework\Setup\ConfigOptionsListInterface; ++use Magento\Framework\Setup\Option\SelectConfigOption; ++use Magento\Framework\Setup\Option\TextConfigOption; ++ ++/** ++ * Deployment configuration options for locks ++ */ ++class Lock implements ConfigOptionsListInterface ++{ ++ /** ++ * The name of an option to set lock provider ++ * ++ * @const string ++ */ ++ const INPUT_KEY_LOCK_PROVIDER = 'lock-provider'; ++ ++ /** ++ * The name of an option to set DB prefix ++ * ++ * @const string ++ */ ++ const INPUT_KEY_LOCK_DB_PREFIX = 'lock-db-prefix'; ++ ++ /** ++ * The name of an option to set Zookeeper host ++ * ++ * @const string ++ */ ++ const INPUT_KEY_LOCK_ZOOKEEPER_HOST = 'lock-zookeeper-host'; ++ ++ /** ++ * The name of an option to set Zookeeper path ++ * ++ * @const string ++ */ ++ const INPUT_KEY_LOCK_ZOOKEEPER_PATH = 'lock-zookeeper-path'; ++ ++ /** ++ * The name of an option to set File path ++ * ++ * @const string ++ */ ++ const INPUT_KEY_LOCK_FILE_PATH = 'lock-file-path'; ++ ++ /** ++ * The configuration path to save lock provider ++ * ++ * @const string ++ */ ++ const CONFIG_PATH_LOCK_PROVIDER = 'lock/provider'; ++ ++ /** ++ * The configuration path to save DB prefix ++ * ++ * @const string ++ */ ++ const CONFIG_PATH_LOCK_DB_PREFIX = 'lock/config/prefix'; ++ ++ /** ++ * The configuration path to save Zookeeper host ++ * ++ * @const string ++ */ ++ const CONFIG_PATH_LOCK_ZOOKEEPER_HOST = 'lock/config/host'; ++ ++ /** ++ * The configuration path to save Zookeeper path ++ * ++ * @const string ++ */ ++ const CONFIG_PATH_LOCK_ZOOKEEPER_PATH = 'lock/config/path'; ++ ++ /** ++ * The configuration path to save locks directory path ++ * ++ * @const string ++ */ ++ const CONFIG_PATH_LOCK_FILE_PATH = 'lock/config/path'; ++ ++ /** ++ * The list of lock providers ++ * ++ * @var array ++ */ ++ private $validLockProviders = [ ++ LockBackendFactory::LOCK_DB, ++ LockBackendFactory::LOCK_ZOOKEEPER, ++ LockBackendFactory::LOCK_CACHE, ++ LockBackendFactory::LOCK_FILE, ++ ]; ++ ++ /** ++ * The mapping input keys with their configuration paths ++ * ++ * @var array ++ */ ++ private $mappingInputKeyToConfigPath = [ ++ LockBackendFactory::LOCK_DB => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_DB_PREFIX => self::CONFIG_PATH_LOCK_DB_PREFIX, ++ ], ++ LockBackendFactory::LOCK_ZOOKEEPER => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_HOST => self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ ], ++ LockBackendFactory::LOCK_CACHE => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ ], ++ LockBackendFactory::LOCK_FILE => [ ++ self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, ++ self::INPUT_KEY_LOCK_FILE_PATH => self::CONFIG_PATH_LOCK_FILE_PATH, ++ ], ++ ]; ++ ++ /** ++ * The list of default values ++ * ++ * @var array ++ */ ++ private $defaultConfigValues = [ ++ self::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, ++ self::INPUT_KEY_LOCK_DB_PREFIX => null, ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => ZookeeperLock::DEFAULT_PATH, ++ ]; ++ ++ /** ++ * @inheritdoc ++ */ ++ public function getOptions() ++ { ++ return [ ++ new SelectConfigOption( ++ self::INPUT_KEY_LOCK_PROVIDER, ++ SelectConfigOption::FRONTEND_WIZARD_SELECT, ++ $this->validLockProviders, ++ self::CONFIG_PATH_LOCK_PROVIDER, ++ 'Lock provider name', ++ LockBackendFactory::LOCK_DB ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_DB_PREFIX, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_DB_PREFIX, ++ 'Installation specific lock prefix to avoid lock conflicts' ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_ZOOKEEPER_HOST, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ 'Host and port to connect to Zookeeper cluster. For example: 127.0.0.1:2181' ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_ZOOKEEPER_PATH, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ 'The path where Zookeeper will save locks. The default path is: ' . ZookeeperLock::DEFAULT_PATH ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_LOCK_FILE_PATH, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_LOCK_FILE_PATH, ++ 'The path where file locks will be saved.' ++ ), ++ ]; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function createConfig(array $options, DeploymentConfig $deploymentConfig) ++ { ++ $configData = new ConfigData(ConfigFilePool::APP_ENV); ++ $configData->setOverrideWhenSave(true); ++ $lockProvider = $this->getLockProvider($options, $deploymentConfig); ++ ++ $this->setDefaultConfiguration($configData, $deploymentConfig, $lockProvider); ++ ++ foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { ++ if (isset($options[$input])) { ++ $configData->set($path, $options[$input]); ++ } ++ } ++ ++ return $configData; ++ } ++ ++ /** ++ * @inheritdoc ++ */ ++ public function validate(array $options, DeploymentConfig $deploymentConfig) ++ { ++ $lockProvider = $this->getLockProvider($options, $deploymentConfig); ++ switch ($lockProvider) { ++ case LockBackendFactory::LOCK_ZOOKEEPER: ++ $errors = $this->validateZookeeperConfig($options, $deploymentConfig); ++ break; ++ case LockBackendFactory::LOCK_FILE: ++ $errors = $this->validateFileConfig($options, $deploymentConfig); ++ break; ++ case LockBackendFactory::LOCK_CACHE: ++ case LockBackendFactory::LOCK_DB: ++ $errors = []; ++ break; ++ default: ++ $errors[] = 'The lock provider ' . $lockProvider . ' does not exist.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Validates File locks configuration ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return array ++ */ ++ private function validateFileConfig(array $options, DeploymentConfig $deploymentConfig): array ++ { ++ $errors = []; ++ ++ $path = $options[self::INPUT_KEY_LOCK_FILE_PATH] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_FILE_PATH, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_FILE_PATH) ++ ); ++ ++ if (!$path) { ++ $errors[] = 'The path needs to be a non-empty string.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Validates Zookeeper configuration ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return array ++ */ ++ private function validateZookeeperConfig(array $options, DeploymentConfig $deploymentConfig): array ++ { ++ $errors = []; ++ ++ if (!extension_loaded(LockBackendFactory::LOCK_ZOOKEEPER)) { ++ $errors[] = 'php extension Zookeeper is not installed.'; ++ } ++ ++ $host = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_HOST] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_HOST) ++ ); ++ $path = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_PATH] ++ ?? $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_PATH) ++ ); ++ ++ if (!$path) { ++ $errors[] = 'Zookeeper path needs to be a non-empty string.'; ++ } ++ ++ if (!$host) { ++ $errors[] = 'Zookeeper host is should be set.'; ++ } ++ ++ return $errors; ++ } ++ ++ /** ++ * Returns the name of lock provider ++ * ++ * @param array $options ++ * @param DeploymentConfig $deploymentConfig ++ * @return string ++ */ ++ private function getLockProvider(array $options, DeploymentConfig $deploymentConfig): string ++ { ++ if (!isset($options[self::INPUT_KEY_LOCK_PROVIDER])) { ++ return (string) $deploymentConfig->get( ++ self::CONFIG_PATH_LOCK_PROVIDER, ++ $this->getDefaultValue(self::INPUT_KEY_LOCK_PROVIDER) ++ ); ++ } ++ ++ return (string) $options[self::INPUT_KEY_LOCK_PROVIDER]; ++ } ++ ++ /** ++ * Sets default configuration for locks ++ * ++ * @param ConfigData $configData ++ * @param DeploymentConfig $deploymentConfig ++ * @param string $lockProvider ++ * @return ConfigData ++ */ ++ private function setDefaultConfiguration( ++ ConfigData $configData, ++ DeploymentConfig $deploymentConfig, ++ string $lockProvider ++ ) { ++ foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { ++ $configData->set($path, $deploymentConfig->get($path, $this->getDefaultValue($input))); ++ } ++ ++ return $configData; ++ } ++ ++ /** ++ * Returns default value by input key ++ * ++ * If default value is not set returns null ++ * ++ * @param string $inputKey ++ * @return mixed|null ++ */ ++ private function getDefaultValue(string $inputKey) ++ { ++ if (isset($this->defaultConfigValues[$inputKey])) { ++ return $this->defaultConfigValues[$inputKey]; ++ } else { ++ return null; ++ } ++ } ++} diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch new file mode 100644 index 00000000..b742ebf8 --- /dev/null +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch @@ -0,0 +1,2441 @@ +diff -Nuar a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php +--- a/vendor/magento/module-config/App/Config/Type/System.php ++++ b/vendor/magento/module-config/App/Config/Type/System.php +@@ -3,6 +3,7 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + namespace Magento\Config\App\Config\Type; + + use Magento\Framework\App\Config\ConfigSourceInterface; +@@ -13,10 +14,14 @@ use Magento\Framework\App\ObjectManager; + use Magento\Config\App\Config\Type\System\Reader; + use Magento\Framework\App\ScopeInterface; + use Magento\Framework\Cache\FrontendInterface; ++use Magento\Framework\Cache\LockGuardedCacheLoader; ++use Magento\Framework\Lock\LockManagerInterface; + use Magento\Framework\Serialize\SerializerInterface; + use Magento\Store\Model\Config\Processor\Fallback; +-use Magento\Store\Model\ScopeInterface as StoreScope; + use Magento\Framework\Encryption\Encryptor; ++use Magento\Store\Model\ScopeInterface as StoreScope; ++use Magento\Framework\App\Cache\StateInterface; ++use Magento\Framework\App\Cache\Type\Config; + + /** + * System configuration type +@@ -24,12 +29,25 @@ use Magento\Framework\Encryption\Encryptor; + * @api + * @since 100.1.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + class System implements ConfigTypeInterface + { ++ /** ++ * Config cache tag. ++ */ + const CACHE_TAG = 'config_scopes'; ++ ++ /** ++ * System config type. ++ */ + const CONFIG_TYPE = 'system'; + ++ /** ++ * @var string ++ */ ++ private static $lockName = 'SYSTEM_CONFIG'; ++ + /** + * @var array + */ +@@ -77,6 +95,17 @@ class System implements ConfigTypeInterface + private $encryptor; + + /** ++ * @var LockGuardedCacheLoader ++ */ ++ private $lockQuery; ++ ++ /** ++ * @var StateInterface ++ */ ++ private $cacheState; ++ ++ /** ++ * System constructor. + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback +@@ -87,7 +116,9 @@ class System implements ConfigTypeInterface + * @param string $configType + * @param Reader|null $reader + * @param Encryptor|null $encryptor +- * ++ * @param LockManagerInterface|null $locker ++ * @param LockGuardedCacheLoader|null $lockQuery ++ * @param StateInterface|null $cacheState + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -101,14 +132,22 @@ class System implements ConfigTypeInterface + $cachingNestedLevel = 1, + $configType = self::CONFIG_TYPE, + Reader $reader = null, +- Encryptor $encryptor = null ++ Encryptor $encryptor = null, ++ LockManagerInterface $locker = null, ++ LockGuardedCacheLoader $lockQuery = null, ++ StateInterface $cacheState = null + ) { + $this->postProcessor = $postProcessor; + $this->cache = $cache; + $this->serializer = $serializer; + $this->configType = $configType; + $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); +- $this->encryptor = $encryptor ?: ObjectManager::getInstance()->get(\Magento\Framework\Encryption\Encryptor::class); ++ $this->encryptor = $encryptor ++ ?: ObjectManager::getInstance()->get(Encryptor::class); ++ $this->lockQuery = $lockQuery ++ ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); ++ $this->cacheState = $cacheState ++ ?: ObjectManager::getInstance()->get(StateInterface::class); + } + + /** +@@ -187,45 +226,64 @@ class System implements ConfigTypeInterface + } + + /** +- * Load configuration data for all scopes ++ * Load configuration data for all scopes. + * + * @return array + */ + private function loadAllData() + { +- $cachedData = $this->cache->load($this->configType); +- +- if ($cachedData === false) { +- $data = $this->readData(); +- } else { +- $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); + } + +- return $data; ++ $loadAction = function () { ++ $cachedData = $this->cache->load($this->configType); ++ $data = false; ++ if ($cachedData !== false) { ++ $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); ++ } ++ return $data; ++ }; ++ ++ return $this->lockQuery->lockedLoadData( ++ self::$lockName, ++ $loadAction, ++ \Closure::fromCallable([$this, 'readData']), ++ \Closure::fromCallable([$this, 'cacheData']) ++ ); + } + + /** +- * Load configuration data for default scope ++ * Load configuration data for default scope. + * + * @param string $scopeType + * @return array + */ + private function loadDefaultScopeData($scopeType) + { +- $cachedData = $this->cache->load($this->configType . '_' . $scopeType); +- +- if ($cachedData === false) { +- $data = $this->readData(); +- $this->cacheData($data); +- } else { +- $data = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); + } + +- return $data; ++ $loadAction = function () use ($scopeType) { ++ $cachedData = $this->cache->load($this->configType . '_' . $scopeType); ++ $scopeData = false; ++ if ($cachedData !== false) { ++ $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; ++ } ++ return $scopeData; ++ }; ++ ++ return $this->lockQuery->lockedLoadData( ++ self::$lockName, ++ $loadAction, ++ \Closure::fromCallable([$this, 'readData']), ++ \Closure::fromCallable([$this, 'cacheData']) ++ ); + } + + /** +- * Load configuration data for a specified scope ++ * Load configuration data for a specified scope. + * + * @param string $scopeType + * @param string $scopeId +@@ -233,31 +291,42 @@ class System implements ConfigTypeInterface + */ + private function loadScopeData($scopeType, $scopeId) + { +- $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); +- +- if ($cachedData === false) { +- if ($this->availableDataScopes === null) { +- $cachedScopeData = $this->cache->load($this->configType . '_scopes'); +- if ($cachedScopeData !== false) { +- $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); +- $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ ++ $loadAction = function () use ($scopeType, $scopeId) { ++ $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); ++ $scopeData = false; ++ if ($cachedData === false) { ++ if ($this->availableDataScopes === null) { ++ $cachedScopeData = $this->cache->load($this->configType . '_scopes'); ++ if ($cachedScopeData !== false) { ++ $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); ++ $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); ++ } + } ++ if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { ++ $scopeData = [$scopeType => [$scopeId => []]]; ++ } ++ } else { ++ $serializedCachedData = $this->encryptor->decrypt($cachedData); ++ $scopeData = [$scopeType => [$scopeId => $this->serializer->unserialize($serializedCachedData)]]; + } +- if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { +- return [$scopeType => [$scopeId => []]]; +- } +- $data = $this->readData(); +- $this->cacheData($data); +- } else { +- $serializedCachedData = $this->encryptor->decrypt($cachedData); +- $data = [$scopeType => [$scopeId => $this->serializer->unserialize($serializedCachedData)]]; +- } + +- return $data; ++ return $scopeData; ++ }; ++ ++ return $this->lockQuery->lockedLoadData( ++ self::$lockName, ++ $loadAction, ++ \Closure::fromCallable([$this, 'readData']), ++ \Closure::fromCallable([$this, 'cacheData']) ++ ); + } + + /** +- * Cache configuration data ++ * Cache configuration data. + * + * Caches data per scope to avoid reading data for all scopes on every request + * +@@ -295,7 +364,7 @@ class System implements ConfigTypeInterface + } + + /** +- * Walk nested hash map by keys from $pathParts ++ * Walk nested hash map by keys from $pathParts. + * + * @param array $data to walk in + * @param array $pathParts keys path +@@ -332,7 +401,7 @@ class System implements ConfigTypeInterface + } + + /** +- * Clean cache and global variables cache ++ * Clean cache and global variables cache. + * + * Next items cleared: + * - Internal property intended to store already loaded configuration data +@@ -344,6 +413,17 @@ class System implements ConfigTypeInterface + public function clean() + { + $this->data = []; +- $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); ++ $cleanAction = function () { ++ $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); ++ }; ++ ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $cleanAction(); ++ } ++ ++ $this->lockQuery->lockedCleanData( ++ self::$lockName, ++ $cleanAction ++ ); + } + } +diff -Nuar a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml +--- a/vendor/magento/module-config/etc/di.xml ++++ b/vendor/magento/module-config/etc/di.xml +@@ -77,6 +77,11 @@ + </argument> + </arguments> + </type> ++ <type name="Magento\Framework\Lock\Backend\Cache"> ++ <arguments> ++ <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> ++ </arguments> ++ </type> + <type name="Magento\Config\App\Config\Type\System"> + <arguments> + <argument name="source" xsi:type="object">systemConfigSourceAggregatedProxy</argument> +@@ -85,8 +90,16 @@ + <argument name="preProcessor" xsi:type="object">Magento\Framework\App\Config\PreProcessorComposite</argument> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> + <argument name="reader" xsi:type="object">Magento\Config\App\Config\Type\System\Reader\Proxy</argument> ++ <argument name="lockQuery" xsi:type="object">systemConfigQueryLocker</argument> + </arguments> + </type> ++ ++ <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> ++ <arguments> ++ <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> ++ </arguments> ++ </virtualType> ++ + <type name="Magento\Config\App\Config\Type\System\Reader"> + <arguments> + <argument name="source" xsi:type="object">systemConfigSourceAggregated</argument> +diff -Nuar a/vendor/magento/framework/Interception/Config/Config.php b/vendor/magento/framework/Interception/Config/Config.php +--- a/vendor/magento/framework/Interception/Config/Config.php ++++ b/vendor/magento/framework/Interception/Config/Config.php +@@ -1,15 +1,17 @@ + <?php + /** +- * Interception config. Responsible for providing list of plugins configured for instance +- * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + namespace Magento\Framework\Interception\Config; + + use Magento\Framework\Serialize\SerializerInterface; +-use Magento\Framework\Serialize\Serializer\Serialize; + ++/** ++ * Interception config. ++ * ++ * Responsible for providing list of plugins configured for instance ++ */ + class Config implements \Magento\Framework\Interception\ConfigInterface + { + /** +@@ -35,7 +37,7 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + + /** + * Cache +- * ++ * @deprecated 102.0.1 + * @var \Magento\Framework\Cache\FrontendInterface + */ + protected $_cache; +@@ -74,21 +76,24 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + protected $_scopeList; + + /** +- * @var SerializerInterface ++ * @var CacheManager + */ +- private $serializer; ++ private $cacheManager; + + /** + * Config constructor + * + * @param \Magento\Framework\Config\ReaderInterface $reader + * @param \Magento\Framework\Config\ScopeListInterface $scopeList +- * @param \Magento\Framework\Cache\FrontendInterface $cache ++ * @param \Magento\Framework\Cache\FrontendInterface $cache @deprecated + * @param \Magento\Framework\ObjectManager\RelationsInterface $relations + * @param \Magento\Framework\Interception\ObjectManager\ConfigInterface $omConfig + * @param \Magento\Framework\ObjectManager\DefinitionInterface $classDefinitions + * @param string $cacheId +- * @param SerializerInterface|null $serializer ++ * @param SerializerInterface|null $serializer @deprecated ++ * @param CacheManager $cacheManager ++ * ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __construct( + \Magento\Framework\Config\ReaderInterface $reader, +@@ -98,7 +103,8 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + \Magento\Framework\Interception\ObjectManager\ConfigInterface $omConfig, + \Magento\Framework\ObjectManager\DefinitionInterface $classDefinitions, + $cacheId = 'interception', +- SerializerInterface $serializer = null ++ SerializerInterface $serializer = null, ++ CacheManager $cacheManager = null + ) { + $this->_omConfig = $omConfig; + $this->_relations = $relations; +@@ -107,13 +113,13 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + $this->_cacheId = $cacheId; + $this->_reader = $reader; + $this->_scopeList = $scopeList; +- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() +- ->get(Serialize::class); +- $intercepted = $this->_cache->load($this->_cacheId); +- if ($intercepted !== false) { +- $this->_intercepted = $this->serializer->unserialize($intercepted); ++ $this->cacheManager = ++ $cacheManager ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CacheManager::class); ++ $intercepted = $this->cacheManager->load($cacheId); ++ if ($intercepted !== null) { ++ $this->_intercepted = $intercepted; + } else { +- $this->initialize($this->_classDefinitions->getClasses()); ++ $this->initializeUncompiled($this->_classDefinitions->getClasses()); + } + } + +@@ -125,24 +131,9 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + */ + public function initialize($classDefinitions = []) + { +- $this->_cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [$this->_cacheId]); +- $config = []; +- foreach ($this->_scopeList->getAllScopes() as $scope) { +- $config = array_replace_recursive($config, $this->_reader->read($scope)); +- } +- unset($config['preferences']); +- foreach ($config as $typeName => $typeConfig) { +- if (!empty($typeConfig['plugins'])) { +- $this->_intercepted[ltrim($typeName, '\\')] = true; +- } +- } +- foreach ($config as $typeName => $typeConfig) { +- $this->hasPlugins($typeName); +- } +- foreach ($classDefinitions as $class) { +- $this->hasPlugins($class); +- } +- $this->_cache->save($this->serializer->serialize($this->_intercepted), $this->_cacheId); ++ $this->generateIntercepted($classDefinitions); ++ ++ $this->cacheManager->saveCompiled($this->_cacheId, $this->_intercepted); + } + + /** +@@ -179,7 +170,7 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function hasPlugins($type) + { +@@ -188,4 +179,41 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + } + return $this->_inheritInterception($type); + } ++ ++ /** ++ * Write interception config to cache ++ * ++ * @param array $classDefinitions ++ */ ++ private function initializeUncompiled($classDefinitions = []) ++ { ++ $this->generateIntercepted($classDefinitions); ++ ++ $this->cacheManager->save($this->_cacheId, $this->_intercepted); ++ } ++ ++ /** ++ * Generate intercepted array to store in compiled metadata or frontend cache ++ * ++ * @param array $classDefinitions ++ */ ++ private function generateIntercepted($classDefinitions) ++ { ++ $config = []; ++ foreach ($this->_scopeList->getAllScopes() as $scope) { ++ $config = array_replace_recursive($config, $this->_reader->read($scope)); ++ } ++ unset($config['preferences']); ++ foreach ($config as $typeName => $typeConfig) { ++ if (!empty($typeConfig['plugins'])) { ++ $this->_intercepted[ltrim($typeName, '\\')] = true; ++ } ++ } ++ foreach ($config as $typeName => $typeConfig) { ++ $this->hasPlugins($typeName); ++ } ++ foreach ($classDefinitions as $class) { ++ $this->hasPlugins($class); ++ } ++ } + } +diff -Nuar a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -1742,4 +1742,25 @@ + </argument> + </arguments> + </type> ++ <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> ++ <arguments> ++ <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> ++ </arguments> ++ </type> ++ <virtualType name="configured_block_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">block_html</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Framework\View\Element\Context"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_block_cache</argument> ++ </arguments> ++ </type> ++ <preference for="Magento\Framework\App\ObjectManager\ConfigWriterInterface" type="Magento\Framework\App\ObjectManager\ConfigWriter\Filesystem" /> ++ <type name="Magento\Framework\Interception\Config\CacheManager"> ++ <arguments> ++ <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +@@ -0,0 +1,147 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++ ++namespace Magento\Framework\Cache; ++ ++use Magento\Framework\Lock\LockManagerInterface; ++ ++/** ++ * Default mutex that provide concurrent access to cache storage. ++ */ ++class LockGuardedCacheLoader ++{ ++ /** ++ * @var LockManagerInterface ++ */ ++ private $locker; ++ ++ /** ++ * Lifetime of the lock for write in cache. ++ * ++ * Value of the variable in milliseconds. ++ * ++ * @var int ++ */ ++ private $lockTimeout; ++ ++ /** ++ * Timeout between retrieves to load the configuration from the cache. ++ * ++ * Value of the variable in milliseconds. ++ * ++ * @var int ++ */ ++ private $delayTimeout; ++ ++ /** ++ * Timeout for information to be collected and saved. ++ * If timeout passed that means that data cannot be saved right now. ++ * And we will just return collected data. ++ * ++ * Value of the variable in milliseconds. ++ * ++ * @var int ++ */ ++ private $loadTimeout; ++ ++ /** ++ * Minimal delay timeout in ms. ++ * ++ * @var int ++ */ ++ private $minimalDelayTimeout; ++ ++ /** ++ * @param LockManagerInterface $locker ++ * @param int $lockTimeout ++ * @param int $delayTimeout ++ * @param int $loadTimeout ++ * @param int $minimalDelayTimeout ++ */ ++ public function __construct( ++ LockManagerInterface $locker, ++ int $lockTimeout = 10000, ++ int $delayTimeout = 20, ++ int $loadTimeout = 10000, ++ int $minimalDelayTimeout = 5 ++ ) { ++ $this->locker = $locker; ++ $this->lockTimeout = $lockTimeout; ++ $this->delayTimeout = $delayTimeout; ++ $this->loadTimeout = $loadTimeout; ++ $this->minimalDelayTimeout = $minimalDelayTimeout; ++ } ++ ++ /** ++ * Load data. ++ * ++ * @param string $lockName ++ * @param callable $dataLoader ++ * @param callable $dataCollector ++ * @param callable $dataSaver ++ * @return mixed ++ */ ++ public function lockedLoadData( ++ string $lockName, ++ callable $dataLoader, ++ callable $dataCollector, ++ callable $dataSaver ++ ) { ++ $cachedData = $dataLoader(); //optimistic read ++ $deadline = microtime(true) + $this->loadTimeout / 100; ++ ++ while ($cachedData === false) { ++ if ($deadline <= microtime(true)) { ++ return $dataCollector(); ++ } ++ ++ if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { ++ try { ++ $data = $dataCollector(); ++ $dataSaver($data); ++ $cachedData = $data; ++ } finally { ++ $this->locker->unlock($lockName); ++ } ++ } ++ ++ if ($cachedData === false) { ++ usleep($this->getLookupTimeout() * 1000); ++ $cachedData = $dataLoader(); ++ } ++ } ++ ++ return $cachedData; ++ } ++ ++ /** ++ * Clean data. ++ * ++ * @param string $lockName ++ * @param callable $dataCleaner ++ * @return void ++ */ ++ public function lockedCleanData(string $lockName, callable $dataCleaner) ++ { ++ while ($this->locker->isLocked($lockName)) { ++ usleep($this->getLookupTimeout() * 1000); ++ } ++ ++ $dataCleaner(); ++ } ++ ++ /** ++ * Delay will be applied as rand($minimalDelayTimeout, $delayTimeout). ++ * This helps to desynchronize multiple clients trying ++ * to acquire the lock for the same resource at the same time ++ * ++ * @return int ++ */ ++ private function getLookupTimeout() ++ { ++ return rand($this->minimalDelayTimeout, $this->delayTimeout); ++ } ++} +diff -Nuar a/vendor/magento/framework/View/Element/AbstractBlock.php b/vendor/magento/framework/View/Element/AbstractBlock.php +--- a/vendor/magento/framework/View/Element/AbstractBlock.php ++++ b/vendor/magento/framework/View/Element/AbstractBlock.php +@@ -3,8 +3,10 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + namespace Magento\Framework\View\Element; + ++use Magento\Framework\Cache\LockGuardedCacheLoader; + use Magento\Framework\DataObject\IdentityInterface; + + /** +@@ -14,6 +16,7 @@ use Magento\Framework\DataObject\IdentityInterface; + * + * Marked as public API because it is actively used now. + * ++ * phpcs:disable Magento2.Classes.AbstractApi + * @api + * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) +@@ -52,6 +55,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + * SID Resolver + * + * @var \Magento\Framework\Session\SidResolverInterface ++ * @deprecated 102.0.5 Not used anymore. + */ + protected $_sidResolver; + +@@ -176,14 +180,21 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + */ + protected $_cache; + ++ /** ++ * @var LockGuardedCacheLoader ++ */ ++ private $lockQuery; ++ + /** + * Constructor + * + * @param \Magento\Framework\View\Element\Context $context + * @param array $data + */ +- public function __construct(\Magento\Framework\View\Element\Context $context, array $data = []) +- { ++ public function __construct( ++ \Magento\Framework\View\Element\Context $context, ++ array $data = [] ++ ) { + $this->_request = $context->getRequest(); + $this->_layout = $context->getLayout(); + $this->_eventManager = $context->getEventManager(); +@@ -201,6 +212,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + $this->filterManager = $context->getFilterManager(); + $this->_localeDate = $context->getLocaleDate(); + $this->inlineTranslation = $context->getInlineTranslation(); ++ $this->lockQuery = $context->getLockGuardedCacheLoader(); + if (isset($data['jsLayout'])) { + $this->jsLayout = $data['jsLayout']; + unset($data['jsLayout']); +@@ -235,6 +247,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + * Please override this one instead of overriding real __construct constructor + * + * @return void ++ * phpcs:disable Magento2.CodeAnalysis.EmptyBlock + */ + protected function _construct() + { +@@ -430,9 +443,9 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + */ + public function unsetCallChild($alias, $callback, $result, $params) + { ++ $args = func_get_args(); + $child = $this->getChildBlock($alias); + if ($child) { +- $args = func_get_args(); + $alias = array_shift($args); + $callback = array_shift($args); + $result = (string)array_shift($args); +@@ -659,19 +672,6 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + } + + $html = $this->_loadCache(); +- if ($html === false) { +- if ($this->hasData('translate_inline')) { +- $this->inlineTranslation->suspend($this->getData('translate_inline')); +- } +- +- $this->_beforeToHtml(); +- $html = $this->_toHtml(); +- $this->_saveCache($html); +- +- if ($this->hasData('translate_inline')) { +- $this->inlineTranslation->resume(); +- } +- } + $html = $this->_afterToHtml($html); + + /** @var \Magento\Framework\DataObject */ +@@ -680,10 +680,13 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + 'html' => $html, + ] + ); +- $this->_eventManager->dispatch('view_block_abstract_to_html_after', [ +- 'block' => $this, +- 'transport' => $transportObject +- ]); ++ $this->_eventManager->dispatch( ++ 'view_block_abstract_to_html_after', ++ [ ++ 'block' => $this, ++ 'transport' => $transportObject ++ ] ++ ); + $html = $transportObject->getHtml(); + + return $html; +@@ -726,7 +729,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + */ + public function getUiId($arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null, $arg5 = null) + { +- return ' data-ui-id="' . $this->getJsId($arg1, $arg2, $arg3, $arg4, $arg5) . '" '; ++ return ' data-ui-id="' . $this->escapeHtmlAttr($this->getJsId($arg1, $arg2, $arg3, $arg4, $arg5)) . '" '; + } + + /** +@@ -875,7 +878,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + $namespace = substr( + $className, + 0, +- strpos($className, '\\' . 'Block') ++ strpos($className, '\\' . 'Block' . '\\') + ); + return str_replace('\\', '_', $namespace); + } +@@ -973,8 +976,8 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + * + * Use $addSlashes = false for escaping js that inside html attribute (onClick, onSubmit etc) + * +- * @param string $data +- * @param bool $addSlashes ++ * @param string $data ++ * @param bool $addSlashes + * @return string + * @deprecated 101.0.0 + */ +@@ -1084,23 +1087,43 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + /** + * Load block html from cache storage + * +- * @return string|false ++ * @return string + */ + protected function _loadCache() + { ++ $collectAction = function () { ++ if ($this->hasData('translate_inline')) { ++ $this->inlineTranslation->suspend($this->getData('translate_inline')); ++ } ++ ++ $this->_beforeToHtml(); ++ return $this->_toHtml(); ++ }; ++ + if ($this->getCacheLifetime() === null || !$this->_cacheState->isEnabled(self::CACHE_GROUP)) { +- return false; +- } +- $cacheKey = $this->getCacheKey(); +- $cacheData = $this->_cache->load($cacheKey); +- if ($cacheData) { +- $cacheData = str_replace( +- $this->_getSidPlaceholder($cacheKey), +- $this->_sidResolver->getSessionIdQueryParam($this->_session) . '=' . $this->_session->getSessionId(), +- $cacheData +- ); ++ $html = $collectAction(); ++ if ($this->hasData('translate_inline')) { ++ $this->inlineTranslation->resume(); ++ } ++ return $html; + } +- return $cacheData; ++ $loadAction = function () { ++ return $this->_cache->load($this->getCacheKey()); ++ }; ++ ++ $saveAction = function ($data) { ++ $this->_saveCache($data); ++ if ($this->hasData('translate_inline')) { ++ $this->inlineTranslation->resume(); ++ } ++ }; ++ ++ return (string)$this->lockQuery->lockedLoadData( ++ $this->getCacheKey(), ++ $loadAction, ++ $collectAction, ++ $saveAction ++ ); + } + + /** +@@ -1115,11 +1138,6 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + return false; + } + $cacheKey = $this->getCacheKey(); +- $data = str_replace( +- $this->_sidResolver->getSessionIdQueryParam($this->_session) . '=' . $this->_session->getSessionId(), +- $this->_getSidPlaceholder($cacheKey), +- $data +- ); + + $this->_cache->save($data, $cacheKey, array_unique($this->getCacheTags()), $this->getCacheLifetime()); + return $this; +diff -Nuar a/vendor/magento/framework/View/Element/Context.php b/vendor/magento/framework/View/Element/Context.php +--- a/vendor/magento/framework/View/Element/Context.php ++++ b/vendor/magento/framework/View/Element/Context.php +@@ -5,6 +5,9 @@ + */ + namespace Magento\Framework\View\Element; + ++use Magento\Framework\Cache\LockGuardedCacheLoader; ++use Magento\Framework\App\ObjectManager; ++ + /** + * Constructor modification point for Magento\Framework\View\Element\AbstractBlock. + * +@@ -16,8 +19,7 @@ namespace Magento\Framework\View\Element; + * As Magento moves from inheritance-based APIs all such classes will be deprecated together with + * the classes they were introduced for. + * +- * @SuppressWarnings(PHPMD.TooManyFields) +- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD) + * + * @api + * @since 100.0.2 +@@ -137,12 +139,16 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + */ + protected $inlineTranslation; + ++ /** ++ * @var LockGuardedCacheLoader ++ */ ++ private $lockQuery; ++ + /** + * @param \Magento\Framework\App\RequestInterface $request + * @param \Magento\Framework\View\LayoutInterface $layout + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\UrlInterface $urlBuilder +- * @param \Magento\Framework\TranslateInterface $translator + * @param \Magento\Framework\App\CacheInterface $cache + * @param \Magento\Framework\View\DesignInterface $design + * @param \Magento\Framework\Session\SessionManagerInterface $session +@@ -156,6 +162,7 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + * @param \Magento\Framework\Filter\FilterManager $filterManager + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation ++ * @param LockGuardedCacheLoader $lockQuery + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -176,7 +183,8 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + \Magento\Framework\Escaper $escaper, + \Magento\Framework\Filter\FilterManager $filterManager, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, +- \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation ++ \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, ++ LockGuardedCacheLoader $lockQuery = null + ) { + $this->_request = $request; + $this->_layout = $layout; +@@ -195,6 +203,7 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + $this->_filterManager = $filterManager; + $this->_localeDate = $localeDate; + $this->inlineTranslation = $inlineTranslation; ++ $this->lockQuery = $lockQuery ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); + } + + /** +@@ -358,10 +367,23 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + } + + /** ++ * Get locale date. ++ * + * @return \Magento\Framework\Stdlib\DateTime\TimezoneInterface + */ + public function getLocaleDate() + { + return $this->_localeDate; + } ++ ++ /** ++ * Lock guarded cache loader. ++ * ++ * @return LockGuardedCacheLoader ++ * @since 102.0.2 ++ */ ++ public function getLockGuardedCacheLoader() ++ { ++ return $this->lockQuery; ++ } + } + +diff -Nuar a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module-eav/Model/Config.php +--- a/vendor/magento/module-eav/Model/Config.php ++++ b/vendor/magento/module-eav/Model/Config.php +@@ -7,12 +7,20 @@ namespace Magento\Eav\Model; + + use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; + use Magento\Eav\Model\Entity\Type; ++use Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface; ++use Magento\Framework\App\Config\ScopeConfigInterface; + use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Model\AbstractModel; + use Magento\Framework\Serialize\SerializerInterface; + + /** ++ * EAV config model. ++ * + * @api ++ * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @since 100.0.2 + */ + class Config +@@ -25,6 +33,11 @@ class Config + const ATTRIBUTES_CODES_CACHE_ID = 'EAV_ENTITY_ATTRIBUTES_CODES'; + /**#@-*/ + ++ /** ++ * Xml path to caching user defined eav attributes configuration. ++ */ ++ private const XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES = 'dev/caching/cache_user_defined_attributes'; ++ + /**#@-*/ + protected $_entityTypeData; + +@@ -116,6 +129,11 @@ class Config + */ + private $serializer; + ++ /** ++ * @var ScopeConfigInterface ++ */ ++ private $scopeConfig; ++ + /** + * Cache of attributes per set + * +@@ -123,13 +141,29 @@ class Config + */ + private $attributesPerSet = []; + ++ /** ++ * Is system attributes loaded flag. ++ * ++ * @var array ++ */ ++ private $isSystemAttributesLoaded = []; ++ ++ /** ++ * List of predefined system attributes for preload. ++ * ++ * @var array ++ */ ++ private $attributesForPreload; ++ + /** + * @param \Magento\Framework\App\CacheInterface $cache +- * @param \Magento\Eav\Model\Entity\TypeFactory $entityTypeFactory +- * @param \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory ++ * @param Entity\TypeFactory $entityTypeFactory ++ * @param ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory + * @param \Magento\Framework\App\Cache\StateInterface $cacheState + * @param \Magento\Framework\Validator\UniversalFactory $universalFactory +- * @param SerializerInterface $serializer ++ * @param SerializerInterface|null $serializer ++ * @param ScopeConfigInterface|null $scopeConfig ++ * @param array $attributesForPreload + * @codeCoverageIgnore + */ + public function __construct( +@@ -138,7 +172,9 @@ class Config + \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory, + \Magento\Framework\App\Cache\StateInterface $cacheState, + \Magento\Framework\Validator\UniversalFactory $universalFactory, +- SerializerInterface $serializer = null ++ SerializerInterface $serializer = null, ++ ScopeConfigInterface $scopeConfig = null, ++ $attributesForPreload = [] + ) { + $this->_cache = $cache; + $this->_entityTypeFactory = $entityTypeFactory; +@@ -146,6 +182,8 @@ class Config + $this->_cacheState = $cacheState; + $this->_universalFactory = $universalFactory; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); ++ $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); ++ $this->attributesForPreload = $attributesForPreload; + } + + /** +@@ -207,8 +245,8 @@ class Config + /** + * Associate object with identifier + * +- * @param mixed $obj +- * @param mixed $id ++ * @param mixed $obj ++ * @param mixed $id + * @return void + * @codeCoverageIgnore + */ +@@ -233,8 +271,8 @@ class Config + /** + * Specify reference for entity type id + * +- * @param int $id +- * @param string $code ++ * @param int $id ++ * @param string $code + * @return $this + * @codeCoverageIgnore + */ +@@ -258,9 +296,9 @@ class Config + /** + * Specify reference between entity attribute id and attribute code + * +- * @param int $id +- * @param string $code +- * @param string $entityTypeCode ++ * @param int $id ++ * @param string $code ++ * @param string $entityTypeCode + * @return $this + */ + protected function _addAttributeReference($id, $code, $entityTypeCode) +@@ -336,7 +374,9 @@ class Config + } + \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); + +- if ($this->isCacheEnabled() && ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID))) { ++ if ($this->isCacheEnabled() && ++ ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID)) ++ ) { + $this->_entityTypeData = $this->serializer->unserialize($cache); + foreach ($this->_entityTypeData as $typeCode => $data) { + $typeId = $data['entity_type_id']; +@@ -484,10 +524,10 @@ class Config + /** + * Get attribute by code for entity type + * +- * @param mixed $entityType +- * @param mixed $code +- * @return AbstractAttribute +- * @throws \Magento\Framework\Exception\LocalizedException ++ * @param mixed $entityType ++ * @param mixed $code ++ * @return AbstractAttribute ++ * @throws LocalizedException + */ + public function getAttribute($entityType, $code) + { +@@ -507,8 +547,152 @@ class Config + return $this->attributes[$entityTypeCode][$code]; + } + ++ if (array_key_exists($entityTypeCode, $this->attributesForPreload) ++ && array_key_exists($code, $this->attributesForPreload[$entityTypeCode]) ++ ) { ++ $this->initSystemAttributes($entityType, $this->attributesForPreload[$entityTypeCode]); ++ } ++ if (isset($this->attributes[$entityTypeCode][$code])) { ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $this->attributes[$entityTypeCode][$code]; ++ } ++ ++ if ($this->scopeConfig->getValue(self::XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES)) { ++ $attribute = $this->cacheUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } else { ++ $attribute = $this->initUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $attribute; ++ } ++ ++ /** ++ * Initialize predefined system attributes for preload. ++ * ++ * @param string $entityType ++ * @param array $systemAttributes ++ * @return $this|bool|void ++ * @throws LocalizedException ++ */ ++ private function initSystemAttributes($entityType, $systemAttributes) ++ { ++ $entityType = $this->getEntityType($entityType); ++ $entityTypeCode = $entityType->getEntityTypeCode(); ++ if (!empty($this->isSystemAttributesLoaded[$entityTypeCode])) { ++ return; ++ } ++ ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-' . $entityTypeCode . '-preload'; ++ if ($this->isCacheEnabled() && ($attributes = $this->_cache->load($cacheKey))) { ++ $attributes = $this->serializer->unserialize($attributes); ++ if ($attributes) { ++ foreach ($attributes as $attribute) { ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ } ++ return true; ++ } ++ } ++ ++ \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); ++ ++ /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection $attributes */ ++ $attributes = $this->_universalFactory->create( ++ $entityType->getEntityAttributeCollection() ++ )->setEntityTypeFilter( ++ $entityType ++ )->addFieldToFilter( ++ 'attribute_code', ++ ['in' => array_keys($systemAttributes)] ++ )->getData(); ++ ++ $attributeData = []; ++ foreach ($attributes as $attribute) { ++ if (empty($attribute['attribute_model'])) { ++ $attribute['attribute_model'] = $entityType->getAttributeModel(); ++ } ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ $attributeData[$attribute['attribute_code']] = $attributeObject->toArray(); ++ } ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attributeData), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ $this->isSystemAttributesLoaded[$entityTypeCode] = true; ++ ++ return $this; ++ } ++ ++ /** ++ * Initialize user defined attribute from cache or cache it. ++ * ++ * @param string $entityType ++ * @param mixed $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute ++ * @throws LocalizedException ++ */ ++ private function cacheUserDefinedAttribute($entityType, $entityTypeCode, $code): AbstractAttribute ++ { ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-attribute-' . $entityTypeCode . '-' . $code; ++ $attributeData = $this->isCacheEnabled() && ($attribute = $this->_cache->load($cacheKey)) ++ ? $this->serializer->unserialize($attribute) ++ : null; ++ if ($attributeData) { ++ if (isset($attributeData['attribute_id'])) { ++ $attribute = $this->_createAttribute($entityType, $attributeData); ++ } else { ++ $entityType = $this->getEntityType($entityType); ++ $attribute = $this->createAttribute($entityType->getAttributeModel()); ++ $attribute->setAttributeCode($code); ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ } ++ } else { ++ $attribute = $this->createAttributeByAttributeCode($entityType, $code); ++ $this->_addAttributeReference( ++ $attribute->getAttributeId(), ++ $attribute->getAttributeCode(), ++ $entityTypeCode ++ ); ++ $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attribute->getData()), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ } ++ ++ return $attribute; ++ } ++ ++ /** ++ * Initialize user defined attribute and save it to memory cache. ++ * ++ * @param mixed $entityType ++ * @param string $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute|null ++ * @throws LocalizedException ++ */ ++ private function initUserDefinedAttribute($entityType, $entityTypeCode, $code): ?AbstractAttribute ++ { + $attributes = $this->loadAttributes($entityTypeCode); +- $attribute = isset($attributes[$code]) ? $attributes[$code] : null; ++ $attribute = $attributes[$code] ?? null; + if (!$attribute) { + $attribute = $this->createAttributeByAttributeCode($entityType, $code); + $this->_addAttributeReference( +@@ -518,7 +702,7 @@ class Config + ); + $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); + } +- \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ + return $attribute; + } + +@@ -555,8 +739,8 @@ class Config + /** + * Get all entity type attributes + * +- * @param int|string|Type $entityType +- * @param \Magento\Framework\DataObject|null $object ++ * @param int|string|Type $entityType ++ * @param \Magento\Framework\DataObject|null $object + * @return AbstractAttribute[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) +@@ -639,7 +823,11 @@ class Config + $existsFullAttribute = $attribute->hasIsRequired(); + $fullAttributeData = array_key_exists('is_required', $attributeData); + +- if ($existsFullAttribute || !$existsFullAttribute && !$fullAttributeData) { ++ if ($existsFullAttribute || (!$existsFullAttribute && !$fullAttributeData)) { ++ $scopeIsRequired = $attributeData['scope_is_required'] ?? null; ++ if ($scopeIsRequired !== null) { ++ $attribute->setData('scope_is_required', $scopeIsRequired); ++ } + return $attribute; + } + } +@@ -708,6 +896,7 @@ class Config + * @param string $entityType + * @param string $attributeCode + * @return AbstractAttribute ++ * @throws LocalizedException + */ + private function createAttributeByAttributeCode($entityType, $attributeCode) + { +@@ -723,13 +912,28 @@ class Config + $attribute->setAttributeCode($attributeCode); + } + ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ ++ return $attribute; ++ } ++ ++ /** ++ * Set entity type id, backend type, is global to attribute. ++ * ++ * @param AbstractAttribute $attribute ++ * @param AbstractModel $entityType ++ * @return AbstractAttribute ++ */ ++ private function setAttributeData($attribute, $entityType): AbstractAttribute ++ { + $entity = $entityType->getEntity(); +- if ($entity instanceof \Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface ++ if ($entity instanceof ProviderInterface + && in_array($attribute->getAttributeCode(), $entity->getDefaultAttributes(), true) + ) { + $attribute->setBackendType(AbstractAttribute::TYPE_STATIC)->setIsGlobal(1); + } + $attribute->setEntityType($entityType)->setEntityTypeId($entityType->getId()); ++ + return $attribute; + } + +diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +--- a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php ++++ b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +@@ -6,6 +6,9 @@ + + namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; + ++/** ++ * Basic implementation for attribute sets ++ */ + class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + { + /** +@@ -24,8 +27,6 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + protected $eavConfig; + + /** +- * Constructor +- * + * @param \Magento\Framework\Model\ResourceModel\Db\Context $context + * @param GroupFactory $attrGroupFactory + * @param \Magento\Eav\Model\Config $eavConfig +@@ -54,7 +55,7 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + + /** +- * Perform actions after object save ++ * Perform actions after object save. + * + * @param \Magento\Framework\Model\AbstractModel $object + * @return $this +diff -Nuar a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/etc/di.xml +--- a/vendor/magento/module-eav/etc/di.xml ++++ b/vendor/magento/module-eav/etc/di.xml +@@ -209,4 +209,14 @@ + </argument> + </arguments> + </type> ++ <virtualType name="configured_eav_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">eav</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Eav\Model\Config"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_eav_cache</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-theme/etc/di.xml +--- a/vendor/magento/module-theme/etc/di.xml ++++ b/vendor/magento/module-theme/etc/di.xml +@@ -285,4 +285,24 @@ + <argument name="identifierName" xsi:type="string">theme_id</argument> + </arguments> + </type> ++ <virtualType name="configured_design_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">layout</argument> ++ </arguments> ++ </virtualType> ++ <virtualType name="design_context" type="Magento\Framework\Model\Context"> ++ <arguments> ++ <argument name="cacheManager" xsi:type="object">configured_design_cache</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Theme\Model\Design"> ++ <arguments> ++ <argument name="context" xsi:type="object">design_context</argument> ++ </arguments> ++ </type> ++ <type name="Magento\Theme\Model\Theme\ThemeProvider"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_design_cache</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/App/Cache.php +--- a/vendor/magento/framework/App/Cache.php ++++ b/vendor/magento/framework/App/Cache.php +@@ -4,12 +4,11 @@ + * See COPYING.txt for license details. + */ + +-/** +- * System cache model +- * support id and tags prefix support, +- */ + namespace Magento\Framework\App; + ++/** ++ * System cache model support id and tags prefix support. ++ */ + class Cache implements CacheInterface + { + /** +@@ -30,12 +29,13 @@ class Cache implements CacheInterface + protected $_frontend; + + /** +- * @param \Magento\Framework\App\Cache\Frontend\Pool $frontendPool ++ * @param Cache\Frontend\Pool $frontendPool ++ * @param string|null $cacheIdentifier + */ +- public function __construct(\Magento\Framework\App\Cache\Frontend\Pool $frontendPool) ++ public function __construct(\Magento\Framework\App\Cache\Frontend\Pool $frontendPool, $cacheIdentifier = null) + { + $this->_frontendPool = $frontendPool; +- $this->_frontend = $frontendPool->get($this->_frontendIdentifier); ++ $this->_frontend = $frontendPool->get($cacheIdentifier ?? $this->_frontendIdentifier); + } + + /** +diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Pool.php b/vendor/magento/framework/App/Cache/Frontend/Pool.php +--- a/vendor/magento/framework/App/Cache/Frontend/Pool.php ++++ b/vendor/magento/framework/App/Cache/Frontend/Pool.php +@@ -152,6 +152,15 @@ class Pool implements \Iterator + if (isset($this->_instances[$identifier])) { + return $this->_instances[$identifier]; + } +- throw new \InvalidArgumentException("Cache frontend '{$identifier}' is not recognized."); ++ ++ if (!isset($this->_instances[self::DEFAULT_FRONTEND_ID])) { ++ throw new \InvalidArgumentException( ++ "Cache frontend '{$identifier}' is not recognized. As well as " . ++ self::DEFAULT_FRONTEND_ID . ++ "cache is not configured" ++ ); ++ } ++ ++ return $this->_instances[self::DEFAULT_FRONTEND_ID]; + } + } +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +--- a/vendor/magento/framework/App/Router/ActionList.php ++++ b/vendor/magento/framework/App/Router/ActionList.php +@@ -5,6 +5,8 @@ + */ + namespace Magento\Framework\App\Router; + ++use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Framework\App\State; + use Magento\Framework\Serialize\SerializerInterface; + use Magento\Framework\Serialize\Serializer\Serialize; + use Magento\Framework\Module\Dir\Reader as ModuleReader; +@@ -70,12 +72,26 @@ class ActionList + $this->reservedWords = array_merge($reservedWords, $this->reservedWords); + $this->actionInterface = $actionInterface; + $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Serialize::class); +- $data = $cache->load($cacheKey); +- if (!$data) { +- $this->actions = $moduleReader->getActionFiles(); +- $cache->save($this->serializer->serialize($this->actions), $cacheKey); ++ $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); ++ $state = $objectManager->get(State::class); ++ ++ if ($state->getMode() === State::MODE_PRODUCTION) { ++ $directoryList = $objectManager->get(DirectoryList::class); ++ $file = $directoryList->getPath(DirectoryList::GENERATED_METADATA) . '/' . $cacheKey . '.' . 'php'; ++ ++ if (file_exists($file)) { ++ $this->actions = (include $file) ?? $moduleReader->getActionFiles(); ++ } else { ++ $this->actions = $moduleReader->getActionFiles(); ++ } + } else { +- $this->actions = $this->serializer->unserialize($data); ++ $data = $cache->load($cacheKey); ++ if (!$data) { ++ $this->actions = $moduleReader->getActionFiles(); ++ $cache->save($this->serializer->serialize($this->actions), $cacheKey); ++ } else { ++ $this->actions = $this->serializer->unserialize($data); ++ } + } + } + +diff -Nuar a/2.3.5/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/Cache/Backend/Redis.php +@@ -0,0 +1,83 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++ ++namespace Magento\Framework\Cache\Backend; ++ ++/** ++ * Redis wrapper to extend current implementation behaviour. ++ */ ++class Redis extends \Cm_Cache_Backend_Redis ++{ ++ /** ++ * Local state of preloaded keys. ++ * ++ * @var array ++ */ ++ private $preloadedData = []; ++ ++ /** ++ * Array of keys to be preloaded. ++ * ++ * @var array ++ */ ++ private $preloadKeys = []; ++ ++ /** ++ * @param array $options ++ */ ++ public function __construct($options = []) ++ { ++ $this->preloadKeys = $options['preload_keys'] ?? []; ++ parent::__construct($options); ++ } ++ ++ /** ++ * Load value with given id from cache ++ * ++ * @param string $id Cache id ++ * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested ++ * @return bool|string ++ */ ++ public function load($id, $doNotTestCacheValidity = false) ++ { ++ if (!empty($this->preloadKeys) && empty($this->preloadedData)) { ++ $redis = $this->_slave ?? $this->_redis; ++ $redis = $redis->pipeline(); ++ ++ foreach ($this->preloadKeys as $key) { ++ $redis->hGet(self::PREFIX_KEY . $key, self::FIELD_DATA); ++ } ++ ++ $this->preloadedData = array_filter(array_combine($this->preloadKeys, $redis->exec())); ++ } ++ ++ if (isset($this->preloadedData[$id])) { ++ return $this->_decodeData($this->preloadedData[$id]); ++ } ++ ++ return parent::load($id, $doNotTestCacheValidity); ++ } ++ ++ /** ++ * Cover errors on save operations, which may occurs when Redis cannot evict keys, which is expected in some cases. ++ * ++ * @param string $data ++ * @param string $id ++ * @param array $tags ++ * @param bool $specificLifetime ++ * @return bool ++ */ ++ public function save($data, $id, $tags = [], $specificLifetime = false) ++ { ++ try { ++ parent::save($data, $id, $tags, $specificLifetime); ++ } catch (\Throwable $exception) { ++ return false; ++ } ++ ++ return true; ++ } ++} +diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +--- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php ++++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +@@ -9,8 +9,10 @@ namespace Magento\Framework\Cache\Backend; + /** + * Remote synchronized cache + * +- * This class created for correct work local caches with multiple web nodes, +- * that will be check cache status from remote cache ++ * This class created for correct work witch local caches and multiple web nodes, ++ * in order to be sure that we always have up to date local version of cache. ++ * This class will be check cache version from remote cache and in case it newer ++ * than local one, it will update local one from remote cache a.k.a two level cache. + */ + class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface + { +@@ -36,11 +38,15 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + protected $cacheInvalidationTime; + + /** +- * {@inheritdoc} ++ * Suffix for hash to compare data version in cache storage. ++ */ ++ private const HASH_SUFFIX = ':hash'; ++ ++ /** ++ * @inheritdoc + */ + protected $_options = [ + 'remote_backend' => '', +- 'remote_backend_invalidation_time_id' => 'default_remote_backend_invalidation_time', + 'remote_backend_custom_naming' => true, + 'remote_backend_autoload' => true, + 'remote_backend_options' => [], +@@ -52,6 +58,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + + /** + * @param array $options ++ * @throws \Zend_Cache_Exception + */ + public function __construct(array $options = []) + { +@@ -97,76 +104,137 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * Update remote cache status info ++ * @inheritdoc ++ */ ++ public function setDirectives($directives) ++ { ++ return $this->local->setDirectives($directives); ++ } ++ ++ /** ++ * Return hash sign of the data. + * +- * @return void ++ * @param string $data ++ * @return string + */ +- private function updateRemoteCacheStatusInfo() ++ private function getDataVersion(string $data) + { +- $this->remote->save(time(), $this->_options['remote_backend_invalidation_time_id'], [], null); +- $this->cacheInvalidationTime = null; ++ return \hash('sha256', $data); + } + + /** +- * {@inheritdoc} ++ * Load data version by id from remote. ++ * ++ * @param string $id ++ * @return false|string + */ +- public function setDirectives($directives) ++ private function loadRemoteDataVersion(string $id) + { +- return $this->local->setDirectives($directives); ++ return $this->remote->load( ++ $id . self::HASH_SUFFIX ++ ); + } + + /** +- * {@inheritdoc} ++ * Save new data version to remote. ++ * ++ * @param string $data ++ * @param string $id ++ * @param array $tags ++ * @param mixed $specificLifetime ++ * @return bool ++ */ ++ private function saveRemoteDataVersion(string $data, string $id, array $tags, $specificLifetime = false) ++ { ++ return $this->remote->save($this->getDataVersion($data), $id . self::HASH_SUFFIX, $tags, $specificLifetime); ++ } ++ ++ /** ++ * Remove remote data version. ++ * ++ * @param string $id ++ * @return bool ++ */ ++ private function removeRemoteDataVersion($id) ++ { ++ return $this->remote->remove($id . self::HASH_SUFFIX); ++ } ++ ++ /** ++ * @inheritdoc + */ + public function load($id, $doNotTestCacheValidity = false) + { +- $dataModificationTime = $this->local->test($id); +- if ($this->cacheInvalidationTime === null) { +- $this->cacheInvalidationTime = $this->remote->load($this->_options['remote_backend_invalidation_time_id']); +- } +- if ($dataModificationTime >= $this->cacheInvalidationTime) { +- return $this->local->load($id, $doNotTestCacheValidity); ++ $localData = $this->local->load($id); ++ $remoteData = false; ++ ++ if (false === $localData) { ++ $remoteData = $this->remote->load($id); ++ ++ if (false === $remoteData) { ++ return false; ++ } + } else { +- return false; ++ if ($this->getDataVersion($localData) !== $this->loadRemoteDataVersion($id)) { ++ $localData = false; ++ $remoteData = $this->remote->load($id); ++ } + } ++ ++ if ($remoteData !== false) { ++ $this->local->save($remoteData, $id); ++ $localData = $remoteData; ++ } ++ ++ return $localData; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function test($id) + { +- return $this->local->test($id); ++ return $this->local->test($id) ?? $this->remote->test($id); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function save($data, $id, $tags = [], $specificLifetime = false) + { +- return $this->local->save($data, $id, $tags, $specificLifetime); ++ $dataToSave = $data; ++ $remHash = $this->loadRemoteDataVersion($id); ++ ++ if ($remHash !== false) { ++ $dataToSave = $this->remote->load($id); ++ } else { ++ $this->remote->save($data, $id, $tags, $specificLifetime); ++ $this->saveRemoteDataVersion($data, $id, $tags, $specificLifetime); ++ } ++ ++ return $this->local->save($dataToSave, $id, [], $specificLifetime); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function remove($id) + { +- $this->updateRemoteCacheStatusInfo(); +- return $this->local->remove($id); ++ return $this->removeRemoteDataVersion($id) && ++ $this->remote->remove($id) && ++ $this->local->remove($id); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = []) + { +- $this->updateRemoteCacheStatusInfo(); +- return $this->local->clean($mode, $tags); ++ return $this->remote->clean($mode, $tags); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIds() + { +@@ -174,7 +242,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getTags() + { +@@ -182,7 +250,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsMatchingTags($tags = []) + { +@@ -190,7 +258,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsNotMatchingTags($tags = []) + { +@@ -198,7 +266,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsMatchingAnyTags($tags = []) + { +@@ -206,7 +274,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getFillingPercentage() + { +@@ -214,7 +282,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getMetadatas($id) + { +@@ -222,7 +290,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function touch($id, $extraLifetime) + { +@@ -230,7 +298,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getCapabilities() + { +diff -Nuar a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +--- a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +@@ -260,9 +260,12 @@ class DiCompileCommand extends Command + */ + private function getExcludedLibraryPaths(array $libraryPaths) + { +- $libraryPaths = array_map(function ($libraryPath) { +- return preg_quote($libraryPath, '#'); +- }, $libraryPaths); ++ $libraryPaths = array_map( ++ function ($libraryPath) { ++ return preg_quote($libraryPath, '#'); ++ }, ++ $libraryPaths ++ ); + + $excludedLibraryPaths = [ + '#^(?:' . join('|', $libraryPaths) . ')/([\\w]+/)?Test#', +@@ -395,7 +398,8 @@ class DiCompileCommand extends Command + $compiledPathsList['application'], + $compiledPathsList['library'], + $compiledPathsList['generated_helpers'], +- ] ++ ], ++ OperationFactory::APPLICATION_ACTION_LIST_GENERATOR => [], + ]; + + return $operations; +diff -Nuar a/2.3.5/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +new file mode 100644 +--- /dev/null ++++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +@@ -0,0 +1,58 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++namespace Magento\Setup\Module\Di\App\Task\Operation; ++ ++use Magento\Setup\Module\Di\App\Task\OperationInterface; ++use Magento\Framework\Module\Dir\Reader as ModuleReader; ++use Magento\Framework\App\ObjectManager\ConfigWriterInterface; ++ ++/** ++ * Pregenerates actions for Magento ++ */ ++class AppActionListGenerator implements OperationInterface ++{ ++ /** ++ * @var ModuleReader ++ */ ++ private $moduleReader; ++ ++ /** ++ * @var \Magento\Framework\App\ObjectManager\ConfigWriterInterface ++ */ ++ private $configWriter; ++ ++ /** ++ * @param ModuleReader $moduleReader ++ * @param ConfigWriterInterface $configWriter ++ */ ++ public function __construct( ++ ModuleReader $moduleReader, ++ ConfigWriterInterface $configWriter ++ ) { ++ $this->moduleReader = $moduleReader; ++ $this->configWriter = $configWriter; ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ public function doOperation() ++ { ++ $actionList = $this->moduleReader->getActionFiles(); ++ $this->configWriter->write( ++ 'app_action_list', ++ $actionList ++ ); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ public function getName() ++ { ++ return 'App action list generation'; ++ } ++} +diff -Nuar a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +--- a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php ++++ b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +@@ -5,6 +5,12 @@ + */ + namespace Magento\Setup\Module\Di\App\Task; + ++use Magento\Setup\Module\Di\App\Task\Operation\AppActionListGenerator; ++use Magento\Setup\Module\Di\App\Task\Operation\PluginListGenerator; ++ ++/** ++ * Factory that creates list of OperationInterface classes ++ */ + class OperationFactory + { + /** +@@ -47,6 +53,11 @@ class OperationFactory + */ + const APPLICATION_CODE_GENERATOR = 'application_code_generator'; + ++ /** ++ * Application action list generator ++ */ ++ const APPLICATION_ACTION_LIST_GENERATOR = 'application_action_list_generator'; ++ + /** + * Operations definitions + * +@@ -61,6 +72,7 @@ class OperationFactory + self::INTERCEPTION_CACHE => \Magento\Setup\Module\Di\App\Task\Operation\InterceptionCache::class, + self::REPOSITORY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\RepositoryGenerator::class, + self::PROXY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\ProxyGenerator::class, ++ self::APPLICATION_ACTION_LIST_GENERATOR => AppActionListGenerator::class, + ]; + + /** + +diff --git a/vendor/magento/framework/Interception/Config/CacheManager.php b/vendor/magento/framework/Interception/Config/CacheManager.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/Interception/Config/CacheManager.php +@@ -0,0 +1,114 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++ ++declare(strict_types=1); ++ ++namespace Magento\Framework\Interception\Config; ++ ++/** ++ * Interception cache manager. ++ * ++ * Responsible for handling interaction with compiled and uncompiled interception data ++ */ ++class CacheManager ++{ ++ /** ++ * @var \Magento\Framework\Cache\FrontendInterface ++ */ ++ private $cache; ++ ++ /** ++ * @var \Magento\Framework\Serialize\SerializerInterface ++ */ ++ private $serializer; ++ ++ /** ++ * @var \Magento\Framework\App\ObjectManager\ConfigWriterInterface ++ */ ++ private $configWriter; ++ ++ /** ++ * @var \Magento\Framework\App\ObjectManager\ConfigLoader\Compiled ++ */ ++ private $compiledLoader; ++ ++ /** ++ * @param \Magento\Framework\Cache\FrontendInterface $cache ++ * @param \Magento\Framework\Serialize\SerializerInterface $serializer ++ * @param \Magento\Framework\App\ObjectManager\ConfigWriterInterface $configWriter ++ * @param \Magento\Framework\App\ObjectManager\ConfigLoader\Compiled $compiledLoader ++ */ ++ public function __construct( ++ \Magento\Framework\Cache\FrontendInterface $cache, ++ \Magento\Framework\Serialize\SerializerInterface $serializer, ++ \Magento\Framework\App\ObjectManager\ConfigWriterInterface $configWriter, ++ \Magento\Framework\App\ObjectManager\ConfigLoader\Compiled $compiledLoader ++ ) { ++ $this->cache = $cache; ++ $this->serializer = $serializer; ++ $this->configWriter = $configWriter; ++ $this->compiledLoader = $compiledLoader; ++ } ++ ++ /** ++ * Load the interception config from cache ++ * ++ * @param string $key ++ * @return array|null ++ */ ++ public function load(string $key): ?array ++ { ++ if ($this->isCompiled($key)) { ++ return $this->compiledLoader->load($key); ++ } ++ ++ $intercepted = $this->cache->load($key); ++ return $intercepted ? $this->serializer->unserialize($intercepted) : null; ++ } ++ ++ /** ++ * Save config to cache backend ++ * ++ * @param string $key ++ * @param array $data ++ */ ++ public function save(string $key, array $data) ++ { ++ $this->cache->save($this->serializer->serialize($data), $key); ++ } ++ ++ /** ++ * Save config to filesystem ++ * ++ * @param string $key ++ * @param array $data ++ */ ++ public function saveCompiled(string $key, array $data) ++ { ++ $this->configWriter->write($key, $data); ++ } ++ ++ /** ++ * Purge interception cache ++ * ++ * @param string $key ++ */ ++ public function clean(string $key) ++ { ++ $this->cache->remove($key); ++ } ++ ++ /** ++ * Check for the compiled config with the generated metadata ++ * ++ * @param string $key ++ * @return bool ++ */ ++ private function isCompiled(string $key): bool ++ { ++ return file_exists(\Magento\Framework\App\ObjectManager\ConfigLoader\Compiled::getFilePath($key)); ++ } ++} + +diff --git a/vendor/magento/framework/App/ObjectManager/ConfigWriterInterface.php b/vendor/magento/framework/App/ObjectManager/ConfigWriterInterface.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/App/ObjectManager/ConfigWriterInterface.php +@@ -0,0 +1,24 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++ ++declare(strict_types=1); ++ ++namespace Magento\Framework\App\ObjectManager; ++ ++/** ++ * Write compiled object manager configuration to storage ++ */ ++interface ConfigWriterInterface ++{ ++ /** ++ * Writes config in storage ++ * ++ * @param string $key ++ * @param array $config ++ * @return void ++ */ ++ public function write(string $key, array $config); ++} + +diff --git a/vendor/magento/framework/App/ObjectManager/ConfigWriter/Filesystem.php b/vendor/magento/framework/App/ObjectManager/ConfigWriter/Filesystem.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/App/ObjectManager/ConfigWriter/Filesystem.php +@@ -0,0 +1,61 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++ ++declare(strict_types=1); ++ ++namespace Magento\Framework\App\ObjectManager\ConfigWriter; ++ ++use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Framework\App\ObjectManager\ConfigWriterInterface; ++ ++/** ++ * @inheritdoc ++ */ ++class Filesystem implements ConfigWriterInterface ++{ ++ /** ++ * @var DirectoryList ++ */ ++ private $directoryList; ++ ++ /** ++ * @param DirectoryList $directoryList ++ */ ++ public function __construct( ++ DirectoryList $directoryList ++ ) { ++ $this->directoryList = $directoryList; ++ } ++ ++ /** ++ * Writes config in storage ++ * ++ * @param string $key ++ * @param array $config ++ * @return void ++ */ ++ public function write(string $key, array $config) ++ { ++ $this->initialize(); ++ $configuration = sprintf('<?php return %s;', var_export($config, true)); ++ file_put_contents( ++ $this->directoryList->getPath(DirectoryList::GENERATED_METADATA) . '/' . $key . '.php', ++ $configuration ++ ); ++ } ++ ++ /** ++ * Initializes writer ++ * ++ * @return void ++ */ ++ private function initialize() ++ { ++ if (!file_exists($this->directoryList->getPath(DirectoryList::GENERATED_METADATA))) { ++ mkdir($this->directoryList->getPath(DirectoryList::GENERATED_METADATA)); ++ } ++ } ++} +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -72,9 +72,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + const STORE_ID = 'store_id'; + + /** +- * @var string ++ * @var string|bool + */ +- protected $_cacheTag = self::CACHE_TAG; ++ protected $_cacheTag = false; + + /** + * @var string +@@ -865,7 +865,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + public function beforeSave() + { +- $this->cleanCache(); + $this->setTypeHasOptions(false); + $this->setTypeHasRequiredOptions(false); + $this->setHasOptions(false); +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +@@ -20,19 +20,21 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class Cache implements ConfigOptionsListInterface + { + const INPUT_VALUE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; + const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; + const INPUT_KEY_CACHE_BACKEND_REDIS_DATABASE = 'cache-backend-redis-db'; + const INPUT_KEY_CACHE_BACKEND_REDIS_PORT = 'cache-backend-redis-port'; + const INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD = 'cache-backend-redis-password'; ++ const INPUT_KEY_CACHE_ID_PREFIX = 'cache-id-prefix'; + + const CONFIG_PATH_CACHE_BACKEND = 'cache/frontend/default/backend'; + const CONFIG_PATH_CACHE_BACKEND_SERVER = 'cache/frontend/default/backend_options/server'; + const CONFIG_PATH_CACHE_BACKEND_DATABASE = 'cache/frontend/default/backend_options/database'; + const CONFIG_PATH_CACHE_BACKEND_PORT = 'cache/frontend/default/backend_options/port'; + const CONFIG_PATH_CACHE_BACKEND_PASSWORD = 'cache/frontend/default/backend_options/password'; ++ const CONFIG_PATH_CACHE_ID_PREFIX = 'cache/frontend/default/id_prefix'; + + /** + * @var array +@@ -77,7 +79,7 @@ class Cache implements ConfigOptionsListInterface + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getOptions() + { +@@ -112,6 +114,12 @@ class Cache implements ConfigOptionsListInterface + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_CACHE_BACKEND_PASSWORD, + 'Redis server password' ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_CACHE_ID_PREFIX, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_CACHE_ID_PREFIX, ++ 'ID prefix for cache keys' + ) + ]; + } +@@ -122,6 +130,11 @@ class Cache implements ConfigOptionsListInterface + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); ++ if (isset($options[self::INPUT_KEY_CACHE_ID_PREFIX])) { ++ $configData->set(self::CONFIG_PATH_CACHE_ID_PREFIX, $options[self::INPUT_KEY_CACHE_ID_PREFIX]); ++ } else { ++ $configData->set(self::CONFIG_PATH_CACHE_ID_PREFIX, $this->generateCachePrefix()); ++ } + + if (isset($options[self::INPUT_KEY_CACHE_BACKEND])) { + if ($options[self::INPUT_KEY_CACHE_BACKEND] == self::INPUT_VALUE_CACHE_REDIS) { +@@ -241,4 +254,14 @@ class Cache implements ConfigOptionsListInterface + return ''; + } + } ++ ++ /** ++ * Generate default cache ID prefix based on installation dir ++ * ++ * @return string ++ */ ++ private function generateCachePrefix(): string ++ { ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; ++ } + } +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class PageCache implements ConfigOptionsListInterface + { + const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_PAGE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; +@@ -28,6 +28,7 @@ class PageCache implements ConfigOptionsListInterface + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PORT = 'page-cache-redis-port'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESS_DATA = 'page-cache-redis-compress-data'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD = 'page-cache-redis-password'; ++ const INPUT_KEY_PAGE_CACHE_ID_PREFIX = 'page-cache-id-prefix'; + + const CONFIG_PATH_PAGE_CACHE_BACKEND = 'cache/frontend/page_cache/backend'; + const CONFIG_PATH_PAGE_CACHE_BACKEND_SERVER = 'cache/frontend/page_cache/backend_options/server'; +@@ -35,6 +36,7 @@ class PageCache implements ConfigOptionsListInterface + const CONFIG_PATH_PAGE_CACHE_BACKEND_PORT = 'cache/frontend/page_cache/backend_options/port'; + const CONFIG_PATH_PAGE_CACHE_BACKEND_COMPRESS_DATA = 'cache/frontend/page_cache/backend_options/compress_data'; + const CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD = 'cache/frontend/page_cache/backend_options/password'; ++ const CONFIG_PATH_PAGE_CACHE_ID_PREFIX = 'cache/frontend/page_cache/id_prefix'; + + /** + * @var array +@@ -81,7 +83,7 @@ class PageCache implements ConfigOptionsListInterface + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getOptions() + { +@@ -122,6 +124,12 @@ class PageCache implements ConfigOptionsListInterface + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD, + 'Redis server password' ++ ), ++ new TextConfigOption( ++ self::INPUT_KEY_PAGE_CACHE_ID_PREFIX, ++ TextConfigOption::FRONTEND_WIZARD_TEXT, ++ self::CONFIG_PATH_PAGE_CACHE_ID_PREFIX, ++ 'ID prefix for cache keys' + ) + ]; + } +@@ -132,6 +140,11 @@ class PageCache implements ConfigOptionsListInterface + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); ++ if (isset($options[self::INPUT_KEY_PAGE_CACHE_ID_PREFIX])) { ++ $configData->set(self::CONFIG_PATH_PAGE_CACHE_ID_PREFIX, $options[self::INPUT_KEY_PAGE_CACHE_ID_PREFIX]); ++ } else { ++ $configData->set(self::CONFIG_PATH_PAGE_CACHE_ID_PREFIX, $this->generateCachePrefix()); ++ } + + if (isset($options[self::INPUT_KEY_PAGE_CACHE_BACKEND])) { + if ($options[self::INPUT_KEY_PAGE_CACHE_BACKEND] == self::INPUT_VALUE_PAGE_CACHE_REDIS) { +@@ -241,7 +254,7 @@ class PageCache implements ConfigOptionsListInterface + /** + * Get the default value for input key + * +- * @param string $inputKey ++ * @param string $inputKeyz + * @return string + */ + private function getDefaultConfigValue($inputKey) +@@ -252,4 +265,14 @@ class PageCache implements ConfigOptionsListInterface + return ''; + } + } ++ ++ /** ++ * Generate default cache ID prefix based on installation dir ++ * ++ * @return string ++ */ ++ private function generateCachePrefix(): string ++ { ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; ++ } + } diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch new file mode 100644 index 00000000..46a00737 --- /dev/null +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch @@ -0,0 +1,2060 @@ +diff -Nuar a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php +--- a/vendor/magento/module-config/App/Config/Type/System.php ++++ b/vendor/magento/module-config/App/Config/Type/System.php +@@ -3,6 +3,7 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + namespace Magento\Config\App\Config\Type; + + use Magento\Framework\App\Config\ConfigSourceInterface; +@@ -13,11 +14,14 @@ use Magento\Framework\App\ObjectManager; + use Magento\Config\App\Config\Type\System\Reader; + use Magento\Framework\App\ScopeInterface; + use Magento\Framework\Cache\FrontendInterface; ++use Magento\Framework\Cache\LockGuardedCacheLoader; + use Magento\Framework\Lock\LockManagerInterface; + use Magento\Framework\Serialize\SerializerInterface; + use Magento\Store\Model\Config\Processor\Fallback; +-use Magento\Store\Model\ScopeInterface as StoreScope; + use Magento\Framework\Encryption\Encryptor; ++use Magento\Store\Model\ScopeInterface as StoreScope; ++use Magento\Framework\App\Cache\StateInterface; ++use Magento\Framework\App\Cache\Type\Config; + + /** + * System configuration type +@@ -25,6 +29,7 @@ use Magento\Framework\Encryption\Encryptor; + * @api + * @since 100.1.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + class System implements ConfigTypeInterface + { +@@ -42,22 +47,6 @@ class System implements ConfigTypeInterface + * @var string + */ + private static $lockName = 'SYSTEM_CONFIG'; +- /** +- * Timeout between retrieves to load the configuration from the cache. +- * +- * Value of the variable in microseconds. +- * +- * @var int +- */ +- private static $delayTimeout = 100000; +- /** +- * Lifetime of the lock for write in cache. +- * +- * Value of the variable in seconds. +- * +- * @var int +- */ +- private static $lockTimeout = 42; + + /** + * @var array +@@ -106,11 +95,17 @@ class System implements ConfigTypeInterface + private $encryptor; + + /** +- * @var LockManagerInterface ++ * @var LockGuardedCacheLoader + */ +- private $locker; ++ private $lockQuery; + + /** ++ * @var StateInterface ++ */ ++ private $cacheState; ++ ++ /** ++ * System constructor. + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback +@@ -122,6 +117,8 @@ class System implements ConfigTypeInterface + * @param Reader|null $reader + * @param Encryptor|null $encryptor + * @param LockManagerInterface|null $locker ++ * @param LockGuardedCacheLoader|null $lockQuery ++ * @param StateInterface|null $cacheState + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -136,7 +133,9 @@ class System implements ConfigTypeInterface + $configType = self::CONFIG_TYPE, + Reader $reader = null, + Encryptor $encryptor = null, +- LockManagerInterface $locker = null ++ LockManagerInterface $locker = null, ++ LockGuardedCacheLoader $lockQuery = null, ++ StateInterface $cacheState = null + ) { + $this->postProcessor = $postProcessor; + $this->cache = $cache; +@@ -145,8 +144,10 @@ class System implements ConfigTypeInterface + $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); + $this->encryptor = $encryptor + ?: ObjectManager::getInstance()->get(Encryptor::class); +- $this->locker = $locker +- ?: ObjectManager::getInstance()->get(LockManagerInterface::class); ++ $this->lockQuery = $lockQuery ++ ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); ++ $this->cacheState = $cacheState ++ ?: ObjectManager::getInstance()->get(StateInterface::class); + } + + /** +@@ -225,83 +226,64 @@ class System implements ConfigTypeInterface + } + + /** +- * Make lock on data load. ++ * Load configuration data for all scopes. + * +- * @param callable $dataLoader +- * @param bool $flush + * @return array + */ +- private function lockedLoadData(callable $dataLoader, bool $flush = false): array ++ private function loadAllData() + { +- $cachedData = $dataLoader(); //optimistic read +- +- while ($cachedData === false && $this->locker->isLocked(self::$lockName)) { +- usleep(self::$delayTimeout); +- $cachedData = $dataLoader(); ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); + } + +- while ($cachedData === false) { +- try { +- if ($this->locker->lock(self::$lockName, self::$lockTimeout)) { +- if (!$flush) { +- $data = $this->readData(); +- $this->cacheData($data); +- $cachedData = $data; +- } else { +- $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); +- $cachedData = []; +- } +- } +- } finally { +- $this->locker->unlock(self::$lockName); +- } +- +- if ($cachedData === false) { +- usleep(self::$delayTimeout); +- $cachedData = $dataLoader(); +- } +- } +- +- return $cachedData; +- } +- +- /** +- * Load configuration data for all scopes +- * +- * @return array +- */ +- private function loadAllData() +- { +- return $this->lockedLoadData(function () { ++ $loadAction = function () { + $cachedData = $this->cache->load($this->configType); + $data = false; + if ($cachedData !== false) { + $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); + } + return $data; +- }); ++ }; ++ ++ return $this->lockQuery->lockedLoadData( ++ self::$lockName, ++ $loadAction, ++ \Closure::fromCallable([$this, 'readData']), ++ \Closure::fromCallable([$this, 'cacheData']) ++ ); + } + + /** +- * Load configuration data for default scope ++ * Load configuration data for default scope. + * + * @param string $scopeType + * @return array + */ + private function loadDefaultScopeData($scopeType) + { +- return $this->lockedLoadData(function () use ($scopeType) { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ ++ $loadAction = function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + $scopeData = false; + if ($cachedData !== false) { + $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; + } + return $scopeData; +- }); ++ }; ++ ++ return $this->lockQuery->lockedLoadData( ++ self::$lockName, ++ $loadAction, ++ \Closure::fromCallable([$this, 'readData']), ++ \Closure::fromCallable([$this, 'cacheData']) ++ ); + } + + /** +- * Load configuration data for a specified scope ++ * Load configuration data for a specified scope. + * + * @param string $scopeType + * @param string $scopeId +@@ -309,7 +291,11 @@ class System implements ConfigTypeInterface + */ + private function loadScopeData($scopeType, $scopeId) + { +- return $this->lockedLoadData(function () use ($scopeType, $scopeId) { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ ++ $loadAction = function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + $scopeData = false; + if ($cachedData === false) { +@@ -329,7 +315,14 @@ class System implements ConfigTypeInterface + } + + return $scopeData; +- }); ++ }; ++ ++ return $this->lockQuery->lockedLoadData( ++ self::$lockName, ++ $loadAction, ++ \Closure::fromCallable([$this, 'readData']), ++ \Closure::fromCallable([$this, 'cacheData']) ++ ); + } + + /** +@@ -371,7 +364,7 @@ class System implements ConfigTypeInterface + } + + /** +- * Walk nested hash map by keys from $pathParts ++ * Walk nested hash map by keys from $pathParts. + * + * @param array $data to walk in + * @param array $pathParts keys path +@@ -408,7 +401,7 @@ class System implements ConfigTypeInterface + } + + /** +- * Clean cache and global variables cache ++ * Clean cache and global variables cache. + * + * Next items cleared: + * - Internal property intended to store already loaded configuration data +@@ -420,11 +413,17 @@ class System implements ConfigTypeInterface + public function clean() + { + $this->data = []; +- $this->lockedLoadData( +- function () { +- return false; +- }, +- true ++ $cleanAction = function () { ++ $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); ++ }; ++ ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $cleanAction(); ++ } ++ ++ $this->lockQuery->lockedCleanData( ++ self::$lockName, ++ $cleanAction + ); + } + } +diff -Nuar a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml +--- a/vendor/magento/module-config/etc/di.xml ++++ b/vendor/magento/module-config/etc/di.xml +@@ -90,9 +90,16 @@ + <argument name="preProcessor" xsi:type="object">Magento\Framework\App\Config\PreProcessorComposite</argument> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> + <argument name="reader" xsi:type="object">Magento\Config\App\Config\Type\System\Reader\Proxy</argument> +- <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> ++ <argument name="lockQuery" xsi:type="object">systemConfigQueryLocker</argument> + </arguments> + </type> ++ ++ <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> ++ <arguments> ++ <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> ++ </arguments> ++ </virtualType> ++ + <type name="Magento\Config\App\Config\Type\System\Reader"> + <arguments> + <argument name="source" xsi:type="object">systemConfigSourceAggregated</argument> +diff -Nuar a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -1756,4 +1756,19 @@ + </argument> + </arguments> + </type> ++ <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> ++ <arguments> ++ <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> ++ </arguments> ++ </type> ++ <virtualType name="configured_block_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">block_html</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Framework\View\Element\Context"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_block_cache</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +@@ -0,0 +1,147 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++ ++namespace Magento\Framework\Cache; ++ ++use Magento\Framework\Lock\LockManagerInterface; ++ ++/** ++ * Default mutex that provide concurrent access to cache storage. ++ */ ++class LockGuardedCacheLoader ++{ ++ /** ++ * @var LockManagerInterface ++ */ ++ private $locker; ++ ++ /** ++ * Lifetime of the lock for write in cache. ++ * ++ * Value of the variable in milliseconds. ++ * ++ * @var int ++ */ ++ private $lockTimeout; ++ ++ /** ++ * Timeout between retrieves to load the configuration from the cache. ++ * ++ * Value of the variable in milliseconds. ++ * ++ * @var int ++ */ ++ private $delayTimeout; ++ ++ /** ++ * Timeout for information to be collected and saved. ++ * If timeout passed that means that data cannot be saved right now. ++ * And we will just return collected data. ++ * ++ * Value of the variable in milliseconds. ++ * ++ * @var int ++ */ ++ private $loadTimeout; ++ ++ /** ++ * Minimal delay timeout in ms. ++ * ++ * @var int ++ */ ++ private $minimalDelayTimeout; ++ ++ /** ++ * @param LockManagerInterface $locker ++ * @param int $lockTimeout ++ * @param int $delayTimeout ++ * @param int $loadTimeout ++ * @param int $minimalDelayTimeout ++ */ ++ public function __construct( ++ LockManagerInterface $locker, ++ int $lockTimeout = 10000, ++ int $delayTimeout = 20, ++ int $loadTimeout = 10000, ++ int $minimalDelayTimeout = 5 ++ ) { ++ $this->locker = $locker; ++ $this->lockTimeout = $lockTimeout; ++ $this->delayTimeout = $delayTimeout; ++ $this->loadTimeout = $loadTimeout; ++ $this->minimalDelayTimeout = $minimalDelayTimeout; ++ } ++ ++ /** ++ * Load data. ++ * ++ * @param string $lockName ++ * @param callable $dataLoader ++ * @param callable $dataCollector ++ * @param callable $dataSaver ++ * @return mixed ++ */ ++ public function lockedLoadData( ++ string $lockName, ++ callable $dataLoader, ++ callable $dataCollector, ++ callable $dataSaver ++ ) { ++ $cachedData = $dataLoader(); //optimistic read ++ $deadline = microtime(true) + $this->loadTimeout / 100; ++ ++ while ($cachedData === false) { ++ if ($deadline <= microtime(true)) { ++ return $dataCollector(); ++ } ++ ++ if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { ++ try { ++ $data = $dataCollector(); ++ $dataSaver($data); ++ $cachedData = $data; ++ } finally { ++ $this->locker->unlock($lockName); ++ } ++ } ++ ++ if ($cachedData === false) { ++ usleep($this->getLookupTimeout() * 1000); ++ $cachedData = $dataLoader(); ++ } ++ } ++ ++ return $cachedData; ++ } ++ ++ /** ++ * Clean data. ++ * ++ * @param string $lockName ++ * @param callable $dataCleaner ++ * @return void ++ */ ++ public function lockedCleanData(string $lockName, callable $dataCleaner) ++ { ++ while ($this->locker->isLocked($lockName)) { ++ usleep($this->getLookupTimeout() * 1000); ++ } ++ ++ $dataCleaner(); ++ } ++ ++ /** ++ * Delay will be applied as rand($minimalDelayTimeout, $delayTimeout). ++ * This helps to desynchronize multiple clients trying ++ * to acquire the lock for the same resource at the same time ++ * ++ * @return int ++ */ ++ private function getLookupTimeout() ++ { ++ return rand($this->minimalDelayTimeout, $this->delayTimeout); ++ } ++} +diff -Nuar a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php +--- a/vendor/magento/framework/Lock/Backend/Cache.php ++++ b/vendor/magento/framework/Lock/Backend/Cache.php +@@ -14,24 +14,57 @@ use Magento\Framework\Cache\FrontendInterface; + */ + class Cache implements \Magento\Framework\Lock\LockManagerInterface + { ++ /** ++ * Prefix for marking that key is locked or not. ++ */ ++ const LOCK_PREFIX = 'LOCKED_RECORD_INFO_'; ++ + /** + * @var FrontendInterface + */ + private $cache; + ++ /** ++ * Sign for locks, helps to avoid removing a lock that was created by another client ++ * ++ * @string ++ */ ++ private $lockSign; ++ + /** + * @param FrontendInterface $cache + */ + public function __construct(FrontendInterface $cache) + { + $this->cache = $cache; ++ $this->lockSign = $this->generateLockSign(); + } ++ + /** + * @inheritdoc + */ + public function lock(string $name, int $timeout = -1): bool + { +- return $this->cache->save('1', $name, [], $timeout); ++ if (empty($this->lockSign)) { ++ $this->lockSign = $this->generateLockSign(); ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false !== $data) { ++ return false; ++ } ++ ++ $timeout = $timeout <= 0 ? null : $timeout; ++ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if ($data === $this->lockSign) { ++ return true; ++ } ++ ++ return false; + } + + /** +@@ -39,7 +72,22 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + public function unlock(string $name): bool + { +- return $this->cache->remove($name); ++ if (empty($this->lockSign)) { ++ return false; ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false === $data) { ++ return false; ++ } ++ ++ $removeResult = false; ++ if ($data === $this->lockSign) { ++ $removeResult = (bool)$this->cache->remove($this->getIdentifier($name)); ++ } ++ ++ return $removeResult; + } + + /** +@@ -47,6 +95,40 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + public function isLocked(string $name): bool + { +- return (bool)$this->cache->test($name); ++ return (bool)$this->cache->test($this->getIdentifier($name)); ++ } ++ ++ /** ++ * Get cache locked identifier based on cache identifier. ++ * ++ * @param string $cacheIdentifier ++ * @return string ++ */ ++ private function getIdentifier(string $cacheIdentifier): string ++ { ++ return self::LOCK_PREFIX . $cacheIdentifier; ++ } ++ ++ /** ++ * Function that generates lock sign that helps to avoid removing a lock that was created by another client. ++ * ++ * @return string ++ */ ++ private function generateLockSign() ++ { ++ $sign = implode( ++ '-', ++ [ ++ \getmypid(), \crc32(\gethostname()) ++ ] ++ ); ++ ++ try { ++ $sign .= '-' . \bin2hex(\random_bytes(4)); ++ } catch (\Exception $e) { ++ $sign .= '-' . \uniqid('-uniqid-'); ++ } ++ ++ return $sign; + } + } +diff -Nuar a/vendor/magento/framework/View/Element/AbstractBlock.php b/vendor/magento/framework/View/Element/AbstractBlock.php +--- a/vendor/magento/framework/View/Element/AbstractBlock.php ++++ b/vendor/magento/framework/View/Element/AbstractBlock.php +@@ -3,8 +3,10 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++ + namespace Magento\Framework\View\Element; + ++use Magento\Framework\Cache\LockGuardedCacheLoader; + use Magento\Framework\DataObject\IdentityInterface; + + /** +@@ -14,6 +16,7 @@ use Magento\Framework\DataObject\IdentityInterface; + * + * Marked as public API because it is actively used now. + * ++ * phpcs:disable Magento2.Classes.AbstractApi + * @api + * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) +@@ -52,6 +55,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + * SID Resolver + * + * @var \Magento\Framework\Session\SidResolverInterface ++ * @deprecated 102.0.5 Not used anymore. + */ + protected $_sidResolver; + +@@ -176,14 +180,21 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + */ + protected $_cache; + ++ /** ++ * @var LockGuardedCacheLoader ++ */ ++ private $lockQuery; ++ + /** + * Constructor + * + * @param \Magento\Framework\View\Element\Context $context + * @param array $data + */ +- public function __construct(\Magento\Framework\View\Element\Context $context, array $data = []) +- { ++ public function __construct( ++ \Magento\Framework\View\Element\Context $context, ++ array $data = [] ++ ) { + $this->_request = $context->getRequest(); + $this->_layout = $context->getLayout(); + $this->_eventManager = $context->getEventManager(); +@@ -201,6 +212,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + $this->filterManager = $context->getFilterManager(); + $this->_localeDate = $context->getLocaleDate(); + $this->inlineTranslation = $context->getInlineTranslation(); ++ $this->lockQuery = $context->getLockGuardedCacheLoader(); + if (isset($data['jsLayout'])) { + $this->jsLayout = $data['jsLayout']; + unset($data['jsLayout']); +@@ -235,6 +247,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + * Please override this one instead of overriding real __construct constructor + * + * @return void ++ * phpcs:disable Magento2.CodeAnalysis.EmptyBlock + */ + protected function _construct() + { +@@ -430,9 +443,9 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + */ + public function unsetCallChild($alias, $callback, $result, $params) + { ++ $args = func_get_args(); + $child = $this->getChildBlock($alias); + if ($child) { +- $args = func_get_args(); + $alias = array_shift($args); + $callback = array_shift($args); + $result = (string)array_shift($args); +@@ -659,19 +672,6 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + } + + $html = $this->_loadCache(); +- if ($html === false) { +- if ($this->hasData('translate_inline')) { +- $this->inlineTranslation->suspend($this->getData('translate_inline')); +- } +- +- $this->_beforeToHtml(); +- $html = $this->_toHtml(); +- $this->_saveCache($html); +- +- if ($this->hasData('translate_inline')) { +- $this->inlineTranslation->resume(); +- } +- } + $html = $this->_afterToHtml($html); + + /** @var \Magento\Framework\DataObject */ +@@ -680,10 +680,13 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + 'html' => $html, + ] + ); +- $this->_eventManager->dispatch('view_block_abstract_to_html_after', [ +- 'block' => $this, +- 'transport' => $transportObject +- ]); ++ $this->_eventManager->dispatch( ++ 'view_block_abstract_to_html_after', ++ [ ++ 'block' => $this, ++ 'transport' => $transportObject ++ ] ++ ); + $html = $transportObject->getHtml(); + + return $html; +@@ -726,7 +729,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + */ + public function getUiId($arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null, $arg5 = null) + { +- return ' data-ui-id="' . $this->getJsId($arg1, $arg2, $arg3, $arg4, $arg5) . '" '; ++ return ' data-ui-id="' . $this->escapeHtmlAttr($this->getJsId($arg1, $arg2, $arg3, $arg4, $arg5)) . '" '; + } + + /** +@@ -875,7 +878,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + $namespace = substr( + $className, + 0, +- strpos($className, '\\' . 'Block') ++ strpos($className, '\\' . 'Block' . '\\') + ); + return str_replace('\\', '_', $namespace); + } +@@ -973,8 +976,8 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + * + * Use $addSlashes = false for escaping js that inside html attribute (onClick, onSubmit etc) + * +- * @param string $data +- * @param bool $addSlashes ++ * @param string $data ++ * @param bool $addSlashes + * @return string + * @deprecated 101.0.0 + */ +@@ -1084,23 +1087,43 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + /** + * Load block html from cache storage + * +- * @return string|false ++ * @return string + */ + protected function _loadCache() + { ++ $collectAction = function () { ++ if ($this->hasData('translate_inline')) { ++ $this->inlineTranslation->suspend($this->getData('translate_inline')); ++ } ++ ++ $this->_beforeToHtml(); ++ return $this->_toHtml(); ++ }; ++ + if ($this->getCacheLifetime() === null || !$this->_cacheState->isEnabled(self::CACHE_GROUP)) { +- return false; +- } +- $cacheKey = $this->getCacheKey(); +- $cacheData = $this->_cache->load($cacheKey); +- if ($cacheData) { +- $cacheData = str_replace( +- $this->_getSidPlaceholder($cacheKey), +- $this->_sidResolver->getSessionIdQueryParam($this->_session) . '=' . $this->_session->getSessionId(), +- $cacheData +- ); ++ $html = $collectAction(); ++ if ($this->hasData('translate_inline')) { ++ $this->inlineTranslation->resume(); ++ } ++ return $html; + } +- return $cacheData; ++ $loadAction = function () { ++ return $this->_cache->load($this->getCacheKey()); ++ }; ++ ++ $saveAction = function ($data) { ++ $this->_saveCache($data); ++ if ($this->hasData('translate_inline')) { ++ $this->inlineTranslation->resume(); ++ } ++ }; ++ ++ return (string)$this->lockQuery->lockedLoadData( ++ $this->getCacheKey(), ++ $loadAction, ++ $collectAction, ++ $saveAction ++ ); + } + + /** +@@ -1115,11 +1138,6 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl + return false; + } + $cacheKey = $this->getCacheKey(); +- $data = str_replace( +- $this->_sidResolver->getSessionIdQueryParam($this->_session) . '=' . $this->_session->getSessionId(), +- $this->_getSidPlaceholder($cacheKey), +- $data +- ); + + $this->_cache->save($data, $cacheKey, array_unique($this->getCacheTags()), $this->getCacheLifetime()); + return $this; +diff -Nuar a/vendor/magento/framework/View/Element/Context.php b/vendor/magento/framework/View/Element/Context.php +--- a/vendor/magento/framework/View/Element/Context.php ++++ b/vendor/magento/framework/View/Element/Context.php +@@ -5,6 +5,9 @@ + */ + namespace Magento\Framework\View\Element; + ++use Magento\Framework\Cache\LockGuardedCacheLoader; ++use Magento\Framework\App\ObjectManager; ++ + /** + * Constructor modification point for Magento\Framework\View\Element\AbstractBlock. + * +@@ -16,8 +19,7 @@ namespace Magento\Framework\View\Element; + * As Magento moves from inheritance-based APIs all such classes will be deprecated together with + * the classes they were introduced for. + * +- * @SuppressWarnings(PHPMD.TooManyFields) +- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD) + * + * @api + * @since 100.0.2 +@@ -137,12 +139,16 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + */ + protected $inlineTranslation; + ++ /** ++ * @var LockGuardedCacheLoader ++ */ ++ private $lockQuery; ++ + /** + * @param \Magento\Framework\App\RequestInterface $request + * @param \Magento\Framework\View\LayoutInterface $layout + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\UrlInterface $urlBuilder +- * @param \Magento\Framework\TranslateInterface $translator + * @param \Magento\Framework\App\CacheInterface $cache + * @param \Magento\Framework\View\DesignInterface $design + * @param \Magento\Framework\Session\SessionManagerInterface $session +@@ -156,6 +162,7 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + * @param \Magento\Framework\Filter\FilterManager $filterManager + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation ++ * @param LockGuardedCacheLoader $lockQuery + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -176,7 +183,8 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + \Magento\Framework\Escaper $escaper, + \Magento\Framework\Filter\FilterManager $filterManager, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, +- \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation ++ \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, ++ LockGuardedCacheLoader $lockQuery = null + ) { + $this->_request = $request; + $this->_layout = $layout; +@@ -195,6 +203,7 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + $this->_filterManager = $filterManager; + $this->_localeDate = $localeDate; + $this->inlineTranslation = $inlineTranslation; ++ $this->lockQuery = $lockQuery ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); + } + + /** +@@ -358,10 +367,23 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface + } + + /** ++ * Get locale date. ++ * + * @return \Magento\Framework\Stdlib\DateTime\TimezoneInterface + */ + public function getLocaleDate() + { + return $this->_localeDate; + } ++ ++ /** ++ * Lock guarded cache loader. ++ * ++ * @return LockGuardedCacheLoader ++ * @since 102.0.2 ++ */ ++ public function getLockGuardedCacheLoader() ++ { ++ return $this->lockQuery; ++ } + } + +diff -Nuar a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module-eav/Model/Config.php +--- a/vendor/magento/module-eav/Model/Config.php ++++ b/vendor/magento/module-eav/Model/Config.php +@@ -7,12 +7,20 @@ namespace Magento\Eav\Model; + + use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; + use Magento\Eav\Model\Entity\Type; ++use Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface; ++use Magento\Framework\App\Config\ScopeConfigInterface; + use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Model\AbstractModel; + use Magento\Framework\Serialize\SerializerInterface; + + /** ++ * EAV config model. ++ * + * @api ++ * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @since 100.0.2 + */ + class Config +@@ -25,6 +33,11 @@ class Config + const ATTRIBUTES_CODES_CACHE_ID = 'EAV_ENTITY_ATTRIBUTES_CODES'; + /**#@-*/ + ++ /** ++ * Xml path to caching user defined eav attributes configuration. ++ */ ++ private const XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES = 'dev/caching/cache_user_defined_attributes'; ++ + /**#@-*/ + protected $_entityTypeData; + +@@ -116,6 +129,11 @@ class Config + */ + private $serializer; + ++ /** ++ * @var ScopeConfigInterface ++ */ ++ private $scopeConfig; ++ + /** + * Cache of attributes per set + * +@@ -123,13 +141,29 @@ class Config + */ + private $attributesPerSet = []; + ++ /** ++ * Is system attributes loaded flag. ++ * ++ * @var array ++ */ ++ private $isSystemAttributesLoaded = []; ++ ++ /** ++ * List of predefined system attributes for preload. ++ * ++ * @var array ++ */ ++ private $attributesForPreload; ++ + /** + * @param \Magento\Framework\App\CacheInterface $cache +- * @param \Magento\Eav\Model\Entity\TypeFactory $entityTypeFactory +- * @param \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory ++ * @param Entity\TypeFactory $entityTypeFactory ++ * @param ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory + * @param \Magento\Framework\App\Cache\StateInterface $cacheState + * @param \Magento\Framework\Validator\UniversalFactory $universalFactory +- * @param SerializerInterface $serializer ++ * @param SerializerInterface|null $serializer ++ * @param ScopeConfigInterface|null $scopeConfig ++ * @param array $attributesForPreload + * @codeCoverageIgnore + */ + public function __construct( +@@ -138,7 +172,9 @@ class Config + \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory, + \Magento\Framework\App\Cache\StateInterface $cacheState, + \Magento\Framework\Validator\UniversalFactory $universalFactory, +- SerializerInterface $serializer = null ++ SerializerInterface $serializer = null, ++ ScopeConfigInterface $scopeConfig = null, ++ $attributesForPreload = [] + ) { + $this->_cache = $cache; + $this->_entityTypeFactory = $entityTypeFactory; +@@ -146,6 +182,8 @@ class Config + $this->_cacheState = $cacheState; + $this->_universalFactory = $universalFactory; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); ++ $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); ++ $this->attributesForPreload = $attributesForPreload; + } + + /** +@@ -207,8 +245,8 @@ class Config + /** + * Associate object with identifier + * +- * @param mixed $obj +- * @param mixed $id ++ * @param mixed $obj ++ * @param mixed $id + * @return void + * @codeCoverageIgnore + */ +@@ -233,8 +271,8 @@ class Config + /** + * Specify reference for entity type id + * +- * @param int $id +- * @param string $code ++ * @param int $id ++ * @param string $code + * @return $this + * @codeCoverageIgnore + */ +@@ -258,9 +296,9 @@ class Config + /** + * Specify reference between entity attribute id and attribute code + * +- * @param int $id +- * @param string $code +- * @param string $entityTypeCode ++ * @param int $id ++ * @param string $code ++ * @param string $entityTypeCode + * @return $this + */ + protected function _addAttributeReference($id, $code, $entityTypeCode) +@@ -336,7 +374,9 @@ class Config + } + \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); + +- if ($this->isCacheEnabled() && ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID))) { ++ if ($this->isCacheEnabled() && ++ ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID)) ++ ) { + $this->_entityTypeData = $this->serializer->unserialize($cache); + foreach ($this->_entityTypeData as $typeCode => $data) { + $typeId = $data['entity_type_id']; +@@ -484,10 +524,10 @@ class Config + /** + * Get attribute by code for entity type + * +- * @param mixed $entityType +- * @param mixed $code +- * @return AbstractAttribute +- * @throws \Magento\Framework\Exception\LocalizedException ++ * @param mixed $entityType ++ * @param mixed $code ++ * @return AbstractAttribute ++ * @throws LocalizedException + */ + public function getAttribute($entityType, $code) + { +@@ -507,8 +547,152 @@ class Config + return $this->attributes[$entityTypeCode][$code]; + } + ++ if (array_key_exists($entityTypeCode, $this->attributesForPreload) ++ && array_key_exists($code, $this->attributesForPreload[$entityTypeCode]) ++ ) { ++ $this->initSystemAttributes($entityType, $this->attributesForPreload[$entityTypeCode]); ++ } ++ if (isset($this->attributes[$entityTypeCode][$code])) { ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $this->attributes[$entityTypeCode][$code]; ++ } ++ ++ if ($this->scopeConfig->getValue(self::XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES)) { ++ $attribute = $this->cacheUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } else { ++ $attribute = $this->initUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $attribute; ++ } ++ ++ /** ++ * Initialize predefined system attributes for preload. ++ * ++ * @param string $entityType ++ * @param array $systemAttributes ++ * @return $this|bool|void ++ * @throws LocalizedException ++ */ ++ private function initSystemAttributes($entityType, $systemAttributes) ++ { ++ $entityType = $this->getEntityType($entityType); ++ $entityTypeCode = $entityType->getEntityTypeCode(); ++ if (!empty($this->isSystemAttributesLoaded[$entityTypeCode])) { ++ return; ++ } ++ ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-' . $entityTypeCode . '-preload'; ++ if ($this->isCacheEnabled() && ($attributes = $this->_cache->load($cacheKey))) { ++ $attributes = $this->serializer->unserialize($attributes); ++ if ($attributes) { ++ foreach ($attributes as $attribute) { ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ } ++ return true; ++ } ++ } ++ ++ \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); ++ ++ /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection $attributes */ ++ $attributes = $this->_universalFactory->create( ++ $entityType->getEntityAttributeCollection() ++ )->setEntityTypeFilter( ++ $entityType ++ )->addFieldToFilter( ++ 'attribute_code', ++ ['in' => array_keys($systemAttributes)] ++ )->getData(); ++ ++ $attributeData = []; ++ foreach ($attributes as $attribute) { ++ if (empty($attribute['attribute_model'])) { ++ $attribute['attribute_model'] = $entityType->getAttributeModel(); ++ } ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ $attributeData[$attribute['attribute_code']] = $attributeObject->toArray(); ++ } ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attributeData), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ $this->isSystemAttributesLoaded[$entityTypeCode] = true; ++ ++ return $this; ++ } ++ ++ /** ++ * Initialize user defined attribute from cache or cache it. ++ * ++ * @param string $entityType ++ * @param mixed $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute ++ * @throws LocalizedException ++ */ ++ private function cacheUserDefinedAttribute($entityType, $entityTypeCode, $code): AbstractAttribute ++ { ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-attribute-' . $entityTypeCode . '-' . $code; ++ $attributeData = $this->isCacheEnabled() && ($attribute = $this->_cache->load($cacheKey)) ++ ? $this->serializer->unserialize($attribute) ++ : null; ++ if ($attributeData) { ++ if (isset($attributeData['attribute_id'])) { ++ $attribute = $this->_createAttribute($entityType, $attributeData); ++ } else { ++ $entityType = $this->getEntityType($entityType); ++ $attribute = $this->createAttribute($entityType->getAttributeModel()); ++ $attribute->setAttributeCode($code); ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ } ++ } else { ++ $attribute = $this->createAttributeByAttributeCode($entityType, $code); ++ $this->_addAttributeReference( ++ $attribute->getAttributeId(), ++ $attribute->getAttributeCode(), ++ $entityTypeCode ++ ); ++ $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attribute->getData()), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ } ++ ++ return $attribute; ++ } ++ ++ /** ++ * Initialize user defined attribute and save it to memory cache. ++ * ++ * @param mixed $entityType ++ * @param string $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute|null ++ * @throws LocalizedException ++ */ ++ private function initUserDefinedAttribute($entityType, $entityTypeCode, $code): ?AbstractAttribute ++ { + $attributes = $this->loadAttributes($entityTypeCode); +- $attribute = isset($attributes[$code]) ? $attributes[$code] : null; ++ $attribute = $attributes[$code] ?? null; + if (!$attribute) { + $attribute = $this->createAttributeByAttributeCode($entityType, $code); + $this->_addAttributeReference( +@@ -518,7 +702,7 @@ class Config + ); + $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); + } +- \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ + return $attribute; + } + +@@ -555,8 +739,8 @@ class Config + /** + * Get all entity type attributes + * +- * @param int|string|Type $entityType +- * @param \Magento\Framework\DataObject|null $object ++ * @param int|string|Type $entityType ++ * @param \Magento\Framework\DataObject|null $object + * @return AbstractAttribute[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) +@@ -639,7 +823,11 @@ class Config + $existsFullAttribute = $attribute->hasIsRequired(); + $fullAttributeData = array_key_exists('is_required', $attributeData); + +- if ($existsFullAttribute || !$existsFullAttribute && !$fullAttributeData) { ++ if ($existsFullAttribute || (!$existsFullAttribute && !$fullAttributeData)) { ++ $scopeIsRequired = $attributeData['scope_is_required'] ?? null; ++ if ($scopeIsRequired !== null) { ++ $attribute->setData('scope_is_required', $scopeIsRequired); ++ } + return $attribute; + } + } +@@ -708,6 +896,7 @@ class Config + * @param string $entityType + * @param string $attributeCode + * @return AbstractAttribute ++ * @throws LocalizedException + */ + private function createAttributeByAttributeCode($entityType, $attributeCode) + { +@@ -723,13 +912,28 @@ class Config + $attribute->setAttributeCode($attributeCode); + } + ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ ++ return $attribute; ++ } ++ ++ /** ++ * Set entity type id, backend type, is global to attribute. ++ * ++ * @param AbstractAttribute $attribute ++ * @param AbstractModel $entityType ++ * @return AbstractAttribute ++ */ ++ private function setAttributeData($attribute, $entityType): AbstractAttribute ++ { + $entity = $entityType->getEntity(); +- if ($entity instanceof \Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface ++ if ($entity instanceof ProviderInterface + && in_array($attribute->getAttributeCode(), $entity->getDefaultAttributes(), true) + ) { + $attribute->setBackendType(AbstractAttribute::TYPE_STATIC)->setIsGlobal(1); + } + $attribute->setEntityType($entityType)->setEntityTypeId($entityType->getId()); ++ + return $attribute; + } + +diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +--- a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php ++++ b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +@@ -6,6 +6,9 @@ + + namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; + ++/** ++ * Basic implementation for attribute sets ++ */ + class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + { + /** +@@ -24,8 +27,6 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + protected $eavConfig; + + /** +- * Constructor +- * + * @param \Magento\Framework\Model\ResourceModel\Db\Context $context + * @param GroupFactory $attrGroupFactory + * @param \Magento\Eav\Model\Config $eavConfig +@@ -54,7 +55,7 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + + /** +- * Perform actions after object save ++ * Perform actions after object save. + * + * @param \Magento\Framework\Model\AbstractModel $object + * @return $this +diff -Nuar a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/etc/di.xml +--- a/vendor/magento/module-eav/etc/di.xml ++++ b/vendor/magento/module-eav/etc/di.xml +@@ -209,4 +209,14 @@ + </argument> + </arguments> + </type> ++ <virtualType name="configured_eav_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">eav</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Eav\Model\Config"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_eav_cache</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-theme/etc/di.xml +--- a/vendor/magento/module-theme/etc/di.xml ++++ b/vendor/magento/module-theme/etc/di.xml +@@ -285,4 +285,24 @@ + <argument name="identifierName" xsi:type="string">theme_id</argument> + </arguments> + </type> ++ <virtualType name="configured_design_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">layout</argument> ++ </arguments> ++ </virtualType> ++ <virtualType name="design_context" type="Magento\Framework\Model\Context"> ++ <arguments> ++ <argument name="cacheManager" xsi:type="object">configured_design_cache</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Theme\Model\Design"> ++ <arguments> ++ <argument name="context" xsi:type="object">design_context</argument> ++ </arguments> ++ </type> ++ <type name="Magento\Theme\Model\Theme\ThemeProvider"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_design_cache</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/App/Cache.php +--- a/vendor/magento/framework/App/Cache.php ++++ b/vendor/magento/framework/App/Cache.php +@@ -4,12 +4,11 @@ + * See COPYING.txt for license details. + */ + +-/** +- * System cache model +- * support id and tags prefix support, +- */ + namespace Magento\Framework\App; + ++/** ++ * System cache model support id and tags prefix support. ++ */ + class Cache implements CacheInterface + { + /** +@@ -30,12 +29,13 @@ class Cache implements CacheInterface + protected $_frontend; + + /** +- * @param \Magento\Framework\App\Cache\Frontend\Pool $frontendPool ++ * @param Cache\Frontend\Pool $frontendPool ++ * @param string|null $cacheIdentifier + */ +- public function __construct(\Magento\Framework\App\Cache\Frontend\Pool $frontendPool) ++ public function __construct(\Magento\Framework\App\Cache\Frontend\Pool $frontendPool, $cacheIdentifier = null) + { + $this->_frontendPool = $frontendPool; +- $this->_frontend = $frontendPool->get($this->_frontendIdentifier); ++ $this->_frontend = $frontendPool->get($cacheIdentifier ?? $this->_frontendIdentifier); + } + + /** +diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Pool.php b/vendor/magento/framework/App/Cache/Frontend/Pool.php +--- a/vendor/magento/framework/App/Cache/Frontend/Pool.php ++++ b/vendor/magento/framework/App/Cache/Frontend/Pool.php +@@ -152,6 +152,15 @@ class Pool implements \Iterator + if (isset($this->_instances[$identifier])) { + return $this->_instances[$identifier]; + } +- throw new \InvalidArgumentException("Cache frontend '{$identifier}' is not recognized."); ++ ++ if (!isset($this->_instances[self::DEFAULT_FRONTEND_ID])) { ++ throw new \InvalidArgumentException( ++ "Cache frontend '{$identifier}' is not recognized. As well as " . ++ self::DEFAULT_FRONTEND_ID . ++ "cache is not configured" ++ ); ++ } ++ ++ return $this->_instances[self::DEFAULT_FRONTEND_ID]; + } + } +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +--- a/vendor/magento/framework/App/Router/ActionList.php ++++ b/vendor/magento/framework/App/Router/ActionList.php +@@ -5,6 +5,8 @@ + */ + namespace Magento\Framework\App\Router; + ++use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Framework\App\State; + use Magento\Framework\Serialize\SerializerInterface; + use Magento\Framework\Serialize\Serializer\Serialize; + use Magento\Framework\Module\Dir\Reader as ModuleReader; +@@ -70,12 +72,26 @@ class ActionList + $this->reservedWords = array_merge($reservedWords, $this->reservedWords); + $this->actionInterface = $actionInterface; + $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Serialize::class); +- $data = $cache->load($cacheKey); +- if (!$data) { +- $this->actions = $moduleReader->getActionFiles(); +- $cache->save($this->serializer->serialize($this->actions), $cacheKey); ++ $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); ++ $state = $objectManager->get(State::class); ++ ++ if ($state->getMode() === State::MODE_PRODUCTION) { ++ $directoryList = $objectManager->get(DirectoryList::class); ++ $file = $directoryList->getPath(DirectoryList::GENERATED_METADATA) . '/' . $cacheKey . '.' . 'php'; ++ ++ if (file_exists($file)) { ++ $this->actions = (include $file) ?? $moduleReader->getActionFiles(); ++ } else { ++ $this->actions = $moduleReader->getActionFiles(); ++ } + } else { +- $this->actions = $this->serializer->unserialize($data); ++ $data = $cache->load($cacheKey); ++ if (!$data) { ++ $this->actions = $moduleReader->getActionFiles(); ++ $cache->save($this->serializer->serialize($this->actions), $cacheKey); ++ } else { ++ $this->actions = $this->serializer->unserialize($data); ++ } + } + } + +diff -Nuar a/2.3.5/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/Cache/Backend/Redis.php +@@ -0,0 +1,83 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++ ++namespace Magento\Framework\Cache\Backend; ++ ++/** ++ * Redis wrapper to extend current implementation behaviour. ++ */ ++class Redis extends \Cm_Cache_Backend_Redis ++{ ++ /** ++ * Local state of preloaded keys. ++ * ++ * @var array ++ */ ++ private $preloadedData = []; ++ ++ /** ++ * Array of keys to be preloaded. ++ * ++ * @var array ++ */ ++ private $preloadKeys = []; ++ ++ /** ++ * @param array $options ++ */ ++ public function __construct($options = []) ++ { ++ $this->preloadKeys = $options['preload_keys'] ?? []; ++ parent::__construct($options); ++ } ++ ++ /** ++ * Load value with given id from cache ++ * ++ * @param string $id Cache id ++ * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested ++ * @return bool|string ++ */ ++ public function load($id, $doNotTestCacheValidity = false) ++ { ++ if (!empty($this->preloadKeys) && empty($this->preloadedData)) { ++ $redis = $this->_slave ?? $this->_redis; ++ $redis = $redis->pipeline(); ++ ++ foreach ($this->preloadKeys as $key) { ++ $redis->hGet(self::PREFIX_KEY . $key, self::FIELD_DATA); ++ } ++ ++ $this->preloadedData = array_filter(array_combine($this->preloadKeys, $redis->exec())); ++ } ++ ++ if (isset($this->preloadedData[$id])) { ++ return $this->_decodeData($this->preloadedData[$id]); ++ } ++ ++ return parent::load($id, $doNotTestCacheValidity); ++ } ++ ++ /** ++ * Cover errors on save operations, which may occurs when Redis cannot evict keys, which is expected in some cases. ++ * ++ * @param string $data ++ * @param string $id ++ * @param array $tags ++ * @param bool $specificLifetime ++ * @return bool ++ */ ++ public function save($data, $id, $tags = [], $specificLifetime = false) ++ { ++ try { ++ parent::save($data, $id, $tags, $specificLifetime); ++ } catch (\Throwable $exception) { ++ return false; ++ } ++ ++ return true; ++ } ++} +diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +--- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php ++++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +@@ -9,8 +9,10 @@ namespace Magento\Framework\Cache\Backend; + /** + * Remote synchronized cache + * +- * This class created for correct work local caches with multiple web nodes, +- * that will be check cache status from remote cache ++ * This class created for correct work witch local caches and multiple web nodes, ++ * in order to be sure that we always have up to date local version of cache. ++ * This class will be check cache version from remote cache and in case it newer ++ * than local one, it will update local one from remote cache a.k.a two level cache. + */ + class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface + { +@@ -36,11 +38,15 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + protected $cacheInvalidationTime; + + /** +- * {@inheritdoc} ++ * Suffix for hash to compare data version in cache storage. ++ */ ++ private const HASH_SUFFIX = ':hash'; ++ ++ /** ++ * @inheritdoc + */ + protected $_options = [ + 'remote_backend' => '', +- 'remote_backend_invalidation_time_id' => 'default_remote_backend_invalidation_time', + 'remote_backend_custom_naming' => true, + 'remote_backend_autoload' => true, + 'remote_backend_options' => [], +@@ -52,6 +58,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + + /** + * @param array $options ++ * @throws \Zend_Cache_Exception + */ + public function __construct(array $options = []) + { +@@ -97,76 +104,137 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * Update remote cache status info ++ * @inheritdoc ++ */ ++ public function setDirectives($directives) ++ { ++ return $this->local->setDirectives($directives); ++ } ++ ++ /** ++ * Return hash sign of the data. + * +- * @return void ++ * @param string $data ++ * @return string + */ +- private function updateRemoteCacheStatusInfo() ++ private function getDataVersion(string $data) + { +- $this->remote->save(time(), $this->_options['remote_backend_invalidation_time_id'], [], null); +- $this->cacheInvalidationTime = null; ++ return \hash('sha256', $data); + } + + /** +- * {@inheritdoc} ++ * Load data version by id from remote. ++ * ++ * @param string $id ++ * @return false|string + */ +- public function setDirectives($directives) ++ private function loadRemoteDataVersion(string $id) + { +- return $this->local->setDirectives($directives); ++ return $this->remote->load( ++ $id . self::HASH_SUFFIX ++ ); + } + + /** +- * {@inheritdoc} ++ * Save new data version to remote. ++ * ++ * @param string $data ++ * @param string $id ++ * @param array $tags ++ * @param mixed $specificLifetime ++ * @return bool ++ */ ++ private function saveRemoteDataVersion(string $data, string $id, array $tags, $specificLifetime = false) ++ { ++ return $this->remote->save($this->getDataVersion($data), $id . self::HASH_SUFFIX, $tags, $specificLifetime); ++ } ++ ++ /** ++ * Remove remote data version. ++ * ++ * @param string $id ++ * @return bool ++ */ ++ private function removeRemoteDataVersion($id) ++ { ++ return $this->remote->remove($id . self::HASH_SUFFIX); ++ } ++ ++ /** ++ * @inheritdoc + */ + public function load($id, $doNotTestCacheValidity = false) + { +- $dataModificationTime = $this->local->test($id); +- if ($this->cacheInvalidationTime === null) { +- $this->cacheInvalidationTime = $this->remote->load($this->_options['remote_backend_invalidation_time_id']); +- } +- if ($dataModificationTime >= $this->cacheInvalidationTime) { +- return $this->local->load($id, $doNotTestCacheValidity); ++ $localData = $this->local->load($id); ++ $remoteData = false; ++ ++ if (false === $localData) { ++ $remoteData = $this->remote->load($id); ++ ++ if (false === $remoteData) { ++ return false; ++ } + } else { +- return false; ++ if ($this->getDataVersion($localData) !== $this->loadRemoteDataVersion($id)) { ++ $localData = false; ++ $remoteData = $this->remote->load($id); ++ } + } ++ ++ if ($remoteData !== false) { ++ $this->local->save($remoteData, $id); ++ $localData = $remoteData; ++ } ++ ++ return $localData; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function test($id) + { +- return $this->local->test($id); ++ return $this->local->test($id) ?? $this->remote->test($id); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function save($data, $id, $tags = [], $specificLifetime = false) + { +- return $this->local->save($data, $id, $tags, $specificLifetime); ++ $dataToSave = $data; ++ $remHash = $this->loadRemoteDataVersion($id); ++ ++ if ($remHash !== false) { ++ $dataToSave = $this->remote->load($id); ++ } else { ++ $this->remote->save($data, $id, $tags, $specificLifetime); ++ $this->saveRemoteDataVersion($data, $id, $tags, $specificLifetime); ++ } ++ ++ return $this->local->save($dataToSave, $id, [], $specificLifetime); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function remove($id) + { +- $this->updateRemoteCacheStatusInfo(); +- return $this->local->remove($id); ++ return $this->removeRemoteDataVersion($id) && ++ $this->remote->remove($id) && ++ $this->local->remove($id); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = []) + { +- $this->updateRemoteCacheStatusInfo(); +- return $this->local->clean($mode, $tags); ++ return $this->remote->clean($mode, $tags); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIds() + { +@@ -174,7 +242,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getTags() + { +@@ -182,7 +250,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsMatchingTags($tags = []) + { +@@ -190,7 +258,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsNotMatchingTags($tags = []) + { +@@ -198,7 +266,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsMatchingAnyTags($tags = []) + { +@@ -206,7 +274,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getFillingPercentage() + { +@@ -214,7 +282,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getMetadatas($id) + { +@@ -222,7 +290,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function touch($id, $extraLifetime) + { +@@ -230,7 +298,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getCapabilities() + { +diff -Nuar a/vendor/magento/framework/Interception/Config/Config.php b/vendor/magento/framework/Interception/Config/Config.php +--- a/vendor/magento/framework/Interception/Config/Config.php ++++ b/vendor/magento/framework/Interception/Config/Config.php +@@ -187,8 +187,6 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + */ + private function initializeUncompiled($classDefinitions = []) + { +- $this->cacheManager->clean($this->_cacheId); +- + $this->generateIntercepted($classDefinitions); + + $this->cacheManager->save($this->_cacheId, $this->_intercepted); +diff -Nuar a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +--- a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +@@ -260,9 +260,12 @@ class DiCompileCommand extends Command + */ + private function getExcludedLibraryPaths(array $libraryPaths) + { +- $libraryPaths = array_map(function ($libraryPath) { +- return preg_quote($libraryPath, '#'); +- }, $libraryPaths); ++ $libraryPaths = array_map( ++ function ($libraryPath) { ++ return preg_quote($libraryPath, '#'); ++ }, ++ $libraryPaths ++ ); + + $excludedLibraryPaths = [ + '#^(?:' . join('|', $libraryPaths) . ')/([\\w]+/)?Test#', +@@ -395,7 +398,8 @@ class DiCompileCommand extends Command + $compiledPathsList['application'], + $compiledPathsList['library'], + $compiledPathsList['generated_helpers'], +- ] ++ ], ++ OperationFactory::APPLICATION_ACTION_LIST_GENERATOR => [], + ]; + + return $operations; +diff -Nuar a/2.3.5/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +new file mode 100644 +--- /dev/null ++++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +@@ -0,0 +1,58 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++namespace Magento\Setup\Module\Di\App\Task\Operation; ++ ++use Magento\Setup\Module\Di\App\Task\OperationInterface; ++use Magento\Framework\Module\Dir\Reader as ModuleReader; ++use Magento\Framework\App\ObjectManager\ConfigWriterInterface; ++ ++/** ++ * Pregenerates actions for Magento ++ */ ++class AppActionListGenerator implements OperationInterface ++{ ++ /** ++ * @var ModuleReader ++ */ ++ private $moduleReader; ++ ++ /** ++ * @var \Magento\Framework\App\ObjectManager\ConfigWriterInterface ++ */ ++ private $configWriter; ++ ++ /** ++ * @param ModuleReader $moduleReader ++ * @param ConfigWriterInterface $configWriter ++ */ ++ public function __construct( ++ ModuleReader $moduleReader, ++ ConfigWriterInterface $configWriter ++ ) { ++ $this->moduleReader = $moduleReader; ++ $this->configWriter = $configWriter; ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ public function doOperation() ++ { ++ $actionList = $this->moduleReader->getActionFiles(); ++ $this->configWriter->write( ++ 'app_action_list', ++ $actionList ++ ); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ public function getName() ++ { ++ return 'App action list generation'; ++ } ++} +diff -Nuar a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +--- a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php ++++ b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +@@ -5,6 +5,12 @@ + */ + namespace Magento\Setup\Module\Di\App\Task; + ++use Magento\Setup\Module\Di\App\Task\Operation\AppActionListGenerator; ++use Magento\Setup\Module\Di\App\Task\Operation\PluginListGenerator; ++ ++/** ++ * Factory that creates list of OperationInterface classes ++ */ + class OperationFactory + { + /** +@@ -47,6 +53,11 @@ class OperationFactory + */ + const APPLICATION_CODE_GENERATOR = 'application_code_generator'; + ++ /** ++ * Application action list generator ++ */ ++ const APPLICATION_ACTION_LIST_GENERATOR = 'application_action_list_generator'; ++ + /** + * Operations definitions + * +@@ -61,6 +72,7 @@ class OperationFactory + self::INTERCEPTION_CACHE => \Magento\Setup\Module\Di\App\Task\Operation\InterceptionCache::class, + self::REPOSITORY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\RepositoryGenerator::class, + self::PROXY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\ProxyGenerator::class, ++ self::APPLICATION_ACTION_LIST_GENERATOR => AppActionListGenerator::class, + ]; + + /** + +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -72,9 +72,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + const STORE_ID = 'store_id'; + + /** +- * @var string ++ * @var string|bool + */ +- protected $_cacheTag = self::CACHE_TAG; ++ protected $_cacheTag = false; + + /** + * @var string +@@ -868,7 +868,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + public function beforeSave() + { +- $this->cleanCache(); + $this->setTypeHasOptions(false); + $this->setTypeHasRequiredOptions(false); + $this->setHasOptions(false); +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class PageCache implements ConfigOptionsListInterface + { + const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_PAGE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; +@@ -273,6 +273,6 @@ class PageCache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class Cache implements ConfigOptionsListInterface + { + const INPUT_VALUE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; + const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; +@@ -262,6 +262,6 @@ class Cache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch new file mode 100644 index 00000000..cafc156a --- /dev/null +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch @@ -0,0 +1,1486 @@ +diff -Nuar a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module-eav/Model/Config.php +--- a/vendor/magento/module-eav/Model/Config.php ++++ b/vendor/magento/module-eav/Model/Config.php +@@ -7,12 +7,20 @@ namespace Magento\Eav\Model; + + use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; + use Magento\Eav\Model\Entity\Type; ++use Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface; ++use Magento\Framework\App\Config\ScopeConfigInterface; + use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Model\AbstractModel; + use Magento\Framework\Serialize\SerializerInterface; + + /** ++ * EAV config model. ++ * + * @api ++ * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @since 100.0.2 + */ + class Config +@@ -25,6 +33,11 @@ class Config + const ATTRIBUTES_CODES_CACHE_ID = 'EAV_ENTITY_ATTRIBUTES_CODES'; + /**#@-*/ + ++ /** ++ * Xml path to caching user defined eav attributes configuration. ++ */ ++ private const XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES = 'dev/caching/cache_user_defined_attributes'; ++ + /**#@-*/ + protected $_entityTypeData; + +@@ -116,6 +129,11 @@ class Config + */ + private $serializer; + ++ /** ++ * @var ScopeConfigInterface ++ */ ++ private $scopeConfig; ++ + /** + * Cache of attributes per set + * +@@ -123,13 +141,29 @@ class Config + */ + private $attributesPerSet = []; + ++ /** ++ * Is system attributes loaded flag. ++ * ++ * @var array ++ */ ++ private $isSystemAttributesLoaded = []; ++ ++ /** ++ * List of predefined system attributes for preload. ++ * ++ * @var array ++ */ ++ private $attributesForPreload; ++ + /** + * @param \Magento\Framework\App\CacheInterface $cache +- * @param \Magento\Eav\Model\Entity\TypeFactory $entityTypeFactory +- * @param \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory ++ * @param Entity\TypeFactory $entityTypeFactory ++ * @param ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory + * @param \Magento\Framework\App\Cache\StateInterface $cacheState + * @param \Magento\Framework\Validator\UniversalFactory $universalFactory +- * @param SerializerInterface $serializer ++ * @param SerializerInterface|null $serializer ++ * @param ScopeConfigInterface|null $scopeConfig ++ * @param array $attributesForPreload + * @codeCoverageIgnore + */ + public function __construct( +@@ -138,7 +172,9 @@ class Config + \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory, + \Magento\Framework\App\Cache\StateInterface $cacheState, + \Magento\Framework\Validator\UniversalFactory $universalFactory, +- SerializerInterface $serializer = null ++ SerializerInterface $serializer = null, ++ ScopeConfigInterface $scopeConfig = null, ++ $attributesForPreload = [] + ) { + $this->_cache = $cache; + $this->_entityTypeFactory = $entityTypeFactory; +@@ -146,6 +182,8 @@ class Config + $this->_cacheState = $cacheState; + $this->_universalFactory = $universalFactory; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); ++ $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); ++ $this->attributesForPreload = $attributesForPreload; + } + + /** +@@ -207,8 +245,8 @@ class Config + /** + * Associate object with identifier + * +- * @param mixed $obj +- * @param mixed $id ++ * @param mixed $obj ++ * @param mixed $id + * @return void + * @codeCoverageIgnore + */ +@@ -233,8 +271,8 @@ class Config + /** + * Specify reference for entity type id + * +- * @param int $id +- * @param string $code ++ * @param int $id ++ * @param string $code + * @return $this + * @codeCoverageIgnore + */ +@@ -258,9 +296,9 @@ class Config + /** + * Specify reference between entity attribute id and attribute code + * +- * @param int $id +- * @param string $code +- * @param string $entityTypeCode ++ * @param int $id ++ * @param string $code ++ * @param string $entityTypeCode + * @return $this + */ + protected function _addAttributeReference($id, $code, $entityTypeCode) +@@ -336,7 +374,9 @@ class Config + } + \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); + +- if ($this->isCacheEnabled() && ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID))) { ++ if ($this->isCacheEnabled() && ++ ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID)) ++ ) { + $this->_entityTypeData = $this->serializer->unserialize($cache); + foreach ($this->_entityTypeData as $typeCode => $data) { + $typeId = $data['entity_type_id']; +@@ -484,10 +524,10 @@ class Config + /** + * Get attribute by code for entity type + * +- * @param mixed $entityType +- * @param mixed $code +- * @return AbstractAttribute +- * @throws \Magento\Framework\Exception\LocalizedException ++ * @param mixed $entityType ++ * @param mixed $code ++ * @return AbstractAttribute ++ * @throws LocalizedException + */ + public function getAttribute($entityType, $code) + { +@@ -507,8 +547,152 @@ class Config + return $this->attributes[$entityTypeCode][$code]; + } + ++ if (array_key_exists($entityTypeCode, $this->attributesForPreload) ++ && array_key_exists($code, $this->attributesForPreload[$entityTypeCode]) ++ ) { ++ $this->initSystemAttributes($entityType, $this->attributesForPreload[$entityTypeCode]); ++ } ++ if (isset($this->attributes[$entityTypeCode][$code])) { ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $this->attributes[$entityTypeCode][$code]; ++ } ++ ++ if ($this->scopeConfig->getValue(self::XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES)) { ++ $attribute = $this->cacheUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } else { ++ $attribute = $this->initUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $attribute; ++ } ++ ++ /** ++ * Initialize predefined system attributes for preload. ++ * ++ * @param string $entityType ++ * @param array $systemAttributes ++ * @return $this|bool|void ++ * @throws LocalizedException ++ */ ++ private function initSystemAttributes($entityType, $systemAttributes) ++ { ++ $entityType = $this->getEntityType($entityType); ++ $entityTypeCode = $entityType->getEntityTypeCode(); ++ if (!empty($this->isSystemAttributesLoaded[$entityTypeCode])) { ++ return; ++ } ++ ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-' . $entityTypeCode . '-preload'; ++ if ($this->isCacheEnabled() && ($attributes = $this->_cache->load($cacheKey))) { ++ $attributes = $this->serializer->unserialize($attributes); ++ if ($attributes) { ++ foreach ($attributes as $attribute) { ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ } ++ return true; ++ } ++ } ++ ++ \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); ++ ++ /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection $attributes */ ++ $attributes = $this->_universalFactory->create( ++ $entityType->getEntityAttributeCollection() ++ )->setEntityTypeFilter( ++ $entityType ++ )->addFieldToFilter( ++ 'attribute_code', ++ ['in' => array_keys($systemAttributes)] ++ )->getData(); ++ ++ $attributeData = []; ++ foreach ($attributes as $attribute) { ++ if (empty($attribute['attribute_model'])) { ++ $attribute['attribute_model'] = $entityType->getAttributeModel(); ++ } ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ $attributeData[$attribute['attribute_code']] = $attributeObject->toArray(); ++ } ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attributeData), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ $this->isSystemAttributesLoaded[$entityTypeCode] = true; ++ ++ return $this; ++ } ++ ++ /** ++ * Initialize user defined attribute from cache or cache it. ++ * ++ * @param string $entityType ++ * @param mixed $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute ++ * @throws LocalizedException ++ */ ++ private function cacheUserDefinedAttribute($entityType, $entityTypeCode, $code): AbstractAttribute ++ { ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-attribute-' . $entityTypeCode . '-' . $code; ++ $attributeData = $this->isCacheEnabled() && ($attribute = $this->_cache->load($cacheKey)) ++ ? $this->serializer->unserialize($attribute) ++ : null; ++ if ($attributeData) { ++ if (isset($attributeData['attribute_id'])) { ++ $attribute = $this->_createAttribute($entityType, $attributeData); ++ } else { ++ $entityType = $this->getEntityType($entityType); ++ $attribute = $this->createAttribute($entityType->getAttributeModel()); ++ $attribute->setAttributeCode($code); ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ } ++ } else { ++ $attribute = $this->createAttributeByAttributeCode($entityType, $code); ++ $this->_addAttributeReference( ++ $attribute->getAttributeId(), ++ $attribute->getAttributeCode(), ++ $entityTypeCode ++ ); ++ $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attribute->getData()), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ } ++ ++ return $attribute; ++ } ++ ++ /** ++ * Initialize user defined attribute and save it to memory cache. ++ * ++ * @param mixed $entityType ++ * @param string $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute|null ++ * @throws LocalizedException ++ */ ++ private function initUserDefinedAttribute($entityType, $entityTypeCode, $code): ?AbstractAttribute ++ { + $attributes = $this->loadAttributes($entityTypeCode); +- $attribute = isset($attributes[$code]) ? $attributes[$code] : null; ++ $attribute = $attributes[$code] ?? null; + if (!$attribute) { + $attribute = $this->createAttributeByAttributeCode($entityType, $code); + $this->_addAttributeReference( +@@ -518,7 +702,7 @@ class Config + ); + $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); + } +- \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ + return $attribute; + } + +@@ -555,8 +739,8 @@ class Config + /** + * Get all entity type attributes + * +- * @param int|string|Type $entityType +- * @param \Magento\Framework\DataObject|null $object ++ * @param int|string|Type $entityType ++ * @param \Magento\Framework\DataObject|null $object + * @return AbstractAttribute[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) +@@ -639,7 +823,11 @@ class Config + $existsFullAttribute = $attribute->hasIsRequired(); + $fullAttributeData = array_key_exists('is_required', $attributeData); + +- if ($existsFullAttribute || !$existsFullAttribute && !$fullAttributeData) { ++ if ($existsFullAttribute || (!$existsFullAttribute && !$fullAttributeData)) { ++ $scopeIsRequired = $attributeData['scope_is_required'] ?? null; ++ if ($scopeIsRequired !== null) { ++ $attribute->setData('scope_is_required', $scopeIsRequired); ++ } + return $attribute; + } + } +@@ -708,6 +896,7 @@ class Config + * @param string $entityType + * @param string $attributeCode + * @return AbstractAttribute ++ * @throws LocalizedException + */ + private function createAttributeByAttributeCode($entityType, $attributeCode) + { +@@ -723,13 +912,28 @@ class Config + $attribute->setAttributeCode($attributeCode); + } + ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ ++ return $attribute; ++ } ++ ++ /** ++ * Set entity type id, backend type, is global to attribute. ++ * ++ * @param AbstractAttribute $attribute ++ * @param AbstractModel $entityType ++ * @return AbstractAttribute ++ */ ++ private function setAttributeData($attribute, $entityType): AbstractAttribute ++ { + $entity = $entityType->getEntity(); +- if ($entity instanceof \Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface ++ if ($entity instanceof ProviderInterface + && in_array($attribute->getAttributeCode(), $entity->getDefaultAttributes(), true) + ) { + $attribute->setBackendType(AbstractAttribute::TYPE_STATIC)->setIsGlobal(1); + } + $attribute->setEntityType($entityType)->setEntityTypeId($entityType->getId()); ++ + return $attribute; + } + +diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +--- a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php ++++ b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +@@ -6,6 +6,9 @@ + + namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; + ++/** ++ * Basic implementation for attribute sets ++ */ + class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + { + /** +@@ -24,8 +27,6 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + protected $eavConfig; + + /** +- * Constructor +- * + * @param \Magento\Framework\Model\ResourceModel\Db\Context $context + * @param GroupFactory $attrGroupFactory + * @param \Magento\Eav\Model\Config $eavConfig +@@ -54,7 +55,7 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb + } + + /** +- * Perform actions after object save ++ * Perform actions after object save. + * + * @param \Magento\Framework\Model\AbstractModel $object + * @return $this +diff -Nuar a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/etc/di.xml +--- a/vendor/magento/module-eav/etc/di.xml ++++ b/vendor/magento/module-eav/etc/di.xml +@@ -209,4 +209,14 @@ + </argument> + </arguments> + </type> ++ <virtualType name="configured_eav_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">eav</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Eav\Model\Config"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_eav_cache</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-theme/etc/di.xml +--- a/vendor/magento/module-theme/etc/di.xml ++++ b/vendor/magento/module-theme/etc/di.xml +@@ -285,4 +285,24 @@ + <argument name="identifierName" xsi:type="string">theme_id</argument> + </arguments> + </type> ++ <virtualType name="configured_design_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">layout</argument> ++ </arguments> ++ </virtualType> ++ <virtualType name="design_context" type="Magento\Framework\Model\Context"> ++ <arguments> ++ <argument name="cacheManager" xsi:type="object">configured_design_cache</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Theme\Model\Design"> ++ <arguments> ++ <argument name="context" xsi:type="object">design_context</argument> ++ </arguments> ++ </type> ++ <type name="Magento\Theme\Model\Theme\ThemeProvider"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_design_cache</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -1760,12 +1760,20 @@ + <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> +- <argument name="lockTimeout" xsi:type="number">10000</argument> +- <argument name="delayTimeout" xsi:type="number">20</argument> + </arguments> + </type> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface" type="Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompare"/> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface" type="Magento\Framework\MessageQueue\PoisonPill\PoisonPillPut"/> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface" type="Magento\Framework\MessageQueue\PoisonPill\PoisonPillRead"/> + <preference for="Magento\Framework\MessageQueue\CallbackInvokerInterface" type="Magento\Framework\MessageQueue\CallbackInvoker"/> ++ <virtualType name="configured_block_cache" type="Magento\Framework\App\Cache"> ++ <arguments> ++ <argument name="cacheIdentifier" xsi:type="string">block_html</argument> ++ </arguments> ++ </virtualType> ++ <type name="Magento\Framework\View\Element\Context"> ++ <arguments> ++ <argument name="cache" xsi:type="object">configured_block_cache</argument> ++ </arguments> ++ </type> + </config> +diff -Nuar a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/App/Cache.php +--- a/vendor/magento/framework/App/Cache.php ++++ b/vendor/magento/framework/App/Cache.php +@@ -4,12 +4,11 @@ + * See COPYING.txt for license details. + */ + +-/** +- * System cache model +- * support id and tags prefix support, +- */ + namespace Magento\Framework\App; + ++/** ++ * System cache model support id and tags prefix support. ++ */ + class Cache implements CacheInterface + { + /** +@@ -30,12 +29,13 @@ class Cache implements CacheInterface + protected $_frontend; + + /** +- * @param \Magento\Framework\App\Cache\Frontend\Pool $frontendPool ++ * @param Cache\Frontend\Pool $frontendPool ++ * @param string|null $cacheIdentifier + */ +- public function __construct(\Magento\Framework\App\Cache\Frontend\Pool $frontendPool) ++ public function __construct(\Magento\Framework\App\Cache\Frontend\Pool $frontendPool, $cacheIdentifier = null) + { + $this->_frontendPool = $frontendPool; +- $this->_frontend = $frontendPool->get($this->_frontendIdentifier); ++ $this->_frontend = $frontendPool->get($cacheIdentifier ?? $this->_frontendIdentifier); + } + + /** +diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Pool.php b/vendor/magento/framework/App/Cache/Frontend/Pool.php +--- a/vendor/magento/framework/App/Cache/Frontend/Pool.php ++++ b/vendor/magento/framework/App/Cache/Frontend/Pool.php +@@ -152,6 +152,15 @@ class Pool implements \Iterator + if (isset($this->_instances[$identifier])) { + return $this->_instances[$identifier]; + } +- throw new \InvalidArgumentException("Cache frontend '{$identifier}' is not recognized."); ++ ++ if (!isset($this->_instances[self::DEFAULT_FRONTEND_ID])) { ++ throw new \InvalidArgumentException( ++ "Cache frontend '{$identifier}' is not recognized. As well as " . ++ self::DEFAULT_FRONTEND_ID . ++ "cache is not configured" ++ ); ++ } ++ ++ return $this->_instances[self::DEFAULT_FRONTEND_ID]; + } + } +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +--- a/vendor/magento/framework/App/Router/ActionList.php ++++ b/vendor/magento/framework/App/Router/ActionList.php +@@ -5,6 +5,8 @@ + */ + namespace Magento\Framework\App\Router; + ++use Magento\Framework\App\Filesystem\DirectoryList; ++use Magento\Framework\App\State; + use Magento\Framework\Serialize\SerializerInterface; + use Magento\Framework\Serialize\Serializer\Serialize; + use Magento\Framework\Module\Dir\Reader as ModuleReader; +@@ -70,12 +72,26 @@ class ActionList + $this->reservedWords = array_merge($reservedWords, $this->reservedWords); + $this->actionInterface = $actionInterface; + $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Serialize::class); +- $data = $cache->load($cacheKey); +- if (!$data) { +- $this->actions = $moduleReader->getActionFiles(); +- $cache->save($this->serializer->serialize($this->actions), $cacheKey); ++ $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); ++ $state = $objectManager->get(State::class); ++ ++ if ($state->getMode() === State::MODE_PRODUCTION) { ++ $directoryList = $objectManager->get(DirectoryList::class); ++ $file = $directoryList->getPath(DirectoryList::GENERATED_METADATA) . '/' . $cacheKey . '.' . 'php'; ++ ++ if (file_exists($file)) { ++ $this->actions = (include $file) ?? $moduleReader->getActionFiles(); ++ } else { ++ $this->actions = $moduleReader->getActionFiles(); ++ } + } else { +- $this->actions = $this->serializer->unserialize($data); ++ $data = $cache->load($cacheKey); ++ if (!$data) { ++ $this->actions = $moduleReader->getActionFiles(); ++ $cache->save($this->serializer->serialize($this->actions), $cacheKey); ++ } else { ++ $this->actions = $this->serializer->unserialize($data); ++ } + } + } + +diff -Nuar a/2.3.5/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/framework/Cache/Backend/Redis.php +@@ -0,0 +1,83 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++ ++namespace Magento\Framework\Cache\Backend; ++ ++/** ++ * Redis wrapper to extend current implementation behaviour. ++ */ ++class Redis extends \Cm_Cache_Backend_Redis ++{ ++ /** ++ * Local state of preloaded keys. ++ * ++ * @var array ++ */ ++ private $preloadedData = []; ++ ++ /** ++ * Array of keys to be preloaded. ++ * ++ * @var array ++ */ ++ private $preloadKeys = []; ++ ++ /** ++ * @param array $options ++ */ ++ public function __construct($options = []) ++ { ++ $this->preloadKeys = $options['preload_keys'] ?? []; ++ parent::__construct($options); ++ } ++ ++ /** ++ * Load value with given id from cache ++ * ++ * @param string $id Cache id ++ * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested ++ * @return bool|string ++ */ ++ public function load($id, $doNotTestCacheValidity = false) ++ { ++ if (!empty($this->preloadKeys) && empty($this->preloadedData)) { ++ $redis = $this->_slave ?? $this->_redis; ++ $redis = $redis->pipeline(); ++ ++ foreach ($this->preloadKeys as $key) { ++ $redis->hGet(self::PREFIX_KEY . $key, self::FIELD_DATA); ++ } ++ ++ $this->preloadedData = array_filter(array_combine($this->preloadKeys, $redis->exec())); ++ } ++ ++ if (isset($this->preloadedData[$id])) { ++ return $this->_decodeData($this->preloadedData[$id]); ++ } ++ ++ return parent::load($id, $doNotTestCacheValidity); ++ } ++ ++ /** ++ * Cover errors on save operations, which may occurs when Redis cannot evict keys, which is expected in some cases. ++ * ++ * @param string $data ++ * @param string $id ++ * @param array $tags ++ * @param bool $specificLifetime ++ * @return bool ++ */ ++ public function save($data, $id, $tags = [], $specificLifetime = false) ++ { ++ try { ++ parent::save($data, $id, $tags, $specificLifetime); ++ } catch (\Throwable $exception) { ++ return false; ++ } ++ ++ return true; ++ } ++} +diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +--- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php ++++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +@@ -9,8 +9,10 @@ namespace Magento\Framework\Cache\Backend; + /** + * Remote synchronized cache + * +- * This class created for correct work local caches with multiple web nodes, +- * that will be check cache status from remote cache ++ * This class created for correct work witch local caches and multiple web nodes, ++ * in order to be sure that we always have up to date local version of cache. ++ * This class will be check cache version from remote cache and in case it newer ++ * than local one, it will update local one from remote cache a.k.a two level cache. + */ + class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface + { +@@ -36,11 +38,15 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + protected $cacheInvalidationTime; + + /** +- * {@inheritdoc} ++ * Suffix for hash to compare data version in cache storage. ++ */ ++ private const HASH_SUFFIX = ':hash'; ++ ++ /** ++ * @inheritdoc + */ + protected $_options = [ + 'remote_backend' => '', +- 'remote_backend_invalidation_time_id' => 'default_remote_backend_invalidation_time', + 'remote_backend_custom_naming' => true, + 'remote_backend_autoload' => true, + 'remote_backend_options' => [], +@@ -52,6 +58,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + + /** + * @param array $options ++ * @throws \Zend_Cache_Exception + */ + public function __construct(array $options = []) + { +@@ -97,76 +104,137 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * Update remote cache status info ++ * @inheritdoc ++ */ ++ public function setDirectives($directives) ++ { ++ return $this->local->setDirectives($directives); ++ } ++ ++ /** ++ * Return hash sign of the data. + * +- * @return void ++ * @param string $data ++ * @return string + */ +- private function updateRemoteCacheStatusInfo() ++ private function getDataVersion(string $data) + { +- $this->remote->save(time(), $this->_options['remote_backend_invalidation_time_id'], [], null); +- $this->cacheInvalidationTime = null; ++ return \hash('sha256', $data); + } + + /** +- * {@inheritdoc} ++ * Load data version by id from remote. ++ * ++ * @param string $id ++ * @return false|string + */ +- public function setDirectives($directives) ++ private function loadRemoteDataVersion(string $id) + { +- return $this->local->setDirectives($directives); ++ return $this->remote->load( ++ $id . self::HASH_SUFFIX ++ ); + } + + /** +- * {@inheritdoc} ++ * Save new data version to remote. ++ * ++ * @param string $data ++ * @param string $id ++ * @param array $tags ++ * @param mixed $specificLifetime ++ * @return bool ++ */ ++ private function saveRemoteDataVersion(string $data, string $id, array $tags, $specificLifetime = false) ++ { ++ return $this->remote->save($this->getDataVersion($data), $id . self::HASH_SUFFIX, $tags, $specificLifetime); ++ } ++ ++ /** ++ * Remove remote data version. ++ * ++ * @param string $id ++ * @return bool ++ */ ++ private function removeRemoteDataVersion($id) ++ { ++ return $this->remote->remove($id . self::HASH_SUFFIX); ++ } ++ ++ /** ++ * @inheritdoc + */ + public function load($id, $doNotTestCacheValidity = false) + { +- $dataModificationTime = $this->local->test($id); +- if ($this->cacheInvalidationTime === null) { +- $this->cacheInvalidationTime = $this->remote->load($this->_options['remote_backend_invalidation_time_id']); +- } +- if ($dataModificationTime >= $this->cacheInvalidationTime) { +- return $this->local->load($id, $doNotTestCacheValidity); ++ $localData = $this->local->load($id); ++ $remoteData = false; ++ ++ if (false === $localData) { ++ $remoteData = $this->remote->load($id); ++ ++ if (false === $remoteData) { ++ return false; ++ } + } else { +- return false; ++ if ($this->getDataVersion($localData) !== $this->loadRemoteDataVersion($id)) { ++ $localData = false; ++ $remoteData = $this->remote->load($id); ++ } + } ++ ++ if ($remoteData !== false) { ++ $this->local->save($remoteData, $id); ++ $localData = $remoteData; ++ } ++ ++ return $localData; + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function test($id) + { +- return $this->local->test($id); ++ return $this->local->test($id) ?? $this->remote->test($id); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function save($data, $id, $tags = [], $specificLifetime = false) + { +- return $this->local->save($data, $id, $tags, $specificLifetime); ++ $dataToSave = $data; ++ $remHash = $this->loadRemoteDataVersion($id); ++ ++ if ($remHash !== false) { ++ $dataToSave = $this->remote->load($id); ++ } else { ++ $this->remote->save($data, $id, $tags, $specificLifetime); ++ $this->saveRemoteDataVersion($data, $id, $tags, $specificLifetime); ++ } ++ ++ return $this->local->save($dataToSave, $id, [], $specificLifetime); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function remove($id) + { +- $this->updateRemoteCacheStatusInfo(); +- return $this->local->remove($id); ++ return $this->removeRemoteDataVersion($id) && ++ $this->remote->remove($id) && ++ $this->local->remove($id); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = []) + { +- $this->updateRemoteCacheStatusInfo(); +- return $this->local->clean($mode, $tags); ++ return $this->remote->clean($mode, $tags); + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIds() + { +@@ -174,7 +242,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getTags() + { +@@ -182,7 +250,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsMatchingTags($tags = []) + { +@@ -190,7 +258,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsNotMatchingTags($tags = []) + { +@@ -198,7 +266,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getIdsMatchingAnyTags($tags = []) + { +@@ -206,7 +274,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getFillingPercentage() + { +@@ -214,7 +282,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getMetadatas($id) + { +@@ -222,7 +290,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function touch($id, $extraLifetime) + { +@@ -230,7 +298,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + } + + /** +- * {@inheritdoc} ++ * @inheritdoc + */ + public function getCapabilities() + { +diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +--- a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php ++++ b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +@@ -36,19 +36,43 @@ class LockGuardedCacheLoader + */ + private $delayTimeout; + ++ /** ++ * Timeout for information to be collected and saved. ++ * If timeout passed that means that data cannot be saved right now. ++ * And we will just return collected data. ++ * ++ * Value of the variable in milliseconds. ++ * ++ * @var int ++ */ ++ private $loadTimeout; ++ ++ /** ++ * Minimal delay timeout in ms. ++ * ++ * @var int ++ */ ++ private $minimalDelayTimeout; ++ + /** + * @param LockManagerInterface $locker + * @param int $lockTimeout + * @param int $delayTimeout ++ * @param int $loadTimeout ++ * @param int $minimalDelayTimeout + */ + public function __construct( + LockManagerInterface $locker, + int $lockTimeout = 10000, +- int $delayTimeout = 20 ++ int $delayTimeout = 20, ++ int $loadTimeout = 10000, ++ int $minimalDelayTimeout = 5 + ) { + $this->locker = $locker; + $this->lockTimeout = $lockTimeout; + $this->delayTimeout = $delayTimeout; ++ $this->loadTimeout = $loadTimeout; ++ $this->minimalDelayTimeout = $minimalDelayTimeout; + } + + /** +@@ -67,25 +91,25 @@ class LockGuardedCacheLoader + callable $dataSaver + ) { + $cachedData = $dataLoader(); //optimistic read +- +- while ($cachedData === false && $this->locker->isLocked($lockName)) { +- usleep($this->delayTimeout * 1000); +- $cachedData = $dataLoader(); +- } ++ $deadline = microtime(true) + $this->loadTimeout / 100; + + while ($cachedData === false) { +- try { +- if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { ++ if ($deadline <= microtime(true)) { ++ return $dataCollector(); ++ } ++ ++ if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { ++ try { + $data = $dataCollector(); + $dataSaver($data); + $cachedData = $data; ++ } finally { ++ $this->locker->unlock($lockName); + } +- } finally { +- $this->locker->unlock($lockName); + } + + if ($cachedData === false) { +- usleep($this->delayTimeout * 1000); ++ usleep($this->getLookupTimeout() * 1000); + $cachedData = $dataLoader(); + } + } +@@ -103,14 +127,21 @@ class LockGuardedCacheLoader + public function lockedCleanData(string $lockName, callable $dataCleaner) + { + while ($this->locker->isLocked($lockName)) { +- usleep($this->delayTimeout * 1000); +- } +- try { +- if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { +- $dataCleaner(); +- } +- } finally { +- $this->locker->unlock($lockName); ++ usleep($this->getLookupTimeout() * 1000); + } ++ ++ $dataCleaner(); ++ } ++ ++ /** ++ * Delay will be applied as rand($minimalDelayTimeout, $delayTimeout). ++ * This helps to desynchronize multiple clients trying ++ * to acquire the lock for the same resource at the same time ++ * ++ * @return int ++ */ ++ private function getLookupTimeout() ++ { ++ return rand($this->minimalDelayTimeout, $this->delayTimeout); + } + } +diff -Nuar a/vendor/magento/framework/Interception/Config/Config.php b/vendor/magento/framework/Interception/Config/Config.php +--- a/vendor/magento/framework/Interception/Config/Config.php ++++ b/vendor/magento/framework/Interception/Config/Config.php +@@ -187,8 +187,6 @@ class Config implements \Magento\Framework\Interception\ConfigInterface + */ + private function initializeUncompiled($classDefinitions = []) + { +- $this->cacheManager->clean($this->_cacheId); +- + $this->generateIntercepted($classDefinitions); + + $this->cacheManager->save($this->_cacheId, $this->_intercepted); +diff -Nuar a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php +--- a/vendor/magento/framework/Lock/Backend/Cache.php ++++ b/vendor/magento/framework/Lock/Backend/Cache.php +@@ -24,12 +24,20 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + private $cache; + ++ /** ++ * Sign for locks, helps to avoid removing a lock that was created by another client ++ * ++ * @string ++ */ ++ private $lockSign; ++ + /** + * @param FrontendInterface $cache + */ + public function __construct(FrontendInterface $cache) + { + $this->cache = $cache; ++ $this->lockSign = $this->generateLockSign(); + } + + /** +@@ -37,7 +45,26 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + public function lock(string $name, int $timeout = -1): bool + { +- return $this->cache->save('1', $this->getIdentifier($name), [], $timeout); ++ if (empty($this->lockSign)) { ++ $this->lockSign = $this->generateLockSign(); ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false !== $data) { ++ return false; ++ } ++ ++ $timeout = $timeout <= 0 ? null : $timeout; ++ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if ($data === $this->lockSign) { ++ return true; ++ } ++ ++ return false; + } + + /** +@@ -45,7 +72,22 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + public function unlock(string $name): bool + { +- return $this->cache->remove($this->getIdentifier($name)); ++ if (empty($this->lockSign)) { ++ return false; ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false === $data) { ++ return false; ++ } ++ ++ $removeResult = false; ++ if ($data === $this->lockSign) { ++ $removeResult = (bool)$this->cache->remove($this->getIdentifier($name)); ++ } ++ ++ return $removeResult; + } + + /** +@@ -66,4 +108,27 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + { + return self::LOCK_PREFIX . $cacheIdentifier; + } ++ ++ /** ++ * Function that generates lock sign that helps to avoid removing a lock that was created by another client. ++ * ++ * @return string ++ */ ++ private function generateLockSign() ++ { ++ $sign = implode( ++ '-', ++ [ ++ \getmypid(), \crc32(\gethostname()) ++ ] ++ ); ++ ++ try { ++ $sign .= '-' . \bin2hex(\random_bytes(4)); ++ } catch (\Exception $e) { ++ $sign .= '-' . \uniqid('-uniqid-'); ++ } ++ ++ return $sign; ++ } + } +diff -Nuar a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +--- a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php ++++ b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +@@ -260,9 +260,12 @@ class DiCompileCommand extends Command + */ + private function getExcludedLibraryPaths(array $libraryPaths) + { +- $libraryPaths = array_map(function ($libraryPath) { +- return preg_quote($libraryPath, '#'); +- }, $libraryPaths); ++ $libraryPaths = array_map( ++ function ($libraryPath) { ++ return preg_quote($libraryPath, '#'); ++ }, ++ $libraryPaths ++ ); + + $excludedLibraryPaths = [ + '#^(?:' . join('|', $libraryPaths) . ')/([\\w]+/)?Test#', +@@ -395,7 +398,8 @@ class DiCompileCommand extends Command + $compiledPathsList['application'], + $compiledPathsList['library'], + $compiledPathsList['generated_helpers'], +- ] ++ ], ++ OperationFactory::APPLICATION_ACTION_LIST_GENERATOR => [], + ]; + + return $operations; +diff -Nuar a/2.3.5/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +new file mode 100644 +--- /dev/null ++++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +@@ -0,0 +1,58 @@ ++<?php ++/** ++ * Copyright © Magento, Inc. All rights reserved. ++ * See COPYING.txt for license details. ++ */ ++namespace Magento\Setup\Module\Di\App\Task\Operation; ++ ++use Magento\Setup\Module\Di\App\Task\OperationInterface; ++use Magento\Framework\Module\Dir\Reader as ModuleReader; ++use Magento\Framework\App\ObjectManager\ConfigWriterInterface; ++ ++/** ++ * Pregenerates actions for Magento ++ */ ++class AppActionListGenerator implements OperationInterface ++{ ++ /** ++ * @var ModuleReader ++ */ ++ private $moduleReader; ++ ++ /** ++ * @var \Magento\Framework\App\ObjectManager\ConfigWriterInterface ++ */ ++ private $configWriter; ++ ++ /** ++ * @param ModuleReader $moduleReader ++ * @param ConfigWriterInterface $configWriter ++ */ ++ public function __construct( ++ ModuleReader $moduleReader, ++ ConfigWriterInterface $configWriter ++ ) { ++ $this->moduleReader = $moduleReader; ++ $this->configWriter = $configWriter; ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ public function doOperation() ++ { ++ $actionList = $this->moduleReader->getActionFiles(); ++ $this->configWriter->write( ++ 'app_action_list', ++ $actionList ++ ); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ public function getName() ++ { ++ return 'App action list generation'; ++ } ++} +diff -Nuar a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +--- a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php ++++ b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +@@ -5,6 +5,12 @@ + */ + namespace Magento\Setup\Module\Di\App\Task; + ++use Magento\Setup\Module\Di\App\Task\Operation\AppActionListGenerator; ++use Magento\Setup\Module\Di\App\Task\Operation\PluginListGenerator; ++ ++/** ++ * Factory that creates list of OperationInterface classes ++ */ + class OperationFactory + { + /** +@@ -47,6 +53,11 @@ class OperationFactory + */ + const APPLICATION_CODE_GENERATOR = 'application_code_generator'; + ++ /** ++ * Application action list generator ++ */ ++ const APPLICATION_ACTION_LIST_GENERATOR = 'application_action_list_generator'; ++ + /** + * Operations definitions + * +@@ -61,6 +72,7 @@ class OperationFactory + self::INTERCEPTION_CACHE => \Magento\Setup\Module\Di\App\Task\Operation\InterceptionCache::class, + self::REPOSITORY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\RepositoryGenerator::class, + self::PROXY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\ProxyGenerator::class, ++ self::APPLICATION_ACTION_LIST_GENERATOR => AppActionListGenerator::class, + ]; + + /** +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class Cache implements ConfigOptionsListInterface + { + const INPUT_VALUE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; + const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; +@@ -282,6 +282,6 @@ class Cache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class PageCache implements ConfigOptionsListInterface + { + const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_PAGE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; +@@ -284,6 +284,6 @@ class PageCache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -74,9 +74,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + const STORE_ID = 'store_id'; + + /** +- * @var string ++ * @var string|bool + */ +- protected $_cacheTag = self::CACHE_TAG; ++ protected $_cacheTag = false; + + /** + * @var string +@@ -910,7 +910,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + public function beforeSave() + { +- $this->cleanCache(); + $this->setTypeHasOptions(false); + $this->setTypeHasRequiredOptions(false); + $this->setHasOptions(false); +diff -Nuar a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php +--- a/vendor/magento/module-config/App/Config/Type/System.php ++++ b/vendor/magento/module-config/App/Config/Type/System.php +@@ -20,6 +20,8 @@ use Magento\Framework\Serialize\SerializerInterface; + use Magento\Store\Model\Config\Processor\Fallback; + use Magento\Framework\Encryption\Encryptor; + use Magento\Store\Model\ScopeInterface as StoreScope; ++use Magento\Framework\App\Cache\StateInterface; ++use Magento\Framework\App\Cache\Type\Config; + + /** + * System configuration type +@@ -98,6 +100,12 @@ class System implements ConfigTypeInterface + private $lockQuery; + + /** ++ * @var StateInterface ++ */ ++ private $cacheState; ++ ++ /** ++ * System constructor. + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback +@@ -110,6 +118,7 @@ class System implements ConfigTypeInterface + * @param Encryptor|null $encryptor + * @param LockManagerInterface|null $locker + * @param LockGuardedCacheLoader|null $lockQuery ++ * @param StateInterface|null $cacheState + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -125,7 +134,8 @@ class System implements ConfigTypeInterface + Reader $reader = null, + Encryptor $encryptor = null, + LockManagerInterface $locker = null, +- LockGuardedCacheLoader $lockQuery = null ++ LockGuardedCacheLoader $lockQuery = null, ++ StateInterface $cacheState = null + ) { + $this->postProcessor = $postProcessor; + $this->cache = $cache; +@@ -136,6 +146,8 @@ class System implements ConfigTypeInterface + ?: ObjectManager::getInstance()->get(Encryptor::class); + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); ++ $this->cacheState = $cacheState ++ ?: ObjectManager::getInstance()->get(StateInterface::class); + } + + /** +@@ -220,6 +232,10 @@ class System implements ConfigTypeInterface + */ + private function loadAllData() + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () { + $cachedData = $this->cache->load($this->configType); + $data = false; +@@ -245,6 +261,10 @@ class System implements ConfigTypeInterface + */ + private function loadDefaultScopeData($scopeType) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + $scopeData = false; +@@ -271,6 +291,10 @@ class System implements ConfigTypeInterface + */ + private function loadScopeData($scopeType, $scopeId) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + $scopeData = false; +@@ -393,6 +417,10 @@ class System implements ConfigTypeInterface + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + }; + ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $cleanAction(); ++ } ++ + $this->lockQuery->lockedCleanData( + self::$lockName, + $cleanAction +diff -Nuar a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml +--- a/vendor/magento/module-config/etc/di.xml ++++ b/vendor/magento/module-config/etc/di.xml +@@ -97,8 +97,6 @@ + <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> +- <argument name="lockTimeout" xsi:type="number">42000</argument> +- <argument name="delayTimeout" xsi:type="number">100</argument> + </arguments> + </virtualType> + diff --git a/patches/MDVA-26795__fix_performance_issue_in_cache_locking_mechanism__2.3.4.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch similarity index 50% rename from patches/MDVA-26795__fix_performance_issue_in_cache_locking_mechanism__2.3.4.patch rename to patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch index 0c1b7e07..51c25589 100644 --- a/patches/MDVA-26795__fix_performance_issue_in_cache_locking_mechanism__2.3.4.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch @@ -1,8 +1,69 @@ diff -Nuar a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module-eav/Model/Config.php --- a/vendor/magento/module-eav/Model/Config.php +++ b/vendor/magento/module-eav/Model/Config.php -@@ -157,12 +157,12 @@ class Config - +@@ -7,12 +7,20 @@ namespace Magento\Eav\Model; + + use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; + use Magento\Eav\Model\Entity\Type; ++use Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface; ++use Magento\Framework\App\Config\ScopeConfigInterface; + use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Model\AbstractModel; + use Magento\Framework\Serialize\SerializerInterface; + + /** ++ * EAV config model. ++ * + * @api ++ * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @since 100.0.2 + */ + class Config +@@ -25,6 +33,11 @@ class Config + const ATTRIBUTES_CODES_CACHE_ID = 'EAV_ENTITY_ATTRIBUTES_CODES'; + /**#@-*/ + ++ /** ++ * Xml path to caching user defined eav attributes configuration. ++ */ ++ private const XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES = 'dev/caching/cache_user_defined_attributes'; ++ + /**#@-*/ + protected $_entityTypeData; + +@@ -116,6 +129,11 @@ class Config + */ + private $serializer; + ++ /** ++ * @var ScopeConfigInterface ++ */ ++ private $scopeConfig; ++ + /** + * Cache of attributes per set + * +@@ -123,13 +141,29 @@ class Config + */ + private $attributesPerSet = []; + ++ /** ++ * Is system attributes loaded flag. ++ * ++ * @var array ++ */ ++ private $isSystemAttributesLoaded = []; ++ ++ /** ++ * List of predefined system attributes for preload. ++ * ++ * @var array ++ */ ++ private $attributesForPreload; ++ /** * @param \Magento\Framework\App\CacheInterface $cache - * @param \Magento\Eav\Model\Entity\TypeFactory $entityTypeFactory @@ -12,16 +73,71 @@ diff -Nuar a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module- * @param \Magento\Framework\App\Cache\StateInterface $cacheState * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param SerializerInterface $serializer -- * @param ScopeConfigInterface $scopeConfig + * @param SerializerInterface|null $serializer + * @param ScopeConfigInterface|null $scopeConfig - * @param array $attributesForPreload ++ * @param array $attributesForPreload * @codeCoverageIgnore */ -@@ -374,7 +374,9 @@ class Config + public function __construct( +@@ -138,7 +172,9 @@ class Config + \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory, + \Magento\Framework\App\Cache\StateInterface $cacheState, + \Magento\Framework\Validator\UniversalFactory $universalFactory, +- SerializerInterface $serializer = null ++ SerializerInterface $serializer = null, ++ ScopeConfigInterface $scopeConfig = null, ++ $attributesForPreload = [] + ) { + $this->_cache = $cache; + $this->_entityTypeFactory = $entityTypeFactory; +@@ -146,6 +182,8 @@ class Config + $this->_cacheState = $cacheState; + $this->_universalFactory = $universalFactory; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); ++ $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); ++ $this->attributesForPreload = $attributesForPreload; + } + + /** +@@ -207,8 +245,8 @@ class Config + /** + * Associate object with identifier + * +- * @param mixed $obj +- * @param mixed $id ++ * @param mixed $obj ++ * @param mixed $id + * @return void + * @codeCoverageIgnore + */ +@@ -233,8 +271,8 @@ class Config + /** + * Specify reference for entity type id + * +- * @param int $id +- * @param string $code ++ * @param int $id ++ * @param string $code + * @return $this + * @codeCoverageIgnore + */ +@@ -258,9 +296,9 @@ class Config + /** + * Specify reference between entity attribute id and attribute code + * +- * @param int $id +- * @param string $code +- * @param string $entityTypeCode ++ * @param int $id ++ * @param string $code ++ * @param string $entityTypeCode + * @return $this + */ + protected function _addAttributeReference($id, $code, $entityTypeCode) +@@ -336,7 +374,9 @@ class Config } \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); - + - if ($this->isCacheEnabled() && ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID))) { + if ($this->isCacheEnabled() && + ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID)) @@ -29,13 +145,253 @@ diff -Nuar a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module- $this->_entityTypeData = $this->serializer->unserialize($cache); foreach ($this->_entityTypeData as $typeCode => $data) { $typeId = $data['entity_type_id']; +@@ -484,10 +524,10 @@ class Config + /** + * Get attribute by code for entity type + * +- * @param mixed $entityType +- * @param mixed $code +- * @return AbstractAttribute +- * @throws \Magento\Framework\Exception\LocalizedException ++ * @param mixed $entityType ++ * @param mixed $code ++ * @return AbstractAttribute ++ * @throws LocalizedException + */ + public function getAttribute($entityType, $code) + { +@@ -507,8 +547,152 @@ class Config + return $this->attributes[$entityTypeCode][$code]; + } + ++ if (array_key_exists($entityTypeCode, $this->attributesForPreload) ++ && array_key_exists($code, $this->attributesForPreload[$entityTypeCode]) ++ ) { ++ $this->initSystemAttributes($entityType, $this->attributesForPreload[$entityTypeCode]); ++ } ++ if (isset($this->attributes[$entityTypeCode][$code])) { ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $this->attributes[$entityTypeCode][$code]; ++ } ++ ++ if ($this->scopeConfig->getValue(self::XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES)) { ++ $attribute = $this->cacheUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } else { ++ $attribute = $this->initUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $attribute; ++ } ++ ++ /** ++ * Initialize predefined system attributes for preload. ++ * ++ * @param string $entityType ++ * @param array $systemAttributes ++ * @return $this|bool|void ++ * @throws LocalizedException ++ */ ++ private function initSystemAttributes($entityType, $systemAttributes) ++ { ++ $entityType = $this->getEntityType($entityType); ++ $entityTypeCode = $entityType->getEntityTypeCode(); ++ if (!empty($this->isSystemAttributesLoaded[$entityTypeCode])) { ++ return; ++ } ++ ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-' . $entityTypeCode . '-preload'; ++ if ($this->isCacheEnabled() && ($attributes = $this->_cache->load($cacheKey))) { ++ $attributes = $this->serializer->unserialize($attributes); ++ if ($attributes) { ++ foreach ($attributes as $attribute) { ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ } ++ return true; ++ } ++ } ++ ++ \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); ++ ++ /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection $attributes */ ++ $attributes = $this->_universalFactory->create( ++ $entityType->getEntityAttributeCollection() ++ )->setEntityTypeFilter( ++ $entityType ++ )->addFieldToFilter( ++ 'attribute_code', ++ ['in' => array_keys($systemAttributes)] ++ )->getData(); ++ ++ $attributeData = []; ++ foreach ($attributes as $attribute) { ++ if (empty($attribute['attribute_model'])) { ++ $attribute['attribute_model'] = $entityType->getAttributeModel(); ++ } ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ $attributeData[$attribute['attribute_code']] = $attributeObject->toArray(); ++ } ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attributeData), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ $this->isSystemAttributesLoaded[$entityTypeCode] = true; ++ ++ return $this; ++ } ++ ++ /** ++ * Initialize user defined attribute from cache or cache it. ++ * ++ * @param string $entityType ++ * @param mixed $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute ++ * @throws LocalizedException ++ */ ++ private function cacheUserDefinedAttribute($entityType, $entityTypeCode, $code): AbstractAttribute ++ { ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-attribute-' . $entityTypeCode . '-' . $code; ++ $attributeData = $this->isCacheEnabled() && ($attribute = $this->_cache->load($cacheKey)) ++ ? $this->serializer->unserialize($attribute) ++ : null; ++ if ($attributeData) { ++ if (isset($attributeData['attribute_id'])) { ++ $attribute = $this->_createAttribute($entityType, $attributeData); ++ } else { ++ $entityType = $this->getEntityType($entityType); ++ $attribute = $this->createAttribute($entityType->getAttributeModel()); ++ $attribute->setAttributeCode($code); ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ } ++ } else { ++ $attribute = $this->createAttributeByAttributeCode($entityType, $code); ++ $this->_addAttributeReference( ++ $attribute->getAttributeId(), ++ $attribute->getAttributeCode(), ++ $entityTypeCode ++ ); ++ $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attribute->getData()), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ } ++ ++ return $attribute; ++ } ++ ++ /** ++ * Initialize user defined attribute and save it to memory cache. ++ * ++ * @param mixed $entityType ++ * @param string $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute|null ++ * @throws LocalizedException ++ */ ++ private function initUserDefinedAttribute($entityType, $entityTypeCode, $code): ?AbstractAttribute ++ { + $attributes = $this->loadAttributes($entityTypeCode); +- $attribute = isset($attributes[$code]) ? $attributes[$code] : null; ++ $attribute = $attributes[$code] ?? null; + if (!$attribute) { + $attribute = $this->createAttributeByAttributeCode($entityType, $code); + $this->_addAttributeReference( +@@ -518,7 +702,7 @@ class Config + ); + $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); + } +- \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ + return $attribute; + } + +@@ -555,8 +739,8 @@ class Config + /** + * Get all entity type attributes + * +- * @param int|string|Type $entityType +- * @param \Magento\Framework\DataObject|null $object ++ * @param int|string|Type $entityType ++ * @param \Magento\Framework\DataObject|null $object + * @return AbstractAttribute[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) +@@ -639,7 +823,11 @@ class Config + $existsFullAttribute = $attribute->hasIsRequired(); + $fullAttributeData = array_key_exists('is_required', $attributeData); + +- if ($existsFullAttribute || !$existsFullAttribute && !$fullAttributeData) { ++ if ($existsFullAttribute || (!$existsFullAttribute && !$fullAttributeData)) { ++ $scopeIsRequired = $attributeData['scope_is_required'] ?? null; ++ if ($scopeIsRequired !== null) { ++ $attribute->setData('scope_is_required', $scopeIsRequired); ++ } + return $attribute; + } + } +@@ -708,6 +896,7 @@ class Config + * @param string $entityType + * @param string $attributeCode + * @return AbstractAttribute ++ * @throws LocalizedException + */ + private function createAttributeByAttributeCode($entityType, $attributeCode) + { +@@ -723,13 +912,28 @@ class Config + $attribute->setAttributeCode($attributeCode); + } + ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ ++ return $attribute; ++ } ++ ++ /** ++ * Set entity type id, backend type, is global to attribute. ++ * ++ * @param AbstractAttribute $attribute ++ * @param AbstractModel $entityType ++ * @return AbstractAttribute ++ */ ++ private function setAttributeData($attribute, $entityType): AbstractAttribute ++ { + $entity = $entityType->getEntity(); +- if ($entity instanceof \Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface ++ if ($entity instanceof ProviderInterface + && in_array($attribute->getAttributeCode(), $entity->getDefaultAttributes(), true) + ) { + $attribute->setBackendType(AbstractAttribute::TYPE_STATIC)->setIsGlobal(1); + } + $attribute->setEntityType($entityType)->setEntityTypeId($entityType->getId()); ++ + return $attribute; + } + diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php --- a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +++ b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php @@ -6,6 +6,9 @@ - + namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; - + +/** + * Basic implementation for attribute sets + */ @@ -44,7 +400,7 @@ diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set. /** @@ -24,8 +27,6 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb protected $eavConfig; - + /** - * Constructor - * @@ -53,7 +409,7 @@ diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set. * @param \Magento\Eav\Model\Config $eavConfig @@ -54,7 +55,7 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb } - + /** - * Perform actions after object save + * Perform actions after object save. @@ -109,9 +465,18 @@ diff -Nuar a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-them diff -Nuar a/app/etc/di.xml b/app/etc/di.xml --- a/app/etc/di.xml +++ b/app/etc/di.xml -@@ -1800,4 +1800,14 @@ +@@ -1779,8 +1779,6 @@ + <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> +- <argument name="lockTimeout" xsi:type="number">10000</argument> +- <argument name="delayTimeout" xsi:type="number">20</argument> + </arguments> </type> - <preference for="Magento\Framework\GraphQl\Query\ErrorHandlerInterface" type="Magento\Framework\GraphQl\Query\ErrorHandler"/> + <preference for="Magento\Framework\HTTP\AsyncClientInterface" type="Magento\Framework\HTTP\AsyncClient\GuzzleAsyncClient" /> +@@ -1795,4 +1793,14 @@ + <preference for="Magento\Framework\Mail\MimePartInterface" + type="Magento\Framework\Mail\MimePart" /> <preference for="Magento\Framework\Filter\VariableResolverInterface" type="Magento\Framework\Filter\VariableResolver\StrategyResolver"/> + <virtualType name="configured_block_cache" type="Magento\Framework\App\Cache"> + <arguments> @@ -170,11 +535,11 @@ diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Pool.php b/vendor/magen - throw new \InvalidArgumentException("Cache frontend '{$identifier}' is not recognized."); + + if (!isset($this->_instances[self::DEFAULT_FRONTEND_ID])) { -+ throw new \InvalidArgumentException(sprintf( -+ 'Cache frontend \'%s\' is not recognized. As well as %s cache is not configured', -+ $identifier, -+ self::DEFAULT_FRONTEND_ID -+ )); ++ throw new \InvalidArgumentException( ++ "Cache frontend '{$identifier}' is not recognized. As well as " . ++ self::DEFAULT_FRONTEND_ID . ++ "cache is not configured" ++ ); + } + + return $this->_instances[self::DEFAULT_FRONTEND_ID]; @@ -224,7 +589,7 @@ diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento } } -diff -Nuar a/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php +diff -Nuar a/2.3.5/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php new file mode 100644 --- /dev/null +++ b/vendor/magento/framework/Cache/Backend/Redis.php @@ -268,7 +633,7 @@ new file mode 100644 + /** + * Load value with given id from cache + * -+ * @param string $id Cache id ++ * @param string $id Cache id + * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested + * @return bool|string + */ @@ -418,7 +783,7 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + * @param string $id + * @return bool + */ -+ private function removeRemoteDataVersion($id): bool ++ private function removeRemoteDataVersion($id) + { + return $this->remote->remove($id . self::HASH_SUFFIX); + } @@ -594,10 +959,11 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php --- a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +++ b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php -@@ -37,18 +37,33 @@ class LockGuardedCacheLoader +@@ -36,19 +36,43 @@ class LockGuardedCacheLoader + */ private $delayTimeout; - /** ++ /** + * Timeout for information to be collected and saved. + * If timeout passed that means that data cannot be saved right now. + * And we will just return collected data. @@ -609,27 +975,36 @@ diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ + private $loadTimeout; + + /** -+ * LockGuardedCacheLoader constructor. ++ * Minimal delay timeout in ms. ++ * ++ * @var int ++ */ ++ private $minimalDelayTimeout; ++ + /** * @param LockManagerInterface $locker * @param int $lockTimeout * @param int $delayTimeout + * @param int $loadTimeout ++ * @param int $minimalDelayTimeout */ public function __construct( LockManagerInterface $locker, int $lockTimeout = 10000, - int $delayTimeout = 20 + int $delayTimeout = 20, -+ int $loadTimeout = 10000 ++ int $loadTimeout = 10000, ++ int $minimalDelayTimeout = 5 ) { $this->locker = $locker; $this->lockTimeout = $lockTimeout; $this->delayTimeout = $delayTimeout; + $this->loadTimeout = $loadTimeout; ++ $this->minimalDelayTimeout = $minimalDelayTimeout; } /** -@@ -67,21 +82,21 @@ class LockGuardedCacheLoader +@@ -67,25 +91,25 @@ class LockGuardedCacheLoader callable $dataSaver ) { $cachedData = $dataLoader(); //optimistic read @@ -638,7 +1013,7 @@ diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ - usleep($this->delayTimeout * 1000); - $cachedData = $dataLoader(); - } -+ $deadline = microtime(true) + $this->loadTimeout; ++ $deadline = microtime(true) + $this->loadTimeout / 100; while ($cachedData === false) { - try { @@ -660,6 +1035,41 @@ diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ } if ($cachedData === false) { +- usleep($this->delayTimeout * 1000); ++ usleep($this->getLookupTimeout() * 1000); + $cachedData = $dataLoader(); + } + } +@@ -103,14 +127,21 @@ class LockGuardedCacheLoader + public function lockedCleanData(string $lockName, callable $dataCleaner) + { + while ($this->locker->isLocked($lockName)) { +- usleep($this->delayTimeout * 1000); +- } +- try { +- if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { +- $dataCleaner(); +- } +- } finally { +- $this->locker->unlock($lockName); ++ usleep($this->getLookupTimeout() * 1000); + } ++ ++ $dataCleaner(); ++ } ++ ++ /** ++ * Delay will be applied as rand($minimalDelayTimeout, $delayTimeout). ++ * This helps to desynchronize multiple clients trying ++ * to acquire the lock for the same resource at the same time ++ * ++ * @return int ++ */ ++ private function getLookupTimeout() ++ { ++ return rand($this->minimalDelayTimeout, $this->delayTimeout); + } + } diff -Nuar a/vendor/magento/framework/Interception/Config/Config.php b/vendor/magento/framework/Interception/Config/Config.php --- a/vendor/magento/framework/Interception/Config/Config.php +++ b/vendor/magento/framework/Interception/Config/Config.php @@ -675,36 +1085,107 @@ diff -Nuar a/vendor/magento/framework/Interception/Config/Config.php b/vendor/ma diff -Nuar a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php --- a/vendor/magento/framework/Lock/Backend/Cache.php +++ b/vendor/magento/framework/Lock/Backend/Cache.php -@@ -37,6 +37,10 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -24,12 +24,20 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + private $cache; + ++ /** ++ * Sign for locks, helps to avoid removing a lock that was created by another client ++ * ++ * @string ++ */ ++ private $lockSign; ++ + /** + * @param FrontendInterface $cache + */ + public function __construct(FrontendInterface $cache) + { + $this->cache = $cache; ++ $this->lockSign = $this->generateLockSign(); + } + + /** +@@ -37,7 +45,26 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface */ public function lock(string $name, int $timeout = -1): bool { -+ if ((bool)$this->cache->test($this->getIdentifier($name))) { +- return $this->cache->save('1', $this->getIdentifier($name), [], $timeout); ++ if (empty($this->lockSign)) { ++ $this->lockSign = $this->generateLockSign(); ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false !== $data) { + return false; + } + - return $this->cache->save('1', $this->getIdentifier($name), [], $timeout); ++ $timeout = $timeout <= 0 ? null : $timeout; ++ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if ($data === $this->lockSign) { ++ return true; ++ } ++ ++ return false; } -diff -Nuar a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php ---- a/vendor/magento/framework/Lock/Backend/FileLock.php -+++ b/vendor/magento/framework/Lock/Backend/FileLock.php -@@ -91,6 +91,7 @@ class FileLock implements LockManagerInterface + /** +@@ -45,7 +72,22 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + public function unlock(string $name): bool + { +- return $this->cache->remove($this->getIdentifier($name)); ++ if (empty($this->lockSign)) { ++ return false; ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false === $data) { ++ return false; ++ } ++ ++ $removeResult = false; ++ if ($data === $this->lockSign) { ++ $removeResult = (bool)$this->cache->remove($this->getIdentifier($name)); ++ } ++ ++ return $removeResult; + } - while (!$this->tryToLock($fileResource)) { - if (!$skipDeadline && $deadline <= microtime(true)) { -+ $this->tryToUnlock($fileResource); - $this->fileDriver->fileClose($fileResource); - return false; - } -@@ -124,6 +125,7 @@ class FileLock implements LockManagerInterface - } else { - $result = true; - } -+ $this->tryToUnlock($fileResource); - $this->fileDriver->fileClose($fileResource); - } - } catch (FileSystemException $exception) { + /** +@@ -66,4 +108,27 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + { + return self::LOCK_PREFIX . $cacheIdentifier; + } ++ ++ /** ++ * Function that generates lock sign that helps to avoid removing a lock that was created by another client. ++ * ++ * @return string ++ */ ++ private function generateLockSign() ++ { ++ $sign = implode( ++ '-', ++ [ ++ \getmypid(), \crc32(\gethostname()) ++ ] ++ ); ++ ++ try { ++ $sign .= '-' . \bin2hex(\random_bytes(4)); ++ } catch (\Exception $e) { ++ $sign .= '-' . \uniqid('-uniqid-'); ++ } ++ ++ return $sign; ++ } + } diff -Nuar a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php --- a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +++ b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php @@ -734,7 +1215,7 @@ diff -Nuar a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setu ]; return $operations; -diff -Nuar a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +diff -Nuar a/2.3.5/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php new file mode 100644 --- /dev/null +++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php @@ -780,7 +1261,7 @@ new file mode 100644 + /** + * @inheritDoc + */ -+ public function doOperation(): void ++ public function doOperation() + { + $actionList = $this->moduleReader->getActionFiles(); + $this->configWriter->write( @@ -792,7 +1273,7 @@ new file mode 100644 + /** + * @inheritDoc + */ -+ public function getName(): string ++ public function getName() + { + return 'App action list generation'; + } @@ -813,18 +1294,18 @@ diff -Nuar a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/s class OperationFactory { /** -@@ -48,6 +54,11 @@ class OperationFactory +@@ -47,6 +53,11 @@ class OperationFactory + */ const APPLICATION_CODE_GENERATOR = 'application_code_generator'; - /** ++ /** + * Application action list generator + */ + const APPLICATION_ACTION_LIST_GENERATOR = 'application_action_list_generator'; + -+ /** + /** * Operations definitions * - * @var array @@ -61,6 +72,7 @@ class OperationFactory self::INTERCEPTION_CACHE => \Magento\Setup\Module\Di\App\Task\Operation\InterceptionCache::class, self::REPOSITORY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\RepositoryGenerator::class, @@ -833,3 +1314,175 @@ diff -Nuar a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/s ]; /** + +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -74,9 +74,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + const STORE_ID = 'store_id'; + + /** +- * @var string ++ * @var string|bool + */ +- protected $_cacheTag = self::CACHE_TAG; ++ protected $_cacheTag = false; + + /** + * @var string +@@ -912,7 +912,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + public function beforeSave() + { +- $this->cleanCache(); + $this->setTypeHasOptions(false); + $this->setTypeHasRequiredOptions(false); + $this->setHasOptions(false); +diff -Nuar a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php +--- a/vendor/magento/module-config/App/Config/Type/System.php ++++ b/vendor/magento/module-config/App/Config/Type/System.php +@@ -20,6 +20,8 @@ use Magento\Framework\Serialize\SerializerInterface; + use Magento\Store\Model\Config\Processor\Fallback; + use Magento\Framework\Encryption\Encryptor; + use Magento\Store\Model\ScopeInterface as StoreScope; ++use Magento\Framework\App\Cache\StateInterface; ++use Magento\Framework\App\Cache\Type\Config; + + /** + * System configuration type +@@ -98,6 +100,12 @@ class System implements ConfigTypeInterface + private $lockQuery; + + /** ++ * @var StateInterface ++ */ ++ private $cacheState; ++ ++ /** ++ * System constructor. + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback +@@ -110,6 +118,7 @@ class System implements ConfigTypeInterface + * @param Encryptor|null $encryptor + * @param LockManagerInterface|null $locker + * @param LockGuardedCacheLoader|null $lockQuery ++ * @param StateInterface|null $cacheState + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -125,7 +134,8 @@ class System implements ConfigTypeInterface + Reader $reader = null, + Encryptor $encryptor = null, + LockManagerInterface $locker = null, +- LockGuardedCacheLoader $lockQuery = null ++ LockGuardedCacheLoader $lockQuery = null, ++ StateInterface $cacheState = null + ) { + $this->postProcessor = $postProcessor; + $this->cache = $cache; +@@ -136,6 +146,8 @@ class System implements ConfigTypeInterface + ?: ObjectManager::getInstance()->get(Encryptor::class); + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); ++ $this->cacheState = $cacheState ++ ?: ObjectManager::getInstance()->get(StateInterface::class); + } + + /** +@@ -220,6 +232,10 @@ class System implements ConfigTypeInterface + */ + private function loadAllData() + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () { + $cachedData = $this->cache->load($this->configType); + $data = false; +@@ -245,6 +261,10 @@ class System implements ConfigTypeInterface + */ + private function loadDefaultScopeData($scopeType) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + $scopeData = false; +@@ -271,6 +291,10 @@ class System implements ConfigTypeInterface + */ + private function loadScopeData($scopeType, $scopeId) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + $scopeData = false; +@@ -393,6 +417,10 @@ class System implements ConfigTypeInterface + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + }; + ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $cleanAction(); ++ } ++ + $this->lockQuery->lockedCleanData( + self::$lockName, + $cleanAction +diff -Nuar a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml +--- a/vendor/magento/module-config/etc/di.xml ++++ b/vendor/magento/module-config/etc/di.xml +@@ -97,8 +97,6 @@ + <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> +- <argument name="lockTimeout" xsi:type="number">42000</argument> +- <argument name="delayTimeout" xsi:type="number">100</argument> + </arguments> + </virtualType> + +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class Cache implements ConfigOptionsListInterface + { + const INPUT_VALUE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; + const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; +@@ -282,6 +282,6 @@ class Cache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class PageCache implements ConfigOptionsListInterface + { + const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_PAGE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; +@@ -284,6 +284,6 @@ class PageCache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } diff --git a/patches/MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch similarity index 69% rename from patches/MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3.patch rename to patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch index 4a510056..90ee3df1 100644 --- a/patches/MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch @@ -1,139 +1,69 @@ -diff --git a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php ---- a/vendor/magento/module-catalog/Model/Product.php -+++ b/vendor/magento/module-catalog/Model/Product.php -@@ -74,9 +74,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - const STORE_ID = 'store_id'; - - /** -- * @var string -+ * @var string|bool - */ -- protected $_cacheTag = self::CACHE_TAG; -+ protected $_cacheTag = false; - - /** - * @var string -@@ -912,7 +912,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - */ - public function beforeSave() - { -- $this->cleanCache(); - $this->setTypeHasOptions(false); - $this->setTypeHasRequiredOptions(false); - $this->setHasOptions(false); -diff --git a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php ---- a/vendor/magento/module-config/App/Config/Type/System.php -+++ b/vendor/magento/module-config/App/Config/Type/System.php -@@ -20,6 +20,8 @@ use Magento\Framework\Serialize\SerializerInterface; - use Magento\Store\Model\Config\Processor\Fallback; - use Magento\Framework\Encryption\Encryptor; - use Magento\Store\Model\ScopeInterface as StoreScope; -+use Magento\Framework\App\Cache\StateInterface; -+use Magento\Framework\App\Cache\Type\Config; - +diff -Nuar a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module-eav/Model/Config.php +--- a/vendor/magento/module-eav/Model/Config.php ++++ b/vendor/magento/module-eav/Model/Config.php +@@ -7,12 +7,20 @@ namespace Magento\Eav\Model; + + use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; + use Magento\Eav\Model\Entity\Type; ++use Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface; ++use Magento\Framework\App\Config\ScopeConfigInterface; + use Magento\Framework\App\ObjectManager; ++use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Model\AbstractModel; + use Magento\Framework\Serialize\SerializerInterface; + /** - * System configuration type -@@ -98,6 +100,12 @@ class System implements ConfigTypeInterface - private $lockQuery; - - /** -+ * @var StateInterface -+ */ -+ private $cacheState; -+ ++ * EAV config model. ++ * + * @api ++ * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) ++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @since 100.0.2 + */ + class Config +@@ -25,6 +33,11 @@ class Config + const ATTRIBUTES_CODES_CACHE_ID = 'EAV_ENTITY_ATTRIBUTES_CODES'; + /**#@-*/ + + /** -+ * System constructor. - * @param ConfigSourceInterface $source - * @param PostProcessorInterface $postProcessor - * @param Fallback $fallback -@@ -110,6 +118,7 @@ class System implements ConfigTypeInterface - * @param Encryptor|null $encryptor - * @param LockManagerInterface|null $locker - * @param LockGuardedCacheLoader|null $lockQuery -+ * @param StateInterface|null $cacheState - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ -@@ -125,7 +134,8 @@ class System implements ConfigTypeInterface - Reader $reader = null, - Encryptor $encryptor = null, - LockManagerInterface $locker = null, -- LockGuardedCacheLoader $lockQuery = null -+ LockGuardedCacheLoader $lockQuery = null, -+ StateInterface $cacheState = null - ) { - $this->postProcessor = $postProcessor; - $this->cache = $cache; -@@ -136,6 +146,8 @@ class System implements ConfigTypeInterface - ?: ObjectManager::getInstance()->get(Encryptor::class); - $this->lockQuery = $lockQuery - ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); -+ $this->cacheState = $cacheState -+ ?: ObjectManager::getInstance()->get(StateInterface::class); - } - - /** -@@ -220,6 +232,10 @@ class System implements ConfigTypeInterface - */ - private function loadAllData() - { -+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { -+ return $this->readData(); -+ } ++ * Xml path to caching user defined eav attributes configuration. ++ */ ++ private const XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES = 'dev/caching/cache_user_defined_attributes'; + - $loadAction = function () { - $cachedData = $this->cache->load($this->configType); - $data = false; -@@ -245,6 +261,10 @@ class System implements ConfigTypeInterface + /**#@-*/ + protected $_entityTypeData; + +@@ -116,6 +129,11 @@ class Config */ - private function loadDefaultScopeData($scopeType) - { -+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { -+ return $this->readData(); -+ } + private $serializer; + ++ /** ++ * @var ScopeConfigInterface ++ */ ++ private $scopeConfig; + - $loadAction = function () use ($scopeType) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType); - $scopeData = false; -@@ -271,6 +291,10 @@ class System implements ConfigTypeInterface + /** + * Cache of attributes per set + * +@@ -123,13 +141,29 @@ class Config */ - private function loadScopeData($scopeType, $scopeId) - { -+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { -+ return $this->readData(); -+ } + private $attributesPerSet = []; + ++ /** ++ * Is system attributes loaded flag. ++ * ++ * @var array ++ */ ++ private $isSystemAttributesLoaded = []; + - $loadAction = function () use ($scopeType, $scopeId) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); - $scopeData = false; -@@ -393,6 +417,10 @@ class System implements ConfigTypeInterface - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); - }; - -+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { -+ return $cleanAction(); -+ } ++ /** ++ * List of predefined system attributes for preload. ++ * ++ * @var array ++ */ ++ private $attributesForPreload; + - $this->lockQuery->lockedCleanData( - self::$lockName, - $cleanAction -diff --git a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml ---- a/vendor/magento/module-config/etc/di.xml -+++ b/vendor/magento/module-config/etc/di.xml -@@ -97,8 +97,6 @@ - <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> - <arguments> - <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> -- <argument name="lockTimeout" xsi:type="number">42000</argument> -- <argument name="delayTimeout" xsi:type="number">100</argument> - </arguments> - </virtualType> - -diff --git a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module-eav/Model/Config.php ---- a/vendor/magento/module-eav/Model/Config.php -+++ b/vendor/magento/module-eav/Model/Config.php -@@ -125,11 +125,11 @@ class Config - /** * @param \Magento\Framework\App\CacheInterface $cache - * @param \Magento\Eav\Model\Entity\TypeFactory $entityTypeFactory @@ -144,13 +74,70 @@ diff --git a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module- * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param SerializerInterface $serializer + * @param SerializerInterface|null $serializer ++ * @param ScopeConfigInterface|null $scopeConfig ++ * @param array $attributesForPreload * @codeCoverageIgnore */ public function __construct( -@@ -336,7 +336,9 @@ class Config +@@ -138,7 +172,9 @@ class Config + \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory, + \Magento\Framework\App\Cache\StateInterface $cacheState, + \Magento\Framework\Validator\UniversalFactory $universalFactory, +- SerializerInterface $serializer = null ++ SerializerInterface $serializer = null, ++ ScopeConfigInterface $scopeConfig = null, ++ $attributesForPreload = [] + ) { + $this->_cache = $cache; + $this->_entityTypeFactory = $entityTypeFactory; +@@ -146,6 +182,8 @@ class Config + $this->_cacheState = $cacheState; + $this->_universalFactory = $universalFactory; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); ++ $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); ++ $this->attributesForPreload = $attributesForPreload; + } + + /** +@@ -207,8 +245,8 @@ class Config + /** + * Associate object with identifier + * +- * @param mixed $obj +- * @param mixed $id ++ * @param mixed $obj ++ * @param mixed $id + * @return void + * @codeCoverageIgnore + */ +@@ -233,8 +271,8 @@ class Config + /** + * Specify reference for entity type id + * +- * @param int $id +- * @param string $code ++ * @param int $id ++ * @param string $code + * @return $this + * @codeCoverageIgnore + */ +@@ -258,9 +296,9 @@ class Config + /** + * Specify reference between entity attribute id and attribute code + * +- * @param int $id +- * @param string $code +- * @param string $entityTypeCode ++ * @param int $id ++ * @param string $code ++ * @param string $entityTypeCode + * @return $this + */ + protected function _addAttributeReference($id, $code, $entityTypeCode) +@@ -336,7 +374,9 @@ class Config } \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); - + - if ($this->isCacheEnabled() && ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID))) { + if ($this->isCacheEnabled() && + ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID)) @@ -158,13 +145,253 @@ diff --git a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module- $this->_entityTypeData = $this->serializer->unserialize($cache); foreach ($this->_entityTypeData as $typeCode => $data) { $typeId = $data['entity_type_id']; -diff --git a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +@@ -484,10 +524,10 @@ class Config + /** + * Get attribute by code for entity type + * +- * @param mixed $entityType +- * @param mixed $code +- * @return AbstractAttribute +- * @throws \Magento\Framework\Exception\LocalizedException ++ * @param mixed $entityType ++ * @param mixed $code ++ * @return AbstractAttribute ++ * @throws LocalizedException + */ + public function getAttribute($entityType, $code) + { +@@ -507,8 +547,152 @@ class Config + return $this->attributes[$entityTypeCode][$code]; + } + ++ if (array_key_exists($entityTypeCode, $this->attributesForPreload) ++ && array_key_exists($code, $this->attributesForPreload[$entityTypeCode]) ++ ) { ++ $this->initSystemAttributes($entityType, $this->attributesForPreload[$entityTypeCode]); ++ } ++ if (isset($this->attributes[$entityTypeCode][$code])) { ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $this->attributes[$entityTypeCode][$code]; ++ } ++ ++ if ($this->scopeConfig->getValue(self::XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES)) { ++ $attribute = $this->cacheUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } else { ++ $attribute = $this->initUserDefinedAttribute($entityType, $entityTypeCode, $code); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ return $attribute; ++ } ++ ++ /** ++ * Initialize predefined system attributes for preload. ++ * ++ * @param string $entityType ++ * @param array $systemAttributes ++ * @return $this|bool|void ++ * @throws LocalizedException ++ */ ++ private function initSystemAttributes($entityType, $systemAttributes) ++ { ++ $entityType = $this->getEntityType($entityType); ++ $entityTypeCode = $entityType->getEntityTypeCode(); ++ if (!empty($this->isSystemAttributesLoaded[$entityTypeCode])) { ++ return; ++ } ++ ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-' . $entityTypeCode . '-preload'; ++ if ($this->isCacheEnabled() && ($attributes = $this->_cache->load($cacheKey))) { ++ $attributes = $this->serializer->unserialize($attributes); ++ if ($attributes) { ++ foreach ($attributes as $attribute) { ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ } ++ return true; ++ } ++ } ++ ++ \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); ++ ++ /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection $attributes */ ++ $attributes = $this->_universalFactory->create( ++ $entityType->getEntityAttributeCollection() ++ )->setEntityTypeFilter( ++ $entityType ++ )->addFieldToFilter( ++ 'attribute_code', ++ ['in' => array_keys($systemAttributes)] ++ )->getData(); ++ ++ $attributeData = []; ++ foreach ($attributes as $attribute) { ++ if (empty($attribute['attribute_model'])) { ++ $attribute['attribute_model'] = $entityType->getAttributeModel(); ++ } ++ $attributeObject = $this->_createAttribute($entityType, $attribute); ++ $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); ++ $attributeData[$attribute['attribute_code']] = $attributeObject->toArray(); ++ } ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attributeData), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ ++ \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ $this->isSystemAttributesLoaded[$entityTypeCode] = true; ++ ++ return $this; ++ } ++ ++ /** ++ * Initialize user defined attribute from cache or cache it. ++ * ++ * @param string $entityType ++ * @param mixed $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute ++ * @throws LocalizedException ++ */ ++ private function cacheUserDefinedAttribute($entityType, $entityTypeCode, $code): AbstractAttribute ++ { ++ $cacheKey = self::ATTRIBUTES_CACHE_ID . '-attribute-' . $entityTypeCode . '-' . $code; ++ $attributeData = $this->isCacheEnabled() && ($attribute = $this->_cache->load($cacheKey)) ++ ? $this->serializer->unserialize($attribute) ++ : null; ++ if ($attributeData) { ++ if (isset($attributeData['attribute_id'])) { ++ $attribute = $this->_createAttribute($entityType, $attributeData); ++ } else { ++ $entityType = $this->getEntityType($entityType); ++ $attribute = $this->createAttribute($entityType->getAttributeModel()); ++ $attribute->setAttributeCode($code); ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ } ++ } else { ++ $attribute = $this->createAttributeByAttributeCode($entityType, $code); ++ $this->_addAttributeReference( ++ $attribute->getAttributeId(), ++ $attribute->getAttributeCode(), ++ $entityTypeCode ++ ); ++ $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); ++ if ($this->isCacheEnabled()) { ++ $this->_cache->save( ++ $this->serializer->serialize($attribute->getData()), ++ $cacheKey, ++ [ ++ \Magento\Eav\Model\Cache\Type::CACHE_TAG, ++ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ++ ] ++ ); ++ } ++ } ++ ++ return $attribute; ++ } ++ ++ /** ++ * Initialize user defined attribute and save it to memory cache. ++ * ++ * @param mixed $entityType ++ * @param string $entityTypeCode ++ * @param string $code ++ * @return AbstractAttribute|null ++ * @throws LocalizedException ++ */ ++ private function initUserDefinedAttribute($entityType, $entityTypeCode, $code): ?AbstractAttribute ++ { + $attributes = $this->loadAttributes($entityTypeCode); +- $attribute = isset($attributes[$code]) ? $attributes[$code] : null; ++ $attribute = $attributes[$code] ?? null; + if (!$attribute) { + $attribute = $this->createAttributeByAttributeCode($entityType, $code); + $this->_addAttributeReference( +@@ -518,7 +702,7 @@ class Config + ); + $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); + } +- \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); ++ + return $attribute; + } + +@@ -555,8 +739,8 @@ class Config + /** + * Get all entity type attributes + * +- * @param int|string|Type $entityType +- * @param \Magento\Framework\DataObject|null $object ++ * @param int|string|Type $entityType ++ * @param \Magento\Framework\DataObject|null $object + * @return AbstractAttribute[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) +@@ -639,7 +823,11 @@ class Config + $existsFullAttribute = $attribute->hasIsRequired(); + $fullAttributeData = array_key_exists('is_required', $attributeData); + +- if ($existsFullAttribute || !$existsFullAttribute && !$fullAttributeData) { ++ if ($existsFullAttribute || (!$existsFullAttribute && !$fullAttributeData)) { ++ $scopeIsRequired = $attributeData['scope_is_required'] ?? null; ++ if ($scopeIsRequired !== null) { ++ $attribute->setData('scope_is_required', $scopeIsRequired); ++ } + return $attribute; + } + } +@@ -708,6 +896,7 @@ class Config + * @param string $entityType + * @param string $attributeCode + * @return AbstractAttribute ++ * @throws LocalizedException + */ + private function createAttributeByAttributeCode($entityType, $attributeCode) + { +@@ -723,13 +912,28 @@ class Config + $attribute->setAttributeCode($attributeCode); + } + ++ $attribute = $this->setAttributeData($attribute, $entityType); ++ ++ return $attribute; ++ } ++ ++ /** ++ * Set entity type id, backend type, is global to attribute. ++ * ++ * @param AbstractAttribute $attribute ++ * @param AbstractModel $entityType ++ * @return AbstractAttribute ++ */ ++ private function setAttributeData($attribute, $entityType): AbstractAttribute ++ { + $entity = $entityType->getEntity(); +- if ($entity instanceof \Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface ++ if ($entity instanceof ProviderInterface + && in_array($attribute->getAttributeCode(), $entity->getDefaultAttributes(), true) + ) { + $attribute->setBackendType(AbstractAttribute::TYPE_STATIC)->setIsGlobal(1); + } + $attribute->setEntityType($entityType)->setEntityTypeId($entityType->getId()); ++ + return $attribute; + } + +diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php --- a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +++ b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php @@ -6,6 +6,9 @@ - + namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; - + +/** + * Basic implementation for attribute sets + */ @@ -173,7 +400,7 @@ diff --git a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set. /** @@ -24,8 +27,6 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb protected $eavConfig; - + /** - * Constructor - * @@ -182,14 +409,14 @@ diff --git a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set. * @param \Magento\Eav\Model\Config $eavConfig @@ -54,7 +55,7 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb } - + /** - * Perform actions after object save + * Perform actions after object save. * * @param \Magento\Framework\Model\AbstractModel $object * @return $this -diff --git a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/etc/di.xml +diff -Nuar a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/etc/di.xml --- a/vendor/magento/module-eav/etc/di.xml +++ b/vendor/magento/module-eav/etc/di.xml @@ -209,4 +209,14 @@ @@ -207,7 +434,7 @@ diff --git a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/et + </arguments> + </type> </config> -diff --git a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-theme/etc/di.xml +diff -Nuar a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-theme/etc/di.xml --- a/vendor/magento/module-theme/etc/di.xml +++ b/vendor/magento/module-theme/etc/di.xml @@ -285,4 +285,24 @@ @@ -235,7 +462,7 @@ diff --git a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-them + </arguments> + </type> </config> -diff --git a/app/etc/di.xml b/app/etc/di.xml +diff -Nuar a/app/etc/di.xml b/app/etc/di.xml --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1779,8 +1779,6 @@ @@ -262,19 +489,19 @@ diff --git a/app/etc/di.xml b/app/etc/di.xml + </arguments> + </type> </config> -diff --git a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/App/Cache.php +diff -Nuar a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/App/Cache.php --- a/vendor/magento/framework/App/Cache.php +++ b/vendor/magento/framework/App/Cache.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ - + -/** - * System cache model - * support id and tags prefix support, - */ namespace Magento\Framework\App; - + +/** + * System cache model support id and tags prefix support. + */ @@ -283,7 +510,7 @@ diff --git a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/A /** @@ -30,12 +29,13 @@ class Cache implements CacheInterface protected $_frontend; - + /** - * @param \Magento\Framework\App\Cache\Frontend\Pool $frontendPool + * @param Cache\Frontend\Pool $frontendPool @@ -296,35 +523,35 @@ diff --git a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/A - $this->_frontend = $frontendPool->get($this->_frontendIdentifier); + $this->_frontend = $frontendPool->get($cacheIdentifier ?? $this->_frontendIdentifier); } - + /** -diff --git a/vendor/magento/framework/App/Cache/Frontend/Pool.php b/vendor/magento/framework/App/Cache/Frontend/Pool.php +diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Pool.php b/vendor/magento/framework/App/Cache/Frontend/Pool.php --- a/vendor/magento/framework/App/Cache/Frontend/Pool.php +++ b/vendor/magento/framework/App/Cache/Frontend/Pool.php -@@ -147,6 +147,15 @@ class Pool implements \Iterator +@@ -152,6 +152,15 @@ class Pool implements \Iterator if (isset($this->_instances[$identifier])) { return $this->_instances[$identifier]; } - throw new \InvalidArgumentException("Cache frontend '{$identifier}' is not recognized."); + + if (!isset($this->_instances[self::DEFAULT_FRONTEND_ID])) { -+ throw new \InvalidArgumentException(sprintf( -+ 'Cache frontend \'%s\' is not recognized. As well as %s cache is not configured', -+ $identifier, -+ self::DEFAULT_FRONTEND_ID -+ )); ++ throw new \InvalidArgumentException( ++ "Cache frontend '{$identifier}' is not recognized. As well as " . ++ self::DEFAULT_FRONTEND_ID . ++ "cache is not configured" ++ ); + } + + return $this->_instances[self::DEFAULT_FRONTEND_ID]; } } -diff --git a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php --- a/vendor/magento/framework/App/Router/ActionList.php +++ b/vendor/magento/framework/App/Router/ActionList.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\App\Router; - + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\State; use Magento\Framework\Serialize\SerializerInterface; @@ -361,8 +588,8 @@ diff --git a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento + } } } - -diff --git a/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php + +diff -Nuar a/2.3.5/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php new file mode 100644 --- /dev/null +++ b/vendor/magento/framework/Cache/Backend/Redis.php @@ -406,7 +633,7 @@ new file mode 100644 + /** + * Load value with given id from cache + * -+ * @param string $id Cache id ++ * @param string $id Cache id + * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested + * @return bool|string + */ @@ -450,7 +677,7 @@ new file mode 100644 + return true; + } +} -diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php --- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php @@ -9,8 +9,10 @@ namespace Magento\Framework\Cache\Backend; @@ -468,7 +695,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -36,11 +38,15 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache protected $cacheInvalidationTime; - + /** - * {@inheritdoc} + * Suffix for hash to compare data version in cache storage. @@ -485,7 +712,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php 'remote_backend_autoload' => true, 'remote_backend_options' => [], @@ -52,6 +58,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache - + /** * @param array $options + * @throws \Zend_Cache_Exception @@ -494,7 +721,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -97,76 +104,137 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * Update remote cache status info + * @inheritdoc @@ -518,7 +745,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php - $this->cacheInvalidationTime = null; + return \hash('sha256', $data); } - + /** - * {@inheritdoc} + * Load data version by id from remote. @@ -534,7 +761,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $id . self::HASH_SUFFIX + ); } - + /** - * {@inheritdoc} + * Save new data version to remote. @@ -556,7 +783,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + * @param string $id + * @return bool + */ -+ private function removeRemoteDataVersion($id): bool ++ private function removeRemoteDataVersion($id) + { + return $this->remote->remove($id . self::HASH_SUFFIX); + } @@ -596,7 +823,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + + return $localData; } - + /** - * {@inheritdoc} + * @inheritdoc @@ -606,7 +833,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php - return $this->local->test($id); + return $this->local->test($id) ?? $this->remote->test($id); } - + /** - * {@inheritdoc} + * @inheritdoc @@ -626,7 +853,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + + return $this->local->save($dataToSave, $id, [], $specificLifetime); } - + /** - * {@inheritdoc} + * @inheritdoc @@ -639,7 +866,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $this->remote->remove($id) && + $this->local->remove($id); } - + /** - * {@inheritdoc} + * @inheritdoc @@ -650,7 +877,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php - return $this->local->clean($mode, $tags); + return $this->remote->clean($mode, $tags); } - + /** - * {@inheritdoc} + * @inheritdoc @@ -659,7 +886,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -174,7 +242,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -668,7 +895,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -182,7 +250,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -677,7 +904,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -190,7 +258,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -686,7 +913,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -198,7 +266,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -695,7 +922,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -206,7 +274,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -704,7 +931,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -214,7 +282,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -713,7 +940,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -222,7 +290,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -722,20 +949,21 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -230,7 +298,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc */ public function getCapabilities() { -diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php --- a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +++ b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php -@@ -37,18 +37,43 @@ class LockGuardedCacheLoader +@@ -36,19 +36,43 @@ class LockGuardedCacheLoader + */ private $delayTimeout; - /** ++ /** + * Timeout for information to be collected and saved. + * If timeout passed that means that data cannot be saved right now. + * And we will just return collected data. @@ -753,8 +981,7 @@ diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ + */ + private $minimalDelayTimeout; + -+ /** -+ * LockGuardedCacheLoader constructor. + /** * @param LockManagerInterface $locker * @param int $lockTimeout * @param int $delayTimeout @@ -777,7 +1004,7 @@ diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ } /** -@@ -67,25 +92,25 @@ class LockGuardedCacheLoader +@@ -67,25 +91,25 @@ class LockGuardedCacheLoader callable $dataSaver ) { $cachedData = $dataLoader(); //optimistic read @@ -813,7 +1040,7 @@ diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ $cachedData = $dataLoader(); } } -@@ -103,14 +128,21 @@ class LockGuardedCacheLoader +@@ -103,14 +127,21 @@ class LockGuardedCacheLoader public function lockedCleanData(string $lockName, callable $dataCleaner) { while ($this->locker->isLocked($lockName)) { @@ -843,7 +1070,7 @@ diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ + return rand($this->minimalDelayTimeout, $this->delayTimeout); } } -diff --git a/vendor/magento/framework/Interception/Config/Config.php b/vendor/magento/framework/Interception/Config/Config.php +diff -Nuar a/vendor/magento/framework/Interception/Config/Config.php b/vendor/magento/framework/Interception/Config/Config.php --- a/vendor/magento/framework/Interception/Config/Config.php +++ b/vendor/magento/framework/Interception/Config/Config.php @@ -187,8 +187,6 @@ class Config implements \Magento\Framework\Interception\ConfigInterface @@ -853,22 +1080,23 @@ diff --git a/vendor/magento/framework/Interception/Config/Config.php b/vendor/ma - $this->cacheManager->clean($this->_cacheId); - $this->generateIntercepted($classDefinitions); - + $this->cacheManager->save($this->_cacheId, $this->_intercepted); -diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php +diff -Nuar a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php --- a/vendor/magento/framework/Lock/Backend/Cache.php +++ b/vendor/magento/framework/Lock/Backend/Cache.php -@@ -25,11 +25,19 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -24,12 +24,20 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ private $cache; - /** ++ /** + * Sign for locks, helps to avoid removing a lock that was created by another client + * -+ * @var string ++ * @string + */ + private $lockSign; + -+ /** + /** * @param FrontendInterface $cache */ public function __construct(FrontendInterface $cache) @@ -878,7 +1106,7 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr } /** -@@ -37,7 +45,24 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -37,7 +45,26 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface */ public function lock(string $name, int $timeout = -1): bool { @@ -890,9 +1118,11 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr + $data = $this->cache->load($this->getIdentifier($name)); + + if (false !== $data) { -+ return false; ++ return false; + } -+ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout * 100); ++ ++ $timeout = $timeout <= 0 ? null : $timeout; ++ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); + + $data = $this->cache->load($this->getIdentifier($name)); + @@ -904,7 +1134,7 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr } /** -@@ -45,7 +70,22 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -45,7 +72,22 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface */ public function unlock(string $name): bool { @@ -928,7 +1158,7 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr } /** -@@ -66,4 +106,27 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -66,4 +108,27 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface { return self::LOCK_PREFIX . $cacheIdentifier; } @@ -956,26 +1186,7 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr + return $sign; + } } -diff --git a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php ---- a/vendor/magento/framework/Lock/Backend/FileLock.php -+++ b/vendor/magento/framework/Lock/Backend/FileLock.php -@@ -91,6 +91,7 @@ class FileLock implements LockManagerInterface - - while (!$this->tryToLock($fileResource)) { - if (!$skipDeadline && $deadline <= microtime(true)) { -+ $this->tryToUnlock($fileResource); - $this->fileDriver->fileClose($fileResource); - return false; - } -@@ -124,6 +125,7 @@ class FileLock implements LockManagerInterface - } else { - $result = true; - } -+ $this->tryToUnlock($fileResource); - $this->fileDriver->fileClose($fileResource); - } - } catch (FileSystemException $exception) { -diff --git a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +diff -Nuar a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php --- a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +++ b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php @@ -260,9 +260,12 @@ class DiCompileCommand extends Command @@ -991,7 +1202,7 @@ diff --git a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setu + }, + $libraryPaths + ); - + $excludedLibraryPaths = [ '#^(?:' . join('|', $libraryPaths) . ')/([\\w]+/)?Test#', @@ -395,7 +398,8 @@ class DiCompileCommand extends Command @@ -1002,49 +1213,9 @@ diff --git a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setu + ], + OperationFactory::APPLICATION_ACTION_LIST_GENERATOR => [], ]; - + return $operations; -diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ---- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php -+++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php -@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; - class Cache implements ConfigOptionsListInterface - { - const INPUT_VALUE_CACHE_REDIS = 'redis'; -- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; -+ const CONFIG_VALUE_CACHE_REDIS = '\\Magento\\Framework\\Cache\\Backend\Redis'; - - const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; - const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; -@@ -282,6 +282,6 @@ class Cache implements ConfigOptionsListInterface - */ - private function generateCachePrefix(): string - { -- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; -+ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; - } - } -diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ---- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php -+++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php -@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; - class PageCache implements ConfigOptionsListInterface - { - const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; -- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; -+ const CONFIG_VALUE_PAGE_CACHE_REDIS = '\\Magento\\Framework\\Cache\\Backend\Redis'; - - const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; - const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; -@@ -284,6 +284,6 @@ class PageCache implements ConfigOptionsListInterface - */ - private function generateCachePrefix(): string - { -- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; -+ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; - } - } -diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +diff -Nuar a/2.3.5/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php new file mode 100644 --- /dev/null +++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php @@ -1090,7 +1261,7 @@ new file mode 100644 + /** + * @inheritDoc + */ -+ public function doOperation(): void ++ public function doOperation() + { + $actionList = $this->moduleReader->getActionFiles(); + $this->configWriter->write( @@ -1102,18 +1273,18 @@ new file mode 100644 + /** + * @inheritDoc + */ -+ public function getName(): string ++ public function getName() + { + return 'App action list generation'; + } +} -diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +diff -Nuar a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php --- a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +++ b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php @@ -5,6 +5,12 @@ */ namespace Magento\Setup\Module\Di\App\Task; - + +use Magento\Setup\Module\Di\App\Task\Operation\AppActionListGenerator; +use Magento\Setup\Module\Di\App\Task\Operation\PluginListGenerator; + @@ -1123,23 +1294,195 @@ diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/s class OperationFactory { /** -@@ -48,6 +54,11 @@ class OperationFactory +@@ -47,6 +53,11 @@ class OperationFactory + */ const APPLICATION_CODE_GENERATOR = 'application_code_generator'; - - /** + ++ /** + * Application action list generator + */ + const APPLICATION_ACTION_LIST_GENERATOR = 'application_action_list_generator'; + -+ /** + /** * Operations definitions * - * @var array @@ -61,6 +72,7 @@ class OperationFactory self::INTERCEPTION_CACHE => \Magento\Setup\Module\Di\App\Task\Operation\InterceptionCache::class, self::REPOSITORY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\RepositoryGenerator::class, self::PROXY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\ProxyGenerator::class, + self::APPLICATION_ACTION_LIST_GENERATOR => AppActionListGenerator::class, ]; + + /** + +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -74,9 +74,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + const STORE_ID = 'store_id'; + + /** +- * @var string ++ * @var string|bool + */ +- protected $_cacheTag = self::CACHE_TAG; ++ protected $_cacheTag = false; + + /** + * @var string +@@ -912,7 +912,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + public function beforeSave() + { +- $this->cleanCache(); + $this->setTypeHasOptions(false); + $this->setTypeHasRequiredOptions(false); + $this->setHasOptions(false); +diff -Nuar a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php +--- a/vendor/magento/module-config/App/Config/Type/System.php ++++ b/vendor/magento/module-config/App/Config/Type/System.php +@@ -20,6 +20,8 @@ use Magento\Framework\Serialize\SerializerInterface; + use Magento\Store\Model\Config\Processor\Fallback; + use Magento\Framework\Encryption\Encryptor; + use Magento\Store\Model\ScopeInterface as StoreScope; ++use Magento\Framework\App\Cache\StateInterface; ++use Magento\Framework\App\Cache\Type\Config; + + /** + * System configuration type +@@ -98,6 +100,12 @@ class System implements ConfigTypeInterface + private $lockQuery; + + /** ++ * @var StateInterface ++ */ ++ private $cacheState; ++ ++ /** ++ * System constructor. + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback +@@ -110,6 +118,7 @@ class System implements ConfigTypeInterface + * @param Encryptor|null $encryptor + * @param LockManagerInterface|null $locker + * @param LockGuardedCacheLoader|null $lockQuery ++ * @param StateInterface|null $cacheState + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -125,7 +134,8 @@ class System implements ConfigTypeInterface + Reader $reader = null, + Encryptor $encryptor = null, + LockManagerInterface $locker = null, +- LockGuardedCacheLoader $lockQuery = null ++ LockGuardedCacheLoader $lockQuery = null, ++ StateInterface $cacheState = null + ) { + $this->postProcessor = $postProcessor; + $this->cache = $cache; +@@ -136,6 +146,8 @@ class System implements ConfigTypeInterface + ?: ObjectManager::getInstance()->get(Encryptor::class); + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); ++ $this->cacheState = $cacheState ++ ?: ObjectManager::getInstance()->get(StateInterface::class); + } /** +@@ -220,6 +232,10 @@ class System implements ConfigTypeInterface + */ + private function loadAllData() + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () { + $cachedData = $this->cache->load($this->configType); + $data = false; +@@ -245,6 +261,10 @@ class System implements ConfigTypeInterface + */ + private function loadDefaultScopeData($scopeType) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + $scopeData = false; +@@ -271,6 +291,10 @@ class System implements ConfigTypeInterface + */ + private function loadScopeData($scopeType, $scopeId) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + $scopeData = false; +@@ -393,6 +417,10 @@ class System implements ConfigTypeInterface + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + }; + ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $cleanAction(); ++ } ++ + $this->lockQuery->lockedCleanData( + self::$lockName, + $cleanAction +diff -Nuar a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml +--- a/vendor/magento/module-config/etc/di.xml ++++ b/vendor/magento/module-config/etc/di.xml +@@ -97,8 +97,6 @@ + <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> +- <argument name="lockTimeout" xsi:type="number">42000</argument> +- <argument name="delayTimeout" xsi:type="number">100</argument> + </arguments> + </virtualType> + +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class Cache implements ConfigOptionsListInterface + { + const INPUT_VALUE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; + const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; +@@ -282,6 +282,6 @@ class Cache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class PageCache implements ConfigOptionsListInterface + { + const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_PAGE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; +@@ -284,6 +284,6 @@ class PageCache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } diff --git a/patches/MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3-p1.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch similarity index 86% rename from patches/MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3-p1.patch rename to patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch index f2bb8553..a46048a7 100644 --- a/patches/MDVA-27538__fix_performance_issue_in_cache_locking_mechanism__2.3.3-p1.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch @@ -1,139 +1,8 @@ -diff --git a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php ---- a/vendor/magento/module-catalog/Model/Product.php -+++ b/vendor/magento/module-catalog/Model/Product.php -@@ -74,9 +74,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - const STORE_ID = 'store_id'; - - /** -- * @var string -+ * @var string|bool - */ -- protected $_cacheTag = self::CACHE_TAG; -+ protected $_cacheTag = false; - - /** - * @var string -@@ -912,7 +912,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - */ - public function beforeSave() - { -- $this->cleanCache(); - $this->setTypeHasOptions(false); - $this->setTypeHasRequiredOptions(false); - $this->setHasOptions(false); -diff --git a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php ---- a/vendor/magento/module-config/App/Config/Type/System.php -+++ b/vendor/magento/module-config/App/Config/Type/System.php -@@ -20,6 +20,8 @@ use Magento\Framework\Serialize\SerializerInterface; - use Magento\Store\Model\Config\Processor\Fallback; - use Magento\Framework\Encryption\Encryptor; - use Magento\Store\Model\ScopeInterface as StoreScope; -+use Magento\Framework\App\Cache\StateInterface; -+use Magento\Framework\App\Cache\Type\Config; - - /** - * System configuration type -@@ -98,6 +100,12 @@ class System implements ConfigTypeInterface - private $lockQuery; - - /** -+ * @var StateInterface -+ */ -+ private $cacheState; -+ -+ /** -+ * System constructor. - * @param ConfigSourceInterface $source - * @param PostProcessorInterface $postProcessor - * @param Fallback $fallback -@@ -110,6 +118,7 @@ class System implements ConfigTypeInterface - * @param Encryptor|null $encryptor - * @param LockManagerInterface|null $locker - * @param LockGuardedCacheLoader|null $lockQuery -+ * @param StateInterface|null $cacheState - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ -@@ -125,7 +134,8 @@ class System implements ConfigTypeInterface - Reader $reader = null, - Encryptor $encryptor = null, - LockManagerInterface $locker = null, -- LockGuardedCacheLoader $lockQuery = null -+ LockGuardedCacheLoader $lockQuery = null, -+ StateInterface $cacheState = null - ) { - $this->postProcessor = $postProcessor; - $this->cache = $cache; -@@ -136,6 +146,8 @@ class System implements ConfigTypeInterface - ?: ObjectManager::getInstance()->get(Encryptor::class); - $this->lockQuery = $lockQuery - ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); -+ $this->cacheState = $cacheState -+ ?: ObjectManager::getInstance()->get(StateInterface::class); - } - - /** -@@ -220,6 +232,10 @@ class System implements ConfigTypeInterface - */ - private function loadAllData() - { -+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { -+ return $this->readData(); -+ } -+ - $loadAction = function () { - $cachedData = $this->cache->load($this->configType); - $data = false; -@@ -245,6 +261,10 @@ class System implements ConfigTypeInterface - */ - private function loadDefaultScopeData($scopeType) - { -+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { -+ return $this->readData(); -+ } -+ - $loadAction = function () use ($scopeType) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType); - $scopeData = false; -@@ -271,6 +291,10 @@ class System implements ConfigTypeInterface - */ - private function loadScopeData($scopeType, $scopeId) - { -+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { -+ return $this->readData(); -+ } -+ - $loadAction = function () use ($scopeType, $scopeId) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); - $scopeData = false; -@@ -393,6 +417,10 @@ class System implements ConfigTypeInterface - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); - }; - -+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { -+ return $cleanAction(); -+ } -+ - $this->lockQuery->lockedCleanData( - self::$lockName, - $cleanAction -diff --git a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml ---- a/vendor/magento/module-config/etc/di.xml -+++ b/vendor/magento/module-config/etc/di.xml -@@ -97,8 +97,6 @@ - <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> - <arguments> - <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> -- <argument name="lockTimeout" xsi:type="number">42000</argument> -- <argument name="delayTimeout" xsi:type="number">100</argument> - </arguments> - </virtualType> - -diff --git a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module-eav/Model/Config.php +diff -Nuar a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module-eav/Model/Config.php --- a/vendor/magento/module-eav/Model/Config.php +++ b/vendor/magento/module-eav/Model/Config.php -@@ -125,11 +125,11 @@ class Config - +@@ -157,12 +157,12 @@ class Config + /** * @param \Magento\Framework\App\CacheInterface $cache - * @param \Magento\Eav\Model\Entity\TypeFactory $entityTypeFactory @@ -143,14 +12,51 @@ diff --git a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module- * @param \Magento\Framework\App\Cache\StateInterface $cacheState * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param SerializerInterface $serializer +- * @param ScopeConfigInterface $scopeConfig + * @param SerializerInterface|null $serializer ++ * @param ScopeConfigInterface|null $scopeConfig + * @param array $attributesForPreload * @codeCoverageIgnore */ - public function __construct( -@@ -336,7 +336,9 @@ class Config +@@ -245,8 +245,8 @@ class Config + /** + * Associate object with identifier + * +- * @param mixed $obj +- * @param mixed $id ++ * @param mixed $obj ++ * @param mixed $id + * @return void + * @codeCoverageIgnore + */ +@@ -271,8 +271,8 @@ class Config + /** + * Specify reference for entity type id + * +- * @param int $id +- * @param string $code ++ * @param int $id ++ * @param string $code + * @return $this + * @codeCoverageIgnore + */ +@@ -296,9 +296,9 @@ class Config + /** + * Specify reference between entity attribute id and attribute code + * +- * @param int $id +- * @param string $code +- * @param string $entityTypeCode ++ * @param int $id ++ * @param string $code ++ * @param string $entityTypeCode + * @return $this + */ + protected function _addAttributeReference($id, $code, $entityTypeCode) +@@ -374,7 +374,9 @@ class Config } \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); - + - if ($this->isCacheEnabled() && ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID))) { + if ($this->isCacheEnabled() && + ($cache = $this->_cache->load(self::ENTITIES_CACHE_ID)) @@ -158,13 +64,48 @@ diff --git a/vendor/magento/module-eav/Model/Config.php b/vendor/magento/module- $this->_entityTypeData = $this->serializer->unserialize($cache); foreach ($this->_entityTypeData as $typeCode => $data) { $typeId = $data['entity_type_id']; -diff --git a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +@@ -522,9 +524,9 @@ class Config + /** + * Get attribute by code for entity type + * +- * @param mixed $entityType +- * @param mixed $code +- * @return AbstractAttribute ++ * @param mixed $entityType ++ * @param mixed $code ++ * @return AbstractAttribute + * @throws LocalizedException + */ + public function getAttribute($entityType, $code) +@@ -737,8 +739,8 @@ class Config + /** + * Get all entity type attributes + * +- * @param int|string|Type $entityType +- * @param \Magento\Framework\DataObject|null $object ++ * @param int|string|Type $entityType ++ * @param \Magento\Framework\DataObject|null $object + * @return AbstractAttribute[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) +@@ -822,6 +824,10 @@ class Config + $fullAttributeData = array_key_exists('is_required', $attributeData); + + if ($existsFullAttribute || (!$existsFullAttribute && !$fullAttributeData)) { ++ $scopeIsRequired = $attributeData['scope_is_required'] ?? null; ++ if ($scopeIsRequired !== null) { ++ $attribute->setData('scope_is_required', $scopeIsRequired); ++ } + return $attribute; + } + } +diff -Nuar a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php --- a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php +++ b/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set.php @@ -6,6 +6,9 @@ - + namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; - + +/** + * Basic implementation for attribute sets + */ @@ -173,7 +114,7 @@ diff --git a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set. /** @@ -24,8 +27,6 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb protected $eavConfig; - + /** - * Constructor - * @@ -182,14 +123,14 @@ diff --git a/vendor/magento/module-eav/Model/ResourceModel/Entity/Attribute/Set. * @param \Magento\Eav\Model\Config $eavConfig @@ -54,7 +55,7 @@ class Set extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb } - + /** - * Perform actions after object save + * Perform actions after object save. * * @param \Magento\Framework\Model\AbstractModel $object * @return $this -diff --git a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/etc/di.xml +diff -Nuar a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/etc/di.xml --- a/vendor/magento/module-eav/etc/di.xml +++ b/vendor/magento/module-eav/etc/di.xml @@ -209,4 +209,14 @@ @@ -207,7 +148,7 @@ diff --git a/vendor/magento/module-eav/etc/di.xml b/vendor/magento/module-eav/et + </arguments> + </type> </config> -diff --git a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-theme/etc/di.xml +diff -Nuar a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-theme/etc/di.xml --- a/vendor/magento/module-theme/etc/di.xml +++ b/vendor/magento/module-theme/etc/di.xml @@ -285,4 +285,24 @@ @@ -235,10 +176,10 @@ diff --git a/vendor/magento/module-theme/etc/di.xml b/vendor/magento/module-them + </arguments> + </type> </config> -diff --git a/app/etc/di.xml b/app/etc/di.xml +diff -Nuar a/app/etc/di.xml b/app/etc/di.xml --- a/app/etc/di.xml +++ b/app/etc/di.xml -@@ -1779,8 +1779,6 @@ +@@ -1780,8 +1780,6 @@ <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> <arguments> <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> @@ -247,9 +188,9 @@ diff --git a/app/etc/di.xml b/app/etc/di.xml </arguments> </type> <preference for="Magento\Framework\HTTP\AsyncClientInterface" type="Magento\Framework\HTTP\AsyncClient\GuzzleAsyncClient" /> -@@ -1795,4 +1793,14 @@ - <preference for="Magento\Framework\Mail\MimePartInterface" - type="Magento\Framework\Mail\MimePart" /> +@@ -1800,4 +1798,14 @@ + </type> + <preference for="Magento\Framework\GraphQl\Query\ErrorHandlerInterface" type="Magento\Framework\GraphQl\Query\ErrorHandler"/> <preference for="Magento\Framework\Filter\VariableResolverInterface" type="Magento\Framework\Filter\VariableResolver\StrategyResolver"/> + <virtualType name="configured_block_cache" type="Magento\Framework\App\Cache"> + <arguments> @@ -258,23 +199,23 @@ diff --git a/app/etc/di.xml b/app/etc/di.xml + </virtualType> + <type name="Magento\Framework\View\Element\Context"> + <arguments> -+ <argument name="cache" xsi:type="object">configured_block_cache</argument> ++ <argument name="cache" xsi:type="object">configured_block_cache</argument> + </arguments> + </type> </config> -diff --git a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/App/Cache.php +diff -Nuar a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/App/Cache.php --- a/vendor/magento/framework/App/Cache.php +++ b/vendor/magento/framework/App/Cache.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ - + -/** - * System cache model - * support id and tags prefix support, - */ namespace Magento\Framework\App; - + +/** + * System cache model support id and tags prefix support. + */ @@ -283,7 +224,7 @@ diff --git a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/A /** @@ -30,12 +29,13 @@ class Cache implements CacheInterface protected $_frontend; - + /** - * @param \Magento\Framework\App\Cache\Frontend\Pool $frontendPool + * @param Cache\Frontend\Pool $frontendPool @@ -296,35 +237,35 @@ diff --git a/vendor/magento/framework/App/Cache.php b/vendor/magento/framework/A - $this->_frontend = $frontendPool->get($this->_frontendIdentifier); + $this->_frontend = $frontendPool->get($cacheIdentifier ?? $this->_frontendIdentifier); } - + /** -diff --git a/vendor/magento/framework/App/Cache/Frontend/Pool.php b/vendor/magento/framework/App/Cache/Frontend/Pool.php +diff -Nuar a/vendor/magento/framework/App/Cache/Frontend/Pool.php b/vendor/magento/framework/App/Cache/Frontend/Pool.php --- a/vendor/magento/framework/App/Cache/Frontend/Pool.php +++ b/vendor/magento/framework/App/Cache/Frontend/Pool.php -@@ -147,6 +147,15 @@ class Pool implements \Iterator +@@ -152,6 +152,15 @@ class Pool implements \Iterator if (isset($this->_instances[$identifier])) { return $this->_instances[$identifier]; } - throw new \InvalidArgumentException("Cache frontend '{$identifier}' is not recognized."); + + if (!isset($this->_instances[self::DEFAULT_FRONTEND_ID])) { -+ throw new \InvalidArgumentException(sprintf( -+ 'Cache frontend \'%s\' is not recognized. As well as %s cache is not configured', -+ $identifier, -+ self::DEFAULT_FRONTEND_ID -+ )); ++ throw new \InvalidArgumentException( ++ "Cache frontend '{$identifier}' is not recognized. As well as " . ++ self::DEFAULT_FRONTEND_ID . ++ "cache is not configured" ++ ); + } + + return $this->_instances[self::DEFAULT_FRONTEND_ID]; } } -diff --git a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php +diff -Nuar a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento/framework/App/Router/ActionList.php --- a/vendor/magento/framework/App/Router/ActionList.php +++ b/vendor/magento/framework/App/Router/ActionList.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\App\Router; - + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\State; use Magento\Framework\Serialize\SerializerInterface; @@ -361,8 +302,8 @@ diff --git a/vendor/magento/framework/App/Router/ActionList.php b/vendor/magento + } } } - -diff --git a/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php + +diff -Nuar a/2.3.5/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php new file mode 100644 --- /dev/null +++ b/vendor/magento/framework/Cache/Backend/Redis.php @@ -406,7 +347,7 @@ new file mode 100644 + /** + * Load value with given id from cache + * -+ * @param string $id Cache id ++ * @param string $id Cache id + * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested + * @return bool|string + */ @@ -450,7 +391,7 @@ new file mode 100644 + return true; + } +} -diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php --- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php @@ -9,8 +9,10 @@ namespace Magento\Framework\Cache\Backend; @@ -468,7 +409,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -36,11 +38,15 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache protected $cacheInvalidationTime; - + /** - * {@inheritdoc} + * Suffix for hash to compare data version in cache storage. @@ -485,7 +426,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php 'remote_backend_autoload' => true, 'remote_backend_options' => [], @@ -52,6 +58,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache - + /** * @param array $options + * @throws \Zend_Cache_Exception @@ -494,7 +435,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -97,76 +104,137 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * Update remote cache status info + * @inheritdoc @@ -518,7 +459,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php - $this->cacheInvalidationTime = null; + return \hash('sha256', $data); } - + /** - * {@inheritdoc} + * Load data version by id from remote. @@ -534,7 +475,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $id . self::HASH_SUFFIX + ); } - + /** - * {@inheritdoc} + * Save new data version to remote. @@ -556,7 +497,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + * @param string $id + * @return bool + */ -+ private function removeRemoteDataVersion($id): bool ++ private function removeRemoteDataVersion($id) + { + return $this->remote->remove($id . self::HASH_SUFFIX); + } @@ -596,7 +537,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + + return $localData; } - + /** - * {@inheritdoc} + * @inheritdoc @@ -606,7 +547,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php - return $this->local->test($id); + return $this->local->test($id) ?? $this->remote->test($id); } - + /** - * {@inheritdoc} + * @inheritdoc @@ -626,7 +567,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + + return $this->local->save($dataToSave, $id, [], $specificLifetime); } - + /** - * {@inheritdoc} + * @inheritdoc @@ -639,7 +580,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $this->remote->remove($id) && + $this->local->remove($id); } - + /** - * {@inheritdoc} + * @inheritdoc @@ -650,7 +591,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php - return $this->local->clean($mode, $tags); + return $this->remote->clean($mode, $tags); } - + /** - * {@inheritdoc} + * @inheritdoc @@ -659,7 +600,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -174,7 +242,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -668,7 +609,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -182,7 +250,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -677,7 +618,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -190,7 +258,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -686,7 +627,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -198,7 +266,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -695,7 +636,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -206,7 +274,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -704,7 +645,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -214,7 +282,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -713,7 +654,7 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -222,7 +290,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc @@ -722,20 +663,21 @@ diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php { @@ -230,7 +298,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache } - + /** - * {@inheritdoc} + * @inheritdoc */ public function getCapabilities() { -diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php --- a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +++ b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php -@@ -37,18 +37,43 @@ class LockGuardedCacheLoader +@@ -36,19 +36,43 @@ class LockGuardedCacheLoader + */ private $delayTimeout; - /** ++ /** + * Timeout for information to be collected and saved. + * If timeout passed that means that data cannot be saved right now. + * And we will just return collected data. @@ -753,8 +695,7 @@ diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ + */ + private $minimalDelayTimeout; + -+ /** -+ * LockGuardedCacheLoader constructor. + /** * @param LockManagerInterface $locker * @param int $lockTimeout * @param int $delayTimeout @@ -777,7 +718,7 @@ diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ } /** -@@ -67,25 +92,25 @@ class LockGuardedCacheLoader +@@ -67,25 +91,25 @@ class LockGuardedCacheLoader callable $dataSaver ) { $cachedData = $dataLoader(); //optimistic read @@ -813,7 +754,7 @@ diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ $cachedData = $dataLoader(); } } -@@ -103,14 +128,21 @@ class LockGuardedCacheLoader +@@ -103,14 +127,21 @@ class LockGuardedCacheLoader public function lockedCleanData(string $lockName, callable $dataCleaner) { while ($this->locker->isLocked($lockName)) { @@ -843,7 +784,7 @@ diff --git a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/ + return rand($this->minimalDelayTimeout, $this->delayTimeout); } } -diff --git a/vendor/magento/framework/Interception/Config/Config.php b/vendor/magento/framework/Interception/Config/Config.php +diff -Nuar a/vendor/magento/framework/Interception/Config/Config.php b/vendor/magento/framework/Interception/Config/Config.php --- a/vendor/magento/framework/Interception/Config/Config.php +++ b/vendor/magento/framework/Interception/Config/Config.php @@ -187,8 +187,6 @@ class Config implements \Magento\Framework\Interception\ConfigInterface @@ -853,22 +794,23 @@ diff --git a/vendor/magento/framework/Interception/Config/Config.php b/vendor/ma - $this->cacheManager->clean($this->_cacheId); - $this->generateIntercepted($classDefinitions); - + $this->cacheManager->save($this->_cacheId, $this->_intercepted); -diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php +diff -Nuar a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php --- a/vendor/magento/framework/Lock/Backend/Cache.php +++ b/vendor/magento/framework/Lock/Backend/Cache.php -@@ -25,11 +25,19 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -24,12 +24,20 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ private $cache; - /** ++ /** + * Sign for locks, helps to avoid removing a lock that was created by another client + * -+ * @var string ++ * @string + */ + private $lockSign; + -+ /** + /** * @param FrontendInterface $cache */ public function __construct(FrontendInterface $cache) @@ -878,7 +820,7 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr } /** -@@ -37,7 +45,24 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -37,7 +45,26 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface */ public function lock(string $name, int $timeout = -1): bool { @@ -890,9 +832,11 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr + $data = $this->cache->load($this->getIdentifier($name)); + + if (false !== $data) { -+ return false; ++ return false; + } -+ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout * 100); ++ ++ $timeout = $timeout <= 0 ? null : $timeout; ++ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); + + $data = $this->cache->load($this->getIdentifier($name)); + @@ -904,7 +848,7 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr } /** -@@ -45,7 +70,22 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -45,7 +72,22 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface */ public function unlock(string $name): bool { @@ -928,7 +872,7 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr } /** -@@ -66,4 +106,27 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface +@@ -66,4 +108,27 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface { return self::LOCK_PREFIX . $cacheIdentifier; } @@ -956,26 +900,7 @@ diff --git a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/fr + return $sign; + } } -diff --git a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php ---- a/vendor/magento/framework/Lock/Backend/FileLock.php -+++ b/vendor/magento/framework/Lock/Backend/FileLock.php -@@ -91,6 +91,7 @@ class FileLock implements LockManagerInterface - - while (!$this->tryToLock($fileResource)) { - if (!$skipDeadline && $deadline <= microtime(true)) { -+ $this->tryToUnlock($fileResource); - $this->fileDriver->fileClose($fileResource); - return false; - } -@@ -124,6 +125,7 @@ class FileLock implements LockManagerInterface - } else { - $result = true; - } -+ $this->tryToUnlock($fileResource); - $this->fileDriver->fileClose($fileResource); - } - } catch (FileSystemException $exception) { -diff --git a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +diff -Nuar a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php --- a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +++ b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php @@ -260,9 +260,12 @@ class DiCompileCommand extends Command @@ -991,7 +916,7 @@ diff --git a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setu + }, + $libraryPaths + ); - + $excludedLibraryPaths = [ '#^(?:' . join('|', $libraryPaths) . ')/([\\w]+/)?Test#', @@ -395,7 +398,8 @@ class DiCompileCommand extends Command @@ -1002,49 +927,9 @@ diff --git a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setu + ], + OperationFactory::APPLICATION_ACTION_LIST_GENERATOR => [], ]; - + return $operations; -diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ---- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php -+++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php -@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; - class Cache implements ConfigOptionsListInterface - { - const INPUT_VALUE_CACHE_REDIS = 'redis'; -- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; -+ const CONFIG_VALUE_CACHE_REDIS = '\\Magento\\Framework\\Cache\\Backend\Redis'; - - const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; - const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; -@@ -282,6 +282,6 @@ class Cache implements ConfigOptionsListInterface - */ - private function generateCachePrefix(): string - { -- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; -+ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; - } - } -diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ---- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php -+++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php -@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; - class PageCache implements ConfigOptionsListInterface - { - const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; -- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; -+ const CONFIG_VALUE_PAGE_CACHE_REDIS = '\\Magento\\Framework\\Cache\\Backend\Redis'; - - const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; - const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; -@@ -284,6 +284,6 @@ class PageCache implements ConfigOptionsListInterface - */ - private function generateCachePrefix(): string - { -- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; -+ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; - } - } -diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php +diff -Nuar a/2.3.5/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php new file mode 100644 --- /dev/null +++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/AppActionListGenerator.php @@ -1090,7 +975,7 @@ new file mode 100644 + /** + * @inheritDoc + */ -+ public function doOperation(): void ++ public function doOperation() + { + $actionList = $this->moduleReader->getActionFiles(); + $this->configWriter->write( @@ -1102,18 +987,18 @@ new file mode 100644 + /** + * @inheritDoc + */ -+ public function getName(): string ++ public function getName() + { + return 'App action list generation'; + } +} -diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +diff -Nuar a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php --- a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +++ b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php @@ -5,6 +5,12 @@ */ namespace Magento\Setup\Module\Di\App\Task; - + +use Magento\Setup\Module\Di\App\Task\Operation\AppActionListGenerator; +use Magento\Setup\Module\Di\App\Task\Operation\PluginListGenerator; + @@ -1123,23 +1008,204 @@ diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/s class OperationFactory { /** -@@ -48,6 +54,11 @@ class OperationFactory +@@ -47,6 +53,11 @@ class OperationFactory + */ const APPLICATION_CODE_GENERATOR = 'application_code_generator'; - - /** + ++ /** + * Application action list generator + */ + const APPLICATION_ACTION_LIST_GENERATOR = 'application_action_list_generator'; + -+ /** + /** * Operations definitions * - * @var array @@ -61,6 +72,7 @@ class OperationFactory self::INTERCEPTION_CACHE => \Magento\Setup\Module\Di\App\Task\Operation\InterceptionCache::class, self::REPOSITORY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\RepositoryGenerator::class, self::PROXY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\ProxyGenerator::class, + self::APPLICATION_ACTION_LIST_GENERATOR => AppActionListGenerator::class, ]; + + /** + +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -72,9 +72,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + const STORE_ID = 'store_id'; + + /** +- * @var string ++ * @var string|bool + */ +- protected $_cacheTag = self::CACHE_TAG; ++ protected $_cacheTag = false; + + /** + * @var string +@@ -874,7 +874,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + public function beforeSave() + { +- $this->cleanCache(); + $this->setTypeHasOptions(false); + $this->setTypeHasRequiredOptions(false); + $this->setHasOptions(false); +@@ -1164,7 +1163,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + /** + * Get formatted by currency product price + * +- * @return array|double ++ * @return array|double + * @since 102.0.6 + */ + public function getFormattedPrice() +diff -Nuar a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php +--- a/vendor/magento/module-config/App/Config/Type/System.php ++++ b/vendor/magento/module-config/App/Config/Type/System.php +@@ -20,6 +20,8 @@ use Magento\Framework\Serialize\SerializerInterface; + use Magento\Store\Model\Config\Processor\Fallback; + use Magento\Framework\Encryption\Encryptor; + use Magento\Store\Model\ScopeInterface as StoreScope; ++use Magento\Framework\App\Cache\StateInterface; ++use Magento\Framework\App\Cache\Type\Config; + + /** + * System configuration type +@@ -98,6 +100,12 @@ class System implements ConfigTypeInterface + private $lockQuery; /** ++ * @var StateInterface ++ */ ++ private $cacheState; ++ ++ /** ++ * System constructor. + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback +@@ -110,6 +118,7 @@ class System implements ConfigTypeInterface + * @param Encryptor|null $encryptor + * @param LockManagerInterface|null $locker + * @param LockGuardedCacheLoader|null $lockQuery ++ * @param StateInterface|null $cacheState + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -125,7 +134,8 @@ class System implements ConfigTypeInterface + Reader $reader = null, + Encryptor $encryptor = null, + LockManagerInterface $locker = null, +- LockGuardedCacheLoader $lockQuery = null ++ LockGuardedCacheLoader $lockQuery = null, ++ StateInterface $cacheState = null + ) { + $this->postProcessor = $postProcessor; + $this->cache = $cache; +@@ -136,6 +146,8 @@ class System implements ConfigTypeInterface + ?: ObjectManager::getInstance()->get(Encryptor::class); + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); ++ $this->cacheState = $cacheState ++ ?: ObjectManager::getInstance()->get(StateInterface::class); + } + + /** +@@ -220,6 +232,10 @@ class System implements ConfigTypeInterface + */ + private function loadAllData() + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () { + $cachedData = $this->cache->load($this->configType); + $data = false; +@@ -245,6 +261,10 @@ class System implements ConfigTypeInterface + */ + private function loadDefaultScopeData($scopeType) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + $scopeData = false; +@@ -271,6 +291,10 @@ class System implements ConfigTypeInterface + */ + private function loadScopeData($scopeType, $scopeId) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + $scopeData = false; +@@ -393,6 +417,10 @@ class System implements ConfigTypeInterface + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + }; + ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $cleanAction(); ++ } ++ + $this->lockQuery->lockedCleanData( + self::$lockName, + $cleanAction +diff -Nuar a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml +--- a/vendor/magento/module-config/etc/di.xml ++++ b/vendor/magento/module-config/etc/di.xml +@@ -97,8 +97,6 @@ + <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> +- <argument name="lockTimeout" xsi:type="number">42000</argument> +- <argument name="delayTimeout" xsi:type="number">100</argument> + </arguments> + </virtualType> + +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class Cache implements ConfigOptionsListInterface + { + const INPUT_VALUE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; + const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; +@@ -282,6 +282,6 @@ class Cache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class PageCache implements ConfigOptionsListInterface + { + const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_PAGE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; +@@ -284,6 +284,6 @@ class PageCache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } diff --git a/patches/MCLOUD-6211__redis_improvement_patches__2.3.5.patch b/patches/MCLOUD-6211__redis_improvement_patches__2.3.5.patch new file mode 100644 index 00000000..4e80e2ab --- /dev/null +++ b/patches/MCLOUD-6211__redis_improvement_patches__2.3.5.patch @@ -0,0 +1,382 @@ +diff -Nuar a/vendor/magento/module-catalog/Model/Product.php b/vendor/magento/module-catalog/Model/Product.php +--- a/vendor/magento/module-catalog/Model/Product.php ++++ b/vendor/magento/module-catalog/Model/Product.php +@@ -72,9 +72,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + const STORE_ID = 'store_id'; + + /** +- * @var string ++ * @var string|bool + */ +- protected $_cacheTag = self::CACHE_TAG; ++ protected $_cacheTag = false; + + /** + * @var string +@@ -874,7 +874,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements + */ + public function beforeSave() + { +- $this->cleanCache(); + $this->setTypeHasOptions(false); + $this->setTypeHasRequiredOptions(false); + $this->setHasOptions(false); +diff -Nuar a/vendor/magento/module-config/App/Config/Type/System.php b/vendor/magento/module-config/App/Config/Type/System.php +--- a/vendor/magento/module-config/App/Config/Type/System.php ++++ b/vendor/magento/module-config/App/Config/Type/System.php +@@ -20,6 +20,8 @@ use Magento\Framework\Serialize\SerializerInterface; + use Magento\Store\Model\Config\Processor\Fallback; + use Magento\Framework\Encryption\Encryptor; + use Magento\Store\Model\ScopeInterface as StoreScope; ++use Magento\Framework\App\Cache\StateInterface; ++use Magento\Framework\App\Cache\Type\Config; + + /** + * System configuration type +@@ -98,6 +100,12 @@ class System implements ConfigTypeInterface + private $lockQuery; + + /** ++ * @var StateInterface ++ */ ++ private $cacheState; ++ ++ /** ++ * System constructor. + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback +@@ -110,6 +118,7 @@ class System implements ConfigTypeInterface + * @param Encryptor|null $encryptor + * @param LockManagerInterface|null $locker + * @param LockGuardedCacheLoader|null $lockQuery ++ * @param StateInterface|null $cacheState + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +@@ -125,7 +134,8 @@ class System implements ConfigTypeInterface + Reader $reader = null, + Encryptor $encryptor = null, + LockManagerInterface $locker = null, +- LockGuardedCacheLoader $lockQuery = null ++ LockGuardedCacheLoader $lockQuery = null, ++ StateInterface $cacheState = null + ) { + $this->postProcessor = $postProcessor; + $this->cache = $cache; +@@ -136,6 +146,8 @@ class System implements ConfigTypeInterface + ?: ObjectManager::getInstance()->get(Encryptor::class); + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); ++ $this->cacheState = $cacheState ++ ?: ObjectManager::getInstance()->get(StateInterface::class); + } + + /** +@@ -220,6 +232,10 @@ class System implements ConfigTypeInterface + */ + private function loadAllData() + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () { + $cachedData = $this->cache->load($this->configType); + $data = false; +@@ -245,6 +261,10 @@ class System implements ConfigTypeInterface + */ + private function loadDefaultScopeData($scopeType) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + $scopeData = false; +@@ -271,6 +291,10 @@ class System implements ConfigTypeInterface + */ + private function loadScopeData($scopeType, $scopeId) + { ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $this->readData(); ++ } ++ + $loadAction = function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + $scopeData = false; +@@ -393,6 +417,10 @@ class System implements ConfigTypeInterface + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + }; + ++ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { ++ return $cleanAction(); ++ } ++ + $this->lockQuery->lockedCleanData( + self::$lockName, + $cleanAction +diff -Nuar a/vendor/magento/module-config/etc/di.xml b/vendor/magento/module-config/etc/di.xml +--- a/vendor/magento/module-config/etc/di.xml ++++ b/vendor/magento/module-config/etc/di.xml +@@ -97,8 +97,6 @@ + <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> +- <argument name="lockTimeout" xsi:type="number">42000</argument> +- <argument name="delayTimeout" xsi:type="number">100</argument> + </arguments> + </virtualType> + +diff -Nuar a/app/etc/di.xml b/app/etc/di.xml +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -1780,8 +1780,6 @@ + <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> +- <argument name="lockTimeout" xsi:type="number">10000</argument> +- <argument name="delayTimeout" xsi:type="number">20</argument> + </arguments> + </type> + <preference for="Magento\Framework\HTTP\AsyncClientInterface" type="Magento\Framework\HTTP\AsyncClient\GuzzleAsyncClient" /> +diff -Nuar a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +--- a/vendor/magento/framework/Cache/LockGuardedCacheLoader.php ++++ b/vendor/magento/framework/Cache/LockGuardedCacheLoader.php +@@ -48,22 +48,31 @@ class LockGuardedCacheLoader + private $loadTimeout; + + /** +- * LockGuardedCacheLoader constructor. ++ * Minimal delay timeout in ms. ++ * ++ * @var int ++ */ ++ private $minimalDelayTimeout; ++ ++ /** + * @param LockManagerInterface $locker + * @param int $lockTimeout + * @param int $delayTimeout + * @param int $loadTimeout ++ * @param int $minimalDelayTimeout + */ + public function __construct( + LockManagerInterface $locker, + int $lockTimeout = 10000, + int $delayTimeout = 20, +- int $loadTimeout = 10000 ++ int $loadTimeout = 10000, ++ int $minimalDelayTimeout = 5 + ) { + $this->locker = $locker; + $this->lockTimeout = $lockTimeout; + $this->delayTimeout = $delayTimeout; + $this->loadTimeout = $loadTimeout; ++ $this->minimalDelayTimeout = $minimalDelayTimeout; + } + + /** +@@ -82,7 +91,7 @@ class LockGuardedCacheLoader + callable $dataSaver + ) { + $cachedData = $dataLoader(); //optimistic read +- $deadline = microtime(true) + $this->loadTimeout; ++ $deadline = microtime(true) + $this->loadTimeout / 100; + + while ($cachedData === false) { + if ($deadline <= microtime(true)) { +@@ -100,7 +109,7 @@ class LockGuardedCacheLoader + } + + if ($cachedData === false) { +- usleep($this->delayTimeout * 1000); ++ usleep($this->getLookupTimeout() * 1000); + $cachedData = $dataLoader(); + } + } +@@ -118,14 +127,21 @@ class LockGuardedCacheLoader + public function lockedCleanData(string $lockName, callable $dataCleaner) + { + while ($this->locker->isLocked($lockName)) { +- usleep($this->delayTimeout * 1000); +- } +- try { +- if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { +- $dataCleaner(); +- } +- } finally { +- $this->locker->unlock($lockName); ++ usleep($this->getLookupTimeout() * 1000); + } ++ ++ $dataCleaner(); ++ } ++ ++ /** ++ * Delay will be applied as rand($minimalDelayTimeout, $delayTimeout). ++ * This helps to desynchronize multiple clients trying ++ * to acquire the lock for the same resource at the same time ++ * ++ * @return int ++ */ ++ private function getLookupTimeout() ++ { ++ return rand($this->minimalDelayTimeout, $this->delayTimeout); + } + } +diff -Nuar a/vendor/magento/framework/Lock/Backend/Cache.php b/vendor/magento/framework/Lock/Backend/Cache.php +--- a/vendor/magento/framework/Lock/Backend/Cache.php ++++ b/vendor/magento/framework/Lock/Backend/Cache.php +@@ -24,12 +24,20 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + private $cache; + ++ /** ++ * Sign for locks, helps to avoid removing a lock that was created by another client ++ * ++ * @string ++ */ ++ private $lockSign; ++ + /** + * @param FrontendInterface $cache + */ + public function __construct(FrontendInterface $cache) + { + $this->cache = $cache; ++ $this->lockSign = $this->generateLockSign(); + } + + /** +@@ -37,11 +45,26 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + public function lock(string $name, int $timeout = -1): bool + { +- if ((bool)$this->cache->test($this->getIdentifier($name))) { ++ if (empty($this->lockSign)) { ++ $this->lockSign = $this->generateLockSign(); ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false !== $data) { + return false; + } + +- return $this->cache->save('1', $this->getIdentifier($name), [], $timeout); ++ $timeout = $timeout <= 0 ? null : $timeout; ++ $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if ($data === $this->lockSign) { ++ return true; ++ } ++ ++ return false; + } + + /** +@@ -49,7 +72,22 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + */ + public function unlock(string $name): bool + { +- return $this->cache->remove($this->getIdentifier($name)); ++ if (empty($this->lockSign)) { ++ return false; ++ } ++ ++ $data = $this->cache->load($this->getIdentifier($name)); ++ ++ if (false === $data) { ++ return false; ++ } ++ ++ $removeResult = false; ++ if ($data === $this->lockSign) { ++ $removeResult = (bool)$this->cache->remove($this->getIdentifier($name)); ++ } ++ ++ return $removeResult; + } + + /** +@@ -70,4 +108,27 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface + { + return self::LOCK_PREFIX . $cacheIdentifier; + } ++ ++ /** ++ * Function that generates lock sign that helps to avoid removing a lock that was created by another client. ++ * ++ * @return string ++ */ ++ private function generateLockSign() ++ { ++ $sign = implode( ++ '-', ++ [ ++ \getmypid(), \crc32(\gethostname()) ++ ] ++ ); ++ ++ try { ++ $sign .= '-' . \bin2hex(\random_bytes(4)); ++ } catch (\Exception $e) { ++ $sign .= '-' . \uniqid('-uniqid-'); ++ } ++ ++ return $sign; ++ } + } +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class Cache implements ConfigOptionsListInterface + { + const INPUT_VALUE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_CACHE_BACKEND = 'cache-backend'; + const INPUT_KEY_CACHE_BACKEND_REDIS_SERVER = 'cache-backend-redis-server'; +@@ -233,7 +233,7 @@ class Cache implements ConfigOptionsListInterface + self::CONFIG_PATH_CACHE_BACKEND_DATABASE, + $this->getDefaultConfigValue(self::INPUT_KEY_CACHE_BACKEND_REDIS_DATABASE) + ); +- ++ + $config['password'] = isset($options[self::INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD]) + ? $options[self::INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD] + : $deploymentConfig->get( +@@ -282,6 +282,6 @@ class Cache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } +diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +--- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php ++++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +@@ -20,7 +20,7 @@ use Magento\Setup\Validator\RedisConnectionValidator; + class PageCache implements ConfigOptionsListInterface + { + const INPUT_VALUE_PAGE_CACHE_REDIS = 'redis'; +- const CONFIG_VALUE_PAGE_CACHE_REDIS = 'Cm_Cache_Backend_Redis'; ++ const CONFIG_VALUE_PAGE_CACHE_REDIS = \Magento\Framework\Cache\Backend\Redis::class; + + const INPUT_KEY_PAGE_CACHE_BACKEND = 'page-cache'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; +@@ -284,6 +284,6 @@ class PageCache implements ConfigOptionsListInterface + */ + private function generateCachePrefix(): string + { +- return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; ++ return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; + } + } diff --git a/patches/MCLOUD_6139__improvement_flock_locks__2.3.2.patch b/patches/MCLOUD_6139__improvement_flock_locks__2.3.2.patch new file mode 100644 index 00000000..785bec6a --- /dev/null +++ b/patches/MCLOUD_6139__improvement_flock_locks__2.3.2.patch @@ -0,0 +1,19 @@ +diff -Nuar a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php +--- a/vendor/magento/framework/Lock/Backend/FileLock.php ++++ b/vendor/magento/framework/Lock/Backend/FileLock.php +@@ -91,6 +91,7 @@ class FileLock implements LockManagerInterface + + while (!$this->tryToLock($fileResource)) { + if (!$skipDeadline && $deadline <= microtime(true)) { ++ $this->tryToUnlock($fileResource); + $this->fileDriver->fileClose($fileResource); + return false; + } +@@ -124,6 +125,7 @@ class FileLock implements LockManagerInterface + } else { + $result = true; + } ++ $this->tryToUnlock($fileResource); + $this->fileDriver->fileClose($fileResource); + } + } catch (FileSystemException $exception) { From aaaa16d88e9e9353dc7af1ca1dea4112d43a16b1 Mon Sep 17 00:00:00 2001 From: Nadiya Syvokonenko <nsyvokonenko@magento.com> Date: Thu, 30 Jul 2020 12:28:12 -0500 Subject: [PATCH 049/157] MCLOUD-6588: Remove amazon patches from magento2-base component (#63) --- patches.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/patches.json b/patches.json index a8d786da..56ccd13b 100644 --- a/patches.json +++ b/patches.json @@ -146,9 +146,6 @@ "2.2.0 - 2.2.5": "MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.0.patch", "2.2.6": "MAGECLOUD-2509__remove_permission_check_for_console_application__2.2.6.patch" }, - "Fix for DI compilation with Amazon_Payment module": { - "2.2.6": "MAGECLOUD-2850__fix_amazon_payment_module__2.2.6.patch" - }, "Add the possibility to install Magento without admin creation" : { "2.1.4 - 2.2.1": "MAGECLOUD-2573__installation_without_admin_creation__2.1.4.patch", "2.2.2 - 2.2.7": "MAGECLOUD-2573__installation_without_admin_creation__2.2.2.patch" @@ -329,7 +326,10 @@ "3.2.0": "MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch" } }, - "amzn/amazon-pay-and-login-magento-2-module": { + "amzn/amazon-pay-module": { + "Fix for DI compilation with Amazon_Payment module": { + "2.1.0": "MAGECLOUD-2850__fix_amazon_payment_module__2.2.6.patch" + }, "Set Payment info bug": { "3.4.1": "BUNDLE-2554__set_payment_info_bug_fix__3.4.1.patch" } From 18ee798b73fb8d86cd31a5334bb2455934f25293 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <posyniak@adobe.com> Date: Wed, 5 Aug 2020 12:34:33 -0500 Subject: [PATCH 050/157] MCLOUD-6651: Remove FPC patch for Magento 2.2.6 --- patches.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches.json b/patches.json index 56ccd13b..a9a09da7 100644 --- a/patches.json +++ b/patches.json @@ -228,7 +228,7 @@ }, "FPC is getting disabled during deployments": { ">=2.3.2 <2.3.5": "MAGECLOUD-5069__fpc_is_getting_disabled_during_deployments__2.3.2.patch", - ">=2.3.5 <2.4.0": "MAGECLOUD-5069__fpc_is_getting_disabled_during_deployments__2.3.5.patch" + ">=2.3.5 <2.3.6": "MAGECLOUD-5069__fpc_is_getting_disabled_during_deployments__2.3.5.patch" }, "Hold deployment config after reading from file": { ">=2.3.3 <2.3.6": "MCLOUD-5650__hold_deployment_config_after_reading_from_file__2.3.3.patch" From 1a8c86735aa254d1246a1259beedc2b464802f7e Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Wed, 5 Aug 2020 15:54:43 -0500 Subject: [PATCH 051/157] MCLOUD-6632: Add Magento 2.4.0 and PHP 7.4 support for FT magento-cloud-patches (#72) --- .travis.yml | 18 ++++++++--- .../Functional/Acceptance/AbstractCest.php | 10 ++++++- .../Acceptance/Acceptance73CeCest.php | 19 ++++++++++++ .../Acceptance/Acceptance73Cest.php | 30 +++++++++++++++++++ .../Acceptance/AcceptanceCeCest.php | 2 +- .../Functional/Acceptance/AcceptanceCest.php | 7 +---- tests/travis/functional-ce.sh | 3 ++ tests/travis/functional-ee.sh | 3 ++ 8 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 src/Test/Functional/Acceptance/Acceptance73CeCest.php create mode 100644 src/Test/Functional/Acceptance/Acceptance73Cest.php diff --git a/.travis.yml b/.travis.yml index 9c62b41e..8d402b02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,10 +20,6 @@ jobs: include: - stage: static-unit script: ./tests/travis/static-unit.sh; - php: '7.0' - env: - - TEST_SUITE=static-unit - - script: ./tests/travis/static-unit.sh; php: '7.1' env: - TEST_SUITE=static-unit @@ -50,6 +46,10 @@ jobs: - php: '7.3' env: - TEST_SUITE=functional-ee + - php: '7.4' + dist: bionic + env: + - TEST_SUITE=functional-ee - php: '7.1' env: - TEST_SUITE=functional-ce @@ -64,12 +64,22 @@ jobs: - php: '7.3' env: - TEST_SUITE=functional-ce + - php: '7.4' + dist: bionic + env: + - TEST_SUITE=functional-ce install: - composer config http-basic.repo.magento.com ${REPO_USERNAME_CE} ${REPO_PASSWORD_CE} - composer config github-oauth.github.com ${GITHUB_TOKEN} - if [ -n "${MCD_VERSION}" ] && [ $TRAVIS_PHP_VERSION != "7.0" ]; then composer config repositories.mcd git git@github.com:magento/magento-cloud-docker.git && composer require "magento/magento-cloud-docker:${MCD_VERSION}" --no-update; fi; - if [ -n "${MQP_VERSION}" ]; then composer config repositories.mqp git git@github.com:magento/quality-patches.git && composer require "magento/quality-patches:${MQP_VERSION}" --no-update; fi; + - if [ -n "${MCC_VERSION}" ]; then composer config repositories.mcc git git@github.com:magento/magento-cloud-components.git && composer require "magento/magento-cloud-components:${MCC_VERSION}" --no-update; fi; + - if [ -n "${ECE_VERSION}" ]; then composer config repositories.ece git git@github.com:magento/ece-tools.git && composer require "magento/ece-tools:${ECE_VERSION}" --no-update; fi; + - composer config repositories.magento composer https://repo.magento.com/ + - composer require "magento/framework:*" --no-update + - composer require "magento/module-store:*" --no-update + - composer require "magento/module-url-rewrite:*" --no-update - composer update -n --no-suggest before_script: diff --git a/src/Test/Functional/Acceptance/AbstractCest.php b/src/Test/Functional/Acceptance/AbstractCest.php index 64f489b9..e893f878 100644 --- a/src/Test/Functional/Acceptance/AbstractCest.php +++ b/src/Test/Functional/Acceptance/AbstractCest.php @@ -40,6 +40,7 @@ protected function prepareTemplate(\CliTester $I, string $templateVersion, strin $I->addEceDockerGitRepoToComposer(); $I->addQualityPatchesGitRepoToComposer(); $I->addEceToolsGitRepoToComposer(); + $I->addCloudComponentsGitRepoToComposer(); $I->addDependencyToComposer('magento/magento-cloud-patches', '1.0.99'); $I->addDependencyToComposer( 'magento/magento-cloud-docker', @@ -50,7 +51,14 @@ protected function prepareTemplate(\CliTester $I, string $templateVersion, strin $I->getDependencyVersion('magento/quality-patches') ); - $I->addDependencyToComposer('magento/ece-tools', 'dev-develop as 2002.1.99'); + $I->addDependencyToComposer( + 'magento/magento-cloud-components', + $I->getDependencyVersion('magento/magento-cloud-components') + ); + $I->addDependencyToComposer( + 'magento/ece-tools', + $I->getDependencyVersion('magento/ece-tools') + ); if ($this->edition === 'CE' || $magentoVersion) { $version = $magentoVersion ?: $this->getVersionRangeForMagento($I); diff --git a/src/Test/Functional/Acceptance/Acceptance73CeCest.php b/src/Test/Functional/Acceptance/Acceptance73CeCest.php new file mode 100644 index 00000000..d25e1805 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance73CeCest.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php73ce + */ +class Acceptance73CeCest extends Acceptance73Cest +{ + /** + * @var string + */ + protected $edition = 'CE'; +} diff --git a/src/Test/Functional/Acceptance/Acceptance73Cest.php b/src/Test/Functional/Acceptance/Acceptance73Cest.php new file mode 100644 index 00000000..45ffa649 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance73Cest.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php73 + */ +class Acceptance73Cest extends AcceptanceCest +{ + /** + * @return array + */ + protected function patchesDataProvider(): array + { + return [ + ['templateVersion' => '2.3.3', 'magentoVersion' => '2.3.3'], + ['templateVersion' => '2.3.3', 'magentoVersion' => '2.3.3-p1'], + ['templateVersion' => '2.3.4', 'magentoVersion' => '2.3.4'], + ['templateVersion' => '2.3.4', 'magentoVersion' => '2.3.4-p2'], + ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5'], + ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5-p1'], + ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/AcceptanceCeCest.php b/src/Test/Functional/Acceptance/AcceptanceCeCest.php index cdf0e2d2..dd234d47 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCeCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCeCest.php @@ -8,7 +8,7 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php73ce + * @group php74ce */ class AcceptanceCeCest extends AcceptanceCest { diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index f0fd0485..b8fcfc82 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -49,12 +49,7 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void protected function patchesDataProvider(): array { return [ - ['templateVersion' => '2.3.3', 'magentoVersion' => '2.3.3'], - ['templateVersion' => '2.3.3', 'magentoVersion' => '2.3.3-p1'], - ['templateVersion' => '2.3.4', 'magentoVersion' => '2.3.4'], - ['templateVersion' => '2.3.4', 'magentoVersion' => '2.3.4-p2'], - ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5'], - ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5-p1'], + ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], ['templateVersion' => 'master'], ]; } diff --git a/tests/travis/functional-ce.sh b/tests/travis/functional-ce.sh index ab0e3f7f..a5431613 100755 --- a/tests/travis/functional-ce.sh +++ b/tests/travis/functional-ce.sh @@ -16,4 +16,7 @@ case $TRAVIS_PHP_VERSION in 7.3) ./vendor/bin/codecept run -g php73ce --steps ;; + 7.4) + ./vendor/bin/codecept run -g php74ce --steps + ;; esac diff --git a/tests/travis/functional-ee.sh b/tests/travis/functional-ee.sh index 467b6f11..b2761f72 100755 --- a/tests/travis/functional-ee.sh +++ b/tests/travis/functional-ee.sh @@ -16,4 +16,7 @@ case $TRAVIS_PHP_VERSION in 7.3) ./vendor/bin/codecept run -g php73 --steps ;; + 7.4) + ./vendor/bin/codecept run -g php74 --steps + ;; esac From e47d6b0de356df62f75433917ea7f2476099d5ab Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Mon, 10 Aug 2020 12:18:37 -0500 Subject: [PATCH 052/157] MCLOUD-6659: Update Redis L2 cache patch (#74) --- patches.json | 3 ++- ...LOUD-6211__redis_improvement_patches__2.3.0.patch | 2 +- ...LOUD-6211__redis_improvement_patches__2.3.1.patch | 2 +- ...LOUD-6211__redis_improvement_patches__2.3.2.patch | 2 +- ...D-6211__redis_improvement_patches__2.3.3-p1.patch | 2 +- ...LOUD-6211__redis_improvement_patches__2.3.3.patch | 2 +- ...LOUD-6211__redis_improvement_patches__2.3.4.patch | 2 +- ...LOUD-6211__redis_improvement_patches__2.3.5.patch | 12 ++++++++++++ patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch | 12 ++++++++++++ src/Test/Functional/Acceptance/AcceptanceCest.php | 2 +- 10 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch diff --git a/patches.json b/patches.json index a9a09da7..eb00b220 100644 --- a/patches.json +++ b/patches.json @@ -254,7 +254,8 @@ "2.3.3": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch", ">=2.3.3-p1 <2.3.4": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch", ">=2.3.4 <2.3.5": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch", - ">=2.3.5 <2.3.6": "MCLOUD-6211__redis_improvement_patches__2.3.5.patch" + ">=2.3.5 <2.3.6": "MCLOUD-6211__redis_improvement_patches__2.3.5.patch", + ">=2.4.0 <2.4.1": "MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch index b742ebf8..852eb2f6 100644 --- a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.0.patch @@ -1798,7 +1798,7 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $dataToSave = $data; + $remHash = $this->loadRemoteDataVersion($id); + -+ if ($remHash !== false) { ++ if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $dataToSave = $this->remote->load($id); + } else { + $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch index 46a00737..ad3c722e 100644 --- a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.1.patch @@ -1742,7 +1742,7 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $dataToSave = $data; + $remHash = $this->loadRemoteDataVersion($id); + -+ if ($remHash !== false) { ++ if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $dataToSave = $this->remote->load($id); + } else { + $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch index cafc156a..a3136c13 100644 --- a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.2.patch @@ -843,7 +843,7 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $dataToSave = $data; + $remHash = $this->loadRemoteDataVersion($id); + -+ if ($remHash !== false) { ++ if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $dataToSave = $this->remote->load($id); + } else { + $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch index 51c25589..5a24b2ff 100644 --- a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch @@ -844,7 +844,7 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $dataToSave = $data; + $remHash = $this->loadRemoteDataVersion($id); + -+ if ($remHash !== false) { ++ if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $dataToSave = $this->remote->load($id); + } else { + $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch index 90ee3df1..0353f689 100644 --- a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3.patch @@ -844,7 +844,7 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $dataToSave = $data; + $remHash = $this->loadRemoteDataVersion($id); + -+ if ($remHash !== false) { ++ if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $dataToSave = $this->remote->load($id); + } else { + $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch index a46048a7..57f285d7 100644 --- a/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch +++ b/patches/MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch @@ -558,7 +558,7 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php + $dataToSave = $data; + $remHash = $this->loadRemoteDataVersion($id); + -+ if ($remHash !== false) { ++ if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $dataToSave = $this->remote->load($id); + } else { + $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/patches/MCLOUD-6211__redis_improvement_patches__2.3.5.patch b/patches/MCLOUD-6211__redis_improvement_patches__2.3.5.patch index 4e80e2ab..49405810 100644 --- a/patches/MCLOUD-6211__redis_improvement_patches__2.3.5.patch +++ b/patches/MCLOUD-6211__redis_improvement_patches__2.3.5.patch @@ -380,3 +380,15 @@ diff -Nuar a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/set + return substr(\hash('sha256', dirname(__DIR__, 6)), 0, 3) . '_'; } } +diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +--- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php ++++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +@@ -205,7 +205,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + $dataToSave = $data; + $remHash = $this->loadRemoteDataVersion($id); + +- if ($remHash !== false) { ++ if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $dataToSave = $this->remote->load($id); + } else { + $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch b/patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch new file mode 100644 index 00000000..24b83952 --- /dev/null +++ b/patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch @@ -0,0 +1,12 @@ +diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +--- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php ++++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +@@ -205,7 +205,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + $dataToSave = $data; + $remHash = $this->loadRemoteDataVersion($id); + +- if ($remHash !== false) { ++ if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $dataToSave = $this->remote->load($id); + } else { + $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index b8fcfc82..406a675f 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -8,7 +8,7 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php73 + * @group php74 */ class AcceptanceCest extends AbstractCest { From b8eb1fb4c4d7b71a7d977b5c095e15951b04af8b Mon Sep 17 00:00:00 2001 From: Tom Erskine <terskine@adobe.com> Date: Wed, 12 Aug 2020 11:39:48 -0500 Subject: [PATCH 053/157] BUNDLE-2683: VBE Braintree - Error occurs in Admin Braintree Settlement Report (#64) --- patches.json | 5 +++++ ...683__braintree_settlement_report_fix__4.1.0.patch | 12 ++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 patches/BUNDLE-2683__braintree_settlement_report_fix__4.1.0.patch diff --git a/patches.json b/patches.json index eb00b220..fef508e2 100644 --- a/patches.json +++ b/patches.json @@ -335,6 +335,11 @@ "3.4.1": "BUNDLE-2554__set_payment_info_bug_fix__3.4.1.patch" } }, + "paypal/module-braintree-core": { + "Fix Braintree Settlement report error": { + "4.1.0": "BUNDLE-2683__braintree_settlement_report_fix__4.1.0.patch" + } + }, "magento/magento2-ee-base": { "Fix pagebuilder module": { "2.3.1": "PB-319__fix_pagebuilder_module__2.3.1.patch", diff --git a/patches/BUNDLE-2683__braintree_settlement_report_fix__4.1.0.patch b/patches/BUNDLE-2683__braintree_settlement_report_fix__4.1.0.patch new file mode 100644 index 00000000..82696b54 --- /dev/null +++ b/patches/BUNDLE-2683__braintree_settlement_report_fix__4.1.0.patch @@ -0,0 +1,12 @@ +diff -Naur a/vendor/paypal/module-braintree-core/Ui/Component/Report/Listing/Column/PaymentType.php b/vendor/paypal/module-braintree-core/Ui/Component/Report/Listing/Column/PaymentType.php +--- a/vendor/paypal/module-braintree-core/Ui/Component/Report/Listing/Column/PaymentType.php ++++ b/vendor/paypal/module-braintree-core/Ui/Component/Report/Listing/Column/PaymentType.php +@@ -44,8 +44,6 @@ private function getAvailablePaymentTypes(): array + // @codingStandardsIgnoreStart + return [ + PaymentInstrumentType::PAYPAL_ACCOUNT => __(PaymentInstrumentType::PAYPAL_ACCOUNT), +- PaymentInstrumentType::COINBASE_ACCOUNT => __(PaymentInstrumentType::COINBASE_ACCOUNT), +- PaymentInstrumentType::EUROPE_BANK_ACCOUNT => __(PaymentInstrumentType::EUROPE_BANK_ACCOUNT), + PaymentInstrumentType::CREDIT_CARD => __(PaymentInstrumentType::CREDIT_CARD), + PaymentInstrumentType::APPLE_PAY_CARD => __(PaymentInstrumentType::APPLE_PAY_CARD), + PaymentInstrumentType::ANDROID_PAY_CARD => __(PaymentInstrumentType::ANDROID_PAY_CARD) From ca0ad7210443f0a9768b07b2a02526ed2be365ad Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Thu, 10 Sep 2020 11:05:29 -0500 Subject: [PATCH 054/157] MCLOUD-6910: MC-37292: Make 'quality-patches' a hard dependency for ece-tools and soft dependency for magento-cloud-patches (#76) * MC-37292: Make 'quality-patches' a hard dependency for ece-tools and soft dependency for magento-cloud-patches Co-authored-by: Viktor Tymchynskyi <vtymchynskyi@magento.com> --- .travis.yml | 4 ++++ composer.json | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8d402b02..434d0daf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,6 +69,10 @@ jobs: env: - TEST_SUITE=functional-ce +before_install: + # https://github.com/kylekatarnls/update-helper/issues/9 + - if [ -n "${COMPOSER_VERSION}" ]; then travis_retry composer self-update ${COMPOSER_VERSION}; fi; + install: - composer config http-basic.repo.magento.com ${REPO_USERNAME_CE} ${REPO_PASSWORD_CE} - composer config github-oauth.github.com ${GITHUB_TOKEN} diff --git a/composer.json b/composer.json index 5c08720b..5343a1fd 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,7 @@ "symfony/process": "^2.1||^4.1", "symfony/proxy-manager-bridge": "^3.3||^4.3", "symfony/yaml": "^3.3||^4.0", - "monolog/monolog": "^1.16", - "magento/quality-patches": "^1.0.0" + "monolog/monolog": "^1.16" }, "require-dev": { "codeception/codeception": "^2.5.3", @@ -25,6 +24,9 @@ "phpunit/phpunit": "^6.2", "squizlabs/php_codesniffer": "^3.0" }, + "suggest": { + "magento/quality-patches": "^1.0.3" + }, "bin": [ "bin/ece-patches", "bin/magento-patches" From be3bc0a3485ca8f29c622ebcef54030226639539 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Wed, 16 Sep 2020 17:37:17 -0500 Subject: [PATCH 055/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available --- config/services.xml | 10 +++ src/Patch/Applier.php | 68 +++++------------- src/Patch/GitPatchCommand.php | 108 ++++++++++++++++++++++++++++ src/Patch/PatchCommand.php | 108 ++++++++++++++++++++++++++++ src/Patch/PatchCommandInterface.php | 17 +++++ src/Patch/PatchCommandNotFound.php | 18 +++++ src/Patch/PatchCommandSelector.php | 79 ++++++++++++++++++++ 7 files changed, 358 insertions(+), 50 deletions(-) create mode 100644 src/Patch/GitPatchCommand.php create mode 100644 src/Patch/PatchCommand.php create mode 100644 src/Patch/PatchCommandInterface.php create mode 100644 src/Patch/PatchCommandNotFound.php create mode 100644 src/Patch/PatchCommandSelector.php diff --git a/config/services.xml b/config/services.xml index 52ca97c1..0272fb66 100644 --- a/config/services.xml +++ b/config/services.xml @@ -19,6 +19,7 @@ <service id="Magento\CloudPatches\Patch\Status\StatusResolverException" autowire="false"/> <service id="Magento\CloudPatches\Patch\PatchIntegrityException" autowire="false"/> <service id="Magento\CloudPatches\Patch\Pool\PatchNotFoundException" autowire="false"/> + <service id="Magento\CloudPatches\Patch\PatchCommandNotFound" autowire="false"/> <service id="Magento\CloudPatches\Patch\ApplierException" autowire="false"/> <service id="Magento\CloudPatches\Shell\PackageNotFoundException" autowire="false"/> <service id="Magento\CloudPatches\Patch\Data\Patch" autowire="false"/> @@ -76,5 +77,14 @@ <argument key="$actionPool" type="service" id="ApplyOptionalActionPool"/> </service> <service id="Magento\CloudPatches\Patch\PatchBuilder" shared="false"/> + <service id="patchCommandSelector" class="Magento\CloudPatches\Patch\PatchCommandSelector"> + <argument key="$commands" type="collection"> + <argument type="service" id="Magento\CloudPatches\Patch\GitPatchCommand"/> + <argument type="service" id="Magento\CloudPatches\Patch\PatchCommand"/> + </argument> + </service> + <service id="Magento\CloudPatches\Patch\Applier"> + <argument key="$patchCommand" type="service" id="patchCommandSelector"/> + </service> </services> </container> diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php index e5f2d1ef..fa0becf0 100644 --- a/src/Patch/Applier.php +++ b/src/Patch/Applier.php @@ -10,7 +10,6 @@ use Magento\CloudPatches\Composer\MagentoVersion; use Magento\CloudPatches\Filesystem\Filesystem; use Magento\CloudPatches\Patch\Status\StatusPool; -use Magento\CloudPatches\Shell\ProcessFactory; use Symfony\Component\Process\Exception\ProcessFailedException; /** @@ -18,11 +17,6 @@ */ class Applier { - /** - * @var ProcessFactory - */ - private $processFactory; - /** * @var GitConverter */ @@ -37,23 +31,27 @@ class Applier * @var Filesystem */ private $filesystem; + /** + * @var PatchCommandInterface + */ + private $patchCommand; /** - * @param ProcessFactory $processFactory * @param GitConverter $gitConverter * @param MagentoVersion $magentoVersion * @param Filesystem $filesystem + * @param PatchCommandInterface $patchCommand */ public function __construct( - ProcessFactory $processFactory, GitConverter $gitConverter, MagentoVersion $magentoVersion, - Filesystem $filesystem + Filesystem $filesystem, + PatchCommandInterface $patchCommand ) { - $this->processFactory = $processFactory; $this->gitConverter = $gitConverter; $this->magentoVersion = $magentoVersion; $this->filesystem = $filesystem; + $this->patchCommand = $patchCommand; } /** @@ -69,20 +67,12 @@ public function apply(string $path, string $id): string { $content = $this->readContent($path); try { - $this->processFactory->create(['git', 'apply'], $content) - ->mustRun(); + $result = $this->patchCommand->apply($content); } catch (ProcessFailedException $exception) { - try { - $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $content) - ->mustRun(); - } catch (ProcessFailedException $reverseException) { - throw new ApplierException($exception->getMessage(), $exception->getCode()); - } - - return sprintf('Patch %s was already applied', $id); + throw new ApplierException($exception->getMessage(), $exception->getCode()); } - return sprintf('Patch %s has been applied', $id); + return $result ? sprintf('Patch %s has been applied', $id) : sprintf('Patch %s was already applied', $id); } /** @@ -98,20 +88,12 @@ public function revert(string $path, string $id): string { $content = $this->readContent($path); try { - $this->processFactory->create(['git', 'apply', '--reverse'], $content) - ->mustRun(); + $result = $this->patchCommand->revert($content); } catch (ProcessFailedException $exception) { - try { - $this->processFactory->create(['git', 'apply', '--check'], $content) - ->mustRun(); - } catch (ProcessFailedException $applyException) { - throw new ApplierException($exception->getMessage(), $exception->getCode()); - } - - return sprintf('Patch %s wasn\'t applied', $id); + throw new ApplierException($exception->getMessage(), $exception->getCode()); } - return sprintf('Patch %s has been reverted', $id); + return $result ? sprintf('Patch %s has been reverted', $id) : sprintf('Patch %s wasn\'t applied', $id); } /** @@ -124,20 +106,12 @@ public function status(string $patchContent): string { $patchContent = $this->prepareContent($patchContent); try { - $this->processFactory->create(['git', 'apply', '--check'], $patchContent) - ->mustRun(); + $result = $this->patchCommand->status($patchContent); } catch (ProcessFailedException $exception) { - try { - $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $patchContent) - ->mustRun(); - } catch (ProcessFailedException $reverseException) { - return StatusPool::NA; - } - - return StatusPool::APPLIED; + return StatusPool::NA; } - return StatusPool::NOT_APPLIED; + return $result ? StatusPool::NOT_APPLIED : StatusPool::APPLIED; } /** @@ -149,14 +123,8 @@ public function status(string $patchContent): string public function checkApply(string $patchContent): bool { $patchContent = $this->prepareContent($patchContent); - try { - $this->processFactory->create(['git', 'apply', '--check'], $patchContent) - ->mustRun(); - } catch (ProcessFailedException $exception) { - return false; - } - return true; + return $this->patchCommand->check($patchContent); } /** diff --git a/src/Patch/GitPatchCommand.php b/src/Patch/GitPatchCommand.php new file mode 100644 index 00000000..76956447 --- /dev/null +++ b/src/Patch/GitPatchCommand.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\Shell\ProcessFactory; +use Symfony\Component\Process\Exception\ProcessFailedException; + +class GitPatchCommand implements PatchCommandInterface +{ + /** + * @var ProcessFactory + */ + private $processFactory; + + public function __construct( + ProcessFactory $processFactory + ) { + $this->processFactory = $processFactory; + } + + /** + * @inheritDoc + */ + public function apply(string $patch): bool + { + try { + $this->processFactory->create(['git', 'apply'], $patch) + ->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $patch) + ->mustRun(); + $result = false; + } + return $result; + } + + /** + * @inheritDoc + */ + public function revert(string $patch): bool + { + try { + $this->processFactory->create(['git', 'apply', '--reverse'], $patch) + ->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $this->processFactory->create(['git', 'apply', '--check'], $patch) + ->mustRun(); + $result = false; + } + return $result; + } + + /** + * @inheritDoc + */ + public function check(string $patch): bool + { + try { + $this->processFactory->create(['git', 'apply', '--check'], $patch) + ->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $result = false; + } + + return $result; + } + + /** + * @inheritDoc + */ + public function status(string $patch): bool + { + try { + $this->processFactory->create(['git', 'apply', '--check'], $patch) + ->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $patch) + ->mustRun(); + $result = false; + } + + return $result; + } + + /** + * @inheritDoc + */ + public function isInstalled(): bool + { + try { + $this->processFactory->create(['git', '--version'])->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $result = false; + } + + return $result; + } +} diff --git a/src/Patch/PatchCommand.php b/src/Patch/PatchCommand.php new file mode 100644 index 00000000..d4601148 --- /dev/null +++ b/src/Patch/PatchCommand.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\Shell\ProcessFactory; +use Symfony\Component\Process\Exception\ProcessFailedException; + +class PatchCommand implements PatchCommandInterface +{ + /** + * @var ProcessFactory + */ + private $processFactory; + + public function __construct( + ProcessFactory $processFactory + ) { + $this->processFactory = $processFactory; + } + + /** + * @inheritDoc + */ + public function apply(string $patch): bool + { + try { + $this->processFactory->create(['patch', '--silent', '-p1'], $patch) + ->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $this->processFactory->create(['patch', '--dry-run', '--silent', '-R', '-p1'], $patch) + ->mustRun(); + $result = false; + } + return $result; + } + + /** + * @inheritDoc + */ + public function revert(string $patch): bool + { + try { + $this->processFactory->create(['patch', '--silent', '-R', '-p1'], $patch) + ->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $this->processFactory->create(['patch', '--dry-run', '--silent', '-p1'], $patch) + ->mustRun(); + $result = false; + } + return $result; + } + + /** + * @inheritDoc + */ + public function check(string $patch): bool + { + try { + $this->processFactory->create(['patch', '--dry-run', '--silent', '-p1'], $patch) + ->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $result = false; + } + + return $result; + } + + /** + * @inheritDoc + */ + public function status(string $patch): bool + { + try { + $this->processFactory->create(['patch', '--dry-run', '--silent', '-p1'], $patch) + ->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $this->processFactory->create(['patch', '--dry-run', '--silent', '-R', '-p1'], $patch) + ->mustRun(); + $result = false; + } + + return $result; + } + + /** + * @inheritDoc + */ + public function isInstalled(): bool + { + try { + $this->processFactory->create(['patch', '--version'])->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $result = false; + } + + return $result; + } +} diff --git a/src/Patch/PatchCommandInterface.php b/src/Patch/PatchCommandInterface.php new file mode 100644 index 00000000..cbd4b938 --- /dev/null +++ b/src/Patch/PatchCommandInterface.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +interface PatchCommandInterface +{ + public function apply(string $patch): bool; + public function revert(string $patch): bool; + public function check(string $patch): bool; + public function status(string $patch): bool; + public function isInstalled(): bool; +} \ No newline at end of file diff --git a/src/Patch/PatchCommandNotFound.php b/src/Patch/PatchCommandNotFound.php new file mode 100644 index 00000000..870fd813 --- /dev/null +++ b/src/Patch/PatchCommandNotFound.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\App\GenericException; + +class PatchCommandNotFound extends GenericException +{ + public function __construct() + { + parent::__construct('git or patch is required to perform this operation.'); + } +} \ No newline at end of file diff --git a/src/Patch/PatchCommandSelector.php b/src/Patch/PatchCommandSelector.php new file mode 100644 index 00000000..b753aa44 --- /dev/null +++ b/src/Patch/PatchCommandSelector.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +class PatchCommandSelector implements PatchCommandInterface +{ + /** + * @var PatchCommandInterface[] + */ + private $commands; + + /** + * PatchCommandSelector constructor. + * @param PatchCommandInterface[] $commands + */ + public function __construct( + array $commands + ) { + $this->commands = $commands; + } + + /** + * @inheritDoc + */ + public function apply(string $patch): bool + { + return $this->getCommand()->apply($patch); + } + + /** + * @inheritDoc + */ + public function revert(string $patch): bool + { + return $this->getCommand()->revert($patch); + } + + /** + * @inheritDoc + */ + public function check(string $patch): bool + { + return $this->getCommand()->check($patch); + } + + /** + * @inheritDoc + */ + public function status(string $patch): bool + { + return $this->getCommand()->status($patch); + } + + /** + * @inheritDoc + */ + public function isInstalled(): bool + { + return $this->getCommand()->isInstalled(); + } + + /** + * Return first available command + */ + private function getCommand() + { + foreach ($this->commands as $command) { + if ($command->isInstalled()) { + return $command; + } + } + throw new PatchCommandNotFound(); + } +} From e2633c4dfa3e47aa24da1aca9a1a16f3726ee1d0 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 21 Sep 2020 09:19:04 -0500 Subject: [PATCH 056/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Move exception handling from patch command implementations --- src/Patch/Applier.php | 60 +++++++++++++++++++--------- src/Patch/GitPatchCommand.php | 59 +++++++--------------------- src/Patch/PatchCommand.php | 61 ++++++++--------------------- src/Patch/PatchCommandInterface.php | 46 +++++++++++++++++++--- src/Patch/PatchCommandNotFound.php | 2 +- src/Patch/PatchCommandSelector.php | 20 +++++----- 6 files changed, 127 insertions(+), 121 deletions(-) diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php index fa0becf0..b381e0b5 100644 --- a/src/Patch/Applier.php +++ b/src/Patch/Applier.php @@ -17,6 +17,11 @@ */ class Applier { + /** + * @var PatchCommandInterface + */ + private $patchCommand; + /** * @var GitConverter */ @@ -31,27 +36,23 @@ class Applier * @var Filesystem */ private $filesystem; - /** - * @var PatchCommandInterface - */ - private $patchCommand; /** + * @param PatchCommandInterface $patchCommand * @param GitConverter $gitConverter * @param MagentoVersion $magentoVersion * @param Filesystem $filesystem - * @param PatchCommandInterface $patchCommand */ public function __construct( + PatchCommandInterface $patchCommand, GitConverter $gitConverter, MagentoVersion $magentoVersion, - Filesystem $filesystem, - PatchCommandInterface $patchCommand + Filesystem $filesystem ) { + $this->patchCommand = $patchCommand; $this->gitConverter = $gitConverter; $this->magentoVersion = $magentoVersion; $this->filesystem = $filesystem; - $this->patchCommand = $patchCommand; } /** @@ -67,12 +68,18 @@ public function apply(string $path, string $id): string { $content = $this->readContent($path); try { - $result = $this->patchCommand->apply($content); + $this->patchCommand->applyCheck($content); } catch (ProcessFailedException $exception) { - throw new ApplierException($exception->getMessage(), $exception->getCode()); + try { + $this->patchCommand->reverseCheck($content); + } catch (ProcessFailedException $reverseException) { + throw new ApplierException($exception->getMessage(), $exception->getCode()); + } + + return sprintf('Patch %s was already applied', $id); } - return $result ? sprintf('Patch %s has been applied', $id) : sprintf('Patch %s was already applied', $id); + return sprintf('Patch %s has been applied', $id); } /** @@ -88,12 +95,18 @@ public function revert(string $path, string $id): string { $content = $this->readContent($path); try { - $result = $this->patchCommand->revert($content); + $this->patchCommand->revert($content); } catch (ProcessFailedException $exception) { - throw new ApplierException($exception->getMessage(), $exception->getCode()); + try { + $this->patchCommand->applyCheck($content); + } catch (ProcessFailedException $applyException) { + throw new ApplierException($exception->getMessage(), $exception->getCode()); + } + + return sprintf('Patch %s wasn\'t applied', $id); } - return $result ? sprintf('Patch %s has been reverted', $id) : sprintf('Patch %s wasn\'t applied', $id); + return sprintf('Patch %s has been reverted', $id); } /** @@ -106,12 +119,18 @@ public function status(string $patchContent): string { $patchContent = $this->prepareContent($patchContent); try { - $result = $this->patchCommand->status($patchContent); + $this->patchCommand->applyCheck($patchContent); } catch (ProcessFailedException $exception) { - return StatusPool::NA; + try { + $this->patchCommand->reverseCheck($patchContent); + } catch (ProcessFailedException $reverseException) { + return StatusPool::NA; + } + + return StatusPool::APPLIED; } - return $result ? StatusPool::NOT_APPLIED : StatusPool::APPLIED; + return StatusPool::NOT_APPLIED; } /** @@ -123,8 +142,13 @@ public function status(string $patchContent): string public function checkApply(string $patchContent): bool { $patchContent = $this->prepareContent($patchContent); + try { + $this->patchCommand->applyCheck($patchContent); + } catch (ProcessFailedException $exception) { + return false; + } - return $this->patchCommand->check($patchContent); + return true; } /** diff --git a/src/Patch/GitPatchCommand.php b/src/Patch/GitPatchCommand.php index 76956447..3d4108a9 100644 --- a/src/Patch/GitPatchCommand.php +++ b/src/Patch/GitPatchCommand.php @@ -10,6 +10,9 @@ use Magento\CloudPatches\Shell\ProcessFactory; use Symfony\Component\Process\Exception\ProcessFailedException; +/** + * Patch command for GIT + */ class GitPatchCommand implements PatchCommandInterface { /** @@ -26,69 +29,37 @@ public function __construct( /** * @inheritDoc */ - public function apply(string $patch): bool + public function apply(string $patch) { - try { - $this->processFactory->create(['git', 'apply'], $patch) - ->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $patch) - ->mustRun(); - $result = false; - } - return $result; + $this->processFactory->create(['git', 'apply'], $patch) + ->mustRun(); } /** * @inheritDoc */ - public function revert(string $patch): bool + public function revert(string $patch) { - try { - $this->processFactory->create(['git', 'apply', '--reverse'], $patch) - ->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $this->processFactory->create(['git', 'apply', '--check'], $patch) - ->mustRun(); - $result = false; - } - return $result; + $this->processFactory->create(['git', 'apply', '--reverse'], $patch) + ->mustRun(); } /** * @inheritDoc */ - public function check(string $patch): bool + public function applyCheck(string $patch) { - try { - $this->processFactory->create(['git', 'apply', '--check'], $patch) - ->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $result = false; - } - - return $result; + $this->processFactory->create(['git', 'apply', '--check'], $patch) + ->mustRun(); } /** * @inheritDoc */ - public function status(string $patch): bool + public function reverseCheck(string $patch) { - try { - $this->processFactory->create(['git', 'apply', '--check'], $patch) - ->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $patch) - ->mustRun(); - $result = false; - } - - return $result; + $this->processFactory->create(['git', 'apply', '--reverse', '--check'], $patch) + ->mustRun(); } /** diff --git a/src/Patch/PatchCommand.php b/src/Patch/PatchCommand.php index d4601148..29a1d385 100644 --- a/src/Patch/PatchCommand.php +++ b/src/Patch/PatchCommand.php @@ -10,6 +10,9 @@ use Magento\CloudPatches\Shell\ProcessFactory; use Symfony\Component\Process\Exception\ProcessFailedException; +/** + * Patch command for unix patch + */ class PatchCommand implements PatchCommandInterface { /** @@ -26,69 +29,39 @@ public function __construct( /** * @inheritDoc */ - public function apply(string $patch): bool + public function apply(string $patch) { - try { - $this->processFactory->create(['patch', '--silent', '-p1'], $patch) - ->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $this->processFactory->create(['patch', '--dry-run', '--silent', '-R', '-p1'], $patch) - ->mustRun(); - $result = false; - } - return $result; + $this->applyCheck($patch); + $this->processFactory->create(['patch', '--silent', '-f', '-p1'], $patch) + ->mustRun(); } /** * @inheritDoc */ - public function revert(string $patch): bool + public function revert(string $patch) { - try { - $this->processFactory->create(['patch', '--silent', '-R', '-p1'], $patch) - ->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $this->processFactory->create(['patch', '--dry-run', '--silent', '-p1'], $patch) - ->mustRun(); - $result = false; - } - return $result; + $this->reverseCheck($patch); + $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse'], $patch) + ->mustRun(); } /** * @inheritDoc */ - public function check(string $patch): bool + public function applyCheck(string $patch) { - try { - $this->processFactory->create(['patch', '--dry-run', '--silent', '-p1'], $patch) - ->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $result = false; - } - - return $result; + $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--dry-run'], $patch) + ->mustRun(); } /** * @inheritDoc */ - public function status(string $patch): bool + public function reverseCheck(string $patch) { - try { - $this->processFactory->create(['patch', '--dry-run', '--silent', '-p1'], $patch) - ->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $this->processFactory->create(['patch', '--dry-run', '--silent', '-R', '-p1'], $patch) - ->mustRun(); - $result = false; - } - - return $result; + $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse', '--dry-run'], $patch) + ->mustRun(); } /** diff --git a/src/Patch/PatchCommandInterface.php b/src/Patch/PatchCommandInterface.php index cbd4b938..f135a097 100644 --- a/src/Patch/PatchCommandInterface.php +++ b/src/Patch/PatchCommandInterface.php @@ -7,11 +7,47 @@ namespace Magento\CloudPatches\Patch; +/** + * Patch command interface + */ interface PatchCommandInterface { - public function apply(string $patch): bool; - public function revert(string $patch): bool; - public function check(string $patch): bool; - public function status(string $patch): bool; + /** + * Applies patch + * + * @param string $patch + * @return void + */ + public function apply(string $patch); + + /** + * Reverts patch + * + * @param string $patch + * @return void + */ + public function revert(string $patch); + + /** + * Checks if the patch can be applied. + * + * @param string $patch + * @return void + */ + public function applyCheck(string $patch); + + /** + * Checks if the patch can be reversed + * + * @param string $patch + * @return void + */ + public function reverseCheck(string $patch); + + /** + * Checks if the command is installed + * + * @return bool + */ public function isInstalled(): bool; -} \ No newline at end of file +} diff --git a/src/Patch/PatchCommandNotFound.php b/src/Patch/PatchCommandNotFound.php index 870fd813..698a70ab 100644 --- a/src/Patch/PatchCommandNotFound.php +++ b/src/Patch/PatchCommandNotFound.php @@ -15,4 +15,4 @@ public function __construct() { parent::__construct('git or patch is required to perform this operation.'); } -} \ No newline at end of file +} diff --git a/src/Patch/PatchCommandSelector.php b/src/Patch/PatchCommandSelector.php index b753aa44..e93d3c92 100644 --- a/src/Patch/PatchCommandSelector.php +++ b/src/Patch/PatchCommandSelector.php @@ -7,6 +7,9 @@ namespace Magento\CloudPatches\Patch; +/** + * Patch command selector + */ class PatchCommandSelector implements PatchCommandInterface { /** @@ -15,7 +18,6 @@ class PatchCommandSelector implements PatchCommandInterface private $commands; /** - * PatchCommandSelector constructor. * @param PatchCommandInterface[] $commands */ public function __construct( @@ -27,33 +29,33 @@ public function __construct( /** * @inheritDoc */ - public function apply(string $patch): bool + public function apply(string $patch) { - return $this->getCommand()->apply($patch); + $this->getCommand()->apply($patch); } /** * @inheritDoc */ - public function revert(string $patch): bool + public function revert(string $patch) { - return $this->getCommand()->revert($patch); + $this->getCommand()->revert($patch); } /** * @inheritDoc */ - public function check(string $patch): bool + public function applyCheck(string $patch) { - return $this->getCommand()->check($patch); + $this->getCommand()->applyCheck($patch); } /** * @inheritDoc */ - public function status(string $patch): bool + public function reverseCheck(string $patch) { - return $this->getCommand()->status($patch); + $this->getCommand()->reverseCheck($patch); } /** From 87f774e6ee3bf0df3168281a4c6824ae188a9bcc Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 21 Sep 2020 10:06:34 -0500 Subject: [PATCH 057/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Fix unit test --- src/Patch/Applier.php | 6 +- src/Patch/GitPatchCommand.php | 2 +- src/Patch/PatchCommand.php | 4 +- src/Patch/PatchCommandInterface.php | 2 +- src/Patch/PatchCommandSelector.php | 4 +- src/Test/Unit/Patch/ApplierTest.php | 184 +++++++++------------------- 6 files changed, 67 insertions(+), 135 deletions(-) diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php index b381e0b5..7ad39ffa 100644 --- a/src/Patch/Applier.php +++ b/src/Patch/Applier.php @@ -68,10 +68,10 @@ public function apply(string $path, string $id): string { $content = $this->readContent($path); try { - $this->patchCommand->applyCheck($content); + $this->patchCommand->apply($content); } catch (ProcessFailedException $exception) { try { - $this->patchCommand->reverseCheck($content); + $this->patchCommand->revertCheck($content); } catch (ProcessFailedException $reverseException) { throw new ApplierException($exception->getMessage(), $exception->getCode()); } @@ -122,7 +122,7 @@ public function status(string $patchContent): string $this->patchCommand->applyCheck($patchContent); } catch (ProcessFailedException $exception) { try { - $this->patchCommand->reverseCheck($patchContent); + $this->patchCommand->revertCheck($patchContent); } catch (ProcessFailedException $reverseException) { return StatusPool::NA; } diff --git a/src/Patch/GitPatchCommand.php b/src/Patch/GitPatchCommand.php index 3d4108a9..ce02bd42 100644 --- a/src/Patch/GitPatchCommand.php +++ b/src/Patch/GitPatchCommand.php @@ -56,7 +56,7 @@ public function applyCheck(string $patch) /** * @inheritDoc */ - public function reverseCheck(string $patch) + public function revertCheck(string $patch) { $this->processFactory->create(['git', 'apply', '--reverse', '--check'], $patch) ->mustRun(); diff --git a/src/Patch/PatchCommand.php b/src/Patch/PatchCommand.php index 29a1d385..a38b4a43 100644 --- a/src/Patch/PatchCommand.php +++ b/src/Patch/PatchCommand.php @@ -41,7 +41,7 @@ public function apply(string $patch) */ public function revert(string $patch) { - $this->reverseCheck($patch); + $this->revertCheck($patch); $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse'], $patch) ->mustRun(); } @@ -58,7 +58,7 @@ public function applyCheck(string $patch) /** * @inheritDoc */ - public function reverseCheck(string $patch) + public function revertCheck(string $patch) { $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse', '--dry-run'], $patch) ->mustRun(); diff --git a/src/Patch/PatchCommandInterface.php b/src/Patch/PatchCommandInterface.php index f135a097..6aee769c 100644 --- a/src/Patch/PatchCommandInterface.php +++ b/src/Patch/PatchCommandInterface.php @@ -42,7 +42,7 @@ public function applyCheck(string $patch); * @param string $patch * @return void */ - public function reverseCheck(string $patch); + public function revertCheck(string $patch); /** * Checks if the command is installed diff --git a/src/Patch/PatchCommandSelector.php b/src/Patch/PatchCommandSelector.php index e93d3c92..48b6afc6 100644 --- a/src/Patch/PatchCommandSelector.php +++ b/src/Patch/PatchCommandSelector.php @@ -53,9 +53,9 @@ public function applyCheck(string $patch) /** * @inheritDoc */ - public function reverseCheck(string $patch) + public function revertCheck(string $patch) { - $this->getCommand()->reverseCheck($patch); + $this->getCommand()->revertCheck($patch); } /** diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php index d62d0c0a..91a31977 100644 --- a/src/Test/Unit/Patch/ApplierTest.php +++ b/src/Test/Unit/Patch/ApplierTest.php @@ -12,8 +12,8 @@ use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; use Magento\CloudPatches\Patch\GitConverter; +use Magento\CloudPatches\Patch\PatchCommandInterface; use Magento\CloudPatches\Patch\Status\StatusPool; -use Magento\CloudPatches\Shell\ProcessFactory; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Exception\ProcessFailedException; @@ -30,9 +30,9 @@ class ApplierTest extends TestCase private $applier; /** - * @var ProcessFactory|MockObject + * @var PatchCommandInterface|MockObject */ - private $processFactory; + private $patchCommand; /** * @var GitConverter|MockObject @@ -54,13 +54,13 @@ class ApplierTest extends TestCase */ protected function setUp() { - $this->processFactory = $this->createMock(ProcessFactory::class); + $this->patchCommand = $this->createMock(PatchCommandInterface::class); $this->gitConverter = $this->createMock(GitConverter::class); $this->magentoVersion = $this->createMock(MagentoVersion::class); $this->filesystem = $this->createMock(Filesystem::class); $this->applier = new Applier( - $this->processFactory, + $this->patchCommand, $this->gitConverter, $this->magentoVersion, $this->filesystem @@ -86,14 +86,10 @@ public function testApply() $this->gitConverter->expects($this->once()) ->method('convert') ->willReturn('gitContent'); - $processMock = $this->createMock(Process::class); - $this->processFactory->expects($this->once()) - ->method('create') - ->withConsecutive([['git', 'apply'], 'gitContent']) - ->willReturn($processMock); - $processMock->expects($this->once()) - ->method('mustRun'); + $this->patchCommand->expects($this->once()) + ->method('apply') + ->with('gitContent'); $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); } @@ -106,14 +102,13 @@ public function testApplyFailed() $path = 'path/to/patch'; $patchId = 'MC-11111'; - /** @var Process|MockObject $result */ - $processMock = $this->createMock(Process::class); - $processMock->method('mustRun') - ->willThrowException(new ProcessFailedException($processMock)); + $this->patchCommand->expects($this->once()) + ->method('apply') + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturn($processMock); + $this->patchCommand->expects($this->once()) + ->method('revertCheck') + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); $this->expectException(ApplierException::class); $this->applier->apply($path, $patchId); @@ -139,45 +134,16 @@ public function testApplyPatchAlreadyApplied() $this->gitConverter->expects($this->never()) ->method('convert'); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturnMap([ - [['git', 'apply'], 'patchContent'], - [['git', 'apply', '--check', '--reverse'], 'patchContent'] - ])->willReturnCallback([$this, 'shellApplyRevertCallback']); + $this->patchCommand->expects($this->once()) + ->method('apply') + ->with('patchContent') + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); - $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); - } + $this->patchCommand->expects($this->once()) + ->method('revertCheck') + ->with('patchContent'); - /** - * Callback for 'apply' and 'revert' operations. - * - * @param array $command - * @return Process - * - * @throws ProcessFailedException when the command isn't a reverse - */ - public function shellApplyRevertCallback(array $command): Process - { - if (in_array('--reverse', $command, true) && in_array('--check', $command, true) || - !in_array('--reverse', $command, true) && in_array('--check', $command, true) - ) { - // Command was the reverse check, it's all good. - /** @var Process|MockObject $result */ - $result = $this->createMock(Process::class); - $result->expects($this->once()) - ->method('mustRun'); - - return $result; - } - - /** @var Process|MockObject $result */ - $result = $this->createMock(Process::class); - $result->expects($this->once()) - ->method('mustRun') - ->willThrowException(new ProcessFailedException($result)); - - return $result; + $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); } /** @@ -201,14 +167,9 @@ public function testRevert() ->method('convert') ->willReturn('gitContent'); - $processMock = $this->createMock(Process::class); - - $this->processFactory->expects($this->once()) - ->method('create') - ->withConsecutive([['git', 'apply', '--reverse'], 'gitContent']) - ->willReturn($processMock); - $processMock->expects($this->once()) - ->method('mustRun'); + $this->patchCommand->expects($this->once()) + ->method('revert') + ->with('gitContent'); $this->assertSame($expectedMessage, $this->applier->revert($path, $patchId)); } @@ -221,14 +182,13 @@ public function testRevertFailed() $path = 'path/to/patch'; $patchId = 'MC-11111'; - /** @var Process|MockObject $result */ - $processMock = $this->createMock(Process::class); - $processMock->method('mustRun') - ->willThrowException(new ProcessFailedException($processMock)); + $this->patchCommand->expects($this->once()) + ->method('revert') + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturn($processMock); + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); $this->expectException(ApplierException::class); $this->applier->revert($path, $patchId); @@ -255,12 +215,14 @@ public function testRevertPatchWasntApplied() $this->gitConverter->expects($this->never()) ->method('convert'); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturnMap([ - [['git', 'apply'], $patchContent], - [['git', 'apply', '--check'], $patchContent] - ])->willReturnCallback([$this, 'shellApplyRevertCallback']); + $this->patchCommand->expects($this->once()) + ->method('revert') + ->with($patchContent) + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($patchContent); $this->assertSame($expectedMessage, $this->applier->revert($path, $patchId)); } @@ -271,14 +233,10 @@ public function testRevertPatchWasntApplied() public function testStatusNotApplied() { $patchContent = 'patch content'; - $processMock = $this->createMock(Process::class); - $this->processFactory->expects($this->once()) - ->method('create') - ->withConsecutive([['git', 'apply', '--check'], $patchContent]) - ->willReturn($processMock); - $processMock->expects($this->once()) - ->method('mustRun'); + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($patchContent); $this->assertSame(StatusPool::NOT_APPLIED, $this->applier->status($patchContent)); } @@ -290,14 +248,15 @@ public function testStatusNotAvailable() { $patchContent = 'patch content'; - /** @var Process|MockObject $result */ - $processMock = $this->createMock(Process::class); - $processMock->method('mustRun') - ->willThrowException(new ProcessFailedException($processMock)); + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($patchContent) + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturn($processMock); + $this->patchCommand->expects($this->once()) + ->method('revertCheck') + ->with($patchContent) + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); $this->assertSame(StatusPool::NA, $this->applier->status($patchContent)); } @@ -309,42 +268,15 @@ public function testStatusApplied() { $patchContent = 'patch content'; - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturnMap([ - [['git', 'apply', '--check']], - [['git', 'apply', '--check', '--reverse']] - ])->willReturnCallback([$this, 'shellStatusCallback']); + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($patchContent) + ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); - $this->assertSame(StatusPool::APPLIED, $this->applier->status($patchContent)); - } + $this->patchCommand->expects($this->once()) + ->method('revertCheck') + ->with($patchContent); - /** - * Callback for 'status' operations. - * - * @param array $command - * @return Process - * - * @throws ProcessFailedException when the command isn't a reverse - */ - public function shellStatusCallback(array $command): Process - { - if (in_array('--reverse', $command, true) && in_array('--check', $command, true)) { - // Command was the reverse check, it's all good. - /** @var Process|MockObject $result */ - $result = $this->createMock(Process::class); - $result->expects($this->once()) - ->method('mustRun'); - - return $result; - } - - /** @var Process|MockObject $result */ - $result = $this->createMock(Process::class); - $result->expects($this->once()) - ->method('mustRun') - ->willThrowException(new ProcessFailedException($result)); - - return $result; + $this->assertSame(StatusPool::APPLIED, $this->applier->status($patchContent)); } } From 678e65078289632ca1b925e48ab454a1380bdc27 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 21 Sep 2020 15:11:55 -0500 Subject: [PATCH 058/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Add unit test --- config/services.xml | 8 +- src/Patch/Applier.php | 15 +- src/Patch/GitPatchCommand.php | 79 -------- src/Patch/PatchCommand.php | 64 +++--- src/Patch/PatchCommandException.php | 17 ++ src/Patch/PatchCommandInterface.php | 11 +- src/Patch/PatchCommandNotFound.php | 2 +- src/Patch/PatchCommandSelector.php | 81 -------- src/Shell/Command/DriverInterface.php | 23 +++ src/Shell/Command/GitDriver.php | 96 +++++++++ src/Shell/Command/PatchDriver.php | 98 +++++++++ src/Test/Unit/Patch/ApplierTest.php | 21 +- .../Unit/Shell/Command/PatchDriverTest.php | 187 ++++++++++++++++++ tests/unit/_data/files/file1.md | 1 + tests/unit/_data/files/file1.patch | 9 + tests/unit/_data/files/file1_and_file2.patch | 18 ++ tests/unit/_data/files/file1_applied_patch.md | 3 + tests/unit/_data/files/file2.md | 1 + tests/unit/_data/files/file2_applied_patch.md | 3 + tests/unit/var/.gitignore | 2 + 20 files changed, 523 insertions(+), 216 deletions(-) delete mode 100644 src/Patch/GitPatchCommand.php create mode 100644 src/Patch/PatchCommandException.php delete mode 100644 src/Patch/PatchCommandSelector.php create mode 100644 src/Shell/Command/DriverInterface.php create mode 100644 src/Shell/Command/GitDriver.php create mode 100644 src/Shell/Command/PatchDriver.php create mode 100644 src/Test/Unit/Shell/Command/PatchDriverTest.php create mode 100644 tests/unit/_data/files/file1.md create mode 100644 tests/unit/_data/files/file1.patch create mode 100644 tests/unit/_data/files/file1_and_file2.patch create mode 100644 tests/unit/_data/files/file1_applied_patch.md create mode 100644 tests/unit/_data/files/file2.md create mode 100644 tests/unit/_data/files/file2_applied_patch.md create mode 100644 tests/unit/var/.gitignore diff --git a/config/services.xml b/config/services.xml index 0272fb66..d635b9a1 100644 --- a/config/services.xml +++ b/config/services.xml @@ -77,14 +77,14 @@ <argument key="$actionPool" type="service" id="ApplyOptionalActionPool"/> </service> <service id="Magento\CloudPatches\Patch\PatchBuilder" shared="false"/> - <service id="patchCommandSelector" class="Magento\CloudPatches\Patch\PatchCommandSelector"> + <service id="patchCommand" class="Magento\CloudPatches\Patch\PatchCommand"> <argument key="$commands" type="collection"> - <argument type="service" id="Magento\CloudPatches\Patch\GitPatchCommand"/> - <argument type="service" id="Magento\CloudPatches\Patch\PatchCommand"/> + <argument type="service" id="Magento\CloudPatches\Shell\Command\GitDriver"/> + <argument type="service" id="Magento\CloudPatches\Shell\Command\PatchDriver"/> </argument> </service> <service id="Magento\CloudPatches\Patch\Applier"> - <argument key="$patchCommand" type="service" id="patchCommandSelector"/> + <argument key="$patchCommand" type="service" id="patchCommand"/> </service> </services> </container> diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php index 7ad39ffa..93ecfb64 100644 --- a/src/Patch/Applier.php +++ b/src/Patch/Applier.php @@ -10,7 +10,6 @@ use Magento\CloudPatches\Composer\MagentoVersion; use Magento\CloudPatches\Filesystem\Filesystem; use Magento\CloudPatches\Patch\Status\StatusPool; -use Symfony\Component\Process\Exception\ProcessFailedException; /** * Applies and reverts patches. @@ -69,10 +68,10 @@ public function apply(string $path, string $id): string $content = $this->readContent($path); try { $this->patchCommand->apply($content); - } catch (ProcessFailedException $exception) { + } catch (PatchCommandException $exception) { try { $this->patchCommand->revertCheck($content); - } catch (ProcessFailedException $reverseException) { + } catch (PatchCommandException $reverseException) { throw new ApplierException($exception->getMessage(), $exception->getCode()); } @@ -96,10 +95,10 @@ public function revert(string $path, string $id): string $content = $this->readContent($path); try { $this->patchCommand->revert($content); - } catch (ProcessFailedException $exception) { + } catch (PatchCommandException $exception) { try { $this->patchCommand->applyCheck($content); - } catch (ProcessFailedException $applyException) { + } catch (PatchCommandException $applyException) { throw new ApplierException($exception->getMessage(), $exception->getCode()); } @@ -120,10 +119,10 @@ public function status(string $patchContent): string $patchContent = $this->prepareContent($patchContent); try { $this->patchCommand->applyCheck($patchContent); - } catch (ProcessFailedException $exception) { + } catch (PatchCommandException $exception) { try { $this->patchCommand->revertCheck($patchContent); - } catch (ProcessFailedException $reverseException) { + } catch (PatchCommandException $reverseException) { return StatusPool::NA; } @@ -144,7 +143,7 @@ public function checkApply(string $patchContent): bool $patchContent = $this->prepareContent($patchContent); try { $this->patchCommand->applyCheck($patchContent); - } catch (ProcessFailedException $exception) { + } catch (PatchCommandException $exception) { return false; } diff --git a/src/Patch/GitPatchCommand.php b/src/Patch/GitPatchCommand.php deleted file mode 100644 index ce02bd42..00000000 --- a/src/Patch/GitPatchCommand.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Patch; - -use Magento\CloudPatches\Shell\ProcessFactory; -use Symfony\Component\Process\Exception\ProcessFailedException; - -/** - * Patch command for GIT - */ -class GitPatchCommand implements PatchCommandInterface -{ - /** - * @var ProcessFactory - */ - private $processFactory; - - public function __construct( - ProcessFactory $processFactory - ) { - $this->processFactory = $processFactory; - } - - /** - * @inheritDoc - */ - public function apply(string $patch) - { - $this->processFactory->create(['git', 'apply'], $patch) - ->mustRun(); - } - - /** - * @inheritDoc - */ - public function revert(string $patch) - { - $this->processFactory->create(['git', 'apply', '--reverse'], $patch) - ->mustRun(); - } - - /** - * @inheritDoc - */ - public function applyCheck(string $patch) - { - $this->processFactory->create(['git', 'apply', '--check'], $patch) - ->mustRun(); - } - - /** - * @inheritDoc - */ - public function revertCheck(string $patch) - { - $this->processFactory->create(['git', 'apply', '--reverse', '--check'], $patch) - ->mustRun(); - } - - /** - * @inheritDoc - */ - public function isInstalled(): bool - { - try { - $this->processFactory->create(['git', '--version'])->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $result = false; - } - - return $result; - } -} diff --git a/src/Patch/PatchCommand.php b/src/Patch/PatchCommand.php index a38b4a43..2097ba61 100644 --- a/src/Patch/PatchCommand.php +++ b/src/Patch/PatchCommand.php @@ -7,23 +7,28 @@ namespace Magento\CloudPatches\Patch; -use Magento\CloudPatches\Shell\ProcessFactory; -use Symfony\Component\Process\Exception\ProcessFailedException; - /** - * Patch command for unix patch + * Patch command selector */ class PatchCommand implements PatchCommandInterface { /** - * @var ProcessFactory + * @var PatchCommandInterface[] + */ + private $commands; + + /** + * @var PatchCommandInterface */ - private $processFactory; + private $command; + /** + * @param PatchCommandInterface[] $commands + */ public function __construct( - ProcessFactory $processFactory + array $commands ) { - $this->processFactory = $processFactory; + $this->commands = $commands; } /** @@ -31,9 +36,7 @@ public function __construct( */ public function apply(string $patch) { - $this->applyCheck($patch); - $this->processFactory->create(['patch', '--silent', '-f', '-p1'], $patch) - ->mustRun(); + $this->getCommand()->apply($patch); } /** @@ -41,9 +44,7 @@ public function apply(string $patch) */ public function revert(string $patch) { - $this->revertCheck($patch); - $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse'], $patch) - ->mustRun(); + $this->getCommand()->revert($patch); } /** @@ -51,8 +52,7 @@ public function revert(string $patch) */ public function applyCheck(string $patch) { - $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--dry-run'], $patch) - ->mustRun(); + $this->getCommand()->applyCheck($patch); } /** @@ -60,8 +60,7 @@ public function applyCheck(string $patch) */ public function revertCheck(string $patch) { - $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse', '--dry-run'], $patch) - ->mustRun(); + $this->getCommand()->revertCheck($patch); } /** @@ -69,13 +68,28 @@ public function revertCheck(string $patch) */ public function isInstalled(): bool { - try { - $this->processFactory->create(['patch', '--version'])->mustRun(); - $result = true; - } catch (ProcessFailedException $exception) { - $result = false; - } + return $this->getCommand()->isInstalled(); + } - return $result; + /** + * Return first available command + * + * @return PatchCommandInterface + * @throws PatchCommandNotFound + */ + private function getCommand(): PatchCommandInterface + { + if ($this->command === null) { + foreach ($this->commands as $command) { + if ($command->isInstalled()) { + $this->command = $command; + break; + } + } + if ($this->command === null) { + throw new PatchCommandNotFound(); + } + } + return $this->command; } } diff --git a/src/Patch/PatchCommandException.php b/src/Patch/PatchCommandException.php new file mode 100644 index 00000000..3bcda3d4 --- /dev/null +++ b/src/Patch/PatchCommandException.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\App\GenericException; + +/** + * Generic patch command exception + */ +class PatchCommandException extends GenericException +{ +} diff --git a/src/Patch/PatchCommandInterface.php b/src/Patch/PatchCommandInterface.php index 6aee769c..04e80aed 100644 --- a/src/Patch/PatchCommandInterface.php +++ b/src/Patch/PatchCommandInterface.php @@ -17,6 +17,7 @@ interface PatchCommandInterface * * @param string $patch * @return void + * @throws PatchCommandException */ public function apply(string $patch); @@ -25,6 +26,7 @@ public function apply(string $patch); * * @param string $patch * @return void + * @throws PatchCommandException */ public function revert(string $patch); @@ -33,6 +35,7 @@ public function revert(string $patch); * * @param string $patch * @return void + * @throws PatchCommandException */ public function applyCheck(string $patch); @@ -41,13 +44,7 @@ public function applyCheck(string $patch); * * @param string $patch * @return void + * @throws PatchCommandException */ public function revertCheck(string $patch); - - /** - * Checks if the command is installed - * - * @return bool - */ - public function isInstalled(): bool; } diff --git a/src/Patch/PatchCommandNotFound.php b/src/Patch/PatchCommandNotFound.php index 698a70ab..b316417a 100644 --- a/src/Patch/PatchCommandNotFound.php +++ b/src/Patch/PatchCommandNotFound.php @@ -13,6 +13,6 @@ class PatchCommandNotFound extends GenericException { public function __construct() { - parent::__construct('git or patch is required to perform this operation.'); + parent::__construct('GIT or PATCH is required to perform this operation.'); } } diff --git a/src/Patch/PatchCommandSelector.php b/src/Patch/PatchCommandSelector.php deleted file mode 100644 index 48b6afc6..00000000 --- a/src/Patch/PatchCommandSelector.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Patch; - -/** - * Patch command selector - */ -class PatchCommandSelector implements PatchCommandInterface -{ - /** - * @var PatchCommandInterface[] - */ - private $commands; - - /** - * @param PatchCommandInterface[] $commands - */ - public function __construct( - array $commands - ) { - $this->commands = $commands; - } - - /** - * @inheritDoc - */ - public function apply(string $patch) - { - $this->getCommand()->apply($patch); - } - - /** - * @inheritDoc - */ - public function revert(string $patch) - { - $this->getCommand()->revert($patch); - } - - /** - * @inheritDoc - */ - public function applyCheck(string $patch) - { - $this->getCommand()->applyCheck($patch); - } - - /** - * @inheritDoc - */ - public function revertCheck(string $patch) - { - $this->getCommand()->revertCheck($patch); - } - - /** - * @inheritDoc - */ - public function isInstalled(): bool - { - return $this->getCommand()->isInstalled(); - } - - /** - * Return first available command - */ - private function getCommand() - { - foreach ($this->commands as $command) { - if ($command->isInstalled()) { - return $command; - } - } - throw new PatchCommandNotFound(); - } -} diff --git a/src/Shell/Command/DriverInterface.php b/src/Shell/Command/DriverInterface.php new file mode 100644 index 00000000..25f44ea8 --- /dev/null +++ b/src/Shell/Command/DriverInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Shell\Command; + +use Magento\CloudPatches\Patch\PatchCommandInterface; + +/** + * Patch driver interface + */ +interface DriverInterface extends PatchCommandInterface +{ + /** + * Checks if the driver is installed + * + * @return bool + */ + public function isInstalled(): bool; +} diff --git a/src/Shell/Command/GitDriver.php b/src/Shell/Command/GitDriver.php new file mode 100644 index 00000000..385168e3 --- /dev/null +++ b/src/Shell/Command/GitDriver.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Shell\Command; + +use Magento\CloudPatches\Patch\PatchCommandException; +use Magento\CloudPatches\Shell\ProcessFactory; +use Symfony\Component\Process\Exception\ProcessFailedException; + +/** + * GIT patch driver + */ +class GitDriver implements DriverInterface +{ + /** + * @var ProcessFactory + */ + private $processFactory; + + public function __construct( + ProcessFactory $processFactory + ) { + $this->processFactory = $processFactory; + } + + /** + * @inheritDoc + */ + public function apply(string $patch) + { + try { + $this->processFactory->create(['git', 'apply'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new PatchCommandException('Failed to apply patch', $exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + */ + public function revert(string $patch) + { + try { + $this->processFactory->create(['git', 'apply', '--reverse'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new PatchCommandException('Failed to revert patch', $exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + */ + public function applyCheck(string $patch) + { + try { + $this->processFactory->create(['git', 'apply', '--check'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new PatchCommandException('Patch cannot be applied', $exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + */ + public function revertCheck(string $patch) + { + try { + $this->processFactory->create(['git', 'apply', '--reverse', '--check'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new PatchCommandException('Patch cannot be reverted', $exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + */ + public function isInstalled(): bool + { + try { + $this->processFactory->create(['git', '--version'])->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $result = false; + } + + return $result; + } +} diff --git a/src/Shell/Command/PatchDriver.php b/src/Shell/Command/PatchDriver.php new file mode 100644 index 00000000..2daa62dc --- /dev/null +++ b/src/Shell/Command/PatchDriver.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Shell\Command; + +use Magento\CloudPatches\Patch\PatchCommandException; +use Magento\CloudPatches\Shell\ProcessFactory; +use Symfony\Component\Process\Exception\ProcessFailedException; + +/** + * Unix patch driver + */ +class PatchDriver implements DriverInterface +{ + /** + * @var ProcessFactory + */ + private $processFactory; + + public function __construct( + ProcessFactory $processFactory + ) { + $this->processFactory = $processFactory; + } + + /** + * @inheritDoc + */ + public function apply(string $patch) + { + try { + $this->applyCheck($patch); + $this->processFactory->create(['patch', '--silent', '-f', '-p1'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new PatchCommandException('Failed to apply patch', $exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + */ + public function revert(string $patch) + { + try { + $this->revertCheck($patch); + $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new PatchCommandException('Failed to revert patch', $exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + */ + public function applyCheck(string $patch) + { + try { + $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--dry-run'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new PatchCommandException('Patch cannot be applied', $exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + */ + public function revertCheck(string $patch) + { + try { + $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse', '--dry-run'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new PatchCommandException('Patch cannot be reverted', $exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + */ + public function isInstalled(): bool + { + try { + $this->processFactory->create(['patch', '--version'])->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $result = false; + } + + return $result; + } +} diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php index 91a31977..cd08b379 100644 --- a/src/Test/Unit/Patch/ApplierTest.php +++ b/src/Test/Unit/Patch/ApplierTest.php @@ -12,12 +12,11 @@ use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; use Magento\CloudPatches\Patch\GitConverter; +use Magento\CloudPatches\Patch\PatchCommandException; use Magento\CloudPatches\Patch\PatchCommandInterface; use Magento\CloudPatches\Patch\Status\StatusPool; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Exception\ProcessFailedException; -use Symfony\Component\Process\Process; /** * @inheritDoc @@ -104,11 +103,11 @@ public function testApplyFailed() $this->patchCommand->expects($this->once()) ->method('apply') - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be applied')); $this->patchCommand->expects($this->once()) ->method('revertCheck') - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be reverted')); $this->expectException(ApplierException::class); $this->applier->apply($path, $patchId); @@ -137,7 +136,7 @@ public function testApplyPatchAlreadyApplied() $this->patchCommand->expects($this->once()) ->method('apply') ->with('patchContent') - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be applied')); $this->patchCommand->expects($this->once()) ->method('revertCheck') @@ -184,11 +183,11 @@ public function testRevertFailed() $this->patchCommand->expects($this->once()) ->method('revert') - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be reverted')); $this->patchCommand->expects($this->once()) ->method('applyCheck') - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be applied')); $this->expectException(ApplierException::class); $this->applier->revert($path, $patchId); @@ -218,7 +217,7 @@ public function testRevertPatchWasntApplied() $this->patchCommand->expects($this->once()) ->method('revert') ->with($patchContent) - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be reverted')); $this->patchCommand->expects($this->once()) ->method('applyCheck') @@ -251,12 +250,12 @@ public function testStatusNotAvailable() $this->patchCommand->expects($this->once()) ->method('applyCheck') ->with($patchContent) - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be applied')); $this->patchCommand->expects($this->once()) ->method('revertCheck') ->with($patchContent) - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be reverted')); $this->assertSame(StatusPool::NA, $this->applier->status($patchContent)); } @@ -271,7 +270,7 @@ public function testStatusApplied() $this->patchCommand->expects($this->once()) ->method('applyCheck') ->with($patchContent) - ->willThrowException(new ProcessFailedException($this->createMock(Process::class))); + ->willThrowException(new PatchCommandException('Patch cannot be applied')); $this->patchCommand->expects($this->once()) ->method('revertCheck') diff --git a/src/Test/Unit/Shell/Command/PatchDriverTest.php b/src/Test/Unit/Shell/Command/PatchDriverTest.php new file mode 100644 index 00000000..d1462d54 --- /dev/null +++ b/src/Test/Unit/Shell/Command/PatchDriverTest.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Shell\Command; + +use Magento\CloudPatches\Patch\PatchCommandException; +use Magento\CloudPatches\Shell\Command\PatchDriver; +use Magento\CloudPatches\Shell\ProcessFactory; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; + +/** + * Tests unix patch driver + */ +class PatchDriverTest extends TestCase +{ + /** + * @var PatchDriver + */ + private $command; + /** + * @var string + */ + private $baseDir; + /** + * @var string + */ + private $cwd; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->baseDir = dirname(__DIR__, 5) . '/tests/unit/'; + $this->cwd = $this->baseDir . 'var/'; + $processFactory = $this->createMock(ProcessFactory::class); + $processFactory->method('create') + ->willReturnCallback( + function (array $cmd, string $input = null) { + return new Process( + $cmd, + $this->cwd, + null, + $input + ); + } + ); + $this->command = new PatchDriver( + $processFactory + ); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + foreach (glob($this->cwd . '*') as $file) { + if (is_file($file)) { + unlink($file); + } + } + parent::tearDown(); + } + + /** + * Tests that patch is applied + */ + public function testApply() + { + $this->copyFileToWorkingDir($this->getFixtureFile('file1.md')); + $patchContent = $this->getFileContent($this->getFixtureFile('file1.patch')); + $this->command->apply($patchContent); + $expected = $this->getFileContent($this->getFixtureFile('file1_applied_patch.md')); + $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that patch is not applied to any target files if an error occurs + */ + public function testApplyFailure() + { + $this->copyFileToWorkingDir($this->getFixtureFile('file1.md')); + $this->copyFileToWorkingDir($this->getFixtureFile('file2_applied_patch.md'), 'file2.md'); + $patchContent = $this->getFileContent($this->getFixtureFile('file1_and_file2.patch')); + $exception = null; + try { + $this->command->apply($patchContent); + } catch (PatchCommandException $e) { + $exception = $e; + } + $this->assertNotNull($exception); + $expected = $this->getFileContent($this->getFixtureFile('file1.md')); + $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); + $expected = $this->getFileContent($this->getFixtureFile('file2_applied_patch.md')); + $actual = $this->getFileContent($this->getVarFile('file2.md')); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that patch is reverted + */ + public function testRevert() + { + $this->copyFileToWorkingDir($this->getFixtureFile('file1_applied_patch.md'), 'file1.md'); + $patchContent = $this->getFileContent($this->getFixtureFile('file1.patch')); + $this->command->revert($patchContent); + $expected = $this->getFileContent($this->getFixtureFile('file1.md')); + $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that patch is not reverted in any target files if an error occurs + */ + public function testRevertFailure() + { + $this->copyFileToWorkingDir($this->getFixtureFile('file1_applied_patch.md'), 'file1.md'); + $this->copyFileToWorkingDir($this->getFixtureFile('file2.md')); + $patchContent = $this->getFileContent($this->getFixtureFile('file1_and_file2.patch')); + $exception = null; + try { + $this->command->revert($patchContent); + } catch (PatchCommandException $e) { + $exception = $e; + } + $this->assertNotNull($exception); + $expected = $this->getFileContent($this->getFixtureFile('file1_applied_patch.md')); + $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); + $expected = $this->getFileContent($this->getFixtureFile('file2.md')); + $actual = $this->getFileContent($this->getVarFile('file2.md')); + $this->assertEquals($expected, $actual); + } + + /** + * Get file path in var directory + * + * @param string $name + * @return string + */ + private function getVarFile(string $name): string + { + return $this->cwd . $name; + } + + /** + * Get file path in files directory + * + * @param string $name + * @return string + */ + private function getFixtureFile(string $name): string + { + return $this->baseDir . '_data/files/' . $name; + } + + /** + * Get the file content + * + * @param string $path + * @return string + */ + private function getFileContent(string $path): string + { + return file_get_contents($path); + } + + /** + * Copy file to working directory + * + * @param string $path + * @param string|null $name + */ + private function copyFileToWorkingDir(string $path, string $name = null) + { + $name = $name ?? basename($path); + copy($path, $this->getVarFile($name)); + } +} diff --git a/tests/unit/_data/files/file1.md b/tests/unit/_data/files/file1.md new file mode 100644 index 00000000..abe4d183 --- /dev/null +++ b/tests/unit/_data/files/file1.md @@ -0,0 +1 @@ +## File 1 diff --git a/tests/unit/_data/files/file1.patch b/tests/unit/_data/files/file1.patch new file mode 100644 index 00000000..9c259fe3 --- /dev/null +++ b/tests/unit/_data/files/file1.patch @@ -0,0 +1,9 @@ +diff --git a/file1.md b/file1.md +index abe4d18..2f8298f 100644 +--- a/file1.md ++++ b/file1.md +@@ -1 +1,3 @@ +-## File 1 ++# File One ++ ++## Description diff --git a/tests/unit/_data/files/file1_and_file2.patch b/tests/unit/_data/files/file1_and_file2.patch new file mode 100644 index 00000000..f2dcb418 --- /dev/null +++ b/tests/unit/_data/files/file1_and_file2.patch @@ -0,0 +1,18 @@ +diff --git a/file1.md b/file1.md +index abe4d18..2f8298f 100644 +--- a/file1.md ++++ b/file1.md +@@ -1 +1,3 @@ +-## File 1 ++# File One ++ ++## Description +diff --git a/file2.md b/file2.md +index 37ae4d5..c49d210 100644 +--- a/file2.md ++++ b/file2.md +@@ -1 +1,3 @@ +-## File 2 ++# File Two ++ ++## Description diff --git a/tests/unit/_data/files/file1_applied_patch.md b/tests/unit/_data/files/file1_applied_patch.md new file mode 100644 index 00000000..2f8298f5 --- /dev/null +++ b/tests/unit/_data/files/file1_applied_patch.md @@ -0,0 +1,3 @@ +# File One + +## Description diff --git a/tests/unit/_data/files/file2.md b/tests/unit/_data/files/file2.md new file mode 100644 index 00000000..37ae4d5b --- /dev/null +++ b/tests/unit/_data/files/file2.md @@ -0,0 +1 @@ +## File 2 diff --git a/tests/unit/_data/files/file2_applied_patch.md b/tests/unit/_data/files/file2_applied_patch.md new file mode 100644 index 00000000..c49d210c --- /dev/null +++ b/tests/unit/_data/files/file2_applied_patch.md @@ -0,0 +1,3 @@ +# File Two + +## Description diff --git a/tests/unit/var/.gitignore b/tests/unit/var/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/tests/unit/var/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From e4a801d307d6dfbad3aa7f07e517eb31a593e0c6 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 21 Sep 2020 17:00:46 -0500 Subject: [PATCH 059/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available --- config/services.xml | 1 + src/Patch/PatchCommand.php | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/config/services.xml b/config/services.xml index d635b9a1..04e00980 100644 --- a/config/services.xml +++ b/config/services.xml @@ -20,6 +20,7 @@ <service id="Magento\CloudPatches\Patch\PatchIntegrityException" autowire="false"/> <service id="Magento\CloudPatches\Patch\Pool\PatchNotFoundException" autowire="false"/> <service id="Magento\CloudPatches\Patch\PatchCommandNotFound" autowire="false"/> + <service id="Magento\CloudPatches\Patch\PatchCommandException" autowire="false"/> <service id="Magento\CloudPatches\Patch\ApplierException" autowire="false"/> <service id="Magento\CloudPatches\Shell\PackageNotFoundException" autowire="false"/> <service id="Magento\CloudPatches\Patch\Data\Patch" autowire="false"/> diff --git a/src/Patch/PatchCommand.php b/src/Patch/PatchCommand.php index 2097ba61..7f646890 100644 --- a/src/Patch/PatchCommand.php +++ b/src/Patch/PatchCommand.php @@ -63,14 +63,6 @@ public function revertCheck(string $patch) $this->getCommand()->revertCheck($patch); } - /** - * @inheritDoc - */ - public function isInstalled(): bool - { - return $this->getCommand()->isInstalled(); - } - /** * Return first available command * From 68a4f8c4779b16c109c7c3b506e59cc624724575 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 21 Sep 2020 17:23:02 -0500 Subject: [PATCH 060/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available --- src/Patch/PatchCommand.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Patch/PatchCommand.php b/src/Patch/PatchCommand.php index 7f646890..42417685 100644 --- a/src/Patch/PatchCommand.php +++ b/src/Patch/PatchCommand.php @@ -7,23 +7,25 @@ namespace Magento\CloudPatches\Patch; +use Magento\CloudPatches\Shell\Command\DriverInterface; + /** * Patch command selector */ class PatchCommand implements PatchCommandInterface { /** - * @var PatchCommandInterface[] + * @var DriverInterface[] */ private $commands; /** - * @var PatchCommandInterface + * @var DriverInterface */ private $command; /** - * @param PatchCommandInterface[] $commands + * @param DriverInterface[] $commands */ public function __construct( array $commands @@ -66,10 +68,10 @@ public function revertCheck(string $patch) /** * Return first available command * - * @return PatchCommandInterface + * @return DriverInterface * @throws PatchCommandNotFound */ - private function getCommand(): PatchCommandInterface + private function getCommand(): DriverInterface { if ($this->command === null) { foreach ($this->commands as $command) { From 661c46429ff78fa61d9905479ffa8e93a709a3ed Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 21 Sep 2020 17:35:40 -0500 Subject: [PATCH 061/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available --- config/services.xml | 2 +- src/Patch/PatchCommand.php | 34 +++++++++++++-------------- src/Patch/PatchCommandInterface.php | 4 ++-- src/Patch/PatchCommandNotFound.php | 3 +++ src/Shell/Command/DriverInterface.php | 2 +- src/Shell/Command/GitDriver.php | 3 +++ src/Shell/Command/PatchDriver.php | 3 +++ 7 files changed, 30 insertions(+), 21 deletions(-) diff --git a/config/services.xml b/config/services.xml index 04e00980..7264d417 100644 --- a/config/services.xml +++ b/config/services.xml @@ -79,7 +79,7 @@ </service> <service id="Magento\CloudPatches\Patch\PatchBuilder" shared="false"/> <service id="patchCommand" class="Magento\CloudPatches\Patch\PatchCommand"> - <argument key="$commands" type="collection"> + <argument key="$drivers" type="collection"> <argument type="service" id="Magento\CloudPatches\Shell\Command\GitDriver"/> <argument type="service" id="Magento\CloudPatches\Shell\Command\PatchDriver"/> </argument> diff --git a/src/Patch/PatchCommand.php b/src/Patch/PatchCommand.php index 42417685..a96baf49 100644 --- a/src/Patch/PatchCommand.php +++ b/src/Patch/PatchCommand.php @@ -17,20 +17,20 @@ class PatchCommand implements PatchCommandInterface /** * @var DriverInterface[] */ - private $commands; + private $drivers; /** * @var DriverInterface */ - private $command; + private $driver; /** - * @param DriverInterface[] $commands + * @param DriverInterface[] $drivers */ public function __construct( - array $commands + array $drivers ) { - $this->commands = $commands; + $this->drivers = $drivers; } /** @@ -38,7 +38,7 @@ public function __construct( */ public function apply(string $patch) { - $this->getCommand()->apply($patch); + $this->getDriver()->apply($patch); } /** @@ -46,7 +46,7 @@ public function apply(string $patch) */ public function revert(string $patch) { - $this->getCommand()->revert($patch); + $this->getDriver()->revert($patch); } /** @@ -54,7 +54,7 @@ public function revert(string $patch) */ public function applyCheck(string $patch) { - $this->getCommand()->applyCheck($patch); + $this->getDriver()->applyCheck($patch); } /** @@ -62,28 +62,28 @@ public function applyCheck(string $patch) */ public function revertCheck(string $patch) { - $this->getCommand()->revertCheck($patch); + $this->getDriver()->revertCheck($patch); } /** - * Return first available command + * Returns first available driver * * @return DriverInterface * @throws PatchCommandNotFound */ - private function getCommand(): DriverInterface + private function getDriver(): DriverInterface { - if ($this->command === null) { - foreach ($this->commands as $command) { - if ($command->isInstalled()) { - $this->command = $command; + if ($this->driver === null) { + foreach ($this->drivers as $driver) { + if ($driver->isInstalled()) { + $this->driver = $driver; break; } } - if ($this->command === null) { + if ($this->driver === null) { throw new PatchCommandNotFound(); } } - return $this->command; + return $this->driver; } } diff --git a/src/Patch/PatchCommandInterface.php b/src/Patch/PatchCommandInterface.php index 04e80aed..4b59757c 100644 --- a/src/Patch/PatchCommandInterface.php +++ b/src/Patch/PatchCommandInterface.php @@ -31,7 +31,7 @@ public function apply(string $patch); public function revert(string $patch); /** - * Checks if the patch can be applied. + * Checks if patch can be applied. * * @param string $patch * @return void @@ -40,7 +40,7 @@ public function revert(string $patch); public function applyCheck(string $patch); /** - * Checks if the patch can be reversed + * Checks if patch can be reverted * * @param string $patch * @return void diff --git a/src/Patch/PatchCommandNotFound.php b/src/Patch/PatchCommandNotFound.php index b316417a..70a23e4f 100644 --- a/src/Patch/PatchCommandNotFound.php +++ b/src/Patch/PatchCommandNotFound.php @@ -9,6 +9,9 @@ use Magento\CloudPatches\App\GenericException; +/** + * Exception thrown if none of defined patch drivers is available + */ class PatchCommandNotFound extends GenericException { public function __construct() diff --git a/src/Shell/Command/DriverInterface.php b/src/Shell/Command/DriverInterface.php index 25f44ea8..9ac8d86b 100644 --- a/src/Shell/Command/DriverInterface.php +++ b/src/Shell/Command/DriverInterface.php @@ -10,7 +10,7 @@ use Magento\CloudPatches\Patch\PatchCommandInterface; /** - * Patch driver interface + * Patch command driver interface */ interface DriverInterface extends PatchCommandInterface { diff --git a/src/Shell/Command/GitDriver.php b/src/Shell/Command/GitDriver.php index 385168e3..ec005a93 100644 --- a/src/Shell/Command/GitDriver.php +++ b/src/Shell/Command/GitDriver.php @@ -21,6 +21,9 @@ class GitDriver implements DriverInterface */ private $processFactory; + /** + * @param ProcessFactory $processFactory + */ public function __construct( ProcessFactory $processFactory ) { diff --git a/src/Shell/Command/PatchDriver.php b/src/Shell/Command/PatchDriver.php index 2daa62dc..6461b7b7 100644 --- a/src/Shell/Command/PatchDriver.php +++ b/src/Shell/Command/PatchDriver.php @@ -21,6 +21,9 @@ class PatchDriver implements DriverInterface */ private $processFactory; + /** + * @param ProcessFactory $processFactory + */ public function __construct( ProcessFactory $processFactory ) { From 7643add5c5485d978be0b63db4f6ce4b514b2837 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Tue, 22 Sep 2020 13:13:21 -0500 Subject: [PATCH 062/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Remove -f flag in patch command - Fix services configuration --- config/services.xml | 5 ++--- src/Shell/Command/PatchDriver.php | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/config/services.xml b/config/services.xml index 7264d417..92357b18 100644 --- a/config/services.xml +++ b/config/services.xml @@ -29,6 +29,8 @@ <service id="Magento\CloudPatches\Patch\Pool\RequiredPool" lazy="true"/> <service id="Magento\CloudPatches\Patch\Pool\LocalPool" lazy="true"/> <service id="Magento\CloudPatches\Patch\Status\StatusPool" autowire="false"/> + <service id="Magento\CloudPatches\Patch\PatchCommand" autowire="false"/> + <service id="Magento\CloudPatches\Patch\PatchCommandInterface" alias="Magento\CloudPatches\Patch\PatchCommand"/> <service id="statusPool" class="Magento\CloudPatches\Patch\Status\StatusPool" lazy="true"> <argument key="$resolvers" type="collection"> <argument type="service" id="Magento\CloudPatches\Patch\Status\LocalResolver"/> @@ -84,8 +86,5 @@ <argument type="service" id="Magento\CloudPatches\Shell\Command\PatchDriver"/> </argument> </service> - <service id="Magento\CloudPatches\Patch\Applier"> - <argument key="$patchCommand" type="service" id="patchCommand"/> - </service> </services> </container> diff --git a/src/Shell/Command/PatchDriver.php b/src/Shell/Command/PatchDriver.php index 6461b7b7..8bf9076d 100644 --- a/src/Shell/Command/PatchDriver.php +++ b/src/Shell/Command/PatchDriver.php @@ -37,7 +37,7 @@ public function apply(string $patch) { try { $this->applyCheck($patch); - $this->processFactory->create(['patch', '--silent', '-f', '-p1'], $patch) + $this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { throw new PatchCommandException('Failed to apply patch', $exception->getCode(), $exception); @@ -51,7 +51,7 @@ public function revert(string $patch) { try { $this->revertCheck($patch); - $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse'], $patch) + $this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch', '--reverse'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { throw new PatchCommandException('Failed to revert patch', $exception->getCode(), $exception); @@ -64,7 +64,7 @@ public function revert(string $patch) public function applyCheck(string $patch) { try { - $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--dry-run'], $patch) + $this->processFactory->create(['patch', '--silent', '-p1', '--dry-run'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { throw new PatchCommandException('Patch cannot be applied', $exception->getCode(), $exception); @@ -77,7 +77,7 @@ public function applyCheck(string $patch) public function revertCheck(string $patch) { try { - $this->processFactory->create(['patch', '--silent', '-f', '-p1', '--reverse', '--dry-run'], $patch) + $this->processFactory->create(['patch', '--silent', '-p1', '--reverse', '--dry-run'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { throw new PatchCommandException('Patch cannot be reverted', $exception->getCode(), $exception); From 5754d25b74d8d091d2efec52b592bf443d38a0c4 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Tue, 22 Sep 2020 13:15:08 -0500 Subject: [PATCH 063/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Fix services configuration --- config/services.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/services.xml b/config/services.xml index 92357b18..1bc514d4 100644 --- a/config/services.xml +++ b/config/services.xml @@ -80,7 +80,7 @@ <argument key="$actionPool" type="service" id="ApplyOptionalActionPool"/> </service> <service id="Magento\CloudPatches\Patch\PatchBuilder" shared="false"/> - <service id="patchCommand" class="Magento\CloudPatches\Patch\PatchCommand"> + <service id="Magento\CloudPatches\Patch\PatchCommand"> <argument key="$drivers" type="collection"> <argument type="service" id="Magento\CloudPatches\Shell\Command\GitDriver"/> <argument type="service" id="Magento\CloudPatches\Shell\Command\PatchDriver"/> From 0060375d3ea85cd63790d8bcfb00f85275a9a3b6 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Wed, 23 Sep 2020 20:55:00 -0500 Subject: [PATCH 064/157] - Update information string --- src/Command/Process/ShowStatus.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 181366ef..0ae52946 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -111,9 +111,14 @@ function ($patch) { */ private function printDetailsInfo(OutputInterface $output) { + $supportUrl = 'https://support.magento.com/'; + $releaseNotesUrl = 'https://devdocs.magento.com/quality-patches/release-notes.html'; + $output->writeln( - '<info>More detailed information about patches you can find on </info>' . - '<href=https://support.magento.com>https://support.magento.com</>' + '<info>Patch details you can find on </info>' . + sprintf('<href=%1$s>%1$s</> <info>(search for patch id, ex. MDVA-30265)</info>', $supportUrl) . + PHP_EOL . + sprintf('<info>Release notes</info> <href=%1$s>%1$s</>', $releaseNotesUrl) ); } From 6cbccf8352720a3a84680fcbd273f63a2e9fd449 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Thu, 24 Sep 2020 15:21:56 -0500 Subject: [PATCH 065/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Fix error message formatting --- config/services.xml | 7 +- src/Shell/Command/DriverException.php | 55 +++++++ src/Shell/Command/GitDriver.php | 9 +- src/Shell/Command/GitDriverException.php | 29 ++++ src/Shell/Command/PatchDriver.php | 9 +- src/Shell/Command/PatchDriverException.php | 29 ++++ .../Shell/Command/DriverExceptionTest.php | 138 ++++++++++++++++++ 7 files changed, 264 insertions(+), 12 deletions(-) create mode 100644 src/Shell/Command/DriverException.php create mode 100644 src/Shell/Command/GitDriverException.php create mode 100644 src/Shell/Command/PatchDriverException.php create mode 100644 src/Test/Unit/Shell/Command/DriverExceptionTest.php diff --git a/config/services.xml b/config/services.xml index 1bc514d4..d9ce6831 100644 --- a/config/services.xml +++ b/config/services.xml @@ -19,8 +19,6 @@ <service id="Magento\CloudPatches\Patch\Status\StatusResolverException" autowire="false"/> <service id="Magento\CloudPatches\Patch\PatchIntegrityException" autowire="false"/> <service id="Magento\CloudPatches\Patch\Pool\PatchNotFoundException" autowire="false"/> - <service id="Magento\CloudPatches\Patch\PatchCommandNotFound" autowire="false"/> - <service id="Magento\CloudPatches\Patch\PatchCommandException" autowire="false"/> <service id="Magento\CloudPatches\Patch\ApplierException" autowire="false"/> <service id="Magento\CloudPatches\Shell\PackageNotFoundException" autowire="false"/> <service id="Magento\CloudPatches\Patch\Data\Patch" autowire="false"/> @@ -29,6 +27,11 @@ <service id="Magento\CloudPatches\Patch\Pool\RequiredPool" lazy="true"/> <service id="Magento\CloudPatches\Patch\Pool\LocalPool" lazy="true"/> <service id="Magento\CloudPatches\Patch\Status\StatusPool" autowire="false"/> + <service id="Magento\CloudPatches\Patch\PatchCommandNotFound" autowire="false"/> + <service id="Magento\CloudPatches\Patch\PatchCommandException" autowire="false"/> + <service id="Magento\CloudPatches\Shell\Command\DriverException" autowire="false"/> + <service id="Magento\CloudPatches\Shell\Command\GitDriverException" autowire="false"/> + <service id="Magento\CloudPatches\Shell\Command\PatchDriverException" autowire="false"/> <service id="Magento\CloudPatches\Patch\PatchCommand" autowire="false"/> <service id="Magento\CloudPatches\Patch\PatchCommandInterface" alias="Magento\CloudPatches\Patch\PatchCommand"/> <service id="statusPool" class="Magento\CloudPatches\Patch\Status\StatusPool" lazy="true"> diff --git a/src/Shell/Command/DriverException.php b/src/Shell/Command/DriverException.php new file mode 100644 index 00000000..ea5b01a0 --- /dev/null +++ b/src/Shell/Command/DriverException.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Shell\Command; + +use Magento\CloudPatches\Patch\PatchCommandException; +use Throwable; + +/** + * Patch command driver exception + */ +class DriverException extends PatchCommandException +{ + /** + * @param string $message + * @param int $code + * @param Throwable|null $previous + */ + public function __construct(string $message, int $code = 0, Throwable $previous = null) + { + parent::__construct($this->formatMessage($message), $code, $previous); + } + + /** + * Format error message + * + * @param string $message + * @return string + */ + private function formatMessage(string $message): string + { + $result = $message; + $errorMsg = null; + $generalMsg = null; + if (preg_match('#^.*?Error Output:(?<errors>.*?)$#is', $result, $matches)) { + $errorMsg = PHP_EOL .'Error Output:' . $matches['errors']; + $result = str_replace($errorMsg, '', $result); + if (!trim(str_replace('=', '', $matches['errors']))) { + $errorMsg = null; + } + } + if (empty($errorMsg) && preg_match('#^.*?Output:(?<errors>.*?)$#is', $result, $matches)) { + $generalMsg = PHP_EOL .'Output:' . $matches['errors']; + if (!trim(str_replace('=', '', $matches['errors']))) { + $generalMsg = null; + } + } + + return $errorMsg ?? $generalMsg ?? $message; + } +} diff --git a/src/Shell/Command/GitDriver.php b/src/Shell/Command/GitDriver.php index ec005a93..16b6a56a 100644 --- a/src/Shell/Command/GitDriver.php +++ b/src/Shell/Command/GitDriver.php @@ -7,7 +7,6 @@ namespace Magento\CloudPatches\Shell\Command; -use Magento\CloudPatches\Patch\PatchCommandException; use Magento\CloudPatches\Shell\ProcessFactory; use Symfony\Component\Process\Exception\ProcessFailedException; @@ -39,7 +38,7 @@ public function apply(string $patch) $this->processFactory->create(['git', 'apply'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchCommandException('Failed to apply patch', $exception->getCode(), $exception); + throw new GitDriverException($exception); } } @@ -52,7 +51,7 @@ public function revert(string $patch) $this->processFactory->create(['git', 'apply', '--reverse'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchCommandException('Failed to revert patch', $exception->getCode(), $exception); + throw new GitDriverException($exception); } } @@ -65,7 +64,7 @@ public function applyCheck(string $patch) $this->processFactory->create(['git', 'apply', '--check'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchCommandException('Patch cannot be applied', $exception->getCode(), $exception); + throw new GitDriverException($exception); } } @@ -78,7 +77,7 @@ public function revertCheck(string $patch) $this->processFactory->create(['git', 'apply', '--reverse', '--check'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchCommandException('Patch cannot be reverted', $exception->getCode(), $exception); + throw new GitDriverException($exception); } } diff --git a/src/Shell/Command/GitDriverException.php b/src/Shell/Command/GitDriverException.php new file mode 100644 index 00000000..490af43a --- /dev/null +++ b/src/Shell/Command/GitDriverException.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Shell\Command; + +use Symfony\Component\Process\Exception\ProcessFailedException; +use Throwable; + +/** + * Git patch driver exception + */ +class GitDriverException extends DriverException +{ + /** + * @param Throwable $previous + */ + public function __construct(Throwable $previous) + { + $message = $previous->getMessage(); + if ($previous instanceof ProcessFailedException) { + $message = $previous->getProcess()->getErrorOutput() ?: ($previous->getProcess()->getOutput() ?: $message); + } + parent::__construct($message, $previous->getCode(), $previous); + } +} diff --git a/src/Shell/Command/PatchDriver.php b/src/Shell/Command/PatchDriver.php index 8bf9076d..11b53887 100644 --- a/src/Shell/Command/PatchDriver.php +++ b/src/Shell/Command/PatchDriver.php @@ -7,7 +7,6 @@ namespace Magento\CloudPatches\Shell\Command; -use Magento\CloudPatches\Patch\PatchCommandException; use Magento\CloudPatches\Shell\ProcessFactory; use Symfony\Component\Process\Exception\ProcessFailedException; @@ -40,7 +39,7 @@ public function apply(string $patch) $this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchCommandException('Failed to apply patch', $exception->getCode(), $exception); + throw new PatchDriverException($exception); } } @@ -54,7 +53,7 @@ public function revert(string $patch) $this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch', '--reverse'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchCommandException('Failed to revert patch', $exception->getCode(), $exception); + throw new PatchDriverException($exception); } } @@ -67,7 +66,7 @@ public function applyCheck(string $patch) $this->processFactory->create(['patch', '--silent', '-p1', '--dry-run'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchCommandException('Patch cannot be applied', $exception->getCode(), $exception); + throw new PatchDriverException($exception); } } @@ -80,7 +79,7 @@ public function revertCheck(string $patch) $this->processFactory->create(['patch', '--silent', '-p1', '--reverse', '--dry-run'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchCommandException('Patch cannot be reverted', $exception->getCode(), $exception); + throw new PatchDriverException($exception); } } diff --git a/src/Shell/Command/PatchDriverException.php b/src/Shell/Command/PatchDriverException.php new file mode 100644 index 00000000..39b09f8b --- /dev/null +++ b/src/Shell/Command/PatchDriverException.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Shell\Command; + +use Symfony\Component\Process\Exception\ProcessFailedException; +use Throwable; + +/** + * Unix patch driver exception + */ +class PatchDriverException extends DriverException +{ + /** + * @param Throwable $previous + */ + public function __construct(Throwable $previous) + { + $message = $previous->getMessage(); + if ($previous instanceof ProcessFailedException) { + $message = $previous->getProcess()->getErrorOutput() ?: ($previous->getProcess()->getOutput() ?: $message); + } + parent::__construct($message, $previous->getCode(), $previous); + } +} diff --git a/src/Test/Unit/Shell/Command/DriverExceptionTest.php b/src/Test/Unit/Shell/Command/DriverExceptionTest.php new file mode 100644 index 00000000..f55e3a09 --- /dev/null +++ b/src/Test/Unit/Shell/Command/DriverExceptionTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Unit\Shell\Command; + +use Magento\CloudPatches\Shell\Command\DriverException; +use PHPUnit\Framework\TestCase; + +class DriverExceptionTest extends TestCase +{ + /** + * Tests exception message formatting. + * + * @param string $errorOutput + * @param string $expectedOutput + * @dataProvider formatMessageDataProvider + */ + public function testFormatMessage(string $errorOutput, string $expectedOutput) + { + $exception = new DriverException($errorOutput); + $this->assertEquals($expectedOutput, $exception->getMessage()); + } + + /** + * @return array + */ + public function formatMessageDataProvider(): array + { + return [ + [ + 'error' => 'The command "\'patch\' \'--silent\' \'-p1\' \'--dry-run\'" failed. + +Exit Code: 1(General error) + +Working directory: /var/www/html + +Output: +================ + + +Error Output: +================ +error: patch failed: path/to/path/file2.php b/path/to/path/file2.php:23 +error: path/to/path/file2.php b/path/to/path/file2.php: patch does not apply', + + 'expectedOutput' => ' +Error Output: +================ +error: patch failed: path/to/path/file2.php b/path/to/path/file2.php:23 +error: path/to/path/file2.php b/path/to/path/file2.php: patch does not apply' + ], + [ + 'error' => 'The command "\'patch\' \'--silent\' \'-p1\' \'--dry-run\'" failed. + +Exit Code: 1(General error) + +Working directory: /var/www/html + +Output: +================ +Hmm... Looks like a unified diff to me... +The text leading up to this was: +-------------------------- +|diff --git a/path/to/path/file1.php b/path/to/path/file1.php +|index 320e0adc29b..576281861d3 100644 +|--- a/path/to/path/file1.php +|+++ b/path/to/path/file1.php +-------------------------- +Patching file path/to/path/file1.php using Plan A... +Hunk #1 succeeded at 30. +Hunk #2 succeeded at 54. +Hunk #3 succeeded at 76. +Hunk #4 succeeded at 113. +Hmm... The next patch looks like a unified diff to me... +The text leading up to this was: +-------------------------- +|diff --git a/path/to/path/file2.php b/path/to/path/file2.php +|index 0ec65c88024..e550de9cb03 100644 +|--- a/path/to/path/file2.php +|+++ b/path/to/path/file2.php +-------------------------- +Patching file path/to/path/file2.php using Plan A... +Hunk #1 succeeded at 71. +Hunk #2 FAILED at 136. +Hunk #3 succeeded at 154. +1 out of 3 hunks FAILED -- saving rejects to file path/to/path/file2.php.rej +done + + +Error Output: +================ + +', + + 'expectedOutput' => ' +Output: +================ +Hmm... Looks like a unified diff to me... +The text leading up to this was: +-------------------------- +|diff --git a/path/to/path/file1.php b/path/to/path/file1.php +|index 320e0adc29b..576281861d3 100644 +|--- a/path/to/path/file1.php +|+++ b/path/to/path/file1.php +-------------------------- +Patching file path/to/path/file1.php using Plan A... +Hunk #1 succeeded at 30. +Hunk #2 succeeded at 54. +Hunk #3 succeeded at 76. +Hunk #4 succeeded at 113. +Hmm... The next patch looks like a unified diff to me... +The text leading up to this was: +-------------------------- +|diff --git a/path/to/path/file2.php b/path/to/path/file2.php +|index 0ec65c88024..e550de9cb03 100644 +|--- a/path/to/path/file2.php +|+++ b/path/to/path/file2.php +-------------------------- +Patching file path/to/path/file2.php using Plan A... +Hunk #1 succeeded at 71. +Hunk #2 FAILED at 136. +Hunk #3 succeeded at 154. +1 out of 3 hunks FAILED -- saving rejects to file path/to/path/file2.php.rej +done + +' + ], + [ + 'error' => 'Some other output', + 'expectedOutput' => 'Some other output' + ], + ]; + } +} From ff40569505fa39bde675fc3c7d32d38b99c282ea Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Thu, 24 Sep 2020 16:37:17 -0500 Subject: [PATCH 066/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Remove formatErrorOutput --- src/Command/Process/Action/RevertAction.php | 2 +- src/Command/Process/ApplyLocal.php | 2 +- src/Command/Process/Ece/Revert.php | 2 +- src/Command/Process/Renderer.php | 15 ------ src/Patch/Conflict/Processor.php | 2 +- src/Shell/Command/DriverException.php | 4 +- .../Process/Action/RevertActionTest.php | 3 -- .../Unit/Command/Process/ApplyLocalTest.php | 3 -- .../Unit/Command/Process/Ece/RevertTest.php | 4 -- .../Unit/Command/Process/RendererTest.php | 47 ------------------- .../Unit/Patch/Conflict/ProcessorTest.php | 7 +-- .../Shell/Command/DriverExceptionTest.php | 7 ++- 12 files changed, 10 insertions(+), 88 deletions(-) diff --git a/src/Command/Process/Action/RevertAction.php b/src/Command/Process/Action/RevertAction.php index 5c472596..3ebd64ed 100644 --- a/src/Command/Process/Action/RevertAction.php +++ b/src/Command/Process/Action/RevertAction.php @@ -179,7 +179,7 @@ private function printPatchRevertingFailed(OutputInterface $output, PatchInterfa 'Reverting patch %s (%s) failed.%s', $patch->getId(), $patch->getPath(), - $this->renderer->formatErrorOutput($errorOutput) + PHP_EOL . $errorOutput ); $this->logger->error($errorMessage); diff --git a/src/Command/Process/ApplyLocal.php b/src/Command/Process/ApplyLocal.php index 3ba8ebde..50db151b 100644 --- a/src/Command/Process/ApplyLocal.php +++ b/src/Command/Process/ApplyLocal.php @@ -95,7 +95,7 @@ public function run(InputInterface $input, OutputInterface $output) $errorMessage = sprintf( 'Applying patch %s failed.%s', $patch->getPath(), - $this->renderer->formatErrorOutput($exception->getMessage()) + PHP_EOL . $exception->getMessage() ); throw new RuntimeException($errorMessage, $exception->getCode()); diff --git a/src/Command/Process/Ece/Revert.php b/src/Command/Process/Ece/Revert.php index b2b93b4e..876267f8 100644 --- a/src/Command/Process/Ece/Revert.php +++ b/src/Command/Process/Ece/Revert.php @@ -121,7 +121,7 @@ function ($patch) { $errorMessage = sprintf( 'Reverting patch %s failed.%s', $patch->getPath(), - $this->renderer->formatErrorOutput($exception->getMessage()) + PHP_EOL . $exception->getMessage() ); $this->printError($output, $errorMessage); } diff --git a/src/Command/Process/Renderer.php b/src/Command/Process/Renderer.php index 319ce7b2..8c39bc9d 100644 --- a/src/Command/Process/Renderer.php +++ b/src/Command/Process/Renderer.php @@ -140,21 +140,6 @@ public function printPatchInfo( $output->writeln(''); } - /** - * Format error output. - * - * @param string $errorOutput - * @return string - */ - public function formatErrorOutput(string $errorOutput): string - { - if (preg_match('#^.*?Error Output:(?<errors>.*?)$#is', $errorOutput, $matches)) { - $errorOutput = PHP_EOL . 'Error Output:' . $matches['errors']; - } - - return $errorOutput; - } - /** * Asks a confirmation question to the user. * diff --git a/src/Patch/Conflict/Processor.php b/src/Patch/Conflict/Processor.php index 29278995..a903b8ee 100644 --- a/src/Patch/Conflict/Processor.php +++ b/src/Patch/Conflict/Processor.php @@ -84,7 +84,7 @@ public function process( 'Applying patch %s (%s) failed.%s%s', $patch->getId(), $patch->getPath(), - $this->renderer->formatErrorOutput($exceptionMessage), + PHP_EOL. $exceptionMessage, $conflictDetails ? PHP_EOL . $conflictDetails : '' ); diff --git a/src/Shell/Command/DriverException.php b/src/Shell/Command/DriverException.php index ea5b01a0..7c6020f1 100644 --- a/src/Shell/Command/DriverException.php +++ b/src/Shell/Command/DriverException.php @@ -37,14 +37,14 @@ private function formatMessage(string $message): string $errorMsg = null; $generalMsg = null; if (preg_match('#^.*?Error Output:(?<errors>.*?)$#is', $result, $matches)) { - $errorMsg = PHP_EOL .'Error Output:' . $matches['errors']; + $errorMsg = 'Error Output:' . $matches['errors']; $result = str_replace($errorMsg, '', $result); if (!trim(str_replace('=', '', $matches['errors']))) { $errorMsg = null; } } if (empty($errorMsg) && preg_match('#^.*?Output:(?<errors>.*?)$#is', $result, $matches)) { - $generalMsg = PHP_EOL .'Output:' . $matches['errors']; + $generalMsg = 'Output:' . $matches['errors']; if (!trim(str_replace('=', '', $matches['errors']))) { $generalMsg = null; } diff --git a/src/Test/Unit/Command/Process/Action/RevertActionTest.php b/src/Test/Unit/Command/Process/Action/RevertActionTest.php index 4c0e3c56..9ef6b34c 100644 --- a/src/Test/Unit/Command/Process/Action/RevertActionTest.php +++ b/src/Test/Unit/Command/Process/Action/RevertActionTest.php @@ -184,9 +184,6 @@ public function testRevertWithException() $this->applier->method('revert') ->willThrowException(new ApplierException('Error')); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->with('Error'); $outputMock->expects($this->once()) ->method('writeln') ->withConsecutive( diff --git a/src/Test/Unit/Command/Process/ApplyLocalTest.php b/src/Test/Unit/Command/Process/ApplyLocalTest.php index cc6490c3..b9f7e85c 100644 --- a/src/Test/Unit/Command/Process/ApplyLocalTest.php +++ b/src/Test/Unit/Command/Process/ApplyLocalTest.php @@ -171,9 +171,6 @@ function ($path, $title) { ->method('process') ->withConsecutive([[$patch1]]) ->willReturn($rollbackMessages); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->with('Applier error message'); $this->expectException(RuntimeException::class); $this->manager->run($inputMock, $outputMock); diff --git a/src/Test/Unit/Command/Process/Ece/RevertTest.php b/src/Test/Unit/Command/Process/Ece/RevertTest.php index 82dacb01..1197ba2b 100644 --- a/src/Test/Unit/Command/Process/Ece/RevertTest.php +++ b/src/Test/Unit/Command/Process/Ece/RevertTest.php @@ -165,10 +165,6 @@ function ($path, $title) { } ); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->with('Applier error message'); - $this->revertAction->expects($this->once()) ->method('execute') ->withConsecutive([$inputMock, $outputMock, []]); diff --git a/src/Test/Unit/Command/Process/RendererTest.php b/src/Test/Unit/Command/Process/RendererTest.php index f78e1612..6a944b4c 100644 --- a/src/Test/Unit/Command/Process/RendererTest.php +++ b/src/Test/Unit/Command/Process/RendererTest.php @@ -122,53 +122,6 @@ public function printPatchInfoDataProvider(): array ]; } - /** - * Tests error output formatting. - * - * @param string $errorOutput - * @param string $expectedOutput - * @dataProvider formatErrorOutputDataProvider - */ - public function testFormatErrorOutput(string $errorOutput, string $expectedOutput) - { - $this->assertEquals($expectedOutput, $this->renderer->formatErrorOutput($errorOutput)); - } - - /** - * @return array - */ - public function formatErrorOutputDataProvider(): array - { - return [ - [ - 'error' => 'The command "\'git\' \'apply\' \'/path/to/patch/MC-1111_test_patch_1.1.1_ce.patch\'" failed. - -Exit Code: 1(General error) - -Working directory: /path/to/patch - -Output: -================ - - -Error Output: -================ -error: patch failed: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php:23 -error: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php: patch does not apply', - - 'expectedOutput' => ' -Error Output: -================ -error: patch failed: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php:23 -error: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php: patch does not apply' - ], - [ - 'error' => 'Some other output', - 'expectedOutput' => 'Some other output' - ], - ]; - } - /** * Creates patch mock. * diff --git a/src/Test/Unit/Patch/Conflict/ProcessorTest.php b/src/Test/Unit/Patch/Conflict/ProcessorTest.php index 00569d60..6b4d8b85 100644 --- a/src/Test/Unit/Patch/Conflict/ProcessorTest.php +++ b/src/Test/Unit/Patch/Conflict/ProcessorTest.php @@ -76,7 +76,6 @@ public function testProcess() $failedPatch = $this->createPatch('MC-3', 'path3'); $exceptionMessage = 'exceptionMessage'; $conflictDetails = 'Conflict details'; - $formattedOutput = 'formattedOutput'; $rollbackMessages = ['Patch 1 has been reverted', 'Patch 2 has been reverted']; /** @var OutputInterface|MockObject $outputMock */ @@ -90,10 +89,6 @@ public function testProcess() ->method('analyze') ->withConsecutive([$failedPatch]) ->willReturn($conflictDetails); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->withConsecutive([$exceptionMessage]) - ->willReturn($formattedOutput); $outputMock->expects($this->exactly(2)) ->method('writeln') ->withConsecutive( @@ -105,7 +100,7 @@ public function testProcess() 'Applying patch %s (%s) failed.%s%s', $failedPatch->getId(), $failedPatch->getPath(), - $formattedOutput, + PHP_EOL .$exceptionMessage, PHP_EOL . $conflictDetails ); diff --git a/src/Test/Unit/Shell/Command/DriverExceptionTest.php b/src/Test/Unit/Shell/Command/DriverExceptionTest.php index f55e3a09..ab5c9974 100644 --- a/src/Test/Unit/Shell/Command/DriverExceptionTest.php +++ b/src/Test/Unit/Shell/Command/DriverExceptionTest.php @@ -47,8 +47,7 @@ public function formatMessageDataProvider(): array error: patch failed: path/to/path/file2.php b/path/to/path/file2.php:23 error: path/to/path/file2.php b/path/to/path/file2.php: patch does not apply', - 'expectedOutput' => ' -Error Output: + 'expectedOutput' => 'Error Output: ================ error: patch failed: path/to/path/file2.php b/path/to/path/file2.php:23 error: path/to/path/file2.php b/path/to/path/file2.php: patch does not apply' @@ -96,8 +95,7 @@ public function formatMessageDataProvider(): array ', - 'expectedOutput' => ' -Output: + 'expectedOutput' => 'Output: ================ Hmm... Looks like a unified diff to me... The text leading up to this was: @@ -127,6 +125,7 @@ public function formatMessageDataProvider(): array 1 out of 3 hunks FAILED -- saving rejects to file path/to/path/file2.php.rej done + ' ], [ From 60a7dabf79490694af2bdd671422a13cd4889ee9 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Thu, 24 Sep 2020 18:17:24 -0500 Subject: [PATCH 067/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Remove dead code --- src/Shell/Command/DriverException.php | 38 ----- .../Shell/Command/DriverExceptionTest.php | 137 ------------------ 2 files changed, 175 deletions(-) delete mode 100644 src/Test/Unit/Shell/Command/DriverExceptionTest.php diff --git a/src/Shell/Command/DriverException.php b/src/Shell/Command/DriverException.php index 7c6020f1..b92a283c 100644 --- a/src/Shell/Command/DriverException.php +++ b/src/Shell/Command/DriverException.php @@ -8,48 +8,10 @@ namespace Magento\CloudPatches\Shell\Command; use Magento\CloudPatches\Patch\PatchCommandException; -use Throwable; /** * Patch command driver exception */ class DriverException extends PatchCommandException { - /** - * @param string $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(string $message, int $code = 0, Throwable $previous = null) - { - parent::__construct($this->formatMessage($message), $code, $previous); - } - - /** - * Format error message - * - * @param string $message - * @return string - */ - private function formatMessage(string $message): string - { - $result = $message; - $errorMsg = null; - $generalMsg = null; - if (preg_match('#^.*?Error Output:(?<errors>.*?)$#is', $result, $matches)) { - $errorMsg = 'Error Output:' . $matches['errors']; - $result = str_replace($errorMsg, '', $result); - if (!trim(str_replace('=', '', $matches['errors']))) { - $errorMsg = null; - } - } - if (empty($errorMsg) && preg_match('#^.*?Output:(?<errors>.*?)$#is', $result, $matches)) { - $generalMsg = 'Output:' . $matches['errors']; - if (!trim(str_replace('=', '', $matches['errors']))) { - $generalMsg = null; - } - } - - return $errorMsg ?? $generalMsg ?? $message; - } } diff --git a/src/Test/Unit/Shell/Command/DriverExceptionTest.php b/src/Test/Unit/Shell/Command/DriverExceptionTest.php deleted file mode 100644 index ab5c9974..00000000 --- a/src/Test/Unit/Shell/Command/DriverExceptionTest.php +++ /dev/null @@ -1,137 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Test\Unit\Shell\Command; - -use Magento\CloudPatches\Shell\Command\DriverException; -use PHPUnit\Framework\TestCase; - -class DriverExceptionTest extends TestCase -{ - /** - * Tests exception message formatting. - * - * @param string $errorOutput - * @param string $expectedOutput - * @dataProvider formatMessageDataProvider - */ - public function testFormatMessage(string $errorOutput, string $expectedOutput) - { - $exception = new DriverException($errorOutput); - $this->assertEquals($expectedOutput, $exception->getMessage()); - } - - /** - * @return array - */ - public function formatMessageDataProvider(): array - { - return [ - [ - 'error' => 'The command "\'patch\' \'--silent\' \'-p1\' \'--dry-run\'" failed. - -Exit Code: 1(General error) - -Working directory: /var/www/html - -Output: -================ - - -Error Output: -================ -error: patch failed: path/to/path/file2.php b/path/to/path/file2.php:23 -error: path/to/path/file2.php b/path/to/path/file2.php: patch does not apply', - - 'expectedOutput' => 'Error Output: -================ -error: patch failed: path/to/path/file2.php b/path/to/path/file2.php:23 -error: path/to/path/file2.php b/path/to/path/file2.php: patch does not apply' - ], - [ - 'error' => 'The command "\'patch\' \'--silent\' \'-p1\' \'--dry-run\'" failed. - -Exit Code: 1(General error) - -Working directory: /var/www/html - -Output: -================ -Hmm... Looks like a unified diff to me... -The text leading up to this was: --------------------------- -|diff --git a/path/to/path/file1.php b/path/to/path/file1.php -|index 320e0adc29b..576281861d3 100644 -|--- a/path/to/path/file1.php -|+++ b/path/to/path/file1.php --------------------------- -Patching file path/to/path/file1.php using Plan A... -Hunk #1 succeeded at 30. -Hunk #2 succeeded at 54. -Hunk #3 succeeded at 76. -Hunk #4 succeeded at 113. -Hmm... The next patch looks like a unified diff to me... -The text leading up to this was: --------------------------- -|diff --git a/path/to/path/file2.php b/path/to/path/file2.php -|index 0ec65c88024..e550de9cb03 100644 -|--- a/path/to/path/file2.php -|+++ b/path/to/path/file2.php --------------------------- -Patching file path/to/path/file2.php using Plan A... -Hunk #1 succeeded at 71. -Hunk #2 FAILED at 136. -Hunk #3 succeeded at 154. -1 out of 3 hunks FAILED -- saving rejects to file path/to/path/file2.php.rej -done - - -Error Output: -================ - -', - - 'expectedOutput' => 'Output: -================ -Hmm... Looks like a unified diff to me... -The text leading up to this was: --------------------------- -|diff --git a/path/to/path/file1.php b/path/to/path/file1.php -|index 320e0adc29b..576281861d3 100644 -|--- a/path/to/path/file1.php -|+++ b/path/to/path/file1.php --------------------------- -Patching file path/to/path/file1.php using Plan A... -Hunk #1 succeeded at 30. -Hunk #2 succeeded at 54. -Hunk #3 succeeded at 76. -Hunk #4 succeeded at 113. -Hmm... The next patch looks like a unified diff to me... -The text leading up to this was: --------------------------- -|diff --git a/path/to/path/file2.php b/path/to/path/file2.php -|index 0ec65c88024..e550de9cb03 100644 -|--- a/path/to/path/file2.php -|+++ b/path/to/path/file2.php --------------------------- -Patching file path/to/path/file2.php using Plan A... -Hunk #1 succeeded at 71. -Hunk #2 FAILED at 136. -Hunk #3 succeeded at 154. -1 out of 3 hunks FAILED -- saving rejects to file path/to/path/file2.php.rej -done - - -' - ], - [ - 'error' => 'Some other output', - 'expectedOutput' => 'Some other output' - ], - ]; - } -} From 8644f03779aa9523b41a8a8d0681516546a394e3 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Thu, 24 Sep 2020 18:30:51 -0500 Subject: [PATCH 068/157] - Update error message --- src/Command/Process/ApplyLocal.php | 2 +- src/Patch/Conflict/Processor.php | 4 ++-- src/Test/Unit/Patch/Conflict/ProcessorTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Command/Process/ApplyLocal.php b/src/Command/Process/ApplyLocal.php index 50db151b..6316fab1 100644 --- a/src/Command/Process/ApplyLocal.php +++ b/src/Command/Process/ApplyLocal.php @@ -89,7 +89,7 @@ public function run(InputInterface $input, OutputInterface $output) $this->printInfo($output, $message); array_push($appliedPatches, $patch); } catch (ApplierException $exception) { - $this->printError($output, 'Error: patch conflict happened'); + $this->printError($output, 'Error: patch can\'t be applied'); $messages = $this->rollbackProcessor->process($appliedPatches); $output->writeln($messages); $errorMessage = sprintf( diff --git a/src/Patch/Conflict/Processor.php b/src/Patch/Conflict/Processor.php index a903b8ee..68b99de6 100644 --- a/src/Patch/Conflict/Processor.php +++ b/src/Patch/Conflict/Processor.php @@ -73,7 +73,7 @@ public function process( array $appliedPatches, string $exceptionMessage ) { - $errorMessage = 'Error: patch conflict happened'; + $errorMessage = 'Error: patch can\'t be applied'; $this->logger->error($errorMessage); $output->writeln('<error>' . $errorMessage . '</error>'); @@ -84,7 +84,7 @@ public function process( 'Applying patch %s (%s) failed.%s%s', $patch->getId(), $patch->getPath(), - PHP_EOL. $exceptionMessage, + PHP_EOL . $exceptionMessage, $conflictDetails ? PHP_EOL . $conflictDetails : '' ); diff --git a/src/Test/Unit/Patch/Conflict/ProcessorTest.php b/src/Test/Unit/Patch/Conflict/ProcessorTest.php index 6b4d8b85..8f09459e 100644 --- a/src/Test/Unit/Patch/Conflict/ProcessorTest.php +++ b/src/Test/Unit/Patch/Conflict/ProcessorTest.php @@ -92,7 +92,7 @@ public function testProcess() $outputMock->expects($this->exactly(2)) ->method('writeln') ->withConsecutive( - [$this->stringContains('Error: patch conflict happened')], + [$this->stringContains('Error: patch can\'t be applied')], [$rollbackMessages] ); From 04156562ba97aba766d52149d091b55adef9a558 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Fri, 25 Sep 2020 07:22:31 -0500 Subject: [PATCH 069/157] MC-37324: Add fallback to 'patch' command when 'git' command is not available - Remove redundant exceptions --- config/services.xml | 2 -- src/Shell/Command/DriverException.php | 13 ++++++++++ src/Shell/Command/GitDriver.php | 8 +++--- src/Shell/Command/GitDriverException.php | 29 ---------------------- src/Shell/Command/PatchDriver.php | 8 +++--- src/Shell/Command/PatchDriverException.php | 29 ---------------------- 6 files changed, 21 insertions(+), 68 deletions(-) delete mode 100644 src/Shell/Command/GitDriverException.php delete mode 100644 src/Shell/Command/PatchDriverException.php diff --git a/config/services.xml b/config/services.xml index d9ce6831..6c668fb3 100644 --- a/config/services.xml +++ b/config/services.xml @@ -30,8 +30,6 @@ <service id="Magento\CloudPatches\Patch\PatchCommandNotFound" autowire="false"/> <service id="Magento\CloudPatches\Patch\PatchCommandException" autowire="false"/> <service id="Magento\CloudPatches\Shell\Command\DriverException" autowire="false"/> - <service id="Magento\CloudPatches\Shell\Command\GitDriverException" autowire="false"/> - <service id="Magento\CloudPatches\Shell\Command\PatchDriverException" autowire="false"/> <service id="Magento\CloudPatches\Patch\PatchCommand" autowire="false"/> <service id="Magento\CloudPatches\Patch\PatchCommandInterface" alias="Magento\CloudPatches\Patch\PatchCommand"/> <service id="statusPool" class="Magento\CloudPatches\Patch\Status\StatusPool" lazy="true"> diff --git a/src/Shell/Command/DriverException.php b/src/Shell/Command/DriverException.php index b92a283c..a2f7dd06 100644 --- a/src/Shell/Command/DriverException.php +++ b/src/Shell/Command/DriverException.php @@ -8,10 +8,23 @@ namespace Magento\CloudPatches\Shell\Command; use Magento\CloudPatches\Patch\PatchCommandException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Throwable; /** * Patch command driver exception */ class DriverException extends PatchCommandException { + /** + * @param Throwable $previous + */ + public function __construct(Throwable $previous) + { + $message = $previous->getMessage(); + if ($previous instanceof ProcessFailedException) { + $message = $previous->getProcess()->getErrorOutput() ?: ($previous->getProcess()->getOutput() ?: $message); + } + parent::__construct($message, $previous->getCode(), $previous); + } } diff --git a/src/Shell/Command/GitDriver.php b/src/Shell/Command/GitDriver.php index 16b6a56a..a03eb616 100644 --- a/src/Shell/Command/GitDriver.php +++ b/src/Shell/Command/GitDriver.php @@ -38,7 +38,7 @@ public function apply(string $patch) $this->processFactory->create(['git', 'apply'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new GitDriverException($exception); + throw new DriverException($exception); } } @@ -51,7 +51,7 @@ public function revert(string $patch) $this->processFactory->create(['git', 'apply', '--reverse'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new GitDriverException($exception); + throw new DriverException($exception); } } @@ -64,7 +64,7 @@ public function applyCheck(string $patch) $this->processFactory->create(['git', 'apply', '--check'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new GitDriverException($exception); + throw new DriverException($exception); } } @@ -77,7 +77,7 @@ public function revertCheck(string $patch) $this->processFactory->create(['git', 'apply', '--reverse', '--check'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new GitDriverException($exception); + throw new DriverException($exception); } } diff --git a/src/Shell/Command/GitDriverException.php b/src/Shell/Command/GitDriverException.php deleted file mode 100644 index 490af43a..00000000 --- a/src/Shell/Command/GitDriverException.php +++ /dev/null @@ -1,29 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Shell\Command; - -use Symfony\Component\Process\Exception\ProcessFailedException; -use Throwable; - -/** - * Git patch driver exception - */ -class GitDriverException extends DriverException -{ - /** - * @param Throwable $previous - */ - public function __construct(Throwable $previous) - { - $message = $previous->getMessage(); - if ($previous instanceof ProcessFailedException) { - $message = $previous->getProcess()->getErrorOutput() ?: ($previous->getProcess()->getOutput() ?: $message); - } - parent::__construct($message, $previous->getCode(), $previous); - } -} diff --git a/src/Shell/Command/PatchDriver.php b/src/Shell/Command/PatchDriver.php index 11b53887..b22c4cca 100644 --- a/src/Shell/Command/PatchDriver.php +++ b/src/Shell/Command/PatchDriver.php @@ -39,7 +39,7 @@ public function apply(string $patch) $this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchDriverException($exception); + throw new DriverException($exception); } } @@ -53,7 +53,7 @@ public function revert(string $patch) $this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch', '--reverse'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchDriverException($exception); + throw new DriverException($exception); } } @@ -66,7 +66,7 @@ public function applyCheck(string $patch) $this->processFactory->create(['patch', '--silent', '-p1', '--dry-run'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchDriverException($exception); + throw new DriverException($exception); } } @@ -79,7 +79,7 @@ public function revertCheck(string $patch) $this->processFactory->create(['patch', '--silent', '-p1', '--reverse', '--dry-run'], $patch) ->mustRun(); } catch (ProcessFailedException $exception) { - throw new PatchDriverException($exception); + throw new DriverException($exception); } } diff --git a/src/Shell/Command/PatchDriverException.php b/src/Shell/Command/PatchDriverException.php deleted file mode 100644 index 39b09f8b..00000000 --- a/src/Shell/Command/PatchDriverException.php +++ /dev/null @@ -1,29 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Shell\Command; - -use Symfony\Component\Process\Exception\ProcessFailedException; -use Throwable; - -/** - * Unix patch driver exception - */ -class PatchDriverException extends DriverException -{ - /** - * @param Throwable $previous - */ - public function __construct(Throwable $previous) - { - $message = $previous->getMessage(); - if ($previous instanceof ProcessFailedException) { - $message = $previous->getProcess()->getErrorOutput() ?: ($previous->getProcess()->getOutput() ?: $message); - } - parent::__construct($message, $previous->getCode(), $previous); - } -} From 9374c7344a0e1498ca07da5fde1760e61194d4af Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Wed, 7 Oct 2020 12:55:26 -0500 Subject: [PATCH 070/157] MC-38123: Update Symfony dependencies for compatibility with Magento 2.4 OnPrem (#1) --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5343a1fd..dbfdd943 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,11 @@ "ext-json": "*", "composer/composer": "@stable", "composer/semver": "^1.5", - "symfony/config": "^3.3||^4.4", + "symfony/config": "^3.3||^4.4||^5.1", "symfony/console": "^2.6||^4.0", - "symfony/dependency-injection": "^3.3||^4.3", + "symfony/dependency-injection": "^3.3||^4.3||^5.1", "symfony/process": "^2.1||^4.1", - "symfony/proxy-manager-bridge": "^3.3||^4.3", + "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.1", "symfony/yaml": "^3.3||^4.0", "monolog/monolog": "^1.16" }, From 8b70b5b318e3630df918655c21ef944cb64d443c Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Thu, 8 Oct 2020 11:55:14 -0500 Subject: [PATCH 071/157] - Add dependency on magento/quality-patches (#4) - Bump version --- composer.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index dbfdd943..f75442f0 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.6", + "version": "1.0.7", "license": "OSL-3.0", "require": { "php": "^7.0", @@ -15,7 +15,8 @@ "symfony/process": "^2.1||^4.1", "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.1", "symfony/yaml": "^3.3||^4.0", - "monolog/monolog": "^1.16" + "monolog/monolog": "^1.16", + "magento/quality-patches": "^1.0.6" }, "require-dev": { "codeception/codeception": "^2.5.3", @@ -24,9 +25,6 @@ "phpunit/phpunit": "^6.2", "squizlabs/php_codesniffer": "^3.0" }, - "suggest": { - "magento/quality-patches": "^1.0.3" - }, "bin": [ "bin/ece-patches", "bin/magento-patches" From 23feb3c3cf773fc3b65e28d7059c38ab5dffec51 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Wed, 14 Oct 2020 19:16:15 -0500 Subject: [PATCH 072/157] MC-38451: Update MCP Symfony dependencies for compatibility with Magento 2.4.1 OnPrem (#6) --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index f75442f0..3919e9cf 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.7", + "version": "1.0.8", "license": "OSL-3.0", "require": { "php": "^7.0", @@ -10,13 +10,13 @@ "composer/composer": "@stable", "composer/semver": "^1.5", "symfony/config": "^3.3||^4.4||^5.1", - "symfony/console": "^2.6||^4.0", + "symfony/console": "^2.6||^4.0||^5.1", "symfony/dependency-injection": "^3.3||^4.3||^5.1", - "symfony/process": "^2.1||^4.1", + "symfony/process": "^2.1||^4.1||^5.1", "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.1", - "symfony/yaml": "^3.3||^4.0", + "symfony/yaml": "^3.3||^4.0||^5.1", "monolog/monolog": "^1.16", - "magento/quality-patches": "^1.0.6" + "magento/quality-patches": "^1.0.7" }, "require-dev": { "codeception/codeception": "^2.5.3", From 5b720e0681ad26ee278ee770750173673a076e26 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Thu, 15 Oct 2020 15:17:38 -0500 Subject: [PATCH 073/157] Added Magento 2.3.6, 2.4.0-p1 and 2.4.1 for testing patches --- src/Test/Functional/Acceptance/Acceptance73Cest.php | 1 + src/Test/Functional/Acceptance/AcceptanceCest.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/Test/Functional/Acceptance/Acceptance73Cest.php b/src/Test/Functional/Acceptance/Acceptance73Cest.php index 45ffa649..a6e872b4 100644 --- a/src/Test/Functional/Acceptance/Acceptance73Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance73Cest.php @@ -24,6 +24,7 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.3.4', 'magentoVersion' => '2.3.4-p2'], ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5'], ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5-p1'], + ['templateVersion' => '2.3.6', 'magentoVersion' => '2.3.6'], ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], ]; } diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 406a675f..da4a806c 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -50,6 +50,8 @@ protected function patchesDataProvider(): array { return [ ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], + ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0-p1'], + ['templateVersion' => '2.4.1'], ['templateVersion' => 'master'], ]; } From ab030d3dc2805cc1b09c4b86b24981011cc7bc36 Mon Sep 17 00:00:00 2001 From: Alejandro Marroni <alegringus@gmail.com> Date: Wed, 25 Nov 2020 15:06:51 -0300 Subject: [PATCH 074/157] minor fix to prevent compativility issue with PHP 7.4 --- src/Patch/Pool/OptionalPool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Patch/Pool/OptionalPool.php b/src/Patch/Pool/OptionalPool.php index 80c3cf0c..4359829c 100644 --- a/src/Patch/Pool/OptionalPool.php +++ b/src/Patch/Pool/OptionalPool.php @@ -231,7 +231,7 @@ function (PatchInterface $patch) { $diff = array_diff($filter, $resultIds); if (count($diff) > 0) { throw new PatchNotFoundException( - 'Next patches weren\'t found: ' . implode($diff, ' ') . '. ' . + 'Next patches weren\'t found: ' . implode($diff) . '. ' . 'Please, check with "status" command availability of these patches for the current Magento version.' ); } From 7c031dcfb59d5639ab045134adfd9b69a64a4bd4 Mon Sep 17 00:00:00 2001 From: Alejandro Marroni <alegringus@gmail.com> Date: Wed, 25 Nov 2020 15:09:41 -0300 Subject: [PATCH 075/157] adding space between array --- src/Patch/Pool/OptionalPool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Patch/Pool/OptionalPool.php b/src/Patch/Pool/OptionalPool.php index 4359829c..6241aef1 100644 --- a/src/Patch/Pool/OptionalPool.php +++ b/src/Patch/Pool/OptionalPool.php @@ -231,7 +231,7 @@ function (PatchInterface $patch) { $diff = array_diff($filter, $resultIds); if (count($diff) > 0) { throw new PatchNotFoundException( - 'Next patches weren\'t found: ' . implode($diff) . '. ' . + 'Next patches weren\'t found: ' . implode(' ', $diff) . '. ' . 'Please, check with "status" command availability of these patches for the current Magento version.' ); } From 0521e536420202f84bca9f65f5766d89fd549ba9 Mon Sep 17 00:00:00 2001 From: Magento Community Engineering <31669971+magento-engcom-team@users.noreply.github.com> Date: Fri, 4 Dec 2020 13:16:42 -0600 Subject: [PATCH 076/157] [Imported] PHP 7.4 compatibility fix using implode() (#10) --- src/Patch/Pool/OptionalPool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Patch/Pool/OptionalPool.php b/src/Patch/Pool/OptionalPool.php index 80c3cf0c..6241aef1 100644 --- a/src/Patch/Pool/OptionalPool.php +++ b/src/Patch/Pool/OptionalPool.php @@ -231,7 +231,7 @@ function (PatchInterface $patch) { $diff = array_diff($filter, $resultIds); if (count($diff) > 0) { throw new PatchNotFoundException( - 'Next patches weren\'t found: ' . implode($diff, ' ') . '. ' . + 'Next patches weren\'t found: ' . implode(' ', $diff) . '. ' . 'Please, check with "status" command availability of these patches for the current Magento version.' ); } From 0797914928854c7b810c90f8f45fb0353d84c1f6 Mon Sep 17 00:00:00 2001 From: Oleksandr Shmyheliuk <oshmyheliuk@magento.com> Date: Thu, 10 Dec 2020 10:52:34 -0600 Subject: [PATCH 077/157] Revert "[Imported] PHP 7.4 compatibility fix using implode() (#10)" (#11) This reverts commit 0521e536420202f84bca9f65f5766d89fd549ba9. --- src/Patch/Pool/OptionalPool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Patch/Pool/OptionalPool.php b/src/Patch/Pool/OptionalPool.php index 6241aef1..80c3cf0c 100644 --- a/src/Patch/Pool/OptionalPool.php +++ b/src/Patch/Pool/OptionalPool.php @@ -231,7 +231,7 @@ function (PatchInterface $patch) { $diff = array_diff($filter, $resultIds); if (count($diff) > 0) { throw new PatchNotFoundException( - 'Next patches weren\'t found: ' . implode(' ', $diff) . '. ' . + 'Next patches weren\'t found: ' . implode($diff, ' ') . '. ' . 'Please, check with "status" command availability of these patches for the current Magento version.' ); } From e95f66e44a615e47aab13a4954590ca8c1be9635 Mon Sep 17 00:00:00 2001 From: Dmytro Yushkin <dyushkin@adobe.com> Date: Fri, 8 Jan 2021 11:12:25 -0600 Subject: [PATCH 078/157] =?UTF-8?q?MCLOUD-6923:=20Layered=20navigation=20f?= =?UTF-8?q?ilter=20is=20present=20only=20when=20product=20i=E2=80=A6=20(#1?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 1 + patches.json | 5 ++ ...e_with_enabled_shared_catalog__2.3.5.patch | 58 +++++++++++++++++++ travis.php.ini | 1 + 4 files changed, 65 insertions(+) create mode 100644 patches/MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch create mode 100644 travis.php.ini diff --git a/.travis.yml b/.travis.yml index 434d0daf..5dba2f04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -74,6 +74,7 @@ before_install: - if [ -n "${COMPOSER_VERSION}" ]; then travis_retry composer self-update ${COMPOSER_VERSION}; fi; install: + - phpenv config-add travis.php.ini - composer config http-basic.repo.magento.com ${REPO_USERNAME_CE} ${REPO_PASSWORD_CE} - composer config github-oauth.github.com ${GITHUB_TOKEN} - if [ -n "${MCD_VERSION}" ] && [ $TRAVIS_PHP_VERSION != "7.0" ]; then composer config repositories.mcd git git@github.com:magento/magento-cloud-docker.git && composer require "magento/magento-cloud-docker:${MCD_VERSION}" --no-update; fi; diff --git a/patches.json b/patches.json index fef508e2..a9f3987d 100644 --- a/patches.json +++ b/patches.json @@ -340,6 +340,11 @@ "4.1.0": "BUNDLE-2683__braintree_settlement_report_fix__4.1.0.patch" } }, + "magento/magento2-b2b-base": { + "Layered navigation filter is present only when product is present on the listing page with enabled Shared catalog": { + ">=1.1.5 <1.3.1": "MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch" + } + }, "magento/magento2-ee-base": { "Fix pagebuilder module": { "2.3.1": "PB-319__fix_pagebuilder_module__2.3.1.patch", diff --git a/patches/MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch b/patches/MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch new file mode 100644 index 00000000..584d1a40 --- /dev/null +++ b/patches/MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch @@ -0,0 +1,58 @@ +diff -Nuar a/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregation/Builder/DataProvider.php b/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregation/Builder/DataProvider.php +--- a/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregation/Builder/DataProvider.php ++++ b/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregation/Builder/DataProvider.php +@@ -103,18 +103,16 @@ public function __construct( + * + * @param BucketInterface $bucket + * @param array $dimensions +- * @param array $documentIds + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Statement_Exception + */ + public function getAggregation( + BucketInterface $bucket, +- array $dimensions, +- array $documentIds ++ array $dimensions + ) { + $result = []; +- $select = $this->getSelect($bucket, $dimensions, $documentIds); ++ $select = $this->getSelect($bucket, $dimensions); + $query = $this->connection->query($select); + + while ($row = $query->fetch()) { +@@ -134,11 +132,10 @@ public function getAggregation( + * + * @param BucketInterface $bucket + * @param array $dimensions +- * @param array $documentIds + * @return Select + * @throws \Magento\Framework\Exception\LocalizedException + */ +- private function getSelect(BucketInterface $bucket, array $dimensions, array $documentIds) ++ private function getSelect(BucketInterface $bucket, array $dimensions) + { + $attribute = $this->eavConfig->getAttribute(Product::ENTITY, $bucket->getField()); + $currentScope = $this->scopeResolver->getScope($dimensions['scope']->getValue()); +@@ -161,7 +158,6 @@ private function getSelect(BucketInterface $bucket, array $dimensions, array $do + 'source_entity.sku = shared_catalog_item.sku', + [] + ) +- ->where('eav.entity_id IN (?)', $documentIds) + ->where('eav.attribute_id = ?', $attribute->getId()) + ->where('eav.store_id = ? ', $currentScope->getId()) + ->where('source_entity.type_id <> ?', 'configurable') +diff -Nuar a/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php b/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php +--- a/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php ++++ b/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php +@@ -65,8 +65,7 @@ public function afterBuild( + $shouldAggregate = ($bucket->getField() !== 'category_ids'); + + if ($sharedCatalogIsEnabled && $shouldAggregate) { +- $documentIds = array_column($queryResult['hits']['hits'], '_id'); +- $values = $this->dataProvider->getAggregation($bucket, $dimensions, $documentIds); ++ $values = $this->dataProvider->getAggregation($bucket, $dimensions); + } + + return $values; diff --git a/travis.php.ini b/travis.php.ini new file mode 100644 index 00000000..0b134267 --- /dev/null +++ b/travis.php.ini @@ -0,0 +1 @@ +memory_limit = 4G From b31f56223f9728ef77c836014f42c713c7f08d41 Mon Sep 17 00:00:00 2001 From: Ben Batschelet <bbatsche@users.noreply.github.com> Date: Thu, 21 Jan 2021 10:34:27 -0600 Subject: [PATCH 079/157] Add @group to PatchApplierCest (#14) --- src/Test/Functional/Acceptance/PatchApplierCest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Test/Functional/Acceptance/PatchApplierCest.php b/src/Test/Functional/Acceptance/PatchApplierCest.php index 9c5492c1..e14f4db7 100644 --- a/src/Test/Functional/Acceptance/PatchApplierCest.php +++ b/src/Test/Functional/Acceptance/PatchApplierCest.php @@ -10,7 +10,7 @@ use Magento\CloudDocker\Test\Functional\Codeception\Docker; /** - * This test runs on the latest version of PHP + * @group php74 */ class PatchApplierCest extends AbstractCest { @@ -21,6 +21,7 @@ public function _before(\CliTester $I): void { parent::_before($I); + $this->prepareTemplate($I, 'master'); $I->copyFileToWorkDir('files/debug_logging/.magento.env.yaml', '.magento.env.yaml'); } @@ -42,7 +43,7 @@ public function testApplyingPatch(\CliTester $I): void $I->assertContains('# Hello Magento', $targetFile); $I->assertContains('## Additional Info', $targetFile); $log = $I->grabFileContent('/var/log/cloud.log', Docker::BUILD_CONTAINER); - $I->assertContains('Patch "/app/m2-hotfixes/patch.patch" applied', $log); + $I->assertContains('Patch ../m2-hotfixes/patch.patch has been applied', $log); } /** @@ -63,7 +64,7 @@ public function testApplyingExistingPatch(\CliTester $I): void $I->assertContains('# Hello Magento', $targetFile); $I->assertContains('## Additional Info', $targetFile); $I->assertContains( - 'Patch "/app/m2-hotfixes/patch.patch" was already applied', + 'Patch ../m2-hotfixes/patch.patch was already applied', $I->grabFileContent('/var/log/cloud.log', Docker::BUILD_CONTAINER) ); } From 6e48b53de4ab3496e6b66612e1b3e2e0869433c3 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Fri, 22 Jan 2021 10:34:33 -0600 Subject: [PATCH 080/157] MCLOUD-7521: Add using a new tests infrastructure to MCP repo (#16) --- src/Test/Functional/Acceptance/AcceptanceCest.php | 4 ++-- src/Test/Functional/Acceptance/PatchApplierCest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index da4a806c..c532f87a 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -30,8 +30,8 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void { $this->prepareTemplate($I, $data['templateVersion'], $data['magentoVersion'] ?? null); $I->copyFileToWorkDir('files/patches/.apply_quality_patches.env.yaml', '.magento.env.yaml'); - $I->runEceDockerCommand(sprintf( - 'build:compose --mode=production --env-vars="%s"', + $I->generateDockerCompose(sprintf( + '--mode=production --env-vars="%s"', $this->convertEnvFromArrayToJson(['MAGENTO_CLOUD_PROJECT' => 'travis-testing']) )); $I->assertTrue($I->runDockerComposeCommand('run build cloud-build')); diff --git a/src/Test/Functional/Acceptance/PatchApplierCest.php b/src/Test/Functional/Acceptance/PatchApplierCest.php index e14f4db7..d3c7b686 100644 --- a/src/Test/Functional/Acceptance/PatchApplierCest.php +++ b/src/Test/Functional/Acceptance/PatchApplierCest.php @@ -31,7 +31,7 @@ public function _before(\CliTester $I): void */ public function testApplyingPatch(\CliTester $I): void { - $I->runEceDockerCommand('build:compose --mode=production'); + $I->generateDockerCompose('--mode=production'); $I->copyFileToWorkDir('files/patches/target_file.md', 'target_file.md'); $I->copyFileToWorkDir('files/patches/patch.patch', 'm2-hotfixes/patch.patch'); @@ -52,7 +52,7 @@ public function testApplyingPatch(\CliTester $I): void */ public function testApplyingExistingPatch(\CliTester $I): void { - $I->runEceDockerCommand('build:compose --mode=production'); + $I->generateDockerCompose('--mode=production'); $I->copyFileToWorkDir('files/patches/target_file_applied_patch.md', 'target_file.md'); $I->copyFileToWorkDir('files/patches/patch.patch', 'm2-hotfixes/patch.patch'); From 5ea435ebbbcf1da06c49f008c05873e74309656c Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Mon, 25 Jan 2021 11:40:09 -0600 Subject: [PATCH 081/157] MC-40529: 'Git apply' command silently fails to apply the patch in some environments (#17) --- src/Command/Apply.php | 3 ++- src/Command/Ece/Apply.php | 3 ++- src/Command/Ece/Revert.php | 3 ++- src/Command/Revert.php | 3 ++- src/Patch/Applier.php | 17 +++++++++++++ src/Test/Unit/Patch/ApplierTest.php | 39 +++++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/Command/Apply.php b/src/Command/Apply.php index ce77f79c..612f90a2 100644 --- a/src/Command/Apply.php +++ b/src/Command/Apply.php @@ -83,12 +83,13 @@ protected function configure() */ public function execute(InputInterface $input, OutputInterface $output) { - $this->logger->notice($this->magentoVersion->get()); + $this->logger->info($this->magentoVersion->get()); try { $this->applyOptional->run($input, $output); } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); + $this->logger->info($this->magentoVersion->get()); $this->logger->error($e->getMessage()); return self::RETURN_FAILURE; diff --git a/src/Command/Ece/Apply.php b/src/Command/Ece/Apply.php index 6090a384..c19f5da0 100644 --- a/src/Command/Ece/Apply.php +++ b/src/Command/Ece/Apply.php @@ -91,7 +91,7 @@ protected function configure() */ public function execute(InputInterface $input, OutputInterface $output) { - $this->logger->notice($this->magentoVersion->get()); + $this->logger->info($this->magentoVersion->get()); try { $this->applyRequired->run($input, $output); @@ -99,6 +99,7 @@ public function execute(InputInterface $input, OutputInterface $output) $this->applyLocal->run($input, $output); } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); + $this->logger->info($this->magentoVersion->get()); $this->logger->error($e->getMessage()); return self::RETURN_FAILURE; diff --git a/src/Command/Ece/Revert.php b/src/Command/Ece/Revert.php index bd8cde31..a06e3914 100644 --- a/src/Command/Ece/Revert.php +++ b/src/Command/Ece/Revert.php @@ -73,12 +73,13 @@ protected function configure() */ public function execute(InputInterface $input, OutputInterface $output) { - $this->logger->notice($this->magentoVersion->get()); + $this->logger->info($this->magentoVersion->get()); try { $this->revert->run($input, $output); } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); + $this->logger->info($this->magentoVersion->get()); $this->logger->error($e->getMessage()); return self::RETURN_FAILURE; diff --git a/src/Command/Revert.php b/src/Command/Revert.php index 8d6f99ed..7a29a3da 100644 --- a/src/Command/Revert.php +++ b/src/Command/Revert.php @@ -96,12 +96,13 @@ protected function configure() */ public function execute(InputInterface $input, OutputInterface $output) { - $this->logger->notice($this->magentoVersion->get()); + $this->logger->info($this->magentoVersion->get()); try { $this->revert->run($input, $output); } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); + $this->logger->info($this->magentoVersion->get()); $this->logger->error($e->getMessage()); return self::RETURN_FAILURE; diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php index 93ecfb64..4ac768da 100644 --- a/src/Patch/Applier.php +++ b/src/Patch/Applier.php @@ -175,6 +175,23 @@ private function prepareContent(string $content): string $content = $this->gitConverter->convert($content); } + $content = $this->replaceDiffGit($content); + return $content; } + + /** + * Replace `diff --git` + * + * In some environments 'git apply' command skips applying a patch + * while execution inside the git working tree. To prevent this issue + * we need to replace `diff --git` with `diff -Nuar` in the patch source. + * + * @param string $content + * @return string + */ + private function replaceDiffGit(string $content): string + { + return preg_replace(['/^diff --git/', '/\ndiff --git/'], ['diff -Nuar', "\ndiff -Nuar"], $content); + } } diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php index cd08b379..89452b6c 100644 --- a/src/Test/Unit/Patch/ApplierTest.php +++ b/src/Test/Unit/Patch/ApplierTest.php @@ -278,4 +278,43 @@ public function testStatusApplied() $this->assertSame(StatusPool::APPLIED, $this->applier->status($patchContent)); } + + /** + * Tests checkApply operation. + * + * Verifies that 'diff --git' is replaced with 'diff -Nuar' + */ + public function testCheckApply() + { + $patchContent = 'diff --git a/vendor/module-deploy/Queue.php b/vendor/module-deploy/Queue.php +--- a/vendor/magento/module-deploy/Process/Queue.php ++++ b/vendor/magento/module-deploy/Process/Queue.php +diff --git a/vendor/magento/module-email/Model/Transport.php b/vendor/magento/module-email/Model/Transport.php +--- a/vendor/magento/module-email/Model/Transport.php ++++ b/vendor/magento/module-email/Model/Transport.php +- echo "diff --git"; ++ echo "diff --Nuar"; +diff -Nuar a/vendor/magento/module-email/Model/Transport.php b/vendor/magento/module-email/Model/Transport.php +'; + $expectedPatchContent = 'diff -Nuar a/vendor/module-deploy/Queue.php b/vendor/module-deploy/Queue.php +--- a/vendor/magento/module-deploy/Process/Queue.php ++++ b/vendor/magento/module-deploy/Process/Queue.php +diff -Nuar a/vendor/magento/module-email/Model/Transport.php b/vendor/magento/module-email/Model/Transport.php +--- a/vendor/magento/module-email/Model/Transport.php ++++ b/vendor/magento/module-email/Model/Transport.php +- echo "diff --git"; ++ echo "diff --Nuar"; +diff -Nuar a/vendor/magento/module-email/Model/Transport.php b/vendor/magento/module-email/Model/Transport.php +'; + + $this->magentoVersion->expects($this->once()) + ->method('isGitBased') + ->willReturn(false); + + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($expectedPatchContent); + + $this->assertTrue($this->applier->checkApply($patchContent)); + } } From ff89ad254e00655994d4036e7d7ff605886560e5 Mon Sep 17 00:00:00 2001 From: Oleksandr Shmyheliuk <oshmyheliuk@magento.com> Date: Wed, 27 Jan 2021 09:16:09 -0600 Subject: [PATCH 082/157] MCLOUD-7583: Release ece-tools 2002.1.5 and magento-cloud-patches 1.0.9 (#19) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3919e9cf..ffc78b48 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.8", + "version": "1.0.9", "license": "OSL-3.0", "require": { "php": "^7.0", From 3361e846cffc705ffdc2f1793f31b20533ebc2c9 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Wed, 27 Jan 2021 13:12:29 -0600 Subject: [PATCH 083/157] MC-40626: 'magento-patches status' command shows deprecated patch (#18) --- src/Command/Process/ShowStatus.php | 20 ++++++++++++--- .../Unit/Command/Process/ShowStatusTest.php | 25 ++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 0ae52946..7cb26cd9 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -7,7 +7,6 @@ namespace Magento\CloudPatches\Command\Process; -use Magento\CloudPatches\App\RuntimeException; use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; use Magento\CloudPatches\Patch\Pool\LocalPool; @@ -89,7 +88,7 @@ public function run(InputInterface $input, OutputInterface $output) array_merge($this->optionalPool->getList(), $this->localPool->getList()) ); foreach ($patches as $patch) { - if ($patch->isDeprecated() && $this->statusPool->isApplied($patch->getId())) { + if ($patch->isDeprecated() && $this->isPatchVisible($patch)) { $this->printDeprecatedWarning($output, $patch); } } @@ -97,10 +96,10 @@ public function run(InputInterface $input, OutputInterface $output) $patches = array_filter( $patches, function ($patch) { - return !$patch->isDeprecated() || $this->statusPool->isApplied($patch->getId()); + return !$patch->isDeprecated() || $this->isPatchVisible($patch); } ); - $this->renderer->printTable($output, $patches); + $this->renderer->printTable($output, array_values($patches)); } /** @@ -138,4 +137,17 @@ private function printDeprecatedWarning(OutputInterface $output, AggregatedPatch ); $output->writeln($message); } + + /** + * Defines if the patch should be visible in the status table. + * + * @param AggregatedPatchInterface $patch + * @return bool + */ + private function isPatchVisible(AggregatedPatchInterface $patch): bool + { + return $patch->getReplacedWith() ? + $this->statusPool->isApplied($patch->getId()) && !$this->statusPool->isApplied($patch->getReplacedWith()) : + $this->statusPool->isApplied($patch->getId()); + } } diff --git a/src/Test/Unit/Command/Process/ShowStatusTest.php b/src/Test/Unit/Command/Process/ShowStatusTest.php index a2dcfd86..b5a555a3 100644 --- a/src/Test/Unit/Command/Process/ShowStatusTest.php +++ b/src/Test/Unit/Command/Process/ShowStatusTest.php @@ -11,6 +11,7 @@ use Magento\CloudPatches\Command\Process\Renderer; use Magento\CloudPatches\Command\Process\ShowStatus; use Magento\CloudPatches\Patch\Aggregator; +use Magento\CloudPatches\Patch\Data\AggregatedPatch; use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Patch\Pool\LocalPool; @@ -89,17 +90,25 @@ protected function setUp() * Patch 1 - deprecated, applied - show warning message, show patch in the table; * Patch 2 - not deprecated, not applied - no warning message, show patch in the table; * Patch 3 - deprecated, not applied - no warning message, don't show patch in the table; + * Patch 4 - deprecated, applied and replaced with applied patch4-v2 - don't show patch in the table; + * Patch 5 - deprecated, applied and replaced with not applied patch5-v2 - show patch in the table; */ public function testShowStatus() { $patch1 = $this->createPatch('patch1', true); $patch2 = $this->createPatch('patch2', false); $patch3 = $this->createPatch('patch3', true); + $patch4 = $this->createPatch('patch4', true, 'patch4-v2'); + $patch5 = $this->createPatch('patch5', true, 'patch5-v2'); $this->statusPool->method('isApplied') ->willReturnMap([ ['patch1', true], ['patch2', false], ['patch3', false], + ['patch4', true], + ['patch4-v2', true], + ['patch5', true], + ['patch5-v2', false], ]); /** @var InputInterface|MockObject $inputMock */ @@ -118,10 +127,10 @@ public function testShowStatus() $this->aggregator->expects($this->once()) ->method('aggregate') - ->willReturn([$patch1, $patch2, $patch3]); + ->willReturn([$patch1, $patch2, $patch3, $patch4, $patch5]); // Show warning message about patch deprecation - $outputMock->expects($this->exactly(2)) + $outputMock->expects($this->exactly(3)) ->method('writeln') ->withConsecutive( [$this->anything()], @@ -131,7 +140,7 @@ public function testShowStatus() // Show patches in the table $this->renderer->expects($this->once()) ->method('printTable') - ->withConsecutive([$outputMock, [$patch1, $patch2]]); + ->withConsecutive([$outputMock, [$patch1, $patch2, $patch5]]); $this->manager->run($inputMock, $outputMock); } @@ -141,14 +150,18 @@ public function testShowStatus() * * @param string $id * @param bool $isDeprecated - * + * @param string $replacedWith * @return AggregatedPatchInterface|MockObject */ - private function createPatch(string $id, bool $isDeprecated) + private function createPatch(string $id, bool $isDeprecated, string $replacedWith = '') { - $patch = $this->getMockForAbstractClass(AggregatedPatchInterface::class); + $patch = $this->createMock(AggregatedPatch::class); $patch->method('getId')->willReturn($id); $patch->method('isDeprecated')->willReturn($isDeprecated); + $patch->method('getReplacedWith')->willReturn($replacedWith); + + // To make mock object unique for assertions and array operations. + $patch->id = microtime(); return $patch; } From 30594d739cd98f0ad440dc497329f5a0c29988ec Mon Sep 17 00:00:00 2001 From: Oleksandr <79673036+o-dubovyk@users.noreply.github.com> Date: Wed, 14 Apr 2021 11:59:46 -0500 Subject: [PATCH 084/157] MCLOUD-7742: Updated patch to fix the layered navigation issue (#24) --- ...e_with_enabled_shared_catalog__2.3.5.patch | 90 +++++++++++++++++-- 1 file changed, 81 insertions(+), 9 deletions(-) diff --git a/patches/MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch b/patches/MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch index 584d1a40..7b55923f 100644 --- a/patches/MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch +++ b/patches/MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch @@ -1,7 +1,64 @@ diff -Nuar a/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregation/Builder/DataProvider.php b/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregation/Builder/DataProvider.php +index 86f4fd48e9..a6061fca6f 100644 --- a/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregation/Builder/DataProvider.php +++ b/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregation/Builder/DataProvider.php -@@ -103,18 +103,16 @@ public function __construct( +@@ -7,6 +7,8 @@ declare(strict_types=1); + + namespace Magento\SharedCatalog\Model\SearchAdapter\Aggregation\Builder; + ++use Magento\Catalog\Model\Layer; ++use Magento\Catalog\Model\Layer\Resolver; + use Magento\Catalog\Model\Product; + use Magento\Catalog\Model\ResourceModel\Product as ProductResource; + use Magento\CatalogInventory\Model\Configuration; +@@ -14,6 +16,7 @@ use Magento\CatalogInventory\Model\Stock; + use Magento\Customer\Model\Session; + use Magento\Eav\Model\Config as EavConfig; + use Magento\Framework\App\Config\ScopeConfigInterface; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\App\ResourceConnection; + use Magento\Framework\App\ScopeResolverInterface; + use Magento\Framework\DB\Adapter\AdapterInterface; +@@ -70,6 +73,11 @@ class DataProvider + */ + private $productItem; + ++ /** ++ * @var Layer ++ */ ++ private $searchLayer; ++ + /** + * @param ResourceConnection $resource + * @param Session $session +@@ -78,6 +86,7 @@ class DataProvider + * @param EavConfig $eavConfig + * @param ProductResource $product + * @param ProductItemResource $productItem ++ * @param Resolver|null $layerResolver + */ + public function __construct( + ResourceConnection $resource, +@@ -86,7 +95,8 @@ class DataProvider + ScopeConfigInterface $scopeConfig, + EavConfig $eavConfig, + ProductResource $product, +- ProductItemResource $productItem ++ ProductItemResource $productItem, ++ ?Resolver $layerResolver = null + ) { + $this->resource = $resource; + $this->connection = $resource->getConnection(); +@@ -96,6 +106,8 @@ class DataProvider + $this->eavConfig = $eavConfig; + $this->product = $product; + $this->productItem = $productItem; ++ $layerResolver = $layerResolver ?? ObjectManager::getInstance()->get(Resolver::class); ++ $this->searchLayer = $layerResolver->get(); + } + + /** +@@ -103,18 +115,16 @@ class DataProvider * * @param BucketInterface $bucket * @param array $dimensions @@ -20,9 +77,9 @@ diff -Nuar a/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregatio - $select = $this->getSelect($bucket, $dimensions, $documentIds); + $select = $this->getSelect($bucket, $dimensions); $query = $this->connection->query($select); - + while ($row = $query->fetch()) { -@@ -134,11 +132,10 @@ public function getAggregation( +@@ -134,15 +144,15 @@ class DataProvider * * @param BucketInterface $bucket * @param array $dimensions @@ -35,24 +92,39 @@ diff -Nuar a/vendor/magento/module-shared-catalog/Model/SearchAdapter/Aggregatio { $attribute = $this->eavConfig->getAttribute(Product::ENTITY, $bucket->getField()); $currentScope = $this->scopeResolver->getScope($dimensions['scope']->getValue()); -@@ -161,7 +158,6 @@ private function getSelect(BucketInterface $bucket, array $dimensions, array $do + $customerGroupId = $this->session->getCustomerGroupId(); ++ $categoryId = $this->searchLayer->getCurrentCategory()->getId(); + + $eavIndexTable = $this->resource->getTableName( + 'catalog_product_index_eav' . ($attribute->getBackendType() === 'decimal' ? '_decimal' : '') +@@ -160,11 +170,14 @@ class DataProvider + ['shared_catalog_item' => $this->productItem->getMainTable()], 'source_entity.sku = shared_catalog_item.sku', [] ++ )->joinInner( ++ ['catalog_category_product' => $this->resource->getTableName('catalog_category_product')], ++ 'eav.entity_id = catalog_category_product.product_id', ++ [] ) - ->where('eav.entity_id IN (?)', $documentIds) ->where('eav.attribute_id = ?', $attribute->getId()) ->where('eav.store_id = ? ', $currentScope->getId()) - ->where('source_entity.type_id <> ?', 'configurable') -diff -Nuar a/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php b/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php +- ->where('source_entity.type_id <> ?', 'configurable') ++ ->where('catalog_category_product.category_id = ?', $categoryId) + ->where('shared_catalog_item.customer_group_id = ?', $customerGroupId); + + $this->addStockFilterToSelect($subSelect); +diff --git a/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php b/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php +index fdc811dbeb..28b1cd4ee4 100644 --- a/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php +++ b/vendor/magento/module-shared-catalog/Plugin/Elasticsearch/SearchAdapter/Aggregation/Builder/UpdateTermBucketBuilderPlugin.php -@@ -65,8 +65,7 @@ public function afterBuild( +@@ -65,8 +65,7 @@ class UpdateTermBucketBuilderPlugin $shouldAggregate = ($bucket->getField() !== 'category_ids'); - + if ($sharedCatalogIsEnabled && $shouldAggregate) { - $documentIds = array_column($queryResult['hits']['hits'], '_id'); - $values = $this->dataProvider->getAggregation($bucket, $dimensions, $documentIds); + $values = $this->dataProvider->getAggregation($bucket, $dimensions); } - + return $values; From f1355ef6a7d69aba962a99ae35d3767357ac642a Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Wed, 21 Apr 2021 09:15:17 -0500 Subject: [PATCH 085/157] MC-41912: 'magento-patches apply' command doesn't allow to apply some patches multiple times (#25) --- src/Command/Apply.php | 2 ++ src/Command/Ece/Apply.php | 1 + src/Command/Ece/Revert.php | 1 + .../Action/ProcessDeprecatedAction.php | 32 ++++++++++------- src/Command/Process/ApplyLocal.php | 2 +- src/Command/Revert.php | 1 + src/Patch/Conflict/Processor.php | 2 +- .../Action/ProcessDeprecatedActionTest.php | 34 +++++++++++++++++++ .../Unit/Patch/Conflict/ProcessorTest.php | 2 +- 9 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/Command/Apply.php b/src/Command/Apply.php index 612f90a2..47b1692e 100644 --- a/src/Command/Apply.php +++ b/src/Command/Apply.php @@ -88,7 +88,9 @@ public function execute(InputInterface $input, OutputInterface $output) try { $this->applyOptional->run($input, $output); } catch (RuntimeException $e) { + $output->writeln($this->magentoVersion->get()); $output->writeln('<error>' . $e->getMessage() . '</error>'); + $this->logger->info($this->magentoVersion->get()); $this->logger->error($e->getMessage()); diff --git a/src/Command/Ece/Apply.php b/src/Command/Ece/Apply.php index c19f5da0..7a1b513e 100644 --- a/src/Command/Ece/Apply.php +++ b/src/Command/Ece/Apply.php @@ -98,6 +98,7 @@ public function execute(InputInterface $input, OutputInterface $output) $this->applyOptional->run($input, $output); $this->applyLocal->run($input, $output); } catch (RuntimeException $e) { + $output->writeln($this->magentoVersion->get()); $output->writeln('<error>' . $e->getMessage() . '</error>'); $this->logger->info($this->magentoVersion->get()); $this->logger->error($e->getMessage()); diff --git a/src/Command/Ece/Revert.php b/src/Command/Ece/Revert.php index a06e3914..82446359 100644 --- a/src/Command/Ece/Revert.php +++ b/src/Command/Ece/Revert.php @@ -78,6 +78,7 @@ public function execute(InputInterface $input, OutputInterface $output) try { $this->revert->run($input, $output); } catch (RuntimeException $e) { + $output->writeln($this->magentoVersion->get()); $output->writeln('<error>' . $e->getMessage() . '</error>'); $this->logger->info($this->magentoVersion->get()); $this->logger->error($e->getMessage()); diff --git a/src/Command/Process/Action/ProcessDeprecatedAction.php b/src/Command/Process/Action/ProcessDeprecatedAction.php index 9341bda6..b04ab3d3 100644 --- a/src/Command/Process/Action/ProcessDeprecatedAction.php +++ b/src/Command/Process/Action/ProcessDeprecatedAction.php @@ -9,7 +9,6 @@ use Magento\CloudPatches\App\RuntimeException; use Magento\CloudPatches\Command\Process\Renderer; -use Magento\CloudPatches\Patch\Data\AggregatedPatch; use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; use Magento\CloudPatches\Patch\Pool\OptionalPool; use Magento\CloudPatches\Patch\Aggregator; @@ -93,6 +92,13 @@ public function execute(InputInterface $input, OutputInterface $output, array $p throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } + $patches = array_filter( + $patches, + function ($patch) { + return !$this->statusPool->isApplied($patch->getId()); + } + ); + foreach ($patches as $patch) { $this->processDeprecation($patch, $output, $input); $this->processReplacement($patch, $output, $input); @@ -144,21 +150,23 @@ function ($patchId) { } ); - if ($requireRevertAndReplace) { - $ids = implode(' ', $requireRevertAndReplace); - $warning = sprintf('%s should be reverted and replaced with %s', $ids, $patch->getId()); - $output->writeln('<info>' . $warning . '</info>'); - $this->logger->warning($warning); + if (empty($requireRevertAndReplace)) { + return; + } - $question = 'Do you want to proceed with reverting?'; - if (!$this->renderer->printQuestion($input, $output, $question)) { - $errorMessage = sprintf('%s can\'t be applied without reverting %s', $patch->getId(), $ids); + $ids = implode(' ', $requireRevertAndReplace); + $warning = sprintf('%s should be reverted and replaced with %s', $ids, $patch->getId()); + $output->writeln('<info>' . $warning . '</info>'); + $this->logger->warning($warning); - throw new RuntimeException($errorMessage); - } + $question = 'Do you want to proceed with reverting?'; + if (!$this->renderer->printQuestion($input, $output, $question)) { + $errorMessage = sprintf('%s can\'t be applied without reverting %s', $patch->getId(), $ids); - $this->revert->execute($input, $output, $requireRevertAndReplace); + throw new RuntimeException($errorMessage); } + + $this->revert->execute($input, $output, $requireRevertAndReplace); } /** diff --git a/src/Command/Process/ApplyLocal.php b/src/Command/Process/ApplyLocal.php index 6316fab1..654f03cb 100644 --- a/src/Command/Process/ApplyLocal.php +++ b/src/Command/Process/ApplyLocal.php @@ -89,7 +89,7 @@ public function run(InputInterface $input, OutputInterface $output) $this->printInfo($output, $message); array_push($appliedPatches, $patch); } catch (ApplierException $exception) { - $this->printError($output, 'Error: patch can\'t be applied'); + $this->printError($output, 'Error: patch ' . $patch->getPath() . ' can\'t be applied'); $messages = $this->rollbackProcessor->process($appliedPatches); $output->writeln($messages); $errorMessage = sprintf( diff --git a/src/Command/Revert.php b/src/Command/Revert.php index 7a29a3da..d0bc7e40 100644 --- a/src/Command/Revert.php +++ b/src/Command/Revert.php @@ -101,6 +101,7 @@ public function execute(InputInterface $input, OutputInterface $output) try { $this->revert->run($input, $output); } catch (RuntimeException $e) { + $output->writeln($this->magentoVersion->get()); $output->writeln('<error>' . $e->getMessage() . '</error>'); $this->logger->info($this->magentoVersion->get()); $this->logger->error($e->getMessage()); diff --git a/src/Patch/Conflict/Processor.php b/src/Patch/Conflict/Processor.php index 68b99de6..0346c159 100644 --- a/src/Patch/Conflict/Processor.php +++ b/src/Patch/Conflict/Processor.php @@ -73,7 +73,7 @@ public function process( array $appliedPatches, string $exceptionMessage ) { - $errorMessage = 'Error: patch can\'t be applied'; + $errorMessage = 'Error: patch ' . $patch->getId() . ' can\'t be applied'; $this->logger->error($errorMessage); $output->writeln('<error>' . $errorMessage . '</error>'); diff --git a/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php b/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php index 68cef97e..50dd537c 100644 --- a/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php @@ -208,6 +208,40 @@ public function testProcessReplacementSuccessful() $this->action->execute($inputMock, $outputMock, $patchFilter); } + /** + * Tests successful skipping of replacement check when patch is already applied. + */ + public function testSkippingReplacementProcessForAppliedPatch() + { + $patch1 = $this->createPatch('MC-11111', false); + $this->statusPool->method('isApplied') + ->willReturnMap([ + ['MC-11111', true] + ]); + $patchFilter = [$patch1->getId()]; + + /** @var InputInterface|MockObject $inputMock */ + $inputMock = $this->getMockForAbstractClass(InputInterface::class); + /** @var OutputInterface|MockObject $outputMock */ + $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + + $patchMock = $this->getMockForAbstractClass(PatchInterface::class); + + $this->optionalPool->expects($this->once()) + ->method('getList') + ->withConsecutive([$patchFilter]) + ->willReturn([$patchMock]); + + $this->aggregator->expects($this->once()) + ->method('aggregate') + ->willReturn([$patch1]); + + $this->renderer->expects($this->never()) + ->method('printQuestion'); + + $this->action->execute($inputMock, $outputMock, $patchFilter); + } + /** * Tests a case when user rejected to revert deprecated patches before applying a new one. */ diff --git a/src/Test/Unit/Patch/Conflict/ProcessorTest.php b/src/Test/Unit/Patch/Conflict/ProcessorTest.php index 8f09459e..c37631f4 100644 --- a/src/Test/Unit/Patch/Conflict/ProcessorTest.php +++ b/src/Test/Unit/Patch/Conflict/ProcessorTest.php @@ -92,7 +92,7 @@ public function testProcess() $outputMock->expects($this->exactly(2)) ->method('writeln') ->withConsecutive( - [$this->stringContains('Error: patch can\'t be applied')], + [$this->stringContains('Error: patch ' . $failedPatch->getId() . ' can\'t be applied')], [$rollbackMessages] ); From c32358a4e170fbc1a2cf3371de38c3ca5a74e745 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Tue, 4 May 2021 11:33:14 -0500 Subject: [PATCH 086/157] MC-42131: MCP can't be installed with Magento 2.3.7 due to the composer dependencies conflict (#26) --- composer.json | 4 ++-- src/Test/Functional/Acceptance/Acceptance73Cest.php | 3 +++ src/Test/Functional/Acceptance/AcceptanceCest.php | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index ffc78b48..ba9b846a 100644 --- a/composer.json +++ b/composer.json @@ -2,13 +2,13 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.9", + "version": "1.0.10", "license": "OSL-3.0", "require": { "php": "^7.0", "ext-json": "*", "composer/composer": "@stable", - "composer/semver": "^1.5", + "composer/semver": "^1.5||^2.0||^3.0", "symfony/config": "^3.3||^4.4||^5.1", "symfony/console": "^2.6||^4.0||^5.1", "symfony/dependency-injection": "^3.3||^4.3||^5.1", diff --git a/src/Test/Functional/Acceptance/Acceptance73Cest.php b/src/Test/Functional/Acceptance/Acceptance73Cest.php index a6e872b4..0985aabc 100644 --- a/src/Test/Functional/Acceptance/Acceptance73Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance73Cest.php @@ -24,7 +24,10 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.3.4', 'magentoVersion' => '2.3.4-p2'], ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5'], ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5-p1'], + ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5-p2'], ['templateVersion' => '2.3.6', 'magentoVersion' => '2.3.6'], + ['templateVersion' => '2.3.6', 'magentoVersion' => '2.3.6-p1'], + ['templateVersion' => '2.3.6', 'magentoVersion' => '2.3.7'], ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], ]; } diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index c532f87a..9b44cf72 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -51,7 +51,10 @@ protected function patchesDataProvider(): array return [ ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0-p1'], - ['templateVersion' => '2.4.1'], + ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1'], + ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1-p1'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p1'], ['templateVersion' => 'master'], ]; } From 82f12cd1a08c838d9b40b04550a3055609b1b87f Mon Sep 17 00:00:00 2001 From: Dmytro Yushkin <dyushkin@adobe.com> Date: Thu, 24 Jun 2021 10:48:58 -0500 Subject: [PATCH 087/157] MCLOUD-7827: Error on Search page after switching Store View (#31) --- patches.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches.json b/patches.json index a9f3987d..6dd47330 100644 --- a/patches.json +++ b/patches.json @@ -235,7 +235,7 @@ }, "Pagination Not working - product_list_limit=all": { ">=2.3.2 <2.3.4": "MCLOUD-5684__pagination_not_working_product_list_limit_all__2.3.2.patch", - ">=2.3.4 <2.4.0": "MCLOUD-5684__pagination_not_working_product_list_limit_all__2.3.4.patch" + ">=2.3.4 <2.3.6": "MCLOUD-5684__pagination_not_working_product_list_limit_all__2.3.4.patch" }, "Sitemap Generation Warnings": { ">=2.3.0 <2.3.2": "MCLOUD-3025__sitemap_generation_warnings__2.3.0.patch", From e334c98605ae37b5d349305121069e433a70f0dd Mon Sep 17 00:00:00 2001 From: Stanislav Idolov <sidolov@adobe.com> Date: Mon, 19 Jul 2021 11:40:04 -0500 Subject: [PATCH 088/157] Community patches & Category support (#30) --- config/patch-categories.json | 68 +++++ config/services.xml | 24 ++ .../Process/Action/ApplyOptionalAction.php | 4 +- .../Process/Action/ReviewAppliedAction.php | 3 +- src/Command/Process/Renderer.php | 18 +- src/Command/Process/ShowStatus.php | 193 +++++++++++- src/Composer/QualityPackage.php | 44 ++- src/Console/QuestionFactory.php | 29 ++ src/Filesystem/FileList.php | 8 + src/Filesystem/JsonConfigReader.php | 50 ++++ src/Patch/AggregatedPatchFactory.php | 32 +- .../GetLocalPatchCategories.php | 46 +++ .../GetSupportPatchCategories.php | 49 +++ src/Patch/Collector/CloudCollector.php | 5 +- src/Patch/Collector/CommunityCollector.php | 16 + .../Collector/GetCommunityPatchesConfig.php | 60 ++++ .../Collector/GetPatchesConfigInterface.php | 20 ++ .../Collector/GetSupportPatchesConfig.php | 60 ++++ src/Patch/Collector/LocalCollector.php | 4 + src/Patch/Collector/QualityCollector.php | 279 ------------------ src/Patch/Collector/SupportCollector.php | 184 ++++++++++++ src/Patch/Collector/ValidatePatchesConfig.php | 104 +++++++ src/Patch/CollectorInterface.php | 25 ++ src/Patch/Data/AggregatedPatch.php | 33 +++ src/Patch/Data/AggregatedPatchInterface.php | 14 + src/Patch/Data/Patch.php | 32 ++ src/Patch/Data/PatchInterface.php | 14 + src/Patch/GetCategoriesList.php | 40 +++ src/Patch/GetCategoriesListInterface.php | 19 ++ src/Patch/PatchBuilder.php | 36 +++ src/Patch/Pool/OptionalPool.php | 55 ++-- src/Patch/Pool/RequiredPool.php | 16 +- src/Patch/SourceProvider.php | 66 ++--- .../Action/ApplyOptionalActionTest.php | 3 +- .../Action/ReviewAppliedActionTest.php | 4 +- .../Unit/Command/Process/ShowStatusTest.php | 19 +- .../Unit/Patch/AggregatedPatchFactoryTest.php | 16 +- .../Fixture/quality_config_invalid.php | 12 +- .../Fixture/quality_config_valid.php | 29 +- .../Patch/Collector/QualityCollectorTest.php | 61 ++-- src/Test/Unit/Patch/Pool/OptionalPoolTest.php | 26 +- src/Test/Unit/Patch/Pool/RequiredPoolTest.php | 2 +- src/Test/Unit/Patch/SourceProviderTest.php | 76 ++--- 43 files changed, 1422 insertions(+), 476 deletions(-) create mode 100644 config/patch-categories.json create mode 100644 src/Console/QuestionFactory.php create mode 100644 src/Filesystem/JsonConfigReader.php create mode 100644 src/Patch/CategoriesList/GetLocalPatchCategories.php create mode 100644 src/Patch/CategoriesList/GetSupportPatchCategories.php create mode 100644 src/Patch/Collector/CommunityCollector.php create mode 100644 src/Patch/Collector/GetCommunityPatchesConfig.php create mode 100644 src/Patch/Collector/GetPatchesConfigInterface.php create mode 100644 src/Patch/Collector/GetSupportPatchesConfig.php delete mode 100644 src/Patch/Collector/QualityCollector.php create mode 100644 src/Patch/Collector/SupportCollector.php create mode 100644 src/Patch/Collector/ValidatePatchesConfig.php create mode 100644 src/Patch/CollectorInterface.php create mode 100644 src/Patch/GetCategoriesList.php create mode 100644 src/Patch/GetCategoriesListInterface.php diff --git a/config/patch-categories.json b/config/patch-categories.json new file mode 100644 index 00000000..901dba83 --- /dev/null +++ b/config/patch-categories.json @@ -0,0 +1,68 @@ +[ + { + "name": "Performance" + }, + { + "name": "Web API" + }, + { + "name": "GraphQL" + }, + { + "name": "B2B" + }, + { + "name": "Content" + }, + { + "name": "Fastly/Varnish" + }, + { + "name": "Redis/Memcache" + }, + { + "name": "Cron" + }, + { + "name": "Inventory" + }, + { + "name": "Elasticsearch" + }, + { + "name": "Import/Export" + }, + { + "name": "Catalog/Product" + }, + { + "name": "Catalog Search" + }, + { + "name": "Payments" + }, + { + "name": "Reports" + }, + { + "name": "Customer" + }, + { + "name": "Shipping" + }, + { + "name": "Order" + }, + { + "name": "Shopping Cart" + }, + { + "name": "Emails" + }, + { + "name": "Admin" + }, + { + "name": "Other" + } +] diff --git a/config/services.xml b/config/services.xml index 6c668fb3..94c04a57 100644 --- a/config/services.xml +++ b/config/services.xml @@ -87,5 +87,29 @@ <argument type="service" id="Magento\CloudPatches\Shell\Command\PatchDriver"/> </argument> </service> + <service id="Magento\CloudPatches\Patch\Collector\SupportCollector"> + <argument key="$getPatchesConfig" type="service" id="Magento\CloudPatches\Patch\Collector\GetSupportPatchesConfig"/> + </service> + <service id="Magento\CloudPatches\Patch\Collector\CommunityCollector"> + <argument key="$getPatchesConfig" type="service" id="Magento\CloudPatches\Patch\Collector\GetCommunityPatchesConfig"/> + </service> + <service id="Magento\CloudPatches\Patch\GetCategoriesList"> + <argument key="$providers" type="collection"> + <argument type="service" id="Magento\CloudPatches\Patch\CategoriesList\GetLocalPatchCategories"/> + <argument type="service" id="Magento\CloudPatches\Patch\CategoriesList\GetSupportPatchCategories"/> + </argument> + </service> + <service id="Magento\CloudPatches\Patch\Pool\OptionalPool"> + <argument key="$collectors" type="collection"> + <argument type="service" id="Magento\CloudPatches\Patch\Collector\CloudCollector"/> + <argument type="service" id="Magento\CloudPatches\Patch\Collector\SupportCollector"/> + <argument type="service" id="Magento\CloudPatches\Patch\Collector\CommunityCollector"/> + </argument> + </service> + <service id="Magento\CloudPatches\Patch\Pool\RequiredPool"> + <argument key="$collectors" type="collection"> + <argument type="service" id="Magento\CloudPatches\Patch\Collector\CloudCollector"/> + </argument> + </service> </services> </container> diff --git a/src/Command/Process/Action/ApplyOptionalAction.php b/src/Command/Process/Action/ApplyOptionalAction.php index 461d6c32..7e6a3e69 100644 --- a/src/Command/Process/Action/ApplyOptionalAction.php +++ b/src/Command/Process/Action/ApplyOptionalAction.php @@ -9,6 +9,7 @@ use Magento\CloudPatches\App\RuntimeException; use Magento\CloudPatches\Command\Process\Renderer; +use Magento\CloudPatches\Patch\Collector\SupportCollector; use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor; use Magento\CloudPatches\Patch\Pool\OptionalPool; use Magento\CloudPatches\Patch\Applier; @@ -24,6 +25,7 @@ * Applies optional patches. * * Patches are applying from top to bottom of config list. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ApplyOptionalAction implements ActionInterface { @@ -139,7 +141,7 @@ private function getPatchList(array $patchFilter): array { if (empty($patchFilter)) { return array_filter( - $this->optionalPool->getOptionalAll(), + $this->optionalPool->getOptionalListByOrigin([SupportCollector::ORIGIN]), function ($patch) { return !$patch->isDeprecated(); } diff --git a/src/Command/Process/Action/ReviewAppliedAction.php b/src/Command/Process/Action/ReviewAppliedAction.php index a4ec579b..19769346 100644 --- a/src/Command/Process/Action/ReviewAppliedAction.php +++ b/src/Command/Process/Action/ReviewAppliedAction.php @@ -7,6 +7,7 @@ namespace Magento\CloudPatches\Command\Process\Action; +use Magento\CloudPatches\Patch\Collector\SupportCollector; use Magento\CloudPatches\Patch\Pool\OptionalPool; use Magento\CloudPatches\Patch\Status\StatusPool; use Psr\Log\LoggerInterface; @@ -59,7 +60,7 @@ public function __construct( public function execute(InputInterface $input, OutputInterface $output, array $patchFilter) { $appliedOptionalPatches = array_filter( - $this->optionalPool->getOptionalAll(), + $this->optionalPool->getOptionalListByOrigin([SupportCollector::ORIGIN]), function ($patch) { return $this->statusPool->isApplied($patch->getId()); } diff --git a/src/Command/Process/Renderer.php b/src/Command/Process/Renderer.php index 8c39bc9d..84627db0 100644 --- a/src/Command/Process/Renderer.php +++ b/src/Command/Process/Renderer.php @@ -9,6 +9,7 @@ use Magento\CloudPatches\Console\ConfirmationQuestionFactory; use Magento\CloudPatches\Console\TableFactory; +use Magento\CloudPatches\Patch\Collector\CommunityCollector; use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Patch\Status\StatusPool; @@ -26,6 +27,10 @@ class Renderer const TITLE = 'Title'; + const CATEGORY = 'Category'; + + const ORIGIN = 'Origin'; + const TYPE = 'Type'; const STATUS = 'Status'; @@ -80,7 +85,7 @@ public function __construct( public function printTable(OutputInterface $output, array $patchList) { $table = $this->tableFactory->create($output); - $table->setHeaders([self::ID, self::TITLE, self::TYPE, self::STATUS, self::DETAILS]); + $table->setHeaders([self::ID, self::TITLE, self::CATEGORY, self::ORIGIN, self::STATUS, self::DETAILS]); $table->setStyle('box-double'); $rows = []; @@ -89,6 +94,9 @@ public function printTable(OutputInterface $output, array $patchList) } usort($rows, function ($a, $b) { + if ($a[self::STATUS] === $b[self::STATUS]) { + return strcmp($a[self::ORIGIN], $b[self::ORIGIN]); + } return strcmp($a[self::STATUS], $b[self::STATUS]); }); @@ -165,7 +173,10 @@ public function printQuestion(InputInterface $input, OutputInterface $output, st */ private function createRow(AggregatedPatchInterface $patch): array { - $details = ''; + $details = 'Patch type: ' + . ($patch->isDeprecated() ? '<error>DEPRECATED</error>' : $patch->getType()) + . PHP_EOL; + if ($patch->getReplacedWith()) { $details .= '<info>Recommended replacement: ' . $patch->getReplacedWith() . '</info>' . PHP_EOL; } @@ -191,7 +202,8 @@ function ($item) { return [ self::ID => '<comment>' . $id . '</comment>', self::TITLE => $title, - self::TYPE => $patch->isDeprecated() ? '<error>DEPRECATED</error>' : $patch->getType(), + self::CATEGORY => implode(PHP_EOL, $patch->getCategories()), + self::ORIGIN => $patch->getOrigin(), self::STATUS => $this->statusPool->get($patch->getId()), self::DETAILS => $details ]; diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 7cb26cd9..5d2d040a 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -8,11 +8,14 @@ namespace Magento\CloudPatches\Command\Process; use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; +use Magento\CloudPatches\Console\QuestionFactory; +use Magento\CloudPatches\Patch\Data\AggregatedPatch; use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; use Magento\CloudPatches\Patch\Pool\LocalPool; use Magento\CloudPatches\Patch\Pool\OptionalPool; use Magento\CloudPatches\Patch\Aggregator; use Magento\CloudPatches\Patch\Status\StatusPool; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -21,6 +24,10 @@ */ class ShowStatus implements ProcessInterface { + const INTERACTIVE_FILTER_THRESHOLD = 50; + + const FILTER_OPTION_ALL = 'All'; + /** * @var Aggregator */ @@ -51,6 +58,16 @@ class ShowStatus implements ProcessInterface */ private $renderer; + /** + * @var QuestionHelper + */ + private $questionHelper; + + /** + * @var QuestionFactory + */ + private $questionFactory; + /** * @param Aggregator $aggregator * @param OptionalPool $optionalPool @@ -58,6 +75,8 @@ class ShowStatus implements ProcessInterface * @param StatusPool $statusPool * @param ReviewAppliedAction $reviewAppliedAction * @param Renderer $renderer + * @param QuestionHelper $questionHelper + * @param QuestionFactory $questionFactory */ public function __construct( Aggregator $aggregator, @@ -65,7 +84,9 @@ public function __construct( LocalPool $localPool, StatusPool $statusPool, ReviewAppliedAction $reviewAppliedAction, - Renderer $renderer + Renderer $renderer, + QuestionHelper $questionHelper, + QuestionFactory $questionFactory ) { $this->aggregator = $aggregator; $this->optionalPool = $optionalPool; @@ -73,6 +94,8 @@ public function __construct( $this->statusPool = $statusPool; $this->reviewAppliedAction = $reviewAppliedAction; $this->renderer = $renderer; + $this->questionHelper = $questionHelper; + $this->questionFactory = $questionFactory; } /** @@ -92,14 +115,147 @@ public function run(InputInterface $input, OutputInterface $output) $this->printDeprecatedWarning($output, $patch); } } + $patches = $this->filterNotVisiblePatches($patches); + + if (count($patches) > self::INTERACTIVE_FILTER_THRESHOLD) { + $this->printPatchProviders($output, $patches); + $patches = $this->filterByPatchProvider($input, $output, $patches); + $this->printCategoriesInfo($output, $patches); + $patches = $this->filterByPatchCategory($input, $output, $patches); + } - $patches = array_filter( + $this->renderer->printTable($output, array_values($patches)); + } + + /** + * @param array $patches + * @return array + */ + private function filterNotVisiblePatches(array $patches): array + { + return array_filter( $patches, function ($patch) { return !$patch->isDeprecated() || $this->isPatchVisible($patch); } ); - $this->renderer->printTable($output, array_values($patches)); + } + + /** + * @param OutputInterface $output + * @param array $patches + */ + private function printPatchProviders(OutputInterface $output, array $patches) + { + $patchProviders = [self::FILTER_OPTION_ALL=> count($patches)]; + /** @var AggregatedPatch $patch */ + foreach ($patches as $patch) { + if (!isset($patchProviders[$patch->getOrigin()])) { + $patchProviders[$patch->getOrigin()] = 0; + } + $patchProviders[$patch->getOrigin()]++; + } + + $providersInfo = PHP_EOL . '<info>Patch providers:</info>' . PHP_EOL; + $i = 1; + foreach ($patchProviders as $type => $count) { + $providersInfo .= sprintf('<info>%d) %s (%s)</info>', $i, $type, $count) . PHP_EOL; + $i++; + } + + $output->writeln($providersInfo); + } + + /** + * @param array $patches + * @return array + */ + private function getPatchProviders(array $patches): array + { + $patchTypes = [self::FILTER_OPTION_ALL]; + /** @var AggregatedPatch $patch */ + foreach ($patches as $patch) { + if (!in_array($patch->getOrigin(), $patchTypes)) { + $patchTypes[] = $patch->getOrigin(); + } + } + return $patchTypes; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param array $patches + * @return array + */ + private function filterByPatchProvider(InputInterface $input, OutputInterface $output, array $patches): array + { + $typeQuestion = $this->questionFactory->create('Please, select patch provider: ', self::FILTER_OPTION_ALL); + $selectedType = $this->questionHelper->ask($input, $output, $typeQuestion); + + if (is_numeric($selectedType)) { + $patchTypes = $this->getPatchProviders($patches); + $selectedType = $patchTypes[(int)$selectedType - 1] ?? self::FILTER_OPTION_ALL; + } + + $output->writeln('<info>Selected patch provider: ' . $selectedType . '</info>' . PHP_EOL); + return $selectedType === self::FILTER_OPTION_ALL + ? $patches + : array_filter( + $patches, + function ($patch) use ($selectedType) { + return strtolower($patch->getOrigin()) === strtolower($selectedType); + } + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param array $patches + * @return array + */ + private function filterByPatchCategory(InputInterface $input, OutputInterface $output, array $patches): array + { + $categoryQuestion = $this->questionFactory->create('Please, select patch category: ', self::FILTER_OPTION_ALL); + $selectedCategory = $this->questionHelper->ask($input, $output, $categoryQuestion); + + if (is_numeric($selectedCategory)) { + $allPatchCategories = $this->getPatchCategories($patches); + $selectedCategory = $allPatchCategories[(int)$selectedCategory - 1] ?? $allPatchCategories[0]; + } + + $output->writeln('<info>Selected patch category: ' . $selectedCategory . '</info>' . PHP_EOL); + return $selectedCategory === self::FILTER_OPTION_ALL + ? $patches + : array_filter( + $patches, + function ($patch) use ($selectedCategory) { + $patchCategories = $patch->getCategories(); + $patchCategories = array_map('strtolower', $patchCategories); + $selectedCategory = strtolower($selectedCategory); + return in_array($selectedCategory, $patchCategories); + } + ); + } + + /** + * @param array $patches + * @return string[] + */ + private function getPatchCategories(array $patches): array + { + $categories = [self::FILTER_OPTION_ALL]; + + /** @var AggregatedPatch $patch */ + foreach ($patches as $patch) { + foreach ($patch->getCategories() as $patchCategory) { + if (!in_array($patchCategory, $categories)) { + $categories[] = $patchCategory; + } + } + } + return $categories; } /** @@ -121,6 +277,37 @@ private function printDetailsInfo(OutputInterface $output) ); } + /** + * Prints patches category information + * + * @param OutputInterface $output + * @param array $patches + * @return void + */ + private function printCategoriesInfo(OutputInterface $output, array $patches) + { + $categories = [self::FILTER_OPTION_ALL => count($patches)]; + + /** @var AggregatedPatch $patch */ + foreach ($patches as $patch) { + foreach ($patch->getCategories() as $patchCategory) { + if (!isset($categories[$patchCategory])) { + $categories[$patchCategory] = 0; + } + $categories[$patchCategory]++; + } + } + + $categoriesInfo = PHP_EOL . '<info>Patch categories:</info>' . PHP_EOL; + $i = 1; + foreach ($categories as $category => $count) { + $categoriesInfo .= sprintf('<info>%d) %s (%s)</info>', $i, $category, $count) . PHP_EOL; + $i++; + } + + $output->writeln($categoriesInfo); + } + /** * Prints warning message about applied deprecated patch. * diff --git a/src/Composer/QualityPackage.php b/src/Composer/QualityPackage.php index 9cc28e93..32c7c5ea 100644 --- a/src/Composer/QualityPackage.php +++ b/src/Composer/QualityPackage.php @@ -20,7 +20,17 @@ class QualityPackage /** * @var string|null */ - private $patchesConfig; + private $supportPatchesConfig; + + /** + * @var string|null + */ + private $communityPatchesConfig; + + /** + * @var string|null + */ + private $categoriesConfig; /** * QualityPackage constructor @@ -30,7 +40,9 @@ public function __construct() if (class_exists(\Magento\QualityPatches\Info::class)) { $info = new \Magento\QualityPatches\Info(); $this->patchesDirectory = $info->getPatchesDirectory(); - $this->patchesConfig = $info->getPatchesConfig(); + $this->supportPatchesConfig = $info->getSupportPatchesConfig(); + $this->communityPatchesConfig = $info->getCommunityPatchesConfig(); + $this->categoriesConfig = $info->getCategoriesConfig(); } } @@ -39,18 +51,38 @@ public function __construct() * * @return string|null */ - public function getPatchesDirectory() + public function getPatchesDirectoryPath() { return $this->patchesDirectory; } /** - * Returns path to patches configuration file. + * Returns path to support patches configuration file. + * + * @return string|null + */ + public function getSupportPatchesConfigPath() + { + return $this->supportPatchesConfig; + } + + /** + * Returns path to community patches configuration file. + * + * @return string|null + */ + public function getCommunityPatchesConfigPath() + { + return $this->communityPatchesConfig; + } + + /** + * Returns path to the categories configuration file. * * @return string|null */ - public function getPatchesConfig() + public function getCategoriesConfigPath() { - return $this->patchesConfig; + return $this->categoriesConfig; } } diff --git a/src/Console/QuestionFactory.php b/src/Console/QuestionFactory.php new file mode 100644 index 00000000..9a21dfd4 --- /dev/null +++ b/src/Console/QuestionFactory.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Console; + +use Symfony\Component\Console\Question\Question; + +/** + * Factory method for ConfirmationQuestion. + */ +class QuestionFactory +{ + /** + * Creates confirmation question. + * + * @param string $question The question to ask to the user + * @param string $default The default answer to return + * + * @return Question + */ + public function create(string $question, string $default = null): Question + { + return new Question($question, $default); + } +} diff --git a/src/Filesystem/FileList.php b/src/Filesystem/FileList.php index 7c95b40c..bdf3c741 100644 --- a/src/Filesystem/FileList.php +++ b/src/Filesystem/FileList.php @@ -33,6 +33,14 @@ public function getPatches(): string return $this->directoryList->getRoot() . '/patches.json'; } + /** + * @return string + */ + public function getCategoriesConfig(): string + { + return $this->directoryList->getRoot() . '/config/patch-categories.json'; + } + /** * @return string */ diff --git a/src/Filesystem/JsonConfigReader.php b/src/Filesystem/JsonConfigReader.php new file mode 100644 index 00000000..58a402b4 --- /dev/null +++ b/src/Filesystem/JsonConfigReader.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Filesystem; + +use Magento\CloudPatches\Patch\SourceProviderException; + +/** + * Json Reader. + */ +class JsonConfigReader +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem) + { + $this->filesystem = $filesystem; + } + + /** + * @param string $configPath + * @return array + * @throws SourceProviderException + */ + public function read(string $configPath): array + { + try { + $content = $this->filesystem->get($configPath); + } catch (FileSystemException $e) { + throw new SourceProviderException($e->getMessage(), $e->getCode(), $e); + } + $result = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new SourceProviderException( + "Unable to unserialize configuration '{$configPath}'. Error: " . json_last_error_msg() + ); + } + return $result; + } +} diff --git a/src/Patch/AggregatedPatchFactory.php b/src/Patch/AggregatedPatchFactory.php index fbcbea6d..fcc0bd7d 100644 --- a/src/Patch/AggregatedPatchFactory.php +++ b/src/Patch/AggregatedPatchFactory.php @@ -30,6 +30,8 @@ public function create( $id = $this->getId($items); $type = $this->getType($items); $title = $this->getTitle($items); + $categories = $this->getCategories($items); + $origin = $this->getOrigin($items); $components = $this->getAffectedComponents($items); $require = $this->getRequire($items); $replacedWith = $this->getReplacedWith($items); @@ -39,6 +41,8 @@ public function create( $id, $type, $title, + $categories, + $origin, $components, $require, $replacedWith, @@ -81,11 +85,37 @@ private function getType(array $patches): string */ private function getTitle(array $patches): string { - $patch = end($patches); + $patch = reset($patches); return $patch->getTitle(); } + /** + * Returns aggregated patch category. + * + * @param PatchInterface[] $patches + * @return array + */ + private function getCategories(array $patches): array + { + $patch = reset($patches); + + return $patch->getCategories(); + } + + /** + * Returns aggregated patch origin. + * + * @param PatchInterface[] $patches + * @return string + */ + private function getOrigin(array $patches): string + { + $patch = reset($patches); + + return $patch->getOrigin(); + } + /** * Returns aggregated list of affected components. * diff --git a/src/Patch/CategoriesList/GetLocalPatchCategories.php b/src/Patch/CategoriesList/GetLocalPatchCategories.php new file mode 100644 index 00000000..8d9e7012 --- /dev/null +++ b/src/Patch/CategoriesList/GetLocalPatchCategories.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\CategoriesList; + +use Magento\CloudPatches\Filesystem\FileList; +use Magento\CloudPatches\Filesystem\JsonConfigReader; +use Magento\CloudPatches\Patch\GetCategoriesListInterface; + +/** + * Returns array of local patch categories. + */ +class GetLocalPatchCategories implements GetCategoriesListInterface +{ + /** + * @var FileList + */ + private $fileList; + + /** + * @var JsonConfigReader + */ + private $jsonConfigReader; + + /** + * @param FileList $fileList + * @param JsonConfigReader $jsonConfigReader + */ + public function __construct(FileList $fileList, JsonConfigReader $jsonConfigReader) + { + $this->fileList = $fileList; + $this->jsonConfigReader = $jsonConfigReader; + } + + /** + * @inheritDoc + */ + public function execute(): array + { + return $this->jsonConfigReader->read($this->fileList->getCategoriesConfig()); + } +} diff --git a/src/Patch/CategoriesList/GetSupportPatchCategories.php b/src/Patch/CategoriesList/GetSupportPatchCategories.php new file mode 100644 index 00000000..469bd6f4 --- /dev/null +++ b/src/Patch/CategoriesList/GetSupportPatchCategories.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\CategoriesList; + +use Magento\CloudPatches\Composer\QualityPackage; +use Magento\CloudPatches\Filesystem\JsonConfigReader; +use Magento\CloudPatches\Patch\GetCategoriesListInterface; + +/** + * Return a list of support patch categories. + */ +class GetSupportPatchCategories implements GetCategoriesListInterface +{ + /** + * @var QualityPackage + */ + private $qualityPackage; + + /** + * @var JsonConfigReader + */ + private $jsonConfigReader; + + /** + + * @param QualityPackage $qualityPackage + * @param JsonConfigReader $jsonConfigReader + */ + public function __construct(QualityPackage $qualityPackage, JsonConfigReader $jsonConfigReader) + { + $this->qualityPackage = $qualityPackage; + $this->jsonConfigReader = $jsonConfigReader; + } + + /** + * @inheritDoc + */ + public function execute(): array + { + return $this->qualityPackage->getCategoriesConfigPath() + ? $this->jsonConfigReader->read($this->qualityPackage->getCategoriesConfigPath()) + : []; + } +} diff --git a/src/Patch/Collector/CloudCollector.php b/src/Patch/Collector/CloudCollector.php index eb818893..89a131bd 100644 --- a/src/Patch/Collector/CloudCollector.php +++ b/src/Patch/Collector/CloudCollector.php @@ -9,6 +9,7 @@ use Magento\CloudPatches\Environment\Config; use Magento\CloudPatches\Filesystem\DirectoryList; +use Magento\CloudPatches\Patch\CollectorInterface; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Composer\Package; use Magento\CloudPatches\Patch\PatchBuilder; @@ -19,7 +20,7 @@ /** * Collects cloud patches. */ -class CloudCollector +class CloudCollector implements CollectorInterface { /** * @var SourceProvider @@ -101,6 +102,8 @@ public function collect(): array $this->patchBuilder->setType($patchType); $this->patchBuilder->setPackageName($packageName); $this->patchBuilder->setPackageConstraint($packageConstraint); + $this->patchBuilder->setOrigin(SupportCollector::ORIGIN); + $this->patchBuilder->setCategories(['Other']); $result[] = $this->patchBuilder->build(); } catch (PatchIntegrityException $e) { diff --git a/src/Patch/Collector/CommunityCollector.php b/src/Patch/Collector/CommunityCollector.php new file mode 100644 index 00000000..03521ba4 --- /dev/null +++ b/src/Patch/Collector/CommunityCollector.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +/** + * Collects community patches. + */ +class CommunityCollector extends SupportCollector +{ + const ORIGIN = 'Magento OS Community'; +} diff --git a/src/Patch/Collector/GetCommunityPatchesConfig.php b/src/Patch/Collector/GetCommunityPatchesConfig.php new file mode 100644 index 00000000..119fb9fb --- /dev/null +++ b/src/Patch/Collector/GetCommunityPatchesConfig.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +use Magento\CloudPatches\Patch\SourceProvider; +use Magento\CloudPatches\Patch\SourceProviderException; + +/** + * Provides community patches configuration. + */ +class GetCommunityPatchesConfig implements GetPatchesConfigInterface +{ + /** + * @var array + */ + private $config = []; + + /** + * @var SourceProvider + */ + private $sourceProvider; + + /** + * @var ValidatePatchesConfig + */ + private $validatePatchesConfig; + + /** + * @param SourceProvider $sourceProvider + * @param ValidatePatchesConfig $validatePatchesConfig + */ + public function __construct(SourceProvider $sourceProvider, ValidatePatchesConfig $validatePatchesConfig) + { + $this->sourceProvider = $sourceProvider; + $this->validatePatchesConfig = $validatePatchesConfig; + } + + /** + * @return array + * @throws CollectorException + */ + public function execute(): array + { + if (empty($this->config)) { + try { + $this->config = $this->sourceProvider->getCommunityPatches(); + } catch (SourceProviderException $e) { + throw new CollectorException($e->getMessage(), $e->getCode(), $e); + } + $this->validatePatchesConfig->execute($this->config); + } + + return $this->config; + } +} diff --git a/src/Patch/Collector/GetPatchesConfigInterface.php b/src/Patch/Collector/GetPatchesConfigInterface.php new file mode 100644 index 00000000..26a95ce7 --- /dev/null +++ b/src/Patch/Collector/GetPatchesConfigInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +/** + * Provides config for patches. + */ +interface GetPatchesConfigInterface +{ + /** + * @return array + * @throws CollectorException + */ + public function execute(): array; +} diff --git a/src/Patch/Collector/GetSupportPatchesConfig.php b/src/Patch/Collector/GetSupportPatchesConfig.php new file mode 100644 index 00000000..c00833c0 --- /dev/null +++ b/src/Patch/Collector/GetSupportPatchesConfig.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +use Magento\CloudPatches\Patch\SourceProvider; +use Magento\CloudPatches\Patch\SourceProviderException; + +/** + * Returns support patches configuration. + */ +class GetSupportPatchesConfig implements GetPatchesConfigInterface +{ + /** + * @var array + */ + private $config = []; + + /** + * @var SourceProvider + */ + private $sourceProvider; + + /** + * @var ValidatePatchesConfig + */ + private $validatePatchesConfig; + + /** + * @param SourceProvider $sourceProvider + * @param ValidatePatchesConfig $validatePatchesConfig + */ + public function __construct(SourceProvider $sourceProvider, ValidatePatchesConfig $validatePatchesConfig) + { + $this->sourceProvider = $sourceProvider; + $this->validatePatchesConfig = $validatePatchesConfig; + } + + /** + * @return array + * @throws CollectorException + */ + public function execute(): array + { + if (empty($this->config)) { + try { + $this->config = $this->sourceProvider->getSupportPatches(); + } catch (SourceProviderException $e) { + throw new CollectorException($e->getMessage(), $e->getCode(), $e); + } + $this->validatePatchesConfig->execute($this->config); + } + + return $this->config; + } +} diff --git a/src/Patch/Collector/LocalCollector.php b/src/Patch/Collector/LocalCollector.php index 8f2b35cf..69a8af6f 100644 --- a/src/Patch/Collector/LocalCollector.php +++ b/src/Patch/Collector/LocalCollector.php @@ -16,6 +16,8 @@ */ class LocalCollector { + const ORIGIN = 'Local'; + /** * @var SourceProvider */ @@ -54,6 +56,8 @@ public function collect(): array $this->patchBuilder->setFilename(basename($file)); $this->patchBuilder->setPath($file); $this->patchBuilder->setType(PatchInterface::TYPE_CUSTOM); + $this->patchBuilder->setOrigin(self::ORIGIN); + $this->patchBuilder->setCategories(['Other']); $result[] = $this->patchBuilder->build(); } diff --git a/src/Patch/Collector/QualityCollector.php b/src/Patch/Collector/QualityCollector.php deleted file mode 100644 index 1671119c..00000000 --- a/src/Patch/Collector/QualityCollector.php +++ /dev/null @@ -1,279 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Patch\Collector; - -use Magento\CloudPatches\Composer\QualityPackage; -use Magento\CloudPatches\Patch\Data\PatchInterface; -use Magento\CloudPatches\Composer\Package; -use Magento\CloudPatches\Patch\PatchBuilder; -use Magento\CloudPatches\Patch\PatchIntegrityException; -use Magento\CloudPatches\Patch\SourceProvider; -use Magento\CloudPatches\Patch\SourceProviderException; - -/** - * Collects patches. - */ -class QualityCollector -{ - /** - * Configuration JSON property. - * - * Contains patch filename, type string. - */ - const PROP_FILE = 'file'; - - /** - * Configuration JSON property. - * - * Contains required patch ids, type array. - */ - const PROP_REQUIRE = 'require'; - - /** - * Configuration JSON property. - * - * Contains patch id that current patch replaced with, type string. - */ - const PROP_REPLACED_WITH = 'replaced-with'; - - /** - * Configuration JSON property. - * - * Defines whether patch is deprecated, type boolean. - */ - const PROP_DEPRECATED = 'deprecated'; - - /** - * @var SourceProvider - */ - private $sourceProvider; - - /** - * @var Package - */ - private $package; - - /** - * @var QualityPackage - */ - private $qualityPackage; - - /** - * @var array|null - */ - private $config = null; - - /** - * @var PatchBuilder - */ - private $patchBuilder; - - /** - * @param SourceProvider $sourceProvider - * @param Package $package - * @param QualityPackage $qualityPackage - * @param PatchBuilder $patchBuilder - */ - public function __construct( - SourceProvider $sourceProvider, - Package $package, - QualityPackage $qualityPackage, - PatchBuilder $patchBuilder - ) { - $this->sourceProvider = $sourceProvider; - $this->package = $package; - $this->qualityPackage = $qualityPackage; - $this->patchBuilder = $patchBuilder; - } - - /** - * Collects quality patches. - * - * @return PatchInterface[] - * - * @throws CollectorException - */ - public function collect() - { - $result = []; - foreach ($this->getConfig() as $patchId => $patchGeneralConfig) { - foreach ($patchGeneralConfig as $packageName => $packageConfiguration) { - foreach ($packageConfiguration as $patchTitle => $patchInfo) { - foreach ($patchInfo as $packageConstraint => $patchData) { - $patchFile = $patchData[static::PROP_FILE]; - $patchRequire = $patchData[static::PROP_REQUIRE] ?? []; - $patchReplacedWith = $patchData[static::PROP_REPLACED_WITH] ?? ''; - $patchDeprecated = $patchData[static::PROP_DEPRECATED] ?? (bool)$patchReplacedWith; - - if ($this->package->matchConstraint($packageName, $packageConstraint)) { - $result[] = $this->createPatch( - $patchId, - $patchTitle, - $patchFile, - $packageName, - $packageConstraint, - $patchRequire, - $patchReplacedWith, - $patchDeprecated - ); - } - } - } - } - } - - return $result; - } - - /** - * Validates patch configuration. - * - * @param array $config - * - * @return void - * @throws CollectorException - */ - private function validateConfiguration(array $config) - { - foreach ($config as $patchId => $patchGeneralConfig) { - $errors = []; - foreach ($patchGeneralConfig as $packageConfiguration) { - foreach ($packageConfiguration as $patchInfo) { - foreach ($patchInfo as $packageConstraint => $patchData) { - $errors = $this->validateProperties($patchData, $packageConstraint, $errors); - } - } - } - - if (!empty($errors)) { - array_unshift($errors, "Patch {$patchId} has invalid configuration:"); - - throw new CollectorException(implode(PHP_EOL . ' - ', $errors)); - } - } - } - - /** - * Returns patches config. - * - * @return array - * @throws CollectorException - */ - private function getConfig(): array - { - if ($this->config === null) { - try { - $this->config = $this->sourceProvider->getQualityPatches(); - } catch (SourceProviderException $e) { - throw new CollectorException($e->getMessage(), $e->getCode(), $e); - } - $this->validateConfiguration($this->config); - } - - return $this->config; - } - - /** - * Creates patch. - * - * @param string $patchId - * @param string $patchTitle - * @param string $patchFile - * @param string $packageName - * @param string $packageConstraint - * @param array $patchRequire - * @param string $patchReplacedWith - * @param bool $patchDeprecated - * - * @return PatchInterface - * @throws CollectorException - */ - private function createPatch( - string $patchId, - string $patchTitle, - string $patchFile, - string $packageName, - string $packageConstraint, - array $patchRequire, - string $patchReplacedWith, - bool $patchDeprecated - ): PatchInterface { - try { - $patchPath = $this->qualityPackage->getPatchesDirectory() . '/' . $patchFile; - $this->patchBuilder->setId($patchId); - $this->patchBuilder->setTitle($patchTitle); - $this->patchBuilder->setFilename($patchFile); - $this->patchBuilder->setPath($patchPath); - $this->patchBuilder->setType(PatchInterface::TYPE_OPTIONAL); - $this->patchBuilder->setPackageName($packageName); - $this->patchBuilder->setPackageConstraint($packageConstraint); - $this->patchBuilder->setRequire($patchRequire); - $this->patchBuilder->setReplacedWith($patchReplacedWith); - $this->patchBuilder->setDeprecated($patchDeprecated); - $patch = $this->patchBuilder->build(); - } catch (PatchIntegrityException $e) { - throw new CollectorException($e->getMessage(), $e->getCode(), $e); - } - - return $patch; - } - - /** - * Validates properties. - * - * @param array $patchData - * @param string $packageConstraint - * @param string[] $errors - * @return array - */ - private function validateProperties( - array $patchData, - string $packageConstraint, - array $errors - ): array { - if (!isset($patchData[static::PROP_FILE])) { - $errors[] = sprintf( - "Property '%s' is not found in '%s'", - static::PROP_FILE, - $packageConstraint - ); - } - - if (isset($patchData[static::PROP_REQUIRE]) && - !is_array($patchData[static::PROP_REQUIRE]) - ) { - $errors[] = sprintf( - "Property '%s' from '%s' should have an array type", - static::PROP_REQUIRE, - $packageConstraint - ); - } - - if (isset($patchData[static::PROP_REPLACED_WITH]) && - !is_string($patchData[static::PROP_REPLACED_WITH]) - ) { - $errors[] = sprintf( - "Property '%s' from '%s' should have a string type", - static::PROP_REPLACED_WITH, - $packageConstraint - ); - } - - if (isset($patchData[static::PROP_DEPRECATED]) && - !is_bool($patchData[static::PROP_DEPRECATED]) - ) { - $errors[] = sprintf( - "Property '%s' from '%s' should have a boolean type", - static::PROP_DEPRECATED, - $packageConstraint - ); - } - - return $errors; - } -} diff --git a/src/Patch/Collector/SupportCollector.php b/src/Patch/Collector/SupportCollector.php new file mode 100644 index 00000000..16d17dd9 --- /dev/null +++ b/src/Patch/Collector/SupportCollector.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +use Magento\CloudPatches\Composer\QualityPackage; +use Magento\CloudPatches\Patch\CollectorInterface; +use Magento\CloudPatches\Patch\Data\PatchInterface; +use Magento\CloudPatches\Composer\Package; +use Magento\CloudPatches\Patch\PatchBuilder; +use Magento\CloudPatches\Patch\PatchIntegrityException; + +/** + * Collects patches. + */ +class SupportCollector implements CollectorInterface +{ + /** + * Configuration JSON property. + * + * Contains patch filename, type string. + */ + const PROP_FILE = 'file'; + + /** + * Configuration JSON property. + * + * Contains required patch ids, type array. + */ + const PROP_REQUIRE = 'require'; + + /** + * Configuration JSON property. + * + * Contains patch id that current patch replaced with, type string. + */ + const PROP_REPLACED_WITH = 'replaced-with'; + + /** + * Configuration JSON property. + * + * Defines whether patch is deprecated, type boolean. + */ + const PROP_DEPRECATED = 'deprecated'; + + const PROP_CATEGORIES = 'categories'; + + const PROP_TITLE = 'title'; + + const PROP_PACKAGES = 'packages'; + + const ORIGIN = 'Adobe Commerce Support'; + + /** + * @var Package + */ + private $package; + + /** + * @var QualityPackage + */ + private $qualityPackage; + + /** + * @var PatchBuilder + */ + private $patchBuilder; + + /** + * @var GetPatchesConfigInterface + */ + private $getPatchesConfig; + + /** + * @param Package $package + * @param QualityPackage $qualityPackage + * @param PatchBuilder $patchBuilder + * @param GetPatchesConfigInterface $getPatchesConfig + */ + public function __construct( + Package $package, + QualityPackage $qualityPackage, + PatchBuilder $patchBuilder, + GetPatchesConfigInterface $getPatchesConfig + ) { + $this->package = $package; + $this->qualityPackage = $qualityPackage; + $this->patchBuilder = $patchBuilder; + $this->getPatchesConfig = $getPatchesConfig; + } + + /** + * Collects quality patches. + * + * @throws CollectorException + * @return PatchInterface[] + */ + public function collect(): array + { + $result = []; + foreach ($this->getPatchesConfig->execute() as $patchId => $patchGeneralConfig) { + foreach ($patchGeneralConfig[self::PROP_PACKAGES] as $packageName => $packageConfiguration) { + foreach ($packageConfiguration as $packageConstraint => $patchData) { + $patchFile = $patchData[self::PROP_FILE]; + $patchTitle = $patchGeneralConfig[self::PROP_TITLE]; + $patchRequire = $patchData[self::PROP_REQUIRE] ?? []; + $patchReplacedWith = $patchData[self::PROP_REPLACED_WITH] ?? ''; + $patchDeprecated = $patchData[self::PROP_DEPRECATED] ?? (bool)$patchReplacedWith; + $category = !empty($patchGeneralConfig[self::PROP_CATEGORIES]) + ? array_map('trim', $patchGeneralConfig[self::PROP_CATEGORIES]) + : ['Other']; + + if ($this->package->matchConstraint($packageName, $packageConstraint)) { + $result[] = $this->createPatch( + $patchId, + $patchTitle, + $category, + $patchFile, + $packageName, + $packageConstraint, + $patchRequire, + $patchReplacedWith, + $patchDeprecated + ); + } + } + } + } + return $result; + } + + /** + * Creates patch. + * + * @param string $patchId + * @param string $patchTitle + * @param array $patchCategories + * @param string $patchFile + * @param string $packageName + * @param string $packageConstraint + * @param array $patchRequire + * @param string $patchReplacedWith + * @param bool $patchDeprecated + * + * @return PatchInterface + * @throws CollectorException + */ + private function createPatch( + string $patchId, + string $patchTitle, + array $patchCategories, + string $patchFile, + string $packageName, + string $packageConstraint, + array $patchRequire, + string $patchReplacedWith, + bool $patchDeprecated + ): PatchInterface { + try { + $patchPath = $this->qualityPackage->getPatchesDirectoryPath() . '/' . $patchFile; + $this->patchBuilder->setId($patchId); + $this->patchBuilder->setTitle($patchTitle); + $this->patchBuilder->setCategories($patchCategories); + $this->patchBuilder->setOrigin(static::ORIGIN); + $this->patchBuilder->setFilename($patchFile); + $this->patchBuilder->setPath($patchPath); + $this->patchBuilder->setType(PatchInterface::TYPE_OPTIONAL); + $this->patchBuilder->setPackageName($packageName); + $this->patchBuilder->setPackageConstraint($packageConstraint); + $this->patchBuilder->setRequire($patchRequire); + $this->patchBuilder->setReplacedWith($patchReplacedWith); + $this->patchBuilder->setDeprecated($patchDeprecated); + $patch = $this->patchBuilder->build(); + } catch (PatchIntegrityException $e) { + throw new CollectorException($e->getMessage(), $e->getCode(), $e); + } + + return $patch; + } +} diff --git a/src/Patch/Collector/ValidatePatchesConfig.php b/src/Patch/Collector/ValidatePatchesConfig.php new file mode 100644 index 00000000..03e6ef7a --- /dev/null +++ b/src/Patch/Collector/ValidatePatchesConfig.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch\Collector; + +/** + * Patch configuration validator. + */ +class ValidatePatchesConfig +{ + /** + * Validates patch configuration. + * + * @param array $config + * + * @return void + * @throws CollectorException + */ + public function execute(array $config) + { + foreach ($config as $patchId => $patchGeneralConfig) { + $errors = []; + foreach ($patchGeneralConfig['packages'] as $packageConfiguration) { + foreach ($packageConfiguration as $packageConstraint => $patchData) { + $errors = $this->validateProperties($patchData, $packageConstraint, $errors); + } + } + + if (!empty($errors)) { + array_unshift($errors, "Patch {$patchId} has invalid configuration:"); + throw new CollectorException(implode(PHP_EOL . ' - ', $errors)); + } + } + } + + /** + * Validates properties. + * + * @param array $patchData + * @param string $packageConstraint + * @param string[] $errors + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function validateProperties( + array $patchData, + string $packageConstraint, + array $errors + ): array { + if (!isset($patchData[SupportCollector::PROP_FILE])) { + $errors[] = sprintf( + "Property '%s' is not found in '%s'", + SupportCollector::PROP_FILE, + $packageConstraint + ); + } + + if (isset($patchData[SupportCollector::PROP_REQUIRE]) && + !is_array($patchData[SupportCollector::PROP_REQUIRE]) + ) { + $errors[] = sprintf( + "Property '%s' from '%s' should have an array type", + SupportCollector::PROP_REQUIRE, + $packageConstraint + ); + } + + if (isset($patchData[SupportCollector::PROP_REPLACED_WITH]) && + !is_string($patchData[SupportCollector::PROP_REPLACED_WITH]) + ) { + $errors[] = sprintf( + "Property '%s' from '%s' should have a string type", + SupportCollector::PROP_REPLACED_WITH, + $packageConstraint + ); + } + + if (isset($patchData[SupportCollector::PROP_DEPRECATED]) && + !is_bool($patchData[SupportCollector::PROP_DEPRECATED]) + ) { + $errors[] = sprintf( + "Property '%s' from '%s' should have a boolean type", + SupportCollector::PROP_DEPRECATED, + $packageConstraint + ); + } + + if (isset($patchData[SupportCollector::PROP_CATEGORIES]) && + !is_array($patchData[SupportCollector::PROP_CATEGORIES]) + ) { + $errors[] = sprintf( + "Property '%s' from '%s' should have a array type", + SupportCollector::PROP_DEPRECATED, + $packageConstraint + ); + } + + return $errors; + } +} diff --git a/src/Patch/CollectorInterface.php b/src/Patch/CollectorInterface.php new file mode 100644 index 00000000..9196ef94 --- /dev/null +++ b/src/Patch/CollectorInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +use Magento\CloudPatches\Patch\Collector\CollectorException; +use Magento\CloudPatches\Patch\Data\PatchInterface; + +/** + * Patches collector interface + */ +interface CollectorInterface +{ + /** + * Collects patches. + * + * @return PatchInterface[] + * @throws CollectorException + */ + public function collect(): array; +} diff --git a/src/Patch/Data/AggregatedPatch.php b/src/Patch/Data/AggregatedPatch.php index 3a379a3c..f4d0bea7 100644 --- a/src/Patch/Data/AggregatedPatch.php +++ b/src/Patch/Data/AggregatedPatch.php @@ -27,6 +27,11 @@ class AggregatedPatch implements AggregatedPatchInterface */ private $title; + /** + * @var array + */ + private $categories; + /** * @var array */ @@ -52,20 +57,30 @@ class AggregatedPatch implements AggregatedPatchInterface */ private $items; + /** + * @var string + */ + private $origin; + /** * @param string $id * @param string $type * @param string $title + * @param array $categories + * @param string $origin * @param string[] $affectedComponents * @param string[] $require * @param string $replacedWith * @param bool $isDeprecated * @param PatchInterface[] $items + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( string $id, string $type, string $title, + array $categories, + string $origin, array $affectedComponents, array $require, string $replacedWith, @@ -76,6 +91,8 @@ public function __construct( $this->id = $id; $this->type = $type; $this->title = $title; + $this->categories = $categories; + $this->origin = $origin; $this->affectedComponents = $affectedComponents; $this->require = $require; $this->replacedWith = $replacedWith; @@ -115,6 +132,22 @@ public function getTitle(): string return $this->title; } + /** + * @inheritDoc + */ + public function getCategories(): array + { + return $this->categories; + } + + /** + * @inheritDoc + */ + public function getOrigin(): string + { + return $this->origin; + } + /** * @inheritDoc */ diff --git a/src/Patch/Data/AggregatedPatchInterface.php b/src/Patch/Data/AggregatedPatchInterface.php index 1c5a81b4..8f65cb42 100644 --- a/src/Patch/Data/AggregatedPatchInterface.php +++ b/src/Patch/Data/AggregatedPatchInterface.php @@ -35,6 +35,20 @@ public function getType(): string; */ public function getTitle(): string; + /** + * Aggregated patch category. + * + * @return array + */ + public function getCategories(): array; + + /** + * Aggregated patch origin. + * + * @return string + */ + public function getOrigin(): string; + /** * List of affected components. * diff --git a/src/Patch/Data/Patch.php b/src/Patch/Data/Patch.php index ffccef19..a35966ea 100644 --- a/src/Patch/Data/Patch.php +++ b/src/Patch/Data/Patch.php @@ -27,6 +27,11 @@ class Patch implements PatchInterface */ private $title; + /** + * @var array + */ + private $categories; + /** * @var string */ @@ -66,10 +71,17 @@ class Patch implements PatchInterface */ private $isDeprecated; + /** + * @var string + */ + private $origin; + /** * @param string $id * @param string $type * @param string $title + * @param array $categories + * @param string $origin * @param string $filename * @param string $path * @param string $packageName @@ -85,6 +97,8 @@ public function __construct( string $id, string $type, string $title, + array $categories, + string $origin, string $filename, string $path, string $packageName, @@ -98,6 +112,8 @@ public function __construct( $this->id = $id; $this->type = $type; $this->title = $title; + $this->categories = $categories; + $this->origin = $origin; $this->filename = $filename; $this->path = $path; $this->packageName = $packageName; @@ -140,6 +156,22 @@ public function getTitle(): string return $this->title; } + /** + * @inheritDoc + */ + public function getCategories(): array + { + return $this->categories; + } + + /** + * @inheritDoc + */ + public function getOrigin(): string + { + return $this->origin; + } + /** * @inheritDoc */ diff --git a/src/Patch/Data/PatchInterface.php b/src/Patch/Data/PatchInterface.php index a9df332b..6d5d7cd1 100644 --- a/src/Patch/Data/PatchInterface.php +++ b/src/Patch/Data/PatchInterface.php @@ -50,6 +50,20 @@ public function getType(): string; */ public function getTitle(): string; + /** + * Patch categories. + * + * @return array + */ + public function getCategories(): array; + + /** + * Patch origin. + * + * @return string + */ + public function getOrigin(): string; + /** * Patch filename. * diff --git a/src/Patch/GetCategoriesList.php b/src/Patch/GetCategoriesList.php new file mode 100644 index 00000000..7f3c059b --- /dev/null +++ b/src/Patch/GetCategoriesList.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +/** + * Provides patch categories list. + */ +class GetCategoriesList implements GetCategoriesListInterface +{ + /** + * @var GetCategoriesListInterface[] + */ + private $providers; + + /** + * @param GetCategoriesListInterface[] $providers + */ + public function __construct(array $providers) + { + $this->providers = $providers; + } + + /** + * @inheritDoc + */ + public function execute(): array + { + $categories = []; + foreach ($this->providers as $provider) { + $categories = array_merge($categories, $provider->execute()); + } + + return array_unique($categories, SORT_REGULAR); + } +} diff --git a/src/Patch/GetCategoriesListInterface.php b/src/Patch/GetCategoriesListInterface.php new file mode 100644 index 00000000..aeb3ae14 --- /dev/null +++ b/src/Patch/GetCategoriesListInterface.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Patch; + +/** + * Categories list provider. + */ +interface GetCategoriesListInterface +{ + /** + * @return array + */ + public function execute(): array; +} diff --git a/src/Patch/PatchBuilder.php b/src/Patch/PatchBuilder.php index fd87e557..cbfd2d2d 100644 --- a/src/Patch/PatchBuilder.php +++ b/src/Patch/PatchBuilder.php @@ -34,6 +34,16 @@ class PatchBuilder */ private $title; + /** + * @var array + */ + private $categories = []; + + /** + * @var string + */ + private $origin = ''; + /** * @var string */ @@ -115,6 +125,30 @@ public function setTitle(string $title) $this->title = $title; } + /** + * Sets patch categories. + * + * @param array $categories + * + * @return void + */ + public function setCategories(array $categories) + { + $this->categories = $categories; + } + + /** + * Sets patch origin. + * + * @param string $origin + * + * @return void + */ + public function setOrigin(string $origin) + { + $this->origin = trim($origin); + } + /** * Sets patch filename. * @@ -207,6 +241,8 @@ public function build() $id, $this->type, $this->title, + $this->categories, + $this->origin, $this->filename, $this->path, $this->packageName, diff --git a/src/Patch/Pool/OptionalPool.php b/src/Patch/Pool/OptionalPool.php index 6241aef1..03185fb8 100644 --- a/src/Patch/Pool/OptionalPool.php +++ b/src/Patch/Pool/OptionalPool.php @@ -8,9 +8,11 @@ namespace Magento\CloudPatches\Patch\Pool; use Magento\CloudPatches\Patch\Collector\CollectorException; -use Magento\CloudPatches\Patch\Collector\QualityCollector; +use Magento\CloudPatches\Patch\Collector\SupportCollector; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Patch\PatchIntegrityException; +use Magento\CloudPatches\Patch\Collector\CommunityCollector; +use Magento\CloudPatches\Patch\CollectorInterface; /** * Contains all optional patches. @@ -20,21 +22,18 @@ class OptionalPool /** * @var PatchInterface[] */ - private $items; + private $items = []; /** - * @param RequiredPool $requiredPool - * @param QualityCollector $qualityCollector + * @param array $collectors * @throws CollectorException */ - public function __construct( - RequiredPool $requiredPool, - QualityCollector $qualityCollector - ) { - $this->items = array_merge( - $requiredPool->getList(), - $qualityCollector->collect() - ); + public function __construct(array $collectors = []) + { + /** @var CollectorInterface $collector */ + foreach ($collectors as $collector) { + $this->items = array_merge($this->items, $collector->collect()); + } } /** @@ -75,12 +74,30 @@ public function getList($filter = [], $useRequire = true) * @return PatchInterface[] * @throws PatchIntegrityException */ - public function getOptionalAll() + public function getOptionalAll(): array + { + return $this->getOptionalListByOrigin( + [ + SupportCollector::ORIGIN, + CommunityCollector::ORIGIN, + ] + ); + } + + /** + * Returns list of all optional patches. + * + * @param array $listOfOrigins + * @return PatchInterface[] + * @throws PatchIntegrityException + */ + public function getOptionalListByOrigin(array $listOfOrigins): array { $items = array_filter( $this->items, - function ($patch) { - return $patch->getType() === PatchInterface::TYPE_OPTIONAL; + function ($patch) use ($listOfOrigins) { + return $patch->getType() === PatchInterface::TYPE_OPTIONAL + && in_array($patch->getOrigin(), $listOfOrigins); } ); @@ -91,9 +108,7 @@ function ($patch) { } $result[] = [$patch]; } - $result = $result ? array_unique(array_merge(...$result)) : []; - - return $result; + return $result ? array_unique(array_merge(...$result)) : []; } /** @@ -102,7 +117,7 @@ function ($patch) { * @param string $patchId * @return string[] */ - public function getDependentOn($patchId) + public function getDependentOn(string $patchId): array { if (!$patchId) { return []; @@ -127,7 +142,7 @@ public function getDependentOn($patchId) * @param string $patchId * @return string[] */ - public function getDependencies($patchId) + public function getDependencies(string $patchId): array { $result = array_map( function (PatchInterface $patch) { diff --git a/src/Patch/Pool/RequiredPool.php b/src/Patch/Pool/RequiredPool.php index 3c2f2ddb..dabfe5f3 100644 --- a/src/Patch/Pool/RequiredPool.php +++ b/src/Patch/Pool/RequiredPool.php @@ -7,8 +7,8 @@ namespace Magento\CloudPatches\Patch\Pool; -use Magento\CloudPatches\Patch\Collector\CloudCollector; use Magento\CloudPatches\Patch\Collector\CollectorException; +use Magento\CloudPatches\Patch\CollectorInterface; use Magento\CloudPatches\Patch\Data\PatchInterface; /** @@ -19,16 +19,18 @@ class RequiredPool /** * @var PatchInterface[] */ - private $items; + private $items = []; /** - * @param CloudCollector $cloudCollector + * @param array $collectors * @throws CollectorException */ - public function __construct( - CloudCollector $cloudCollector - ) { - $this->items = $cloudCollector->collect(); + public function __construct(array $collectors = []) + { + /** @var CollectorInterface $collector */ + foreach ($collectors as $collector) { + $this->items = array_merge($this->items, $collector->collect()); + } } /** diff --git a/src/Patch/SourceProvider.php b/src/Patch/SourceProvider.php index 743fdbd4..7d824121 100644 --- a/src/Patch/SourceProvider.php +++ b/src/Patch/SourceProvider.php @@ -10,8 +10,7 @@ use Magento\CloudPatches\Composer\QualityPackage; use Magento\CloudPatches\Filesystem\DirectoryList; use Magento\CloudPatches\Filesystem\FileList; -use Magento\CloudPatches\Filesystem\FileSystemException; -use Magento\CloudPatches\Filesystem\Filesystem; +use Magento\CloudPatches\Filesystem\JsonConfigReader; /** * Patches config provider. @@ -23,11 +22,6 @@ class SourceProvider */ const HOT_FIXES_DIR = 'm2-hotfixes'; - /** - * @var Filesystem - */ - private $filesystem; - /** * @var FileList */ @@ -44,21 +38,26 @@ class SourceProvider private $qualityPackage; /** - * @param Filesystem $filesystem + * @var JsonConfigReader + */ + private $jsonConfigReader; + + /** * @param FileList $fileList * @param DirectoryList $directoryList * @param QualityPackage $qualityPackage + * @param JsonConfigReader $jsonConfigReader */ public function __construct( - Filesystem $filesystem, FileList $fileList, DirectoryList $directoryList, - QualityPackage $qualityPackage + QualityPackage $qualityPackage, + JsonConfigReader $jsonConfigReader ) { - $this->filesystem = $filesystem; $this->fileList = $fileList; $this->directoryList = $directoryList; $this->qualityPackage = $qualityPackage; + $this->jsonConfigReader = $jsonConfigReader; } /** @@ -82,7 +81,7 @@ public function getCloudPatches(): array { $configPath = $this->fileList->getPatches(); - return $this->readConfiguration($configPath); + return $this->jsonConfigReader->read($configPath); } /** @@ -91,11 +90,22 @@ public function getCloudPatches(): array * @return array * @throws SourceProviderException */ - public function getQualityPatches(): array + public function getSupportPatches(): array { - $configPath = $this->qualityPackage->getPatchesConfig(); + $configPath = $this->qualityPackage->getSupportPatchesConfigPath(); + return $configPath ? $this->jsonConfigReader->read($configPath) : []; + } - return $configPath ? $this->readConfiguration($configPath) : []; + /** + * Returns configuration of Community patches. + * + * @return array + * @throws SourceProviderException + */ + public function getCommunityPatches(): array + { + $configPath = $this->qualityPackage->getCommunityPatchesConfigPath(); + return $configPath ? $this->jsonConfigReader->read($configPath) : []; } /** @@ -113,30 +123,4 @@ public function getLocalPatches(): array return $files ?: []; } - - /** - * Return patch configuration. - * - * @param string $configPath - * - * @return array - * @throws SourceProviderException - */ - private function readConfiguration(string $configPath): array - { - try { - $content = $this->filesystem->get($configPath); - } catch (FileSystemException $e) { - throw new SourceProviderException($e->getMessage(), $e->getCode(), $e); - } - - $result = json_decode($content, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new SourceProviderException( - "Unable to unserialize patches configuration '{$configPath}'. Error: " . json_last_error_msg() - ); - } - - return $result; - } } diff --git a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php index dabaa7fc..c543a2de 100644 --- a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php @@ -192,7 +192,8 @@ public function testApplyingAllPatchesAndSkipDeprecated() /** @var OutputInterface|MockObject $outputMock */ $outputMock = $this->getMockForAbstractClass(OutputInterface::class); $this->optionalPool->expects($this->once()) - ->method('getOptionalAll') + ->method('getOptionalListByOrigin') + ->with(['Adobe Commerce Support']) ->willReturn([$patch1, $patch2]); $this->applier->method('apply') diff --git a/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php b/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php index 3d7b2354..c5224b3b 100644 --- a/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php @@ -90,7 +90,7 @@ public function testAppliedPatchesExceedsLimit() ->willReturn(true); $this->optionalPool->expects($this->once()) - ->method('getOptionalAll') + ->method('getOptionalListByOrigin') ->willReturn($appliedPatches); $outputMock->expects($this->once()) @@ -119,7 +119,7 @@ public function testAppliedPatchesNotExceedLimit() ->willReturn(true); $this->optionalPool->expects($this->once()) - ->method('getOptionalAll') + ->method('getOptionalListByOrigin') ->willReturn($appliedPatches); $outputMock->expects($this->never()) diff --git a/src/Test/Unit/Command/Process/ShowStatusTest.php b/src/Test/Unit/Command/Process/ShowStatusTest.php index b5a555a3..15fa3f2e 100644 --- a/src/Test/Unit/Command/Process/ShowStatusTest.php +++ b/src/Test/Unit/Command/Process/ShowStatusTest.php @@ -10,6 +10,7 @@ use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; use Magento\CloudPatches\Command\Process\Renderer; use Magento\CloudPatches\Command\Process\ShowStatus; +use Magento\CloudPatches\Console\QuestionFactory; use Magento\CloudPatches\Patch\Aggregator; use Magento\CloudPatches\Patch\Data\AggregatedPatch; use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; @@ -19,11 +20,13 @@ use Magento\CloudPatches\Patch\Status\StatusPool; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * @inheritdoc + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShowStatusTest extends TestCase { @@ -62,6 +65,16 @@ class ShowStatusTest extends TestCase */ private $localPool; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Symfony\Component\Console\Helper\QuestionHelper + */ + private $questionHelper; + + /** + * @var \Magento\CloudPatches\Console\QuestionFactory|\PHPUnit\Framework\MockObject\MockObject + */ + private $questionFactory; + /** * @inheritdoc */ @@ -73,6 +86,8 @@ protected function setUp() $this->statusPool = $this->createMock(StatusPool::class); $this->reviewAppliedAction = $this->createMock(ReviewAppliedAction::class); $this->renderer = $this->createMock(Renderer::class); + $this->questionHelper = $this->createMock(QuestionHelper::class); + $this->questionFactory = $this->createMock(QuestionFactory::class); $this->manager = new ShowStatus( $this->aggregator, @@ -80,7 +95,9 @@ protected function setUp() $this->localPool, $this->statusPool, $this->reviewAppliedAction, - $this->renderer + $this->renderer, + $this->questionHelper, + $this->questionFactory ); } diff --git a/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php b/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php index 032f9e61..fa1b8567 100644 --- a/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php +++ b/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php @@ -42,13 +42,13 @@ public function testCreate(array $patches, array $expectedResult) { $aggregatedPatch = $this->aggregatedPatchFactory->create($patches); - $this->assertEquals($aggregatedPatch->getId(), $expectedResult['id']); - $this->assertEquals($aggregatedPatch->getTitle(), $expectedResult['title']); - $this->assertEquals($aggregatedPatch->getType(), $expectedResult['type']); - $this->assertEquals($aggregatedPatch->getAffectedComponents(), $expectedResult['affected_components']); - $this->assertEquals($aggregatedPatch->getRequire(), $expectedResult['require']); - $this->assertEquals($aggregatedPatch->getReplacedWith(), $expectedResult['replaced_with']); - $this->assertEquals($aggregatedPatch->isDeprecated(), $expectedResult['is_deprecated']); + $this->assertEquals($expectedResult['id'], $aggregatedPatch->getId()); + $this->assertEquals($expectedResult['title'], $aggregatedPatch->getTitle()); + $this->assertEquals($expectedResult['type'], $aggregatedPatch->getType()); + $this->assertEquals($expectedResult['affected_components'], $aggregatedPatch->getAffectedComponents()); + $this->assertEquals($expectedResult['require'], $aggregatedPatch->getRequire()); + $this->assertEquals($expectedResult['replaced_with'], $aggregatedPatch->getReplacedWith()); + $this->assertEquals($expectedResult['is_deprecated'], $aggregatedPatch->isDeprecated()); } /** @@ -80,7 +80,7 @@ public function createDataProvider(): array ], 'expectedResult' => [ 'id' => 'MC-1', - 'title' => 'Title patch MC-1 EE', + 'title' => 'Title patch MC-1 CE', 'type' => 'Optional', 'affected_components' => ['magento-module1', 'magento-module2', 'magento-module3'], 'require' => ['MC-2', 'MC-3'], diff --git a/src/Test/Unit/Patch/Collector/Fixture/quality_config_invalid.php b/src/Test/Unit/Patch/Collector/Fixture/quality_config_invalid.php index 5d2f2282..e49e9aa1 100644 --- a/src/Test/Unit/Patch/Collector/Fixture/quality_config_invalid.php +++ b/src/Test/Unit/Patch/Collector/Fixture/quality_config_invalid.php @@ -9,14 +9,18 @@ return [ 'MDVA-2033' => [ - 'magento/magento2-ee-base' => [ - 'Allow DB dumps done with the support module to complete' => [ + 'categories' => [ + 'Performance' + ], + 'title' => 'Allow DB dumps done with the support module to complete', + 'packages' => [ + 'magento/magento2-ee-base' => [ '2.2.0 - 2.2.5' => [ 'require' => 'MC-11111 MC-22222', 'replaced-with' => ['MC-33333'], 'deprecated' => 1 ] - ], - ] + ] + ], ], ]; diff --git a/src/Test/Unit/Patch/Collector/Fixture/quality_config_valid.php b/src/Test/Unit/Patch/Collector/Fixture/quality_config_valid.php index 23345411..e9ca2bc3 100644 --- a/src/Test/Unit/Patch/Collector/Fixture/quality_config_valid.php +++ b/src/Test/Unit/Patch/Collector/Fixture/quality_config_valid.php @@ -9,8 +9,12 @@ return [ 'MDVA-2470' => [ - 'magento/magento2-base' => [ - 'Fix asset locker race condition when using Redis' => [ + 'categories' => [ + 'Performance' + ], + 'title' => 'Fix asset locker race condition when using Redis', + 'packages' => [ + 'magento/magento2-base' => [ '2.1.4 - 2.1.14' => [ 'file' => 'MDVA-2470__fix_asset_locking_race_condition__2.1.4.patch' ], @@ -18,26 +22,27 @@ 'file' => 'MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch' ] ], - ], - 'magento/magento2-ee-base' => [ - 'Fix asset locker race condition when using Redis EE' => [ + 'magento/magento2-ee-base' => [ '2.2.0 - 2.2.5' => [ 'file' => 'MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch' ] - ], - ] - + ] + ], ], 'MDVA-2033' => [ - 'magento/magento2-ee-base' => [ - 'Allow DB dumps done with the support module to complete' => [ + 'categories' => [ + 'Performance' + ], + 'title' => 'Allow DB dumps done with the support module to complete', + 'packages' => [ + 'magento/magento2-ee-base' => [ '2.2.0 - 2.2.5' => [ 'file' => 'MDVA-2033__prevent_deadlock_during_db_dump__2.2.0.patch', 'require' => ['MC-11111', 'MC-22222'], 'replaced-with' => 'MC-33333', 'deprecated' => true ] - ], - ] + ] + ], ], ]; diff --git a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php index b9e38531..19f6d67a 100644 --- a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php @@ -7,10 +7,14 @@ namespace Magento\CloudPatches\Test\Unit\Patch\Collector; +use Magento\CloudPatches\App\GenericException; use Magento\CloudPatches\Composer\Package; use Magento\CloudPatches\Composer\QualityPackage; use Magento\CloudPatches\Patch\Collector\CollectorException; -use Magento\CloudPatches\Patch\Collector\QualityCollector; +use Magento\CloudPatches\Patch\Collector\GetPatchesConfigInterface; +use Magento\CloudPatches\Patch\Collector\GetSupportPatchesConfig; +use Magento\CloudPatches\Patch\Collector\SupportCollector; +use Magento\CloudPatches\Patch\Collector\ValidatePatchesConfig; use Magento\CloudPatches\Patch\Data\Patch; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Patch\PatchBuilder; @@ -28,7 +32,7 @@ class QualityCollectorTest extends TestCase const QUALITY_PATCH_DIR = 'quality/patch/dir'; /** - * @var QualityCollector + * @var SupportCollector */ private $collector; @@ -37,11 +41,6 @@ class QualityCollectorTest extends TestCase */ private $patchBuilder; - /** - * @var SourceProvider|MockObject - */ - private $sourceProvider; - /** * @var Package|MockObject */ @@ -52,21 +51,26 @@ class QualityCollectorTest extends TestCase */ private $qualityPackage; + /** + * @var \Magento\CloudPatches\Patch\Collector\GetPatchesConfigInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $patchesConfig; + /** * @inheritDoc */ protected function setUp() { - $this->sourceProvider = $this->createMock(SourceProvider::class); $this->package = $this->createMock(Package::class); $this->qualityPackage = $this->createMock(QualityPackage::class); $this->patchBuilder = $this->createMock(PatchBuilder::class); + $this->patchesConfig = $this->createMock(GetPatchesConfigInterface::class); - $this->collector = new QualityCollector( - $this->sourceProvider, + $this->collector = new SupportCollector( $this->package, $this->qualityPackage, - $this->patchBuilder + $this->patchBuilder, + $this->patchesConfig ); } @@ -76,10 +80,10 @@ protected function setUp() public function testCollectSuccessful() { $validConfig = require __DIR__ . '/Fixture/quality_config_valid.php'; - $this->sourceProvider->expects($this->once()) - ->method('getQualityPatches') + $this->patchesConfig->expects($this->once()) + ->method('execute') ->willReturn($validConfig); - $this->qualityPackage->method('getPatchesDirectory') + $this->qualityPackage->method('getPatchesDirectoryPath') ->willReturn(self::QUALITY_PATCH_DIR); $this->package->method('matchConstraint') @@ -103,7 +107,7 @@ public function testCollectSuccessful() ->method('setTitle') ->withConsecutive( ['Fix asset locker race condition when using Redis'], - ['Fix asset locker race condition when using Redis EE'], + ['Fix asset locker race condition when using Redis'], ['Allow DB dumps done with the support module to complete'] ); $this->patchBuilder->expects($this->exactly(3)) @@ -171,9 +175,13 @@ public function testInvalidConfiguration() PHP_EOL . ' - Property \'replaced-with\' from \'2.2.0 - 2.2.5\' should have a string type' . PHP_EOL . ' - Property \'deprecated\' from \'2.2.0 - 2.2.5\' should have a boolean type'; - $this->sourceProvider->expects($this->once()) - ->method('getQualityPatches') - ->willReturn($config); + $sourceProvider = $this->createMock(SourceProvider::class); + $sourceProvider->expects($this->once())->method('getSupportPatches')->willReturn($config); + + $this->patchesConfig = new GetSupportPatchesConfig( + $sourceProvider, + new ValidatePatchesConfig() + ); $this->patchBuilder->expects($this->never()) ->method('build'); @@ -181,6 +189,13 @@ public function testInvalidConfiguration() $this->expectException(CollectorException::class); $this->expectExceptionMessage($expectedExceptionMessage); + $this->collector = new SupportCollector( + $this->package, + $this->qualityPackage, + $this->patchBuilder, + $this->patchesConfig + ); + $this->collector->collect(); } @@ -190,8 +205,8 @@ public function testInvalidConfiguration() public function testPatchIntegrityException() { $validConfig = require __DIR__ . '/Fixture/quality_config_valid.php'; - $this->sourceProvider->expects($this->once()) - ->method('getQualityPatches') + $this->patchesConfig->expects($this->once()) + ->method('execute') ->willReturn($validConfig); $this->package->method('matchConstraint') @@ -213,9 +228,9 @@ public function testPatchIntegrityException() */ public function testSourceProviderException() { - $this->sourceProvider->expects($this->once()) - ->method('getQualityPatches') - ->willThrowException(new SourceProviderException('')); + $this->patchesConfig->expects($this->once()) + ->method('execute') + ->willThrowException(new CollectorException('')); $this->patchBuilder->expects($this->never()) ->method('build'); diff --git a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php index 58c68cca..61370979 100644 --- a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php +++ b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php @@ -7,8 +7,9 @@ namespace Magento\CloudPatches\Test\Unit\Patch\Pool; +use Magento\CloudPatches\Patch\Collector\CloudCollector; use Magento\CloudPatches\Patch\Collector\CollectorException; -use Magento\CloudPatches\Patch\Collector\QualityCollector; +use Magento\CloudPatches\Patch\Collector\SupportCollector; use Magento\CloudPatches\Patch\Data\Patch; use Magento\CloudPatches\Patch\Data\PatchInterface; use Magento\CloudPatches\Patch\PatchIntegrityException; @@ -24,22 +25,22 @@ class OptionalPoolTest extends TestCase { /** - * @var RequiredPool|MockObject + * @var SupportCollector|MockObject */ - private $requiredPool; + private $qualityCollector; /** - * @var QualityCollector|MockObject + * @var \Magento\CloudPatches\Patch\Collector\CloudCollector|\PHPUnit\Framework\MockObject\MockObject */ - private $qualityCollector; + private $cloudCollector; /** * @inheritDoc */ protected function setUp() { - $this->requiredPool = $this->createMock(RequiredPool::class); - $this->qualityCollector = $this->createMock(QualityCollector::class); + $this->cloudCollector = $this->createMock(CloudCollector::class); + $this->qualityCollector = $this->createMock(SupportCollector::class); } /** @@ -322,6 +323,7 @@ private function createPatch(string $id, array $require = [], string $replacedWi $patch->method('getId')->willReturn($id); $patch->method('getRequire')->willReturn($require); $patch->method('getReplacedWith')->willReturn($replacedWith); + $patch->method('getOrigin')->willReturn(SupportCollector::ORIGIN); // To make mock object unique for assertions and array operations. $patch->id = microtime(); @@ -341,15 +343,19 @@ private function createPatch(string $id, array $require = [], string $replacedWi */ private function createPool(array $cloudPatches = [], array $qualityPatches = []): OptionalPool { - $this->requiredPool->expects($this->once()) - ->method('getList') + $this->cloudCollector->expects($this->once()) + ->method('collect') ->willReturn($cloudPatches); $this->qualityCollector->expects($this->once()) ->method('collect') ->willReturn($qualityPatches); - $pool = new OptionalPool($this->requiredPool, $this->qualityCollector); + $collectors = [ + $this->cloudCollector, + $this->qualityCollector + ]; + $pool = new OptionalPool($collectors); return $pool; } diff --git a/src/Test/Unit/Patch/Pool/RequiredPoolTest.php b/src/Test/Unit/Patch/Pool/RequiredPoolTest.php index f3f92d9e..a67a0c4f 100644 --- a/src/Test/Unit/Patch/Pool/RequiredPoolTest.php +++ b/src/Test/Unit/Patch/Pool/RequiredPoolTest.php @@ -33,7 +33,7 @@ public function testGetList() ->method('collect') ->willReturn([$patch1, $patch2, $patch3]); - $pool = new RequiredPool($cloudCollector); + $pool = new RequiredPool([$cloudCollector]); $this->assertEquals([$patch1, $patch2, $patch3], $pool->getList()); } diff --git a/src/Test/Unit/Patch/SourceProviderTest.php b/src/Test/Unit/Patch/SourceProviderTest.php index 3ad8ca52..15f065a8 100644 --- a/src/Test/Unit/Patch/SourceProviderTest.php +++ b/src/Test/Unit/Patch/SourceProviderTest.php @@ -7,11 +7,12 @@ namespace Magento\CloudPatches\Test\Unit\Patch; +use Magento\CloudPatches\App\GenericException; use Magento\CloudPatches\Composer\QualityPackage; use Magento\CloudPatches\Filesystem\DirectoryList; use Magento\CloudPatches\Filesystem\FileList; -use Magento\CloudPatches\Filesystem\Filesystem; use Magento\CloudPatches\Filesystem\FileSystemException; +use Magento\CloudPatches\Filesystem\JsonConfigReader; use Magento\CloudPatches\Patch\SourceProvider; use Magento\CloudPatches\Patch\SourceProviderException; use PHPUnit\Framework\MockObject\MockObject; @@ -27,11 +28,6 @@ class SourceProviderTest extends TestCase */ private $sourceProvider; - /** - * @var Filesystem|MockObject - */ - private $filesystem; - /** * @var DirectoryList|MockObject */ @@ -47,21 +43,26 @@ class SourceProviderTest extends TestCase */ private $filelist; + /** + * @var \Magento\CloudPatches\Filesystem\JsonConfigReader|\PHPUnit\Framework\MockObject\MockObject + */ + private $jsonConfigReader; + /** * @inheritDoc */ protected function setUp() { - $this->filesystem = $this->createMock(Filesystem::class); $this->filelist = $this->createMock(FileList::class); $this->directoryList = $this->createMock(DirectoryList::class); $this->qualityPackage = $this->createMock(QualityPackage::class); + $this->jsonConfigReader = $this->createMock(JsonConfigReader::class); $this->sourceProvider = new SourceProvider( - $this->filesystem, $this->filelist, $this->directoryList, - $this->qualityPackage + $this->qualityPackage, + $this->jsonConfigReader ); } @@ -72,16 +73,15 @@ public function testGetCloudPatches() { $configPath = '/cloud/patches.json'; $configSource = require __DIR__ . '/Collector/Fixture/cloud_config_valid.php'; - $jsonConfig = json_encode($configSource); $this->filelist->expects($this->once()) ->method('getPatches') ->willReturn($configPath); - $this->filesystem->expects($this->once()) - ->method('get') + $this->jsonConfigReader->expects($this->once()) + ->method('read') ->with($configPath) - ->willReturn($jsonConfig); + ->willReturn($configSource); $this->assertEquals($configSource, $this->sourceProvider->getCloudPatches()); } @@ -93,18 +93,17 @@ public function testGetQualityPatches() { $configPath = '/quality/patches.json'; $configSource = require __DIR__ . '/Collector/Fixture/quality_config_valid.php'; - $jsonConfig = json_encode($configSource); $this->qualityPackage->expects($this->once()) - ->method('getPatchesConfig') + ->method('getSupportPatchesConfigPath') ->willReturn($configPath); - $this->filesystem->expects($this->once()) - ->method('get') + $this->jsonConfigReader->expects($this->once()) + ->method('read') ->with($configPath) - ->willReturn($jsonConfig); + ->willReturn($configSource); - $this->assertEquals($configSource, $this->sourceProvider->getQualityPatches()); + $this->assertEquals($configSource, $this->sourceProvider->getSupportPatches()); } /** @@ -115,13 +114,10 @@ public function testGetQualityPatches() public function testGetQualityPatchesWithNullConfigPath() { $this->qualityPackage->expects($this->once()) - ->method('getPatchesConfig') + ->method('getSupportPatchesConfigPath') ->willReturn(null); - $this->filesystem->expects($this->never()) - ->method('get'); - - $this->assertEquals([], $this->sourceProvider->getQualityPatches()); + $this->assertEquals([], $this->sourceProvider->getSupportPatches()); } /** @@ -148,36 +144,14 @@ public function testGetQualityPatchesFilesystemException() $configPath = '/quality/patches.json'; $this->qualityPackage->expects($this->once()) - ->method('getPatchesConfig') + ->method('getSupportPatchesConfigPath') ->willReturn($configPath); - $this->filesystem->expects($this->once()) - ->method('get') - ->willThrowException(new FileSystemException('')); + $this->jsonConfigReader->expects($this->once()) + ->method('read') + ->willThrowException(new SourceProviderException('')); $this->expectException(SourceProviderException::class); - $this->sourceProvider->getQualityPatches(); - } - - /** - * Tests retrieving Quality patch configuration with json decoding exception. - */ - public function testGetQualityPatchesJsonException() - { - $configPath = '/quality/patches.json'; - - $this->qualityPackage->expects($this->once()) - ->method('getPatchesConfig') - ->willReturn($configPath); - - $this->filesystem->expects($this->once()) - ->method('get') - ->with($configPath) - ->willReturn('Invalid config format'); - - $this->expectException(SourceProviderException::class); - $this->expectExceptionMessageRegExp('/Unable to unserialize patches configuration/'); - - $this->sourceProvider->getQualityPatches(); + $this->sourceProvider->getSupportPatches(); } } From 3410bcc76e130607d9bd34bcd762b39073ee6906 Mon Sep 17 00:00:00 2001 From: Oleksandr Shmyheliuk <oshmyheliuk@magento.com> Date: Tue, 20 Jul 2021 12:53:20 -0500 Subject: [PATCH 089/157] MCLOUD-8053: Release Cloud Tools (#32) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ba9b846a..ab123377 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.10", + "version": "1.0.11", "license": "OSL-3.0", "require": { "php": "^7.0", From 494afc90a6db6a93765b1485da64838ea84c5968 Mon Sep 17 00:00:00 2001 From: Oleksandr Shmyheliuk <shmyheli@adobe.com> Date: Thu, 22 Jul 2021 09:14:54 -0500 Subject: [PATCH 090/157] MCLOUD-8053: Release Cloud Tools --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ab123377..98939307 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.1", "symfony/yaml": "^3.3||^4.0||^5.1", "monolog/monolog": "^1.16", - "magento/quality-patches": "^1.0.7" + "magento/quality-patches": "^1.1.0" }, "require-dev": { "codeception/codeception": "^2.5.3", From 4c9ea7361071dd0c8495745c3e2090d8d355bf1d Mon Sep 17 00:00:00 2001 From: Nathan Smith <nathsmit@adobe.com> Date: Fri, 23 Jul 2021 09:24:11 -0500 Subject: [PATCH 091/157] MC-42674 (#35) --- patches.json | 3 +++ patches/MC-42674__paypal_checkout__2.3.7-p1.patch | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 patches/MC-42674__paypal_checkout__2.3.7-p1.patch diff --git a/patches.json b/patches.json index 6dd47330..26821830 100644 --- a/patches.json +++ b/patches.json @@ -365,6 +365,9 @@ }, "Re-work consumers to terminate as soon as there is nothing left to process": { ">=2.2.0 <2.3.2": "MAGECLOUD-4071__terminate_consumers_if_the_queue_is_empty__2.2.0.patch" + }, + "Fixes issue with PayPal Express where previously placed order price is displayed": { + "2.3.7-p1": "MC-42674__paypal_checkout__2.3.7-p1.patch" } } } diff --git a/patches/MC-42674__paypal_checkout__2.3.7-p1.patch b/patches/MC-42674__paypal_checkout__2.3.7-p1.patch new file mode 100644 index 00000000..747886f9 --- /dev/null +++ b/patches/MC-42674__paypal_checkout__2.3.7-p1.patch @@ -0,0 +1,12 @@ +diff --git a/vendor/magento/module-paypal/Controller/Express/AbstractExpress/PlaceOrder.php b/vendor/magento/module-paypal/Controller/Express/AbstractExpress/PlaceOrder.php +index 055af4162d5f3..3f4fafc110ae2 100644 +--- a/vendor/magento/module-paypal/Controller/Express/AbstractExpress/PlaceOrder.php ++++ b/vendor/magento/module-paypal/Controller/Express/AbstractExpress/PlaceOrder.php +@@ -99,6 +99,7 @@ public function execute() + + // prepare session to success or cancellation page + $this->_getCheckoutSession()->clearHelperData(); ++ $this->_getSession()->unsQuoteId(); + + // "last successful quote" + $quoteId = $this->_getQuote()->getId(); From 3498b3708b84df410f3ccc7f9c77b6aa61ad9ed1 Mon Sep 17 00:00:00 2001 From: Nathan Smith <nathsmit@adobe.com> Date: Wed, 11 Aug 2021 14:19:33 -0500 Subject: [PATCH 092/157] MC-43048 (#38) --- composer.json | 2 +- patches.json | 4 +++ .../MC-43048__set_rate_limits__2.3.7-p1.patch | 28 +++++++++++++++++++ .../MC-43048__set_rate_limits__2.4.3.patch | 28 +++++++++++++++++++ .../Acceptance/Acceptance73Cest.php | 3 +- .../Functional/Acceptance/AcceptanceCest.php | 2 ++ 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 patches/MC-43048__set_rate_limits__2.3.7-p1.patch create mode 100644 patches/MC-43048__set_rate_limits__2.4.3.patch diff --git a/composer.json b/composer.json index 98939307..31dcd8f2 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.11", + "version": "1.0.12", "license": "OSL-3.0", "require": { "php": "^7.0", diff --git a/patches.json b/patches.json index 26821830..f3a65b92 100644 --- a/patches.json +++ b/patches.json @@ -368,6 +368,10 @@ }, "Fixes issue with PayPal Express where previously placed order price is displayed": { "2.3.7-p1": "MC-42674__paypal_checkout__2.3.7-p1.patch" + }, + "Raise rate limits": { + "2.3.7-p1": "MC-43048__set_rate_limits__2.3.7-p1.patch", + "2.4.3": "MC-43048__set_rate_limits__2.4.3.patch" } } } diff --git a/patches/MC-43048__set_rate_limits__2.3.7-p1.patch b/patches/MC-43048__set_rate_limits__2.3.7-p1.patch new file mode 100644 index 00000000..e6777c3f --- /dev/null +++ b/patches/MC-43048__set_rate_limits__2.3.7-p1.patch @@ -0,0 +1,28 @@ +diff --git a/app/etc/di.xml b/app/etc/di.xml +index ace043beb62..7751604ea1d 100644 +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -1896,12 +1896,12 @@ + </type> + <type name="Magento\Framework\Webapi\Validator\EntityArrayValidator"> + <arguments> +- <argument name="complexArrayItemLimit" xsi:type="number">20</argument> ++ <argument name="complexArrayItemLimit" xsi:type="number">999999</argument> + </arguments> + </type> + <type name="Magento\Framework\Webapi\Validator\SearchCriteriaValidator"> + <arguments> +- <argument name="maximumPageSize" xsi:type="number">300</argument> ++ <argument name="maximumPageSize" xsi:type="number">999999</argument> + </arguments> + </type> + <preference for="Magento\Framework\GraphQl\Query\Resolver\Argument\ValidatorInterface" type="Magento\Framework\GraphQl\Query\Resolver\Argument\Validator\CompositeValidator"/> +@@ -1914,7 +1914,7 @@ + </type> + <type name="Magento\Framework\GraphQl\Query\Resolver\Argument\Validator\SearchCriteriaValidator"> + <arguments> +- <argument name="maxPageSize" xsi:type="number">300</argument> ++ <argument name="maxPageSize" xsi:type="number">999999</argument> + </arguments> + </type> + <type name="Magento\Framework\Webapi\ServiceInputProcessor"> diff --git a/patches/MC-43048__set_rate_limits__2.4.3.patch b/patches/MC-43048__set_rate_limits__2.4.3.patch new file mode 100644 index 00000000..45181dde --- /dev/null +++ b/patches/MC-43048__set_rate_limits__2.4.3.patch @@ -0,0 +1,28 @@ +diff --git a/app/etc/di.xml b/app/etc/di.xml +index 078fe71913f..6ab3bdfd35c 100644 +--- a/app/etc/di.xml ++++ b/app/etc/di.xml +@@ -1946,12 +1946,12 @@ + </type> + <type name="Magento\Framework\Webapi\Validator\EntityArrayValidator"> + <arguments> +- <argument name="complexArrayItemLimit" xsi:type="number">20</argument> ++ <argument name="complexArrayItemLimit" xsi:type="number">999999</argument> + </arguments> + </type> + <type name="Magento\Framework\Webapi\Validator\SearchCriteriaValidator"> + <arguments> +- <argument name="maximumPageSize" xsi:type="number">300</argument> ++ <argument name="maximumPageSize" xsi:type="number">999999</argument> + </arguments> + </type> + <preference for="Magento\Framework\GraphQl\Query\Resolver\Argument\ValidatorInterface" type="Magento\Framework\GraphQl\Query\Resolver\Argument\Validator\CompositeValidator"/> +@@ -1964,7 +1964,7 @@ + </type> + <type name="Magento\Framework\GraphQl\Query\Resolver\Argument\Validator\SearchCriteriaValidator"> + <arguments> +- <argument name="maxPageSize" xsi:type="number">300</argument> ++ <argument name="maxPageSize" xsi:type="number">999999</argument> + </arguments> + </type> + <type name="Magento\Framework\Webapi\ServiceInputProcessor"> diff --git a/src/Test/Functional/Acceptance/Acceptance73Cest.php b/src/Test/Functional/Acceptance/Acceptance73Cest.php index 0985aabc..d34533ac 100644 --- a/src/Test/Functional/Acceptance/Acceptance73Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance73Cest.php @@ -27,7 +27,8 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.3.5', 'magentoVersion' => '2.3.5-p2'], ['templateVersion' => '2.3.6', 'magentoVersion' => '2.3.6'], ['templateVersion' => '2.3.6', 'magentoVersion' => '2.3.6-p1'], - ['templateVersion' => '2.3.6', 'magentoVersion' => '2.3.7'], + ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7'], + ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p1'], ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], ]; } diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 9b44cf72..9c614be3 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -55,6 +55,8 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1-p1'], ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2'], ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p1'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p2'], + ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], ['templateVersion' => 'master'], ]; } From 723e28384cdbae72d87af7f752d03b5d4bdd0d08 Mon Sep 17 00:00:00 2001 From: Dmytro Voskoboinikov <voskoboi@adobe.com> Date: Mon, 30 Aug 2021 13:14:48 -0500 Subject: [PATCH 093/157] AC-384: Fix Incompatible PHP Method (#42) --- patches.json | 4 ++++ ...4__Fix_Incompatible_PHP_Method__2.3.7-p1_ce.patch | 12 ++++++++++++ ...-384__Fix_Incompatible_PHP_Method__2.4.3_ce.patch | 12 ++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 patches/AC-384__Fix_Incompatible_PHP_Method__2.3.7-p1_ce.patch create mode 100644 patches/AC-384__Fix_Incompatible_PHP_Method__2.4.3_ce.patch diff --git a/patches.json b/patches.json index f3a65b92..2512fe81 100644 --- a/patches.json +++ b/patches.json @@ -256,6 +256,10 @@ ">=2.3.4 <2.3.5": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch", ">=2.3.5 <2.3.6": "MCLOUD-6211__redis_improvement_patches__2.3.5.patch", ">=2.4.0 <2.4.1": "MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch" + }, + "Incompatible PHP Method Fix": { + "2.3.7-p1": "AC-384__Fix_Incompatible_PHP_Method__2.3.7-p1_ce.patch", + "2.4.3": "AC-384__Fix_Incompatible_PHP_Method__2.4.3_ce.patch" } }, "magento/module-paypal": { diff --git a/patches/AC-384__Fix_Incompatible_PHP_Method__2.3.7-p1_ce.patch b/patches/AC-384__Fix_Incompatible_PHP_Method__2.3.7-p1_ce.patch new file mode 100644 index 00000000..98a92086 --- /dev/null +++ b/patches/AC-384__Fix_Incompatible_PHP_Method__2.3.7-p1_ce.patch @@ -0,0 +1,12 @@ +diff -Nuar a/vendor/magento/framework/Filesystem/Directory/DenyListPathValidator.php b/vendor/magento/framework/Filesystem/Directory/DenyListPathValidator.php +--- a/vendor/magento/framework/Filesystem/Directory/DenyListPathValidator.php (revision d40e8cbfd24ac841900166877e9910f9d549fdf5) ++++ b/vendor/magento/framework/Filesystem/Directory/DenyListPathValidator.php (revision 7d6a3eeb669d4a11978bf639dc2536ef23d56d09) +@@ -71,7 +71,7 @@ + + foreach ($this->fileDenyList as $file) { + $baseName = pathinfo($actualPath, PATHINFO_BASENAME); +- if (str_contains($baseName, $file) || preg_match('#' . "\." . $file . '#', $fullPath)) { ++ if (strpos($baseName, $file) !== false || preg_match('#' . "\." . $file . '#', $fullPath)) { + throw new ValidatorException( + new Phrase('"%1" is not a valid file path', [$path]) + ); diff --git a/patches/AC-384__Fix_Incompatible_PHP_Method__2.4.3_ce.patch b/patches/AC-384__Fix_Incompatible_PHP_Method__2.4.3_ce.patch new file mode 100644 index 00000000..98a92086 --- /dev/null +++ b/patches/AC-384__Fix_Incompatible_PHP_Method__2.4.3_ce.patch @@ -0,0 +1,12 @@ +diff -Nuar a/vendor/magento/framework/Filesystem/Directory/DenyListPathValidator.php b/vendor/magento/framework/Filesystem/Directory/DenyListPathValidator.php +--- a/vendor/magento/framework/Filesystem/Directory/DenyListPathValidator.php (revision d40e8cbfd24ac841900166877e9910f9d549fdf5) ++++ b/vendor/magento/framework/Filesystem/Directory/DenyListPathValidator.php (revision 7d6a3eeb669d4a11978bf639dc2536ef23d56d09) +@@ -71,7 +71,7 @@ + + foreach ($this->fileDenyList as $file) { + $baseName = pathinfo($actualPath, PATHINFO_BASENAME); +- if (str_contains($baseName, $file) || preg_match('#' . "\." . $file . '#', $fullPath)) { ++ if (strpos($baseName, $file) !== false || preg_match('#' . "\." . $file . '#', $fullPath)) { + throw new ValidatorException( + new Phrase('"%1" is not a valid file path', [$path]) + ); From 6f63cb3ed1d3acf47a53b8148d76282227a7cd45 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Tue, 7 Sep 2021 22:12:28 -0500 Subject: [PATCH 094/157] ACP2E-138: PHP error 'Undefined variable: errorMessage' when conflict happens during a patch applying (#43) --- src/Patch/Conflict/Analyzer.php | 1 + src/Test/Unit/Patch/Conflict/AnalyzerTest.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/Patch/Conflict/Analyzer.php b/src/Patch/Conflict/Analyzer.php index 3862823e..97e0f7c9 100644 --- a/src/Patch/Conflict/Analyzer.php +++ b/src/Patch/Conflict/Analyzer.php @@ -88,6 +88,7 @@ public function analyze(PatchInterface $failedPatch, array $patchFilter = []): s */ private function analyzeOptional(string $failedPatchId, array $patchFilter = []): string { + $errorMessage = ''; $optionalPatchIds = $patchFilter ?: $this->optionalPool->getIdsByType(PatchInterface::TYPE_OPTIONAL); $ids = $this->getIncompatiblePatches($optionalPatchIds, $failedPatchId); if ($ids) { diff --git a/src/Test/Unit/Patch/Conflict/AnalyzerTest.php b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php index f518d8f9..d8f0bc06 100644 --- a/src/Test/Unit/Patch/Conflict/AnalyzerTest.php +++ b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php @@ -143,6 +143,14 @@ public function analyzeDataProvider(): array ], 'expectedMessage' => 'Patch MC-1 can\'t be applied to clean Magento instance' ], + [ + 'checkApplyMap' => [ + [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], true], + [['OPTIONAL-1', 'MC-1'], true], + [['OPTIONAL-2', 'MC-1'], true], + ], + 'expectedMessage' => '' + ], ]; } From e525467c494ec05897b3ac43739c40d10505b773 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <posyniak@adobe.com> Date: Tue, 14 Sep 2021 10:10:39 -0500 Subject: [PATCH 095/157] ACMP-1263: Update Monolog (#41) --- composer.json | 14 ++++++++++---- src/App/Logger.php | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 31dcd8f2..6e61edc7 100644 --- a/composer.json +++ b/composer.json @@ -2,27 +2,33 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.12", + "version": "1.0.13", "license": "OSL-3.0", + "repositories": { + "repo.magento.com": { + "type": "composer", + "url": "https://repo.magento.com/" + } + }, "require": { "php": "^7.0", "ext-json": "*", "composer/composer": "@stable", - "composer/semver": "^1.5||^2.0||^3.0", + "composer/semver": "@stable", "symfony/config": "^3.3||^4.4||^5.1", "symfony/console": "^2.6||^4.0||^5.1", "symfony/dependency-injection": "^3.3||^4.3||^5.1", "symfony/process": "^2.1||^4.1||^5.1", "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.1", "symfony/yaml": "^3.3||^4.0||^5.1", - "monolog/monolog": "^1.16", + "monolog/monolog": "^1.25||^2.3", "magento/quality-patches": "^1.1.0" }, "require-dev": { "codeception/codeception": "^2.5.3", "consolidation/robo": "^1.2", "phpmd/phpmd": "@stable", - "phpunit/phpunit": "^6.2", + "phpunit/phpunit": "^7.2", "squizlabs/php_codesniffer": "^3.0" }, "bin": [ diff --git a/src/App/Logger.php b/src/App/Logger.php index af7f9882..05c8f51c 100644 --- a/src/App/Logger.php +++ b/src/App/Logger.php @@ -52,7 +52,7 @@ public function __construct( /** * @inheritDoc */ - public function info($message, array $context = []) + public function info($message, array $context = []): void { $message = strip_tags($message); From 71650c83af6f3338d191e773bfd86a2eabf58fa9 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorbivskyi <gorbivsk@adobe.com> Date: Tue, 9 Nov 2021 07:30:19 -0800 Subject: [PATCH 096/157] MCLOUD-8279: Added MDVA-40924 patch (Fixed currency displaying on product page). (#45) --- patches.json | 3 + ...cy_displaying_on_product_page__2.4.3.patch | 143 ++++++++++++++++++ .../Acceptance/Acceptance73Cest.php | 1 + .../Functional/Acceptance/AcceptanceCest.php | 1 + 4 files changed, 148 insertions(+) create mode 100644 patches/MCLOUD-8279__Fixed_currency_displaying_on_product_page__2.4.3.patch diff --git a/patches.json b/patches.json index 2512fe81..d4606dcf 100644 --- a/patches.json +++ b/patches.json @@ -260,6 +260,9 @@ "Incompatible PHP Method Fix": { "2.3.7-p1": "AC-384__Fix_Incompatible_PHP_Method__2.3.7-p1_ce.patch", "2.4.3": "AC-384__Fix_Incompatible_PHP_Method__2.4.3_ce.patch" + }, + "Fixed currency displaying on product page": { + ">=2.4.3 <2.4.4": "MCLOUD-8279__Fixed_currency_displaying_on_product_page__2.4.3.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-8279__Fixed_currency_displaying_on_product_page__2.4.3.patch b/patches/MCLOUD-8279__Fixed_currency_displaying_on_product_page__2.4.3.patch new file mode 100644 index 00000000..429c6ba9 --- /dev/null +++ b/patches/MCLOUD-8279__Fixed_currency_displaying_on_product_page__2.4.3.patch @@ -0,0 +1,143 @@ +diff -Nuar a/vendor/magento/module-directory/Model/Currency.php b/vendor/magento/module-directory/Model/Currency.php +index 65b47d7535c..e1815b25619 100644 +--- a/vendor/magento/module-directory/Model/Currency.php ++++ b/vendor/magento/module-directory/Model/Currency.php +@@ -13,6 +13,7 @@ use Magento\Framework\Locale\Currency as LocaleCurrency; + use Magento\Framework\Locale\ResolverInterface as LocalResolverInterface; + use Magento\Framework\NumberFormatterFactory; + use Magento\Framework\Serialize\Serializer\Json; ++use Magento\Framework\Exception\LocalizedException; + + /** + * Currency model +@@ -39,8 +40,6 @@ class Currency extends \Magento\Framework\Model\AbstractModel + protected $_filter; + + /** +- * Currency Rates +- * + * @var array + */ + protected $_rates; +@@ -147,11 +146,14 @@ class Currency extends \Magento\Framework\Model\AbstractModel + $this->_localeCurrency = $localeCurrency; + $this->currencyConfig = $currencyConfig ?: ObjectManager::getInstance()->get(CurrencyConfig::class); + $this->localeResolver = $localeResolver ?: ObjectManager::getInstance()->get(LocalResolverInterface::class); +- $this->numberFormatterFactory = $numberFormatterFactory ?: ObjectManager::getInstance()->get(NumberFormatterFactory::class); ++ $this->numberFormatterFactory = $numberFormatterFactory ?: ++ ObjectManager::getInstance()->get(NumberFormatterFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + } + + /** ++ * Initializing Currency Resource model ++ * + * @return void + */ + protected function _construct() +@@ -253,10 +255,10 @@ class Currency extends \Magento\Framework\Model\AbstractModel + /** + * Convert price to currency format + * +- * @param float $price +- * @param mixed $toCurrency +- * @return float +- * @throws \Exception ++ * @param float $price ++ * @param mixed $toCurrency ++ * @return float ++ * @throws LocalizedException + */ + public function convert($price, $toCurrency = null) + { +@@ -266,7 +268,7 @@ class Currency extends \Magento\Framework\Model\AbstractModel + return (float)$price * (float)$rate; + } + +- throw new \Exception(__( ++ throw new LocalizedException(__( + 'Undefined rate from "%1-%2".', + $this->getCode(), + $this->getCurrencyCodeFromToCurrency($toCurrency) +@@ -274,7 +276,10 @@ class Currency extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Return the currency code ++ * + * @param mixed $toCurrency ++ * + * @return string + * @throws \Magento\Framework\Exception\InputException + */ +@@ -348,8 +353,11 @@ class Currency extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Return formatted currency ++ * + * @param float $price + * @param array $options ++ * + * @return string + */ + public function formatTxt($price, $options = []) +@@ -420,7 +428,8 @@ class Currency extends \Magento\Framework\Model\AbstractModel + $this->numberFormatter = $this->getNumberFormatter($options); + + $formattedCurrency = $this->numberFormatter->formatCurrency( +- $price, $this->getCode() ?? $this->numberFormatter->getTextAttribute(\NumberFormatter::CURRENCY_CODE) ++ $price, ++ $this->getCode() ?? $this->numberFormatter->getTextAttribute(\NumberFormatter::CURRENCY_CODE) + ); + + if (array_key_exists(LocaleCurrency::CURRENCY_OPTION_SYMBOL, $options)) { +@@ -430,7 +439,7 @@ class Currency extends \Magento\Framework\Model\AbstractModel + + if ((array_key_exists(LocaleCurrency::CURRENCY_OPTION_DISPLAY, $options) + && $options[LocaleCurrency::CURRENCY_OPTION_DISPLAY] === \Magento\Framework\Currency::NO_SYMBOL)) { +- $formattedCurrency = str_replace(' ', '', $formattedCurrency); ++ $formattedCurrency = preg_replace(['/[^0-9.,۰٫]+/', '/ /'], '', $formattedCurrency); + } + + return preg_replace('/^\s+|\s+$/u', '', $formattedCurrency); +@@ -444,7 +453,10 @@ class Currency extends \Magento\Framework\Model\AbstractModel + */ + private function getNumberFormatter(array $options): \Magento\Framework\NumberFormatter + { +- $key = 'currency_' . md5($this->localeResolver->getLocale() . $this->serializer->serialize($options)); ++ $key = 'currency_' . hash( ++ 'sha256', ++ ($this->localeResolver->getLocale() . $this->serializer->serialize($options)) ++ ); + if (!isset($this->numberFormatterCache[$key])) { + $this->numberFormatter = $this->numberFormatterFactory->create( + ['locale' => $this->localeResolver->getLocale(), 'style' => \NumberFormatter::CURRENCY] +@@ -467,7 +479,8 @@ class Currency extends \Magento\Framework\Model\AbstractModel + { + if (array_key_exists(LocaleCurrency::CURRENCY_OPTION_SYMBOL, $options)) { + $this->numberFormatter->setSymbol( +- \NumberFormatter::CURRENCY_SYMBOL, $options[LocaleCurrency::CURRENCY_OPTION_SYMBOL] ++ \NumberFormatter::CURRENCY_SYMBOL, ++ $options[LocaleCurrency::CURRENCY_OPTION_SYMBOL] + ); + } + if (array_key_exists(LocaleCurrency::CURRENCY_OPTION_DISPLAY, $options) +@@ -490,6 +503,8 @@ class Currency extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Return the price format to be displayed to user ++ * + * @return string + */ + public function getOutputFormat() +@@ -532,6 +547,8 @@ class Currency extends \Magento\Framework\Model\AbstractModel + } + + /** ++ * Retrieve base config currency data by config path. ++ * + * @return array + */ + public function getConfigBaseCurrencies() diff --git a/src/Test/Functional/Acceptance/Acceptance73Cest.php b/src/Test/Functional/Acceptance/Acceptance73Cest.php index d34533ac..f52d7cda 100644 --- a/src/Test/Functional/Acceptance/Acceptance73Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance73Cest.php @@ -29,6 +29,7 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.3.6', 'magentoVersion' => '2.3.6-p1'], ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7'], ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p1'], + ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p2'], ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], ]; } diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 9c614be3..50938081 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -57,6 +57,7 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p1'], ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p2'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], + ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], ['templateVersion' => 'master'], ]; } From 8e004ee66ced8f531f74a06b06be2198afa2bcd4 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Fri, 12 Nov 2021 14:21:46 -0600 Subject: [PATCH 097/157] ACDOG-53: Add repo metadata (#47) --- .github/.metadata.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/.metadata.json diff --git a/.github/.metadata.json b/.github/.metadata.json new file mode 100644 index 00000000..d0377faf --- /dev/null +++ b/.github/.metadata.json @@ -0,0 +1,26 @@ +{ + "templateVersion": "0.1", + "product": { + "name": "Magento Cloud Patches", + "description": "The Magento Cloud Patches package is a set of patches, previously used within the ece-tools package to improve the integration of all Magento versions with Cloud environments and to deliver critical fixes quickly" + }, + "contacts": { + "team": { + "name": "Magic Mountain", + "DL": "Grp-magento-cloud-all", + "slackChannel": "magic_mountain" + } + }, + "ticketTracker": { + "functionalJiraQueue": { + "projectKey": "MCLOUD" + }, + "securityJiraQueue": { + "projectKey": "MAGREQ", + "component": "Magento Cloud Engineering" + } + }, + "staticScan": { + "enable": false + } +} From 5a4d952ea726082c098644e97623d4a0997e6bd3 Mon Sep 17 00:00:00 2001 From: Nathan Smith <nathsmit@adobe.com> Date: Sun, 13 Feb 2022 12:31:26 -0600 Subject: [PATCH 098/157] AC-2391 (#48) --- composer.json | 2 +- patches.json | 3 ++ ...VA-43395__parser_token_fix__2.4.3-p2.patch | 33 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch diff --git a/composer.json b/composer.json index 6e61edc7..63d196d8 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.13", + "version": "1.0.14", "license": "OSL-3.0", "repositories": { "repo.magento.com": { diff --git a/patches.json b/patches.json index 2512fe81..5ce7e21b 100644 --- a/patches.json +++ b/patches.json @@ -376,6 +376,9 @@ "Raise rate limits": { "2.3.7-p1": "MC-43048__set_rate_limits__2.3.7-p1.patch", "2.4.3": "MC-43048__set_rate_limits__2.4.3.patch" + }, + "Parser token fix": { + ">=2.3.3-p1 <=2.4.4": "MDVA-43395__parser_token_fix__2.4.3-p2.patch" } } } diff --git a/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch b/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch new file mode 100644 index 00000000..f1850439 --- /dev/null +++ b/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch @@ -0,0 +1,33 @@ +diff -Nuar a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php +index 1a7c3683820a..586cb485ee1f 100644 +--- a/vendor/magento/module-email/Model/Template/Filter.php ++++ b/vendor/magento/module-email/Model/Template/Filter.php +@@ -618,6 +618,12 @@ public function transDirective($construction) + } + + $text = __($text, $params)->render(); ++ ++ $pattern = '/{{.*?}}/'; ++ do { ++ $text = preg_replace($pattern, '', (string)$text); ++ } while (preg_match($pattern, $text)); ++ + return $this->applyModifiers($text, $modifiers); + } + +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +index f2fe398c3848..78034d70ba51 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +@@ -55,6 +55,11 @@ public function process(array $construction, Template $filter, array $templateVa + $result = $this->filterApplier->applyFromRawParam($construction['filters'], $result); + } + ++ $pattern = '/{{.*?}}/'; ++ do { ++ $result = preg_replace($pattern, '', (string)$result); ++ } while (preg_match($pattern, $result)); ++ + return $result; + } + From 8a3b49a239d7656398d363e2c0d729020d249c69 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Sun, 13 Feb 2022 20:47:42 -0600 Subject: [PATCH 099/157] AC-2391 (#48) (#49) Co-authored-by: Nathan Smith <nathsmit@adobe.com> --- composer.json | 2 +- patches.json | 3 ++ ...VA-43395__parser_token_fix__2.4.3-p2.patch | 33 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch diff --git a/composer.json b/composer.json index 6e61edc7..63d196d8 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.13", + "version": "1.0.14", "license": "OSL-3.0", "repositories": { "repo.magento.com": { diff --git a/patches.json b/patches.json index d4606dcf..f308f3e5 100644 --- a/patches.json +++ b/patches.json @@ -379,6 +379,9 @@ "Raise rate limits": { "2.3.7-p1": "MC-43048__set_rate_limits__2.3.7-p1.patch", "2.4.3": "MC-43048__set_rate_limits__2.4.3.patch" + }, + "Parser token fix": { + ">=2.3.3-p1 <=2.4.4": "MDVA-43395__parser_token_fix__2.4.3-p2.patch" } } } diff --git a/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch b/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch new file mode 100644 index 00000000..f1850439 --- /dev/null +++ b/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch @@ -0,0 +1,33 @@ +diff -Nuar a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php +index 1a7c3683820a..586cb485ee1f 100644 +--- a/vendor/magento/module-email/Model/Template/Filter.php ++++ b/vendor/magento/module-email/Model/Template/Filter.php +@@ -618,6 +618,12 @@ public function transDirective($construction) + } + + $text = __($text, $params)->render(); ++ ++ $pattern = '/{{.*?}}/'; ++ do { ++ $text = preg_replace($pattern, '', (string)$text); ++ } while (preg_match($pattern, $text)); ++ + return $this->applyModifiers($text, $modifiers); + } + +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +index f2fe398c3848..78034d70ba51 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +@@ -55,6 +55,11 @@ public function process(array $construction, Template $filter, array $templateVa + $result = $this->filterApplier->applyFromRawParam($construction['filters'], $result); + } + ++ $pattern = '/{{.*?}}/'; ++ do { ++ $result = preg_replace($pattern, '', (string)$result); ++ } while (preg_match($pattern, $result)); ++ + return $result; + } + From e77421d5374742bf099de4a5af6593e4575ae36e Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 22 Feb 2022 23:59:21 -0600 Subject: [PATCH 100/157] ACMP-1459: Make ece-tools compatible with php 8.0/8.1 (#46) --- composer.json | 12 ++++-- .../Functional/Acceptance/AbstractCest.php | 28 +++++++------- .../Acceptance/Acceptance74CeCest.php | 38 +++++++++++++++++++ .../Acceptance/Acceptance74Cest.php | 32 ++++++++++++++++ .../Acceptance/AcceptanceCeCest.php | 2 +- .../Functional/Acceptance/AcceptanceCest.php | 13 +------ .../Acceptance/PatchApplierCest.php | 16 ++++---- src/Test/Unit/Command/ApplyTest.php | 2 +- src/Test/Unit/Command/Ece/ApplyTest.php | 2 +- src/Test/Unit/Command/Ece/RevertTest.php | 2 +- .../Action/ApplyOptionalActionTest.php | 2 +- .../Action/ConfirmRequiredActionTest.php | 2 +- .../Action/ProcessDeprecatedActionTest.php | 2 +- .../Process/Action/RevertActionTest.php | 2 +- .../Action/ReviewAppliedActionTest.php | 2 +- .../Unit/Command/Process/ApplyLocalTest.php | 2 +- .../Command/Process/ApplyOptionalTest.php | 2 +- .../Command/Process/ApplyRequiredTest.php | 2 +- .../Command/Process/Ece/ApplyOptionalTest.php | 2 +- .../Unit/Command/Process/Ece/RevertTest.php | 2 +- .../Unit/Command/Process/RendererTest.php | 2 +- src/Test/Unit/Command/Process/RevertTest.php | 2 +- .../Unit/Command/Process/ShowStatusTest.php | 2 +- src/Test/Unit/Command/RevertTest.php | 2 +- src/Test/Unit/Command/StatusTest.php | 2 +- src/Test/Unit/Composer/MagentoVersionTest.php | 2 +- .../Unit/Environment/ConfigReaderTest.php | 2 +- src/Test/Unit/Environment/ConfigTest.php | 2 +- .../Unit/Filesystem/DirectoryListTest.php | 2 +- src/Test/Unit/Filesystem/FileListTest.php | 2 +- .../Unit/Patch/AggregatedPatchFactoryTest.php | 2 +- src/Test/Unit/Patch/AggregatorTest.php | 2 +- src/Test/Unit/Patch/ApplierTest.php | 2 +- .../Patch/Collector/CloudCollectorTest.php | 2 +- .../Patch/Collector/LocalCollectorTest.php | 2 +- .../Patch/Collector/QualityCollectorTest.php | 2 +- src/Test/Unit/Patch/Conflict/AnalyzerTest.php | 2 +- .../Unit/Patch/Conflict/ApplyCheckerTest.php | 2 +- .../Unit/Patch/Conflict/ProcessorTest.php | 2 +- src/Test/Unit/Patch/FilterFactoryTest.php | 2 +- src/Test/Unit/Patch/GitConverterTest.php | 2 +- src/Test/Unit/Patch/PatchBuilderTest.php | 2 +- src/Test/Unit/Patch/Pool/OptionalPoolTest.php | 2 +- src/Test/Unit/Patch/RevertValidatorTest.php | 2 +- src/Test/Unit/Patch/RollbackProcessorTest.php | 2 +- src/Test/Unit/Patch/SourceProviderTest.php | 2 +- .../Unit/Patch/Status/LocalResolverTest.php | 2 +- .../Patch/Status/OptionalResolverTest.php | 2 +- .../Unit/Shell/Command/PatchDriverTest.php | 4 +- 49 files changed, 145 insertions(+), 82 deletions(-) create mode 100644 src/Test/Functional/Acceptance/Acceptance74CeCest.php create mode 100644 src/Test/Functional/Acceptance/Acceptance74Cest.php diff --git a/composer.json b/composer.json index 63d196d8..92c2ff75 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.14", + "version": "1.0.15", "license": "OSL-3.0", "repositories": { "repo.magento.com": { @@ -11,7 +11,7 @@ } }, "require": { - "php": "^7.0", + "php": "^7.2 || ^8.0", "ext-json": "*", "composer/composer": "@stable", "composer/semver": "@stable", @@ -25,10 +25,14 @@ "magento/quality-patches": "^1.1.0" }, "require-dev": { - "codeception/codeception": "^2.5.3", + "codeception/codeception": "^4.1", + "codeception/module-asserts": "^1.2", + "codeception/module-db": "^1.0", + "codeception/module-phpbrowser": "^1.0", + "codeception/module-rest": "^1.2", "consolidation/robo": "^1.2", "phpmd/phpmd": "@stable", - "phpunit/phpunit": "^7.2", + "phpunit/phpunit": "^8.5 || ^9.5", "squizlabs/php_codesniffer": "^3.0" }, "bin": [ diff --git a/src/Test/Functional/Acceptance/AbstractCest.php b/src/Test/Functional/Acceptance/AbstractCest.php index e893f878..d3374bbc 100644 --- a/src/Test/Functional/Acceptance/AbstractCest.php +++ b/src/Test/Functional/Acceptance/AbstractCest.php @@ -37,28 +37,26 @@ protected function prepareTemplate(\CliTester $I, string $templateVersion, strin $I->createArtifactsDir(); $I->createArtifactCurrentTestedCode('patches', '1.0.99'); $I->addArtifactsRepoToComposer(); + $I->addDependencyToComposer('magento/magento-cloud-patches', '1.0.99'); + $I->addEceDockerGitRepoToComposer(); $I->addQualityPatchesGitRepoToComposer(); $I->addEceToolsGitRepoToComposer(); $I->addCloudComponentsGitRepoToComposer(); - $I->addDependencyToComposer('magento/magento-cloud-patches', '1.0.99'); - $I->addDependencyToComposer( - 'magento/magento-cloud-docker', - $I->getDependencyVersion('magento/magento-cloud-docker') - ); - $I->addDependencyToComposer( - 'magento/quality-patches', - $I->getDependencyVersion('magento/quality-patches') - ); - $I->addDependencyToComposer( + $dependencies = [ + 'magento/magento-cloud-docker', 'magento/magento-cloud-components', - $I->getDependencyVersion('magento/magento-cloud-components') - ); - $I->addDependencyToComposer( + 'magento/quality-patches', 'magento/ece-tools', - $I->getDependencyVersion('magento/ece-tools') - ); + ]; + + foreach ($dependencies as $dependency) { + $I->assertTrue( + $I->addDependencyToComposer($dependency, $I->getDependencyVersion($dependency)), + 'Can not add dependency ' . $dependency + ); + } if ($this->edition === 'CE' || $magentoVersion) { $version = $magentoVersion ?: $this->getVersionRangeForMagento($I); diff --git a/src/Test/Functional/Acceptance/Acceptance74CeCest.php b/src/Test/Functional/Acceptance/Acceptance74CeCest.php new file mode 100644 index 00000000..78efdd66 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance74CeCest.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php74ce + */ +class Acceptance74CeCest extends Acceptance74Cest +{ + /** + * @var string + */ + protected $edition = 'CE'; + + /** + * @return array + */ + protected function patchesDataProvider(): array + { + return [ + ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], + ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0-p1'], + ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1'], + ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1-p1'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p1'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p2'], + // Magento 2.4.3 CE does not work on the cloud + // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], + // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/Acceptance74Cest.php b/src/Test/Functional/Acceptance/Acceptance74Cest.php new file mode 100644 index 00000000..39830b2c --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance74Cest.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php74 + */ +class Acceptance74Cest extends AcceptanceCest +{ + /** + * @return array + */ + protected function patchesDataProvider(): array + { + return [ + ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], + ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0-p1'], + ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1'], + ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1-p1'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p1'], + ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p2'], + ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], + ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/AcceptanceCeCest.php b/src/Test/Functional/Acceptance/AcceptanceCeCest.php index dd234d47..c3d98f22 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCeCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCeCest.php @@ -8,7 +8,7 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php74ce + * @group php81ce */ class AcceptanceCeCest extends AcceptanceCest { diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 50938081..5728a525 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -8,7 +8,7 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php74 + * @group php81 */ class AcceptanceCest extends AbstractCest { @@ -49,16 +49,7 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void protected function patchesDataProvider(): array { return [ - ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], - ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0-p1'], - ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1'], - ['templateVersion' => '2.4.1', 'magentoVersion' => '2.4.1-p1'], - ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2'], - ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p1'], - ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p2'], - ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], - ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], - ['templateVersion' => 'master'], + ['templateVersion' => '2.4.4'], ]; } } diff --git a/src/Test/Functional/Acceptance/PatchApplierCest.php b/src/Test/Functional/Acceptance/PatchApplierCest.php index d3c7b686..ae3129c6 100644 --- a/src/Test/Functional/Acceptance/PatchApplierCest.php +++ b/src/Test/Functional/Acceptance/PatchApplierCest.php @@ -10,7 +10,7 @@ use Magento\CloudDocker\Test\Functional\Codeception\Docker; /** - * @group php74 + * @group php81 */ class PatchApplierCest extends AbstractCest { @@ -21,7 +21,7 @@ public function _before(\CliTester $I): void { parent::_before($I); - $this->prepareTemplate($I, 'master'); + $this->prepareTemplate($I, '2.4.4'); $I->copyFileToWorkDir('files/debug_logging/.magento.env.yaml', '.magento.env.yaml'); } @@ -40,10 +40,10 @@ public function testApplyingPatch(\CliTester $I): void $I->startEnvironment(); $targetFile = $I->grabFileContent('/target_file.md', Docker::BUILD_CONTAINER); - $I->assertContains('# Hello Magento', $targetFile); - $I->assertContains('## Additional Info', $targetFile); + $I->assertStringContainsString('# Hello Magento', $targetFile); + $I->assertStringContainsString('## Additional Info', $targetFile); $log = $I->grabFileContent('/var/log/cloud.log', Docker::BUILD_CONTAINER); - $I->assertContains('Patch ../m2-hotfixes/patch.patch has been applied', $log); + $I->assertStringContainsString('Patch ../m2-hotfixes/patch.patch has been applied', $log); } /** @@ -61,9 +61,9 @@ public function testApplyingExistingPatch(\CliTester $I): void $I->startEnvironment(); $targetFile = $I->grabFileContent('/target_file.md', Docker::BUILD_CONTAINER); - $I->assertContains('# Hello Magento', $targetFile); - $I->assertContains('## Additional Info', $targetFile); - $I->assertContains( + $I->assertStringContainsString('# Hello Magento', $targetFile); + $I->assertStringContainsString('## Additional Info', $targetFile); + $I->assertStringContainsString( 'Patch ../m2-hotfixes/patch.patch was already applied', $I->grabFileContent('/var/log/cloud.log', Docker::BUILD_CONTAINER) ); diff --git a/src/Test/Unit/Command/ApplyTest.php b/src/Test/Unit/Command/ApplyTest.php index b88db0dc..58e8f42c 100644 --- a/src/Test/Unit/Command/ApplyTest.php +++ b/src/Test/Unit/Command/ApplyTest.php @@ -46,7 +46,7 @@ class ApplyTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->applyOptional = $this->createMock(ApplyOptional::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Command/Ece/ApplyTest.php b/src/Test/Unit/Command/Ece/ApplyTest.php index 8b9906c0..eafba6ed 100644 --- a/src/Test/Unit/Command/Ece/ApplyTest.php +++ b/src/Test/Unit/Command/Ece/ApplyTest.php @@ -58,7 +58,7 @@ class ApplyTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->applyLocal = $this->createMock(ApplyLocal::class); $this->applyOptionalEce = $this->createMock(ApplyOptional::class); diff --git a/src/Test/Unit/Command/Ece/RevertTest.php b/src/Test/Unit/Command/Ece/RevertTest.php index 55f9075a..573ab13b 100644 --- a/src/Test/Unit/Command/Ece/RevertTest.php +++ b/src/Test/Unit/Command/Ece/RevertTest.php @@ -41,7 +41,7 @@ class RevertTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->revertEce = $this->createMock(RevertProcess::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php index c543a2de..eb0c62ff 100644 --- a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php @@ -65,7 +65,7 @@ class ApplyOptionalActionTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->applier = $this->createMock(Applier::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php b/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php index 1424fafb..2d374ca9 100644 --- a/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php @@ -54,7 +54,7 @@ class ConfirmRequiredActionTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->statusPool = $this->createMock(StatusPool::class); $this->optionalPool = $this->createMock(OptionalPool::class); diff --git a/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php b/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php index 50dd537c..441a0722 100644 --- a/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php @@ -65,7 +65,7 @@ class ProcessDeprecatedActionTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->optionalPool = $this->createMock(OptionalPool::class); $this->statusPool = $this->createMock(StatusPool::class); diff --git a/src/Test/Unit/Command/Process/Action/RevertActionTest.php b/src/Test/Unit/Command/Process/Action/RevertActionTest.php index 9ef6b34c..b8f515e0 100644 --- a/src/Test/Unit/Command/Process/Action/RevertActionTest.php +++ b/src/Test/Unit/Command/Process/Action/RevertActionTest.php @@ -60,7 +60,7 @@ class RevertActionTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->applier = $this->createMock(Applier::class); $this->revertValidator = $this->createMock(RevertValidator::class); diff --git a/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php b/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php index c5224b3b..b793c373 100644 --- a/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php @@ -52,7 +52,7 @@ class ReviewAppliedActionTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); $this->statusPool = $this->createMock(StatusPool::class); diff --git a/src/Test/Unit/Command/Process/ApplyLocalTest.php b/src/Test/Unit/Command/Process/ApplyLocalTest.php index b9f7e85c..d2212697 100644 --- a/src/Test/Unit/Command/Process/ApplyLocalTest.php +++ b/src/Test/Unit/Command/Process/ApplyLocalTest.php @@ -59,7 +59,7 @@ class ApplyLocalTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->applier = $this->createMock(Applier::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Command/Process/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/ApplyOptionalTest.php index a32b68c9..0b947307 100644 --- a/src/Test/Unit/Command/Process/ApplyOptionalTest.php +++ b/src/Test/Unit/Command/Process/ApplyOptionalTest.php @@ -46,7 +46,7 @@ class ApplyOptionalTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->filterFactory = $this->createMock(FilterFactory::class); $this->actionPool = $this->createMock(ActionPool::class); diff --git a/src/Test/Unit/Command/Process/ApplyRequiredTest.php b/src/Test/Unit/Command/Process/ApplyRequiredTest.php index 9675f5c2..45b794ac 100644 --- a/src/Test/Unit/Command/Process/ApplyRequiredTest.php +++ b/src/Test/Unit/Command/Process/ApplyRequiredTest.php @@ -59,7 +59,7 @@ class ApplyRequiredTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->applier = $this->createMock(Applier::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php index f269a989..4f547b8d 100644 --- a/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php +++ b/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php @@ -51,7 +51,7 @@ class ApplyOptionalTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->filterFactory = $this->createMock(FilterFactory::class); $this->actionPool = $this->createMock(ActionPool::class); diff --git a/src/Test/Unit/Command/Process/Ece/RevertTest.php b/src/Test/Unit/Command/Process/Ece/RevertTest.php index 1197ba2b..cbba07ac 100644 --- a/src/Test/Unit/Command/Process/Ece/RevertTest.php +++ b/src/Test/Unit/Command/Process/Ece/RevertTest.php @@ -65,7 +65,7 @@ class RevertTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->revertAction = $this->createMock(RevertAction::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Command/Process/RendererTest.php b/src/Test/Unit/Command/Process/RendererTest.php index 6a944b4c..b7b1f66d 100644 --- a/src/Test/Unit/Command/Process/RendererTest.php +++ b/src/Test/Unit/Command/Process/RendererTest.php @@ -43,7 +43,7 @@ class RendererTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { /** @var StatusPool|MockObject $statusPool */ $statusPool = $this->createMock(StatusPool::class); diff --git a/src/Test/Unit/Command/Process/RevertTest.php b/src/Test/Unit/Command/Process/RevertTest.php index 37bb7a55..d2d00a4d 100644 --- a/src/Test/Unit/Command/Process/RevertTest.php +++ b/src/Test/Unit/Command/Process/RevertTest.php @@ -46,7 +46,7 @@ class RevertTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->filterFactory = $this->createMock(FilterFactory::class); $this->revertAction = $this->createMock(RevertAction::class); diff --git a/src/Test/Unit/Command/Process/ShowStatusTest.php b/src/Test/Unit/Command/Process/ShowStatusTest.php index 15fa3f2e..bafd697f 100644 --- a/src/Test/Unit/Command/Process/ShowStatusTest.php +++ b/src/Test/Unit/Command/Process/ShowStatusTest.php @@ -78,7 +78,7 @@ class ShowStatusTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->aggregator = $this->createMock(Aggregator::class); $this->optionalPool = $this->createMock(OptionalPool::class); diff --git a/src/Test/Unit/Command/RevertTest.php b/src/Test/Unit/Command/RevertTest.php index f732ea8c..6494abf7 100644 --- a/src/Test/Unit/Command/RevertTest.php +++ b/src/Test/Unit/Command/RevertTest.php @@ -41,7 +41,7 @@ class RevertTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->revert = $this->createMock(RevertProcess::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Command/StatusTest.php b/src/Test/Unit/Command/StatusTest.php index 31094acd..7cd7eec3 100644 --- a/src/Test/Unit/Command/StatusTest.php +++ b/src/Test/Unit/Command/StatusTest.php @@ -42,7 +42,7 @@ class StatusTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->showStatus = $this->createMock(ShowStatus::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Composer/MagentoVersionTest.php b/src/Test/Unit/Composer/MagentoVersionTest.php index 615b679d..1481b7de 100644 --- a/src/Test/Unit/Composer/MagentoVersionTest.php +++ b/src/Test/Unit/Composer/MagentoVersionTest.php @@ -41,7 +41,7 @@ class MagentoVersionTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->repository = $this->getMockForAbstractClass(WritableRepositoryInterface::class); $this->rootPackage = $this->getMockForAbstractClass(RootPackageInterface::class); diff --git a/src/Test/Unit/Environment/ConfigReaderTest.php b/src/Test/Unit/Environment/ConfigReaderTest.php index ddd0f5ad..04ffcbf6 100644 --- a/src/Test/Unit/Environment/ConfigReaderTest.php +++ b/src/Test/Unit/Environment/ConfigReaderTest.php @@ -37,7 +37,7 @@ class ConfigReaderTest extends TestCase /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { $this->fileList = $this->createMock(FileList::class); $this->filesystem = $this->createPartialMock(Filesystem::class, ['exists']); diff --git a/src/Test/Unit/Environment/ConfigTest.php b/src/Test/Unit/Environment/ConfigTest.php index 79d33297..fdc8455b 100644 --- a/src/Test/Unit/Environment/ConfigTest.php +++ b/src/Test/Unit/Environment/ConfigTest.php @@ -30,7 +30,7 @@ class ConfigTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->configReader = $this->createMock(ConfigReader::class); diff --git a/src/Test/Unit/Filesystem/DirectoryListTest.php b/src/Test/Unit/Filesystem/DirectoryListTest.php index c2ee383e..7df929f7 100644 --- a/src/Test/Unit/Filesystem/DirectoryListTest.php +++ b/src/Test/Unit/Filesystem/DirectoryListTest.php @@ -33,7 +33,7 @@ class DirectoryListTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->directoryList = new DirectoryList( $this->root, diff --git a/src/Test/Unit/Filesystem/FileListTest.php b/src/Test/Unit/Filesystem/FileListTest.php index f0f019a7..b8db3040 100644 --- a/src/Test/Unit/Filesystem/FileListTest.php +++ b/src/Test/Unit/Filesystem/FileListTest.php @@ -29,7 +29,7 @@ class FileListTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->directoryListMock = $this->createMock(DirectoryList::class); diff --git a/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php b/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php index fa1b8567..d509f6cd 100644 --- a/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php +++ b/src/Test/Unit/Patch/AggregatedPatchFactoryTest.php @@ -26,7 +26,7 @@ class AggregatedPatchFactoryTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->aggregatedPatchFactory = new AggregatedPatchFactory(); } diff --git a/src/Test/Unit/Patch/AggregatorTest.php b/src/Test/Unit/Patch/AggregatorTest.php index 5427dca2..b438c516 100644 --- a/src/Test/Unit/Patch/AggregatorTest.php +++ b/src/Test/Unit/Patch/AggregatorTest.php @@ -31,7 +31,7 @@ class AggregatorTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->aggregatedPatchFactory = $this->createMock(AggregatedPatchFactory::class); $this->aggregator = new Aggregator($this->aggregatedPatchFactory); diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php index 89452b6c..fa3330bb 100644 --- a/src/Test/Unit/Patch/ApplierTest.php +++ b/src/Test/Unit/Patch/ApplierTest.php @@ -51,7 +51,7 @@ class ApplierTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->patchCommand = $this->createMock(PatchCommandInterface::class); $this->gitConverter = $this->createMock(GitConverter::class); diff --git a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php index b83df61d..b0a3ff2e 100644 --- a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php @@ -61,7 +61,7 @@ class CloudCollectorTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->sourceProvider = $this->createMock(SourceProvider::class); $this->package = $this->createMock(Package::class); diff --git a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php index 972436dd..7787d081 100644 --- a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php @@ -38,7 +38,7 @@ class LocalCollectorTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->patchBuilder = $this->createMock(PatchBuilder::class); $this->sourceProvider = $this->createMock(SourceProvider::class); diff --git a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php index 19f6d67a..9cf82dc7 100644 --- a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php @@ -59,7 +59,7 @@ class QualityCollectorTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->package = $this->createMock(Package::class); $this->qualityPackage = $this->createMock(QualityPackage::class); diff --git a/src/Test/Unit/Patch/Conflict/AnalyzerTest.php b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php index d8f0bc06..b3c00ff7 100644 --- a/src/Test/Unit/Patch/Conflict/AnalyzerTest.php +++ b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php @@ -49,7 +49,7 @@ class AnalyzerTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->optionalPool = $this->createMock(OptionalPool::class); $this->config = $this->createMock(Config::class); diff --git a/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php b/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php index f59d651d..fcbecaed 100644 --- a/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php +++ b/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php @@ -43,7 +43,7 @@ class ApplyCheckerTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->optionalPool = $this->createMock(OptionalPool::class); $this->filesystem = $this->createMock(Filesystem::class); diff --git a/src/Test/Unit/Patch/Conflict/ProcessorTest.php b/src/Test/Unit/Patch/Conflict/ProcessorTest.php index c37631f4..20dacd06 100644 --- a/src/Test/Unit/Patch/Conflict/ProcessorTest.php +++ b/src/Test/Unit/Patch/Conflict/ProcessorTest.php @@ -51,7 +51,7 @@ class ProcessorTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->renderer = $this->createMock(Renderer::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Patch/FilterFactoryTest.php b/src/Test/Unit/Patch/FilterFactoryTest.php index db9f2bd8..dbe63fe7 100644 --- a/src/Test/Unit/Patch/FilterFactoryTest.php +++ b/src/Test/Unit/Patch/FilterFactoryTest.php @@ -23,7 +23,7 @@ class FilterFactoryTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->filterFactory = new FilterFactory(); } diff --git a/src/Test/Unit/Patch/GitConverterTest.php b/src/Test/Unit/Patch/GitConverterTest.php index a250edf1..bbeeda58 100644 --- a/src/Test/Unit/Patch/GitConverterTest.php +++ b/src/Test/Unit/Patch/GitConverterTest.php @@ -23,7 +23,7 @@ class GitConverterTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->gitConverter = new GitConverter(); } diff --git a/src/Test/Unit/Patch/PatchBuilderTest.php b/src/Test/Unit/Patch/PatchBuilderTest.php index de7abbef..908f88dd 100644 --- a/src/Test/Unit/Patch/PatchBuilderTest.php +++ b/src/Test/Unit/Patch/PatchBuilderTest.php @@ -35,7 +35,7 @@ class PatchBuilderTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->filesystem = $this->createMock(Filesystem::class); diff --git a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php index 61370979..9796426a 100644 --- a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php +++ b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php @@ -37,7 +37,7 @@ class OptionalPoolTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->cloudCollector = $this->createMock(CloudCollector::class); $this->qualityCollector = $this->createMock(SupportCollector::class); diff --git a/src/Test/Unit/Patch/RevertValidatorTest.php b/src/Test/Unit/Patch/RevertValidatorTest.php index f39dc7fb..7c2e6eca 100644 --- a/src/Test/Unit/Patch/RevertValidatorTest.php +++ b/src/Test/Unit/Patch/RevertValidatorTest.php @@ -37,7 +37,7 @@ class RevertValidatorTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->optionalPool = $this->createMock(OptionalPool::class); $this->statusPool = $this->createMock(StatusPool::class); diff --git a/src/Test/Unit/Patch/RollbackProcessorTest.php b/src/Test/Unit/Patch/RollbackProcessorTest.php index 60a8cfde..7b185b47 100644 --- a/src/Test/Unit/Patch/RollbackProcessorTest.php +++ b/src/Test/Unit/Patch/RollbackProcessorTest.php @@ -37,7 +37,7 @@ class RollbackProcessorTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->applier = $this->createMock(Applier::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); diff --git a/src/Test/Unit/Patch/SourceProviderTest.php b/src/Test/Unit/Patch/SourceProviderTest.php index 15f065a8..f4de6d12 100644 --- a/src/Test/Unit/Patch/SourceProviderTest.php +++ b/src/Test/Unit/Patch/SourceProviderTest.php @@ -51,7 +51,7 @@ class SourceProviderTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->filelist = $this->createMock(FileList::class); $this->directoryList = $this->createMock(DirectoryList::class); diff --git a/src/Test/Unit/Patch/Status/LocalResolverTest.php b/src/Test/Unit/Patch/Status/LocalResolverTest.php index cf0c8247..3dabaa71 100644 --- a/src/Test/Unit/Patch/Status/LocalResolverTest.php +++ b/src/Test/Unit/Patch/Status/LocalResolverTest.php @@ -47,7 +47,7 @@ class LocalResolverTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->filesystem = $this->createMock(Filesystem::class); $this->applier = $this->createMock(Applier::class); diff --git a/src/Test/Unit/Patch/Status/OptionalResolverTest.php b/src/Test/Unit/Patch/Status/OptionalResolverTest.php index 9789bd29..a8902b47 100644 --- a/src/Test/Unit/Patch/Status/OptionalResolverTest.php +++ b/src/Test/Unit/Patch/Status/OptionalResolverTest.php @@ -54,7 +54,7 @@ class OptionalResolverTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->filesystem = $this->createMock(Filesystem::class); $this->applier = $this->createMock(Applier::class); diff --git a/src/Test/Unit/Shell/Command/PatchDriverTest.php b/src/Test/Unit/Shell/Command/PatchDriverTest.php index d1462d54..02ac4ae5 100644 --- a/src/Test/Unit/Shell/Command/PatchDriverTest.php +++ b/src/Test/Unit/Shell/Command/PatchDriverTest.php @@ -34,7 +34,7 @@ class PatchDriverTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->baseDir = dirname(__DIR__, 5) . '/tests/unit/'; $this->cwd = $this->baseDir . 'var/'; @@ -58,7 +58,7 @@ function (array $cmd, string $input = null) { /** * @inheritDoc */ - protected function tearDown() + protected function tearDown(): void { foreach (glob($this->cwd . '*') as $file) { if (is_file($file)) { From f8d148819dc817df2a19296947b3d32fa883b98e Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 29 Mar 2022 10:00:42 -0500 Subject: [PATCH 101/157] MCLOUD-8479: Add and release patch MDVA-43443 (#50) --- patches.json | 6 +- ...VA-43395__parser_token_fix__2.4.3-p2.patch | 33 -- ...3443__parser_token_new_fix__2.3.3-p1.patch | 354 +++++++++++++++++ ...3443__parser_token_new_fix__2.3.4-p2.patch | 358 ++++++++++++++++++ ...A-43443__parser_token_new_fix__2.4.3.patch | 357 +++++++++++++++++ .../Acceptance/Acceptance73Cest.php | 1 + .../Acceptance/Acceptance74CeCest.php | 1 + .../Acceptance/Acceptance74Cest.php | 1 + .../Acceptance/AcceptanceCeCest.php | 19 - 9 files changed, 1076 insertions(+), 54 deletions(-) delete mode 100644 patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch create mode 100644 patches/MDVA-43443__parser_token_new_fix__2.3.3-p1.patch create mode 100644 patches/MDVA-43443__parser_token_new_fix__2.3.4-p2.patch create mode 100644 patches/MDVA-43443__parser_token_new_fix__2.4.3.patch delete mode 100644 src/Test/Functional/Acceptance/AcceptanceCeCest.php diff --git a/patches.json b/patches.json index f308f3e5..01a4e711 100644 --- a/patches.json +++ b/patches.json @@ -380,8 +380,10 @@ "2.3.7-p1": "MC-43048__set_rate_limits__2.3.7-p1.patch", "2.4.3": "MC-43048__set_rate_limits__2.4.3.patch" }, - "Parser token fix": { - ">=2.3.3-p1 <=2.4.4": "MDVA-43395__parser_token_fix__2.4.3-p2.patch" + "Parser token new fix": { + ">=2.3.3-p1 <=2.3.4": "MDVA-43443__parser_token_new_fix__2.3.3-p1.patch", + ">=2.3.4-p2 <2.3.7-p4 || >=2.4.0 <2.4.3": "MDVA-43443__parser_token_new_fix__2.3.4-p2.patch", + ">=2.4.3 <2.4.3-p3": "MDVA-43443__parser_token_new_fix__2.4.3.patch" } } } diff --git a/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch b/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch deleted file mode 100644 index f1850439..00000000 --- a/patches/MDVA-43395__parser_token_fix__2.4.3-p2.patch +++ /dev/null @@ -1,33 +0,0 @@ -diff -Nuar a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php -index 1a7c3683820a..586cb485ee1f 100644 ---- a/vendor/magento/module-email/Model/Template/Filter.php -+++ b/vendor/magento/module-email/Model/Template/Filter.php -@@ -618,6 +618,12 @@ public function transDirective($construction) - } - - $text = __($text, $params)->render(); -+ -+ $pattern = '/{{.*?}}/'; -+ do { -+ $text = preg_replace($pattern, '', (string)$text); -+ } while (preg_match($pattern, $text)); -+ - return $this->applyModifiers($text, $modifiers); - } - -diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php -index f2fe398c3848..78034d70ba51 100644 ---- a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php -+++ b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php -@@ -55,6 +55,11 @@ public function process(array $construction, Template $filter, array $templateVa - $result = $this->filterApplier->applyFromRawParam($construction['filters'], $result); - } - -+ $pattern = '/{{.*?}}/'; -+ do { -+ $result = preg_replace($pattern, '', (string)$result); -+ } while (preg_match($pattern, $result)); -+ - return $result; - } - diff --git a/patches/MDVA-43443__parser_token_new_fix__2.3.3-p1.patch b/patches/MDVA-43443__parser_token_new_fix__2.3.3-p1.patch new file mode 100644 index 00000000..27a6468e --- /dev/null +++ b/patches/MDVA-43443__parser_token_new_fix__2.3.3-p1.patch @@ -0,0 +1,354 @@ +diff -Nuar a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php +--- a/vendor/magento/module-email/Model/Template/Filter.php ++++ b/vendor/magento/module-email/Model/Template/Filter.php +@@ -379,14 +379,14 @@ class Filter extends \Magento\Framework\Filter\Template + } + + /** +- * Retrieve Block html directive +- * + * @param array $construction ++ * + * @return string ++ * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ +- public function blockDirective($construction) ++ private function resolveBlockDirective($construction) + { + $skipParams = ['class', 'id', 'output']; + $blockParameters = $this->getParameters($construction[2]); +@@ -427,12 +427,26 @@ class Filter extends \Magento\Framework\Filter\Template + } + + /** +- * Retrieve layout html directive ++ * Retrieve Block html directive + * ++ * @param array $construction ++ * @return string ++ * @SuppressWarnings(PHPMD.CyclomaticComplexity) ++ * @SuppressWarnings(PHPMD.NPathComplexity) ++ */ ++ public function blockDirective($construction) ++ { ++ $result = $this->resolveBlockDirective($construction); ++ ++ return preg_replace("/{{/", "{{", $result); ++ } ++ ++ /** + * @param string[] $construction ++ * + * @return string + */ +- public function layoutDirective($construction) ++ private function resolveLayoutDirective($construction) + { + $this->_directiveParams = $this->getParameters($construction[2]); + if (!isset($this->_directiveParams['area'])) { +@@ -448,6 +462,19 @@ class Filter extends \Magento\Framework\Filter\Template + } + } + ++ /** ++ * Retrieve layout html directive ++ * ++ * @param string[] $construction ++ * @return string ++ */ ++ public function layoutDirective($construction) ++ { ++ $result = $this->resolveLayoutDirective($construction); ++ ++ return preg_replace("/{{/", "{{", $result); ++ } ++ + /** + * Retrieve layout html directive callback + * +@@ -515,7 +542,7 @@ class Filter extends \Magento\Framework\Filter\Template + { + $params = $this->getParameters($construction[2]); + $url = $this->_assetRepo->getUrlWithParams($params['url'], $params); +- return $url; ++ return $this->sanitizeValue($url); + } + + /** +@@ -528,8 +555,11 @@ class Filter extends \Magento\Framework\Filter\Template + { + // phpcs:disable Magento2.Functions.DiscouragedFunction + $params = $this->getParameters(html_entity_decode($construction[2], ENT_QUOTES)); +- return $this->_storeManager->getStore() +- ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url']; ++ return $this->sanitizeValue( ++ $this->_storeManager->getStore() ++ ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url'] ++ ); ++ + } + + /** +@@ -567,7 +597,7 @@ class Filter extends \Magento\Framework\Filter\Template + unset($params['url']); + } + +- return $this->urlModel->getUrl($path, $params); ++ return $this->sanitizeValue($this->urlModel->getUrl($path, $params)); + } + + /** +@@ -605,7 +635,8 @@ class Filter extends \Magento\Framework\Filter\Template + } + + $text = __($text, $params)->render(); +- return $this->applyModifiers($text, $modifiers); ++ ++ return $this->applyModifiers($this->sanitizeValue($text), $modifiers); + } + + /** +@@ -649,7 +680,10 @@ class Filter extends \Magento\Framework\Filter\Template + $construction[2] . ($construction['filters'] ?? ''), + 'escape' + ); +- return $this->applyModifiers($this->getVariable($directive, ''), $modifiers); ++ ++ $result = $this->sanitizeValue($this->getVariable($directive, '')); ++ ++ return $this->applyModifiers($result, $modifiers); + } + + /** +@@ -730,20 +764,11 @@ class Filter extends \Magento\Framework\Filter\Template + } + + /** +- * HTTP Protocol directive +- * +- * Usage: +- * +- * {{protocol}} - current protocol http or https +- * {{protocol url="www.domain.com/"}} - domain URL with current protocol +- * {{protocol http="http://url/" https="https://url/"}} +- * {{protocol store="1"}} - Optional parameter which gets protocol from provide store based on store ID or code +- * + * @param string[] $construction + * @throws \Magento\Framework\Exception\MailException + * @return string + */ +- public function protocolDirective($construction) ++ private function resolveProtocolDirective($construction) + { + $params = $this->getParameters($construction[2]); + $store = null; +@@ -770,6 +795,27 @@ class Filter extends \Magento\Framework\Filter\Template + return $protocol; + } + ++ /** ++ * HTTP Protocol directive ++ * ++ * Usage: ++ * ++ * {{protocol}} - current protocol http or https ++ * {{protocol url="www.domain.com/"}} - domain URL with current protocol ++ * {{protocol http="http://url/" https="https://url/"}} ++ * {{protocol store="1"}} - Optional parameter which gets protocol from provide store based on store ID or code ++ * ++ * @param string[] $construction ++ * @throws \Magento\Framework\Exception\MailException ++ * @return string ++ */ ++ public function protocolDirective($construction) ++ { ++ return $this->sanitizeValue( ++ $this->resolveProtocolDirective($construction) ++ ); ++ } ++ + /** + * Store config directive + * +@@ -788,7 +834,7 @@ class Filter extends \Magento\Framework\Filter\Template + $storeId + ); + } +- return $configValue; ++ return $this->sanitizeValue($configValue); + } + + /** +@@ -829,7 +875,8 @@ class Filter extends \Magento\Framework\Filter\Template + $customVarValue = $value; + } + } +- return $customVarValue; ++ ++ return $this->sanitizeValue($customVarValue); + } + + /** +@@ -1057,4 +1104,14 @@ class Filter extends \Magento\Framework\Filter\Template + } + return $value; + } ++ ++ /** ++ * @param string $value ++ * ++ * @return string|bool ++ */ ++ private function sanitizeValue($value) ++ { ++ return is_bool($value) ? $value : str_replace(['{', '}'], '', (string) $value); ++ } + } +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php +index f557f7465b5..83345acd6e5 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php +@@ -32,9 +32,13 @@ class DependDirective implements DirectiveProcessorInterface + } + + /** +- * @inheritdoc ++ * @param array $construction ++ * @param Template $filter ++ * @param array $templateVariables ++ * ++ * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (empty($templateVariables)) { + // If template processing +@@ -48,6 +52,16 @@ class DependDirective implements DirectiveProcessorInterface + } + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * @inheritdoc + */ +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php +index 2b51185b1b5..41cd58118fd 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php +@@ -36,14 +36,13 @@ class ForDirective implements DirectiveProcessorInterface + } + + /** +- * Filter the string as template. +- * + * @param array $construction + * @param Template $filter + * @param array $templateVariables ++ * + * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (!$this->isValidLoop($construction)) { + return $construction[0]; +@@ -67,6 +66,16 @@ class ForDirective implements DirectiveProcessorInterface + return $construction[0]; + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * Check if the matched construction is valid. + * +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php +index 7fedc7946f2..469dae71d06 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php +@@ -32,9 +32,13 @@ class IfDirective implements DirectiveProcessorInterface + } + + /** +- * @inheritdoc ++ * @param array $construction ++ * @param Template $filter ++ * @param array $templateVariables ++ * ++ * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (empty($templateVariables)) { + return $construction[0]; +@@ -50,6 +54,16 @@ class IfDirective implements DirectiveProcessorInterface + } + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * @inheritdoc + */ +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php +index 9f4b30d0c96..b9280aec283 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php +@@ -68,7 +68,7 @@ class SimpleDirective implements DirectiveProcessorInterface + ->get($construction['directiveName']); + } catch (\InvalidArgumentException $e) { + // This directive doesn't have a SimpleProcessor +- return $construction[0]; ++ return ''; + } + + $parameters = $this->extractParameters($construction, $filter, $templateVariables); +@@ -79,6 +79,8 @@ class SimpleDirective implements DirectiveProcessorInterface + !empty($construction['content']) ? $filter->filter($construction['content']) : null + ); + ++ $value = str_replace(['{', '}'], '', (string) $value); ++ + $value = $this->filterApplier->applyFromRawParam( + $construction['filters'] ?? '', + $value, +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +--- a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +@@ -55,6 +55,8 @@ class VarDirective implements DirectiveProcessorInterface + $result = $this->filterApplier->applyFromRawParam($construction['filters'], $result); + } + ++ $result = str_replace(['{', '}'], '', (string) $result); ++ + return $result; + } + diff --git a/patches/MDVA-43443__parser_token_new_fix__2.3.4-p2.patch b/patches/MDVA-43443__parser_token_new_fix__2.3.4-p2.patch new file mode 100644 index 00000000..d8175a03 --- /dev/null +++ b/patches/MDVA-43443__parser_token_new_fix__2.3.4-p2.patch @@ -0,0 +1,358 @@ +diff -Nuar a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php +--- a/vendor/magento/module-email/Model/Template/Filter.php ++++ b/vendor/magento/module-email/Model/Template/Filter.php +@@ -380,14 +380,14 @@ class Filter extends \Magento\Framework\Filter\Template + } + + /** +- * Retrieve Block html directive +- * + * @param array $construction ++ * + * @return string ++ * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ +- public function blockDirective($construction) ++ private function resolveBlockDirective($construction) + { + $skipParams = ['class', 'id', 'output']; + $blockParameters = $this->getParameters($construction[2]); +@@ -428,12 +428,26 @@ class Filter extends \Magento\Framework\Filter\Template + } + + /** +- * Retrieve layout html directive ++ * Retrieve Block html directive + * ++ * @param array $construction ++ * @return string ++ * @SuppressWarnings(PHPMD.CyclomaticComplexity) ++ * @SuppressWarnings(PHPMD.NPathComplexity) ++ */ ++ public function blockDirective($construction) ++ { ++ $result = $this->resolveBlockDirective($construction); ++ ++ return preg_replace("/{{/", "{{", $result); ++ } ++ ++ /** + * @param string[] $construction ++ * + * @return string + */ +- public function layoutDirective($construction) ++ private function resolveLayoutDirective($construction) + { + $this->_directiveParams = $this->getParameters($construction[2]); + if (!isset($this->_directiveParams['area'])) { +@@ -449,6 +463,19 @@ class Filter extends \Magento\Framework\Filter\Template + } + } + ++ /** ++ * Retrieve layout html directive ++ * ++ * @param string[] $construction ++ * @return string ++ */ ++ public function layoutDirective($construction) ++ { ++ $result = $this->resolveLayoutDirective($construction); ++ ++ return preg_replace("/{{/", "{{", $result); ++ } ++ + /** + * Retrieve layout html directive callback + * +@@ -516,7 +543,7 @@ class Filter extends \Magento\Framework\Filter\Template + { + $params = $this->getParameters($construction[2]); + $url = $this->_assetRepo->getUrlWithParams($params['url'], $params); +- return $url; ++ return $this->sanitizeValue($url); + } + + /** +@@ -529,8 +556,11 @@ class Filter extends \Magento\Framework\Filter\Template + { + // phpcs:disable Magento2.Functions.DiscouragedFunction + $params = $this->getParameters(html_entity_decode($construction[2], ENT_QUOTES)); +- return $this->_storeManager->getStore() +- ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url']; ++ return $this->sanitizeValue( ++ $this->_storeManager->getStore() ++ ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url'] ++ ); ++ + } + + /** +@@ -568,7 +598,7 @@ class Filter extends \Magento\Framework\Filter\Template + unset($params['url']); + } + +- return $this->urlModel->getUrl($path, $params); ++ return $this->sanitizeValue($this->urlModel->getUrl($path, $params)); + } + + /** +@@ -606,7 +636,8 @@ class Filter extends \Magento\Framework\Filter\Template + } + + $text = __($text, $params)->render(); +- return $this->applyModifiers($text, $modifiers); ++ ++ return $this->applyModifiers($this->sanitizeValue($text), $modifiers); + } + + /** +@@ -650,7 +681,10 @@ class Filter extends \Magento\Framework\Filter\Template + $construction[2] . ($construction['filters'] ?? ''), + 'escape' + ); +- return $this->applyModifiers($this->getVariable($directive, ''), $modifiers); ++ ++ $result = $this->sanitizeValue($this->getVariable($directive, '')); ++ ++ return $this->applyModifiers($result, $modifiers); + } + + /** +@@ -731,21 +765,14 @@ class Filter extends \Magento\Framework\Filter\Template + } + + /** +- * HTTP Protocol directive +- * +- * Usage: +- * +- * {{protocol}} - current protocol http or https +- * {{protocol url="www.domain.com/"}} - domain URL with current protocol +- * {{protocol http="http://url/" https="https://url/"}} +- * {{protocol store="1"}} - Optional parameter which gets protocol from provide store based on store ID or code +- * + * @param string[] $construction ++ * + * @return string ++ * + * @throws MailException + * @throws NoSuchEntityException + */ +- public function protocolDirective($construction) ++ private function resolveProtocolDirective($construction) + { + $params = $this->getParameters($construction[2]); + +@@ -776,6 +803,28 @@ class Filter extends \Magento\Framework\Filter\Template + return $protocol; + } + ++ /** ++ * HTTP Protocol directive ++ * ++ * Usage: ++ * ++ * {{protocol}} - current protocol http or https ++ * {{protocol url="www.domain.com/"}} - domain URL with current protocol ++ * {{protocol http="http://url/" https="https://url/"}} ++ * {{protocol store="1"}} - Optional parameter which gets protocol from provide store based on store ID or code ++ * ++ * @param string[] $construction ++ * @return string ++ * @throws MailException ++ * @throws NoSuchEntityException ++ */ ++ public function protocolDirective($construction) ++ { ++ return $this->sanitizeValue( ++ $this->resolveProtocolDirective($construction) ++ ); ++ } ++ + /** + * Validate protocol directive HTTP parameters. + * +@@ -825,7 +874,7 @@ class Filter extends \Magento\Framework\Filter\Template + $storeId + ); + } +- return $configValue; ++ return $this->sanitizeValue($configValue); + } + + /** +@@ -866,7 +915,8 @@ class Filter extends \Magento\Framework\Filter\Template + $customVarValue = $value; + } + } +- return $customVarValue; ++ ++ return $this->sanitizeValue($customVarValue); + } + + /** +@@ -1093,4 +1143,14 @@ class Filter extends \Magento\Framework\Filter\Template + } + return $value; + } ++ ++ /** ++ * @param string $value ++ * ++ * @return string|bool ++ */ ++ private function sanitizeValue($value) ++ { ++ return is_bool($value) ? $value : str_replace(['{', '}'], '', (string) $value); ++ } + } +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php +index f557f7465b5..83345acd6e5 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php +@@ -32,9 +32,13 @@ class DependDirective implements DirectiveProcessorInterface + } + + /** +- * @inheritdoc ++ * @param array $construction ++ * @param Template $filter ++ * @param array $templateVariables ++ * ++ * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (empty($templateVariables)) { + // If template processing +@@ -48,6 +52,16 @@ class DependDirective implements DirectiveProcessorInterface + } + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * @inheritdoc + */ +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php +index 2b51185b1b5..41cd58118fd 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php +@@ -36,14 +36,13 @@ class ForDirective implements DirectiveProcessorInterface + } + + /** +- * Filter the string as template. +- * + * @param array $construction + * @param Template $filter + * @param array $templateVariables ++ * + * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (!$this->isValidLoop($construction)) { + return $construction[0]; +@@ -67,6 +66,16 @@ class ForDirective implements DirectiveProcessorInterface + return $construction[0]; + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * Check if the matched construction is valid. + * +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php +index 7fedc7946f2..469dae71d06 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php +@@ -32,9 +32,13 @@ class IfDirective implements DirectiveProcessorInterface + } + + /** +- * @inheritdoc ++ * @param array $construction ++ * @param Template $filter ++ * @param array $templateVariables ++ * ++ * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (empty($templateVariables)) { + return $construction[0]; +@@ -50,6 +54,16 @@ class IfDirective implements DirectiveProcessorInterface + } + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * @inheritdoc + */ +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php +index 9f4b30d0c96..b9280aec283 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php +@@ -68,7 +68,7 @@ class SimpleDirective implements DirectiveProcessorInterface + ->get($construction['directiveName']); + } catch (\InvalidArgumentException $e) { + // This directive doesn't have a SimpleProcessor +- return $construction[0]; ++ return ''; + } + + $parameters = $this->extractParameters($construction, $filter, $templateVariables); +@@ -79,6 +79,8 @@ class SimpleDirective implements DirectiveProcessorInterface + !empty($construction['content']) ? $filter->filter($construction['content']) : null + ); + ++ $value = str_replace(['{', '}'], '', (string) $value); ++ + $value = $this->filterApplier->applyFromRawParam( + $construction['filters'] ?? '', + $value, +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +--- a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +@@ -55,6 +55,8 @@ class VarDirective implements DirectiveProcessorInterface + $result = $this->filterApplier->applyFromRawParam($construction['filters'], $result); + } + ++ $result = str_replace(['{', '}'], '', (string) $result); ++ + return $result; + } + diff --git a/patches/MDVA-43443__parser_token_new_fix__2.4.3.patch b/patches/MDVA-43443__parser_token_new_fix__2.4.3.patch new file mode 100644 index 00000000..5708fda3 --- /dev/null +++ b/patches/MDVA-43443__parser_token_new_fix__2.4.3.patch @@ -0,0 +1,357 @@ +diff -Nuar a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php +--- a/vendor/magento/module-email/Model/Template/Filter.php ++++ b/vendor/magento/module-email/Model/Template/Filter.php +@@ -392,14 +392,14 @@ class Filter extends Template + } + + /** +- * Retrieve Block html directive +- * + * @param array $construction ++ * + * @return string ++ * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ +- public function blockDirective($construction) ++ private function resolveBlockDirective($construction) + { + $skipParams = ['class', 'id', 'output']; + $blockParameters = $this->getParameters($construction[2]); +@@ -440,12 +440,26 @@ class Filter extends Template + } + + /** +- * Retrieve layout html directive ++ * Retrieve Block html directive + * ++ * @param array $construction ++ * @return string ++ * @SuppressWarnings(PHPMD.CyclomaticComplexity) ++ * @SuppressWarnings(PHPMD.NPathComplexity) ++ */ ++ public function blockDirective($construction) ++ { ++ $result = $this->resolveBlockDirective($construction); ++ ++ return preg_replace("/{{/", "{{", $result); ++ } ++ ++ /** + * @param string[] $construction ++ * + * @return string + */ +- public function layoutDirective($construction) ++ private function resolveLayoutDirective($construction) + { + $this->_directiveParams = $this->getParameters($construction[2]); + if (!isset($this->_directiveParams['area'])) { +@@ -461,6 +475,19 @@ class Filter extends Template + } + } + ++ /** ++ * Retrieve layout html directive ++ * ++ * @param string[] $construction ++ * @return string ++ */ ++ public function layoutDirective($construction) ++ { ++ $result = $this->resolveLayoutDirective($construction); ++ ++ return preg_replace("/{{/", "{{", $result); ++ } ++ + /** + * Retrieve layout html directive callback + * +@@ -528,7 +555,7 @@ class Filter extends Template + { + $params = $this->getParameters($construction[2]); + $url = $this->_assetRepo->getUrlWithParams($params['url'], $params); +- return $url; ++ return $this->sanitizeValue($url); + } + + /** +@@ -541,8 +568,10 @@ class Filter extends Template + { + // phpcs:disable Magento2.Functions.DiscouragedFunction + $params = $this->getParameters(html_entity_decode($construction[2], ENT_QUOTES)); +- return $this->_storeManager->getStore() +- ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $params['url']; ++ return $this->sanitizeValue( ++ $this->_storeManager->getStore() ++ ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $params['url'] ++ ); + } + + /** +@@ -580,7 +609,7 @@ class Filter extends Template + unset($params['url']); + } + +- return $this->urlModel->getUrl($path, $params); ++ return $this->sanitizeValue($this->urlModel->getUrl($path, $params)); + } + + /** +@@ -618,7 +647,8 @@ class Filter extends Template + } + + $text = __($text, $params)->render(); +- return $this->applyModifiers($text, $modifiers); ++ ++ return $this->applyModifiers($this->sanitizeValue($text), $modifiers); + } + + /** +@@ -662,7 +692,10 @@ class Filter extends Template + $construction[2] . ($construction['filters'] ?? ''), + 'escape' + ); +- return $this->applyModifiers($this->getVariable($directive, ''), $modifiers); ++ ++ $result = $this->sanitizeValue($this->getVariable($directive, '')); ++ ++ return $this->applyModifiers($result, $modifiers); + } + + /** +@@ -743,21 +776,14 @@ class Filter extends Template + } + + /** +- * HTTP Protocol directive +- * +- * Usage: +- * +- * {{protocol}} - current protocol http or https +- * {{protocol url="www.domain.com/"}} - domain URL with current protocol +- * {{protocol http="http://url/" https="https://url/"}} +- * {{protocol store="1"}} - Optional parameter which gets protocol from provide store based on store ID or code +- * + * @param string[] $construction ++ * + * @return string ++ * + * @throws MailException + * @throws NoSuchEntityException + */ +- public function protocolDirective($construction) ++ private function resolveProtocolDirective($construction) + { + $params = $this->getParameters($construction[2]); + +@@ -788,6 +814,28 @@ class Filter extends Template + return $protocol; + } + ++ /** ++ * HTTP Protocol directive ++ * ++ * Usage: ++ * ++ * {{protocol}} - current protocol http or https ++ * {{protocol url="www.domain.com/"}} - domain URL with current protocol ++ * {{protocol http="http://url/" https="https://url/"}} ++ * {{protocol store="1"}} - Optional parameter which gets protocol from provide store based on store ID or code ++ * ++ * @param string[] $construction ++ * @return string ++ * @throws MailException ++ * @throws NoSuchEntityException ++ */ ++ public function protocolDirective($construction) ++ { ++ return $this->sanitizeValue( ++ $this->resolveProtocolDirective($construction) ++ ); ++ } ++ + /** + * Validate protocol directive HTTP parameters. + * +@@ -837,7 +885,7 @@ class Filter extends Template + $storeId + ); + } +- return $configValue; ++ return $this->sanitizeValue($configValue); + } + + /** +@@ -878,7 +926,8 @@ class Filter extends Template + $customVarValue = $value; + } + } +- return $customVarValue; ++ ++ return $this->sanitizeValue($customVarValue); + } + + /** +@@ -1107,4 +1156,14 @@ class Filter extends Template + } + return $value; + } ++ ++ /** ++ * @param string $value ++ * ++ * @return string|bool ++ */ ++ private function sanitizeValue($value) ++ { ++ return is_bool($value) ? $value : str_replace(['{', '}'], '', (string) $value); ++ } + } +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php +index f557f7465b5f..83345acd6e5b 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/DependDirective.php +@@ -32,9 +32,13 @@ public function __construct( + } + + /** +- * @inheritdoc ++ * @param array $construction ++ * @param Template $filter ++ * @param array $templateVariables ++ * ++ * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (empty($templateVariables)) { + // If template processing +@@ -48,6 +52,16 @@ public function process(array $construction, Template $filter, array $templateVa + } + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * @inheritdoc + */ +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php +index 2b51185b1b5f..41cd58118fd6 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/ForDirective.php +@@ -36,14 +36,13 @@ public function __construct( + } + + /** +- * Filter the string as template. +- * + * @param array $construction + * @param Template $filter + * @param array $templateVariables ++ * + * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (!$this->isValidLoop($construction)) { + return $construction[0]; +@@ -67,6 +66,16 @@ public function process(array $construction, Template $filter, array $templateVa + return $construction[0]; + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * Check if the matched construction is valid. + * +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php +index 7fedc7946f21..469dae71d068 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/IfDirective.php +@@ -32,9 +32,13 @@ public function __construct( + } + + /** +- * @inheritdoc ++ * @param array $construction ++ * @param Template $filter ++ * @param array $templateVariables ++ * ++ * @return string + */ +- public function process(array $construction, Template $filter, array $templateVariables): string ++ private function resolve(array $construction, Template $filter, array $templateVariables): string + { + if (empty($templateVariables)) { + return $construction[0]; +@@ -50,6 +54,16 @@ public function process(array $construction, Template $filter, array $templateVa + } + } + ++ /** ++ * @inheritdoc ++ */ ++ public function process(array $construction, Template $filter, array $templateVariables): string ++ { ++ $result = $this->resolve($construction, $filter, $templateVariables); ++ ++ return str_replace(['{', '}'], '', (string) $result); ++ } ++ + /** + * @inheritdoc + */ +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php +index 9f4b30d0c96c..b9280aec2833 100644 +--- a/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/SimpleDirective.php +@@ -68,7 +68,7 @@ public function process(array $construction, Template $filter, array $templateVa + ->get($construction['directiveName']); + } catch (\InvalidArgumentException $e) { + // This directive doesn't have a SimpleProcessor +- return $construction[0]; ++ return ''; + } + + $parameters = $this->extractParameters($construction, $filter, $templateVariables); +@@ -79,6 +79,8 @@ public function process(array $construction, Template $filter, array $templateVa + !empty($construction['content']) ? $filter->filter($construction['content']) : null + ); + ++ $value = str_replace(['{', '}'], '', (string) $value); ++ + $value = $this->filterApplier->applyFromRawParam( + $construction['filters'] ?? '', + $value, +diff -Nuar a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +--- a/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php ++++ b/vendor/magento/framework/Filter/DirectiveProcessor/VarDirective.php +@@ -55,6 +55,8 @@ class VarDirective implements DirectiveProcessorInterface + $result = $this->filterApplier->applyFromRawParam($construction['filters'], $result); + } + ++ $result = str_replace(['{', '}'], '', (string) $result); ++ + return $result; + } + diff --git a/src/Test/Functional/Acceptance/Acceptance73Cest.php b/src/Test/Functional/Acceptance/Acceptance73Cest.php index f52d7cda..7072aab5 100644 --- a/src/Test/Functional/Acceptance/Acceptance73Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance73Cest.php @@ -30,6 +30,7 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7'], ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p1'], ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p2'], + ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p3'], ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], ]; } diff --git a/src/Test/Functional/Acceptance/Acceptance74CeCest.php b/src/Test/Functional/Acceptance/Acceptance74CeCest.php index 78efdd66..7abe839d 100644 --- a/src/Test/Functional/Acceptance/Acceptance74CeCest.php +++ b/src/Test/Functional/Acceptance/Acceptance74CeCest.php @@ -33,6 +33,7 @@ protected function patchesDataProvider(): array // Magento 2.4.3 CE does not work on the cloud // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], + // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p2'], ]; } } diff --git a/src/Test/Functional/Acceptance/Acceptance74Cest.php b/src/Test/Functional/Acceptance/Acceptance74Cest.php index 39830b2c..41b0791e 100644 --- a/src/Test/Functional/Acceptance/Acceptance74Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance74Cest.php @@ -27,6 +27,7 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p2'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], + ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p2'], ]; } } diff --git a/src/Test/Functional/Acceptance/AcceptanceCeCest.php b/src/Test/Functional/Acceptance/AcceptanceCeCest.php deleted file mode 100644 index c3d98f22..00000000 --- a/src/Test/Functional/Acceptance/AcceptanceCeCest.php +++ /dev/null @@ -1,19 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CloudPatches\Test\Functional\Acceptance; - -/** - * @group php81ce - */ -class AcceptanceCeCest extends AcceptanceCest -{ - /** - * @var string - */ - protected $edition = 'CE'; -} From e0de76f5a9630fe592d18e7e1972d0b9f5ee388f Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 29 Mar 2022 11:33:37 -0500 Subject: [PATCH 102/157] MCLOUD-8574: Release Cloud Tools --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 92c2ff75..47b37516 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.15", + "version": "1.0.16", "license": "OSL-3.0", "repositories": { "repo.magento.com": { From 69e8b44d6d7779b238553db17e3224846ca5b5e7 Mon Sep 17 00:00:00 2001 From: Leon Helmus <l.helmus@youweagency.com> Date: Mon, 25 Apr 2022 13:39:21 +0200 Subject: [PATCH 103/157] Restrict patch from being applied to version 2.4.3-p2. Since it's been resolved in the patch it self. For more information see: * https://github.com/magento/magento2/issues/35341 --- patches.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches.json b/patches.json index 01a4e711..c5eec8d1 100644 --- a/patches.json +++ b/patches.json @@ -383,7 +383,7 @@ "Parser token new fix": { ">=2.3.3-p1 <=2.3.4": "MDVA-43443__parser_token_new_fix__2.3.3-p1.patch", ">=2.3.4-p2 <2.3.7-p4 || >=2.4.0 <2.4.3": "MDVA-43443__parser_token_new_fix__2.3.4-p2.patch", - ">=2.4.3 <2.4.3-p3": "MDVA-43443__parser_token_new_fix__2.4.3.patch" + ">=2.4.3 <2.4.3-p2": "MDVA-43443__parser_token_new_fix__2.4.3.patch" } } } From b66884b27d75fc39374adb082e8a9b11f0ebd2c8 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Fri, 29 Apr 2022 14:48:57 -0500 Subject: [PATCH 104/157] MCLOUD-8720: Process the MCP PR #85 --- composer.json | 2 +- patches.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 47b37516..036df6b0 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.16", + "version": "1.0.17", "license": "OSL-3.0", "repositories": { "repo.magento.com": { diff --git a/patches.json b/patches.json index c5eec8d1..1086acc1 100644 --- a/patches.json +++ b/patches.json @@ -382,7 +382,7 @@ }, "Parser token new fix": { ">=2.3.3-p1 <=2.3.4": "MDVA-43443__parser_token_new_fix__2.3.3-p1.patch", - ">=2.3.4-p2 <2.3.7-p4 || >=2.4.0 <2.4.3": "MDVA-43443__parser_token_new_fix__2.3.4-p2.patch", + ">=2.3.4-p2 <2.3.7-p3 || >=2.4.0 <2.4.3": "MDVA-43443__parser_token_new_fix__2.3.4-p2.patch", ">=2.4.3 <2.4.3-p2": "MDVA-43443__parser_token_new_fix__2.4.3.patch" } } From a82b08a3022d3a655898850e532c7fc33f1bd6f9 Mon Sep 17 00:00:00 2001 From: Peter Jaap Blaakmeer <peterjaap@elgentos.nl> Date: Fri, 13 May 2022 16:39:45 +0200 Subject: [PATCH 105/157] Add --format=json option to status command --- .../Process/Action/ReviewAppliedAction.php | 2 +- src/Command/Process/Renderer.php | 35 +++++++++++++++++++ src/Command/Process/ShowStatus.php | 15 ++++++-- src/Command/Status.php | 9 +++-- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/Command/Process/Action/ReviewAppliedAction.php b/src/Command/Process/Action/ReviewAppliedAction.php index 19769346..afcd3211 100644 --- a/src/Command/Process/Action/ReviewAppliedAction.php +++ b/src/Command/Process/Action/ReviewAppliedAction.php @@ -73,7 +73,7 @@ function ($patch) { $appliedOptionalPatches ); $totalNumber = count(array_unique(array_merge($ids, $patchFilter))); - if ($totalNumber >= self::UPGRADE_THRESHOLD) { + if ($totalNumber >= self::UPGRADE_THRESHOLD && $input->getOption('format') !== 'json') { $message = 'Warning for those on a previous minor line! Magento recommends installing a limited'. ' number of quality patches to ensure a smooth upgrade to the latest line. Please begin planning'. ' an upgrade to the latest release line.'; diff --git a/src/Command/Process/Renderer.php b/src/Command/Process/Renderer.php index 84627db0..3ba10558 100644 --- a/src/Command/Process/Renderer.php +++ b/src/Command/Process/Renderer.php @@ -105,6 +105,30 @@ public function printTable(OutputInterface $output, array $patchList) $table->render(); } + /** + * Renders patches list as JSON. + * + * @param OutputInterface $output + * @param AggregatedPatchInterface[] $patchList + * @return void + */ + public function printJson(OutputInterface $output, array $patchList) + { + $rows = []; + foreach ($patchList as $patch) { + $rows[] = $this->createJsonRow($patch); + } + + usort($rows, function ($a, $b) { + if ($a[self::STATUS] === $b[self::STATUS]) { + return strcmp($a[self::ORIGIN], $b[self::ORIGIN]); + } + return strcmp($a[self::STATUS], $b[self::STATUS]); + }); + + $output->writeln(json_encode($rows, JSON_PRETTY_PRINT)); + } + /** * Print patch info. * @@ -209,6 +233,17 @@ function ($item) { ]; } + /** + * Creates JSON row. + * + * @param AggregatedPatchInterface $patch + * @return array + */ + private function createJsonRow(AggregatedPatchInterface $patch): array + { + return array_map('strip_tags', $this->createRow($patch)); + } + /** * Adds table separator. * diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 5d2d040a..55cfdd93 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -7,6 +7,7 @@ namespace Magento\CloudPatches\Command\Process; +use Laminas\View\Renderer\JsonRenderer; use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; use Magento\CloudPatches\Console\QuestionFactory; use Magento\CloudPatches\Patch\Data\AggregatedPatch; @@ -18,6 +19,7 @@ use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\HttpFoundation\JsonResponse; /** * Show information about available patches and their statuses. @@ -103,15 +105,18 @@ public function __construct( */ public function run(InputInterface $input, OutputInterface $output) { - $this->printDetailsInfo($output); + if ($input->getOption('format') !== 'json') { + $this->printDetailsInfo($output); + } $this->reviewAppliedAction->execute($input, $output, []); $patches = $this->aggregator->aggregate( array_merge($this->optionalPool->getList(), $this->localPool->getList()) ); + foreach ($patches as $patch) { - if ($patch->isDeprecated() && $this->isPatchVisible($patch)) { + if ($patch->isDeprecated() && $this->isPatchVisible($patch) && $input->getOption('format') !== 'json') { $this->printDeprecatedWarning($output, $patch); } } @@ -124,7 +129,11 @@ public function run(InputInterface $input, OutputInterface $output) $patches = $this->filterByPatchCategory($input, $output, $patches); } - $this->renderer->printTable($output, array_values($patches)); + if ($input->getOption('format') === 'json') { + $this->renderer->printJson($output, array_values($patches)); + } else { + $this->renderer->printTable($output, array_values($patches)); + } } /** diff --git a/src/Command/Status.php b/src/Command/Status.php index 857b2975..12ad9dbe 100644 --- a/src/Command/Status.php +++ b/src/Command/Status.php @@ -11,7 +11,9 @@ use Magento\CloudPatches\Command\Process\ShowStatus; use Magento\CloudPatches\Composer\MagentoVersion; use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -59,7 +61,8 @@ public function __construct( protected function configure() { $this->setName(self::NAME) - ->setDescription('Shows the list of available patches and their statuses'); + ->setDescription('Shows the list of available patches and their statuses') + ->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'Output format', 'table'); parent::configure(); } @@ -71,7 +74,9 @@ public function execute(InputInterface $input, OutputInterface $output) { try { $this->showStatus->run($input, $output); - $output->writeln('<info>' . $this->magentoVersion->get() . '</info>'); + if ($input->getOption('format') !== 'json') { + $output->writeln('<info>' . $this->magentoVersion->get() . '</info>'); + } } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); $this->logger->error($e->getMessage()); From ec8244c088b4d67ef407fa1ef8babd8399a1abd9 Mon Sep 17 00:00:00 2001 From: Peter Jaap Blaakmeer <peterjaap@elgentos.nl> Date: Fri, 13 May 2022 16:41:46 +0200 Subject: [PATCH 106/157] Removed unused classes --- src/Command/Process/ShowStatus.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 55cfdd93..3cd1b6b3 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -7,7 +7,6 @@ namespace Magento\CloudPatches\Command\Process; -use Laminas\View\Renderer\JsonRenderer; use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; use Magento\CloudPatches\Console\QuestionFactory; use Magento\CloudPatches\Patch\Data\AggregatedPatch; @@ -19,7 +18,6 @@ use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\HttpFoundation\JsonResponse; /** * Show information about available patches and their statuses. From a7728877c3120c67a5b950cf268c61a49a04b131 Mon Sep 17 00:00:00 2001 From: Michael Casey <12436335+michaelcasey316@users.noreply.github.com> Date: Wed, 10 Aug 2022 12:05:10 -0500 Subject: [PATCH 107/157] MCLOUD-9137: Add patch BUNDLE-3137 --- composer.json | 2 +- patches.json | 3 +++ ...7__braintree_create_order_fix__2.4.5.patch | 20 +++++++++++++++++++ .../Acceptance/Acceptance74CeCest.php | 3 +++ .../Acceptance/Acceptance74Cest.php | 3 +++ 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100755 patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch diff --git a/composer.json b/composer.json index 036df6b0..adc9e9b9 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.17", + "version": "1.0.18", "license": "OSL-3.0", "repositories": { "repo.magento.com": { diff --git a/patches.json b/patches.json index 1086acc1..55230801 100644 --- a/patches.json +++ b/patches.json @@ -345,6 +345,9 @@ "paypal/module-braintree-core": { "Fix Braintree Settlement report error": { "4.1.0": "BUNDLE-2683__braintree_settlement_report_fix__4.1.0.patch" + }, + "Fix creating orders when Braintree is enabled": { + "4.4.0": "BUNDLE-3137__braintree_create_order_fix__2.4.5.patch" } }, "magento/magento2-b2b-base": { diff --git a/patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch b/patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch new file mode 100755 index 00000000..dfbad797 --- /dev/null +++ b/patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch @@ -0,0 +1,20 @@ +diff --git a/vendor/paypal/module-braintree-core/Block/Form.php b/vendor/paypal/module-braintree-core/Block/Form.php +index 4bc1bdf1..70559bbe 100755 +--- a/vendor/paypal/module-braintree-core/Block/Form.php ++++ b/vendor/paypal/module-braintree-core/Block/Form.php +@@ -66,6 +66,7 @@ public function __construct( + GatewayConfig $gatewayConfig, + CcType $ccType, + LoggerInterface $logger, ++ Data $paymentDataHelper, + array $data = [] + ) { + parent::__construct($context, $paymentConfig, $data); +@@ -74,6 +75,7 @@ public function __construct( + $this->gatewayConfig = $gatewayConfig; + $this->ccType = $ccType; + $this->logger = $logger; ++ $this->paymentDataHelper = $paymentDataHelper; + } + + /** diff --git a/src/Test/Functional/Acceptance/Acceptance74CeCest.php b/src/Test/Functional/Acceptance/Acceptance74CeCest.php index 7abe839d..83d9b83e 100644 --- a/src/Test/Functional/Acceptance/Acceptance74CeCest.php +++ b/src/Test/Functional/Acceptance/Acceptance74CeCest.php @@ -34,6 +34,9 @@ protected function patchesDataProvider(): array // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p2'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], + ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], ]; } } diff --git a/src/Test/Functional/Acceptance/Acceptance74Cest.php b/src/Test/Functional/Acceptance/Acceptance74Cest.php index 41b0791e..36873b62 100644 --- a/src/Test/Functional/Acceptance/Acceptance74Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance74Cest.php @@ -28,6 +28,9 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p2'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], + ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], ]; } } From cc5bc4f5d107e01ac0020eba2dd887a377a1b70b Mon Sep 17 00:00:00 2001 From: Michael Casey <12436335+michaelcasey316@users.noreply.github.com> Date: Wed, 10 Aug 2022 12:12:06 -0500 Subject: [PATCH 108/157] MCLOUD-9137: Improve patch application --- patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch b/patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch index dfbad797..9a9671ef 100755 --- a/patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch +++ b/patches/BUNDLE-3137__braintree_create_order_fix__2.4.5.patch @@ -1,4 +1,4 @@ -diff --git a/vendor/paypal/module-braintree-core/Block/Form.php b/vendor/paypal/module-braintree-core/Block/Form.php +diff -Nuar a/vendor/paypal/module-braintree-core/Block/Form.php b/vendor/paypal/module-braintree-core/Block/Form.php index 4bc1bdf1..70559bbe 100755 --- a/vendor/paypal/module-braintree-core/Block/Form.php +++ b/vendor/paypal/module-braintree-core/Block/Form.php From 3baf395b82bcc86a04f2514ef5dfa240df146888 Mon Sep 17 00:00:00 2001 From: Michael Casey <12436335+michaelcasey316@users.noreply.github.com> Date: Wed, 10 Aug 2022 12:26:37 -0500 Subject: [PATCH 109/157] MCLOUD-9137: Update functional tests --- src/Test/Functional/Acceptance/Acceptance74CeCest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Test/Functional/Acceptance/Acceptance74CeCest.php b/src/Test/Functional/Acceptance/Acceptance74CeCest.php index 83d9b83e..92bc176a 100644 --- a/src/Test/Functional/Acceptance/Acceptance74CeCest.php +++ b/src/Test/Functional/Acceptance/Acceptance74CeCest.php @@ -30,13 +30,13 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2'], ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p1'], ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p2'], - // Magento 2.4.3 CE does not work on the cloud + // Magento 2.4.3 CE and later does not work on the cloud // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p2'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], - ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], + // ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], + // ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], + // ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], ]; } } From 4a8b0b4445c6d7c03cd78de7fb199c27aed742fe Mon Sep 17 00:00:00 2001 From: Michael Casey <12436335+michaelcasey316@users.noreply.github.com> Date: Wed, 10 Aug 2022 15:48:43 -0500 Subject: [PATCH 110/157] MCLOUD-9137: Update functional tests --- src/Test/Functional/Acceptance/Acceptance74Cest.php | 3 --- src/Test/Functional/Acceptance/AcceptanceCest.php | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Test/Functional/Acceptance/Acceptance74Cest.php b/src/Test/Functional/Acceptance/Acceptance74Cest.php index 36873b62..41b0791e 100644 --- a/src/Test/Functional/Acceptance/Acceptance74Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance74Cest.php @@ -28,9 +28,6 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p2'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], - ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], ]; } } diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 5728a525..f2d94573 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -50,6 +50,8 @@ protected function patchesDataProvider(): array { return [ ['templateVersion' => '2.4.4'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], + ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], ]; } } From c55d0d1cdd600716fa7d6e723b40e3ffd0616364 Mon Sep 17 00:00:00 2001 From: Michael Casey <12436335+michaelcasey316@users.noreply.github.com> Date: Wed, 10 Aug 2022 16:04:42 -0500 Subject: [PATCH 111/157] MCLOUD-9137: Update functional tests for CE --- src/Test/Functional/Acceptance/Acceptance74CeCest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Test/Functional/Acceptance/Acceptance74CeCest.php b/src/Test/Functional/Acceptance/Acceptance74CeCest.php index 92bc176a..7abe839d 100644 --- a/src/Test/Functional/Acceptance/Acceptance74CeCest.php +++ b/src/Test/Functional/Acceptance/Acceptance74CeCest.php @@ -30,13 +30,10 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2'], ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p1'], ['templateVersion' => '2.4.2', 'magentoVersion' => '2.4.2-p2'], - // Magento 2.4.3 CE and later does not work on the cloud + // Magento 2.4.3 CE does not work on the cloud // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], // ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p2'], - // ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], - // ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], - // ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], ]; } } From a383f43bde194bfaf15436ecedb8c3916728aaac Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Fri, 2 Sep 2022 13:46:17 -0500 Subject: [PATCH 112/157] ACP2E-1158: QPT is not compatible with certain php 8.1.x versions (#59) --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index adc9e9b9..db2233ac 100644 --- a/composer.json +++ b/composer.json @@ -15,12 +15,12 @@ "ext-json": "*", "composer/composer": "@stable", "composer/semver": "@stable", - "symfony/config": "^3.3||^4.4||^5.1", - "symfony/console": "^2.6||^4.0||^5.1", - "symfony/dependency-injection": "^3.3||^4.3||^5.1", - "symfony/process": "^2.1||^4.1||^5.1", - "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.1", - "symfony/yaml": "^3.3||^4.0||^5.1", + "symfony/config": "^3.3||^4.4||^5.0", + "symfony/console": "^2.6||^4.0||^5.0", + "symfony/dependency-injection": "^3.3||^4.3||^5.0", + "symfony/process": "^2.1||^4.1||^5.0", + "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.0", + "symfony/yaml": "^3.3||^4.0||^5.0", "monolog/monolog": "^1.25||^2.3", "magento/quality-patches": "^1.1.0" }, From 6d90908db94294dcd622204573f6b32ec3736362 Mon Sep 17 00:00:00 2001 From: Michael Casey <12436335+michaelcasey316@users.noreply.github.com> Date: Wed, 7 Sep 2022 16:53:47 -0500 Subject: [PATCH 113/157] MCLOUD-9275: Cloud Tools Release (#60) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index db2233ac..5055cd5d 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.18", + "version": "1.0.19", "license": "OSL-3.0", "repositories": { "repo.magento.com": { From 853e57e7a42d0802004b87ea3565122b92976965 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Thu, 22 Sep 2022 18:11:16 -0500 Subject: [PATCH 114/157] Remove Travis stuff (#62) --- .travis.yml | 96 ----------------------------------- tests/travis/functional-ce.sh | 22 -------- tests/travis/functional-ee.sh | 22 -------- tests/travis/static-unit.sh | 11 ---- travis.php.ini | 1 - 5 files changed, 152 deletions(-) delete mode 100644 .travis.yml delete mode 100755 tests/travis/functional-ce.sh delete mode 100755 tests/travis/functional-ee.sh delete mode 100755 tests/travis/static-unit.sh delete mode 100644 travis.php.ini diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5dba2f04..00000000 --- a/.travis.yml +++ /dev/null @@ -1,96 +0,0 @@ -dist: xenial - -git: - depth: false - -addons: - hosts: - - magento2.docker - -services: - - docker - -language: php - -stages: - - static-unit - - test - -jobs: - include: - - stage: static-unit - script: ./tests/travis/static-unit.sh; - php: '7.1' - env: - - TEST_SUITE=static-unit - - script: ./tests/travis/static-unit.sh; - php: '7.2' - env: - - TEST_SUITE=static-unit - - script: ./tests/travis/static-unit.sh; - php: '7.3' - env: - - TEST_SUITE=static-unit - - stage: test - php: '7.1' - env: - - TEST_SUITE=functional-ee - - PART_INDEX=1 - - php: '7.1' - env: - - TEST_SUITE=functional-ee - - PART_INDEX=2 - - php: '7.2' - env: - - TEST_SUITE=functional-ee - - php: '7.3' - env: - - TEST_SUITE=functional-ee - - php: '7.4' - dist: bionic - env: - - TEST_SUITE=functional-ee - - php: '7.1' - env: - - TEST_SUITE=functional-ce - - PART_INDEX=1 - - php: '7.1' - env: - - TEST_SUITE=functional-ce - - PART_INDEX=2 - - php: '7.2' - env: - - TEST_SUITE=functional-ce - - php: '7.3' - env: - - TEST_SUITE=functional-ce - - php: '7.4' - dist: bionic - env: - - TEST_SUITE=functional-ce - -before_install: - # https://github.com/kylekatarnls/update-helper/issues/9 - - if [ -n "${COMPOSER_VERSION}" ]; then travis_retry composer self-update ${COMPOSER_VERSION}; fi; - -install: - - phpenv config-add travis.php.ini - - composer config http-basic.repo.magento.com ${REPO_USERNAME_CE} ${REPO_PASSWORD_CE} - - composer config github-oauth.github.com ${GITHUB_TOKEN} - - if [ -n "${MCD_VERSION}" ] && [ $TRAVIS_PHP_VERSION != "7.0" ]; then composer config repositories.mcd git git@github.com:magento/magento-cloud-docker.git && composer require "magento/magento-cloud-docker:${MCD_VERSION}" --no-update; fi; - - if [ -n "${MQP_VERSION}" ]; then composer config repositories.mqp git git@github.com:magento/quality-patches.git && composer require "magento/quality-patches:${MQP_VERSION}" --no-update; fi; - - if [ -n "${MCC_VERSION}" ]; then composer config repositories.mcc git git@github.com:magento/magento-cloud-components.git && composer require "magento/magento-cloud-components:${MCC_VERSION}" --no-update; fi; - - if [ -n "${ECE_VERSION}" ]; then composer config repositories.ece git git@github.com:magento/ece-tools.git && composer require "magento/ece-tools:${ECE_VERSION}" --no-update; fi; - - composer config repositories.magento composer https://repo.magento.com/ - - composer require "magento/framework:*" --no-update - - composer require "magento/module-store:*" --no-update - - composer require "magento/module-url-rewrite:*" --no-update - - composer update -n --no-suggest - -before_script: - - sudo sysctl -w vm.max_map_count=262144 - - if [ $TEST_SUITE == "functional-ce" ]; then cp codeception.dist.yml codeception.yml && sed -i "s/REPO_USERNAME/REPO_USERNAME_CE/" codeception.yml && sed -i "s/REPO_PASSWORD/REPO_PASSWORD_CE/" codeception.yml; fi; - -script: - - if [ $TRAVIS_SECURE_ENV_VARS == "true" ] && [ $TEST_SUITE == "functional-ee" ]; then ./tests/travis/functional-ee.sh; fi; - - if [ $TEST_SUITE == "functional-ce" ]; then ./tests/travis/functional-ce.sh; fi; diff --git a/tests/travis/functional-ce.sh b/tests/travis/functional-ce.sh deleted file mode 100755 index a5431613..00000000 --- a/tests/travis/functional-ce.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# Copyright © Magento, Inc. All rights reserved. -# See COPYING.txt for license details. - -set -e -trap '>&2 echo Error: Command \`$BASH_COMMAND\` on line $LINENO failed with exit code $?' ERR - -case $TRAVIS_PHP_VERSION in - 7.1) - ./vendor/bin/codecept run -g php71cePart$PART_INDEX --steps - ;; - 7.2) - ./vendor/bin/codecept run -g php72ce --steps - ;; - 7.3) - ./vendor/bin/codecept run -g php73ce --steps - ;; - 7.4) - ./vendor/bin/codecept run -g php74ce --steps - ;; -esac diff --git a/tests/travis/functional-ee.sh b/tests/travis/functional-ee.sh deleted file mode 100755 index b2761f72..00000000 --- a/tests/travis/functional-ee.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# Copyright © Magento, Inc. All rights reserved. -# See COPYING.txt for license details. - -set -e -trap '>&2 echo Error: Command \`$BASH_COMMAND\` on line $LINENO failed with exit code $?' ERR - -case $TRAVIS_PHP_VERSION in - 7.1) - ./vendor/bin/codecept run -g php71cePart$PART_INDEX --steps - ;; - 7.2) - ./vendor/bin/codecept run -g php72 --steps - ;; - 7.3) - ./vendor/bin/codecept run -g php73 --steps - ;; - 7.4) - ./vendor/bin/codecept run -g php74 --steps - ;; -esac diff --git a/tests/travis/static-unit.sh b/tests/travis/static-unit.sh deleted file mode 100755 index dc99e6a9..00000000 --- a/tests/travis/static-unit.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# Copyright © Magento, Inc. All rights reserved. -# See COPYING.txt for license details. - -set -e -trap '>&2 echo Error: Command \`$BASH_COMMAND\` on line $LINENO failed with exit code $?' ERR - -./vendor/bin/phpcs ./src --standard=./tests/static/phpcs-ruleset.xml -p -n -./vendor/bin/phpmd ./src xml ./tests/static/phpmd-ruleset.xml -./vendor/bin/phpunit --configuration ./tests/unit diff --git a/travis.php.ini b/travis.php.ini deleted file mode 100644 index 0b134267..00000000 --- a/travis.php.ini +++ /dev/null @@ -1 +0,0 @@ -memory_limit = 4G From 022230c54bbfc638244845174948d47f80d50657 Mon Sep 17 00:00:00 2001 From: Peter Jaap Blaakmeer <peterjaap@blaakmeer.com> Date: Wed, 28 Sep 2022 21:15:46 +0200 Subject: [PATCH 115/157] Apply suggestions from code review Co-authored-by: Viktor Tymchynskyi <vtymchynskyi@magento.com> --- src/Command/Process/Action/ReviewAppliedAction.php | 2 +- src/Command/Process/ShowStatus.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Command/Process/Action/ReviewAppliedAction.php b/src/Command/Process/Action/ReviewAppliedAction.php index afcd3211..19769346 100644 --- a/src/Command/Process/Action/ReviewAppliedAction.php +++ b/src/Command/Process/Action/ReviewAppliedAction.php @@ -73,7 +73,7 @@ function ($patch) { $appliedOptionalPatches ); $totalNumber = count(array_unique(array_merge($ids, $patchFilter))); - if ($totalNumber >= self::UPGRADE_THRESHOLD && $input->getOption('format') !== 'json') { + if ($totalNumber >= self::UPGRADE_THRESHOLD) { $message = 'Warning for those on a previous minor line! Magento recommends installing a limited'. ' number of quality patches to ensure a smooth upgrade to the latest line. Please begin planning'. ' an upgrade to the latest release line.'; diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 3cd1b6b3..683ffe97 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -105,9 +105,9 @@ public function run(InputInterface $input, OutputInterface $output) { if ($input->getOption('format') !== 'json') { $this->printDetailsInfo($output); + $this->reviewAppliedAction->execute($input, $output, []); } - $this->reviewAppliedAction->execute($input, $output, []); $patches = $this->aggregator->aggregate( array_merge($this->optionalPool->getList(), $this->localPool->getList()) From 40bb41ac55ef14769a10d05b4bf84c782299ee69 Mon Sep 17 00:00:00 2001 From: Peter Jaap Blaakmeer <peterjaap@elgentos.nl> Date: Thu, 29 Sep 2022 09:06:17 +0200 Subject: [PATCH 116/157] Refactor checks into single variable isJsonFormat --- src/Command/Process/ShowStatus.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 683ffe97..51b9358f 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -28,6 +28,8 @@ class ShowStatus implements ProcessInterface const FILTER_OPTION_ALL = 'All'; + const FORMAT_JSON = 'json'; + /** * @var Aggregator */ @@ -103,31 +105,31 @@ public function __construct( */ public function run(InputInterface $input, OutputInterface $output) { - if ($input->getOption('format') !== 'json') { - $this->printDetailsInfo($output); - $this->reviewAppliedAction->execute($input, $output, []); - } - - + $isJsonFormat = $input->getOption('format') === self::FORMAT_JSON; $patches = $this->aggregator->aggregate( array_merge($this->optionalPool->getList(), $this->localPool->getList()) ); - foreach ($patches as $patch) { - if ($patch->isDeprecated() && $this->isPatchVisible($patch) && $input->getOption('format') !== 'json') { - $this->printDeprecatedWarning($output, $patch); + if (!$isJsonFormat) { + $this->printDetailsInfo($output); + $this->reviewAppliedAction->execute($input, $output, []); + foreach ($patches as $patch) { + if ($patch->isDeprecated() && $this->isPatchVisible($patch)) { + $this->printDeprecatedWarning($output, $patch); + } } } + $patches = $this->filterNotVisiblePatches($patches); - if (count($patches) > self::INTERACTIVE_FILTER_THRESHOLD) { + if (!$isJsonFormat && count($patches) > self::INTERACTIVE_FILTER_THRESHOLD) { $this->printPatchProviders($output, $patches); $patches = $this->filterByPatchProvider($input, $output, $patches); $this->printCategoriesInfo($output, $patches); $patches = $this->filterByPatchCategory($input, $output, $patches); } - if ($input->getOption('format') === 'json') { + if ($isJsonFormat) { $this->renderer->printJson($output, array_values($patches)); } else { $this->renderer->printTable($output, array_values($patches)); From ff0aa7b8bd3ff656daa5fe4cc25e3867c912b3cb Mon Sep 17 00:00:00 2001 From: Peter Jaap Blaakmeer <peterjaap@elgentos.nl> Date: Fri, 30 Sep 2022 10:46:33 +0200 Subject: [PATCH 117/157] Move magentoVersion dependency and code to ShowStatus process instead of Status command --- src/Command/Process/ShowStatus.php | 14 +++++++++++++- src/Command/Status.php | 14 +------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 51b9358f..47b8de67 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -8,6 +8,7 @@ namespace Magento\CloudPatches\Command\Process; use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; +use Magento\CloudPatches\Composer\MagentoVersion; use Magento\CloudPatches\Console\QuestionFactory; use Magento\CloudPatches\Patch\Data\AggregatedPatch; use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; @@ -45,6 +46,11 @@ class ShowStatus implements ProcessInterface */ private $localPool; + /** + * @var MagentoVersion + */ + private $magentoVersion; + /** * @var StatusPool */ @@ -88,7 +94,8 @@ public function __construct( ReviewAppliedAction $reviewAppliedAction, Renderer $renderer, QuestionHelper $questionHelper, - QuestionFactory $questionFactory + QuestionFactory $questionFactory, + MagentoVersion $magentoVersion ) { $this->aggregator = $aggregator; $this->optionalPool = $optionalPool; @@ -98,6 +105,7 @@ public function __construct( $this->renderer = $renderer; $this->questionHelper = $questionHelper; $this->questionFactory = $questionFactory; + $this->magentoVersion = $magentoVersion; } /** @@ -134,6 +142,10 @@ public function run(InputInterface $input, OutputInterface $output) } else { $this->renderer->printTable($output, array_values($patches)); } + + if (!$isJsonFormat) { + $output->writeln('<info>' . $this->magentoVersion->get() . '</info>'); + } } /** diff --git a/src/Command/Status.php b/src/Command/Status.php index 12ad9dbe..72df3a05 100644 --- a/src/Command/Status.php +++ b/src/Command/Status.php @@ -9,7 +9,6 @@ use Magento\CloudPatches\App\RuntimeException; use Magento\CloudPatches\Command\Process\ShowStatus; -use Magento\CloudPatches\Composer\MagentoVersion; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -33,24 +32,16 @@ class Status extends AbstractCommand */ private $logger; - /** - * @var MagentoVersion - */ - private $magentoVersion; - /** * @param ShowStatus $showStatus * @param LoggerInterface $logger - * @param MagentoVersion $magentoVersion */ public function __construct( ShowStatus $showStatus, - LoggerInterface $logger, - MagentoVersion $magentoVersion + LoggerInterface $logger ) { $this->showStatus = $showStatus; $this->logger = $logger; - $this->magentoVersion = $magentoVersion; parent::__construct(self::NAME); } @@ -74,9 +65,6 @@ public function execute(InputInterface $input, OutputInterface $output) { try { $this->showStatus->run($input, $output); - if ($input->getOption('format') !== 'json') { - $output->writeln('<info>' . $this->magentoVersion->get() . '</info>'); - } } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); $this->logger->error($e->getMessage()); From bedd072cb2bf34882453edfff6bd4e4a585a965d Mon Sep 17 00:00:00 2001 From: Peter Jaap Blaakmeer <peterjaap@blaakmeer.com> Date: Fri, 30 Sep 2022 16:28:15 +0200 Subject: [PATCH 118/157] Update src/Command/Process/ShowStatus.php Co-authored-by: Viktor Tymchynskyi <vtymchynskyi@magento.com> --- src/Command/Process/ShowStatus.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 47b8de67..d013aa47 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -141,9 +141,6 @@ public function run(InputInterface $input, OutputInterface $output) $this->renderer->printJson($output, array_values($patches)); } else { $this->renderer->printTable($output, array_values($patches)); - } - - if (!$isJsonFormat) { $output->writeln('<info>' . $this->magentoVersion->get() . '</info>'); } } From 22e59d404ec068a2b2bc7163427939ea73385b14 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Mon, 3 Oct 2022 13:09:49 -0500 Subject: [PATCH 119/157] - Update unit tests after moving MgentoVersion dependency --- src/Test/Unit/Command/Process/ShowStatusTest.php | 9 +++++++-- src/Test/Unit/Command/StatusTest.php | 6 +----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Test/Unit/Command/Process/ShowStatusTest.php b/src/Test/Unit/Command/Process/ShowStatusTest.php index bafd697f..97e9b1d6 100644 --- a/src/Test/Unit/Command/Process/ShowStatusTest.php +++ b/src/Test/Unit/Command/Process/ShowStatusTest.php @@ -10,6 +10,7 @@ use Magento\CloudPatches\Command\Process\Action\ReviewAppliedAction; use Magento\CloudPatches\Command\Process\Renderer; use Magento\CloudPatches\Command\Process\ShowStatus; +use Magento\CloudPatches\Composer\MagentoVersion; use Magento\CloudPatches\Console\QuestionFactory; use Magento\CloudPatches\Patch\Aggregator; use Magento\CloudPatches\Patch\Data\AggregatedPatch; @@ -88,6 +89,9 @@ protected function setUp(): void $this->renderer = $this->createMock(Renderer::class); $this->questionHelper = $this->createMock(QuestionHelper::class); $this->questionFactory = $this->createMock(QuestionFactory::class); + /** @var MagentoVersion|MockObject $magentoVersion */ + $magentoVersion = $this->createMock(MagentoVersion::class); + $this->manager = new ShowStatus( $this->aggregator, @@ -97,7 +101,8 @@ protected function setUp(): void $this->reviewAppliedAction, $this->renderer, $this->questionHelper, - $this->questionFactory + $this->questionFactory, + $magentoVersion ); } @@ -147,7 +152,7 @@ public function testShowStatus() ->willReturn([$patch1, $patch2, $patch3, $patch4, $patch5]); // Show warning message about patch deprecation - $outputMock->expects($this->exactly(3)) + $outputMock->expects($this->exactly(4)) ->method('writeln') ->withConsecutive( [$this->anything()], diff --git a/src/Test/Unit/Command/StatusTest.php b/src/Test/Unit/Command/StatusTest.php index 7cd7eec3..cc6e58a3 100644 --- a/src/Test/Unit/Command/StatusTest.php +++ b/src/Test/Unit/Command/StatusTest.php @@ -12,7 +12,6 @@ use Magento\CloudPatches\Command\Apply; use Magento\CloudPatches\Command\Process\ShowStatus; use Magento\CloudPatches\Command\Status; -use Magento\CloudPatches\Composer\MagentoVersion; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -46,13 +45,10 @@ protected function setUp(): void { $this->showStatus = $this->createMock(ShowStatus::class); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); - /** @var MagentoVersion|MockObject $magentoVersion */ - $magentoVersion = $this->createMock(MagentoVersion::class); $this->command = new Status( $this->showStatus, - $this->logger, - $magentoVersion + $this->logger ); } From 4faf0fc6aa6749968ae8940e948b9dc30b4491b8 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Fri, 14 Oct 2022 10:12:48 -0500 Subject: [PATCH 120/157] - Fix code style --- src/Test/Unit/Command/Process/ShowStatusTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Test/Unit/Command/Process/ShowStatusTest.php b/src/Test/Unit/Command/Process/ShowStatusTest.php index 97e9b1d6..6caf965c 100644 --- a/src/Test/Unit/Command/Process/ShowStatusTest.php +++ b/src/Test/Unit/Command/Process/ShowStatusTest.php @@ -92,7 +92,6 @@ protected function setUp(): void /** @var MagentoVersion|MockObject $magentoVersion */ $magentoVersion = $this->createMock(MagentoVersion::class); - $this->manager = new ShowStatus( $this->aggregator, $this->optionalPool, From 138c65e0c454519f97af8fcb8a4bc349c6d3ccc0 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Mon, 17 Oct 2022 14:27:55 -0500 Subject: [PATCH 121/157] MCLOUD-7845: Add MCLOUD-7205(MCP-103) to magento-cloud-patches (#64) --- composer.json | 2 +- patches.json | 3 +- ...LOUD-6659__fix_L2_redis_cache__2.4.0.patch | 36 +++++++++++++++++- ...__fix_local_flushing_L2_cache__2.4.0.patch | 38 +++++++++++++++++++ .../Acceptance/Acceptance73Cest.php | 1 + .../Acceptance/Acceptance74Cest.php | 1 + .../Functional/Acceptance/AcceptanceCest.php | 4 +- 7 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 patches/MCLOUD-7845__fix_local_flushing_L2_cache__2.4.0.patch diff --git a/composer.json b/composer.json index 5055cd5d..6df0d557 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.19", + "version": "1.0.20", "license": "OSL-3.0", "repositories": { "repo.magento.com": { diff --git a/patches.json b/patches.json index 55230801..87558fd4 100644 --- a/patches.json +++ b/patches.json @@ -255,7 +255,8 @@ ">=2.3.3-p1 <2.3.4": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.3-p1.patch", ">=2.3.4 <2.3.5": "MCLOUD-6139_MCLOUD-6211__redis_improvement_patches__2.3.4.patch", ">=2.3.5 <2.3.6": "MCLOUD-6211__redis_improvement_patches__2.3.5.patch", - ">=2.4.0 <2.4.1": "MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch" + ">=2.4.0 <2.4.1": "MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch", + ">=2.4.1 <2.4.2": "MCLOUD-7845__fix_local_flushing_L2_cache__2.4.0.patch" }, "Incompatible PHP Method Fix": { "2.3.7-p1": "AC-384__Fix_Incompatible_PHP_Method__2.3.7-p1_ce.patch", diff --git a/patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch b/patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch index 24b83952..2eea35b3 100644 --- a/patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch +++ b/patches/MCLOUD-6659__fix_L2_redis_cache__2.4.0.patch @@ -1,7 +1,7 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php --- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php -@@ -205,7 +205,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache +@@ -237,7 +237,7 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache $dataToSave = $data; $remHash = $this->loadRemoteDataVersion($id); @@ -10,3 +10,37 @@ diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php $dataToSave = $this->remote->load($id); } else { $this->remote->save($data, $id, $tags, $specificLifetime); +@@ -248,9 +248,23 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + $this->unlock($id); + } + ++ if (!mt_rand(0, 100) && $this->checkIfLocalCacheSpaceExceeded()) { ++ $this->local->clean(); ++ } ++ + return $this->local->save($dataToSave, $id, [], $specificLifetime); + } + ++ /** ++ * Check if local cache space bigger that configure amount ++ * ++ * @return bool ++ */ ++ private function checkIfLocalCacheSpaceExceeded() ++ { ++ return $this->getFillingPercentage() >= 95; ++ } ++ + /** + * @inheritdoc + */ +@@ -266,7 +280,8 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + */ + public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = []) + { +- return $this->remote->clean($mode, $tags); ++ return $this->remote->clean($mode, $tags) && ++ $this->local->clean($mode, $tags); + } + + /** diff --git a/patches/MCLOUD-7845__fix_local_flushing_L2_cache__2.4.0.patch b/patches/MCLOUD-7845__fix_local_flushing_L2_cache__2.4.0.patch new file mode 100644 index 00000000..766f85ca --- /dev/null +++ b/patches/MCLOUD-7845__fix_local_flushing_L2_cache__2.4.0.patch @@ -0,0 +1,38 @@ +diff -Nuar a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +index d0c05613..96f7ad84 100644 +--- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php ++++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +@@ -248,9 +248,23 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + $this->unlock($id); + } + ++ if (!mt_rand(0, 100) && $this->checkIfLocalCacheSpaceExceeded()) { ++ $this->local->clean(); ++ } ++ + return $this->local->save($dataToSave, $id, [], $specificLifetime); + } + ++ /** ++ * Check if local cache space bigger that configure amount ++ * ++ * @return bool ++ */ ++ private function checkIfLocalCacheSpaceExceeded() ++ { ++ return $this->getFillingPercentage() >= 95; ++ } ++ + /** + * @inheritdoc + */ +@@ -266,7 +280,8 @@ class RemoteSynchronizedCache extends \Zend_Cache_Backend implements \Zend_Cache + */ + public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = []) + { +- return $this->remote->clean($mode, $tags); ++ return $this->remote->clean($mode, $tags) && ++ $this->local->clean($mode, $tags); + } + + /** diff --git a/src/Test/Functional/Acceptance/Acceptance73Cest.php b/src/Test/Functional/Acceptance/Acceptance73Cest.php index 7072aab5..62fa643d 100644 --- a/src/Test/Functional/Acceptance/Acceptance73Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance73Cest.php @@ -31,6 +31,7 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p1'], ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p2'], ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p3'], + ['templateVersion' => '2.3.7', 'magentoVersion' => '2.3.7-p4'], ['templateVersion' => '2.4.0', 'magentoVersion' => '2.4.0'], ]; } diff --git a/src/Test/Functional/Acceptance/Acceptance74Cest.php b/src/Test/Functional/Acceptance/Acceptance74Cest.php index 41b0791e..03e3be11 100644 --- a/src/Test/Functional/Acceptance/Acceptance74Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance74Cest.php @@ -28,6 +28,7 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p1'], ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p2'], + ['templateVersion' => '2.4.3', 'magentoVersion' => '2.4.3-p3'], ]; } } diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index f2d94573..e85bffe9 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -49,9 +49,11 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void protected function patchesDataProvider(): array { return [ - ['templateVersion' => '2.4.4'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p2'], ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], + ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5-p1'], ]; } } From ef757a04324057a02e149f0b5e7571b593d0ef00 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Mon, 28 Nov 2022 15:59:48 -0600 Subject: [PATCH 122/157] MCLOUD-9683: Update Code of Conduct on public Cloud repos --- .github/CODE_OF_CONDUCT.md | 71 +++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 4e82725a..5eb057af 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,46 +1,75 @@ -# Contributor Covenant Code of Conduct +# Magento Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contribute to a positive environment for our project and community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best, not just for us as individuals but for the overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Publishing others’ private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any instances of unacceptable behavior. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies when an individual is representing the project or its community both within project spaces and in public spaces. Examples of representing a project or community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at engcom@magento.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by first contacting the project team at engcom@adobe.com. Oversight of Adobe projects is handled by the Adobe Open Source Office, which has final say in any violations and enforcement of this Code of Conduct and can be reached at Grp-opensourceoffice@adobe.com. All complaints will be reviewed and investigated promptly and fairly. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +The project team must respect the privacy and security of the reporter of any incident. -## Attribution +Project maintainers who do not follow or enforce the Code of Conduct may face temporary or permanent repercussions as determined by other members of the project's leadership or the Adobe Open Source Office. + +## Enforcement Guidelines + +Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem to be in violation of this Code of Conduct: + +**1. Correction** + +Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +Consequence: A private, written warning from project maintainers describing the violation and why the behavior was unacceptable. A public apology may be requested from the violator before any further involvement in the project by violator. + +**2. Warning** + +Community Impact: A relatively minor violation through a single incident or series of actions. -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +Consequence: A written warning from project maintainers that includes stated consequences for continued unacceptable behavior. Violator must refrain from interacting with the people involved for a specified period of time as determined by the project maintainers, including, but not limited to, unsolicited interaction with those enforcing the Code of Conduct through channels such as community spaces and social media. Continued violations may lead to a temporary or permanent ban. + +**3. Temporary Ban** + +Community Impact: A more serious violation of community standards, including sustained unacceptable behavior. + +Consequence: A temporary ban from any interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Failure to comply with the temporary ban may lead to a permanent ban. + +**4. Permanent Ban** + +Community Impact: Demonstrating a consistent pattern of violation of community standards or an egregious violation of community standards, including, but not limited to, sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any interaction with the community. + +## Attribution -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. From dc1a38bf61b2bec8f48afefb9136dfa037ae91ac Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Fri, 24 Feb 2023 14:42:15 -0600 Subject: [PATCH 123/157] ACP2E-1651: Adjust QPT for 2.4.6/2.4.4-p3/2.4.5-p2 Adobe Commerce releases --- composer.json | 12 ++++++------ config/services.xml | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 6df0d557..c2f2e336 100644 --- a/composer.json +++ b/composer.json @@ -15,12 +15,12 @@ "ext-json": "*", "composer/composer": "@stable", "composer/semver": "@stable", - "symfony/config": "^3.3||^4.4||^5.0", - "symfony/console": "^2.6||^4.0||^5.0", - "symfony/dependency-injection": "^3.3||^4.3||^5.0", - "symfony/process": "^2.1||^4.1||^5.0", - "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.0", - "symfony/yaml": "^3.3||^4.0||^5.0", + "symfony/config": "^3.3||^4.4||^5.0||^6.0", + "symfony/console": "^2.6||^4.0||^5.0||^6.0", + "symfony/dependency-injection": "^3.3||^4.3||^5.0||^6.0", + "symfony/process": "^2.1||^4.1||^5.0||^6.0", + "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.0||^6.0", + "symfony/yaml": "^3.3||^4.0||^5.0||^6.0", "monolog/monolog": "^1.25||^2.3", "magento/quality-patches": "^1.1.0" }, diff --git a/config/services.xml b/config/services.xml index 94c04a57..f7785634 100644 --- a/config/services.xml +++ b/config/services.xml @@ -7,6 +7,7 @@ <prototype namespace="Magento\CloudPatches\" resource="../src/*" exclude="../src/{Test}"/> <service id="Psr\Log\LoggerInterface" alias="Magento\CloudPatches\App\Logger" /> + <service id="Psr\Container\ContainerInterface" alias="Magento\CloudPatches\App\Container" /> <service id="Magento\CloudPatches\App\Container" autowire="false"/> <service id="Magento\CloudPatches\Filesystem\DirectoryList" autowire="false"/> <service id="Symfony\Component\Console\Helper\QuestionHelper"/> From fda9e6ecbb18717c036d472c236344effcf323b3 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorbivskyi <gorbivsk@adobe.com> Date: Mon, 27 Feb 2023 10:41:33 -0800 Subject: [PATCH 124/157] MCLOUD-10032: Added ACSD-49067 patch - Auto increment number jumping up for catalog_product_entity_* tables --- patches.json | 3 ++ ...alog_product_entity_tables__2.4.3-p1.patch | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 patches/MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch diff --git a/patches.json b/patches.json index 87558fd4..73a56d16 100644 --- a/patches.json +++ b/patches.json @@ -264,6 +264,9 @@ }, "Fixed currency displaying on product page": { ">=2.4.3 <2.4.4": "MCLOUD-8279__Fixed_currency_displaying_on_product_page__2.4.3.patch" + }, + "Auto increment number jumping up for catalog_product_entity_* tables": { + ">=2.3.7 <2.4.7": "MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch b/patches/MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch new file mode 100644 index 00000000..40d2d319 --- /dev/null +++ b/patches/MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch @@ -0,0 +1,48 @@ +diff --git a/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php b/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php +index c71225b4fc6..3f0ee96d70e 100644 +--- a/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php ++++ b/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php +@@ -24,14 +24,14 @@ use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; + abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity + { + /** +- * Store manager ++ * Store manager to get the store information + * + * @var \Magento\Store\Model\StoreManagerInterface + */ + protected $_storeManager; + + /** +- * Model factory ++ * Model factory to create a model object + * + * @var \Magento\Catalog\Model\Factory + */ +@@ -325,7 +325,25 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity + */ + protected function _updateAttribute($object, $attribute, $valueId, $value) + { +- return $this->_saveAttributeValue($object, $attribute, $value); ++ $entity = $attribute->getEntity(); ++ $row = $this->getAttributeRow($entity, $object, $attribute); ++ $hasSingleStore = $this->_storeManager->hasSingleStore(); ++ $storeId = $hasSingleStore ++ ? $this->getDefaultStoreId() ++ : (int) $this->_storeManager->getStore($object->getStoreId())->getId(); ++ if ($valueId > 0 && array_key_exists('store_id', $row) && $storeId === $row['store_id']) { ++ $table = $attribute->getBackend()->getTable(); ++ $connection = $this->getConnection(); ++ $connection->update( ++ $table, ++ ['value' => $this->_prepareValueForSave($value, $attribute)], ++ sprintf('%s=%d', $connection->quoteIdentifier('value_id'), $valueId) ++ ); ++ ++ return $this; ++ } else { ++ return $this->_saveAttributeValue($object, $attribute, $value); ++ } + } + + /** From e6e1405c1e6f46d149008cd7f1a6c54f81c9844a Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 28 Feb 2023 12:09:16 -0600 Subject: [PATCH 125/157] Update MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch --- ...ent_number_for_catalog_product_entity_tables__2.4.3-p1.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch b/patches/MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch index 40d2d319..8ec73785 100644 --- a/patches/MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch +++ b/patches/MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch @@ -1,4 +1,4 @@ -diff --git a/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php b/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php +diff -Nuar a/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php b/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php index c71225b4fc6..3f0ee96d70e 100644 --- a/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php +++ b/vendor/magento/module-catalog/Model/ResourceModel/AbstractResource.php From e01d0fff744e0bb7a35e4d7e85fc4b7584f6f650 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 28 Feb 2023 18:17:13 -0600 Subject: [PATCH 126/157] MCLOUD-9902: Add FT to Magento Cloud Tools for PHP 8.2 & Magento 2.4.6 (#67) --- composer.json | 9 ++++-- .../Acceptance/Acceptance81Cest.php | 30 +++++++++++++++++++ .../Functional/Acceptance/AcceptanceCest.php | 8 ++--- .../Acceptance/PatchApplierCest.php | 4 +-- 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 src/Test/Functional/Acceptance/Acceptance81Cest.php diff --git a/composer.json b/composer.json index 6df0d557..cb39aefb 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^7.2 || ^8.0", "ext-json": "*", - "composer/composer": "@stable", + "composer/composer": "<2.3", "composer/semver": "@stable", "symfony/config": "^3.3||^4.4||^5.0", "symfony/console": "^2.6||^4.0||^5.0", @@ -30,7 +30,7 @@ "codeception/module-db": "^1.0", "codeception/module-phpbrowser": "^1.0", "codeception/module-rest": "^1.2", - "consolidation/robo": "^1.2", + "consolidation/robo": "^1.2 || ^2.0", "phpmd/phpmd": "@stable", "phpunit/phpunit": "^8.5 || ^9.5", "squizlabs/php_codesniffer": "^3.0" @@ -56,7 +56,10 @@ "phpunit": "phpunit --configuration tests/unit" }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "magento/*": true + } }, "prefer-stable": true } diff --git a/src/Test/Functional/Acceptance/Acceptance81Cest.php b/src/Test/Functional/Acceptance/Acceptance81Cest.php new file mode 100644 index 00000000..ac0f7029 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance81Cest.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php81 + */ +class Acceptance81Cest extends AcceptanceCest +{ + /** + * @return array + */ + protected function patchesDataProvider(): array + { + return [ + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p2'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p3'], + ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], + ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5-p1'], + ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5-p2'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index e85bffe9..762775fa 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -8,7 +8,7 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php81 + * @group php82 */ class AcceptanceCest extends AbstractCest { @@ -49,11 +49,7 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void protected function patchesDataProvider(): array { return [ - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p2'], - ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], - ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5-p1'], + ['templateVersion' => '2.4.6', 'magentoVersion' => null], ]; } } diff --git a/src/Test/Functional/Acceptance/PatchApplierCest.php b/src/Test/Functional/Acceptance/PatchApplierCest.php index ae3129c6..0b5b6e80 100644 --- a/src/Test/Functional/Acceptance/PatchApplierCest.php +++ b/src/Test/Functional/Acceptance/PatchApplierCest.php @@ -10,7 +10,7 @@ use Magento\CloudDocker\Test\Functional\Codeception\Docker; /** - * @group php81 + * @group php82 */ class PatchApplierCest extends AbstractCest { @@ -21,7 +21,7 @@ public function _before(\CliTester $I): void { parent::_before($I); - $this->prepareTemplate($I, '2.4.4'); + $this->prepareTemplate($I, '2.4.6'); $I->copyFileToWorkDir('files/debug_logging/.magento.env.yaml', '.magento.env.yaml'); } From 9fb0f97089d28e46f9933365d53279e7f1a7ab9e Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Wed, 1 Mar 2023 15:37:32 -0600 Subject: [PATCH 127/157] Fix composer version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b7ddc769..72ac04be 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^7.2 || ^8.0", "ext-json": "*", - "composer/composer": "<2.3", + "composer/composer": "^1.4 || ^2.0", "composer/semver": "@stable", "symfony/config": "^3.3||^4.4||^5.0||^6.0", "symfony/console": "^2.6||^4.0||^5.0||^6.0", From 923fd683163b9f2ec9e9b4efe6c9bcdce1c98e1e Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Wed, 1 Mar 2023 16:15:01 -0600 Subject: [PATCH 128/157] Fix unit tests for different composer versions (#70) --- composer.json | 2 +- src/Test/Unit/Composer/MagentoVersionTest.php | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index cb39aefb..7413fd1a 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^7.2 || ^8.0", "ext-json": "*", - "composer/composer": "<2.3", + "composer/composer": "^1.4 || ^2.0", "composer/semver": "@stable", "symfony/config": "^3.3||^4.4||^5.0", "symfony/console": "^2.6||^4.0||^5.0", diff --git a/src/Test/Unit/Composer/MagentoVersionTest.php b/src/Test/Unit/Composer/MagentoVersionTest.php index 1481b7de..46332cda 100644 --- a/src/Test/Unit/Composer/MagentoVersionTest.php +++ b/src/Test/Unit/Composer/MagentoVersionTest.php @@ -12,6 +12,7 @@ use Composer\Package\RootPackageInterface; use Composer\Repository\RepositoryManager; use Composer\Repository\WritableRepositoryInterface; +use Composer\Repository\InstalledRepositoryInterface; use Magento\CloudPatches\Composer\MagentoVersion; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -24,7 +25,7 @@ class MagentoVersionTest extends TestCase const VERSION = '2.3.5'; /** - * @var WritableRepositoryInterface|MockObject + * @var WritableRepositoryInterface|InstalledRepositoryInterface|MockObject */ private $repository; @@ -43,7 +44,11 @@ class MagentoVersionTest extends TestCase */ protected function setUp(): void { - $this->repository = $this->getMockForAbstractClass(WritableRepositoryInterface::class); + $this->repository = $this->getMockForAbstractClass( + (version_compare(PHP_VERSION, '7.3') == -1) + ? WritableRepositoryInterface::class + : InstalledRepositoryInterface::class + ); $this->rootPackage = $this->getMockForAbstractClass(RootPackageInterface::class); $repositoryManager = $this->createMock(RepositoryManager::class); $repositoryManager->method('getLocalRepository') From 31fb0ae60a3582755ae65a6085084d00cdea15ee Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Mon, 6 Mar 2023 14:26:08 -0600 Subject: [PATCH 129/157] Bump version 1.0.21 (#71) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 72ac04be..3ab197f4 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.20", + "version": "1.0.21", "license": "OSL-3.0", "repositories": { "repo.magento.com": { From 854a060a9e3ee132b549026e1902c9417b1d4f9b Mon Sep 17 00:00:00 2001 From: Kateryna Muntianu <muntianu@adobe.com> Date: Tue, 11 Apr 2023 19:10:59 -0500 Subject: [PATCH 130/157] MCLOUD-10279: Added ACSD-50165 patch --- patches.json | 3 ++ ...shing_js_css_cache_from_admin__2.4.4.patch | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch diff --git a/patches.json b/patches.json index 87558fd4..439edae8 100644 --- a/patches.json +++ b/patches.json @@ -264,6 +264,9 @@ }, "Fixed currency displaying on product page": { ">=2.4.3 <2.4.4": "MCLOUD-8279__Fixed_currency_displaying_on_product_page__2.4.3.patch" + }, + "Fixes the error 'The file can't be deleted. Warning!unlink: No such file or directory' when flushing JS/CSS cache from the Admin": { + ">=2.4.1-p1 <2.4.7": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch b/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch new file mode 100644 index 00000000..548c2116 --- /dev/null +++ b/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch @@ -0,0 +1,28 @@ +diff --git a/vendor/magento/framework/Filesystem/Driver/File.php b/vendor/magento/framework/Filesystem/Driver/File.php +index 5dcfeeef23ab..e26acb5a9369 100644 +--- a/vendor/magento/framework/Filesystem/Driver/File.php ++++ b/vendor/magento/framework/Filesystem/Driver/File.php +@@ -440,11 +440,12 @@ public function symlink($source, $destination, DriverInterface $targetDriver = n + */ + public function deleteFile($path) + { +- $result = @unlink($this->getScheme() . $path); ++ @unlink($this->getScheme() . $path); + if ($this->stateful) { + clearstatcache(true, $this->getScheme() . $path); + } +- if (!$result) { ++ ++ if ($this->isFile($path)) { + throw new FileSystemException( + new Phrase( + 'The "%1" file can\'t be deleted. %2', +@@ -452,7 +453,7 @@ public function deleteFile($path) + ) + ); + } +- return $result; ++ return true; + } + + /** From ac798faa61c921a6caf750c4712008f67b0e2ede Mon Sep 17 00:00:00 2001 From: Kateryna Muntianu <muntianu@adobe.com> Date: Wed, 12 Apr 2023 13:51:38 -0500 Subject: [PATCH 131/157] MCLOUD-10279: Added ACSD-50284 patch --- patches.json | 1 + ...shing_js_css_cache_from_admin__2.4.0.patch | 24 +++++++++++++++++++ ...shing_js_css_cache_from_admin__2.4.4.patch | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch diff --git a/patches.json b/patches.json index 439edae8..598c66e5 100644 --- a/patches.json +++ b/patches.json @@ -266,6 +266,7 @@ ">=2.4.3 <2.4.4": "MCLOUD-8279__Fixed_currency_displaying_on_product_page__2.4.3.patch" }, "Fixes the error 'The file can't be deleted. Warning!unlink: No such file or directory' when flushing JS/CSS cache from the Admin": { + ">=2.4.0 <2.4.1-p1": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch" ">=2.4.1-p1 <2.4.7": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch" } }, diff --git a/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch b/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch new file mode 100644 index 00000000..8cc54806 --- /dev/null +++ b/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch @@ -0,0 +1,24 @@ +diff -Nuar a/vendor/magento/framework/Filesystem/Driver/File.php b/vendor/magento/framework/Filesystem/Driver/File.php +index 1affad552137..4edb095f6c48 100644 +--- a/vendor/magento/framework/Filesystem/Driver/File.php ++++ b/vendor/magento/framework/Filesystem/Driver/File.php +@@ -391,8 +391,8 @@ public function symlink($source, $destination, DriverInterface $targetDriver = n + */ + public function deleteFile($path) + { +- $result = @unlink($this->getScheme() . $path); +- if (!$result) { ++ @unlink($this->getScheme() . $path); ++ if ($this->isFile($path)) { + throw new FileSystemException( + new Phrase( + 'The "%1" file can\'t be deleted. %2', +@@ -400,7 +400,7 @@ public function deleteFile($path) + ) + ); + } +- return $result; ++ return true; + } + + /** diff --git a/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch b/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch index 548c2116..8cbc2761 100644 --- a/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch +++ b/patches/MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch @@ -1,4 +1,4 @@ -diff --git a/vendor/magento/framework/Filesystem/Driver/File.php b/vendor/magento/framework/Filesystem/Driver/File.php +diff -Nuar a/vendor/magento/framework/Filesystem/Driver/File.php b/vendor/magento/framework/Filesystem/Driver/File.php index 5dcfeeef23ab..e26acb5a9369 100644 --- a/vendor/magento/framework/Filesystem/Driver/File.php +++ b/vendor/magento/framework/Filesystem/Driver/File.php From 17be5c74d6c1fc64c66d523408a0328881d7f826 Mon Sep 17 00:00:00 2001 From: Kateryna Muntianu <muntianu@adobe.com> Date: Thu, 20 Apr 2023 08:02:11 -0500 Subject: [PATCH 132/157] Update for MCLOUD-10279 --- patches.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches.json b/patches.json index 8fb98849..63300fd1 100644 --- a/patches.json +++ b/patches.json @@ -269,7 +269,7 @@ ">=2.3.7 <2.4.7": "MCLOUD-10032__Increment_number_for_catalog_product_entity_tables__2.4.3-p1.patch" }, "Fixes the error 'The file can't be deleted. Warning!unlink: No such file or directory' when flushing JS/CSS cache from the Admin": { - ">=2.4.0 <2.4.1-p1": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch" + ">=2.4.0 <2.4.1-p1": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch", ">=2.4.1-p1 <2.4.7": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch" } }, From 5d8f506511da07ff707f054a4788c2fe5544678d Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Thu, 25 May 2023 16:07:26 -0500 Subject: [PATCH 133/157] ACP2E-1963: Add warning to the QPT CLI wizard/output in case patch has dependencies --- .../Process/Action/ConfirmRequiredAction.php | 10 +++++++++- src/Command/Process/Renderer.php | 5 +++++ src/Command/Process/ShowStatus.php | 5 +++-- src/Patch/AggregatedPatchFactory.php | 17 ++++++++++++++++- src/Patch/Collector/SupportCollector.php | 12 ++++++++++-- src/Patch/Data/AggregatedPatch.php | 16 +++++++++++++++- src/Patch/Data/AggregatedPatchInterface.php | 7 +++++++ src/Patch/Data/Patch.php | 18 +++++++++++++++++- src/Patch/Data/PatchInterface.php | 7 +++++++ src/Patch/PatchBuilder.php | 19 ++++++++++++++++++- src/Test/Unit/Patch/PatchBuilderTest.php | 8 ++++++-- 11 files changed, 113 insertions(+), 11 deletions(-) diff --git a/src/Command/Process/Action/ConfirmRequiredAction.php b/src/Command/Process/Action/ConfirmRequiredAction.php index a8d74623..c0a935d7 100644 --- a/src/Command/Process/Action/ConfirmRequiredAction.php +++ b/src/Command/Process/Action/ConfirmRequiredAction.php @@ -21,6 +21,8 @@ */ class ConfirmRequiredAction implements ActionInterface { + const PATCH_INFO_URL = "https://experienceleague.adobe.com/tools/commerce-quality-patches/index.html"; + /** * @var OptionalPool */ @@ -76,10 +78,16 @@ function ($patch) { } if ($requiredNotAppliedPatches) { + $url = self::PATCH_INFO_URL . '?keyword=' . current($patchFilter); + $output->writeln( + '<info>Please double check patch details and requirements at ' . + sprintf('<href=%1$s>%1$s</>', $url) . + '</info>' . + PHP_EOL + ); $output->writeln( '<info>Next patches are required by ' . implode(' ', $patchFilter) . ':</info>' . PHP_EOL ); - $aggregatedPatches = $this->aggregator->aggregate($requiredNotAppliedPatches); $this->renderer->printTable($output, $aggregatedPatches); diff --git a/src/Command/Process/Renderer.php b/src/Command/Process/Renderer.php index 3ba10558..8162a4ea 100644 --- a/src/Command/Process/Renderer.php +++ b/src/Command/Process/Renderer.php @@ -220,6 +220,11 @@ function ($item) { $details .= 'Affected components:' . $glue . implode($glue, $patch->getAffectedComponents()); } + if ($patch->getRequirements()) { + $requirements = rtrim(chunk_split($patch->getRequirements(), 50, PHP_EOL)); + $details .= PHP_EOL . '<comment>Requirements:</comment>' . PHP_EOL . ' - ' . $requirements; + } + $id = $patch->getType() === PatchInterface::TYPE_CUSTOM ? 'N/A' : $patch->getId(); $title = chunk_split($patch->getTitle(), 60, PHP_EOL); diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index d013aa47..ad5ae6a2 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -284,8 +284,9 @@ private function getPatchCategories(array $patches): array */ private function printDetailsInfo(OutputInterface $output) { - $supportUrl = 'https://support.magento.com/'; - $releaseNotesUrl = 'https://devdocs.magento.com/quality-patches/release-notes.html'; + // phpcs:ignore + $releaseNotesUrl = 'https://experienceleague.adobe.com/docs/commerce-operations/tools/quality-patches-tool/release-notes.html'; + $supportUrl = 'https://experienceleague.adobe.com/tools/commerce-quality-patches/index.html'; $output->writeln( '<info>Patch details you can find on </info>' . diff --git a/src/Patch/AggregatedPatchFactory.php b/src/Patch/AggregatedPatchFactory.php index fcc0bd7d..d58495ca 100644 --- a/src/Patch/AggregatedPatchFactory.php +++ b/src/Patch/AggregatedPatchFactory.php @@ -36,6 +36,7 @@ public function create( $require = $this->getRequire($items); $replacedWith = $this->getReplacedWith($items); $isDeprecated = $this->isDeprecated($items); + $requirements = $this->getRequirements($items); return new AggregatedPatch( $id, @@ -47,7 +48,8 @@ public function create( $require, $replacedWith, $isDeprecated, - $items + $items, + $requirements ); } @@ -189,4 +191,17 @@ private function isDeprecated(array $patches): bool return false; } + + /** + * Returns aggregated patch requirements. + * + * @param PatchInterface[] $patches + * @return string + */ + private function getRequirements(array $patches): string + { + $patch = reset($patches); + + return $patch->getRequirements(); + } } diff --git a/src/Patch/Collector/SupportCollector.php b/src/Patch/Collector/SupportCollector.php index 16d17dd9..91d1345b 100644 --- a/src/Patch/Collector/SupportCollector.php +++ b/src/Patch/Collector/SupportCollector.php @@ -55,6 +55,8 @@ class SupportCollector implements CollectorInterface const ORIGIN = 'Adobe Commerce Support'; + const PROP_REQUIREMENTS = 'requirements'; + /** * @var Package */ @@ -113,6 +115,7 @@ public function collect(): array $category = !empty($patchGeneralConfig[self::PROP_CATEGORIES]) ? array_map('trim', $patchGeneralConfig[self::PROP_CATEGORIES]) : ['Other']; + $patchRequirements = $patchGeneralConfig[self::PROP_REQUIREMENTS] ?? ''; if ($this->package->matchConstraint($packageName, $packageConstraint)) { $result[] = $this->createPatch( @@ -124,7 +127,8 @@ public function collect(): array $packageConstraint, $patchRequire, $patchReplacedWith, - $patchDeprecated + $patchDeprecated, + $patchRequirements ); } } @@ -145,9 +149,11 @@ public function collect(): array * @param array $patchRequire * @param string $patchReplacedWith * @param bool $patchDeprecated + * @param string $patchRequirements * * @return PatchInterface * @throws CollectorException + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ private function createPatch( string $patchId, @@ -158,7 +164,8 @@ private function createPatch( string $packageConstraint, array $patchRequire, string $patchReplacedWith, - bool $patchDeprecated + bool $patchDeprecated, + string $patchRequirements ): PatchInterface { try { $patchPath = $this->qualityPackage->getPatchesDirectoryPath() . '/' . $patchFile; @@ -174,6 +181,7 @@ private function createPatch( $this->patchBuilder->setRequire($patchRequire); $this->patchBuilder->setReplacedWith($patchReplacedWith); $this->patchBuilder->setDeprecated($patchDeprecated); + $this->patchBuilder->setRequirements($patchRequirements); $patch = $this->patchBuilder->build(); } catch (PatchIntegrityException $e) { throw new CollectorException($e->getMessage(), $e->getCode(), $e); diff --git a/src/Patch/Data/AggregatedPatch.php b/src/Patch/Data/AggregatedPatch.php index f4d0bea7..c80e4ce6 100644 --- a/src/Patch/Data/AggregatedPatch.php +++ b/src/Patch/Data/AggregatedPatch.php @@ -62,6 +62,11 @@ class AggregatedPatch implements AggregatedPatchInterface */ private $origin; + /** + * @var string + */ + private $requirements; + /** * @param string $id * @param string $type @@ -85,7 +90,8 @@ public function __construct( array $require, string $replacedWith, bool $isDeprecated, - array $items + array $items, + string $requirements ) { $this->id = $id; @@ -98,6 +104,7 @@ public function __construct( $this->replacedWith = $replacedWith; $this->isDeprecated = $isDeprecated; $this->items = $items; + $this->requirements = $requirements; } /** @@ -187,4 +194,11 @@ public function getItems(): array { return $this->items; } + /** + * @inheritDoc + */ + public function getRequirements(): string + { + return $this->requirements; + } } diff --git a/src/Patch/Data/AggregatedPatchInterface.php b/src/Patch/Data/AggregatedPatchInterface.php index 8f65cb42..f58db7b9 100644 --- a/src/Patch/Data/AggregatedPatchInterface.php +++ b/src/Patch/Data/AggregatedPatchInterface.php @@ -83,4 +83,11 @@ public function isDeprecated(): bool; * @return PatchInterface[] */ public function getItems(): array; + + /** + * Patch requirements. + * + * @return string + */ + public function getRequirements(): string; } diff --git a/src/Patch/Data/Patch.php b/src/Patch/Data/Patch.php index a35966ea..01246891 100644 --- a/src/Patch/Data/Patch.php +++ b/src/Patch/Data/Patch.php @@ -76,6 +76,11 @@ class Patch implements PatchInterface */ private $origin; + /** + * @var string + */ + private $requirements; + /** * @param string $id * @param string $type @@ -90,6 +95,7 @@ class Patch implements PatchInterface * @param string[] $require * @param string $replacedWith * @param bool $isDeprecated + * @param string $requirements * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -106,7 +112,8 @@ public function __construct( array $affectedComponents, array $require, string $replacedWith, - bool $isDeprecated + bool $isDeprecated, + string $requirements ) { $this->id = $id; @@ -122,6 +129,7 @@ public function __construct( $this->require = $require; $this->replacedWith = $replacedWith; $this->isDeprecated = $isDeprecated; + $this->requirements = $requirements; } /** @@ -235,4 +243,12 @@ public function isDeprecated(): bool { return $this->isDeprecated; } + + /** + * @inheritDoc + */ + public function getRequirements(): string + { + return $this->requirements; + } } diff --git a/src/Patch/Data/PatchInterface.php b/src/Patch/Data/PatchInterface.php index 6d5d7cd1..3955311e 100644 --- a/src/Patch/Data/PatchInterface.php +++ b/src/Patch/Data/PatchInterface.php @@ -119,4 +119,11 @@ public function getReplacedWith(): string; * @return bool */ public function isDeprecated(): bool; + + /** + * Patch requirements. + * + * @return string + */ + public function getRequirements(): string; } diff --git a/src/Patch/PatchBuilder.php b/src/Patch/PatchBuilder.php index cbfd2d2d..2babc8af 100644 --- a/src/Patch/PatchBuilder.php +++ b/src/Patch/PatchBuilder.php @@ -84,6 +84,11 @@ class PatchBuilder */ private $filesystem; + /** + * @var string + */ + private $requirements; + /** * @param Filesystem $filesystem */ @@ -226,6 +231,17 @@ public function setDeprecated(bool $deprecated) $this->deprecated = $deprecated; } + /** + * Sets patch requirements. + * + * @param string $requirements + * @return void + */ + public function setRequirements(string $requirements) + { + $this->requirements = $requirements; + } + /** * Builds patch data object. * @@ -250,7 +266,8 @@ public function build() $components, $this->require, $this->replacedWith, - $this->deprecated + $this->deprecated, + $this->requirements ); } diff --git a/src/Test/Unit/Patch/PatchBuilderTest.php b/src/Test/Unit/Patch/PatchBuilderTest.php index 908f88dd..b607c10a 100644 --- a/src/Test/Unit/Patch/PatchBuilderTest.php +++ b/src/Test/Unit/Patch/PatchBuilderTest.php @@ -57,7 +57,8 @@ public function testBuild() 'packageConstraint' => '2.3.5', 'require' => ['MC-2'], 'replacedWith' => 'MC-3', - 'deprecated' => true + 'deprecated' => true, + 'requirements'=> 'Some requirements' ]; $patchContent = file_get_contents(__DIR__ . '/Fixture/MC-1__testfixture__1.1.patch'); @@ -76,6 +77,7 @@ public function testBuild() $this->assertEquals($patch->getRequire(), $patchData['require']); $this->assertEquals($patch->getReplacedWith(), $patchData['replacedWith']); $this->assertEquals($patch->isDeprecated(), $patchData['deprecated']); + $this->assertEquals($patch->getRequirements(), $patchData['requirements']); $this->assertEquals( ['magento/framework', 'magento/module-email', 'setup/src'], $patch->getAffectedComponents() @@ -99,7 +101,8 @@ public function testBuildWithException() 'packageConstraint' => '2.3.5', 'require' => ['MC-2'], 'replacedWith' => 'MC-3', - 'deprecated' => true + 'deprecated' => true, + 'requirements'=> 'Some requirements' ]; $this->filesystem->method('get') @@ -129,6 +132,7 @@ private function buildPatch(array $patchData): PatchInterface $this->patchBuilder->setRequire($patchData['require']); $this->patchBuilder->setReplacedWith($patchData['replacedWith']); $this->patchBuilder->setDeprecated($patchData['deprecated']); + $this->patchBuilder->setRequirements($patchData['requirements']); return $this->patchBuilder->build(); } From 4be72873e9bd6881cf332431c48d2c5e060838cf Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Thu, 25 May 2023 19:20:26 -0500 Subject: [PATCH 134/157] ACP2E-1963: Add warning to the QPT CLI wizard/output in case patch has dependencies --- src/Patch/Data/Patch.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Patch/Data/Patch.php b/src/Patch/Data/Patch.php index 01246891..5e1deb43 100644 --- a/src/Patch/Data/Patch.php +++ b/src/Patch/Data/Patch.php @@ -79,7 +79,7 @@ class Patch implements PatchInterface /** * @var string */ - private $requirements; + private $requirements = ''; /** * @param string $id @@ -113,7 +113,7 @@ public function __construct( array $require, string $replacedWith, bool $isDeprecated, - string $requirements + string $requirements = '' ) { $this->id = $id; From a75ef2ad8e7294563b5723af25a6eef0bb312902 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <tymchyns@adobe.com> Date: Thu, 25 May 2023 21:29:29 -0500 Subject: [PATCH 135/157] ACP2E-1963: Add warning to the QPT CLI wizard/output in case patch has dependencies --- src/Patch/Data/AggregatedPatch.php | 4 ++-- src/Patch/PatchBuilder.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Patch/Data/AggregatedPatch.php b/src/Patch/Data/AggregatedPatch.php index c80e4ce6..883ec490 100644 --- a/src/Patch/Data/AggregatedPatch.php +++ b/src/Patch/Data/AggregatedPatch.php @@ -65,7 +65,7 @@ class AggregatedPatch implements AggregatedPatchInterface /** * @var string */ - private $requirements; + private $requirements = ''; /** * @param string $id @@ -91,7 +91,7 @@ public function __construct( string $replacedWith, bool $isDeprecated, array $items, - string $requirements + string $requirements = '' ) { $this->id = $id; diff --git a/src/Patch/PatchBuilder.php b/src/Patch/PatchBuilder.php index 2babc8af..b7bc9e2d 100644 --- a/src/Patch/PatchBuilder.php +++ b/src/Patch/PatchBuilder.php @@ -87,7 +87,7 @@ class PatchBuilder /** * @var string */ - private $requirements; + private $requirements = ''; /** * @param Filesystem $filesystem From 81c36518d4c06345a613e015a8a653af1dd38efe Mon Sep 17 00:00:00 2001 From: Andrii Poltoratskyi <poltorat@adobe.com> Date: Tue, 30 May 2023 11:18:41 -0700 Subject: [PATCH 136/157] MCLOUD-10604: Added ACSD-51892 patch --- patches.json | 3 + ...ound_deployment_configuration__2.4.6.patch | 134 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch diff --git a/patches.json b/patches.json index 63300fd1..5710cea7 100644 --- a/patches.json +++ b/patches.json @@ -271,6 +271,9 @@ "Fixes the error 'The file can't be deleted. Warning!unlink: No such file or directory' when flushing JS/CSS cache from the Admin": { ">=2.4.0 <2.4.1-p1": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch", ">=2.4.1-p1 <2.4.7": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch" + }, + "Reduced the number of times the same deployment configurations load": { + ">=2.4.6 <2.4.7": "MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch b/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch new file mode 100644 index 00000000..15fb7045 --- /dev/null +++ b/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch @@ -0,0 +1,134 @@ +diff -Nuar a/vendor/magento/framework/App/DeploymentConfig.php b/vendor/magento/framework/App/DeploymentConfig.php +index 6713baa3a1d..64f32d5516b 100644 +--- a/vendor/magento/framework/App/DeploymentConfig.php ++++ b/vendor/magento/framework/App/DeploymentConfig.php +@@ -51,6 +51,16 @@ class DeploymentConfig + */ + private $overrideData; + ++ /** ++ * @var array ++ */ ++ private $envOverrides = []; ++ ++ /** ++ * @var array ++ */ ++ private $readerLoad = []; ++ + /** + * Constructor + * +@@ -84,7 +94,9 @@ class DeploymentConfig + } + $result = $this->getByKey($key); + if ($result === null) { +- $this->reloadData(); ++ if (empty($this->flatData) || count($this->getAllEnvOverrides())) { ++ $this->reloadData(); ++ } + $result = $this->getByKey($key); + } + return $result ?? $defaultValue; +@@ -114,13 +126,13 @@ class DeploymentConfig + { + if ($key === null) { + if (empty($this->data)) { +- $this->reloadData(); ++ $this->reloadInitialData(); + } + return $this->data; + } + $result = $this->getConfigDataByKey($key); + if ($result === null) { +- $this->reloadData(); ++ $this->reloadInitialData(); + $result = $this->getConfigDataByKey($key); + } + return $result; +@@ -170,28 +182,55 @@ class DeploymentConfig + * @throws FileSystemException + * @throws RuntimeException + */ +- private function reloadData(): void ++ private function reloadInitialData(): void + { ++ if (empty($this->readerLoad) || empty($this->data) || empty($this->flatData)) { ++ $this->readerLoad = $this->reader->load(); ++ } + $this->data = array_replace( +- $this->reader->load(), ++ $this->readerLoad, + $this->overrideData ?? [], + $this->getEnvOverride() + ); ++ } ++ ++ /** ++ * Loads the configuration data ++ * ++ * @return void ++ * @throws FileSystemException ++ * @throws RuntimeException ++ */ ++ private function reloadData(): void ++ { ++ $this->reloadInitialData(); + // flatten data for config retrieval using get() + $this->flatData = $this->flattenParams($this->data); ++ $this->flatData = $this->getAllEnvOverrides() + $this->flatData; ++ } + +- // allow reading values from env variables by convention +- // MAGENTO_DC_{path}, like db/connection/default/host => +- // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST +- foreach (getenv() as $key => $value) { +- if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) +- && $key !== self::OVERRIDE_KEY +- ) { +- // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host +- $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); +- $this->flatData[$flatKey] = $value; ++ /** ++ * Load all getenv() configs once ++ * ++ * @return array ++ */ ++ private function getAllEnvOverrides(): array ++ { ++ if (empty($this->envOverrides)) { ++ // allow reading values from env variables by convention ++ // MAGENTO_DC_{path}, like db/connection/default/host => ++ // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST ++ foreach (getenv() as $key => $value) { ++ if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) ++ && $key !== self::OVERRIDE_KEY ++ ) { ++ // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host ++ $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); ++ $this->envOverrides[$flatKey] = $value; ++ } + } + } ++ return $this->envOverrides; + } + + /** +diff -Nuar a/vendor/magento/framework/Module/ModuleList.php b/vendor/magento/framework/Module/ModuleList.php +index b3cf433bbaf..32e2d2b1550 100644 +--- a/vendor/magento/framework/Module/ModuleList.php ++++ b/vendor/magento/framework/Module/ModuleList.php +@@ -140,8 +140,11 @@ class ModuleList implements ModuleListInterface + */ + private function loadConfigData() + { +- if (null === $this->configData && null !== $this->config->get(ConfigOptionsListConstants::KEY_MODULES)) { +- $this->configData = $this->config->get(ConfigOptionsListConstants::KEY_MODULES); ++ if (null === $this->configData) { ++ $config = $this->config->get(ConfigOptionsListConstants::KEY_MODULES); ++ if (null !== $config) { ++ $this->configData = $config; ++ } + } + } + } \ No newline at end of file From 51402859250e91ba317092ebefa88e39a70daab1 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Thu, 1 Jun 2023 14:41:54 -0500 Subject: [PATCH 137/157] MCLOUD-10226: Fix regexp cache tag validation --- patches.json | 5 ++ ...x_regexp_cache_tag_validation__2.4.6.patch | 46 +++++++++++++++++++ .../Functional/Acceptance/AcceptanceCest.php | 3 +- 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 patches/MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch diff --git a/patches.json b/patches.json index 63300fd1..68e0a85d 100644 --- a/patches.json +++ b/patches.json @@ -396,5 +396,10 @@ ">=2.3.4-p2 <2.3.7-p3 || >=2.4.0 <2.4.3": "MDVA-43443__parser_token_new_fix__2.3.4-p2.patch", ">=2.4.3 <2.4.3-p2": "MDVA-43443__parser_token_new_fix__2.4.3.patch" } + }, + "magento/framework": { + "Fix regexp cache tag validation": { + ">=103.0.6 <103.0.7": "MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch" + } } } diff --git a/patches/MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch b/patches/MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch new file mode 100644 index 00000000..9f29d05a --- /dev/null +++ b/patches/MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch @@ -0,0 +1,46 @@ +diff -Nuar a/lib/internal/Magento/Framework/Cache/Core.php b/lib/internal/Magento/Framework/Cache/Core.php +--- a/lib/internal/Magento/Framework/Cache/Core.php ++++ b/lib/internal/Magento/Framework/Cache/Core.php +@@ -5,6 +5,10 @@ + */ + namespace Magento\Framework\Cache; + ++use Magento\Framework\Cache\Backend\Redis; ++use Zend_Cache; ++use Zend_Cache_Exception; ++ + class Core extends \Zend_Cache_Core + { + /** +@@ -126,6 +130,34 @@ public function getIdsNotMatchingTags($tags = []) + return parent::getIdsNotMatchingTags($tags); + } + ++ /** ++ * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...) ++ * ++ * Throw an exception if a problem is found ++ * ++ * @param string $string Cache id or tag ++ * @throws Zend_Cache_Exception ++ * @return void ++ */ ++ protected function _validateIdOrTag($string) ++ { ++ if ($this->_backend instanceof Redis) { ++ if (!is_string($string)) { ++ Zend_Cache::throwException('Invalid id or tag : must be a string'); ++ } ++ if (substr($string, 0, 9) == 'internal-') { ++ Zend_Cache::throwException('"internal-*" ids or tags are reserved'); ++ } ++ if (!preg_match('~^[a-zA-Z0-9_{}]+$~D', $string)) { ++ Zend_Cache::throwException("Invalid id or tag '$string' : must use only [a-zA-Z0-9_{}]"); ++ } ++ ++ return; ++ } ++ ++ parent::_validateIdOrTag($string); ++ } ++ diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 762775fa..139dbf26 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -49,7 +49,8 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void protected function patchesDataProvider(): array { return [ - ['templateVersion' => '2.4.6', 'magentoVersion' => null], + ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6'], + ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6-p1'], ]; } } From 440222a2c1659dfcfdc76c3e1e79441c4a1a2cab Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 6 Jun 2023 14:28:19 -0500 Subject: [PATCH 138/157] MCLOUD-10226: Fix regexp cache tag validation --- ...x_regexp_cache_tag_validation__2.4.6.patch | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/patches/MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch b/patches/MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch index 9f29d05a..c6777a63 100644 --- a/patches/MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch +++ b/patches/MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch @@ -1,7 +1,7 @@ -diff -Nuar a/lib/internal/Magento/Framework/Cache/Core.php b/lib/internal/Magento/Framework/Cache/Core.php ---- a/lib/internal/Magento/Framework/Cache/Core.php -+++ b/lib/internal/Magento/Framework/Cache/Core.php -@@ -5,6 +5,10 @@ +diff -Naur a/vendor/magento/framework/Cache/Core.php b/vendor/magento/framework/Cache/Core.php +--- a/vendor/magento/framework/Cache/Core.php 2023-02-23 14:11:04 ++++ b/vendor/magento/framework/Cache/Core.php 2023-04-14 11:54:58 +@@ -5,6 +5,10 @@ namespace Magento\Framework\Cache; */ namespace Magento\Framework\Cache; @@ -12,10 +12,12 @@ diff -Nuar a/lib/internal/Magento/Framework/Cache/Core.php b/lib/internal/Magent class Core extends \Zend_Cache_Core { /** -@@ -126,6 +130,34 @@ public function getIdsNotMatchingTags($tags = []) +@@ -124,6 +128,34 @@ class Core extends \Zend_Cache_Core + { + $tags = $this->_tags($tags); return parent::getIdsNotMatchingTags($tags); - } - ++ } ++ + /** + * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...) + * @@ -42,5 +44,6 @@ diff -Nuar a/lib/internal/Magento/Framework/Cache/Core.php b/lib/internal/Magent + } + + parent::_validateIdOrTag($string); -+ } -+ + } + + /** From 1cc7c09c1d8e0dce0f102a08046a5f8da4ff379e Mon Sep 17 00:00:00 2001 From: Andrii Poltoratskyi <poltorat@adobe.com> Date: Mon, 12 Jun 2023 09:39:51 -0700 Subject: [PATCH 139/157] MCLOUD-10604: Added an empty line --- ...nce_degradation_around_deployment_configuration__2.4.6.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch b/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch index 15fb7045..0f638652 100644 --- a/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch +++ b/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch @@ -131,4 +131,4 @@ index b3cf433bbaf..32e2d2b1550 100644 + } } } - } \ No newline at end of file + } From 9ab92390a8cdabeda177c8af68202ed496ca0335 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Thu, 15 Jun 2023 10:10:01 -0500 Subject: [PATCH 140/157] MCLOUD-10674: Release MCP 1.0.22 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3ab197f4..d5c7a7b8 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.21", + "version": "1.0.22", "license": "OSL-3.0", "repositories": { "repo.magento.com": { From 36d0994b34dea2776a47e24e661bb4a6299082a9 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Thu, 15 Jun 2023 10:13:27 -0500 Subject: [PATCH 141/157] MCLOUD-10674: Update FT --- src/Test/Functional/Acceptance/Acceptance81Cest.php | 1 + src/Test/Functional/Acceptance/AcceptanceCest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Test/Functional/Acceptance/Acceptance81Cest.php b/src/Test/Functional/Acceptance/Acceptance81Cest.php index ac0f7029..49ffd695 100644 --- a/src/Test/Functional/Acceptance/Acceptance81Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance81Cest.php @@ -22,6 +22,7 @@ protected function patchesDataProvider(): array ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p2'], ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p3'], + ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p4'], ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5-p1'], ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5-p2'], diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 139dbf26..79cd3217 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -51,6 +51,7 @@ protected function patchesDataProvider(): array return [ ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6'], ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6-p1'], + ['templateVersion' => '2.4.7-beta', 'magentoVersion' => null], ]; } } From c77e7d06ba4cfd7023b5aca434e7bcd9411fe6f1 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorbivskyi <gorbivsk@adobe.com> Date: Mon, 26 Jun 2023 13:24:21 -0700 Subject: [PATCH 142/157] MCLOUD-10736: Revert MCLOUD-10604 patch --- patches.json | 3 - ...ound_deployment_configuration__2.4.6.patch | 134 ------------------ 2 files changed, 137 deletions(-) delete mode 100644 patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch diff --git a/patches.json b/patches.json index c5044ac4..68e0a85d 100644 --- a/patches.json +++ b/patches.json @@ -271,9 +271,6 @@ "Fixes the error 'The file can't be deleted. Warning!unlink: No such file or directory' when flushing JS/CSS cache from the Admin": { ">=2.4.0 <2.4.1-p1": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch", ">=2.4.1-p1 <2.4.7": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch" - }, - "Reduced the number of times the same deployment configurations load": { - ">=2.4.6 <2.4.7": "MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch b/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch deleted file mode 100644 index 0f638652..00000000 --- a/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch +++ /dev/null @@ -1,134 +0,0 @@ -diff -Nuar a/vendor/magento/framework/App/DeploymentConfig.php b/vendor/magento/framework/App/DeploymentConfig.php -index 6713baa3a1d..64f32d5516b 100644 ---- a/vendor/magento/framework/App/DeploymentConfig.php -+++ b/vendor/magento/framework/App/DeploymentConfig.php -@@ -51,6 +51,16 @@ class DeploymentConfig - */ - private $overrideData; - -+ /** -+ * @var array -+ */ -+ private $envOverrides = []; -+ -+ /** -+ * @var array -+ */ -+ private $readerLoad = []; -+ - /** - * Constructor - * -@@ -84,7 +94,9 @@ class DeploymentConfig - } - $result = $this->getByKey($key); - if ($result === null) { -- $this->reloadData(); -+ if (empty($this->flatData) || count($this->getAllEnvOverrides())) { -+ $this->reloadData(); -+ } - $result = $this->getByKey($key); - } - return $result ?? $defaultValue; -@@ -114,13 +126,13 @@ class DeploymentConfig - { - if ($key === null) { - if (empty($this->data)) { -- $this->reloadData(); -+ $this->reloadInitialData(); - } - return $this->data; - } - $result = $this->getConfigDataByKey($key); - if ($result === null) { -- $this->reloadData(); -+ $this->reloadInitialData(); - $result = $this->getConfigDataByKey($key); - } - return $result; -@@ -170,28 +182,55 @@ class DeploymentConfig - * @throws FileSystemException - * @throws RuntimeException - */ -- private function reloadData(): void -+ private function reloadInitialData(): void - { -+ if (empty($this->readerLoad) || empty($this->data) || empty($this->flatData)) { -+ $this->readerLoad = $this->reader->load(); -+ } - $this->data = array_replace( -- $this->reader->load(), -+ $this->readerLoad, - $this->overrideData ?? [], - $this->getEnvOverride() - ); -+ } -+ -+ /** -+ * Loads the configuration data -+ * -+ * @return void -+ * @throws FileSystemException -+ * @throws RuntimeException -+ */ -+ private function reloadData(): void -+ { -+ $this->reloadInitialData(); - // flatten data for config retrieval using get() - $this->flatData = $this->flattenParams($this->data); -+ $this->flatData = $this->getAllEnvOverrides() + $this->flatData; -+ } - -- // allow reading values from env variables by convention -- // MAGENTO_DC_{path}, like db/connection/default/host => -- // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST -- foreach (getenv() as $key => $value) { -- if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) -- && $key !== self::OVERRIDE_KEY -- ) { -- // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host -- $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); -- $this->flatData[$flatKey] = $value; -+ /** -+ * Load all getenv() configs once -+ * -+ * @return array -+ */ -+ private function getAllEnvOverrides(): array -+ { -+ if (empty($this->envOverrides)) { -+ // allow reading values from env variables by convention -+ // MAGENTO_DC_{path}, like db/connection/default/host => -+ // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST -+ foreach (getenv() as $key => $value) { -+ if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) -+ && $key !== self::OVERRIDE_KEY -+ ) { -+ // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host -+ $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); -+ $this->envOverrides[$flatKey] = $value; -+ } - } - } -+ return $this->envOverrides; - } - - /** -diff -Nuar a/vendor/magento/framework/Module/ModuleList.php b/vendor/magento/framework/Module/ModuleList.php -index b3cf433bbaf..32e2d2b1550 100644 ---- a/vendor/magento/framework/Module/ModuleList.php -+++ b/vendor/magento/framework/Module/ModuleList.php -@@ -140,8 +140,11 @@ class ModuleList implements ModuleListInterface - */ - private function loadConfigData() - { -- if (null === $this->configData && null !== $this->config->get(ConfigOptionsListConstants::KEY_MODULES)) { -- $this->configData = $this->config->get(ConfigOptionsListConstants::KEY_MODULES); -+ if (null === $this->configData) { -+ $config = $this->config->get(ConfigOptionsListConstants::KEY_MODULES); -+ if (null !== $config) { -+ $this->configData = $config; -+ } - } - } - } From 8a66555143d69cf11decbe93ea0bb4b34baa81f4 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 25 Jul 2023 17:31:01 -0500 Subject: [PATCH 143/157] MCLOUD-10863: Release MCP 1.0.23 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d5c7a7b8..cbe82619 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.22", + "version": "1.0.23", "license": "OSL-3.0", "repositories": { "repo.magento.com": { From bc36351f351d48fa9e4b46a5394293fb305d7dd0 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorbivskyi <gorbivsk@adobe.com> Date: Fri, 8 Sep 2023 14:23:30 -0700 Subject: [PATCH 144/157] MCLOUD-11143: Return MCLOUD-10604 patch back with updated constraints --- patches.json | 3 + ...ound_deployment_configuration__2.4.6.patch | 134 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch diff --git a/patches.json b/patches.json index 68e0a85d..76969c61 100644 --- a/patches.json +++ b/patches.json @@ -271,6 +271,9 @@ "Fixes the error 'The file can't be deleted. Warning!unlink: No such file or directory' when flushing JS/CSS cache from the Admin": { ">=2.4.0 <2.4.1-p1": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.0.patch", ">=2.4.1-p1 <2.4.7": "MCLOUD-10279__errors_when_flushing_js_css_cache_from_admin__2.4.4.patch" + }, + "Reduced the number of times the same deployment configurations load": { + ">=2.4.6 <2.4.6-p2": "MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch b/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch new file mode 100644 index 00000000..0f638652 --- /dev/null +++ b/patches/MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch @@ -0,0 +1,134 @@ +diff -Nuar a/vendor/magento/framework/App/DeploymentConfig.php b/vendor/magento/framework/App/DeploymentConfig.php +index 6713baa3a1d..64f32d5516b 100644 +--- a/vendor/magento/framework/App/DeploymentConfig.php ++++ b/vendor/magento/framework/App/DeploymentConfig.php +@@ -51,6 +51,16 @@ class DeploymentConfig + */ + private $overrideData; + ++ /** ++ * @var array ++ */ ++ private $envOverrides = []; ++ ++ /** ++ * @var array ++ */ ++ private $readerLoad = []; ++ + /** + * Constructor + * +@@ -84,7 +94,9 @@ class DeploymentConfig + } + $result = $this->getByKey($key); + if ($result === null) { +- $this->reloadData(); ++ if (empty($this->flatData) || count($this->getAllEnvOverrides())) { ++ $this->reloadData(); ++ } + $result = $this->getByKey($key); + } + return $result ?? $defaultValue; +@@ -114,13 +126,13 @@ class DeploymentConfig + { + if ($key === null) { + if (empty($this->data)) { +- $this->reloadData(); ++ $this->reloadInitialData(); + } + return $this->data; + } + $result = $this->getConfigDataByKey($key); + if ($result === null) { +- $this->reloadData(); ++ $this->reloadInitialData(); + $result = $this->getConfigDataByKey($key); + } + return $result; +@@ -170,28 +182,55 @@ class DeploymentConfig + * @throws FileSystemException + * @throws RuntimeException + */ +- private function reloadData(): void ++ private function reloadInitialData(): void + { ++ if (empty($this->readerLoad) || empty($this->data) || empty($this->flatData)) { ++ $this->readerLoad = $this->reader->load(); ++ } + $this->data = array_replace( +- $this->reader->load(), ++ $this->readerLoad, + $this->overrideData ?? [], + $this->getEnvOverride() + ); ++ } ++ ++ /** ++ * Loads the configuration data ++ * ++ * @return void ++ * @throws FileSystemException ++ * @throws RuntimeException ++ */ ++ private function reloadData(): void ++ { ++ $this->reloadInitialData(); + // flatten data for config retrieval using get() + $this->flatData = $this->flattenParams($this->data); ++ $this->flatData = $this->getAllEnvOverrides() + $this->flatData; ++ } + +- // allow reading values from env variables by convention +- // MAGENTO_DC_{path}, like db/connection/default/host => +- // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST +- foreach (getenv() as $key => $value) { +- if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) +- && $key !== self::OVERRIDE_KEY +- ) { +- // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host +- $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); +- $this->flatData[$flatKey] = $value; ++ /** ++ * Load all getenv() configs once ++ * ++ * @return array ++ */ ++ private function getAllEnvOverrides(): array ++ { ++ if (empty($this->envOverrides)) { ++ // allow reading values from env variables by convention ++ // MAGENTO_DC_{path}, like db/connection/default/host => ++ // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST ++ foreach (getenv() as $key => $value) { ++ if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) ++ && $key !== self::OVERRIDE_KEY ++ ) { ++ // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host ++ $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); ++ $this->envOverrides[$flatKey] = $value; ++ } + } + } ++ return $this->envOverrides; + } + + /** +diff -Nuar a/vendor/magento/framework/Module/ModuleList.php b/vendor/magento/framework/Module/ModuleList.php +index b3cf433bbaf..32e2d2b1550 100644 +--- a/vendor/magento/framework/Module/ModuleList.php ++++ b/vendor/magento/framework/Module/ModuleList.php +@@ -140,8 +140,11 @@ class ModuleList implements ModuleListInterface + */ + private function loadConfigData() + { +- if (null === $this->configData && null !== $this->config->get(ConfigOptionsListConstants::KEY_MODULES)) { +- $this->configData = $this->config->get(ConfigOptionsListConstants::KEY_MODULES); ++ if (null === $this->configData) { ++ $config = $this->config->get(ConfigOptionsListConstants::KEY_MODULES); ++ if (null !== $config) { ++ $this->configData = $config; ++ } + } + } + } From 541d08674f6ab35bcf9c39ec745286c442bf435f Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 12 Sep 2023 09:55:12 -0500 Subject: [PATCH 145/157] Bump version to 1.0.24 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cbe82619..9dcd12fe 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.23", + "version": "1.0.24", "license": "OSL-3.0", "repositories": { "repo.magento.com": { From c79678f70ef501a082601149874f7a9f9a556592 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Wed, 4 Oct 2023 17:22:12 -0500 Subject: [PATCH 146/157] MAGREQ-8989: Add metadata file to active repos --- .github/.metadata.json | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/.metadata.json b/.github/.metadata.json index d0377faf..46baa333 100644 --- a/.github/.metadata.json +++ b/.github/.metadata.json @@ -1,14 +1,14 @@ { - "templateVersion": "0.1", + "templateVersion": "0.2", "product": { "name": "Magento Cloud Patches", "description": "The Magento Cloud Patches package is a set of patches, previously used within the ece-tools package to improve the integration of all Magento versions with Cloud environments and to deliver critical fixes quickly" }, "contacts": { "team": { - "name": "Magic Mountain", - "DL": "Grp-magento-cloud-all", - "slackChannel": "magic_mountain" + "name": "Mystic Mountain", + "DL": "Grp-Mystic-Mountain", + "slackChannel": "#mystic-mountain-team" } }, "ticketTracker": { @@ -17,10 +17,8 @@ }, "securityJiraQueue": { "projectKey": "MAGREQ", - "component": "Magento Cloud Engineering" + "component": "MAGREQ/Magento Cloud Engineering" } }, - "staticScan": { - "enable": false - } + "productionCodeBranches": ["1.0"] } From 486128798237a053a59000d51c6db907b4d958e6 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorbivskyi <gorbivsk@adobe.com> Date: Wed, 18 Oct 2023 15:36:43 -0700 Subject: [PATCH 147/157] MCLOUD-11329: Add ACSD-54799 patch to cloud patches --- patches.json | 3 + ...arily_wait_for_cron_job_locks__2.4.6.patch | 95 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 patches/MCLOUD-11329__missed_jobs_unnecessarily_wait_for_cron_job_locks__2.4.6.patch diff --git a/patches.json b/patches.json index 76969c61..693ee7ff 100644 --- a/patches.json +++ b/patches.json @@ -274,6 +274,9 @@ }, "Reduced the number of times the same deployment configurations load": { ">=2.4.6 <2.4.6-p2": "MCLOUD-10604__performance_degradation_around_deployment_configuration__2.4.6.patch" + }, + "Fixes the issue where missed jobs unnecessarily wait for cron job locks.": { + ">=2.4.4 <2.4.7": "MCLOUD-11329__missed_jobs_unnecessarily_wait_for_cron_job_locks__2.4.6.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-11329__missed_jobs_unnecessarily_wait_for_cron_job_locks__2.4.6.patch b/patches/MCLOUD-11329__missed_jobs_unnecessarily_wait_for_cron_job_locks__2.4.6.patch new file mode 100644 index 00000000..7e07ad01 --- /dev/null +++ b/patches/MCLOUD-11329__missed_jobs_unnecessarily_wait_for_cron_job_locks__2.4.6.patch @@ -0,0 +1,95 @@ +diff --git a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +index a4a11156956d..d58e60ba2cab 100644 +--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php ++++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php +@@ -320,17 +320,11 @@ private function lockGroup(string $groupId, callable $callback): void + * + * @return void + * @throws Exception|Throwable ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) + { + $jobCode = $schedule->getJobCode(); +- $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); +- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; +- if ($scheduledTime < $currentTime - $scheduleLifetime) { +- $schedule->setStatus(Schedule::STATUS_MISSED); +- // phpcs:ignore Magento2.Exceptions.DirectThrow +- throw new Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); +- } + + if (!isset($jobConfig['instance'], $jobConfig['method'])) { + $schedule->setStatus(Schedule::STATUS_ERROR); +@@ -832,7 +826,7 @@ private function processPendingJobs(string $groupId, array $jobsRoot, int $curre + } + + $scheduledTime = strtotime($schedule->getScheduledAt()); +- if ($scheduledTime > $currentTime) { ++ if (!$this->shouldRunJob($schedule, $groupId, $currentTime, (int) $scheduledTime)) { + continue; + } + +@@ -929,4 +923,62 @@ function () use ($scheduleResource, $where) { + $scheduleResource->getConnection() + ); + } ++ ++ /** ++ * Mark job as missed ++ * ++ * @param Schedule $schedule ++ * @return void ++ */ ++ private function markJobAsMissed(Schedule $schedule): void ++ { ++ $jobCode = $schedule->getJobCode(); ++ $scheduleId = $schedule->getId(); ++ $resource = $schedule->getResource(); ++ $connection = $resource->getConnection(); ++ $message = sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt()); ++ $result = $this->retrier->execute( ++ function () use ($resource, $connection, $scheduleId, $message) { ++ return $connection->update( ++ $resource->getTable('cron_schedule'), ++ ['status' => Schedule::STATUS_MISSED, 'messages' => $message], ++ ['schedule_id = ?' => $scheduleId, 'status = ?' => Schedule::STATUS_PENDING] ++ ); ++ }, ++ $connection ++ ); ++ if ($result == 1) { ++ $schedule->setStatus(Schedule::STATUS_MISSED); ++ $schedule->setMessages($message); ++ if ($this->state->getMode() === State::MODE_DEVELOPER) { ++ $this->logger->info($message); ++ } ++ } ++ } ++ ++ /** ++ * Check if job should be run ++ * ++ * @param Schedule $schedule ++ * @param string $groupId ++ * @param int $currentTime ++ * @param int $scheduledTime ++ * @return bool ++ */ ++ private function shouldRunJob(Schedule $schedule, string $groupId, int $currentTime, int $scheduledTime): bool ++ { ++ if ($scheduledTime > $currentTime) { ++ return false; ++ } ++ ++ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); ++ $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; ++ ++ if ($scheduledTime < $currentTime - $scheduleLifetime) { ++ $this->markJobAsMissed($schedule); ++ return false; ++ } ++ ++ return true; ++ } + } From d845f0d0fc8671c21a93151b27972441a9fda4fc Mon Sep 17 00:00:00 2001 From: Oleksandr Gorbivskyi <gorbivsk@adobe.com> Date: Thu, 7 Dec 2023 16:41:40 -0800 Subject: [PATCH 148/157] MCLOUD-11514: Add ACSD-56624 patch to cloud patches --- patches.json | 3 + ...ed_layout_cache_efficiency__2.4.6-p3.patch | 92 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 patches/MCLOUD-11514__enhanced_layout_cache_efficiency__2.4.6-p3.patch diff --git a/patches.json b/patches.json index 693ee7ff..6702cf75 100644 --- a/patches.json +++ b/patches.json @@ -277,6 +277,9 @@ }, "Fixes the issue where missed jobs unnecessarily wait for cron job locks.": { ">=2.4.4 <2.4.7": "MCLOUD-11329__missed_jobs_unnecessarily_wait_for_cron_job_locks__2.4.6.patch" + }, + "Enhanced Layout Cache Efficiency (memory usage reduced)": { + ">=2.4.2 <2.4.7": "MCLOUD-11514__enhanced_layout_cache_efficiency__2.4.6-p3.patch" } }, "magento/module-paypal": { diff --git a/patches/MCLOUD-11514__enhanced_layout_cache_efficiency__2.4.6-p3.patch b/patches/MCLOUD-11514__enhanced_layout_cache_efficiency__2.4.6-p3.patch new file mode 100644 index 00000000..9f994581 --- /dev/null +++ b/patches/MCLOUD-11514__enhanced_layout_cache_efficiency__2.4.6-p3.patch @@ -0,0 +1,92 @@ +diff --git a/vendor/magento/framework/App/Cache/Type/Layout.php b/vendor/magento/framework/App/Cache/Type/Layout.php +index 2ea069a..57b1cb4 100644 +--- a/vendor/magento/framework/App/Cache/Type/Layout.php ++++ b/vendor/magento/framework/App/Cache/Type/Layout.php +@@ -3,6 +3,8 @@ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ ++declare(strict_types=1); ++ + namespace Magento\Framework\App\Cache\Type; + + /** +@@ -11,14 +13,29 @@ namespace Magento\Framework\App\Cache\Type; + class Layout extends \Magento\Framework\Cache\Frontend\Decorator\TagScope + { + /** ++ * Prefix for hash kay and hash data ++ */ ++ public const HASH_PREFIX = 'l:'; ++ ++ /** ++ * Hash type, not used for security, only for uniqueness ++ */ ++ public const HASH_TYPE = 'xxh3'; ++ ++ /** ++ * Data lifetime in milliseconds ++ */ ++ public const DATA_LIFETIME = 86_400_000; // "1 day" milliseconds ++ ++ /** + * Cache type code unique among all cache types + */ +- const TYPE_IDENTIFIER = 'layout'; ++ public const TYPE_IDENTIFIER = 'layout'; + + /** + * Cache tag used to distinguish the cache type from all other cache + */ +- const CACHE_TAG = 'LAYOUT_GENERAL_CACHE_TAG'; ++ public const CACHE_TAG = 'LAYOUT_GENERAL_CACHE_TAG'; + + /** + * @param FrontendPool $cacheFrontendPool +@@ -27,4 +44,33 @@ class Layout extends \Magento\Framework\Cache\Frontend\Decorator\TagScope + { + parent::__construct($cacheFrontendPool->get(self::TYPE_IDENTIFIER), self::CACHE_TAG); + } ++ ++ /** ++ * @inheritDoc ++ */ ++ public function save($data, $identifier, array $tags = [], $lifeTime = null) ++ { ++ $dataHash = hash(self::HASH_TYPE, $data); ++ $identifierForHash = self::HASH_PREFIX . $dataHash; ++ return parent::save($data, $identifierForHash, $tags, self::DATA_LIFETIME) // key is hash of data hash ++ && parent::save(self::HASH_PREFIX . $dataHash, $identifier, $tags, $lifeTime); // store hash of data ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ public function load($identifier) ++ { ++ $data = parent::load($identifier); ++ if ($data === false || $data === null) { ++ return $data; ++ } ++ ++ if (str_starts_with($data, self::HASH_PREFIX)) { ++ // so data stored in other place ++ return parent::load($data); ++ } else { ++ return $data; ++ } ++ } + } +diff --git a/vendor/magento/framework/Cache/Backend/Redis.php b/vendor/magento/framework/Cache/Backend/Redis.php +index 565777d..9527ebc 100644 +--- a/vendor/magento/framework/Cache/Backend/Redis.php ++++ b/vendor/magento/framework/Cache/Backend/Redis.php +@@ -70,7 +70,7 @@ class Redis extends \Cm_Cache_Backend_Redis + * @param bool $specificLifetime + * @return bool + */ +- public function save($data, $id, $tags = [], $specificLifetime = false) ++ public function save($data, $id, $tags = [], $specificLifetime = 86_400_000) + { + try { + $result = parent::save($data, $id, $tags, $specificLifetime); From d0bf3c06f6af9c216a9d3af06fbf75928d3b63e2 Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Fri, 5 Jan 2024 16:47:40 -0600 Subject: [PATCH 149/157] MCLOUD-11514: Add ACSD-56624 patch to cloud patches --- patches.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches.json b/patches.json index 6702cf75..ea81ffbb 100644 --- a/patches.json +++ b/patches.json @@ -279,7 +279,7 @@ ">=2.4.4 <2.4.7": "MCLOUD-11329__missed_jobs_unnecessarily_wait_for_cron_job_locks__2.4.6.patch" }, "Enhanced Layout Cache Efficiency (memory usage reduced)": { - ">=2.4.2 <2.4.7": "MCLOUD-11514__enhanced_layout_cache_efficiency__2.4.6-p3.patch" + ">=2.4.4 <2.4.7": "MCLOUD-11514__enhanced_layout_cache_efficiency__2.4.6-p3.patch" } }, "magento/module-paypal": { From 1fd2478d72511cf5b7b2969114c84bfa6185236d Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 9 Jan 2024 13:51:09 -0600 Subject: [PATCH 150/157] MCLOUD-11596: Release Cloud Tools --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9dcd12fe..6f277b03 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.24", + "version": "1.0.25", "license": "OSL-3.0", "repositories": { "repo.magento.com": { From afb9a185730e8b8da308d5e7892e2eafca24c33e Mon Sep 17 00:00:00 2001 From: Shambhu Kumar <glo72880@adobe.com> Date: Thu, 18 Jan 2024 21:41:04 +0530 Subject: [PATCH 151/157] Added support for PHP8.3 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6f277b03..af828662 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "symfony/process": "^2.1||^4.1||^5.0||^6.0", "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.0||^6.0", "symfony/yaml": "^3.3||^4.0||^5.0||^6.0", - "monolog/monolog": "^1.25||^2.3", + "monolog/monolog": "^1.25||^2.3||^2.7", "magento/quality-patches": "^1.1.0" }, "require-dev": { From 1d6b19f527f8897db4ef650bdc430b67a7b9eb4d Mon Sep 17 00:00:00 2001 From: Bohdan Korablov <korablov@adobe.com> Date: Tue, 20 Feb 2024 10:45:15 -0600 Subject: [PATCH 152/157] MCLOUD-11527: Added support for PHP 8.3 --- src/Application.php | 2 +- src/ApplicationEce.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Application.php b/src/Application.php index 2c5d576b..baac772b 100644 --- a/src/Application.php +++ b/src/Application.php @@ -37,7 +37,7 @@ public function __construct(ContainerInterface $container) /** * @inheritdoc */ - protected function getDefaultCommands() + protected function getDefaultCommands(): array { return array_merge(parent::getDefaultCommands(), [ $this->container->get(Command\Apply::class), diff --git a/src/ApplicationEce.php b/src/ApplicationEce.php index fc6e4c2e..dbb7460e 100644 --- a/src/ApplicationEce.php +++ b/src/ApplicationEce.php @@ -37,7 +37,7 @@ public function __construct(ContainerInterface $container) /** * @inheritdoc */ - protected function getDefaultCommands() + protected function getDefaultCommands(): array { return array_merge(parent::getDefaultCommands(), [ $this->container->get(Command\Ece\Apply::class), From 5814a517973b7f9e3435058f1ad5a157ae9e9308 Mon Sep 17 00:00:00 2001 From: Shambhu Kumar <glo72880@adobe.com> Date: Mon, 18 Mar 2024 21:39:07 +0530 Subject: [PATCH 153/157] Updated testcase --- .../Acceptance/Acceptance82Cest.php | 57 +++++++++++++++++++ .../Functional/Acceptance/AcceptanceCest.php | 6 +- .../Acceptance/PatchApplierCest.php | 4 +- 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/Test/Functional/Acceptance/Acceptance82Cest.php diff --git a/src/Test/Functional/Acceptance/Acceptance82Cest.php b/src/Test/Functional/Acceptance/Acceptance82Cest.php new file mode 100644 index 00000000..79cd3217 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance82Cest.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php82 + */ +class AcceptanceCest extends AbstractCest +{ + /** + * @param \CliTester $I + */ + public function _before(\CliTester $I): void + { + parent::_before($I); + } + + /** + * @param \CliTester $I + * @param \Codeception\Example $data + * @throws \Robo\Exception\TaskException + * @dataProvider patchesDataProvider + */ + public function testPatches(\CliTester $I, \Codeception\Example $data): void + { + $this->prepareTemplate($I, $data['templateVersion'], $data['magentoVersion'] ?? null); + $I->copyFileToWorkDir('files/patches/.apply_quality_patches.env.yaml', '.magento.env.yaml'); + $I->generateDockerCompose(sprintf( + '--mode=production --env-vars="%s"', + $this->convertEnvFromArrayToJson(['MAGENTO_CLOUD_PROJECT' => 'travis-testing']) + )); + $I->assertTrue($I->runDockerComposeCommand('run build cloud-build')); + $I->assertTrue($I->startEnvironment()); + $I->assertTrue($I->runDockerComposeCommand('run deploy cloud-deploy')); + $I->assertTrue($I->runDockerComposeCommand('run deploy cloud-post-deploy')); + $I->amOnPage('/'); + $I->see('Home page'); + $I->see('CMS homepage content goes here.'); + } + + /** + * @return array + */ + protected function patchesDataProvider(): array + { + return [ + ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6'], + ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6-p1'], + ['templateVersion' => '2.4.7-beta', 'magentoVersion' => null], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 79cd3217..cedf47e6 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -8,7 +8,7 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php82 + * @group php83 */ class AcceptanceCest extends AbstractCest { @@ -49,9 +49,7 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void protected function patchesDataProvider(): array { return [ - ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6'], - ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6-p1'], - ['templateVersion' => '2.4.7-beta', 'magentoVersion' => null], + ['templateVersion' => '2.4.7-beta-test', 'magentoVersion' => '2.4.7-beta401'], ]; } } diff --git a/src/Test/Functional/Acceptance/PatchApplierCest.php b/src/Test/Functional/Acceptance/PatchApplierCest.php index 0b5b6e80..abf4508e 100644 --- a/src/Test/Functional/Acceptance/PatchApplierCest.php +++ b/src/Test/Functional/Acceptance/PatchApplierCest.php @@ -10,7 +10,7 @@ use Magento\CloudDocker\Test\Functional\Codeception\Docker; /** - * @group php82 + * @group php83 */ class PatchApplierCest extends AbstractCest { @@ -21,7 +21,7 @@ public function _before(\CliTester $I): void { parent::_before($I); - $this->prepareTemplate($I, '2.4.6'); + $this->prepareTemplate($I, '2.4.7-beta-test'); $I->copyFileToWorkDir('files/debug_logging/.magento.env.yaml', '.magento.env.yaml'); } From 29a1c107e8a477928fc247a2caf5e0f8f3adbca0 Mon Sep 17 00:00:00 2001 From: Sivaram Manijeganathan <sivaramm@adobe.com> Date: Tue, 19 Mar 2024 10:05:41 -0500 Subject: [PATCH 154/157] php 82 test file fix --- .../Acceptance/Acceptance82Cest.php | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/Test/Functional/Acceptance/Acceptance82Cest.php b/src/Test/Functional/Acceptance/Acceptance82Cest.php index 79cd3217..d9ea8d68 100644 --- a/src/Test/Functional/Acceptance/Acceptance82Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance82Cest.php @@ -10,39 +10,8 @@ /** * @group php82 */ -class AcceptanceCest extends AbstractCest +class Acceptance82Cest extends AcceptanceCest { - /** - * @param \CliTester $I - */ - public function _before(\CliTester $I): void - { - parent::_before($I); - } - - /** - * @param \CliTester $I - * @param \Codeception\Example $data - * @throws \Robo\Exception\TaskException - * @dataProvider patchesDataProvider - */ - public function testPatches(\CliTester $I, \Codeception\Example $data): void - { - $this->prepareTemplate($I, $data['templateVersion'], $data['magentoVersion'] ?? null); - $I->copyFileToWorkDir('files/patches/.apply_quality_patches.env.yaml', '.magento.env.yaml'); - $I->generateDockerCompose(sprintf( - '--mode=production --env-vars="%s"', - $this->convertEnvFromArrayToJson(['MAGENTO_CLOUD_PROJECT' => 'travis-testing']) - )); - $I->assertTrue($I->runDockerComposeCommand('run build cloud-build')); - $I->assertTrue($I->startEnvironment()); - $I->assertTrue($I->runDockerComposeCommand('run deploy cloud-deploy')); - $I->assertTrue($I->runDockerComposeCommand('run deploy cloud-post-deploy')); - $I->amOnPage('/'); - $I->see('Home page'); - $I->see('CMS homepage content goes here.'); - } - /** * @return array */ From 1bd13e4c5e38e3e41cb4acf8b1e5042748ab512d Mon Sep 17 00:00:00 2001 From: Sivaram Manijeganathan <sivaramm@adobe.com> Date: Mon, 8 Apr 2024 09:25:11 -0500 Subject: [PATCH 155/157] bump version 1.0.26 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index af828662..a6412cfa 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.25", + "version": "1.0.26", "license": "OSL-3.0", "repositories": { "repo.magento.com": { From e5c3c7e3b289709ff78e0806e4424cfaf581f8a4 Mon Sep 17 00:00:00 2001 From: Shambhu Kumar <83451160+glo72880@users.noreply.github.com> Date: Tue, 21 May 2024 18:03:32 +0530 Subject: [PATCH 156/157] MCP Release 1.0.27 (#99) * update template version * update template * update compatible versions * Use abstract parent cest to fix tests * Add abstract patchesDataProvider signature * phpcs fix for missing doc (#101) --------- Co-authored-by: Sivaram Manijeganathan <sivaramm@adobe.com> Co-authored-by: Andrii Shevtsov <ashevtso@adobe.com> Co-authored-by: Tom Reece <treece@adobe.com> Co-authored-by: Tom Reece <tomreece@gmail.com> --- composer.json | 21 ++++++++-------- .../Acceptance/Acceptance82Cest.php | 2 +- .../Acceptance/Acceptance83Cest.php | 24 +++++++++++++++++++ .../Functional/Acceptance/AcceptanceCest.php | 13 ++++------ .../Acceptance/PatchApplierCest.php | 2 +- 5 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 src/Test/Functional/Acceptance/Acceptance83Cest.php diff --git a/composer.json b/composer.json index a6412cfa..57b22cfe 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "license": "OSL-3.0", "repositories": { "repo.magento.com": { - "type": "composer", - "url": "https://repo.magento.com/" + "type": "composer", + "url": "https://repo.magento.com/" } }, "require": { @@ -18,19 +18,18 @@ "symfony/config": "^3.3||^4.4||^5.0||^6.0", "symfony/console": "^2.6||^4.0||^5.0||^6.0", "symfony/dependency-injection": "^3.3||^4.3||^5.0||^6.0", - "symfony/process": "^2.1||^4.1||^5.0||^6.0", + "symfony/process": "^2.1 || ^4.1 || ^5.1 || ^5.4 || ^6.4", "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.0||^6.0", "symfony/yaml": "^3.3||^4.0||^5.0||^6.0", - "monolog/monolog": "^1.25||^2.3||^2.7", - "magento/quality-patches": "^1.1.0" + "monolog/monolog": "^1.25||^2.3||^2.7" }, "require-dev": { - "codeception/codeception": "^4.1", - "codeception/module-asserts": "^1.2", - "codeception/module-db": "^1.0", - "codeception/module-phpbrowser": "^1.0", - "codeception/module-rest": "^1.2", - "consolidation/robo": "^1.2 || ^2.0", + "codeception/codeception": "^4.1 || ^5.1", + "codeception/module-asserts": "^1.2 || ^3.0", + "codeception/module-db": "^1.0 || ^3.0", + "codeception/module-phpbrowser": "^1.0 || ^3.0", + "codeception/module-rest": "^1.2 || ^3.0", + "consolidation/robo": "^1.2 || ^3.0", "phpmd/phpmd": "@stable", "phpunit/phpunit": "^8.5 || ^9.5", "squizlabs/php_codesniffer": "^3.0" diff --git a/src/Test/Functional/Acceptance/Acceptance82Cest.php b/src/Test/Functional/Acceptance/Acceptance82Cest.php index d9ea8d68..4433514b 100644 --- a/src/Test/Functional/Acceptance/Acceptance82Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance82Cest.php @@ -20,7 +20,7 @@ protected function patchesDataProvider(): array return [ ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6'], ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6-p1'], - ['templateVersion' => '2.4.7-beta', 'magentoVersion' => null], + ['templateVersion' => '2.4.7', 'magentoVersion' => null], ]; } } diff --git a/src/Test/Functional/Acceptance/Acceptance83Cest.php b/src/Test/Functional/Acceptance/Acceptance83Cest.php new file mode 100644 index 00000000..412ad871 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance83Cest.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CloudPatches\Test\Functional\Acceptance; + +/** + * @group php83 + */ +class Acceptance83Cest extends AcceptanceCest +{ + /** + * @return array + */ + protected function patchesDataProvider(): array + { + return [ + ['templateVersion' => '2.4.7', 'magentoVersion' => '2.4.7'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index cedf47e6..070482ac 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -8,9 +8,11 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php83 + * Abstract AcceptanceCest + * + * @abstract */ -class AcceptanceCest extends AbstractCest +abstract class AcceptanceCest extends AbstractCest { /** * @param \CliTester $I @@ -46,10 +48,5 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void /** * @return array */ - protected function patchesDataProvider(): array - { - return [ - ['templateVersion' => '2.4.7-beta-test', 'magentoVersion' => '2.4.7-beta401'], - ]; - } + abstract protected function patchesDataProvider(): array; } diff --git a/src/Test/Functional/Acceptance/PatchApplierCest.php b/src/Test/Functional/Acceptance/PatchApplierCest.php index abf4508e..05a0990e 100644 --- a/src/Test/Functional/Acceptance/PatchApplierCest.php +++ b/src/Test/Functional/Acceptance/PatchApplierCest.php @@ -21,7 +21,7 @@ public function _before(\CliTester $I): void { parent::_before($I); - $this->prepareTemplate($I, '2.4.7-beta-test'); + $this->prepareTemplate($I, '2.4.7'); $I->copyFileToWorkDir('files/debug_logging/.magento.env.yaml', '.magento.env.yaml'); } From e78a1367d18624657f7cf6f53dac197ca0bd207d Mon Sep 17 00:00:00 2001 From: Shambhu Kumar <83451160+glo72880@users.noreply.github.com> Date: Tue, 21 May 2024 19:53:19 +0530 Subject: [PATCH 157/157] fix bump 1.0.27 (#104) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 57b22cfe..05fd2f8f 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.26", + "version": "1.0.27", "license": "OSL-3.0", "repositories": { "repo.magento.com": {