From 059867c3f96750652c80f39e44c442a58c2549ee Mon Sep 17 00:00:00 2001 From: Jeff Veit Date: Tue, 13 Nov 2018 18:52:29 +0000 Subject: [PATCH] Updated all the contrib modules to their latest versions. --- composer.json | 18 +- composer.lock | 2260 +++---- vendor/composer/autoload_files.php | 4 +- vendor/composer/autoload_static.php | 4 +- vendor/composer/installed.json | 305 - .../contrib/admin_toolbar/CHANGELOG.txt | 169 +- .../admin_toolbar/admin_toolbar.info.yml | 8 +- .../admin_toolbar/admin_toolbar.module | 5 +- .../README.txt | 10 - ...admin_toolbar_links_access_filter.info.yml | 6 +- .../admin_toolbar_links_access_filter.module | 17 +- .../composer.json | 38 + .../admin_toolbar_tools/README.txt | 9 - .../admin_toolbar_tools.info.yml | 7 +- .../admin_toolbar_tools.links.menu.yml | 6 + .../admin_toolbar_tools.module | 25 +- .../admin_toolbar_tools.routing.yml | 9 + .../admin_toolbar_tools/composer.json | 39 + .../src/Controller/ToolbarController.php | 63 +- .../AdminToolbarToolsAlterTest.php | 12 +- .../contrib/admin_toolbar/composer.json | 34 +- .../src/Functional}/AdminToolbarAlterTest.php | 12 +- .../advanced_help/src/AdvancedHelpManager.php | 7 +- web/modules/contrib/blazy/blazy.info.yml | 6 +- web/modules/contrib/blazy/blazy.module | 3 +- web/modules/contrib/blazy/blazy.views.inc | 3 +- .../contrib/blazy/blazy_ui/blazy_ui.info.yml | 6 +- .../contrib/blazy/src/BlazyLightbox.php | 5 +- .../blazy/src/Dejavu/BlazyVideoTrait.php | 2 +- .../modules/blazy_test/blazy_test.info.yml | 6 +- web/modules/contrib/block_class/README.txt | 65 +- .../contrib/block_class/block_class.info.yml | 8 +- .../contrib/block_class/block_class.module | 53 +- web/modules/contrib/block_class/composer.json | 65 + .../block_class/src/Tests/BlockClassTest.php | 26 +- .../core.entity_view_mode.node.diff.yml | 1 + web/modules/contrib/diff/diff.info.yml | 6 +- web/modules/contrib/diff/diff.services.yml | 2 + .../Controller/PluginRevisionController.php | 3 +- .../contrib/diff/src/DiffEntityComparison.php | 82 +- .../contrib/diff/src/DiffLayoutBase.php | 16 +- .../diff/src/Form/RevisionOverviewForm.php | 22 +- .../diff/src/Tests/DiffAdminFormsTest.php | 7 +- .../contrib/diff/src/Tests/DiffLocaleTest.php | 11 +- .../diff/src/Tests/DiffPluginEntityTest.php | 6 +- .../diff/src/Tests/DiffPluginFileTest.php | 20 +- .../contrib/diff/src/Tests/DiffPluginTest.php | 20 +- .../diff/src/Tests/DiffPluginVariousTest.php | 16 +- .../DiffRevisionContentModerationTest.php | 104 + .../diff/src/Tests/DiffRevisionTest.php | 85 +- .../diff/src/Tests/DiffViewModeTest.php | 5 +- .../modules/diff_test/diff_test.info.yml | 6 +- .../src/Functional/CoreVersionUiTestTrait.php | 39 + .../tests/src/Functional/NodeAccessTest.php | 71 + web/modules/contrib/draggableviews/README.md | 59 + .../contrib/draggableviews/composer.json | 39 + .../draggableviews/draggableviews.info.yml | 13 +- .../draggableviews/draggableviews.install | 60 +- .../draggableviews/draggableviews.module | 107 +- .../draggableviews/draggableviews.module.rej | 26 + .../draggableviews.permissions.yml | 1 - .../install/node.type.draggableviews_demo.yml | 18 + .../views.view.draggableviews_demo.yml | 519 ++ .../draggableviews_demo.info.yml | 22 + .../draggableviews_demo.install | 6 + .../draggableviews/src/DraggableViews.php | 7 +- .../migrate/destination/DraggableViews.php | 23 +- .../views/field/DraggableViewsField.php | 99 +- .../views/field/DraggableViewsField.php.rej | 19 + .../src/Functional/DraggableviewsTest.php | 123 + web/modules/contrib/entity/.travis.yml | 58 - web/modules/contrib/entity/README.txt | 11 - web/modules/contrib/entity/composer.json | 10 - .../entity/config/schema/entity.schema.yml | 11 - web/modules/contrib/entity/entity.info.yml | 12 - .../contrib/entity/entity.links.action.yml | 2 - .../contrib/entity/entity.links.task.yml | 3 - web/modules/contrib/entity/entity.module | 88 - .../contrib/entity/entity.permissions.yml | 2 - .../contrib/entity/entity.services.yml | 22 - web/modules/contrib/entity/entity.views.inc | 32 - .../EntityDeleteMultipleAccessCheck.php | 87 - .../EntityRevisionRouteAccessChecker.php | 162 - .../entity/src/BundleFieldDefinition.php | 27 - .../src/BundlePlugin/BundlePluginHandler.php | 102 - .../BundlePluginHandlerInterface.php | 37 - .../BundlePlugin/BundlePluginInstaller.php | 94 - .../BundlePluginInstallerInterface.php | 34 - .../BundlePlugin/BundlePluginInterface.php | 24 - .../BundlePluginUninstallValidator.php | 76 - .../Controller/RevisionControllerTrait.php | 192 - .../Controller/RevisionOverviewController.php | 160 - .../RevisionableEntityBundleInterface.php | 14 - .../entity/src/EntityAccessControlHandler.php | 152 - .../entity/src/EntityPermissionProvider.php | 59 - .../src/EntityPermissionProviderBase.php | 231 - .../src/EntityPermissionProviderInterface.php | 23 - .../contrib/entity/src/EntityPermissions.php | 63 - .../contrib/entity/src/EntityViewBuilder.php | 17 - .../entity/src/Form/DeleteMultipleForm.php | 323 - .../entity/src/Form/RevisionRevertForm.php | 167 - .../Form/RevisionableContentEntityForm.php | 164 - .../entity/src/Menu/EntityAddLocalAction.php | 81 - .../EntityCollectionLocalActionProvider.php | 41 - .../EntityLocalActionProviderInterface.php | 23 - .../entity/src/Plugin/Action/DeleteAction.php | 97 - .../Action/Derivative/DeleteActionDeriver.php | 78 - .../Derivative/EntityActionsDeriver.php | 58 - .../Derivative/RevisionsOverviewDeriver.php | 68 - .../RevisionableContentEntityBase.php | 25 - .../src/Routing/AdminHtmlRouteProvider.php | 26 - .../src/Routing/DefaultHtmlRouteProvider.php | 26 - .../Routing/DeleteMultipleRouteProvider.php | 47 - .../src/Routing/RevisionRouteProvider.php | 127 - .../UncacheableEntityAccessControlHandler.php | 138 - .../UncacheableEntityPermissionProvider.php | 138 - .../RevisionOverviewIntegrationTest.php | 51 - ...odule_bundle_plugin_examples_test.info.yml | 13 - .../src/Plugin/BundlePluginTest/Second.php | 31 - .../entity_module_bundle_plugin_test.info.yml | 13 - ...ity_module_bundle_plugin_test.services.yml | 4 - .../src/Annotation/BundlePluginTest.php | 34 - .../src/BundlePluginTestManager.php | 35 - .../src/Entity/EntityTestBundlePlugin.php | 27 - .../BundlePluginTestInterface.php | 12 - .../src/Plugin/BundlePluginTest/First.php | 31 - .../schema/entity_module_test.schema.yml | 16 - .../entity_module_test.info.yml | 10 - .../entity_module_test.links.task.yml | 9 - .../entity_module_test.permissions.yml | 3 - .../src/Entity/EnhancedEntity.php | 89 - .../src/Entity/EnhancedEntityBundle.php | 81 - .../Functional/CollectionRouteAccessTest.php | 77 - .../src/Functional/DeleteMultipleFormTest.php | 81 - .../Functional/Menu/EntityLocalActionTest.php | 44 - .../Functional/RevisionRouteAccessTest.php | 97 - .../tests/src/Kernel/BundlePluginTest.php | 146 - .../tests/src/Kernel/DeleteActionTest.php | 83 - .../tests/src/Kernel/RevisionBasicUITest.php | 192 - .../Unit/EntityAccessControlHandlerTest.php | 257 - .../src/Unit/EntityPermissionProviderTest.php | 177 - ...acheableEntityAccessControlHandlerTest.php | 299 - ...ncacheableEntityPermissionProviderTest.php | 186 - .../config/schema/entity_browser.schema.yml | 6 + .../entity_browser/entity_browser.info.yml | 6 +- .../entity_browser/entity_browser.views.inc | 15 + .../js/entity_browser.entity_reference.js | 15 + .../entity_browser_entity_form.info.yml | 6 +- .../EntityBrowser/Widget/EntityForm.php | 9 + .../entity_browser_entity_form_test.info.yml | 6 +- .../EntityFormWidgetTest.php | 11 + ...splay.node.entity_browser_test.default.yml | 5 + .../example/entity_browser_example.info.yml | 6 +- .../Controllers/EntityBrowserController.php | 10 +- .../src/Entity/EntityBrowser.php | 8 + .../src/EntityBrowserFormInterface.php | 8 + .../src/EntityBrowserInterface.php | 4 +- .../src/Form/EntityBrowserForm.php | 50 +- .../src/Plugin/EntityBrowser/Widget/View.php | 24 +- .../EntityBrowser/WidgetSelector/DropDown.php | 12 +- .../EntityBrowser/WidgetSelector/Tabs.php | 3 + .../EntityReferenceBrowserWidget.php | 48 +- .../Field/FieldWidget/FileBrowserWidget.php | 25 +- .../views/field/SearchApiSelectForm.php | 22 + .../entity_browser/src/Tests/ConfigUITest.php | 4 +- .../contrib/entity_browser/src/WidgetBase.php | 8 + .../entity_browser/src/WidgetInterface.php | 8 + ...display.media.ief_media_bundle.default.yml | 1 + .../entity_browser_ief_test.info.yml | 6 +- ...owser.browser.test_entity_browser_file.yml | 7 + .../entity_browser_test.info.yml | 6 +- .../entity_browser_test.services.yml | 5 + .../src/Cache/Context/DummyCacheContext.php | 36 + .../EntityBrowser/Widget/DummyWidget.php | 16 + ...isplay.paragraph.content_embed.default.yml | 1 + ...ay.paragraph.nested_paragraph.default.yml} | 1 + .../entity_browser_test_paragraphs.info.yml | 6 +- .../EntityBrowserTest.php | 46 +- .../EntityBrowserViewsWidgetTest.php | 15 + .../EntityReferenceWidgetTest.php | 178 +- .../FunctionalJavascript/ImageFieldTest.php | 20 + .../Kernel/Extension/EntityBrowserTest.php | 38 + .../entity_reference_revisions/LICENSE.txt | 339 -- .../entity_reference_revisions.schema.yml | 69 - ...ntity_reference_revisions.views.schema.yml | 20 - .../entity_reference_revisions.info.yml | 14 - .../entity_reference_revisions.module | 220 - .../entity_reference_revisions.views.inc | 73 - .../src/EntityNeedsSaveInterface.php | 17 - .../src/EntityNeedsSaveTrait.php | 39 - .../EntityReferenceRevisionsFieldItemList.php | 125 - ...ntityReferenceRevisionsServiceProvider.php | 35 - .../EntityReferenceRevisionItemNormalizer.php | 43 - .../DataType/EntityReferenceRevisions.php | 123 - .../DataType/EntityRevisionsAdapter.php | 27 - .../MigrateEntityReferenceRevisions.php | 30 - ...ntityReferenceRevisionsEntityFormatter.php | 164 - .../EntityReferenceRevisionsFormatterBase.php | 30 - .../EntityReferenceRevisionsItem.php | 477 -- ...tyReferenceRevisionsAutocompleteWidget.php | 44 - ...tityReferenceRevisionsFieldDiffBuilder.php | 59 - .../destination/EntityReferenceRevisions.php | 153 - .../display/EntityReferenceRevisions.php | 176 - .../views/row/EntityReferenceRevisions.php | 57 - .../views/style/EntityReferenceRevisions.php | 103 - .../EntityReferenceRevisionsAdminTest.php | 184 - ...tityReferenceRevisionsAutocompleteTest.php | 147 - ...ferenceRevisionsCoreVersionUiTestTrait.php | 35 - .../EntityReferenceRevisionsDiffTest.php | 121 - ...EntityReferenceRevisionsNormalizerTest.php | 106 - .../EntityRevisionDataDefinition.php | 50 - ...ntity_composite_relationship_test.info.yml | 14 - ...omposite_relationship_test.permissions.yml | 2 - .../EntityTestCompositeRelationship.php | 60 - .../EntityReferenceRevisionsCompositeTest.php | 407 -- .../EntityReferenceRevisionsFormatterTest.php | 106 - .../EntityReferenceRevisionsSaveTest.php | 266 - .../EntityReferenceRevisionsDeriverTest.php | 46 - ...ntityReferenceRevisionsDestinationTest.php | 484 -- web/modules/contrib/entityqueue/composer.json | 26 + .../config/schema/entityqueue.schema.yml | 20 +- .../contrib/entityqueue/entityqueue.info.yml | 6 +- .../entityqueue/entityqueue.links.task.yml | 3 + .../contrib/entityqueue/entityqueue.module | 61 +- .../entityqueue/entityqueue.routing.yml | 22 +- .../entityqueue/entityqueue.services.yml | 5 + .../contrib/entityqueue/entityqueue.views.inc | 49 +- ...nce_SelectionHandler_EntityQueue.class.php | 85 - .../entityreference/selection/entityqueue.inc | 13 - .../Controller/EntityQueueUIController.php | 173 +- .../entityqueue/src/Entity/EntityQueue.php | 25 +- .../entityqueue/src/Entity/EntitySubqueue.php | 54 +- .../src/EntityQueueAccessControlHandler.php | 48 + .../src/EntityQueueHandlerBase.php | 15 +- .../src/EntityQueueHandlerInterface.php | 14 +- .../entityqueue/src/EntityQueueInterface.php | 24 +- .../src/EntityQueueListBuilder.php | 48 +- .../src/EntityQueuePermissions.php | 26 +- .../EntitySubqueueAccessControlHandler.php | 7 +- .../src/EntitySubqueueInterface.php | 21 + .../entityqueue/src/Form/EntityQueueForm.php | 111 +- .../src/Form/EntitySubqueueDeleteForm.php | 2 +- .../src/Form/EntitySubqueueForm.php | 71 +- .../Derivative/EntityqueueLocalTask.php | 72 + .../src/Plugin/EntityQueueHandler/Simple.php | 9 +- .../EntityqueueDragtableWidget.php | 47 +- .../QueueSizeConstraintValidator.php | 1 - .../relationship/EntityQueueRelationship.php | 6 +- .../Plugin/views/sort/EntityQueueInQueue.php | 103 + .../src/Routing/RouteSubscriber.php | 98 + .../entityqueue.entity_queue.simple_queue.yml | 24 + .../entityqueue.entity_queue.test_queue.yml | 25 + .../install/views.view.entityqueue_test.yml | 181 + .../views.view.simple_queue_listing.yml | 185 + .../config/schema/entityqueue_test.schema.yml | 8 + .../entityqueue_test.info.yml | 15 + .../src/Plugin/EntityQueueHandler/Test.php | 69 + .../src/Functional/EntityQueueUiTest.php | 46 + .../src/Kernel/EntityQueueCacheTagsTest.php | 87 + .../contrib/eu_cookie_compliance/README.txt | 2 +- .../eu_cookie_compliance/composer.json | 43 + .../install/eu_cookie_compliance.settings.yml | 13 +- .../schema/eu_cookie_compliance.schema.yml | 87 +- .../css/eu_cookie_compliance.bare.css | 22 +- .../css/eu_cookie_compliance.css | 73 +- .../eu_cookie_compliance.info.yml | 8 +- .../eu_cookie_compliance.install | 221 +- .../eu_cookie_compliance.module | 270 +- .../eu_cookie_compliance.routing.yml | 7 + .../eu_cookie_compliance.services.yml | 4 + .../js/eu_cookie_compliance.js | 202 +- .../src/Annotation/ConsentStorage.php | 52 + .../CheckIfEuCountryJsController.php | 4 + .../src/Controller/StoreConsent.php | 37 + .../src/Form/EuCookieComplianceConfigForm.php | 583 +- .../ConsentStorage/BasicConsentStorage.php | 47 + .../src/Plugin/ConsentStorageBase.php | 122 + .../src/Plugin/ConsentStorageInterface.php | 34 + .../src/Plugin/ConsentStorageManager.php | 49 + .../Plugin/ConsentStorageManagerInterface.php | 31 + .../src/Routing/CheckIfEuCountryJs.php | 4 +- ...u_cookie_compliance_popup_agreed.html.twig | 4 +- .../eu_cookie_compliance_popup_info.html.twig | 22 +- .../eu_cookie_compliance_withdraw.html.twig | 28 + .../contrib/{entity => file_mdm}/LICENSE.txt | 0 web/modules/contrib/file_mdm/README.md | 115 + web/modules/contrib/file_mdm/composer.json | 10 + ..._mdm.file_metadata_plugin.getimagesize.yml | 7 + .../config/install/file_mdm.settings.yml | 4 + .../schema/file_mdm.data_types.schema.yml | 32 + .../config/schema/file_mdm.schema.yml | 18 + .../contrib/file_mdm/file_mdm.info.yml | 15 + .../contrib/file_mdm/file_mdm.links.menu.yml | 5 + web/modules/contrib/file_mdm/file_mdm.module | 26 + .../contrib/file_mdm/file_mdm.routing.yml | 9 + .../contrib/file_mdm/file_mdm.services.yml | 17 + .../contrib/file_mdm/file_mdm_exif/README.md | 134 + ...ile_mdm_exif.file_metadata_plugin.exif.yml | 33 + .../file_mdm_exif.data_types.schema.yml | 14 + .../config/schema/file_mdm_exif.schema.yml | 13 + .../file_mdm_exif/file_mdm_exif.info.yml | 17 + .../file_mdm_exif/file_mdm_exif.services.yml | 4 + .../file_mdm_exif/src/ExifTagMapper.php | 350 ++ .../src/ExifTagMapperInterface.php | 54 + .../src/Plugin/FileMetadata/Exif.php | 399 ++ .../tests/src/Kernel/FileMetadataExifTest.php | 350 ++ .../contrib/file_mdm/file_mdm_font/README.md | 68 + ...ile_mdm_font.file_metadata_plugin.font.yml | 7 + .../config/schema/file_mdm_font.schema.yml | 9 + .../file_mdm_font/file_mdm_font.info.yml | 17 + .../src/Plugin/FileMetadata/Font.php | 113 + .../tests/src/Kernel/FileMetadataFontTest.php | 114 + web/modules/contrib/file_mdm/phpcs.xml.dist | 13 + .../src/Element/FileMetadataCaching.php | 112 + .../contrib/file_mdm/src/FileMetadata.php | 276 + .../file_mdm/src/FileMetadataException.php | 20 + .../file_mdm/src/FileMetadataInterface.php | 208 + .../file_mdm/src/FileMetadataManager.php | 159 + .../src/FileMetadataManagerInterface.php | 63 + .../file_mdm/src/Form/SettingsForm.php | 136 + .../src/Plugin/Annotation/FileMetadata.php | 43 + .../FileMetadata/FileMetadataPluginBase.php | 538 ++ .../src/Plugin/FileMetadata/GetImageSize.php | 89 + .../Plugin/FileMetadataPluginInterface.php | 211 + .../src/Plugin/FileMetadataPluginManager.php | 43 + .../tests/files/1024-2006_1011_093752.jpg | Bin 0 -> 209855 bytes .../file_mdm/tests/files/sample-1.tiff | Bin 0 -> 6925 bytes .../file_mdm/tests/files/test-exif.jpeg | Bin 0 -> 15287 bytes .../src/Kernel/FileMetadataManagerTest.php | 424 ++ .../Kernel/FileMetadataManagerTestBase.php | 46 + .../contrib/htmlawed/htmlawed.info.yml | 7 +- .../{htmLawed.module => htmlawed.module} | 0 .../contrib/image_widget_crop/composer.json | 2 +- .../install/image_widget_crop.settings.yml | 1 + .../schema/image_widget_crop.schema.yml | 10 + .../css/image_widget_crop.css | 45 +- .../image_widget_crop.info.yml | 6 +- .../image_widget_crop.module | 4 +- .../js/ImageWidgetCropType.js | 39 +- ...y.node.crop_responsive_example.default.yml | 2 + ...splay.node.crop_simple_example.default.yml | 2 + .../image_widget_crop_examples.info.yml | 6 +- .../image_widget_crop_examples.install | 3 + .../src/Tests/ImageWidgetCropExamplesTest.php | 54 - .../ImageWidgetCropExamplesTest.php | 56 + .../src/Element/ImageCrop.php | 137 +- .../src/Form/CropWidgetForm.php | 14 +- .../Field/FieldWidget/ImageCropWidget.php | 166 +- .../src/Tests/ImageWidgetCropTest.php | 7 +- .../schema/inline_entity_form.schema.yml | 15 + .../inline_entity_form.info.yml | 6 +- .../inline_entity_form.module | 23 +- .../src/Element/InlineEntityForm.php | 58 +- .../src/Form/EntityInlineForm.php | 39 +- .../FieldWidget/InlineEntityFormBase.php | 34 +- .../FieldWidget/InlineEntityFormComplex.php | 82 +- .../FieldWidget/InlineEntityFormSimple.php | 20 +- .../src/Tests/ComplexWidgetWebTest.php | 96 +- .../src/Tests/ElementWebTest.php | 2 +- .../src/Tests/InlineEntityFormTestBase.php | 4 +- .../src/Tests/SimpleWidgetWebTest.php | 4 +- .../src/Tests/TranslationTest.php | 2 +- .../src/TranslationHelper.php | 7 +- .../inline_entity_form_test.info.yml | 6 +- .../inline_entity_form_test.routing.yml | 11 +- .../src/IefEditTest.php | 47 - .../inline_entity_form_test/src/IefTest.php | 8 +- .../contrib/linkchecker/drush.services.yml | 2 +- .../contrib/linkchecker/linkchecker.module | 2 +- .../d6_linkchecker_settings.yml | 0 .../d7_linkchecker_settings.yml | 0 ...erCommands.php => LinkCheckerCommands.php} | 4 +- .../src/Form/LinkCheckerAdminSettingsForm.php | 2 +- .../Tests/LinkCheckerLinkExtractionTest.php | 4 + web/modules/contrib/media_entity/LICENSE.txt | 339 -- web/modules/contrib/media_entity/README.md | 48 - .../contrib/media_entity/drush.services.yml | 6 - .../media_entity/images/icons/generic.png | Bin 1522 -> 0 bytes .../media_entity/media_entity.drush.inc | 85 - .../media_entity/media_entity.info.yml | 17 - .../contrib/media_entity/media_entity.install | 566 -- .../contrib/media_entity/media_entity.module | 57 - .../media_entity/media_entity.services.yml | 7 - .../media_entity/src/Annotation/MediaType.php | 43 - .../contrib/media_entity/src/CliService.php | 124 - .../src/Commands/DrushCommands.php | 64 - .../contrib/media_entity/src/Media.php | 113 - .../contrib/media_entity/src/MediaBundle.php | 8 - .../media_entity/src/MediaTypeManager.php | 32 - .../fixtures/drupal-8.4.0-media-entity.php.gz | Bin 146513 -> 0 bytes .../Functional/CoreMediaUpdatePathTest.php | 149 - .../media_entity_instagram.info.yml | 6 +- .../InstagramEmbedCodeConstraint.php | 2 +- .../InstagramEmbedFormatterTest.php | 177 +- web/modules/contrib/memcache/README.txt | 138 +- .../contrib/memcache/example.services.yml | 29 + .../contrib/memcache/memcache.info.yml | 6 +- web/modules/contrib/memcache/memcache.install | 5 + .../contrib/memcache/memcache.services.yml | 14 +- .../install/memcache_admin.settings.yml | 1 + .../memcache_admin/memcache_admin.info.yml | 17 + .../memcache_admin/memcache_admin.install | 13 + .../memcache_admin.links.menu.yml | 11 + .../memcache_admin.permissions.yml | 6 + .../memcache_admin/memcache_admin.routing.yml | 47 + .../memcache_admin.services.yml | 5 + .../MemcacheStatisticsController.php | 563 ++ .../MemcacheAdminSubscriber.php | 143 + .../src/Form/MemcacheAdminSettingsForm.php | 52 + .../src/Cache/TimestampCacheTagsChecksum.php | 147 + .../src/Connection/MemcacheConnection.php | 81 + .../MemcacheConnectionInterface.php | 33 + .../src/Connection/MemcachedConnection.php | 78 + .../memcache/src/Driver/DriverBase.php | 355 ++ .../memcache/src/Driver/MemcacheDriver.php | 90 + .../src/Driver/MemcacheDriverFactory.php | 206 + .../memcache/src/Driver/MemcachedDriver.php | 94 + .../contrib/memcache/src/DrupalMemcache.php | 109 - .../memcache/src/DrupalMemcacheBase.php | 99 - .../memcache/src/DrupalMemcacheFactory.php | 187 - .../memcache/src/DrupalMemcacheInterface.php | 36 +- .../contrib/memcache/src/DrupalMemcached.php | 108 - .../MemcacheTimestampInvalidator.php | 62 + .../Invalidator/TimestampInvalidatorBase.php | 85 + .../TimestampInvalidatorInterface.php | 61 + .../memcache/src/Lock/MemcacheLockBackend.php | 33 +- .../memcache/src/Lock/MemcacheLockFactory.php | 26 +- .../Lock/PersistentMemcacheLockBackend.php | 32 - .../contrib/memcache/src/MemcacheBackend.php | 197 +- .../memcache/src/MemcacheBackendFactory.php | 59 +- .../memcache/src/MemcacheException.php | 5 - ...emcacheConfig.php => MemcacheSettings.php} | 10 +- .../src/Tests/MemcacheLockFunctionalTest.php | 6 - .../memcache_test/memcache_test.info.yml | 7 +- .../src/MemcacheTestServiceProvider.php | 5 - .../src/Kernel/MemcacheBackendTest.php} | 13 +- ...onfigTest.php => MemcacheSettingsTest.php} | 20 +- web/modules/contrib/metatag/CHANGELOG.txt | 61 + web/modules/contrib/metatag/README.txt | 22 + web/modules/contrib/metatag/composer.json | 4 +- .../install/metatag.metatag_defaults.user.yml | 2 +- .../schema/metatag.metatag_tag.schema.yml | 3 + .../config/schema/metatag.settings.schema.yml | 7 + web/modules/contrib/metatag/metatag.api.php | 22 +- web/modules/contrib/metatag/metatag.info.yml | 7 +- web/modules/contrib/metatag/metatag.install | 7 +- .../contrib/metatag/metatag.links.menu.yml | 5 + .../contrib/metatag/metatag.links.task.yml | 10 + web/modules/contrib/metatag/metatag.module | 48 +- .../contrib/metatag/metatag.routing.yml | 10 + .../metatag_app_links.info.yml | 6 +- .../metatag/metatag_dc/metatag_dc.info.yml | 6 +- .../metatag_dc_advanced.info.yml | 6 +- .../metatag_facebook.info.yml | 6 +- .../metatag_favicons.info.yml | 6 +- .../metatag_favicons/metatag_favicons.module | 2 +- .../src/Tests/MetatagFaviconsTagsTest.php | 35 +- .../metatag_google_cse.info.yml | 6 +- .../metatag_google_plus.info.yml | 6 +- .../metatag_hreflang.info.yml | 6 +- .../src/Plugin/Derivative/HreflangDeriver.php | 2 - .../metatag_mobile.metatag_tag.schema.yml | 3 + .../metatag_mobile/metatag_mobile.info.yml | 6 +- .../metatag_mobile/metatag_mobile.module | 12 - .../src/Plugin/metatag/Tag/WebManifest.php | 24 + .../src/Tests/MetatagMobileTagsTest.php | 20 +- .../metatag_open_graph.metatag_tag.schema.yml | 3 + .../metatag_open_graph.info.yml | 6 +- .../metatag_open_graph.install | 2 +- .../metatag_open_graph.module | 10 +- .../src/Plugin/metatag/Tag/BookAuthor.php | 2 +- .../src/Plugin/metatag/Tag/BookISBN.php | 2 +- .../Plugin/metatag/Tag/BookReleaseDate.php | 2 +- .../src/Plugin/metatag/Tag/BookTag.php | 2 +- .../src/Plugin/metatag/Tag/OgImageAlt.php | 25 + .../Plugin/metatag/Tag/OgImageSecureUrl.php | 2 +- .../src/Tests/MetatagOpenGraphTagsTest.php | 1 + .../metatag_open_graph_products.info.yml | 6 +- .../Plugin/metatag/Tag/ProductPriceAmount.php | 2 +- .../metatag/Tag/ProductPriceCurrency.php | 2 +- .../MetatagOpenGraphProductsTagsTest.php | 21 +- .../metatag_page_manager.info.yml | 14 + .../metatag_page_manager.module | 106 + .../Functional/MetatagPageManagerTest.php | 177 + .../metatag_pinterest.info.yml | 6 +- .../metatag_twitter_cards.info.yml | 6 +- .../metatag_verification.info.yml | 6 +- .../metatag_views/metatag_views.info.yml | 10 +- .../metatag_views/metatag_views.module | 2 +- .../src/Controller/MetatagViewsController.php | 14 +- .../MetatagViewsTranslationController.php | 6 +- .../src/Form/MetatagViewsEditForm.php | 6 +- .../src/Form/MetatagViewsRevertForm.php | 4 +- .../src/Form/MetatagViewsTranslationForm.php | 12 +- .../MetatagDisplayExtender.php | 4 +- .../src/Command/GenerateGroupCommand.php | 14 +- .../src/Command/GenerateTagCommand.php | 22 +- .../metatag/src/Entity/MetatagDefaults.php | 23 + .../metatag/src/Form/MetatagDefaultsForm.php | 53 +- .../metatag/src/Form/MetatagSettingsForm.php | 104 + .../src/Generator/MetatagGroupGenerator.php | 17 +- .../src/Generator/MetatagTagGenerator.php | 22 +- .../src/MetatagDefaultsListBuilder.php | 43 +- .../contrib/metatag/src/MetatagManager.php | 84 +- .../contrib/metatag/src/MetatagToken.php | 8 +- .../Field/FieldType/MetatagFieldItem.php | 2 +- .../Field/FieldWidget/MetatagFirehose.php | 21 +- .../Field/MetatagEntityFieldItemList.php | 2 +- .../Plugin/GraphQL/Scalars/MetatagScalar.php | 20 + .../src/Plugin/metatag/Group/AppLinks.php | 17 - .../metatag/Group/DublinCoreAdvanced.php | 17 - .../Plugin/metatag/Group/DublinCoreBasic.php | 17 - .../src/Plugin/metatag/Group/Facebook.php | 17 - .../src/Plugin/metatag/Group/GooglePlus.php | 17 - .../Plugin/metatag/Group/SiteValidation.php | 17 - .../src/Plugin/metatag/Group/TwitterCards.php | 17 - .../src/Plugin/metatag/Tag/GeoPosition.php | 2 +- .../metatag/src/Plugin/metatag/Tag/Google.php | 22 + .../src/Plugin/metatag/Tag/MetaNameBase.php | 20 +- .../metatag/src/Tests/MetatagAdminTest.php | 53 + .../metatag/src/Tests/MetatagHelperTrait.php | 2 +- .../src/Tests/MetatagNodeTranslationTest.php | 3 + .../metatag/src/Tests/MetatagTagTypesTest.php | 4 +- .../metatag/src/Tests/MetatagTagsTest.php | 1 + .../metatag/src/Tests/MetatagTagsTestBase.php | 5 +- .../metatag_test_custom_route.info.yml | 6 +- .../MetatagTestCustomRouteController.php | 2 +- .../metatag_test_tag.info.yml | 6 +- .../src/Functional/MetatagHelperTrait.php | 2 +- .../src/Functional/SchemaMetatagTest.php | 19 + .../tests/src/Kernel/MetatagManagerTest.php | 64 +- .../schema/migrate_plus.data_types.schema.yml | 30 +- .../schema/migrate_plus.process.schema.yml | 4 +- .../migrate_plus.migration.beer_node.yml | 2 + .../migrate_plus.migration.beer_term.yml | 4 + .../migrate_plus.migration.beer_user.yml | 2 + .../migrate_example/migrate_example.info.yml | 6 +- .../migrate_example_setup.info.yml | 6 +- .../migrate_plus.migration.weather_soap.yml | 8 +- .../migrate_plus.migration.wine_role_json.yml | 5 +- .../migrate_plus.migration.wine_role_xml.yml | 5 +- .../migrate_plus.migration.wine_terms.yml | 2 + ...e_plus.migration.wine_variety_list.yml.txt | 5 +- ..._plus.migration.wine_variety_multi_xml.yml | 2 + .../migrate_example_advanced.info.yml | 6 +- .../migrate_example_advanced.install | 12 +- .../migrate_example_advanced_setup.info.yml | 6 +- .../migrate_plus/migrate_plus.info.yml | 6 +- .../Plugin/migrate/process/EntityGenerate.php | 86 +- .../Plugin/migrate/process/EntityLookup.php | 9 +- .../src/Plugin/migrate/process/Merge.php | 10 +- .../Plugin/migrate/process/MultipleValues.php | 58 + .../Plugin/migrate/process/SingleValue.php | 45 + .../Plugin/migrate/process/SkipOnValue.php | 60 +- .../Plugin/migrate_plus/data_parser/Json.php | 14 +- .../migrate_plus/data_parser/SimpleXml.php | 2 +- .../tests/data/missing_properties.json | 21 + .../data/simple_xml_reduce_single_value.xml | 14 + .../tests/src/Functional/LoadTest.php | 50 + .../migrate/process/EntityGenerateTest.php | 487 +- .../migrate_plus/data_parser/JsonTest.php | 112 + .../data_parser/SimpleXmlTest.php | 77 + .../src/Unit/process/MultipleValuesTest.php | 32 + .../src/Unit/process/SingleValueTest.php | 32 + .../src/Unit/process/SkipOnValueTest.php | 30 + .../contrib/migrate_tools/composer.json | 27 + .../contrib/migrate_tools/drush.services.yml | 6 + .../migrate_tools/migrate_tools.drush.inc | 187 +- .../migrate_tools/migrate_tools.info.yml | 10 +- .../migrate_tools.links.task.yml | 12 + .../migrate_tools/migrate_tools.module | 41 +- .../migrate_tools/migrate_tools.routing.yml | 94 +- .../migrate_tools/migrate_tools.services.yml | 9 + web/modules/contrib/migrate_tools/phpcs.xml | 207 + .../src/Commands/MigrateToolsCommands.php | 719 +++ .../src/Controller/MessageController.php | 38 +- .../src/Controller/MigrationController.php | 136 +- .../Controller/MigrationGroupListBuilder.php | 4 +- .../src/Controller/MigrationListBuilder.php | 181 +- .../src/Drush9LogMigrateMessage.php | 48 + .../src/DrushLogMigrateMessage.php | 5 + .../src/Form/MigrationAddForm.php | 1 - .../src/Form/MigrationDeleteForm.php | 12 +- .../src/Form/MigrationEditForm.php | 9 +- .../src/Form/MigrationExecuteForm.php | 212 + .../src/Form/MigrationFormBase.php | 53 +- .../src/Form/MigrationGroupDeleteForm.php | 12 +- .../src/Form/MigrationGroupEditForm.php | 2 - .../src/Form/MigrationGroupFormBase.php | 58 +- .../migrate_tools/src/Form/SourceCsvForm.php | 453 ++ .../src/MigrateBatchExecutable.php | 295 + .../migrate_tools/src/MigrateExecutable.php | 112 +- .../src/Routing/RouteProcessor.php | 27 + ...migrate_plus.migration.csv_source_test.yml | 33 + .../migrate_plus.migration_group.csv_test.yml | 4 + .../csv_source_test/csv_source_test.info.yml | 15 + .../migrate_plus.migration.fruit_terms.yml | 32 + .../migrate_plus.migration_group.default.yml | 9 + .../migrate_tools_test.info.yml | 14 + .../Functional/MigrateExecutionFormTest.php | 132 + .../src/Functional/SourceCsvFormTest.php | 241 + .../migrate_upgrade/drush.services.yml | 6 + .../migrate_upgrade/migrate_upgrade.drush.inc | 2 +- .../migrate_upgrade/migrate_upgrade.info.yml | 6 +- .../src/Commands/MigrateUpgradeCommands.php | 190 + .../src/MigrateUpgradeDrushRunner.php | 200 +- .../Unit/MigrateUpgradeDrushRunnerTest.php | 8 +- web/modules/contrib/paragraphs/LICENSE.txt | 339 -- web/modules/contrib/paragraphs/README.txt | 79 - web/modules/contrib/paragraphs/composer.json | 17 - ...ore.entity_view_mode.paragraph.preview.yml | 9 - .../config/schema/paragraphs_type.schema.yml | 96 - web/modules/contrib/paragraphs/css/.gitignore | 1 - .../contrib/paragraphs/css/.stylelintrc.yml | 294 - web/modules/contrib/paragraphs/css/README.md | 83 - .../contrib/paragraphs/css/_variables.scss | 31 - .../contrib/paragraphs/css/gulp-options.yml | 51 - .../contrib/paragraphs/css/gulp-tasks.js | 37 - .../contrib/paragraphs/css/gulpfile.js | 23 - .../contrib/paragraphs/css/package.json | 22 - .../paragraphs/css/paragraphs.actions.css | 127 - .../paragraphs/css/paragraphs.actions.scss | 126 - .../paragraphs/css/paragraphs.admin.css | 82 - .../paragraphs/css/paragraphs.dragdrop.css | 98 - .../paragraphs/css/paragraphs.dragdrop.scss | 91 - .../css/paragraphs.list-builder.css | 7 - .../css/paragraphs.list-builder.scss | 7 - .../paragraphs/css/paragraphs.modal.css | 23 - .../paragraphs/css/paragraphs.modal.scss | 23 - .../paragraphs/css/paragraphs.widget.css | 288 - .../paragraphs/css/paragraphs.widget.scss | 266 - .../contrib/paragraphs/icons/icon-actions.svg | 4 - .../contrib/paragraphs/icons/icon-changed.svg | 4 - .../icons/icon-collapse-disabled.svg | 4 - .../paragraphs/icons/icon-collapse.svg | 4 - .../paragraphs/icons/icon-delete-disabled.svg | 4 - .../contrib/paragraphs/icons/icon-delete.svg | 4 - .../paragraphs/icons/icon-edit-disabled.svg | 4 - .../paragraphs/icons/icon-edit-info.svg | 4 - .../contrib/paragraphs/icons/icon-edit.svg | 4 - .../contrib/paragraphs/icons/icon-error.svg | 4 - .../contrib/paragraphs/icons/icon-lock.svg | 4 - .../contrib/paragraphs/icons/icon-view.svg | 4 - .../contrib/paragraphs/icons/icon-warning.svg | 4 - .../paragraphs/js/paragraphs.actions.js | 38 - .../contrib/paragraphs/js/paragraphs.admin.js | 113 - .../paragraphs/js/paragraphs.dragdrop.js | 228 - .../contrib/paragraphs/js/paragraphs.modal.js | 74 - ....node.paragraphed_content_demo.default.yml | 75 - ...m_display.paragraph.image_text.default.yml | 32 - ..._form_display.paragraph.images.default.yml | 23 - ...lay.paragraph.nested_paragraph.default.yml | 27 - ...ty_form_display.paragraph.text.default.yml | 23 - ...m_display.paragraph.text_image.default.yml | 32 - ...ty_form_display.paragraph.user.default.yml | 23 - ....node.paragraphed_content_demo.default.yml | 34 - ...y.node.paragraphed_content_demo.teaser.yml | 25 - ...w_display.paragraph.image_text.default.yml | 32 - ..._view_display.paragraph.images.default.yml | 24 - ...lay.paragraph.nested_paragraph.default.yml | 24 - ...ty_view_display.paragraph.text.default.yml | 22 - ...w_display.paragraph.text_image.default.yml | 32 - ...ty_view_display.paragraph.user.default.yml | 21 - ...hed_content_demo.field_paragraphs_demo.yml | 42 - ....paragraph.image_text.field_image_demo.yml | 36 - ...d.paragraph.image_text.field_text_demo.yml | 20 - ...eld.paragraph.images.field_images_demo.yml | 36 - ...nested_paragraph.field_paragraphs_demo.yml | 42 - ...d.field.paragraph.text.field_text_demo.yml | 20 - ....paragraph.text_image.field_image_demo.yml | 44 - ...d.paragraph.text_image.field_text_demo.yml | 20 - ...d.field.paragraph.user.field_user_demo.yml | 25 - ...eld.storage.node.field_paragraphs_demo.yml | 18 - ...eld.storage.paragraph.field_image_demo.yml | 27 - ...ld.storage.paragraph.field_images_demo.yml | 27 - ...torage.paragraph.field_paragraphs_demo.yml | 18 - ...ield.storage.paragraph.field_text_demo.yml | 17 - ...ield.storage.paragraph.field_user_demo.yml | 18 - ...settings.node.paragraphed_content_demo.yml | 15 - ....content_settings.paragraph.image_text.yml | 15 - ...uage.content_settings.paragraph.images.yml | 15 - ...nguage.content_settings.paragraph.text.yml | 15 - ....content_settings.paragraph.text_image.yml | 15 - ...nguage.content_settings.paragraph.user.yml | 15 - .../node.type.paragraphed_content_demo.yml | 11 - .../paragraphs.paragraphs_type.image_text.yml | 6 - .../paragraphs.paragraphs_type.images.yml | 6 - ...raphs.paragraphs_type.nested_paragraph.yml | 6 - .../paragraphs.paragraphs_type.text.yml | 6 - .../paragraphs.paragraphs_type.text_image.yml | 6 - .../paragraphs.paragraphs_type.user.yml | 6 - .../config/optional/language.entity.de.yml | 8 - .../config/optional/language.entity.fr.yml | 8 - ...search_api.index.paragraphs_demo_index.yml | 61 - ...arch_api.server.paragraphs_demo_server.yml | 16 - .../optional/views.view.paragraphs_search.yml | 226 - .../paragraphs_demo/css/paragraphs_demo.css | 50 - .../paragraphs_demo/paragraphs_demo.info.yml | 25 - .../paragraphs_demo/paragraphs_demo.install | 96 - .../paragraphs_demo.libraries.yml | 5 - .../paragraphs_demo/paragraphs_demo.module | 42 - .../src/Tests/ParagraphsDemoTest.php | 166 - .../paragraphs_type_permissions.info.yml | 14 - .../paragraphs_type_permissions.module | 95 - ...aragraphs_type_permissions.permissions.yml | 3 - .../src/ParagraphsTypePermissions.php | 80 - .../Tests/ParagraphsTypePermissionsTest.php | 196 - .../contrib/paragraphs/paragraphs.api.php | 50 - .../contrib/paragraphs/paragraphs.info.yml | 22 - .../contrib/paragraphs/paragraphs.install | 215 - .../paragraphs/paragraphs.libraries.yml | 77 - .../paragraphs/paragraphs.links.action.yml | 5 - .../paragraphs/paragraphs.links.menu.yml | 5 - .../paragraphs/paragraphs.links.task.yml | 9 - .../contrib/paragraphs/paragraphs.module | 424 -- .../paragraphs/paragraphs.permissions.yml | 7 - .../paragraphs/paragraphs.post_update.php | 115 - .../contrib/paragraphs/paragraphs.routing.yml | 31 - .../paragraphs/paragraphs.services.yml | 5 - .../src/Annotation/ParagraphsBehavior.php | 50 - .../Controller/ParagraphsTypeListBuilder.php | 71 - .../src/Element/ParagraphOperations.php | 38 - .../src/Element/ParagraphsActions.php | 76 - .../paragraphs/src/Entity/Paragraph.php | 672 --- .../paragraphs/src/Entity/ParagraphsType.php | 228 - .../ReplicateFieldSubscriber.php | 54 - .../src/Feeds/Target/Paragraphs.php | 133 - .../src/Form/ParagraphsTypeDeleteConfirm.php | 31 - .../src/Form/ParagraphsTypeForm.php | 228 - .../src/ParagraphAccessControlHandler.php | 45 - .../paragraphs/src/ParagraphInterface.php | 113 - .../paragraphs/src/ParagraphStorageSchema.php | 43 - .../paragraphs/src/ParagraphViewBuilder.php | 44 - .../paragraphs/src/ParagraphsBehaviorBase.php | 189 - .../src/ParagraphsBehaviorCollection.php | 71 - .../src/ParagraphsBehaviorInterface.php | 140 - .../src/ParagraphsBehaviorManager.php | 62 - .../src/ParagraphsServiceProvider.php | 31 - .../src/ParagraphsTypeInterface.php | 74 - .../ParagraphSelection.php | 342 -- .../ParagraphsSummaryFormatter.php | 52 - .../FieldWidget/InlineParagraphsWidget.php | 1465 ----- .../Field/FieldWidget/ParagraphsWidget.php | 2392 -------- .../Tests/Classic/ParagraphsAccessTest.php | 145 - .../Tests/Classic/ParagraphsAddModesTest.php | 227 - .../Classic/ParagraphsAdministrationTest.php | 573 -- .../Tests/Classic/ParagraphsConfigTest.php | 240 - .../Tests/Classic/ParagraphsContactTest.php | 49 - .../ParagraphsCoreVersionUiTestTrait.php | 58 - .../Tests/Classic/ParagraphsEditModesTest.php | 105 - ...anslationWithNonTranslatableParagraphs.php | 109 - .../Classic/ParagraphsFieldGroupTest.php | 70 - .../ParagraphsInlineEntityFormTest.php | 142 - .../Tests/Classic/ParagraphsPreviewTest.php | 101 - .../ParagraphsSummaryFormatterTest.php | 74 - .../src/Tests/Classic/ParagraphsTestBase.php | 207 - .../Classic/ParagraphsTranslationTest.php | 844 --- .../src/Tests/Classic/ParagraphsTypesTest.php | 134 - .../src/Tests/Classic/ParagraphsUiTest.php | 70 - .../Classic/ParagraphsWidgetButtonsTest.php | 112 - .../ParagraphsExperimentalAccessTest.php | 143 - .../ParagraphsExperimentalAddModesTest.php | 226 - ...ragraphsExperimentalAdministrationTest.php | 580 -- .../ParagraphsExperimentalBehaviorsTest.php | 323 - .../ParagraphsExperimentalConfigTest.php | 252 - .../ParagraphsExperimentalContactTest.php | 48 - ...graphsExperimentalDuplicateFeatureTest.php | 153 - .../ParagraphsExperimentalEditModesTest.php | 140 - ...anslationWithNonTranslatableParagraphs.php | 109 - .../ParagraphsExperimentalFieldGroupTest.php | 72 - ...aragraphsExperimentalHeaderActionsTest.php | 256 - ...graphsExperimentalInlineEntityFormTest.php | 148 - .../ParagraphsExperimentalPreviewTest.php | 102 - ...agraphsExperimentalReplicateEnableTest.php | 17 - ...graphsExperimentalSummaryFormatterTest.php | 72 - .../ParagraphsExperimentalTestBase.php | 51 - .../ParagraphsExperimentalTranslationTest.php | 862 --- .../ParagraphsExperimentalTypesTest.php | 63 - .../ParagraphsExperimentalUiTest.php | 80 - ...aragraphsExperimentalWidgetButtonsTest.php | 187 - .../src/Tests/ParagraphsUninstallTest.php | 55 - .../paragraphs/templates/paragraph.html.twig | 54 - .../templates/paragraphs-actions.html.twig | 29 - .../templates/paragraphs-add-dialog.html.twig | 23 - .../paragraphs-dropbutton-wrapper.html.twig | 21 - .../templates/paragraphs-info-icon.html.twig | 17 - .../config/schema/paragraphs_test.schema.yml | 15 - .../modules/paragraphs_test/css/bold-text.css | 3 - .../paragraphs_test/css/color-text.css | 6 - .../paragraphs_test/paragraphs_test.info.yml | 15 - .../paragraphs_test.libraries.yml | 9 - .../paragraphs_test/paragraphs_test.module | 25 - .../Behavior/TestBoldTextBehavior.php | 66 - .../paragraphs/Behavior/TestDummyBehavior.php | 37 - .../Behavior/TestFieldsSelectionBehavior.php | 58 - .../Behavior/TestTextColorBehavior.php | 102 - .../ParagraphsExperimentalBehaviorsTest.php | 95 - ...agraphsExperimentalDragAndDropModeTest.php | 672 --- ...aragraphsExperimentalWidgetButtonsTest.php | 238 - .../FunctionalJavascript/LoginAdminTrait.php | 44 - .../ParagraphsExperimentalAddWidgetTest.php | 176 - ...aphsExperimentalEditPerspectivesUiTest.php | 207 - .../ParagraphsTestBaseTrait.php | 224 - .../Kernel/ParagraphsBehaviorPluginsTest.php | 92 - .../Kernel/ParagraphsCollapsedSummaryTest.php | 166 - .../ParagraphsCompositeRelationshipTest.php | 348 -- .../src/Kernel/ParagraphsIsChangedTest.php | 107 - .../src/Kernel/ParagraphsReplicateTest.php | 190 - ...graphsTypeHasEnabledBehaviorPluginTest.php | 75 - .../config/install/pathauto.settings.yml | 7 + .../config/schema/pathauto.schema.yml | 7 +- web/modules/contrib/pathauto/pathauto.api.php | 15 +- .../contrib/pathauto/pathauto.info.yml | 7 +- web/modules/contrib/pathauto/pathauto.install | 27 +- web/modules/contrib/pathauto/pathauto.js | 6 +- .../pathauto/pathauto.links.action.yml | 1 - web/modules/contrib/pathauto/pathauto.module | 2 +- .../contrib/pathauto/pathauto.services.yml | 2 +- .../contrib/pathauto/src/AliasCleaner.php | 10 +- .../pathauto/src/AliasCleanerInterface.php | 4 +- .../src/AliasStorageHelperInterface.php | 2 +- .../src/AliasTypeBatchUpdateInterface.php | 3 +- .../contrib/pathauto/src/AliasUniquifier.php | 4 +- .../pathauto/src/AliasUniquifierInterface.php | 1 + .../pathauto/src/Annotation/AliasType.php | 4 +- .../PathautoSettingsCacheTag.php | 14 + .../pathauto/src/Form/PathautoAdminDelete.php | 18 +- .../src/Form/PathautoBulkUpdateForm.php | 12 +- .../src/Form/PathautoSettingsForm.php | 15 +- .../pathauto/src/Form/PatternDisableForm.php | 4 +- .../pathauto/src/Form/PatternEditForm.php | 20 +- .../pathauto/src/Form/PatternEnableForm.php | 4 +- .../pathauto/src/PathautoFieldItemList.php | 4 +- .../pathauto/src/PathautoGenerator.php | 36 +- .../src/PathautoGeneratorInterface.php | 1 + .../contrib/pathauto/src/PathautoState.php | 1 + .../Plugin/Deriver/EntityAliasTypeDeriver.php | 4 +- .../AliasType/EntityAliasTypeBase.php | 16 +- .../src/Tests/PathautoBulkUpdateTest.php | 13 +- .../Tests/PathautoEnablingEntityTypesTest.php | 3 +- .../src/Tests/PathautoMassDeleteTest.php | 9 +- .../src/Tests/PathautoNodeWebTest.php | 6 +- .../src/Tests/PathautoSettingsFormWebTest.php | 2 +- .../src/Tests/PathautoTaxonomyWebTest.php | 3 +- .../src/Tests/PathautoTestHelperTrait.php | 10 +- .../pathauto/src/Tests/PathautoUiTest.php | 2 +- .../src/Tests/PathautoUserWebTest.php | 8 +- .../contrib/pathauto/src/VerboseMessenger.php | 20 +- .../pathauto_string_id_test.info.yml | 6 +- .../pathauto_views_test.info.yml | 6 +- .../Kernel/PathautoEntityWithStringIdTest.php | 4 +- .../tests/src/Kernel/PathautoKernelTest.php | 16 +- .../tests/src/Kernel/PathautoTokenTest.php | 36 + .../tests/src/Unit/VerboseMessengerTest.php | 93 +- .../contrib/permissions_by_term/.gitignore | 2 + .../bitbucket-pipelines.yml | 53 + .../contrib/permissions_by_term/js/.babelrc | 12 + .../contrib/permissions_by_term/js/README.md | 31 +- .../permissions_by_term/js/node-form.js | 118 - .../js/node-form.prototype.js | 242 - .../permissions_by_term/js/package-lock.json | 5283 +++++++++++++++++ .../permissions_by_term/js/package.json | 24 +- .../js/spec/node-form.spec.js | 15 - .../js/spec/support/jasmine.json | 11 - .../src/async-function/create-permission.js | 27 + .../src/async-function/fetch-from-backend.js | 23 + .../js/src/client/dom-client.prototype.js | 142 + .../permission-output-collector.prototype.js | 63 + .../js/src/client/term-collector.prototype.js | 54 + .../node-form-client.js | 115 + .../js/src/model/backend.prototype.js | 33 + .../src/model/permission-output.prototype.js | 34 + .../js/test/dom-client.tests.js | 31 + .../js/test/permission-creator.tests.js | 47 + .../js/test/permission-output.tests.js | 82 + .../js/test/term-selector.tests.js | 32 + .../permissions_by_term/js/test/tests.html | 15 + .../js/webpack-dist/bundle-test.js | 890 +++ .../js/webpack-dist/bundle.js | 9 + .../js/webpack.production.js | 22 + .../contrib/permissions_by_term/js/yarn.lock | 80 - .../permissions_by_entity.info.yml | 6 +- .../pbt_entity_test/pbt_entity_test.info.yml | 6 +- .../permissions_by_term.info.yml | 6 +- .../permissions_by_term.install | 17 +- .../permissions_by_term.libraries.yml | 3 +- .../permissions_by_term.module | 88 +- .../permissions_by_term.services.yml | 8 +- .../Controller/NodeEntityBundleController.php | 20 +- .../src/Factory/NodeAccessRecordFactory.php | 4 +- .../src/Listener/KernelEventListener.php | 10 +- .../src/Service/AccessCheck.php | 13 +- .../src/Service/AccessStorage.php | 134 +- .../src/Service/NodeEntityBundleInfo.php | 23 +- .../src/Service/{Term.php => TermHandler.php} | 22 +- .../tests/phpunit.xml.dist | 12 +- .../Context/PermissionsByTermContext.php | 45 +- .../tests/src/Behat/Features/access.feature | 134 +- .../src/Behat/Features/entityMetaInfo.feature | 102 - .../tests/src/Behat/behat.yml.dist | 18 +- .../src/Behat/composer-require-namespace.php | 7 + .../tests/src/Behat/composer.json | 27 - .../tests/src/Kernel/AccessStorageTest.php | 111 + .../tests/src/Kernel/MultilingualTest.php | 4 +- web/modules/contrib/php/composer.json | 2 +- web/modules/contrib/php/php.info.yml | 2 +- web/modules/contrib/php/php.module | 4 +- web/modules/contrib/php/php.services.yml | 7 + .../contrib/php/src/PhpUninstallValidator.php | 34 + .../contrib/php/src/Plugin/Condition/Php.php | 7 +- .../contrib/php/src/Plugin/Filter/Php.php | 13 +- .../src/Plugin/views/argument_default/Php.php | 8 +- .../Plugin/views/argument_validator/Php.php | 8 +- .../src/Tests/Condition/PhpConditionTest.php | 9 +- .../contrib/php/src/Tests/PhpAccessTest.php | 12 +- .../contrib/php/src/Tests/PhpFilterTest.php | 21 +- .../contrib/php/src/Tests/PhpTestBase.php | 7 +- .../php/src/Tests/PhpUninstallTest.php | 55 + .../Plugin/views/PhpArgumentValidatorTest.php | 9 +- web/modules/contrib/redirect/README.txt | 60 +- .../config/install/views.view.redirect.yml | 2 +- .../redirect_404/redirect_404.info.yml | 6 +- .../EventSubscriber/Redirect404Subscriber.php | 5 +- .../src/SqlRedirectNotFoundStorage.php | 2 +- .../Tests/Fix404RedirectUILanguageTest.php | 29 +- .../src/Tests/Fix404RedirectUITest.php | 29 +- .../redirect_domain/redirect_domain.info.yml | 6 +- .../src/Form/RedirectDomainForm.php | 4 +- .../contrib/redirect/redirect.info.yml | 6 +- .../contrib/redirect/src/Entity/Redirect.php | 13 +- .../RedirectRequestSubscriber.php | 22 +- .../redirect/src/Form/RedirectDeleteForm.php | 2 +- .../src/Form/RedirectDeleteMultipleForm.php | 2 +- .../redirect/src/Form/RedirectForm.php | 20 +- .../src/Form/RedirectSettingsForm.php | 4 +- .../FieldWidget/RedirectSourceWidget.php | 4 +- .../redirect/src/RedirectRepository.php | 8 +- .../redirect/src/RedirectViewsData.php | 25 + .../redirect/src/Tests/GlobalRedirectTest.php | 20 + .../src/Tests/RedirectUILanguageTest.php | 31 + .../redirect/src/Tests/RedirectUITest.php | 3 +- .../tests/src/Kernel/RedirectAPITest.php | 13 + .../Unit/RedirectRequestSubscriberTest.php | 33 +- .../security_review/src/Checks/Field.php | 124 +- .../security_review/src/Checks/Field.php.rej | 9 + .../contrib/slick_media/slick_media.info.yml | 7 +- web/modules/contrib/token/README.md | 29 +- web/modules/contrib/token/js/token.js | 6 + .../TokenAutocompleteController.php | 5 +- .../src/Controller/TokenCacheController.php | 4 +- .../src/Controller/TokenTreeController.php | 11 + .../token/src/Routing/RouteSubscriber.php | 1 + .../token/src/Tests/TokenBlockTest.php | 25 +- .../token/src/Tests/TokenCurrentPageTest.php | 30 +- .../token/src/Tests/TokenFieldUiTest.php | 34 +- .../contrib/token/src/Tests/TokenMenuTest.php | 76 +- .../contrib/token/src/Tests/TokenTestBase.php | 2 +- .../token/src/Tests/TokenTestTrait.php | 42 +- .../contrib/token/src/Tests/TokenURLTest.php | 14 +- .../contrib/token/src/Tests/TokenUserTest.php | 45 +- .../contrib/token/src/Tests/Tree/TreeTest.php | 2 +- web/modules/contrib/token/src/Token.php | 10 +- web/modules/contrib/token/src/TreeBuilder.php | 12 +- .../views.view.token_views_test.yml | 130 + .../token_module_test.info.yml | 6 +- .../token_module_test.module | 5 +- .../token_module_test.tokens.inc | 4 +- .../token_user_picture.info.yml | 6 +- .../token/tests/src/Functional/UrlTest.php | 137 + .../token/tests/src/Kernel/ArrayTest.php | 25 +- .../token/tests/src/Kernel/BookTest.php | 23 +- .../token/tests/src/Kernel/CommentTest.php | 28 +- .../token/tests/src/Kernel/DateTest.php | 7 +- .../token/tests/src/Kernel/EntityTest.php | 38 +- .../token/tests/src/Kernel/FileTest.php | 20 +- .../token/tests/src/Kernel/NodeTest.php | 27 +- .../token/tests/src/Kernel/RandomTest.php | 9 +- .../token/tests/src/Kernel/TaxonomyTest.php | 83 +- .../token/tests/src/Kernel/UnitTest.php | 31 +- .../token/tests/src/Kernel/UrlTest.php | 88 + .../token/tests/src/Kernel/ViewsTest.php | 50 + web/modules/contrib/token/token.info.yml | 8 +- web/modules/contrib/token/token.install | 52 +- web/modules/contrib/token/token.module | 107 +- web/modules/contrib/token/token.pages.inc | 2 +- web/modules/contrib/token/token.tokens.inc | 509 +- .../Plugin/views/style/ViewsBootstrapTab.php | 1 + .../views-bootstrap-carousel.html.twig | 2 + .../templates/views-bootstrap-grid.html.twig | 3 + .../views_bootstrap/views_bootstrap.info.yml | 2 + .../views_bootstrap/views_bootstrap.theme.inc | 2 +- 991 files changed, 29980 insertions(+), 40749 deletions(-) create mode 100644 web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/composer.json create mode 100644 web/modules/contrib/admin_toolbar/admin_toolbar_tools/composer.json rename web/modules/contrib/admin_toolbar/admin_toolbar_tools/{src/Tests => tests/src/Functional}/AdminToolbarToolsAlterTest.php (78%) rename web/modules/contrib/admin_toolbar/{src/Tests => tests/src/Functional}/AdminToolbarAlterTest.php (78%) create mode 100644 web/modules/contrib/block_class/composer.json create mode 100644 web/modules/contrib/diff/src/Tests/DiffRevisionContentModerationTest.php create mode 100644 web/modules/contrib/diff/tests/src/Functional/CoreVersionUiTestTrait.php create mode 100644 web/modules/contrib/diff/tests/src/Functional/NodeAccessTest.php create mode 100755 web/modules/contrib/draggableviews/README.md create mode 100644 web/modules/contrib/draggableviews/composer.json create mode 100644 web/modules/contrib/draggableviews/draggableviews.module.rej create mode 100644 web/modules/contrib/draggableviews/modules/draggableviews_demo/config/install/node.type.draggableviews_demo.yml create mode 100644 web/modules/contrib/draggableviews/modules/draggableviews_demo/config/install/views.view.draggableviews_demo.yml create mode 100644 web/modules/contrib/draggableviews/modules/draggableviews_demo/draggableviews_demo.info.yml create mode 100644 web/modules/contrib/draggableviews/modules/draggableviews_demo/draggableviews_demo.install create mode 100644 web/modules/contrib/draggableviews/src/Plugin/views/field/DraggableViewsField.php.rej create mode 100644 web/modules/contrib/draggableviews/tests/src/Functional/DraggableviewsTest.php delete mode 100644 web/modules/contrib/entity/.travis.yml delete mode 100644 web/modules/contrib/entity/README.txt delete mode 100644 web/modules/contrib/entity/composer.json delete mode 100644 web/modules/contrib/entity/config/schema/entity.schema.yml delete mode 100644 web/modules/contrib/entity/entity.info.yml delete mode 100644 web/modules/contrib/entity/entity.links.action.yml delete mode 100644 web/modules/contrib/entity/entity.links.task.yml delete mode 100644 web/modules/contrib/entity/entity.module delete mode 100644 web/modules/contrib/entity/entity.permissions.yml delete mode 100644 web/modules/contrib/entity/entity.services.yml delete mode 100644 web/modules/contrib/entity/entity.views.inc delete mode 100644 web/modules/contrib/entity/src/Access/EntityDeleteMultipleAccessCheck.php delete mode 100644 web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php delete mode 100644 web/modules/contrib/entity/src/BundleFieldDefinition.php delete mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandler.php delete mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php delete mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstaller.php delete mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php delete mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginInterface.php delete mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginUninstallValidator.php delete mode 100644 web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php delete mode 100644 web/modules/contrib/entity/src/Controller/RevisionOverviewController.php delete mode 100644 web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php delete mode 100644 web/modules/contrib/entity/src/EntityAccessControlHandler.php delete mode 100644 web/modules/contrib/entity/src/EntityPermissionProvider.php delete mode 100644 web/modules/contrib/entity/src/EntityPermissionProviderBase.php delete mode 100644 web/modules/contrib/entity/src/EntityPermissionProviderInterface.php delete mode 100644 web/modules/contrib/entity/src/EntityPermissions.php delete mode 100644 web/modules/contrib/entity/src/EntityViewBuilder.php delete mode 100644 web/modules/contrib/entity/src/Form/DeleteMultipleForm.php delete mode 100644 web/modules/contrib/entity/src/Form/RevisionRevertForm.php delete mode 100644 web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php delete mode 100644 web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php delete mode 100644 web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php delete mode 100644 web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php delete mode 100644 web/modules/contrib/entity/src/Plugin/Action/DeleteAction.php delete mode 100644 web/modules/contrib/entity/src/Plugin/Action/Derivative/DeleteActionDeriver.php delete mode 100644 web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php delete mode 100644 web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php delete mode 100644 web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php delete mode 100644 web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php delete mode 100644 web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php delete mode 100644 web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php delete mode 100644 web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php delete mode 100644 web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php delete mode 100644 web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php delete mode 100644 web/modules/contrib/entity/tests/Kernel/RevisionOverviewIntegrationTest.php delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/BundlePluginTestManager.php delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/BundlePluginTestInterface.php delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/First.php delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.permissions.yml delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php delete mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityBundle.php delete mode 100644 web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php delete mode 100644 web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php delete mode 100644 web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php create mode 100644 web/modules/contrib/entity_browser/src/Plugin/views/field/SearchApiSelectForm.php create mode 100644 web/modules/contrib/entity_browser/tests/modules/entity_browser_test/entity_browser_test.services.yml create mode 100644 web/modules/contrib/entity_browser/tests/modules/entity_browser_test/src/Cache/Context/DummyCacheContext.php rename web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/{core.entity_form_display.paragraph.nested_paragrah.default.yml => core.entity_form_display.paragraph.nested_paragraph.default.yml} (95%) delete mode 100644 web/modules/contrib/entity_reference_revisions/LICENSE.txt delete mode 100644 web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml delete mode 100644 web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml delete mode 100644 web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml delete mode 100644 web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module delete mode 100644 web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc delete mode 100644 web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveTrait.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php delete mode 100644 web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php delete mode 100644 web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml delete mode 100644 web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml delete mode 100644 web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php delete mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php delete mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php delete mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php delete mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php delete mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php create mode 100644 web/modules/contrib/entityqueue/composer.json create mode 100644 web/modules/contrib/entityqueue/entityqueue.links.task.yml delete mode 100644 web/modules/contrib/entityqueue/plugins/entityreference/selection/EntityReference_SelectionHandler_EntityQueue.class.php delete mode 100644 web/modules/contrib/entityqueue/plugins/entityreference/selection/entityqueue.inc create mode 100644 web/modules/contrib/entityqueue/src/EntityQueueAccessControlHandler.php create mode 100644 web/modules/contrib/entityqueue/src/Plugin/Derivative/EntityqueueLocalTask.php create mode 100644 web/modules/contrib/entityqueue/src/Plugin/views/sort/EntityQueueInQueue.php create mode 100644 web/modules/contrib/entityqueue/src/Routing/RouteSubscriber.php create mode 100644 web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/entityqueue.entity_queue.simple_queue.yml create mode 100644 web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/entityqueue.entity_queue.test_queue.yml create mode 100644 web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/views.view.entityqueue_test.yml create mode 100644 web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/views.view.simple_queue_listing.yml create mode 100644 web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/schema/entityqueue_test.schema.yml create mode 100644 web/modules/contrib/entityqueue/tests/modules/entityqueue_test/entityqueue_test.info.yml create mode 100644 web/modules/contrib/entityqueue/tests/modules/entityqueue_test/src/Plugin/EntityQueueHandler/Test.php create mode 100644 web/modules/contrib/entityqueue/tests/src/Functional/EntityQueueUiTest.php create mode 100644 web/modules/contrib/entityqueue/tests/src/Kernel/EntityQueueCacheTagsTest.php create mode 100644 web/modules/contrib/eu_cookie_compliance/composer.json create mode 100644 web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.services.yml create mode 100644 web/modules/contrib/eu_cookie_compliance/src/Annotation/ConsentStorage.php create mode 100644 web/modules/contrib/eu_cookie_compliance/src/Controller/StoreConsent.php create mode 100644 web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorage/BasicConsentStorage.php create mode 100644 web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageBase.php create mode 100644 web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageInterface.php create mode 100644 web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageManager.php create mode 100644 web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageManagerInterface.php create mode 100755 web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_withdraw.html.twig rename web/modules/contrib/{entity => file_mdm}/LICENSE.txt (100%) create mode 100644 web/modules/contrib/file_mdm/README.md create mode 100644 web/modules/contrib/file_mdm/composer.json create mode 100644 web/modules/contrib/file_mdm/config/install/file_mdm.file_metadata_plugin.getimagesize.yml create mode 100644 web/modules/contrib/file_mdm/config/install/file_mdm.settings.yml create mode 100644 web/modules/contrib/file_mdm/config/schema/file_mdm.data_types.schema.yml create mode 100644 web/modules/contrib/file_mdm/config/schema/file_mdm.schema.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm.info.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm.links.menu.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm.module create mode 100644 web/modules/contrib/file_mdm/file_mdm.routing.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm.services.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/README.md create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/config/install/file_mdm_exif.file_metadata_plugin.exif.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/config/schema/file_mdm_exif.data_types.schema.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/config/schema/file_mdm_exif.schema.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/file_mdm_exif.info.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/file_mdm_exif.services.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/src/ExifTagMapper.php create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/src/ExifTagMapperInterface.php create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/src/Plugin/FileMetadata/Exif.php create mode 100644 web/modules/contrib/file_mdm/file_mdm_exif/tests/src/Kernel/FileMetadataExifTest.php create mode 100644 web/modules/contrib/file_mdm/file_mdm_font/README.md create mode 100644 web/modules/contrib/file_mdm/file_mdm_font/config/install/file_mdm_font.file_metadata_plugin.font.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_font/config/schema/file_mdm_font.schema.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_font/file_mdm_font.info.yml create mode 100644 web/modules/contrib/file_mdm/file_mdm_font/src/Plugin/FileMetadata/Font.php create mode 100644 web/modules/contrib/file_mdm/file_mdm_font/tests/src/Kernel/FileMetadataFontTest.php create mode 100644 web/modules/contrib/file_mdm/phpcs.xml.dist create mode 100644 web/modules/contrib/file_mdm/src/Element/FileMetadataCaching.php create mode 100644 web/modules/contrib/file_mdm/src/FileMetadata.php create mode 100644 web/modules/contrib/file_mdm/src/FileMetadataException.php create mode 100644 web/modules/contrib/file_mdm/src/FileMetadataInterface.php create mode 100644 web/modules/contrib/file_mdm/src/FileMetadataManager.php create mode 100644 web/modules/contrib/file_mdm/src/FileMetadataManagerInterface.php create mode 100644 web/modules/contrib/file_mdm/src/Form/SettingsForm.php create mode 100644 web/modules/contrib/file_mdm/src/Plugin/Annotation/FileMetadata.php create mode 100644 web/modules/contrib/file_mdm/src/Plugin/FileMetadata/FileMetadataPluginBase.php create mode 100644 web/modules/contrib/file_mdm/src/Plugin/FileMetadata/GetImageSize.php create mode 100644 web/modules/contrib/file_mdm/src/Plugin/FileMetadataPluginInterface.php create mode 100644 web/modules/contrib/file_mdm/src/Plugin/FileMetadataPluginManager.php create mode 100644 web/modules/contrib/file_mdm/tests/files/1024-2006_1011_093752.jpg create mode 100644 web/modules/contrib/file_mdm/tests/files/sample-1.tiff create mode 100644 web/modules/contrib/file_mdm/tests/files/test-exif.jpeg create mode 100644 web/modules/contrib/file_mdm/tests/src/Kernel/FileMetadataManagerTest.php create mode 100644 web/modules/contrib/file_mdm/tests/src/Kernel/FileMetadataManagerTestBase.php rename web/modules/contrib/htmlawed/{htmLawed.module => htmlawed.module} (100%) delete mode 100644 web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/src/Tests/ImageWidgetCropExamplesTest.php create mode 100644 web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/tests/src/Functional/ImageWidgetCropExamplesTest.php delete mode 100644 web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/src/IefEditTest.php rename web/modules/contrib/linkchecker/{migration_templates => migrations}/d6_linkchecker_settings.yml (100%) rename web/modules/contrib/linkchecker/{migration_templates => migrations}/d7_linkchecker_settings.yml (100%) rename web/modules/contrib/linkchecker/src/Commands/{LinkcheckerCommands.php => LinkCheckerCommands.php} (97%) delete mode 100644 web/modules/contrib/media_entity/LICENSE.txt delete mode 100644 web/modules/contrib/media_entity/README.md delete mode 100644 web/modules/contrib/media_entity/drush.services.yml delete mode 100644 web/modules/contrib/media_entity/images/icons/generic.png delete mode 100644 web/modules/contrib/media_entity/media_entity.drush.inc delete mode 100644 web/modules/contrib/media_entity/media_entity.info.yml delete mode 100644 web/modules/contrib/media_entity/media_entity.install delete mode 100644 web/modules/contrib/media_entity/media_entity.module delete mode 100644 web/modules/contrib/media_entity/media_entity.services.yml delete mode 100644 web/modules/contrib/media_entity/src/Annotation/MediaType.php delete mode 100644 web/modules/contrib/media_entity/src/CliService.php delete mode 100644 web/modules/contrib/media_entity/src/Commands/DrushCommands.php delete mode 100644 web/modules/contrib/media_entity/src/Media.php delete mode 100644 web/modules/contrib/media_entity/src/MediaBundle.php delete mode 100644 web/modules/contrib/media_entity/src/MediaTypeManager.php delete mode 100644 web/modules/contrib/media_entity/tests/fixtures/drupal-8.4.0-media-entity.php.gz delete mode 100644 web/modules/contrib/media_entity/tests/src/Functional/CoreMediaUpdatePathTest.php create mode 100644 web/modules/contrib/memcache/example.services.yml create mode 100644 web/modules/contrib/memcache/memcache_admin/config/install/memcache_admin.settings.yml create mode 100644 web/modules/contrib/memcache/memcache_admin/memcache_admin.info.yml create mode 100644 web/modules/contrib/memcache/memcache_admin/memcache_admin.install create mode 100644 web/modules/contrib/memcache/memcache_admin/memcache_admin.links.menu.yml create mode 100644 web/modules/contrib/memcache/memcache_admin/memcache_admin.permissions.yml create mode 100644 web/modules/contrib/memcache/memcache_admin/memcache_admin.routing.yml create mode 100644 web/modules/contrib/memcache/memcache_admin/memcache_admin.services.yml create mode 100644 web/modules/contrib/memcache/memcache_admin/src/Controller/MemcacheStatisticsController.php create mode 100644 web/modules/contrib/memcache/memcache_admin/src/EventSubscriber/MemcacheAdminSubscriber.php create mode 100644 web/modules/contrib/memcache/memcache_admin/src/Form/MemcacheAdminSettingsForm.php create mode 100644 web/modules/contrib/memcache/src/Cache/TimestampCacheTagsChecksum.php create mode 100644 web/modules/contrib/memcache/src/Connection/MemcacheConnection.php create mode 100644 web/modules/contrib/memcache/src/Connection/MemcacheConnectionInterface.php create mode 100755 web/modules/contrib/memcache/src/Connection/MemcachedConnection.php create mode 100644 web/modules/contrib/memcache/src/Driver/DriverBase.php create mode 100644 web/modules/contrib/memcache/src/Driver/MemcacheDriver.php create mode 100644 web/modules/contrib/memcache/src/Driver/MemcacheDriverFactory.php create mode 100755 web/modules/contrib/memcache/src/Driver/MemcachedDriver.php delete mode 100644 web/modules/contrib/memcache/src/DrupalMemcache.php delete mode 100644 web/modules/contrib/memcache/src/DrupalMemcacheBase.php delete mode 100644 web/modules/contrib/memcache/src/DrupalMemcacheFactory.php delete mode 100755 web/modules/contrib/memcache/src/DrupalMemcached.php create mode 100644 web/modules/contrib/memcache/src/Invalidator/MemcacheTimestampInvalidator.php create mode 100644 web/modules/contrib/memcache/src/Invalidator/TimestampInvalidatorBase.php create mode 100644 web/modules/contrib/memcache/src/Invalidator/TimestampInvalidatorInterface.php delete mode 100644 web/modules/contrib/memcache/src/Lock/PersistentMemcacheLockBackend.php rename web/modules/contrib/memcache/src/{DrupalMemcacheConfig.php => MemcacheSettings.php} (89%) rename web/modules/contrib/memcache/{src/Tests/MemcacheBackendUnitTest.php => tests/src/Kernel/MemcacheBackendTest.php} (56%) rename web/modules/contrib/memcache/tests/src/Unit/{DrupalMemcacheConfigTest.php => MemcacheSettingsTest.php} (76%) create mode 100644 web/modules/contrib/metatag/config/schema/metatag.settings.schema.yml create mode 100644 web/modules/contrib/metatag/metatag.links.task.yml create mode 100644 web/modules/contrib/metatag/metatag_mobile/src/Plugin/metatag/Tag/WebManifest.php create mode 100644 web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/OgImageAlt.php create mode 100644 web/modules/contrib/metatag/metatag_page_manager/metatag_page_manager.info.yml create mode 100644 web/modules/contrib/metatag/metatag_page_manager/metatag_page_manager.module create mode 100644 web/modules/contrib/metatag/metatag_page_manager/src/Tests/Functional/MetatagPageManagerTest.php create mode 100644 web/modules/contrib/metatag/src/Form/MetatagSettingsForm.php create mode 100644 web/modules/contrib/metatag/src/Plugin/GraphQL/Scalars/MetatagScalar.php delete mode 100644 web/modules/contrib/metatag/src/Plugin/metatag/Group/AppLinks.php delete mode 100644 web/modules/contrib/metatag/src/Plugin/metatag/Group/DublinCoreAdvanced.php delete mode 100644 web/modules/contrib/metatag/src/Plugin/metatag/Group/DublinCoreBasic.php delete mode 100644 web/modules/contrib/metatag/src/Plugin/metatag/Group/Facebook.php delete mode 100644 web/modules/contrib/metatag/src/Plugin/metatag/Group/GooglePlus.php delete mode 100644 web/modules/contrib/metatag/src/Plugin/metatag/Group/SiteValidation.php delete mode 100644 web/modules/contrib/metatag/src/Plugin/metatag/Group/TwitterCards.php create mode 100644 web/modules/contrib/metatag/src/Plugin/metatag/Tag/Google.php create mode 100644 web/modules/contrib/metatag/tests/src/Functional/SchemaMetatagTest.php create mode 100644 web/modules/contrib/migrate_plus/src/Plugin/migrate/process/MultipleValues.php create mode 100644 web/modules/contrib/migrate_plus/src/Plugin/migrate/process/SingleValue.php create mode 100644 web/modules/contrib/migrate_plus/tests/data/missing_properties.json create mode 100644 web/modules/contrib/migrate_plus/tests/data/simple_xml_reduce_single_value.xml create mode 100644 web/modules/contrib/migrate_plus/tests/src/Functional/LoadTest.php create mode 100644 web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php create mode 100644 web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php create mode 100644 web/modules/contrib/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php create mode 100644 web/modules/contrib/migrate_plus/tests/src/Unit/process/SingleValueTest.php create mode 100644 web/modules/contrib/migrate_tools/composer.json create mode 100644 web/modules/contrib/migrate_tools/drush.services.yml create mode 100644 web/modules/contrib/migrate_tools/migrate_tools.services.yml create mode 100644 web/modules/contrib/migrate_tools/phpcs.xml create mode 100644 web/modules/contrib/migrate_tools/src/Commands/MigrateToolsCommands.php create mode 100644 web/modules/contrib/migrate_tools/src/Drush9LogMigrateMessage.php create mode 100644 web/modules/contrib/migrate_tools/src/Form/MigrationExecuteForm.php create mode 100644 web/modules/contrib/migrate_tools/src/Form/SourceCsvForm.php create mode 100644 web/modules/contrib/migrate_tools/src/MigrateBatchExecutable.php create mode 100644 web/modules/contrib/migrate_tools/src/Routing/RouteProcessor.php create mode 100644 web/modules/contrib/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml create mode 100644 web/modules/contrib/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml create mode 100644 web/modules/contrib/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml create mode 100644 web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml create mode 100644 web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml create mode 100644 web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml create mode 100644 web/modules/contrib/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php create mode 100644 web/modules/contrib/migrate_tools/tests/src/Functional/SourceCsvFormTest.php create mode 100644 web/modules/contrib/migrate_upgrade/drush.services.yml create mode 100644 web/modules/contrib/migrate_upgrade/src/Commands/MigrateUpgradeCommands.php delete mode 100644 web/modules/contrib/paragraphs/LICENSE.txt delete mode 100644 web/modules/contrib/paragraphs/README.txt delete mode 100644 web/modules/contrib/paragraphs/composer.json delete mode 100644 web/modules/contrib/paragraphs/config/install/core.entity_view_mode.paragraph.preview.yml delete mode 100644 web/modules/contrib/paragraphs/config/schema/paragraphs_type.schema.yml delete mode 100644 web/modules/contrib/paragraphs/css/.gitignore delete mode 100644 web/modules/contrib/paragraphs/css/.stylelintrc.yml delete mode 100644 web/modules/contrib/paragraphs/css/README.md delete mode 100644 web/modules/contrib/paragraphs/css/_variables.scss delete mode 100644 web/modules/contrib/paragraphs/css/gulp-options.yml delete mode 100644 web/modules/contrib/paragraphs/css/gulp-tasks.js delete mode 100644 web/modules/contrib/paragraphs/css/gulpfile.js delete mode 100644 web/modules/contrib/paragraphs/css/package.json delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.actions.css delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.actions.scss delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.admin.css delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.dragdrop.css delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.dragdrop.scss delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.list-builder.css delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.list-builder.scss delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.modal.css delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.modal.scss delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.widget.css delete mode 100644 web/modules/contrib/paragraphs/css/paragraphs.widget.scss delete mode 100644 web/modules/contrib/paragraphs/icons/icon-actions.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-changed.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-collapse-disabled.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-collapse.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-delete-disabled.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-delete.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-edit-disabled.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-edit-info.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-edit.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-error.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-lock.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-view.svg delete mode 100644 web/modules/contrib/paragraphs/icons/icon-warning.svg delete mode 100644 web/modules/contrib/paragraphs/js/paragraphs.actions.js delete mode 100644 web/modules/contrib/paragraphs/js/paragraphs.admin.js delete mode 100644 web/modules/contrib/paragraphs/js/paragraphs.dragdrop.js delete mode 100644 web/modules/contrib/paragraphs/js/paragraphs.modal.js delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.node.paragraphed_content_demo.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.image_text.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.images.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.nested_paragraph.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.text.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.text_image.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.user.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.node.paragraphed_content_demo.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.node.paragraphed_content_demo.teaser.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.image_text.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.images.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.nested_paragraph.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.text.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.text_image.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.user.default.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.node.paragraphed_content_demo.field_paragraphs_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.image_text.field_image_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.image_text.field_text_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.images.field_images_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.nested_paragraph.field_paragraphs_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text.field_text_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text_image.field_image_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text_image.field_text_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.user.field_user_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.node.field_paragraphs_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_image_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_images_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_paragraphs_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_text_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_user_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.node.paragraphed_content_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.image_text.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.images.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.text.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.text_image.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.user.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/node.type.paragraphed_content_demo.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.image_text.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.images.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.nested_paragraph.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.text.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.text_image.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.user.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/language.entity.de.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/language.entity.fr.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/search_api.index.paragraphs_demo_index.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/search_api.server.paragraphs_demo_server.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/views.view.paragraphs_search.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/css/paragraphs_demo.css delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.info.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.install delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.libraries.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.module delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_demo/src/Tests/ParagraphsDemoTest.php delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.info.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.module delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.permissions.yml delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/src/ParagraphsTypePermissions.php delete mode 100644 web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/src/Tests/ParagraphsTypePermissionsTest.php delete mode 100644 web/modules/contrib/paragraphs/paragraphs.api.php delete mode 100644 web/modules/contrib/paragraphs/paragraphs.info.yml delete mode 100644 web/modules/contrib/paragraphs/paragraphs.install delete mode 100644 web/modules/contrib/paragraphs/paragraphs.libraries.yml delete mode 100644 web/modules/contrib/paragraphs/paragraphs.links.action.yml delete mode 100644 web/modules/contrib/paragraphs/paragraphs.links.menu.yml delete mode 100644 web/modules/contrib/paragraphs/paragraphs.links.task.yml delete mode 100644 web/modules/contrib/paragraphs/paragraphs.module delete mode 100644 web/modules/contrib/paragraphs/paragraphs.permissions.yml delete mode 100644 web/modules/contrib/paragraphs/paragraphs.post_update.php delete mode 100644 web/modules/contrib/paragraphs/paragraphs.routing.yml delete mode 100755 web/modules/contrib/paragraphs/paragraphs.services.yml delete mode 100644 web/modules/contrib/paragraphs/src/Annotation/ParagraphsBehavior.php delete mode 100644 web/modules/contrib/paragraphs/src/Controller/ParagraphsTypeListBuilder.php delete mode 100644 web/modules/contrib/paragraphs/src/Element/ParagraphOperations.php delete mode 100644 web/modules/contrib/paragraphs/src/Element/ParagraphsActions.php delete mode 100644 web/modules/contrib/paragraphs/src/Entity/Paragraph.php delete mode 100644 web/modules/contrib/paragraphs/src/Entity/ParagraphsType.php delete mode 100644 web/modules/contrib/paragraphs/src/EventSubscriber/ReplicateFieldSubscriber.php delete mode 100644 web/modules/contrib/paragraphs/src/Feeds/Target/Paragraphs.php delete mode 100644 web/modules/contrib/paragraphs/src/Form/ParagraphsTypeDeleteConfirm.php delete mode 100644 web/modules/contrib/paragraphs/src/Form/ParagraphsTypeForm.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphAccessControlHandler.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphInterface.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphStorageSchema.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphViewBuilder.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphsBehaviorBase.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphsBehaviorCollection.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphsBehaviorInterface.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphsBehaviorManager.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphsServiceProvider.php delete mode 100644 web/modules/contrib/paragraphs/src/ParagraphsTypeInterface.php delete mode 100644 web/modules/contrib/paragraphs/src/Plugin/EntityReferenceSelection/ParagraphSelection.php delete mode 100644 web/modules/contrib/paragraphs/src/Plugin/Field/FieldFormatter/ParagraphsSummaryFormatter.php delete mode 100644 web/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php delete mode 100644 web/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/ParagraphsWidget.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAccessTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAddModesTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAdministrationTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsConfigTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsContactTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsCoreVersionUiTestTrait.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsEditModesTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsEntityTranslationWithNonTranslatableParagraphs.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsFieldGroupTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsInlineEntityFormTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsPreviewTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsSummaryFormatterTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTestBase.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTranslationTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTypesTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsUiTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsWidgetButtonsTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAccessTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAddModesTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAdministrationTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalBehaviorsTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalConfigTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalContactTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalDuplicateFeatureTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalEditModesTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalEntityTranslationWithNonTranslatableParagraphs.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalFieldGroupTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalHeaderActionsTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalInlineEntityFormTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalPreviewTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalReplicateEnableTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalSummaryFormatterTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTestBase.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTranslationTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTypesTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalUiTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalWidgetButtonsTest.php delete mode 100644 web/modules/contrib/paragraphs/src/Tests/ParagraphsUninstallTest.php delete mode 100644 web/modules/contrib/paragraphs/templates/paragraph.html.twig delete mode 100644 web/modules/contrib/paragraphs/templates/paragraphs-actions.html.twig delete mode 100644 web/modules/contrib/paragraphs/templates/paragraphs-add-dialog.html.twig delete mode 100644 web/modules/contrib/paragraphs/templates/paragraphs-dropbutton-wrapper.html.twig delete mode 100644 web/modules/contrib/paragraphs/templates/paragraphs-info-icon.html.twig delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/config/schema/paragraphs_test.schema.yml delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/css/bold-text.css delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/css/color-text.css delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.info.yml delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.libraries.yml delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.module delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestBoldTextBehavior.php delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestDummyBehavior.php delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestFieldsSelectionBehavior.php delete mode 100644 web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestTextColorBehavior.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalBehaviorsTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalDragAndDropModeTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalWidgetButtonsTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/LoginAdminTrait.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsExperimentalAddWidgetTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsExperimentalEditPerspectivesUiTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsTestBaseTrait.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsBehaviorPluginsTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsCollapsedSummaryTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsCompositeRelationshipTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsIsChangedTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsReplicateTest.php delete mode 100644 web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsTypeHasEnabledBehaviorPluginTest.php create mode 100644 web/modules/contrib/permissions_by_term/bitbucket-pipelines.yml create mode 100644 web/modules/contrib/permissions_by_term/js/.babelrc delete mode 100644 web/modules/contrib/permissions_by_term/js/node-form.js delete mode 100644 web/modules/contrib/permissions_by_term/js/node-form.prototype.js create mode 100644 web/modules/contrib/permissions_by_term/js/package-lock.json delete mode 100644 web/modules/contrib/permissions_by_term/js/spec/node-form.spec.js delete mode 100644 web/modules/contrib/permissions_by_term/js/spec/support/jasmine.json create mode 100644 web/modules/contrib/permissions_by_term/js/src/async-function/create-permission.js create mode 100644 web/modules/contrib/permissions_by_term/js/src/async-function/fetch-from-backend.js create mode 100644 web/modules/contrib/permissions_by_term/js/src/client/dom-client.prototype.js create mode 100644 web/modules/contrib/permissions_by_term/js/src/client/permission-output-collector.prototype.js create mode 100644 web/modules/contrib/permissions_by_term/js/src/client/term-collector.prototype.js create mode 100644 web/modules/contrib/permissions_by_term/js/src/drupal-behavior-function/node-form-client.js create mode 100644 web/modules/contrib/permissions_by_term/js/src/model/backend.prototype.js create mode 100644 web/modules/contrib/permissions_by_term/js/src/model/permission-output.prototype.js create mode 100644 web/modules/contrib/permissions_by_term/js/test/dom-client.tests.js create mode 100644 web/modules/contrib/permissions_by_term/js/test/permission-creator.tests.js create mode 100644 web/modules/contrib/permissions_by_term/js/test/permission-output.tests.js create mode 100644 web/modules/contrib/permissions_by_term/js/test/term-selector.tests.js create mode 100644 web/modules/contrib/permissions_by_term/js/test/tests.html create mode 100644 web/modules/contrib/permissions_by_term/js/webpack-dist/bundle-test.js create mode 100644 web/modules/contrib/permissions_by_term/js/webpack-dist/bundle.js create mode 100644 web/modules/contrib/permissions_by_term/js/webpack.production.js delete mode 100644 web/modules/contrib/permissions_by_term/js/yarn.lock rename web/modules/contrib/permissions_by_term/src/Service/{Term.php => TermHandler.php} (82%) delete mode 100644 web/modules/contrib/permissions_by_term/tests/src/Behat/Features/entityMetaInfo.feature create mode 100644 web/modules/contrib/permissions_by_term/tests/src/Behat/composer-require-namespace.php delete mode 100644 web/modules/contrib/permissions_by_term/tests/src/Behat/composer.json create mode 100644 web/modules/contrib/permissions_by_term/tests/src/Kernel/AccessStorageTest.php create mode 100644 web/modules/contrib/php/php.services.yml create mode 100644 web/modules/contrib/php/src/PhpUninstallValidator.php create mode 100644 web/modules/contrib/php/src/Tests/PhpUninstallTest.php create mode 100644 web/modules/contrib/redirect/src/RedirectViewsData.php mode change 100644 => 100755 web/modules/contrib/token/README.md create mode 100644 web/modules/contrib/token/tests/modules/token_module_test/test_views/views.view.token_views_test.yml create mode 100644 web/modules/contrib/token/tests/src/Functional/UrlTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/UrlTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/ViewsTest.php diff --git a/composer.json b/composer.json index 63caacd56..5e6145fec 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "drupal/token": "^1.0", "drupal/ctools": "^3.0", "drupal/migrate_upgrade": "^3.0", - "drupal/migrate_tools": "^3.0", + "drupal/migrate_tools": "^4.0", "drupal/bootstrap": "^3.2", "drupal/admin_toolbar": "^1.18", "drupal/pathauto": "^1.0", @@ -32,7 +32,6 @@ "drupal/simple_sitemap": "^2.9", "drupal/eu_cookie_compliance": "^1.0@beta", "twbs/bootstrap-sass": "^3.3", - "drupal/paragraphs": "^1.1", "drupal/front": "^1.0@beta", "drupal/filefield_sources": "1.x-dev", "drupal/php": "1.x-dev", @@ -52,7 +51,6 @@ "drupal/fontyourface": "^3.0", "drupal/devel": "^1.0", "drupal/drupalmoduleupgrader": "^1.2", - "drupal/security_review": "1.x-dev", "drupal/memcache": "^2.0@alpha", "enyo/dropzone": "^4.3", "drush/config-extra": "^1.0", @@ -64,9 +62,9 @@ "mehrpadin/superfish": "^2.0", "drupal/superfish": "^1.0@RC", "drupal/entity_browser": "^1.0", - "drupal/views_bootstrap": "3.0", + "drupal/views_bootstrap": "^3.0", "drupal/views_responsive_grid": "@dev", - "drupal/imagemagick": "^1.0@alpha", + "drupal/imagemagick": "^2.0", "drupal/diff": "^1.0@RC", "drupal/permissions_by_term": "^1.19", "drupal/linkit": "^4.3", @@ -80,20 +78,18 @@ "drupal/layout_plugin": "^1.0@alpha", "drupal/draggableviews": "^1.0", "roave/security-advisories": "dev-master", - - "drupal/video_embed_field": "^2.0", + "drupal/video_embed_field": "^2.0", "drupal/slick_media":"^2.0", "drupal/media_entity_twitter":"^2.0", "drupal/media_entity_slideshow":"^2.0", "drupal/media_entity_instagram":"^2.0", "drupal/image_widget_crop":"^2.1", "drupal/crop":"^2.0", - "drupal/media_entity":"^2.0", "drupal/media_entity_actions":"^1.0", "drupal/inline_entity_form": "^1.0@beta", "drupal/entity_embed": "^1.0@beta", - "drupal/dropzonejs": "^2.0@alpha" - + "drupal/dropzonejs": "^2.0@alpha", + "drupal/security_review": "1.x-dev" }, "require-dev": { "behat/mink": "~1.7", @@ -176,7 +172,7 @@ }, "config": { "platform": { - "php": "7.0.27" + "php": "7.0.30" } } } diff --git a/composer.lock b/composer.lock index 87df4c691..ccd2b1cc1 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7759d61f833cd58108a5bfa9a47e551b", + "content-hash": "9bed23b474ece9af626936ebc6cb7d4b", "packages": [ { "name": "alchemy/zippy", @@ -240,19 +240,20 @@ }, { "name": "chi-teck/drupal-code-generator", - "version": "1.24.0", + "version": "1.27.0", "source": { "type": "git", "url": "https://github.com/Chi-teck/drupal-code-generator.git", - "reference": "20682ec4acc92e9671ba7b9bd0e972978c3a08f2" + "reference": "a839bc89d385087d8a7a96a9c1c4bd470ffb627e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Chi-teck/drupal-code-generator/zipball/20682ec4acc92e9671ba7b9bd0e972978c3a08f2", - "reference": "20682ec4acc92e9671ba7b9bd0e972978c3a08f2", + "url": "https://api.github.com/repos/Chi-teck/drupal-code-generator/zipball/a839bc89d385087d8a7a96a9c1c4bd470ffb627e", + "reference": "a839bc89d385087d8a7a96a9c1c4bd470ffb627e", "shasum": "" }, "require": { + "ext-json": "*", "php": ">=5.5.9", "symfony/console": "~2.7|^3", "symfony/filesystem": "~2.7|^3", @@ -262,6 +263,11 @@ "bin/dcg" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { "files": [ "src/bootstrap.php" @@ -275,20 +281,20 @@ "GPL-2.0-or-later" ], "description": "Drupal code generator", - "time": "2018-04-25T17:35:50+00:00" + "time": "2018-10-11T08:05:59+00:00" }, { "name": "composer/installers", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/composer/installers.git", - "reference": "049797d727261bf27f2690430d935067710049c2" + "reference": "cfcca6b1b60bc4974324efb5783c13dca6932b5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/049797d727261bf27f2690430d935067710049c2", - "reference": "049797d727261bf27f2690430d935067710049c2", + "url": "https://api.github.com/repos/composer/installers/zipball/cfcca6b1b60bc4974324efb5783c13dca6932b5b", + "reference": "cfcca6b1b60bc4974324efb5783c13dca6932b5b", "shasum": "" }, "require": { @@ -395,7 +401,7 @@ "zend", "zikula" ], - "time": "2017-12-29T09:13:20+00:00" + "time": "2018-08-27T06:10:37+00:00" }, { "name": "composer/semver", @@ -461,16 +467,16 @@ }, { "name": "consolidation/annotated-command", - "version": "2.8.3", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "8f8f5da2ca06fbd3a85f7d551c49f844b7c59437" + "reference": "4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/8f8f5da2ca06fbd3a85f7d551c49f844b7c59437", - "reference": "8f8f5da2ca06fbd3a85f7d551c49f844b7c59437", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac", + "reference": "4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac", "shasum": "" }, "require": { @@ -482,9 +488,9 @@ "symfony/finder": "^2.5|^3|^4" }, "require-dev": { - "greg-1-anderson/composer-test-scenarios": "^1", - "phpunit/phpunit": "^4.8", - "satooshi/php-coveralls": "^1.0.2 | dev-master", + "g1a/composer-test-scenarios": "^2", + "phpunit/phpunit": "^6", + "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.7" }, "type": "library", @@ -509,20 +515,20 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-02-23T16:32:04+00:00" + "time": "2018-09-19T17:47:18+00:00" }, { "name": "consolidation/config", - "version": "1.0.9", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/consolidation/config.git", - "reference": "34ca8d7c1ee60a7b591b10617114cf1210a2e92c" + "reference": "925231dfff32f05b787e1fddb265e789b939cf4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/34ca8d7c1ee60a7b591b10617114cf1210a2e92c", - "reference": "34ca8d7c1ee60a7b591b10617114cf1210a2e92c", + "url": "https://api.github.com/repos/consolidation/config/zipball/925231dfff32f05b787e1fddb265e789b939cf4c", + "reference": "925231dfff32f05b787e1fddb265e789b939cf4c", "shasum": "" }, "require": { @@ -531,8 +537,8 @@ "php": ">=5.4.0" }, "require-dev": { - "greg-1-anderson/composer-test-scenarios": "^1", - "phpunit/phpunit": "^4", + "g1a/composer-test-scenarios": "^1", + "phpunit/phpunit": "^5", "satooshi/php-coveralls": "^1.0", "squizlabs/php_codesniffer": "2.*", "symfony/console": "^2.5|^3|^4", @@ -563,20 +569,20 @@ } ], "description": "Provide configuration services for a commandline tool.", - "time": "2017-12-22T17:28:19+00:00" + "time": "2018-10-24T17:55:35+00:00" }, { "name": "consolidation/log", - "version": "1.0.5", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/consolidation/log.git", - "reference": "dbc7c535f319a4a2d5a5077738f8eb7c10df8821" + "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/log/zipball/dbc7c535f319a4a2d5a5077738f8eb7c10df8821", - "reference": "dbc7c535f319a4a2d5a5077738f8eb7c10df8821", + "url": "https://api.github.com/repos/consolidation/log/zipball/dfd8189a771fe047bf3cd669111b2de5f1c79395", + "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395", "shasum": "" }, "require": { @@ -585,8 +591,9 @@ "symfony/console": "^2.8|^3|^4" }, "require-dev": { + "g1a/composer-test-scenarios": "^1", "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "dev-master", + "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "2.*" }, "type": "library", @@ -611,29 +618,30 @@ } ], "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", - "time": "2017-11-29T01:44:16+00:00" + "time": "2018-05-25T18:14:39+00:00" }, { "name": "consolidation/output-formatters", - "version": "3.2.0", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "da889e4bce19f145ca4ec5b1725a946f4eb625a9" + "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/da889e4bce19f145ca4ec5b1725a946f4eb625a9", - "reference": "da889e4bce19f145ca4ec5b1725a946f4eb625a9", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/a942680232094c4a5b21c0b7e54c20cce623ae19", + "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19", "shasum": "" }, "require": { + "dflydev/dot-access-data": "^1.1.0", "php": ">=5.4.0", "symfony/console": "^2.8|^3|^4", "symfony/finder": "^2.5|^3|^4" }, "require-dev": { - "g-1-a/composer-test-scenarios": "^2", + "g1a/composer-test-scenarios": "^2", "phpunit/phpunit": "^5.7.27", "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.7", @@ -666,27 +674,29 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2018-03-20T15:18:32+00:00" + "time": "2018-10-19T22:35:38+00:00" }, { "name": "consolidation/robo", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "54a13e268917b92576d75e10dca8227b95a574d9" + "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/54a13e268917b92576d75e10dca8227b95a574d9", - "reference": "54a13e268917b92576d75e10dca8227b95a574d9", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", + "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", "shasum": "" }, "require": { "consolidation/annotated-command": "^2.8.2", - "consolidation/config": "^1.0.1", + "consolidation/config": "^1.0.10", "consolidation/log": "~1", "consolidation/output-formatters": "^3.1.13", + "consolidation/self-update": "^1", + "g1a/composer-test-scenarios": "^2", "grasmash/yaml-expander": "^1.3", "league/container": "^2.2", "php": ">=5.5.0", @@ -703,7 +713,6 @@ "codeception/aspect-mock": "^1|^2.1.1", "codeception/base": "^2.3.7", "codeception/verify": "^0.3.2", - "g-1-a/composer-test-scenarios": "^2", "goaop/framework": "~2.1.2", "goaop/parser-reflection": "^1.1.0", "natxet/cssmin": "3.0.4", @@ -746,7 +755,113 @@ } ], "description": "Modern task runner", - "time": "2018-04-06T05:27:37+00:00" + "time": "2018-08-17T18:44:18+00:00" + }, + { + "name": "consolidation/self-update", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/consolidation/self-update.git", + "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consolidation/self-update/zipball/a1c273b14ce334789825a09d06d4c87c0a02ad54", + "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54", + "shasum": "" + }, + "require": { + "php": ">=5.5.0", + "symfony/console": "^2.8|^3|^4", + "symfony/filesystem": "^2.5|^3|^4" + }, + "bin": [ + "scripts/release" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "SelfUpdate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Anderson", + "email": "greg.1.anderson@greenknowe.org" + }, + { + "name": "Alexander Menk", + "email": "menk@mestrona.net" + } + ], + "description": "Provides a self:update command for Symfony Console applications.", + "time": "2018-10-28T01:52:03+00:00" + }, + { + "name": "consolidation/site-alias", + "version": "1.1.11", + "source": { + "type": "git", + "url": "https://github.com/consolidation/site-alias.git", + "reference": "54ea74ee7dbd54ef356798028ca9a3548cb8df14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consolidation/site-alias/zipball/54ea74ee7dbd54ef356798028ca9a3548cb8df14", + "reference": "54ea74ee7dbd54ef356798028ca9a3548cb8df14", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "consolidation/robo": "^1.2.3", + "g1a/composer-test-scenarios": "^2", + "knplabs/github-api": "^2.7", + "php-http/guzzle6-adapter": "^1.1", + "phpunit/phpunit": "^5", + "satooshi/php-coveralls": "^2", + "squizlabs/php_codesniffer": "^2.8", + "symfony/console": "^2.8|^3|^4", + "symfony/yaml": "~2.3|^3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Consolidation\\SiteAlias\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Moshe Weitzman", + "email": "weitzman@tejasa.com" + }, + { + "name": "Greg Anderson", + "email": "greg.1.anderson@greenknowe.org" + } + ], + "description": "Manage alias records for local and remote sites.", + "time": "2018-11-03T05:07:56+00:00" }, { "name": "container-interop/container-interop", @@ -781,16 +896,16 @@ }, { "name": "cweagans/composer-patches", - "version": "1.6.4", + "version": "1.6.5", "source": { "type": "git", "url": "https://github.com/cweagans/composer-patches.git", - "reference": "462e65061606dc6149349535d4322241515d1b16" + "reference": "2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/462e65061606dc6149349535d4322241515d1b16", - "reference": "462e65061606dc6149349535d4322241515d1b16", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3", + "reference": "2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3", "shasum": "" }, "require": { @@ -812,7 +927,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "BSD-3-Clause" ], "authors": [ { @@ -821,20 +936,20 @@ } ], "description": "Provides a way to patch Composer packages.", - "time": "2017-12-07T16:16:31+00:00" + "time": "2018-05-11T18:00:16+00:00" }, { "name": "dflydev/dot-access-configuration", - "version": "v1.0.2", + "version": "v1.0.3", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-configuration.git", - "reference": "ae6e7138b1d9063d343322cca63994ee1ac5161d" + "reference": "2e6eb0c8b8830b26bb23defcfc38d4276508fc49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-configuration/zipball/ae6e7138b1d9063d343322cca63994ee1ac5161d", - "reference": "ae6e7138b1d9063d343322cca63994ee1ac5161d", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-configuration/zipball/2e6eb0c8b8830b26bb23defcfc38d4276508fc49", + "reference": "2e6eb0c8b8830b26bb23defcfc38d4276508fc49", "shasum": "" }, "require": { @@ -881,7 +996,7 @@ "config", "configuration" ], - "time": "2016-12-12T17:43:40+00:00" + "time": "2018-09-08T23:00:17+00:00" }, { "name": "dflydev/dot-access-data", @@ -1428,16 +1543,16 @@ }, { "name": "drupal-composer/drupal-scaffold", - "version": "2.4.0", + "version": "2.5.4", "source": { "type": "git", "url": "https://github.com/drupal-composer/drupal-scaffold.git", - "reference": "745f0a2d4141fc83d3b42222beff43d66afb3dc6" + "reference": "fc6bf4ceecb5d47327f54d48d4d4f67b17da956d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal-composer/drupal-scaffold/zipball/745f0a2d4141fc83d3b42222beff43d66afb3dc6", - "reference": "745f0a2d4141fc83d3b42222beff43d66afb3dc6", + "url": "https://api.github.com/repos/drupal-composer/drupal-scaffold/zipball/fc6bf4ceecb5d47327f54d48d4d4f67b17da956d", + "reference": "fc6bf4ceecb5d47327f54d48d4d4f67b17da956d", "shasum": "" }, "require": { @@ -1447,7 +1562,9 @@ }, "require-dev": { "composer/composer": "dev-master", - "phpunit/phpunit": "^4.4.0" + "g1a/composer-test-scenarios": "^2.1.0", + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "^2.8" }, "type": "composer-plugin", "extra": { @@ -1463,24 +1580,24 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "description": "Composer Plugin for updating the Drupal scaffold files when using drupal/core", - "time": "2017-12-08T22:53:11+00:00" + "time": "2018-07-27T10:07:07+00:00" }, { "name": "drupal/admin_toolbar", - "version": "1.23.0", + "version": "1.24.0", "source": { "type": "git", "url": "https://git.drupal.org/project/admin_toolbar", - "reference": "8.x-1.23" + "reference": "8.x-1.24" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-1.23.zip", - "reference": "8.x-1.23", - "shasum": "46d7ed18a9154c35e765ae43ce43aa292c025d65" + "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-1.24.zip", + "reference": "8.x-1.24", + "shasum": "682ffa443a1e339583022eb6adbb00c1103ea6d4" }, "require": { "drupal/core": "*" @@ -1491,8 +1608,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.23", - "datestamp": "1517936581", + "version": "8.x-1.24", + "datestamp": "1527523080", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -1505,20 +1622,25 @@ ], "authors": [ { - "name": "Mohamed Anis Taktak", - "homepage": "https://www.drupal.org/u/matio89" + "name": "Wilfrid Roze (eme)", + "homepage": "https://www.drupal.org/u/eme", + "role": "Maintainer" }, { - "name": "adriancid", - "homepage": "https://www.drupal.org/user/1962106" + "name": "Romain Jarraud (romainj)", + "homepage": "https://www.drupal.org/u/romainj", + "role": "Maintainer" }, { - "name": "bolbol", - "homepage": "https://www.drupal.org/user/3400070" + "name": "Adrian Cid Almaguer (adriancid)", + "homepage": "https://www.drupal.org/u/adriancid", + "email": "adriancid@gmail.com", + "role": "Maintainer" }, { - "name": "eme", - "homepage": "https://www.drupal.org/user/542492" + "name": "Mohamed Anis Taktak (matio89)", + "homepage": "https://www.drupal.org/u/matio89", + "role": "Maintainer" }, { "name": "fethi.krout", @@ -1533,8 +1655,12 @@ "homepage": "https://www.drupal.org/user/370706" } ], - "description": "Admin Toolbar improve the default Drupal Toolbar, it lets the hover of sub menus.", + "description": "Provides a drop-down menu interface to the core Drupal Toolbar.", "homepage": "http://drupal.org/project/admin_toolbar", + "keywords": [ + "Drupal", + "Toolbar" + ], "support": { "source": "http://cgit.drupalcode.org/admin_toolbar", "issues": "https://www.drupal.org/project/issues/admin_toolbar" @@ -1614,17 +1740,17 @@ }, { "name": "drupal/advanced_help", - "version": "1.0.0-alpha1", + "version": "1.0.0-alpha2", "source": { "type": "git", "url": "https://git.drupal.org/project/advanced_help", - "reference": "8.x-1.0-alpha1" + "reference": "8.x-1.0-alpha2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/advanced_help-8.x-1.0-alpha1.zip", - "reference": "8.x-1.0-alpha1", - "shasum": "a8d48d3cef48d648b3f7d5e8d0bd302cb797df8c" + "url": "https://ftp.drupal.org/files/projects/advanced_help-8.x-1.0-alpha2.zip", + "reference": "8.x-1.0-alpha2", + "shasum": "15893996c66bcb81e200dfb9871df6f0da682cc0" }, "require": { "drupal/core": "~8.0", @@ -1636,8 +1762,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0-alpha1", - "datestamp": "1523980980", + "version": "8.x-1.0-alpha2", + "datestamp": "1542000180", "security-coverage": { "status": "not-covered", "message": "Alpha releases are not covered by Drupal security advisories." @@ -1730,22 +1856,21 @@ "homepage": "https://www.drupal.org/project/better_formats", "support": { "source": "http://cgit.drupalcode.org/better_formats" - }, - "time": "2018-04-24T13:52:26+00:00" + } }, { "name": "drupal/blazy", - "version": "1.0.0-rc2", + "version": "1.0.0-rc3", "source": { "type": "git", "url": "https://git.drupal.org/project/blazy", - "reference": "8.x-1.0-rc2" + "reference": "8.x-1.0-rc3" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/blazy-8.x-1.0-rc2.zip", - "reference": "8.x-1.0-rc2", - "shasum": "242f3022b039c6fd3b98f6ce1955d295119b4b5a" + "url": "https://ftp.drupal.org/files/projects/blazy-8.x-1.0-rc3.zip", + "reference": "8.x-1.0-rc3", + "shasum": "a92409f7d19ed984362b213c6245f8eda3bef03a" }, "require": { "drupal/core": "*" @@ -1756,8 +1881,12 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0-rc2", - "datestamp": "1495745283" + "version": "8.x-1.0-rc3", + "datestamp": "1538045580", + "security-coverage": { + "status": "not-covered", + "message": "Project has not opted into security advisory coverage!" + } } }, "notification-url": "https://packages.drupal.org/8/downloads", @@ -1774,6 +1903,10 @@ "name": "Contributors", "homepage": "https://www.drupal.org/node/2663268/committers", "role": "Contributor" + }, + { + "name": "sun", + "homepage": "https://www.drupal.org/user/54136" } ], "description": "Provides basic bLazy integration for lazy loading and multi-serving images.", @@ -1790,17 +1923,17 @@ }, { "name": "drupal/block_class", - "version": "1.0.0-alpha1", + "version": "1.0.0", "source": { "type": "git", "url": "https://git.drupal.org/project/block_class", - "reference": "8.x-1.0-alpha1" + "reference": "8.x-1.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/block_class-8.x-1.0-alpha1.zip", - "reference": "8.x-1.0-alpha1", - "shasum": "194cd52210ea2e0e5dbbd34fad4528d500424430" + "url": "https://ftp.drupal.org/files/projects/block_class-8.x-1.0.zip", + "reference": "8.x-1.0", + "shasum": "1a691999d051a3c010c2b6f2df73954ee02f424d" }, "require": { "drupal/core": "*" @@ -1811,8 +1944,12 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0-alpha1", - "datestamp": "1475623439" + "version": "8.x-1.0", + "datestamp": "1531440821", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } } }, "notification-url": "https://packages.drupal.org/8/downloads", @@ -1821,65 +1958,82 @@ ], "authors": [ { - "name": "Aaron Stanush", - "homepage": "https://www.drupal.org/user/89718" + "name": "Todd Nienkerk", + "homepage": "https://www.drupal.org/u/todd-nienkerk", + "role": "Maintainer" }, { - "name": "DYdave", - "homepage": "https://www.drupal.org/user/467284" + "name": "Renato Gonçalves (RenatoG)", + "homepage": "https://www.drupal.org/u/RenatoG", + "email": "renatog@ciandt.com", + "role": "Maintainer" }, { - "name": "Four Kitchens", - "homepage": "https://www.drupal.org/user/358502" + "name": "Aaron Stanush", + "homepage": "https://www.drupal.org/u/aaron-stanush", + "role": "Maintainer" }, { - "name": "Todd Nienkerk", - "homepage": "https://www.drupal.org/user/92096" + "name": "David Suissa (DYdave)", + "homepage": "https://www.drupal.org/u/DYdave", + "role": "Maintainer" + }, + { + "name": "Four Kitchens", + "homepage": "https://www.drupal.org/user/358502", + "role": "Maintainer" }, { "name": "berenddeboer", - "homepage": "https://www.drupal.org/user/143552" + "homepage": "https://www.drupal.org/u/berenddeboer", + "role": "Maintainer" }, { "name": "elliotttf", - "homepage": "https://www.drupal.org/user/61601" + "homepage": "https://www.drupal.org/u/elliotttf", + "role": "Maintainer" }, { - "name": "mirzu", - "homepage": "https://www.drupal.org/user/7710" + "name": "Michal Minecki (mirzu)", + "homepage": "https://www.drupal.org/u/mirzu", + "role": "Maintainer" }, { - "name": "patrickcoffeyo", - "homepage": "https://www.drupal.org/user/2837945" + "name": "Patrick Coffey (pcoffey)", + "homepage": "https://www.drupal.org/u/pcoffey", + "role": "Maintainer" }, { - "name": "pcoffey", - "homepage": "https://www.drupal.org/user/1595818" + "name": "Taylor Smith (tsmith512)", + "homepage": "https://www.drupal.org/u/tsmith512", + "role": "Maintainer" }, { "name": "tsmith512", "homepage": "https://www.drupal.org/user/2031446" } ], - "description": "Allows assigning classes to Blocks", + "description": "Allows assigning classes to Blocks.", "homepage": "https://www.drupal.org/project/block_class", "support": { - "source": "http://cgit.drupalcode.org/block_class" + "source": "https://cgit.drupalcode.org/block_class", + "issues": "https://www.drupal.org/project/issues/block_class", + "irc": "irc://irc.freenode.org/drupal-contribute" } }, { "name": "drupal/bootstrap", - "version": "3.11.0", + "version": "3.14.0", "source": { "type": "git", "url": "https://git.drupal.org/project/bootstrap", - "reference": "8.x-3.11" + "reference": "8.x-3.14" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/bootstrap-8.x-3.11.zip", - "reference": "8.x-3.11", - "shasum": "6ff80e37e2e1f916646300889f3392764c9cdb32" + "url": "https://ftp.drupal.org/files/projects/bootstrap-8.x-3.14.zip", + "reference": "8.x-3.14", + "shasum": "bd32ae78846d7f535e5a71f631377dba93ee8bd2" }, "require": { "drupal/core": "~8.0" @@ -1890,8 +2044,8 @@ "dev-3.x": "3.x-dev" }, "drupal": { - "version": "8.x-3.11", - "datestamp": "1520793438", + "version": "8.x-3.14", + "datestamp": "1542006180", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -2326,16 +2480,16 @@ }, { "name": "drupal/core", - "version": "8.5.3", + "version": "8.6.3", "source": { "type": "git", "url": "https://github.com/drupal/core.git", - "reference": "b012f0ae51504880e920f2c6efdbdf03b6fe2129" + "reference": "9e9a1dd9e280ebaf10622217e54448b529167965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core/zipball/b012f0ae51504880e920f2c6efdbdf03b6fe2129", - "reference": "b012f0ae51504880e920f2c6efdbdf03b6fe2129", + "url": "https://api.github.com/repos/drupal/core/zipball/9e9a1dd9e280ebaf10622217e54448b529167965", + "reference": "9e9a1dd9e280ebaf10622217e54448b529167965", "shasum": "" }, "require": { @@ -2368,8 +2522,8 @@ "symfony/console": "~3.4.0", "symfony/dependency-injection": "~3.4.0", "symfony/event-dispatcher": "~3.4.0", - "symfony/http-foundation": "~3.4.0", - "symfony/http-kernel": "~3.4.0", + "symfony/http-foundation": "~3.4.14", + "symfony/http-kernel": "~3.4.14", "symfony/polyfill-iconv": "^1.0", "symfony/process": "~3.4.0", "symfony/psr-http-message-bridge": "^1.0", @@ -2453,10 +2607,12 @@ "drupal/link": "self.version", "drupal/locale": "self.version", "drupal/media": "self.version", + "drupal/media_library": "self.version", "drupal/menu_link_content": "self.version", "drupal/menu_ui": "self.version", "drupal/migrate": "self.version", "drupal/migrate_drupal": "self.version", + "drupal/migrate_drupal_multilingual": "self.version", "drupal/migrate_drupal_ui": "self.version", "drupal/minimal": "self.version", "drupal/node": "self.version", @@ -2488,7 +2644,8 @@ "drupal/user": "self.version", "drupal/views": "self.version", "drupal/views_ui": "self.version", - "drupal/workflows": "self.version" + "drupal/workflows": "self.version", + "drupal/workspaces": "self.version" }, "require-dev": { "behat/mink": "1.7.x-dev", @@ -2498,8 +2655,8 @@ "jcalderonzumba/gastonjs": "^1.0.2", "jcalderonzumba/mink-phantomjs-driver": "^0.3.1", "mikey179/vfsstream": "^1.2", - "phpspec/prophecy": "^1.4", - "phpunit/phpunit": "^4.8.35 || ^6.1", + "phpspec/prophecy": "^1.7", + "phpunit/phpunit": "^4.8.35 || ^6.5", "symfony/css-selector": "^3.4.0", "symfony/debug": "^3.4.0", "symfony/phpunit-bridge": "^3.4.3" @@ -2557,7 +2714,7 @@ "GPL-2.0-or-later" ], "description": "Drupal is an open source content management platform powering millions of websites and applications.", - "time": "2018-04-25T15:39:01+00:00" + "time": "2018-11-07T14:45:40+00:00" }, { "name": "drupal/crop", @@ -2784,17 +2941,17 @@ }, { "name": "drupal/diff", - "version": "1.0.0-rc1", + "version": "1.0.0-rc2", "source": { "type": "git", "url": "https://git.drupal.org/project/diff", - "reference": "8.x-1.0-rc1" + "reference": "8.x-1.0-rc2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/diff-8.x-1.0-rc1.zip", - "reference": "8.x-1.0-rc1", - "shasum": "4945154c2c07444195a7ae07011a3b3db941c72a" + "url": "https://ftp.drupal.org/files/projects/diff-8.x-1.0-rc2.zip", + "reference": "8.x-1.0-rc2", + "shasum": "2e8f4efe6e7d355a3d07d33531f077348b13f68d" }, "require": { "drupal/core": "~8.0", @@ -2806,8 +2963,12 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0-rc1", - "datestamp": "1484566683" + "version": "8.x-1.0-rc2", + "datestamp": "1530178424", + "security-coverage": { + "status": "not-covered", + "message": "RC releases are not covered by Drupal security advisories." + } } }, "notification-url": "https://packages.drupal.org/8/downloads", @@ -2866,55 +3027,66 @@ }, { "name": "drupal/draggableviews", - "version": "1.0.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://git.drupal.org/project/draggableviews", - "reference": "8.x-1.0" + "reference": "8.x-1.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/draggableviews-8.x-1.0.zip", - "reference": "8.x-1.0", - "shasum": "007082c2621b45bd8cf73fe5fdd4d292ee88a458" + "url": "https://ftp.drupal.org/files/projects/draggableviews-8.x-1.2.zip", + "reference": "8.x-1.2", + "shasum": "0f5e9195ceec209552aa50f8ce3c230692c284db" }, "require": { "drupal/core": "*" }, + "require-dev": { + "drupal/draggableviews_demo": "*" + }, "type": "drupal-module", "extra": { "branch-alias": { "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0", - "datestamp": "1477076039", + "version": "8.x-1.2", + "datestamp": "1541518680", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" } }, - "patches_applied": { - "Sort order not saved when using bootstrap theme": "https://www.drupal.org/files/issues/classes-defined-in-array-2729935-2.patch", - "DraggableViews displays multiple node instances when used in multiple views": "https://www.drupal.org/files/issues/2867159_fixed_problems_with_duplicates_0.patch" - } + "patches_applied": [] }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0-or-later" + "GPL-2.0+" ], "authors": [ { - "name": "dixon_", - "homepage": "https://www.drupal.org/user/239911" + "name": "Tyler Struyk (iStryker)", + "homepage": "https://www.drupal.org/u/istryker", + "email": "tyler.struyk@gmail.com", + "role": "Maintainer" + }, + { + "name": "Andrii Podanenko (podarok)", + "homepage": "https://www.drupal.org/u/podarok", + "email": "podarokua@gmail.com", + "role": "Drupal 7 to 8 Porter" }, { - "name": "ginc", - "homepage": "https://www.drupal.org/user/332249" + "name": "Yuriy Gerasimov (ygerasimov)", + "homepage": "https://www.drupal.org/u/ygerasimov", + "email": "yuriy.gerasimov@gmail.com", + "role": "Ex Maintainer (D7)" }, { - "name": "iStryker", - "homepage": "https://www.drupal.org/user/303676" + "name": "Severin Unger (sevi)", + "homepage": "https://www.drupal.org/u/sevi", + "role": "Ex Maintainer (D6)" }, { "name": "podarok", @@ -2929,10 +3101,11 @@ "homepage": "https://www.drupal.org/user/257311" } ], - "description": "Complete rewrite of D7 draggableviews", + "description": "DraggableViews module makes views draggable.", "homepage": "https://www.drupal.org/project/draggableviews", "support": { - "source": "http://cgit.drupalcode.org/draggableviews" + "source": "https://cgit.drupalcode.org/draggableviews", + "issues": "https://www.drupal.org/project/issues/draggableviews" } }, { @@ -3024,11 +3197,17 @@ }, { "name": "drupal/drupalmoduleupgrader", - "version": "dev-1.x", + "version": "1.3.0", "source": { "type": "git", "url": "https://git.drupal.org/project/drupalmoduleupgrader", - "reference": "726c7a7b9e3cda8c51f6dfbc003550bdc821433b" + "reference": "8.x-1.3" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/drupalmoduleupgrader-8.x-1.3.zip", + "reference": "8.x-1.3", + "shasum": "5aa969b218278c390ca5554a3c8a520e321f081d" }, "require": { "cebe/markdown": "1.0.*@dev", @@ -3047,11 +3226,11 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.2+10-dev", - "datestamp": "1490462883", + "version": "8.x-1.3", + "datestamp": "1528129380", "security-coverage": { - "status": "not-covered", - "message": "Dev releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -3093,10 +3272,6 @@ "name": "jcnventura", "homepage": "https://www.drupal.org/user/122464" }, - { - "name": "pfrenssen", - "homepage": "https://www.drupal.org/user/382067" - }, { "name": "phenaproxima", "homepage": "https://www.drupal.org/user/205645" @@ -3127,8 +3302,7 @@ "support": { "source": "https://drupal.org/project/drupalmoduleupgrader", "issues": "https://drupal.org/project/issues/drupalmoduleupgrader" - }, - "time": "2018-05-15T15:59:31+00:00" + } }, { "name": "drupal/embed", @@ -3192,33 +3366,41 @@ } }, { - "name": "drupal/entity", - "version": "1.0.0-beta3", + "name": "drupal/entity_browser", + "version": "1.6.0", "source": { "type": "git", - "url": "https://git.drupal.org/project/entity", - "reference": "8.x-1.0-beta3" + "url": "https://git.drupal.org/project/entity_browser", + "reference": "8.x-1.6" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity-8.x-1.0-beta3.zip", - "reference": "8.x-1.0-beta3", - "shasum": "5d74dbef7bff0db099517b2c7aad14312eb02afa" + "url": "https://ftp.drupal.org/files/projects/entity_browser-8.x-1.6.zip", + "reference": "8.x-1.6", + "shasum": "ed80f338cc002e188df21967cf2222a01a88f90c" }, "require": { - "drupal/core": "~8.5" + "drupal/core": "~8.0" + }, + "require-dev": { + "drupal/ctools": "*", + "drupal/inline_entity_form": "*", + "drupal/media_entity": "*", + "drupal/paragraphs": "*", + "drupal/token": "*" }, "type": "drupal-module", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-1.x": "1.x-dev", + "dev-8.x-1.x": "8.1.x-dev" }, "drupal": { - "version": "8.x-1.0-beta3", - "datestamp": "1520958485", + "version": "8.x-1.6", + "datestamp": "1536328684", "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -3228,94 +3410,23 @@ ], "authors": [ { - "name": "Berdir", - "homepage": "https://www.drupal.org/user/214652" + "name": "Janez Urevc", + "homepage": "https://github.com/slashrsm", + "role": "Maintainer" }, { - "name": "bojanz", - "homepage": "https://www.drupal.org/user/86106" + "name": "Primoz Hmeljak", + "homepage": "https://github.com/primsi", + "role": "Maintainer" }, { - "name": "dawehner", - "homepage": "https://www.drupal.org/user/99340" + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/1943336/committers", + "role": "contributor" }, { - "name": "dixon_", - "homepage": "https://www.drupal.org/user/239911" - }, - { - "name": "fago", - "homepage": "https://www.drupal.org/user/16747" - } - ], - "description": "Provides expanded entity APIs, which will be moved to Drupal core one day.", - "homepage": "http://drupal.org/project/entity", - "support": { - "source": "http://cgit.drupalcode.org/entity" - } - }, - { - "name": "drupal/entity_browser", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://git.drupal.org/project/entity_browser", - "reference": "8.x-1.4" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_browser-8.x-1.4.zip", - "reference": "8.x-1.4", - "shasum": "4375e996b8d1e103ca5daf9ce352e2af9cab568f" - }, - "require": { - "drupal/core": "~8.0" - }, - "require-dev": { - "drupal/ctools": "*", - "drupal/inline_entity_form": "*", - "drupal/media_entity": "*", - "drupal/paragraphs": "*", - "drupal/token": "*" - }, - "type": "drupal-module", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev", - "dev-8.x-1.x": "8.1.x-dev" - }, - "drupal": { - "version": "8.x-1.4", - "datestamp": "1512033785", - "security-coverage": { - "status": "covered", - "message": "Covered by Drupal's security advisory policy" - } - } - }, - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0+" - ], - "authors": [ - { - "name": "Janez Urevc", - "homepage": "https://github.com/slashrsm", - "role": "Maintainer" - }, - { - "name": "Primoz Hmeljak", - "homepage": "https://github.com/primsi", - "role": "Maintainer" - }, - { - "name": "See other contributors", - "homepage": "https://www.drupal.org/node/1943336/committers", - "role": "contributor" - }, - { - "name": "Primsi", - "homepage": "https://www.drupal.org/user/282629" + "name": "Primsi", + "homepage": "https://www.drupal.org/user/282629" }, { "name": "marcingy", @@ -3403,77 +3514,19 @@ "irc": "irc://irc.freenode.org/drupal-media" } }, - { - "name": "drupal/entity_reference_revisions", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://git.drupal.org/project/entity_reference_revisions", - "reference": "8.x-1.4" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.4.zip", - "reference": "8.x-1.4", - "shasum": "0d5e159ab52fe8e5aa7c27e7ccfc0299e1af4d72" - }, - "require": { - "drupal/core": "~8.0" - }, - "require-dev": { - "drupal/diff": "*" - }, - "type": "drupal-module", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, - "drupal": { - "version": "8.x-1.4", - "datestamp": "1515143885", - "security-coverage": { - "status": "covered", - "message": "Covered by Drupal's security advisory policy" - } - } - }, - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "Frans", - "homepage": "https://www.drupal.org/user/514222" - }, - { - "name": "jeroen.b", - "homepage": "https://www.drupal.org/user/1853532" - }, - { - "name": "miro_dietiker", - "homepage": "https://www.drupal.org/user/227761" - } - ], - "description": "Adds a Entity Reference field type with revision support.", - "homepage": "https://www.drupal.org/project/entity_reference_revisions", - "support": { - "source": "http://cgit.drupalcode.org/entity_reference_revisions" - } - }, { "name": "drupal/entityqueue", - "version": "1.0.0-alpha7", + "version": "1.0.0-alpha8", "source": { "type": "git", "url": "https://git.drupal.org/project/entityqueue", - "reference": "8.x-1.0-alpha7" + "reference": "8.x-1.0-alpha8" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entityqueue-8.x-1.0-alpha7.zip", - "reference": "8.x-1.0-alpha7", - "shasum": "d3cf4c655c2af08b2018af8675db54319fce29d8" + "url": "https://ftp.drupal.org/files/projects/entityqueue-8.x-1.0-alpha8.zip", + "reference": "8.x-1.0-alpha8", + "shasum": "edb66ae3767b09f1723e3b775dec591641dd67f8" }, "require": { "drupal/core": "~8.0" @@ -3484,8 +3537,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0-alpha7", - "datestamp": "1506839344", + "version": "8.x-1.0-alpha8", + "datestamp": "1536140284", "security-coverage": { "status": "not-covered", "message": "Alpha releases are not covered by Drupal security advisories." @@ -3494,16 +3547,18 @@ }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0-or-later" + "GPL-2.0+" ], "authors": [ { - "name": "amateescu", - "homepage": "https://www.drupal.org/user/729614" + "name": "Andrei Mateescu", + "homepage": "https://www.drupal.org/u/amateescu", + "role": "Maintainer" }, { - "name": "jojonaloha", - "homepage": "https://www.drupal.org/user/1579186" + "name": "Jonathan Jordan", + "homepage": "https://www.drupal.org/u/jojonaloha", + "role": "Maintainer" }, { "name": "quicksketch", @@ -3514,10 +3569,12 @@ "homepage": "https://www.drupal.org/user/241634" } ], - "description": "Allows users to collect entities in arbitrarily ordered lists.", + "description": "The Entityqueue module allows users to create queues of any entity type.", "homepage": "https://www.drupal.org/project/entityqueue", "support": { - "source": "http://cgit.drupalcode.org/entityqueue" + "source": "http://cgit.drupalcode.org/entityqueue", + "issues": "https://www.drupal.org/project/issues/entityqueue", + "irc": "irc://irc.freenode.org/drupal-contribute" } }, { @@ -3573,17 +3630,17 @@ }, { "name": "drupal/eu_cookie_compliance", - "version": "1.0.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://git.drupal.org/project/eu-cookie-compliance", - "reference": "8.x-1.0" + "reference": "8.x-1.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/eu_cookie_compliance-8.x-1.0.zip", - "reference": "8.x-1.0", - "shasum": "925f5267fce132e54cfdff58e560cb5fc1656634" + "url": "https://ftp.drupal.org/files/projects/eu_cookie_compliance-8.x-1.2.zip", + "reference": "8.x-1.2", + "shasum": "8e9f2a6c2d5abaa02b58b4b48e0626ed7c3518ad" }, "require": { "drupal/core": "*" @@ -3594,8 +3651,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0", - "datestamp": "1522572784", + "version": "8.x-1.2", + "datestamp": "1531429121", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -3604,20 +3661,22 @@ }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0-or-later" + "GPL-2.0+" ], "authors": [ { "name": "Marcin Pajdzik", - "homepage": "https://www.drupal.org/user/160555" + "homepage": "https://www.drupal.org/u/marcin-pajdzik", + "role": "Maintainer" }, { - "name": "achton", - "homepage": "https://www.drupal.org/user/712454" + "name": "Sven Berg Ryen", + "homepage": "https://www.drupal.org/u/svenryen", + "role": "Maintainer" }, { - "name": "blairski", - "homepage": "https://www.drupal.org/user/120835" + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/1538032/committers" }, { "name": "dakku", @@ -3648,10 +3707,113 @@ "homepage": "https://www.drupal.org/user/667244" } ], - "description": "This module aims at making the website compliant with the new EU cookie regulation", - "homepage": "https://www.drupal.org/project/eu_cookie_compliance", + "description": "This module aims at making the website compliant with the new EU cookie regulation.", + "homepage": "https://drupal.org/project/eu_cookie_compliance", + "keywords": [ + "Cookie", + "CookieCompliance", + "Drupal", + "GDPR" + ], "support": { - "source": "http://cgit.drupalcode.org/eu_cookie_compliance" + "source": "https://cgit.drupalcode.org/eu-cookie-compliance", + "docs": "https://www.drupal.org/project/eu_cookie_compliance", + "forum": "https://drupal.stackexchange.com/search?q=eu+cookie+compliance", + "issues": "https://www.drupal.org/project/issues/eu_cookie_compliance?version=8.x", + "irc": "irc://irc.freenode.org/drupal-contribute" + } + }, + { + "name": "drupal/file_mdm", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/file_mdm", + "reference": "8.x-1.1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/file_mdm-8.x-1.1.zip", + "reference": "8.x-1.1", + "shasum": "7f354aec6f89e3141c2aa1fb3747ad5d4578c13f" + }, + "require": { + "drupal/core": "~8.0", + "lsolesen/pel": "0.9.6", + "phenx/php-font-lib": "0.5", + "php": ">=5.6" + }, + "require-dev": { + "drupal/image_effects": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.1", + "datestamp": "1488273785", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "mondrake", + "homepage": "https://www.drupal.org/user/1307444" + } + ], + "description": "Provides a service to manage file metadata.", + "homepage": "https://www.drupal.org/project/file_mdm", + "support": { + "source": "http://cgit.drupalcode.org/file_mdm" + } + }, + { + "name": "drupal/file_mdm_exif", + "version": "1.1.0", + "require": { + "drupal/core": "~8.0", + "drupal/file_mdm": "self.version" + }, + "require-dev": { + "drupal/image_effects": "*" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.1", + "datestamp": "1488273785", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "mondrake", + "homepage": "https://www.drupal.org/user/1307444" + } + ], + "description": "Provides a file metadata plugin for EXIF image information.", + "homepage": "https://www.drupal.org/project/file_mdm", + "support": { + "source": "http://cgit.drupalcode.org/file_mdm" } }, { @@ -3874,17 +4036,17 @@ }, { "name": "drupal/htmlawed", - "version": "3.3.0", + "version": "3.5.0", "source": { "type": "git", "url": "https://git.drupal.org/project/htmLawed", - "reference": "8.x-3.3" + "reference": "8.x-3.5" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/htmlawed-8.x-3.3.zip", - "reference": "8.x-3.3", - "shasum": "5728561a9d8b5ad9bb56c9270742469ed28cc65d" + "url": "https://ftp.drupal.org/files/projects/htmlawed-8.x-3.5.zip", + "reference": "8.x-3.5", + "shasum": "48cf7dda2d327fcc2024273a5b37861872e0f53f" }, "require": { "drupal/core": "~8.0" @@ -3895,8 +4057,8 @@ "dev-3.x": "3.x-dev" }, "drupal": { - "version": "8.x-3.3", - "datestamp": "1512623885", + "version": "8.x-3.5", + "datestamp": "1530751724", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -3964,21 +4126,21 @@ }, { "name": "drupal/image_widget_crop", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://git.drupal.org/project/image_widget_crop", - "reference": "8.x-2.1" + "reference": "8.x-2.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/image_widget_crop-8.x-2.1.zip", - "reference": "8.x-2.1", - "shasum": "76bda9a8c8d3149149bb7812e48e9f14e9ce1ffb" + "url": "https://ftp.drupal.org/files/projects/image_widget_crop-8.x-2.2.zip", + "reference": "8.x-2.2", + "shasum": "0a61cfedc409c5ef448174f26060cd6c291d6a11" }, "require": { "drupal/core": "*", - "drupal/crop": "1.0 - 2.0" + "drupal/crop": "^1.0 || ^2.0" }, "require-dev": { "drupal/crop": "*", @@ -3994,8 +4156,8 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.1", - "datestamp": "1510136887", + "version": "8.x-2.2", + "datestamp": "1530698921", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4040,36 +4202,46 @@ }, { "name": "drupal/imagemagick", - "version": "1.0.0-alpha6", + "version": "2.3.0", "source": { "type": "git", "url": "https://git.drupal.org/project/imagemagick", - "reference": "8.x-1.0-alpha6" + "reference": "8.x-2.3" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/imagemagick-8.x-1.0-alpha6.zip", - "reference": "8.x-1.0-alpha6", - "shasum": "9631b407762076b2f5d9bf3b50b40c28976ddbb7" + "url": "https://ftp.drupal.org/files/projects/imagemagick-8.x-2.3.zip", + "reference": "8.x-2.3", + "shasum": "8359921d4700e954364c2633332c20579bb75a34" }, "require": { - "drupal/core": "^8.1.0" + "drupal/core": "^8.3", + "drupal/file_mdm": "^1.1", + "drupal/file_mdm_exif": "^1.1" }, "type": "drupal-module", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-1.0-alpha6", - "datestamp": "1488787683" + "version": "8.x-2.3", + "datestamp": "1520958305", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } } }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ + { + "name": "Chris Charlton", + "homepage": "https://www.drupal.org/user/17089" + }, { "name": "chx", "homepage": "https://www.drupal.org/user/9446" @@ -4111,7 +4283,7 @@ "homepage": "https://www.drupal.org/user/1531" } ], - "description": "Provides ImageMagick integration.", + "description": "Provides an image toolkit to integrate ImageMagick with the Image API.", "homepage": "https://www.drupal.org/project/imagemagick", "support": { "source": "http://cgit.drupalcode.org/imagemagick" @@ -4119,17 +4291,17 @@ }, { "name": "drupal/inline_entity_form", - "version": "1.0.0-beta1", + "version": "1.0.0-rc1", "source": { "type": "git", "url": "https://git.drupal.org/project/inline_entity_form", - "reference": "8.x-1.0-beta1" + "reference": "8.x-1.0-rc1" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/inline_entity_form-8.x-1.0-beta1.zip", - "reference": "8.x-1.0-beta1", - "shasum": "185ffc28a7b68d19cce057855d1c111f1741a3ea" + "url": "https://ftp.drupal.org/files/projects/inline_entity_form-8.x-1.0-rc1.zip", + "reference": "8.x-1.0-rc1", + "shasum": "898789fb6a0662fc2572b87f8d0654a0241473f9" }, "require": { "drupal/core": "~8.0" @@ -4143,8 +4315,12 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0-beta1", - "datestamp": "1477868343" + "version": "8.x-1.0-rc1", + "datestamp": "1527030784", + "security-coverage": { + "status": "not-covered", + "message": "RC releases are not covered by Drupal security advisories." + } } }, "notification-url": "https://packages.drupal.org/8/downloads", @@ -4360,7 +4536,7 @@ "source": { "type": "git", "url": "https://git.drupal.org/project/linkchecker", - "reference": "780fc89bd85fb1fad955d7993a6a4e03ef9a7882" + "reference": "f59edc88741c60991526dc54adc89554c0ca571b" }, "require": { "drupal/core": "~8.0" @@ -4372,7 +4548,7 @@ }, "drupal": { "version": "8.x-1.x-dev", - "datestamp": "1520629084", + "datestamp": "1534714080", "security-coverage": { "status": "not-covered", "message": "Dev releases are not covered by Drupal security advisories." @@ -4404,7 +4580,7 @@ "source": "http://git.drupal.org/project/linkchecker.git", "issues": "https://www.drupal.org/project/issues/linkchecker" }, - "time": "2018-05-09T22:30:43+00:00" + "time": "2018-10-31T11:26:37+00:00" }, { "name": "drupal/linkit", @@ -4452,102 +4628,6 @@ "issues": "http://drupal.org/project/linkit" } }, - { - "name": "drupal/media_entity", - "version": "2.0.0-beta2", - "source": { - "type": "git", - "url": "https://git.drupal.org/project/media_entity", - "reference": "8.x-2.0-beta2" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/media_entity-8.x-2.0-beta2.zip", - "reference": "8.x-2.0-beta2", - "shasum": "2223e342634551041ef7fcb80c2860e5b62aed4e" - }, - "require": { - "drupal/core": "*", - "drupal/entity": "*" - }, - "type": "drupal-module", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - }, - "drupal": { - "version": "8.x-2.0-beta2", - "datestamp": "1520603584", - "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." - } - } - }, - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "Berdir", - "homepage": "https://www.drupal.org/user/214652" - }, - { - "name": "Dave Reid", - "homepage": "https://www.drupal.org/user/53892" - }, - { - "name": "Drupal Media Team", - "homepage": "https://www.drupal.org/user/3260690" - }, - { - "name": "Drupal media CI", - "homepage": "https://www.drupal.org/user/3057985" - }, - { - "name": "Primsi", - "homepage": "https://www.drupal.org/user/282629" - }, - { - "name": "boztek", - "homepage": "https://www.drupal.org/user/134410" - }, - { - "name": "chr.fritsch", - "homepage": "https://www.drupal.org/user/2103716" - }, - { - "name": "jcisio", - "homepage": "https://www.drupal.org/user/210762" - }, - { - "name": "katzilla", - "homepage": "https://www.drupal.org/user/260398" - }, - { - "name": "marcoscano", - "homepage": "https://www.drupal.org/user/1288796" - }, - { - "name": "phenaproxima", - "homepage": "https://www.drupal.org/user/205645" - }, - { - "name": "seanB", - "homepage": "https://www.drupal.org/user/545912" - }, - { - "name": "slashrsm", - "homepage": "https://www.drupal.org/user/744628" - } - ], - "description": "Media entity API.", - "homepage": "https://www.drupal.org/project/media_entity", - "support": { - "source": "http://cgit.drupalcode.org/media_entity" - } - }, { "name": "drupal/media_entity_actions", "version": "1.0.0-alpha2", @@ -4613,17 +4693,17 @@ }, { "name": "drupal/media_entity_instagram", - "version": "2.0.0-alpha1", + "version": "2.0.0-alpha2", "source": { "type": "git", "url": "https://git.drupal.org/project/media_entity_instagram", - "reference": "8.x-2.0-alpha1" + "reference": "8.x-2.0-alpha2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/media_entity_instagram-8.x-2.0-alpha1.zip", - "reference": "8.x-2.0-alpha1", - "shasum": "602d84d2e385ce2f3b0562f7cf50d1e084f92658" + "url": "https://ftp.drupal.org/files/projects/media_entity_instagram-8.x-2.0-alpha2.zip", + "reference": "8.x-2.0-alpha2", + "shasum": "3a61feafacf7003bf27ec8d4271a07896788b5e3" }, "require": { "drupal/core": "^8.4.0" @@ -4634,8 +4714,8 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.0-alpha1", - "datestamp": "1506671645", + "version": "8.x-2.0-alpha2", + "datestamp": "1537264380", "security-coverage": { "status": "not-covered", "message": "Alpha releases are not covered by Drupal security advisories." @@ -4787,17 +4867,17 @@ }, { "name": "drupal/memcache", - "version": "2.0.0-alpha5", + "version": "2.0.0", "source": { "type": "git", "url": "https://git.drupal.org/project/memcache", - "reference": "8.x-2.0-alpha5" + "reference": "8.x-2.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/memcache-8.x-2.0-alpha5.zip", - "reference": "8.x-2.0-alpha5", - "shasum": "4210217dd4040a5687831da82884f7bcad155c66" + "url": "https://ftp.drupal.org/files/projects/memcache-8.x-2.0.zip", + "reference": "8.x-2.0", + "shasum": "78c09097c42781343f4069b4823dce7acd98a8b1" }, "require": { "drupal/core": "~8.0" @@ -4808,11 +4888,11 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.0-alpha5", - "datestamp": "1508351413", + "version": "8.x-2.0", + "datestamp": "1540546681", "security-coverage": { - "status": "not-covered", - "message": "Alpha releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -4821,10 +4901,18 @@ "GPL-2.0+" ], "authors": [ + { + "name": "Fabianx", + "homepage": "https://www.drupal.org/user/693738" + }, { "name": "Jeremy", "homepage": "https://www.drupal.org/user/409" }, + { + "name": "bdragon", + "homepage": "https://www.drupal.org/user/53081" + }, { "name": "catch", "homepage": "https://www.drupal.org/user/35733" @@ -4851,17 +4939,17 @@ }, { "name": "drupal/metatag", - "version": "1.5.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://git.drupal.org/project/metatag", - "reference": "8.x-1.5" + "reference": "8.x-1.7" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/metatag-8.x-1.5.zip", - "reference": "8.x-1.5", - "shasum": "48f1c2a4e93ef1af1eb4b5b6cc6321e951fd1002" + "url": "https://ftp.drupal.org/files/projects/metatag-8.x-1.7.zip", + "reference": "8.x-1.7", + "shasum": "93decaefd053c524918ceae5b5ef05dd77de0857" }, "require": { "drupal/core": "*", @@ -4871,8 +4959,11 @@ "drupal/devel": "^1.0", "drupal/metatag_dc": "*", "drupal/metatag_open_graph": "*", + "drupal/page_manager": "^4.0", "drupal/redirect": "^1.0", - "drupal/restui": "^1.0" + "drupal/restui": "^1.0", + "drupal/schema_metatag": "^1.0", + "drupal/schema_web_page": "*" }, "type": "drupal-module", "extra": { @@ -4880,8 +4971,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.5", - "datestamp": "1522344484", + "version": "8.x-1.7", + "datestamp": "1535726393", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4916,17 +5007,17 @@ }, { "name": "drupal/migrate_plus", - "version": "4.0.0-beta3", + "version": "4.0.0", "source": { "type": "git", "url": "https://git.drupal.org/project/migrate_plus", - "reference": "8.x-4.0-beta3" + "reference": "8.x-4.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-4.0-beta3.zip", - "reference": "8.x-4.0-beta3", - "shasum": "a0da6dc169a0315bf29a8dd0ee2146d18215a56b" + "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-4.0.zip", + "reference": "8.x-4.0", + "shasum": "63dad289defe8298aa5ca5e30062fe9761d19eca" }, "require": { "drupal/core": "^8.3" @@ -4945,11 +5036,11 @@ "dev-4.x": "4.x-dev" }, "drupal": { - "version": "8.x-4.0-beta3", - "datestamp": "1519400592", + "version": "8.x-4.0", + "datestamp": "1536264180", "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -4978,30 +5069,43 @@ }, { "name": "drupal/migrate_tools", - "version": "3.0.0-beta1", + "version": "4.0.0", "source": { "type": "git", "url": "https://git.drupal.org/project/migrate_tools", - "reference": "8.x-3.0-beta1" + "reference": "8.x-4.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_tools-8.x-3.0-beta1.zip", - "reference": "8.x-3.0-beta1", - "shasum": "1511d1da1444743fa2defa54a17e62264e8ab836" + "url": "https://ftp.drupal.org/files/projects/migrate_tools-8.x-4.0.zip", + "reference": "8.x-4.0", + "shasum": "016dfb010df76723c5a6a447921fdccd3c885237" }, "require": { - "drupal/core": "^8.2", - "drupal/migrate_plus": "*" + "drupal/core": "^8.3", + "drupal/migrate_plus": "^4" + }, + "require-dev": { + "drupal/coder": "^8", + "drupal/migrate_source_csv": "^2.2" }, "type": "drupal-module", "extra": { "branch-alias": { - "dev-3.x": "3.x-dev" + "dev-4.x": "4.x-dev" }, "drupal": { - "version": "8.x-3.0-beta1", - "datestamp": "1476313439" + "version": "8.x-4.0", + "datestamp": "1535380084", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": "^9" + } } }, "notification-url": "https://packages.drupal.org/8/downloads", @@ -5009,30 +5113,40 @@ "GPL-2.0+" ], "authors": [ + { + "name": "heddn", + "homepage": "https://www.drupal.org/user/1463982" + }, { "name": "mikeryan", "homepage": "https://www.drupal.org/user/4420" + }, + { + "name": "moshe weitzman", + "homepage": "https://www.drupal.org/user/23" } ], "description": "Tools to assist in developing and running migrations.", - "homepage": "https://www.drupal.org/project/migrate_tools", + "homepage": "http://drupal.org/project/migrate_tools", "support": { - "source": "http://cgit.drupalcode.org/migrate_tools" + "source": "http://cgit.drupalcode.org/migrate_tools", + "issues": "http://drupal.org/project/migrate_tools", + "irc": "irc://irc.freenode.org/drupal-migrate" } }, { "name": "drupal/migrate_upgrade", - "version": "3.0.0-rc4", + "version": "3.0.0-rc5", "source": { "type": "git", "url": "https://git.drupal.org/project/migrate_upgrade", - "reference": "8.x-3.0-rc4" + "reference": "8.x-3.0-rc5" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_upgrade-8.x-3.0-rc4.zip", - "reference": "8.x-3.0-rc4", - "shasum": "345d56e98c763e19617d317aaece3cded22670ea" + "url": "https://ftp.drupal.org/files/projects/migrate_upgrade-8.x-3.0-rc5.zip", + "reference": "8.x-3.0-rc5", + "shasum": "608375003cfd42664ecd6d5b0b5e2c3b510a2e0a" }, "require": { "drupal/core": "*", @@ -5047,8 +5161,8 @@ "dev-3.x": "3.x-dev" }, "drupal": { - "version": "8.x-3.0-rc4", - "datestamp": "1519400285", + "version": "8.x-3.0-rc5", + "datestamp": "1535381580", "security-coverage": { "status": "not-covered", "message": "RC releases are not covered by Drupal security advisories." @@ -5091,95 +5205,22 @@ "source": "http://cgit.drupalcode.org/migrate_upgrade" } }, - { - "name": "drupal/paragraphs", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://git.drupal.org/project/paragraphs", - "reference": "8.x-1.2" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/paragraphs-8.x-1.2.zip", - "reference": "8.x-1.2", - "shasum": "6efc9d4351d4b21a55e7a6428c78495aaca662ef" - }, - "require": { - "drupal/core": "~8", - "drupal/entity_reference_revisions": "~1.3" - }, - "require-dev": { - "drupal/block_field": "~1.0", - "drupal/diff": "~1.0", - "drupal/field_group": "~1.0", - "drupal/inline_entity_form": "~1.0", - "drupal/replicate": "~1.0", - "drupal/search_api": "*", - "drupal/search_api_db": "*" - }, - "type": "drupal-module", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, - "drupal": { - "version": "8.x-1.2", - "datestamp": "1505802867", - "security-coverage": { - "status": "covered", - "message": "Covered by Drupal's security advisory policy" - } - } - }, - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0" - ], - "authors": [ - { - "name": "Berdir", - "homepage": "https://www.drupal.org/user/214652" - }, - { - "name": "Frans", - "homepage": "https://www.drupal.org/user/514222" - }, - { - "name": "Primsi", - "homepage": "https://www.drupal.org/user/282629" - }, - { - "name": "jeroen.b", - "homepage": "https://www.drupal.org/user/1853532" - }, - { - "name": "miro_dietiker", - "homepage": "https://www.drupal.org/user/227761" - } - ], - "description": "Enables the creation of Paragraphs entities.", - "homepage": "https://www.drupal.org/project/paragraphs", - "support": { - "source": "http://cgit.drupalcode.org/paragraphs" - } - }, { "name": "drupal/pathauto", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://git.drupal.org/project/pathauto", - "reference": "8.x-1.2" + "reference": "8.x-1.3" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/pathauto-8.x-1.2.zip", - "reference": "8.x-1.2", - "shasum": "ba265dbafb27e93d4a61655d783441b653206c73" + "url": "https://ftp.drupal.org/files/projects/pathauto-8.x-1.3.zip", + "reference": "8.x-1.3", + "shasum": "115d5998d7636a03e26c7ce34261b65809d53965" }, "require": { - "drupal/core": "*", + "drupal/core": "^8.5", "drupal/ctools": "*", "drupal/token": "*" }, @@ -5189,8 +5230,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.2", - "datestamp": "1524421084", + "version": "8.x-1.3", + "datestamp": "1536407884", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -5282,17 +5323,17 @@ }, { "name": "drupal/permissions_by_term", - "version": "1.53.0", + "version": "1.61.0", "source": { "type": "git", "url": "https://git.drupal.org/project/permissions_by_term", - "reference": "8.x-1.53" + "reference": "8.x-1.61" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/permissions_by_term-8.x-1.53.zip", - "reference": "8.x-1.53", - "shasum": "a31243be046d11741aaeac5884ae77d7206d8946" + "url": "https://ftp.drupal.org/files/projects/permissions_by_term-8.x-1.61.zip", + "reference": "8.x-1.61", + "shasum": "c984a00f1efaf8856b5804f5837672bf9fb527bb" }, "require": { "drupal/core": "^8.0" @@ -5303,8 +5344,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.53", - "datestamp": "1526672880", + "version": "8.x-1.61", + "datestamp": "1534865884", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -5321,6 +5362,10 @@ "homepage": "https://www.drupal.org/user/786132", "email": "p.majmesku@gmail.com" }, + { + "name": "SaschaHannes", + "homepage": "https://www.drupal.org/user/3536189" + }, { "name": "dakku", "homepage": "https://www.drupal.org/user/97634" @@ -5342,7 +5387,7 @@ "source": { "type": "git", "url": "https://git.drupal.org/project/php", - "reference": "304022be52874ef2d7916ea3acafd312766da9cc" + "reference": "e5c1c4047f5f1522e5d630bca93d50c61ef6a2c0" }, "require": { "drupal/core": "~8.0" @@ -5353,8 +5398,12 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.0-beta2+1-dev", - "datestamp": "1461875939" + "version": "8.x-1.0+0-dev", + "datestamp": "1529156324", + "security-coverage": { + "status": "revoked", + "message": "Project has been unsupported by the Drupal Security Team" + } } }, "notification-url": "https://packages.drupal.org/8/downloads", @@ -5402,23 +5451,24 @@ "description": "Allows embedded PHP code/snippets to be evaluated. Enabling this can cause security and performance issues as it allows users to execute PHP code on your site.", "homepage": "https://www.drupal.org/project/php", "support": { - "source": "http://git.drupal.org/project/php.git", + "source": "https://git.drupal.org/project/php.git", "issues": "https://www.drupal.org/project/issues/php" - } + }, + "time": "2018-08-04T18:55:12+00:00" }, { "name": "drupal/redirect", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://git.drupal.org/project/redirect", - "reference": "8.x-1.2" + "reference": "8.x-1.3" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/redirect-8.x-1.2.zip", - "reference": "8.x-1.2", - "shasum": "ac503fbfbd15523c65f38d8b326e5f2159683cdb" + "url": "https://ftp.drupal.org/files/projects/redirect-8.x-1.3.zip", + "reference": "8.x-1.3", + "shasum": "3f9620d186e25f36ac56755979932b8ea965b8c7" }, "require": { "drupal/core": "~8" @@ -5429,8 +5479,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.2", - "datestamp": "1525419785", + "version": "8.x-1.3", + "datestamp": "1539682684", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -5449,6 +5499,10 @@ { "name": "Dave Reid", "homepage": "https://www.drupal.org/user/53892" + }, + { + "name": "pifagor", + "homepage": "https://www.drupal.org/user/2375692" } ], "description": "Allows users to redirect from old URLs to new URLs.", @@ -5463,7 +5517,7 @@ "source": { "type": "git", "url": "https://git.drupal.org/project/security_review", - "reference": "9a3bdf82a645dabb9801f73d2d76acf8c3081d88" + "reference": "9b8a34a21cac85913845df4eebb9a05c69de82d8" }, "require": { "drupal/core": "~8.0" @@ -5475,7 +5529,7 @@ }, "drupal": { "version": "8.x-1.x-dev", - "datestamp": "1519158780", + "datestamp": "1532558881", "security-coverage": { "status": "not-covered", "message": "Dev releases are not covered by Drupal security advisories." @@ -5510,7 +5564,7 @@ "support": { "source": "http://cgit.drupalcode.org/security_review" }, - "time": "2018-02-20T20:48:19+00:00" + "time": "2018-07-27T02:32:58+00:00" }, { "name": "drupal/simple_sitemap", @@ -5629,22 +5683,21 @@ }, { "name": "drupal/slick_media", - "version": "2.0.0-alpha1", + "version": "2.0.0-alpha2", "source": { "type": "git", "url": "https://git.drupal.org/project/slick_media", - "reference": "8.x-2.0-alpha1" + "reference": "8.x-2.0-alpha2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/slick_media-8.x-2.0-alpha1.zip", - "reference": "8.x-2.0-alpha1", - "shasum": "b7efb7edabd444be1f3ad925efcfa2eabb7a4fdf" + "url": "https://ftp.drupal.org/files/projects/slick_media-8.x-2.0-alpha2.zip", + "reference": "8.x-2.0-alpha2", + "shasum": "9aa7dfeaebc0776454bbf74450e5f2ed360f320d" }, "require": { "drupal/core": "^8.4", - "drupal/slick": "*", - "drupal/video_embed_media": "*" + "drupal/slick": "*" }, "type": "drupal-module", "extra": { @@ -5652,8 +5705,8 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.0-alpha1", - "datestamp": "1514970784", + "version": "8.x-2.0-alpha2", + "datestamp": "1533721384", "security-coverage": { "status": "not-covered", "message": "Alpha releases are not covered by Drupal security advisories." @@ -5834,20 +5887,20 @@ }, { "name": "drupal/token", - "version": "1.1.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://git.drupal.org/project/token", - "reference": "8.x-1.1" + "reference": "8.x-1.5" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/token-8.x-1.1.zip", - "reference": "8.x-1.1", - "shasum": "f11042a76bec028b0a86dc33cf6daa19eb55d545" + "url": "https://ftp.drupal.org/files/projects/token-8.x-1.5.zip", + "reference": "8.x-1.5", + "shasum": "6382a7e1aabbd8246f1117a26bf4916d285b401d" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8.5" }, "type": "drupal-module", "extra": { @@ -5855,8 +5908,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.1", - "datestamp": "1513810384", + "version": "8.x-1.5", + "datestamp": "1537557481", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -6080,51 +6133,6 @@ "source": "http://cgit.drupalcode.org/video_embed_field" } }, - { - "name": "drupal/video_embed_media", - "version": "2.0.0", - "require": { - "drupal/core": "^8.4", - "drupal/video_embed_field": "self.version" - }, - "type": "metapackage", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - }, - "drupal": { - "version": "8.x-2.0", - "datestamp": "1523338084", - "security-coverage": { - "status": "covered", - "message": "Covered by Drupal's security advisory policy" - } - } - }, - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "Sam152", - "homepage": "https://www.drupal.org/user/1485048" - }, - { - "name": "jec006", - "homepage": "https://www.drupal.org/user/855980" - }, - { - "name": "plopesc", - "homepage": "https://www.drupal.org/user/282415" - } - ], - "description": "Integrates video_embed_field with the Media module, creating a new media type tailored to display embedded videos. Useful for websites which are using the media suite of modules.", - "homepage": "https://www.drupal.org/project/video_embed_field", - "support": { - "source": "http://cgit.drupalcode.org/video_embed_field" - } - }, { "name": "drupal/videojs", "version": "1.0.0", @@ -6181,17 +6189,17 @@ }, { "name": "drupal/views_bootstrap", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://git.drupal.org/project/views_bootstrap", - "reference": "8.x-3.0" + "reference": "8.x-3.1" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/views_bootstrap-8.x-3.0.zip", - "reference": "8.x-3.0", - "shasum": "65f6ebc231b28235d0404140cc8420580206e101" + "url": "https://ftp.drupal.org/files/projects/views_bootstrap-8.x-3.1.zip", + "reference": "8.x-3.1", + "shasum": "7fd556457f028fa736d1422a9b5a763566fd7d2e" }, "require": { "drupal/core": "*" @@ -6202,8 +6210,8 @@ "dev-3.x": "3.x-dev" }, "drupal": { - "version": "8.x-3.0", - "datestamp": "1507098244", + "version": "8.x-3.1", + "datestamp": "1537360681", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -6338,54 +6346,88 @@ }, { "name": "drush/drush", - "version": "9.2.3", + "version": "9.5.2", "source": { "type": "git", "url": "https://github.com/drush-ops/drush.git", - "reference": "c07b5b4527d6fb9226adaee14a33de5d4aa93ce8" + "reference": "17f0106706391675a281c6d212850853bdbe90f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drush-ops/drush/zipball/c07b5b4527d6fb9226adaee14a33de5d4aa93ce8", - "reference": "c07b5b4527d6fb9226adaee14a33de5d4aa93ce8", + "url": "https://api.github.com/repos/drush-ops/drush/zipball/17f0106706391675a281c6d212850853bdbe90f9", + "reference": "17f0106706391675a281c6d212850853bdbe90f9", "shasum": "" }, "require": { - "chi-teck/drupal-code-generator": "^1.21.0", + "chi-teck/drupal-code-generator": "^1.27.0", "composer/semver": "^1.4", - "consolidation/annotated-command": "^2.8.1", - "consolidation/config": "^1.0.9", + "consolidation/annotated-command": "^2.9.1", + "consolidation/config": "^1.1.0", "consolidation/output-formatters": "^3.1.12", "consolidation/robo": "^1.1.5", + "consolidation/site-alias": "^1.1.5", "ext-dom": "*", "grasmash/yaml-expander": "^1.1.1", "league/container": "~2", "php": ">=5.6.0", "psr/log": "~1.0", "psy/psysh": "~0.6", - "sebastian/version": "^1|^2", - "symfony/config": "~2.2|^3", - "symfony/console": "~2.7|^3", - "symfony/event-dispatcher": "~2.7|^3", - "symfony/finder": "~2.7|^3", - "symfony/process": "~2.7|^3", - "symfony/var-dumper": "~2.7|^3", - "symfony/yaml": "~2.3|^3", + "symfony/config": "^3.4", + "symfony/console": "^3.4", + "symfony/event-dispatcher": "^3.4", + "symfony/finder": "^3.4", + "symfony/process": "^3.4", + "symfony/var-dumper": "^3.4", + "symfony/yaml": "^3.4", "webflo/drupal-finder": "^1.1", "webmozart/path-util": "^2.1.0" }, "require-dev": { + "composer/installers": "^1.2", + "cweagans/composer-patches": "~1.0", + "drupal/alinks": "1.0.0", + "drupal/devel": "^1.0@RC", + "drupal/empty_theme": "1.0", + "g1a/composer-test-scenarios": "^2.2.0", "lox/xhprof": "dev-master", - "phpunit/phpunit": "^4.8|^5.5.4", - "squizlabs/php_codesniffer": "^2.7" + "phpunit/phpunit": "^4.8.36 || ^6.1", + "squizlabs/php_codesniffer": "^2.7", + "vlucas/phpdotenv": "^2.4", + "webflo/drupal-core-strict": "8.6.x-dev" }, "bin": [ "drush" ], "type": "library", "extra": { + "installer-paths": { + "sut/core": [ + "type:drupal-core" + ], + "sut/libraries/{$name}": [ + "type:drupal-library" + ], + "sut/modules/unish/{$name}": [ + "drupal/devel" + ], + "sut/themes/unish/{$name}": [ + "drupal/empty_theme" + ], + "sut/modules/contrib/{$name}": [ + "type:drupal-module" + ], + "sut/profiles/contrib/{$name}": [ + "type:drupal-profile" + ], + "sut/themes/contrib/{$name}": [ + "type:drupal-theme" + ], + "sut/drush/contrib/{$name}": [ + "type:drupal-drush" + ] + }, "branch-alias": { - "dev-master": "9.0.x-dev" + "dev-master": "9.x-dev" } }, "autoload": { @@ -6435,7 +6477,7 @@ ], "description": "Drush is a command line shell and scripting interface for Drupal, a veritable Swiss Army knife designed to make life easier for those of us who spend some of our working hours hacking away at the command prompt.", "homepage": "http://www.drush.org", - "time": "2018-04-03T13:45:59+00:00" + "time": "2018-10-17T18:37:53+00:00" }, { "name": "easyrdf/easyrdf", @@ -6501,16 +6543,16 @@ }, { "name": "egulias/email-validator", - "version": "1.2.14", + "version": "1.2.15", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "5642614492f0ca2064c01d60cc33284cc2f731a9" + "reference": "758a77525bdaabd6c0f5669176bd4361cb2dda9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/5642614492f0ca2064c01d60cc33284cc2f731a9", - "reference": "5642614492f0ca2064c01d60cc33284cc2f731a9", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/758a77525bdaabd6c0f5669176bd4361cb2dda9e", + "reference": "758a77525bdaabd6c0f5669176bd4361cb2dda9e", "shasum": "" }, "require": { @@ -6549,7 +6591,7 @@ "validation", "validator" ], - "time": "2017-02-03T22:48:59+00:00" + "time": "2018-09-25T20:59:41+00:00" }, { "name": "enyo/dropzone", @@ -6634,6 +6676,39 @@ ], "time": "2018-02-23T01:58:20+00:00" }, + { + "name": "g1a/composer-test-scenarios", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/g1a/composer-test-scenarios.git", + "reference": "a166fd15191aceab89f30c097e694b7cf3db4880" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/g1a/composer-test-scenarios/zipball/a166fd15191aceab89f30c097e694b7cf3db4880", + "reference": "a166fd15191aceab89f30c097e694b7cf3db4880", + "shasum": "" + }, + "bin": [ + "scripts/create-scenario", + "scripts/dependency-licenses", + "scripts/install-scenario" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Anderson", + "email": "greg.1.anderson@greenknowe.org" + } + ], + "description": "Useful scripts for testing multiple sets of Composer dependencies.", + "time": "2018-08-08T23:37:23+00:00" + }, { "name": "geedmo/yamm3", "version": "1.1.0", @@ -7042,32 +7117,32 @@ }, { "name": "jakub-onderka/php-console-color", - "version": "0.1", + "version": "v0.2", "source": { "type": "git", "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", - "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1" + "reference": "d5deaecff52a0d61ccb613bb3804088da0307191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/e0b393dacf7703fc36a4efc3df1435485197e6c1", - "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191", + "reference": "d5deaecff52a0d61ccb613bb3804088da0307191", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": ">=5.4.0" }, "require-dev": { "jakub-onderka/php-code-style": "1.0", - "jakub-onderka/php-parallel-lint": "0.*", + "jakub-onderka/php-parallel-lint": "1.0", "jakub-onderka/php-var-dump-check": "0.*", - "phpunit/phpunit": "3.7.*", + "phpunit/phpunit": "~4.3", "squizlabs/php_codesniffer": "1.*" }, "type": "library", "autoload": { - "psr-0": { - "JakubOnderka\\PhpConsoleColor": "src/" + "psr-4": { + "JakubOnderka\\PhpConsoleColor\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7077,41 +7152,41 @@ "authors": [ { "name": "Jakub Onderka", - "email": "jakub.onderka@gmail.com", - "homepage": "http://www.acci.cz" + "email": "jakub.onderka@gmail.com" } ], - "time": "2014-04-08T15:00:19+00:00" + "time": "2018-09-29T17:23:10+00:00" }, { "name": "jakub-onderka/php-console-highlighter", - "version": "v0.3.2", + "version": "v0.4", "source": { "type": "git", "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", - "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5" + "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/7daa75df45242c8d5b75a22c00a201e7954e4fb5", - "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/9f7a229a69d52506914b4bc61bfdb199d90c5547", + "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547", "shasum": "" }, "require": { - "jakub-onderka/php-console-color": "~0.1", - "php": ">=5.3.0" + "ext-tokenizer": "*", + "jakub-onderka/php-console-color": "~0.2", + "php": ">=5.4.0" }, "require-dev": { "jakub-onderka/php-code-style": "~1.0", - "jakub-onderka/php-parallel-lint": "~0.5", + "jakub-onderka/php-parallel-lint": "~1.0", "jakub-onderka/php-var-dump-check": "~0.1", "phpunit/phpunit": "~4.0", "squizlabs/php_codesniffer": "~1.5" }, "type": "library", "autoload": { - "psr-0": { - "JakubOnderka\\PhpConsoleHighlighter": "src/" + "psr-4": { + "JakubOnderka\\PhpConsoleHighlighter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7125,7 +7200,8 @@ "homepage": "http://www.acci.cz/" } ], - "time": "2015-04-20T18:58:01+00:00" + "description": "Highlight PHP code in terminal", + "time": "2018-09-29T18:48:56+00:00" }, { "name": "league/container", @@ -7192,18 +7268,73 @@ ], "time": "2017-05-10T09:20:27+00:00" }, + { + "name": "lsolesen/pel", + "version": "0.9.6", + "source": { + "type": "git", + "url": "https://github.com/lsolesen/pel.git", + "reference": "c9e3919f5db3b85c3c422d4f8d448dbcb2a87a23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lsolesen/pel/zipball/c9e3919f5db3b85c3c422d4f8d448dbcb2a87a23", + "reference": "c9e3919f5db3b85c3c422d4f8d448dbcb2a87a23", + "shasum": "" + }, + "require": { + "php": ">=5.0.0" + }, + "require-dev": { + "ext-gd": "*", + "phpunit/phpunit": "5.7.*", + "satooshi/php-coveralls": "1.0.*", + "squizlabs/php_codesniffer": "3.0.0RC3" + }, + "type": "library", + "autoload": { + "psr-4": { + "lsolesen\\pel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0" + ], + "authors": [ + { + "name": "Lars Olesen", + "email": "lars@intraface.dk", + "homepage": "http://intraface.dk", + "role": "Developer" + }, + { + "name": "Martin Geisler", + "email": "martin@geisler.net", + "homepage": "http://geisler.net", + "role": "Developer" + } + ], + "description": "PHP Exif Library. A library for reading and writing Exif headers in JPEG and TIFF images using PHP.", + "homepage": "http://lsolesen.github.com/pel/", + "keywords": [ + "exif", + "image" + ], + "time": "2017-02-03T11:58:58+00:00" + }, { "name": "masterminds/html5", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "2c37c6c520b995b761674de3be8455a381679067" + "reference": "33f8d475d28741398be26cdff7a10a63003324a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/2c37c6c520b995b761674de3be8455a381679067", - "reference": "2c37c6c520b995b761674de3be8455a381679067", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/33f8d475d28741398be26cdff7a10a63003324a3", + "reference": "33f8d475d28741398be26cdff7a10a63003324a3", "shasum": "" }, "require": { @@ -7255,7 +7386,7 @@ "serializer", "xml" ], - "time": "2017-09-04T12:26:28+00:00" + "time": "2018-10-22T16:58:34+00:00" }, { "name": "mehrpadin/superfish", @@ -7369,16 +7500,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.0.1", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "e4a54fa90a5cd8e8dd3fb4099942681731c5cdd3" + "reference": "d0230c5c77a7e3cfa69446febf340978540958c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/e4a54fa90a5cd8e8dd3fb4099942681731c5cdd3", - "reference": "e4a54fa90a5cd8e8dd3fb4099942681731c5cdd3", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/d0230c5c77a7e3cfa69446febf340978540958c0", + "reference": "d0230c5c77a7e3cfa69446febf340978540958c0", "shasum": "" }, "require": { @@ -7394,7 +7525,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -7416,20 +7547,20 @@ "parser", "php" ], - "time": "2018-03-25T17:35:16+00:00" + "time": "2018-10-10T09:24:14+00:00" }, { "name": "paragonie/random_compat", - "version": "v2.0.12", + "version": "v2.0.17", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb" + "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/258c89a6b97de7dfaf5b8c7607d0478e236b04fb", - "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d", + "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d", "shasum": "" }, "require": { @@ -7461,10 +7592,48 @@ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", "keywords": [ "csprng", + "polyfill", "pseudorandom", "random" ], - "time": "2018-04-04T21:24:14+00:00" + "time": "2018-07-04T16:31:37+00:00" + }, + { + "name": "phenx/php-font-lib", + "version": "0.5", + "source": { + "type": "git", + "url": "https://github.com/PhenX/php-font-lib.git", + "reference": "19ad2bebc35be028fcc0221025fcbf3d436a3962" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhenX/php-font-lib/zipball/19ad2bebc35be028fcc0221025fcbf3d436a3962", + "reference": "19ad2bebc35be028fcc0221025fcbf3d436a3962", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Fabien Ménager", + "email": "fabien.menager@gmail.com" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/PhenX/php-font-lib", + "time": "2017-02-11T10:58:43+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -7663,21 +7832,23 @@ }, { "name": "psy/psysh", - "version": "v0.9.4", + "version": "v0.9.9", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "4d969a0e08e1e05e7207c07cb4207017ecc9a331" + "reference": "9aaf29575bb8293206bb0420c1e1c87ff2ffa94e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4d969a0e08e1e05e7207c07cb4207017ecc9a331", - "reference": "4d969a0e08e1e05e7207c07cb4207017ecc9a331", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/9aaf29575bb8293206bb0420c1e1c87ff2ffa94e", + "reference": "9aaf29575bb8293206bb0420c1e1c87ff2ffa94e", "shasum": "" }, "require": { "dnoegel/php-xdg-base-dir": "0.1", - "jakub-onderka/php-console-highlighter": "0.3.*", + "ext-json": "*", + "ext-tokenizer": "*", + "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*", "nikic/php-parser": "~1.3|~2.0|~3.0|~4.0", "php": ">=5.4.0", "symfony/console": "~2.3.10|^2.4.2|~3.0|~4.0", @@ -7731,7 +7902,7 @@ "interactive", "shell" ], - "time": "2018-05-22T06:48:07+00:00" + "time": "2018-10-13T15:16:03+00:00" }, { "name": "roave/security-advisories", @@ -7739,21 +7910,23 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "79ef6566b9c7fe6c1b6f6d12e5729d5cb545ac69" + "reference": "74a42b8d8d9f9cd672be58e7d1c65094da4ae971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/79ef6566b9c7fe6c1b6f6d12e5729d5cb545ac69", - "reference": "79ef6566b9c7fe6c1b6f6d12e5729d5cb545ac69", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/74a42b8d8d9f9cd672be58e7d1c65094da4ae971", + "reference": "74a42b8d8d9f9cd672be58e7d1c65094da4ae971", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", "adodb/adodb-php": "<5.20.12", + "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "amphp/artax": "<1.0.6|>=2,<2.0.6", "amphp/http": "<1.0.1", "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99", "aws/aws-sdk-php": ">=3,<3.2.1", + "brightlocal/phpwhois": "<=4.2.5", "bugsnag/bugsnag-laravel": ">=2,<2.0.2", "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.0.15|>=3.1,<3.1.4|>=3.4,<3.4.14|>=3.5,<3.5.17|>=3.6,<3.6.4", "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", @@ -7765,6 +7938,7 @@ "contao/core-bundle": ">=4,<4.4.18|>=4.5,<4.5.8", "contao/listing-bundle": ">=4,<4.4.8", "contao/newsletter-bundle": ">=4,<4.1", + "david-garcia/phpwhois": "<=4.3.1", "doctrine/annotations": ">=1,<1.2.7", "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1", @@ -7775,11 +7949,14 @@ "doctrine/mongodb-odm-bundle": ">=2,<3.0.1", "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1", "dompdf/dompdf": ">=0.6,<0.6.2", - "drupal/core": ">=7,<7.59|>=8,<8.4.8|>=8.5,<8.5.3", - "drupal/drupal": ">=7,<7.59|>=8,<8.4.8|>=8.5,<8.5.3", + "drupal/core": ">=7,<7.60|>=8,<8.5.8|>=8.6,<8.6.2", + "drupal/drupal": ">=7,<7.60|>=8,<8.5.8|>=8.6,<8.6.2", "erusev/parsedown": "<1.7", - "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.3|>=5.4,<5.4.11.3|>=2017.8,<2017.8.1.1|>=2017.12,<2017.12.2.1", + "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.5|>=5.4,<5.4.12.2|>=2017.8,<2017.8.1.1|>=2017.12,<2017.12.4.2|>=2018.6,<2018.6.1.3|>=2018.9,<2018.9.1.2", + "ezyang/htmlpurifier": "<4.1.1", "firebase/php-jwt": "<2", + "fooman/tcpdf": "<6.2.22", + "fossar/tcpdf-parser": "<6.2.22", "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", "fuel/core": "<1.8.1", @@ -7787,18 +7964,25 @@ "gregwar/rst": "<1.0.3", "guzzlehttp/guzzle": ">=6,<6.2.1|>=4.0.0-rc2,<4.2.4|>=5,<5.3.1", "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29", "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", + "ivankristianto/phpwhois": "<=4.3", + "james-heinrich/getid3": "<1.9.9", "joomla/session": "<1.3.1", + "jsmitty12/phpwhois": "<5.1", + "kazist/phpwhois": "<=4.2.6", "kreait/firebase-php": ">=3.2,<3.8.1", - "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", + "la-haute-societe/tcpdf": "<6.2.22", + "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", - "magento/magento1ce": ">=1.5.0.1,<1.9.3.2", + "magento/magento1ce": "<1.9.3.9", "magento/magento1ee": ">=1.9,<1.14.3.2", - "magento/magento2ce": ">=2,<2.2", + "magento/product-community-edition": ">=2,<2.2.6", "monolog/monolog": ">=1.8,<1.12", "namshi/jose": "<2.2", "onelogin/php-saml": "<2.10.4", + "openid/php-openid": "<2.3", "oro/crm": ">=1.7,<1.7.4", "oro/platform": ">=1.7,<1.7.4", "padraic/humbug_get_contents": "<1.1.2", @@ -7807,48 +7991,66 @@ "paypal/merchant-sdk-php": "<3.12", "phpmailer/phpmailer": ">=5,<5.2.24", "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", "propel/propel": ">=2.0.0-alpha1,<=2.0.0-alpha7", "propel/propel1": ">=1,<=1.7.1", "pusher/pusher-php-server": "<2.2.1", + "robrichards/xmlseclibs": ">=1,<3.0.2", "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9", + "sensiolabs/connect": "<4.2.3", + "serluck/phpwhois": "<=4.2.6", "shopware/shopware": "<5.3.7", "silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11", "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", "silverstripe/framework": ">=3,<3.3", "silverstripe/userforms": "<3", + "simple-updates/phpwhois": "<=1", "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4", "simplesamlphp/simplesamlphp": "<1.15.2", "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", "slim/slim": "<2.6", + "smarty/smarty": "<3.1.33", "socalnick/scn-social-auth": "<1.15.2", + "spoonity/tcpdf": "<6.2.22", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", "stormpath/sdk": ">=0,<9.9.99", "swiftmailer/swiftmailer": ">=4,<5.4.5", + "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", + "sylius/sylius": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", "symfony/dependency-injection": ">=2,<2.0.17", "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2", - "symfony/http-foundation": ">=2,<2.3.27|>=2.4,<2.5.11|>=2.6,<2.6.6", + "symfony/http-foundation": ">=2,<2.7.49|>=2.8,<2.8.44|>=3,<3.3.18|>=3.4,<3.4.14|>=4,<4.0.14|>=4.1,<4.1.3", "symfony/http-kernel": ">=2,<2.3.29|>=2.4,<2.5.12|>=2.6,<2.6.8", "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/polyfill": ">=1,<1.10", + "symfony/polyfill-php55": ">=1,<1.10", "symfony/routing": ">=2,<2.0.19", - "symfony/security": ">=2,<2.0.25|>=2.1,<2.1.13|>=2.2,<2.2.9|>=2.3,<2.3.37|>=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8.23,<2.8.25|>=3.2.10,<3.2.12|>=3.3.3,<3.3.5", - "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.6|>=2.8.23,<2.8.25|>=3,<3.0.6|>=3.2.10,<3.2.12|>=3.3.3,<3.3.5", - "symfony/security-csrf": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", - "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/security": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7", + "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", "symfony/serializer": ">=2,<2.0.11", - "symfony/symfony": ">=2,<2.3.41|>=2.4,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/symfony": ">=2,<2.7.49|>=2.8,<2.8.44|>=3,<3.3.18|>=3.4,<3.4.14|>=4,<4.0.14|>=4.1,<4.1.3", "symfony/translation": ">=2,<2.0.17", "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7", + "tecnickcom/tcpdf": "<6.2.22", "thelia/backoffice-default-template": ">=2.1,<2.1.2", "thelia/thelia": ">=2.1.0-beta1,<2.1.3|>=2.1,<2.1.2", + "theonedemon/phpwhois": "<=4.2.5", "titon/framework": ">=0,<9.9.99", + "truckersmp/phpwhois": "<=4.3.1", "twig/twig": "<1.20", - "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.22|>=8,<8.7.5", + "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.30|>=8,<8.7.17|>=9,<9.3.2", + "typo3/cms-core": ">=8,<8.7.17|>=9,<9.3.2", "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5", "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4", + "wallabag/tcpdf": "<6.2.22", "willdurand/js-translation-bundle": "<2.1.1", "yiisoft/yii": ">=1.1.14,<1.1.15", "yiisoft/yii2": "<2.0.15", @@ -7862,9 +8064,10 @@ "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5", - "zendframework/zend-diactoros": ">=1,<1.0.4", + "zendframework/zend-diactoros": ">=1,<1.8.4", + "zendframework/zend-feed": ">=1,<2.10.3", "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1", - "zendframework/zend-http": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.3,<2.3.8|>=2.4,<2.4.1", + "zendframework/zend-http": ">=1,<2.8.1", "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2", @@ -7873,7 +8076,7 @@ "zendframework/zend-validator": ">=2.3,<2.3.6", "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", - "zendframework/zendframework": ">=2,<2.4.11|>=2.5,<2.5.1", + "zendframework/zendframework": "<2.5.1", "zendframework/zendframework1": "<1.12.20", "zendframework/zendopenid": ">=2,<2.0.2", "zendframework/zendxml": ">=1,<1.0.1", @@ -7895,42 +8098,7 @@ } ], "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", - "time": "2018-05-21T07:43:38+00:00" - }, - { - "name": "sebastian/version", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21T13:59:46+00:00" + "time": "2018-11-01T18:39:28+00:00" }, { "name": "stack/builder", @@ -8135,16 +8303,16 @@ }, { "name": "symfony/class-loader", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e" + "reference": "5605edec7b8f034ead2497ff4aab17bb70d558c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/e63c12699822bb3b667e7216ba07fbcc3a3e203e", - "reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/5605edec7b8f034ead2497ff4aab17bb70d558c1", + "reference": "5605edec7b8f034ead2497ff4aab17bb70d558c1", "shasum": "" }, "require": { @@ -8187,20 +8355,20 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-10-31T09:06:03+00:00" }, { "name": "symfony/config", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "73e055cf2e6467715f187724a0347ea32079967c" + "reference": "99b2fa8acc244e656cdf324ff419fbe6fd300a4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/73e055cf2e6467715f187724a0347ea32079967c", - "reference": "73e055cf2e6467715f187724a0347ea32079967c", + "url": "https://api.github.com/repos/symfony/config/zipball/99b2fa8acc244e656cdf324ff419fbe6fd300a4d", + "reference": "99b2fa8acc244e656cdf324ff419fbe6fd300a4d", "shasum": "" }, "require": { @@ -8251,20 +8419,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-05-14T16:49:53+00:00" + "time": "2018-10-31T09:06:03+00:00" }, { "name": "symfony/console", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "36f83f642443c46f3cf751d4d2ee5d047d757a27" + "reference": "1d228fb4602047d7b26a0554e0d3efd567da5803" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/36f83f642443c46f3cf751d4d2ee5d047d757a27", - "reference": "36f83f642443c46f3cf751d4d2ee5d047d757a27", + "url": "https://api.github.com/repos/symfony/console/zipball/1d228fb4602047d7b26a0554e0d3efd567da5803", + "reference": "1d228fb4602047d7b26a0554e0d3efd567da5803", "shasum": "" }, "require": { @@ -8320,20 +8488,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-05-16T08:49:21+00:00" + "time": "2018-10-30T16:50:50+00:00" }, { "name": "symfony/css-selector", - "version": "v2.8.40", + "version": "v2.8.47", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "3cdc270724e4666006118283c700a4d7f9cbe264" + "reference": "208aca6c35e332f87c84707dd228d404370c8835" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/3cdc270724e4666006118283c700a4d7f9cbe264", - "reference": "3cdc270724e4666006118283c700a4d7f9cbe264", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/208aca6c35e332f87c84707dd228d404370c8835", + "reference": "208aca6c35e332f87c84707dd228d404370c8835", "shasum": "" }, "require": { @@ -8373,20 +8541,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-03-10T18:19:36+00:00" + "time": "2018-10-02T16:27:16+00:00" }, { "name": "symfony/debug", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68" + "reference": "fe9793af008b651c5441bdeab21ede8172dab097" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/b28fd73fefbac341f673f5efd707d539d6a19f68", - "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68", + "url": "https://api.github.com/repos/symfony/debug/zipball/fe9793af008b651c5441bdeab21ede8172dab097", + "reference": "fe9793af008b651c5441bdeab21ede8172dab097", "shasum": "" }, "require": { @@ -8429,20 +8597,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-05-16T14:03:39+00:00" + "time": "2018-10-31T09:06:03+00:00" }, { "name": "symfony/dependency-injection", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "c2741b26377804e3d70775a3ef29335685ee9b71" + "reference": "9c98452ac7fff4b538956775630bc9701f5384ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c2741b26377804e3d70775a3ef29335685ee9b71", - "reference": "c2741b26377804e3d70775a3ef29335685ee9b71", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9c98452ac7fff4b538956775630bc9701f5384ba", + "reference": "9c98452ac7fff4b538956775630bc9701f5384ba", "shasum": "" }, "require": { @@ -8500,20 +8668,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-05-18T01:58:36+00:00" + "time": "2018-10-31T10:49:51+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8" + "reference": "c705bee03ade5b47c087807dd9ffaaec8dda2722" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/201b210fafcdd193c1e45b2994bf7133fb6263e8", - "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c705bee03ade5b47c087807dd9ffaaec8dda2722", + "reference": "c705bee03ade5b47c087807dd9ffaaec8dda2722", "shasum": "" }, "require": { @@ -8557,20 +8725,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-05-01T22:53:27+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8" + "reference": "db9e829c8f34c3d35cf37fcd4cdb4293bc4a2f14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/fdd5abcebd1061ec647089c6c41a07ed60af09f8", - "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/db9e829c8f34c3d35cf37fcd4cdb4293bc4a2f14", + "reference": "db9e829c8f34c3d35cf37fcd4cdb4293bc4a2f14", "shasum": "" }, "require": { @@ -8620,20 +8788,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-04-06T07:35:25+00:00" + "time": "2018-10-30T16:50:50+00:00" }, { "name": "symfony/filesystem", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0" + "reference": "d69930fc337d767607267d57c20a7403d0a822a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0", - "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d69930fc337d767607267d57c20a7403d0a822a4", + "reference": "d69930fc337d767607267d57c20a7403d0a822a4", "shasum": "" }, "require": { @@ -8670,20 +8838,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-05-16T08:49:21+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/finder", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "472a92f3df8b247b49ae364275fb32943b9656c6" + "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/472a92f3df8b247b49ae364275fb32943b9656c6", - "reference": "472a92f3df8b247b49ae364275fb32943b9656c6", + "url": "https://api.github.com/repos/symfony/finder/zipball/54ba444dddc5bd5708a34bd095ea67c6eb54644d", + "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d", "shasum": "" }, "require": { @@ -8719,20 +8887,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-05-16T08:49:21+00:00" + "time": "2018-10-03T08:46:40+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "9a7469ec3e0225e7f0e14264bcd9e838e16186fe" + "reference": "5aea7a86ca3203dd7a257e765b4b9c9cfd01c6c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9a7469ec3e0225e7f0e14264bcd9e838e16186fe", - "reference": "9a7469ec3e0225e7f0e14264bcd9e838e16186fe", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5aea7a86ca3203dd7a257e765b4b9c9cfd01c6c0", + "reference": "5aea7a86ca3203dd7a257e765b4b9c9cfd01c6c0", "shasum": "" }, "require": { @@ -8773,20 +8941,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-05-16T08:49:21+00:00" + "time": "2018-10-31T08:57:11+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "66644bc7d17cc071d796efab9b950adbb86da134" + "reference": "4bf0be7c7fe63eff6a5eae2f21c83e77e31a56fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/66644bc7d17cc071d796efab9b950adbb86da134", - "reference": "66644bc7d17cc071d796efab9b950adbb86da134", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4bf0be7c7fe63eff6a5eae2f21c83e77e31a56fb", + "reference": "4bf0be7c7fe63eff6a5eae2f21c83e77e31a56fb", "shasum": "" }, "require": { @@ -8794,12 +8962,12 @@ "psr/log": "~1.0", "symfony/debug": "~2.8|~3.0|~4.0", "symfony/event-dispatcher": "~2.8|~3.0|~4.0", - "symfony/http-foundation": "^3.4.4|^4.0.4", + "symfony/http-foundation": "~3.4.12|~4.0.12|^4.1.1", "symfony/polyfill-ctype": "~1.8" }, "conflict": { "symfony/config": "<2.8", - "symfony/dependency-injection": "<3.4.5|<4.0.5,>=4", + "symfony/dependency-injection": "<3.4.10|<4.0.10,>=4", "symfony/var-dumper": "<3.3", "twig/twig": "<1.34|<2.4,>=2" }, @@ -8813,7 +8981,7 @@ "symfony/config": "~2.8|~3.0|~4.0", "symfony/console": "~2.8|~3.0|~4.0", "symfony/css-selector": "~2.8|~3.0|~4.0", - "symfony/dependency-injection": "^3.4.5|^4.0.5", + "symfony/dependency-injection": "^3.4.10|^4.0.10", "symfony/dom-crawler": "~2.8|~3.0|~4.0", "symfony/expression-language": "~2.8|~3.0|~4.0", "symfony/finder": "~2.8|~3.0|~4.0", @@ -8862,29 +9030,32 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-05-21T13:44:03+00:00" + "time": "2018-11-03T10:03:02+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "suggest": { + "ext-ctype": "For best performance" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -8917,20 +9088,20 @@ "polyfill", "portable" ], - "time": "2018-04-30T19:57:29+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "7cb8436a814d5b0fcf292810ee26f8b0cb47584d" + "reference": "97001cfc283484c9691769f51cdf25259037eba2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/7cb8436a814d5b0fcf292810ee26f8b0cb47584d", - "reference": "7cb8436a814d5b0fcf292810ee26f8b0cb47584d", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/97001cfc283484c9691769f51cdf25259037eba2", + "reference": "97001cfc283484c9691769f51cdf25259037eba2", "shasum": "" }, "require": { @@ -8942,7 +9113,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -8976,20 +9147,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-09-21T06:26:08+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", "shasum": "" }, "require": { @@ -9001,7 +9172,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -9035,30 +9206,30 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-09-21T13:07:52+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "77454693d8f10dd23bb24955cffd2d82db1007a6" + "reference": "6b88000cdd431cd2e940caa2cb569201f3f84224" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/77454693d8f10dd23bb24955cffd2d82db1007a6", - "reference": "77454693d8f10dd23bb24955cffd2d82db1007a6", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/6b88000cdd431cd2e940caa2cb569201f3f84224", + "reference": "6b88000cdd431cd2e940caa2cb569201f3f84224", "shasum": "" }, "require": { - "paragonie/random_compat": "~1.0|~2.0", + "paragonie/random_compat": "~1.0|~2.0|~9.99", "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -9094,20 +9265,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-09-21T06:26:08+00:00" }, { "name": "symfony/process", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "4cbf2db9abcb01486a21b7a059e03a62fae63187" + "reference": "35c2914a9f50519bd207164c353ae4d59182c2cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4cbf2db9abcb01486a21b7a059e03a62fae63187", - "reference": "4cbf2db9abcb01486a21b7a059e03a62fae63187", + "url": "https://api.github.com/repos/symfony/process/zipball/35c2914a9f50519bd207164c353ae4d59182c2cb", + "reference": "35c2914a9f50519bd207164c353ae4d59182c2cb", "shasum": "" }, "require": { @@ -9143,38 +9314,39 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-05-16T08:49:21+00:00" + "time": "2018-10-14T17:33:21+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v1.0.2", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "c2b757934f2d9681a287e662efbc27c41fe8ef86" + "reference": "53c15a6a7918e6c2ab16ae370ea607fb40cab196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/c2b757934f2d9681a287e662efbc27c41fe8ef86", - "reference": "c2b757934f2d9681a287e662efbc27c41fe8ef86", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/53c15a6a7918e6c2ab16ae370ea607fb40cab196", + "reference": "53c15a6a7918e6c2ab16ae370ea607fb40cab196", "shasum": "" }, "require": { - "php": ">=5.3.3", - "psr/http-message": "~1.0", - "symfony/http-foundation": "~2.3|~3.0|~4.0" + "php": "^5.3.3 || ^7.0", + "psr/http-message": "^1.0", + "symfony/http-foundation": "^2.3.42 || ^3.4 || ^4.0" }, "require-dev": { - "symfony/phpunit-bridge": "~3.2|4.0" + "symfony/phpunit-bridge": "^3.4 || 4.0" }, "suggest": { + "psr/http-factory-implementation": "To use the PSR-17 factory", "psr/http-message-implementation": "To use the HttpFoundation factory", "zendframework/zend-diactoros": "To use the Zend Diactoros factory" }, "type": "symfony-bridge", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.1-dev" } }, "autoload": { @@ -9203,20 +9375,20 @@ "http-message", "psr-7" ], - "time": "2017-12-19T00:31:44+00:00" + "time": "2018-08-30T16:28:28+00:00" }, { "name": "symfony/routing", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "e382da877f5304aabc12ec3073eec430670c8296" + "reference": "585f6e2d740393d546978769dd56e496a6233e0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/e382da877f5304aabc12ec3073eec430670c8296", - "reference": "e382da877f5304aabc12ec3073eec430670c8296", + "url": "https://api.github.com/repos/symfony/routing/zipball/585f6e2d740393d546978769dd56e496a6233e0b", + "reference": "585f6e2d740393d546978769dd56e496a6233e0b", "shasum": "" }, "require": { @@ -9229,7 +9401,6 @@ }, "require-dev": { "doctrine/annotations": "~1.0", - "doctrine/common": "~2.2", "psr/log": "~1.0", "symfony/config": "^3.3.1|~4.0", "symfony/dependency-injection": "~3.3|~4.0", @@ -9281,20 +9452,20 @@ "uri", "url" ], - "time": "2018-05-16T12:49:49+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/serializer", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "2f3630bb2b855de2284798db34d3ac5734766575" + "reference": "8bc00ef47a428bfebc4641f29d158e7c56137fcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/2f3630bb2b855de2284798db34d3ac5734766575", - "reference": "2f3630bb2b855de2284798db34d3ac5734766575", + "url": "https://api.github.com/repos/symfony/serializer/zipball/8bc00ef47a428bfebc4641f29d158e7c56137fcb", + "reference": "8bc00ef47a428bfebc4641f29d158e7c56137fcb", "shasum": "" }, "require": { @@ -9360,20 +9531,20 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "time": "2018-05-16T12:49:49+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/translation", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "7047f725e35eab768137c677f8c38e4a2a8e38fb" + "reference": "94bc3a79008e6640defedf5e14eb3b4f20048352" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/7047f725e35eab768137c677f8c38e4a2a8e38fb", - "reference": "7047f725e35eab768137c677f8c38e4a2a8e38fb", + "url": "https://api.github.com/repos/symfony/translation/zipball/94bc3a79008e6640defedf5e14eb3b4f20048352", + "reference": "94bc3a79008e6640defedf5e14eb3b4f20048352", "shasum": "" }, "require": { @@ -9428,20 +9599,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-05-21T10:06:52+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/validator", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "62ccdf62042fbca3be1e7b2dae84965940023362" + "reference": "6ab5fee7c0763d90753f37fce6db9fdbca1b1a4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/62ccdf62042fbca3be1e7b2dae84965940023362", - "reference": "62ccdf62042fbca3be1e7b2dae84965940023362", + "url": "https://api.github.com/repos/symfony/validator/zipball/6ab5fee7c0763d90753f37fce6db9fdbca1b1a4c", + "reference": "6ab5fee7c0763d90753f37fce6db9fdbca1b1a4c", "shasum": "" }, "require": { @@ -9513,20 +9684,20 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2018-05-18T02:00:55+00:00" + "time": "2018-10-14T18:32:13+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "0e6545672d8c9ce70dd472adc2f8b03155a46f73" + "reference": "ff8ac19e97e5c7c3979236b584719a1190f84181" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e6545672d8c9ce70dd472adc2f8b03155a46f73", - "reference": "0e6545672d8c9ce70dd472adc2f8b03155a46f73", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ff8ac19e97e5c7c3979236b584719a1190f84181", + "reference": "ff8ac19e97e5c7c3979236b584719a1190f84181", "shasum": "" }, "require": { @@ -9582,20 +9753,20 @@ "debug", "dump" ], - "time": "2018-04-26T12:42:15+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0" + "reference": "640b6c27fed4066d64b64d5903a86043f4a4de7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c5010cc1692ce1fa328b1fb666961eb3d4a85bb0", - "reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/640b6c27fed4066d64b64d5903a86043f4a4de7f", + "reference": "640b6c27fed4066d64b64d5903a86043f4a4de7f", "shasum": "" }, "require": { @@ -9641,7 +9812,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-05-03T23:18:14+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "twbs/bootstrap-sass", @@ -9698,20 +9869,21 @@ }, { "name": "twig/twig", - "version": "v1.35.3", + "version": "v1.35.4", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "b48680b6eb7d16b5025b9bfc4108d86f6b8af86f" + "reference": "7e081e98378a1e78c29cc9eba4aefa5d78a05d2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/b48680b6eb7d16b5025b9bfc4108d86f6b8af86f", - "reference": "b48680b6eb7d16b5025b9bfc4108d86f6b8af86f", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/7e081e98378a1e78c29cc9eba4aefa5d78a05d2a", + "reference": "7e081e98378a1e78c29cc9eba4aefa5d78a05d2a", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.3.3", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "psr/container": "^1.0", @@ -9750,16 +9922,16 @@ }, { "name": "Twig Team", - "homepage": "http://twig.sensiolabs.org/contributors", + "homepage": "https://twig.symfony.com/contributors", "role": "Contributors" } ], "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "http://twig.sensiolabs.org", + "homepage": "https://twig.symfony.com", "keywords": [ "templating" ], - "time": "2018-03-20T04:25:58+00:00" + "time": "2018-07-13T07:12:17+00:00" }, { "name": "webflo/drupal-finder", @@ -9896,16 +10068,16 @@ }, { "name": "zendframework/zend-diactoros", - "version": "1.7.1", + "version": "1.8.6", "source": { "type": "git", "url": "https://github.com/zendframework/zend-diactoros.git", - "reference": "bf26aff803a11c5cc8eb7c4878a702c403ec67f1" + "reference": "20da13beba0dde8fb648be3cc19765732790f46e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/bf26aff803a11c5cc8eb7c4878a702c403ec67f1", - "reference": "bf26aff803a11c5cc8eb7c4878a702c403ec67f1", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e", + "reference": "20da13beba0dde8fb648be3cc19765732790f46e", "shasum": "" }, "require": { @@ -9918,17 +10090,29 @@ "require-dev": { "ext-dom": "*", "ext-libxml": "*", - "phpunit/phpunit": "^5.7.16 || ^6.0.8", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7", "zendframework/zend-coding-standard": "~1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev", - "dev-develop": "1.8.x-dev" + "dev-master": "1.8.x-dev", + "dev-develop": "1.9.x-dev", + "dev-release-2.0": "2.0.x-dev" } }, "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], "psr-4": { "Zend\\Diactoros\\": "src/" } @@ -9944,7 +10128,7 @@ "psr", "psr-7" ], - "time": "2018-02-26T15:44:50+00:00" + "time": "2018-09-05T19:29:37+00:00" }, { "name": "zendframework/zend-escaper", @@ -9993,16 +10177,16 @@ }, { "name": "zendframework/zend-feed", - "version": "2.9.1", + "version": "2.10.3", "source": { "type": "git", "url": "https://github.com/zendframework/zend-feed.git", - "reference": "15a6ca78dd8f67b687e4f902657b4e420a8fd1e0" + "reference": "6641f4cf3f4586c63f83fd70b6d19966025c8888" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-feed/zipball/15a6ca78dd8f67b687e4f902657b4e420a8fd1e0", - "reference": "15a6ca78dd8f67b687e4f902657b4e420a8fd1e0", + "url": "https://api.github.com/repos/zendframework/zend-feed/zipball/6641f4cf3f4586c63f83fd70b6d19966025c8888", + "reference": "6641f4cf3f4586c63f83fd70b6d19966025c8888", "shasum": "" }, "require": { @@ -10031,8 +10215,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.9-dev", - "dev-develop": "2.10-dev" + "dev-master": "2.10.x-dev", + "dev-develop": "2.11.x-dev" } }, "autoload": { @@ -10050,20 +10234,20 @@ "feed", "zf" ], - "time": "2018-05-14T21:45:18+00:00" + "time": "2018-08-01T13:53:20+00:00" }, { "name": "zendframework/zend-stdlib", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-stdlib.git", - "reference": "cd164b4a18b5d1aeb69be2c26db035b5ed6925ae" + "reference": "66536006722aff9e62d1b331025089b7ec71c065" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/cd164b4a18b5d1aeb69be2c26db035b5ed6925ae", - "reference": "cd164b4a18b5d1aeb69be2c26db035b5ed6925ae", + "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/66536006722aff9e62d1b331025089b7ec71c065", + "reference": "66536006722aff9e62d1b331025089b7ec71c065", "shasum": "" }, "require": { @@ -10096,7 +10280,7 @@ "stdlib", "zf" ], - "time": "2018-04-30T13:50:40+00:00" + "time": "2018-08-28T21:34:05+00:00" } ], "packages-dev": [ @@ -10469,16 +10653,16 @@ }, { "name": "fabpot/goutte", - "version": "v3.2.2", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/Goutte.git", - "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96" + "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/395f61d7c2e15a813839769553a4de16fa3b3c96", - "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/3f0eaf0a40181359470651f1565b3e07e3dd31b8", + "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8", "shasum": "" }, "require": { @@ -10520,7 +10704,7 @@ "keywords": [ "scraper" ], - "time": "2017-11-19T08:45:40+00:00" + "time": "2018-06-29T15:13:57+00:00" }, { "name": "jcalderonzumba/gastonjs", @@ -10685,16 +10869,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.7.6", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", "shasum": "" }, "require": { @@ -10706,12 +10890,12 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -10744,7 +10928,7 @@ "spy", "stub" ], - "time": "2018-04-18T13:57:24+00:00" + "time": "2018-08-05T17:53:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -11459,18 +11643,53 @@ "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "time": "2016-10-03T07:41:43+00:00" }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21T13:59:46+00:00" + }, { "name": "symfony/browser-kit", - "version": "v3.4.10", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79" + "reference": "f6668d1a6182d5a8dec65a1c863a4c1d963816c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/840bb6f0d5b3701fd768b68adf7193c2d0f98f79", - "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/f6668d1a6182d5a8dec65a1c863a4c1d963816c0", + "reference": "f6668d1a6182d5a8dec65a1c863a4c1d963816c0", "shasum": "" }, "require": { @@ -11514,7 +11733,7 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2018-03-19T22:32:39+00:00" + "time": "2018-07-26T09:06:28+00:00" } ], "aliases": [], @@ -11533,12 +11752,10 @@ "drupal/block_class": 15, "drupal/libraries": 15, "drupal/linkchecker": 20, - "drupal/security_review": 20, "drupal/memcache": 15, "grom358/pharborist": 20, "drupal/superfish": 5, "drupal/views_responsive_grid": 20, - "drupal/imagemagick": 15, "drupal/diff": 5, "drupal/ckeditor_widgets": 20, "drupal/layout_plugin": 15, @@ -11546,6 +11763,7 @@ "drupal/inline_entity_form": 10, "drupal/entity_embed": 10, "drupal/dropzonejs": 15, + "drupal/security_review": 20, "drupal/livereload": 20 }, "prefer-stable": true, @@ -11553,6 +11771,6 @@ "platform": [], "platform-dev": [], "platform-overrides": { - "php": "7.0.27" + "php": "7.0.30" } } diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index 981c16091..9efa99a86 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -24,9 +24,9 @@ return array( '1ca3bc274755662169f9629d5412a1da' => $vendorDir . '/zendframework/zend-diactoros/src/functions/normalize_uploaded_files.php', '40360c0b9b437e69bcbb7f1349ce029e' => $vendorDir . '/zendframework/zend-diactoros/src/functions/parse_cookie_header.php', '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', - 'fbeead2280a8f3911a1fe6dd034f7d5e' => $vendorDir . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvancedInterface.php', - '96f8d8288528d52059397cad6ec61f17' => $vendorDir . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvanced.php', '801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php', '952683d815ff0a7bf322b93c0be7e4e4' => $vendorDir . '/chi-teck/drupal-code-generator/src/bootstrap.php', '5a12a5271c58108e0aa33355e6ac54ea' => $vendorDir . '/drupal/console-core/src/functions.php', + 'fbeead2280a8f3911a1fe6dd034f7d5e' => $vendorDir . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvancedInterface.php', + '96f8d8288528d52059397cad6ec61f17' => $vendorDir . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvanced.php', ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 969c0c0c8..df87bb0e1 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -25,11 +25,11 @@ class ComposerStaticInit045d6a3105edf51cf91c16e965235549 '1ca3bc274755662169f9629d5412a1da' => __DIR__ . '/..' . '/zendframework/zend-diactoros/src/functions/normalize_uploaded_files.php', '40360c0b9b437e69bcbb7f1349ce029e' => __DIR__ . '/..' . '/zendframework/zend-diactoros/src/functions/parse_cookie_header.php', '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', - 'fbeead2280a8f3911a1fe6dd034f7d5e' => __DIR__ . '/..' . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvancedInterface.php', - '96f8d8288528d52059397cad6ec61f17' => __DIR__ . '/..' . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvanced.php', '801c31d8ed748cfa537fa45402288c95' => __DIR__ . '/..' . '/psy/psysh/src/functions.php', '952683d815ff0a7bf322b93c0be7e4e4' => __DIR__ . '/..' . '/chi-teck/drupal-code-generator/src/bootstrap.php', '5a12a5271c58108e0aa33355e6ac54ea' => __DIR__ . '/..' . '/drupal/console-core/src/functions.php', + 'fbeead2280a8f3911a1fe6dd034f7d5e' => __DIR__ . '/..' . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvancedInterface.php', + '96f8d8288528d52059397cad6ec61f17' => __DIR__ . '/..' . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvanced.php', ); public static $prefixLengthsPsr4 = array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 7d74c928c..d8035451f 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -3750,71 +3750,6 @@ "irc": "irc://irc.freenode.org/drupal-media" } }, - { - "name": "drupal/entity", - "version": "1.0.0-rc1", - "version_normalized": "1.0.0.0-RC1", - "source": { - "type": "git", - "url": "https://git.drupal.org/project/entity", - "reference": "8.x-1.0-rc1" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity-8.x-1.0-rc1.zip", - "reference": "8.x-1.0-rc1", - "shasum": "f2dcd7afd0d36b6f261b10a204057a5c801ad20b" - }, - "require": { - "drupal/core": "^8.6" - }, - "type": "drupal-module", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, - "drupal": { - "version": "8.x-1.0-rc1", - "datestamp": "1539272769", - "security-coverage": { - "status": "not-covered", - "message": "RC releases are not covered by Drupal security advisories." - } - } - }, - "installation-source": "dist", - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0+" - ], - "authors": [ - { - "name": "Berdir", - "homepage": "https://www.drupal.org/user/214652" - }, - { - "name": "bojanz", - "homepage": "https://www.drupal.org/user/86106" - }, - { - "name": "dawehner", - "homepage": "https://www.drupal.org/user/99340" - }, - { - "name": "dixon_", - "homepage": "https://www.drupal.org/user/239911" - }, - { - "name": "fago", - "homepage": "https://www.drupal.org/user/16747" - } - ], - "description": "Provides expanded entity APIs, which will be moved to Drupal core one day.", - "homepage": "http://drupal.org/project/entity", - "support": { - "source": "http://cgit.drupalcode.org/entity" - } - }, { "name": "drupal/entity_browser", "version": "1.6.0", @@ -3968,66 +3903,6 @@ "irc": "irc://irc.freenode.org/drupal-media" } }, - { - "name": "drupal/entity_reference_revisions", - "version": "1.6.0", - "version_normalized": "1.6.0.0", - "source": { - "type": "git", - "url": "https://git.drupal.org/project/entity_reference_revisions", - "reference": "8.x-1.6" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.6.zip", - "reference": "8.x-1.6", - "shasum": "82d515de04d3a75fb677ed82241a6aff3f54ab47" - }, - "require": { - "drupal/core": "~8.0" - }, - "require-dev": { - "drupal/diff": "*" - }, - "type": "drupal-module", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, - "drupal": { - "version": "8.x-1.6", - "datestamp": "1539588781", - "security-coverage": { - "status": "covered", - "message": "Covered by Drupal's security advisory policy" - } - } - }, - "installation-source": "dist", - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "Frans", - "homepage": "https://www.drupal.org/user/514222" - }, - { - "name": "jeroen.b", - "homepage": "https://www.drupal.org/user/1853532" - }, - { - "name": "miro_dietiker", - "homepage": "https://www.drupal.org/user/227761" - } - ], - "description": "Adds a Entity Reference field type with revision support.", - "homepage": "https://www.drupal.org/project/entity_reference_revisions", - "support": { - "source": "http://cgit.drupalcode.org/entity_reference_revisions" - } - }, { "name": "drupal/entityqueue", "version": "1.0.0-alpha8", @@ -5221,104 +5096,6 @@ "source": "http://cgit.drupalcode.org/livereload" } }, - { - "name": "drupal/media_entity", - "version": "2.0.0-beta3", - "version_normalized": "2.0.0.0-beta3", - "source": { - "type": "git", - "url": "https://git.drupal.org/project/media_entity", - "reference": "8.x-2.0-beta3" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/media_entity-8.x-2.0-beta3.zip", - "reference": "8.x-2.0-beta3", - "shasum": "c07bf870f279efaf642b9963ea23ea6977982e67" - }, - "require": { - "drupal/core": "*", - "drupal/entity": "*" - }, - "type": "drupal-module", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - }, - "drupal": { - "version": "8.x-2.0-beta3", - "datestamp": "1536584580", - "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." - } - } - }, - "installation-source": "dist", - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "Berdir", - "homepage": "https://www.drupal.org/user/214652" - }, - { - "name": "Dave Reid", - "homepage": "https://www.drupal.org/user/53892" - }, - { - "name": "Drupal Media Team", - "homepage": "https://www.drupal.org/user/3260690" - }, - { - "name": "Drupal media CI", - "homepage": "https://www.drupal.org/user/3057985" - }, - { - "name": "Primsi", - "homepage": "https://www.drupal.org/user/282629" - }, - { - "name": "boztek", - "homepage": "https://www.drupal.org/user/134410" - }, - { - "name": "chr.fritsch", - "homepage": "https://www.drupal.org/user/2103716" - }, - { - "name": "jcisio", - "homepage": "https://www.drupal.org/user/210762" - }, - { - "name": "katzilla", - "homepage": "https://www.drupal.org/user/260398" - }, - { - "name": "marcoscano", - "homepage": "https://www.drupal.org/user/1288796" - }, - { - "name": "phenaproxima", - "homepage": "https://www.drupal.org/user/205645" - }, - { - "name": "seanB", - "homepage": "https://www.drupal.org/user/545912" - }, - { - "name": "slashrsm", - "homepage": "https://www.drupal.org/user/744628" - } - ], - "description": "Media entity API.", - "homepage": "https://www.drupal.org/project/media_entity", - "support": { - "source": "http://cgit.drupalcode.org/media_entity" - } - }, { "name": "drupal/media_entity_actions", "version": "1.0.0-alpha2", @@ -5914,88 +5691,6 @@ "source": "http://cgit.drupalcode.org/migrate_upgrade" } }, - { - "name": "drupal/paragraphs", - "version": "1.5.0", - "version_normalized": "1.5.0.0", - "source": { - "type": "git", - "url": "https://git.drupal.org/project/paragraphs", - "reference": "8.x-1.5" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/paragraphs-8.x-1.5.zip", - "reference": "8.x-1.5", - "shasum": "85ba97dd1c602d33fc5904b6e1df5973312afa94" - }, - "require": { - "drupal/core": "~8", - "drupal/entity_reference_revisions": "~1.3" - }, - "require-dev": { - "drupal/block_field": "~1.0", - "drupal/ctools": "3.x-dev", - "drupal/diff": "~1.0", - "drupal/entity_browser": "1.x-dev", - "drupal/entity_usage": "2.x-dev", - "drupal/field_group": "~1.0", - "drupal/inline_entity_form": "~1.0", - "drupal/paragraphs-paragraphs_library": "*", - "drupal/replicate": "~1.0", - "drupal/search_api": "~1.0", - "drupal/search_api_db": "*" - }, - "suggest": { - "drupal/entity_browser": "Recommended for an improved user experience when using the Paragraphs library module" - }, - "type": "drupal-module", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, - "drupal": { - "version": "8.x-1.5", - "datestamp": "1541009695", - "security-coverage": { - "status": "covered", - "message": "Covered by Drupal's security advisory policy" - } - } - }, - "installation-source": "dist", - "notification-url": "https://packages.drupal.org/8/downloads", - "license": [ - "GPL-2.0" - ], - "authors": [ - { - "name": "Berdir", - "homepage": "https://www.drupal.org/user/214652" - }, - { - "name": "Frans", - "homepage": "https://www.drupal.org/user/514222" - }, - { - "name": "Primsi", - "homepage": "https://www.drupal.org/user/282629" - }, - { - "name": "jeroen.b", - "homepage": "https://www.drupal.org/user/1853532" - }, - { - "name": "miro_dietiker", - "homepage": "https://www.drupal.org/user/227761" - } - ], - "description": "Enables the creation of Paragraphs entities.", - "homepage": "https://www.drupal.org/project/paragraphs", - "support": { - "source": "http://cgit.drupalcode.org/paragraphs" - } - }, { "name": "drupal/pathauto", "version": "1.3.0", diff --git a/web/modules/contrib/admin_toolbar/CHANGELOG.txt b/web/modules/contrib/admin_toolbar/CHANGELOG.txt index 12f4450ae..a3a0d75b4 100644 --- a/web/modules/contrib/admin_toolbar/CHANGELOG.txt +++ b/web/modules/contrib/admin_toolbar/CHANGELOG.txt @@ -1,6 +1,33 @@ -Admin Toolbar 8.1.23, 2018-02-06 --------------------------------- -Changes since 8.1.22: +Admin Toolbar 8.x-1.24, 2018-05-28 +---------------------------------- +Changes since 8.x-1.23: + +- #2973131 by adriancid: Don't refer to Drupal in the help page. +- #2975170 by adriancid: Add composer.json file to submodules. +- #2975165 by Gamewalker, adriancid: datetime.time services is not found in + ToolbarController. +- #2971466 by recrit, adriancid, acbramley: Add media links incorrectly set to + "node.add.". +- #2972553 by dww, adriancid: Add static cache to speed up + admin_toolbar_links_access_filter_user_has_admin_role(). +- #2971435 by adriancid: Module help page for Admin Toolbar Extra Tools module + doesn't show the links. +- #2971398 by adriancid: Add the docblock to the ToolbarController constructor. +- #2969686 by adriancid, Vidushi Mehta: Add a menu link to clean the twig cache. +- #2969325 by adriancid: Fix the module version in the CHANGELOG.txt. +- #2961450 by adriancid: Convert the AdminToolbarAlterTest test class to + PHPUnit. +- #2961445 by adriancid: Convert the AdminToolbarToolsAlterTest test class to + PHPUnit. +- #2959684 by adriancid: Remove recommended modules section from README.txt. +- #2959647 by adriancid: Use interfaces instead of classes to inject + dependencies. +- #2952643 by romainj, if-jds, adriancid: Add Item for Files under Content Menu. +- #2944463 by adriancid: Update the composer.json file. + +Admin Toolbar 8.x-1.23, 2018-02-06 +---------------------------------- +Changes since 8.x-1.22: - #2924266 by sunlix, romainj, adriancid, samerali: Add a menu link for the media module. @@ -19,9 +46,9 @@ Changes since 8.1.22: core/drupal library. - #2931503 by K3vin_nl: Admin toolbar generates invalid class names. -Admin Toolbar 8.1.22, 2018-01-02 --------------------------------- -Changes since 8.1.21: +Admin Toolbar 8.x-1.22, 2018-01-02 +---------------------------------- +Changes since 8.x-1.21: - #2929053 by kkuhnen, eme, adriancid: admin_toolbar.js should use Drupal behaviors. @@ -42,9 +69,9 @@ Changes since 8.1.21: - #2925128 by adriancid: Create the module help page for the Admin Toolbar Links Access Filter submodule. -Admin Toolbar 8.1.21, 2017-11-20 --------------------------------- -Changes since 8.1.20: +Admin Toolbar 8.x-1.21, 2017-11-20 +---------------------------------- +Changes since 8.x-1.20: - #2923580 by sylus, adriancid: Unsupported operand types in ToolbarHandler::lazyBuilder(). @@ -87,9 +114,9 @@ Changes since 8.1.20: - #2909003 by eme: fix z-index for CKEditor. - #2504449 by robin.ingelbrecht, rwam, eme: Un-hover delay. -Admin Toolbar 8.1.20, 2017-09-07 --------------------------------- -Changes since 8.1.19: +Admin Toolbar 8.x-1.20, 2017-09-07 +---------------------------------- +Changes since 8.x-1.19: - #2887439 by romainj: Fix test. - #2759135 by Johnny vd Laar, miiimooo, chegor, romainj: Content type, @@ -106,9 +133,9 @@ Changes since 8.1.19: - #2870404 by romainj: Add a link to the Webprofiler settings page. - #2706643 by esod, romainj, Keenegan, DamienMcKenna: Use short array syntax. -Admin Toolbar 8.1.19, 2017-04-06 --------------------------------- -Changes since 8.1.18: +Admin Toolbar 8.x-1.19, 2017-04-06 +---------------------------------- +Changes since 8.x-1.18: - #2706643 by esod, romainj, Keenegan, DamienMcKenna: Use short array syntax. - #2781745 by minakshiPh, eelkeblok, romainj, akhilavnair_zyxware: Drupal coding @@ -117,9 +144,9 @@ Changes since 8.1.18: - #2841512 by Chi, romainj: User error: Redirects to external URLs are not allowed by default. -Admin Toolbar 8.1.18, 2016-12-01 --------------------------------- -Changes since 8.1.17: +Admin Toolbar 8.x-1.18, 2016-12-01 +---------------------------------- +Changes since 8.x-1.17: - #2830677 by vaplas, romainj: z-index for sub menu. - by romainj: Get rid of the Hello popup. @@ -130,9 +157,9 @@ Changes since 8.1.17: - #2518202 by james.williams, chegor, eme, romainj, DuneBL: Change default link to taxonomy. -Admin Toolbar 8.1.17, 2016-08-19 --------------------------------- -Changes since 8.1.16: +Admin Toolbar 8.x-1.17, 2016-08-19 +---------------------------------- +Changes since 8.x-1.16: - #2779251 by romainj, DuneBL: Wrong integration of field_collection delete form. @@ -150,9 +177,9 @@ Changes since 8.1.16: - #2707747 by Balu Ertl, nevergone, eme, esod: D8 logo not rendered with smooth antialiasing. -Admin Toolbar 8.1.16, 2016-07-28 --------------------------------- -Changes since 8.1.15: +Admin Toolbar 8.x-1.16, 2016-07-28 +---------------------------------- +Changes since 8.x-1.15: - #2723209 by andrewmacpherson, colan, mattshoaf: Enabling admin_toolbar_tools causes Devel Settings to disappear from the main configuration page. @@ -185,9 +212,9 @@ Changes since 8.1.15: - by romainj: Fixes issue #2731663 by canceling the move of the Logout menu link. -Admin Toolbar 8.1.15, 2016-05-23 --------------------------------- -Changes since 8.1.14: +Admin Toolbar 8.x-1.15, 2016-05-23 +---------------------------------- +Changes since 8.x-1.14: - by matio89: add RTL in admin.toolbar.css. - by romainj: Changes Devel menu item titles. @@ -208,9 +235,9 @@ Changes since 8.1.14: - by eme: fix issue #2658896 and refactor and fix chevron-right.svg. - by eme: fix Issue #2493037. -Admin Toolbar 8.1.14, 2016-02-08 --------------------------------- -Changes since 8.1.13: +Admin Toolbar 8.x-1.14, 2016-02-08 +---------------------------------- +Changes since 8.x-1.13: - by matio89: Correction differents bugs(problem white page when installing a new module). @@ -218,17 +245,17 @@ Changes since 8.1.13: - by matio89: resolving the dependecies problem(dependencies of others modules). - by matio89: Correction route of entities. -Admin Toolbar 8.1.13, 2016-02-02 --------------------------------- -Changes since 8.1.12: +Admin Toolbar 8.x-1.13, 2016-02-02 +---------------------------------- +Changes since 8.x-1.12: - by matio89: Admin toolbar is compatible with 8.0.2. - by matio89: Commmit dev version compatible with drupal 8.0.2. - by matio89: New version of admin toolbar compatible with 8.0.2. -Admin Toolbar 8.1.12, 2016-01-15 --------------------------------- -Changes since 8.1.11: +Admin Toolbar 8.x-1.12, 2016-01-15 +---------------------------------- +Changes since 8.x-1.11: - by matio89: Correction AdminToolbaToolsAlterTest. - #2493037 by mimran: Empty elements in #toolbar-bar. @@ -236,9 +263,9 @@ Changes since 8.1.11: - #2635154 by Lukas von Blarer: Too general CSS selectors. - by eme: Fix chevron right in local state. -Admin Toolbar 8.1.11, 2015-12-11 --------------------------------- -Changes since 8.1.10: +Admin Toolbar 8.x-1.11, 2015-12-11 +---------------------------------- +Changes since 8.x-1.10: - #2620430 by Vagelis, NarendraR: Typo. - #2632888 by JamesK: Add dependency on node module. @@ -255,46 +282,46 @@ Changes since 8.1.10: login link. - by matio89: Correction the login link on the site when in a logged-out state. -Admin Toolbar 8.1.10, 2015-08-07 --------------------------------- -Changes since 8.1.9: +Admin Toolbar 8.x-1.10, 2015-08-07 +---------------------------------- +Changes since 8.x-1.9: - #2546939 by jonhattan: Add dependency on toolbar module. -Admin Toolbar 8.1.9, 2015-07-22 -------------------------------- -Changes since 8.1.8: +Admin Toolbar 8.x-1.9, 2015-07-22 +--------------------------------- +Changes since 8.x-1.8: - #2537016 by matio89: Removing dependance for user & system. - #2533420 by Dave Reid, twistor: Chase HEAD changes in toolbar. - #2537016 by twistor: Rewrite admin_toolbar_tools.module - by matio89: Correction all bugs and add new security features. -Admin Toolbar 8.1.8, 2015-06-22 -------------------------------- -Changes since 8.1.7: +Admin Toolbar 8.x-1.8, 2015-06-22 +--------------------------------- +Changes since 8.x-1.7: - correction not found route. - #2493037 by bobrov1989: Empty elements in #toolbar-bar. - by fethi: icones. -Admin Toolbar 8.1.7, 2015-06-22 -------------------------------- -Changes since 8.1.6: +Admin Toolbar 8.x-1.7, 2015-06-22 +--------------------------------- +Changes since 8.x-1.6: - correction not found route. - #2493037 by bobrov1989: Empty elements in #toolbar-bar. - by fethi: icones. -Admin Toolbar 8.1.6, 2015-06-03 -------------------------------- -Changes since 8.1.5: +Admin Toolbar 8.x-1.6, 2015-06-03 +--------------------------------- +Changes since 8.x-1.5: - by matio89: correction admin_toolbar. -Admin Toolbar 8.1.5, 2015-06-03 -------------------------------- -Changes since 8.1.4: +Admin Toolbar 8.x-1.5, 2015-06-03 +--------------------------------- +Changes since 8.x-1.4: - by matio89: test if module update exist or no to display install and update module. @@ -302,36 +329,36 @@ Changes since 8.1.4: - by matio89: compatibility with Drupal 8 béta 11. - by fethi: Change the colors used to match the Seven styleguide. -Admin Toolbar 8.1.4, 2015-05-20 -------------------------------- -Changes since 8.1.3: +Admin Toolbar 8.x-1.4, 2015-05-20 +--------------------------------- +Changes since 8.x-1.3: - by matio89: correction add/node. -Admin Toolbar 8.1.3, 2015-05-19 -------------------------------- -Changes since 8.1.2: +Admin Toolbar 8.x-1.3, 2015-05-19 +--------------------------------- +Changes since 8.x-1.2: - by matio89: add new functionalities. - by matio89: add administration devel link. - by matio89: correction reload page. - by matio89: correction redirect to the same page. -Admin Toolbar 8.1.2, 2015-05-19 -------------------------------- -Changes since 8.1.1: +Admin Toolbar 8.x-1.2, 2015-05-19 +--------------------------------- +Changes since 8.x-1.1: - by matio89: add new functionalities. - by matio89: add administration devel link. - by matio89: correction reload page. - by matio89: correction redirect to the same page. -Admin Toolbar 8.1.1, 2015-05-19 -------------------------------- -Changes since 8.1.0: +Admin Toolbar 8.x-1.1, 2015-05-19 +--------------------------------- +Changes since 8.x-1.0: - by matio89: add new functionalities. -Admin Toolbar 8.1.0, 2015-05-07 -------------------------------- +Admin Toolbar 8.x-1.0, 2015-05-07 +--------------------------------- - Initial release. diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar.info.yml b/web/modules/contrib/admin_toolbar/admin_toolbar.info.yml index 900077c7b..a8b87bf02 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar.info.yml +++ b/web/modules/contrib/admin_toolbar/admin_toolbar.info.yml @@ -1,5 +1,5 @@ name: Admin Toolbar -description: Provides a drop-down menu interface to the core Drupal Toolbar. +description: Provides a drop-down menu interface to the site Toolbar. package: Administration type: module @@ -8,8 +8,8 @@ type: module dependencies: - drupal:toolbar -# Information added by Drupal.org packaging script on 2018-02-06 -version: '8.x-1.23' +# Information added by Drupal.org packaging script on 2018-05-28 +version: '8.x-1.24' core: '8.x' project: 'admin_toolbar' -datestamp: 1517936588 +datestamp: 1527522484 diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar.module b/web/modules/contrib/admin_toolbar/admin_toolbar.module index a9199e91a..c8ee104da 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar.module +++ b/web/modules/contrib/admin_toolbar/admin_toolbar.module @@ -27,14 +27,13 @@ function admin_toolbar_help($route_name, RouteMatchInterface $route_match) { $variables = [ ':toolbar' => Url::fromRoute('help.page', ['name' => 'toolbar'])->toString(), ':automated_cron' => (\Drupal::moduleHandler()->moduleExists('automated_cron')) ? Url::fromRoute('help.page', ['name' => 'automated_cron'])->toString() : '#', - ':admin_toolbar_documentation' => 'https://www.drupal.org/node/2713693', ]; $output = ''; $output .= '

' . t('About') . '

'; - $output .= '

' . t('The Admin Toolbar module enhances the Toolbar module by providing fast access to all the administrative links at the top of your site. Admin Toolbar remains a very "lightweight" module by closely integrating with all Toolbar functionality. It can be used in conjunction with all the sub or complimentary modules, listed on Admin Toolbar, for quick access to system commands such as Flush all caches, Run cron, Run Updates, etc... For more information, see the online documentation for the Admin Toolbar module.', $variables) . '

'; + $output .= '

' . t('The Admin Toolbar module enhances the Toolbar module by providing fast access to all the administrative links at the top of your site. Admin Toolbar remains a very "lightweight" module by closely integrating with all Toolbar functionality. It can be used in conjunction with all the sub modules included on Admin Toolbar, for quick access to system commands such as Flush all caches, Run cron, Run Updates, etc.', $variables) . '

'; $output .= '

' . t('Uses') . '

'; - $output .= '

' . t('The Admin Toolbar greatly improves the user experience for those who regularly interact with the Drupal Toolbar by providing fast, full access to all links in the Drupal Toolbar without having to click to get there.') . '

'; + $output .= '

' . t('The Admin Toolbar greatly improves the user experience for those who regularly interact with the site Toolbar by providing fast, full access to all links in the site Toolbar without having to click to get there.') . '

'; return $output; } diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/README.txt b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/README.txt index 60598b871..37670cf49 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/README.txt +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/README.txt @@ -3,7 +3,6 @@ CONTENTS OF THIS FILE * Introduction * Requirements - * Recommended modules * Installation * Configuration * Maintainers @@ -32,15 +31,6 @@ This module requires the following modules: * Admin Toolbar (https://www.drupal.org/project/admin_toolbar) -RECOMMENDED MODULES -------------------- - - * Admin Toolbar (https://www.drupal.org/project/admin_toolbar): - Improve the default Drupal Toolbar (the administration menu at the top of - your site) to transform it into a drop-down menu, providing a fast access to - all administration pages. - - INSTALLATION ------------ diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml index 355a40cac..46edfd8a4 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml @@ -8,8 +8,8 @@ type: module dependencies: - admin_toolbar:admin_toolbar -# Information added by Drupal.org packaging script on 2018-02-06 -version: '8.x-1.23' +# Information added by Drupal.org packaging script on 2018-05-28 +version: '8.x-1.24' core: '8.x' project: 'admin_toolbar' -datestamp: 1517936588 +datestamp: 1527522484 diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.module b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.module index 7b4476897..d9930e07b 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.module +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.module @@ -20,7 +20,7 @@ function admin_toolbar_links_access_filter_help($route_name, RouteMatchInterface case 'help.page.admin_toolbar_links_access_filter': $output = ''; $output .= '

' . t('About') . '

'; - $output .= '

' . t("The Admin Toolbar Links Access Filter module Provides a workaround for the common problem that users with Use the administration pages and help permission see menu links they don't have access permission for. Once the issue Hide empty admin categories be solved, this module will be deprecated.", ['@url' => Url::fromUri('https://www.drupal.org/node/296693')->toString()]) . '

'; + $output .= '

' . t('The Admin Toolbar Links Access Filter module provides a workaround for the common problem that users with Use the administration pages and help permission see menu links they done not have access permission for.') . '

'; return $output; } @@ -184,10 +184,17 @@ function admin_toolbar_links_access_filter_is_overview_page($route_name) { * assigned, FALSE otherwise. */ function admin_toolbar_links_access_filter_user_has_admin_role(AccountInterface $account) { - foreach ($account->getRoles() as $role_id) { - if (Role::load($role_id)->isAdmin()) { - return TRUE; + static $user_has_admin_role = []; + $uid = $account->id(); + if (!isset($user_has_admin_role[$uid])) { + $roles = Role::loadMultiple($account->getRoles()); + foreach ($roles as $role) { + if ($role->isAdmin()) { + $user_has_admin_role[$uid] = TRUE; + break; + } + $user_has_admin_role[$uid] = FALSE; } } - return FALSE; + return $user_has_admin_role[$uid]; } diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/composer.json b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/composer.json new file mode 100644 index 000000000..d75f3b0c6 --- /dev/null +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/composer.json @@ -0,0 +1,38 @@ +{ + "name": "drupal/admin_toolbar_links_access_filter", + "description": "Provides a workaround for the common problem that users with 'Use the administration pages and help' permission see menu links they don't have access permission for. Once the issue https://www.drupal.org/node/296693 be solved, this module will be deprecated.", + "type": "drupal-module", + "keywords": ["Drupal", "Toolbar"], + "homepage": "http://drupal.org/project/admin_toolbar", + "license": "GPL-2.0+", + "authors": [ + { + "name": "Wilfrid Roze (eme)", + "homepage": "https://www.drupal.org/u/eme", + "role": "Maintainer" + }, + { + "name": "Romain Jarraud (romainj)", + "homepage": "https://www.drupal.org/u/romainj", + "role": "Maintainer" + }, + { + "name": "Adrian Cid Almaguer (adriancid)", + "email": "adriancid@gmail.com", + "homepage": "https://www.drupal.org/u/adriancid", + "role": "Maintainer" + }, + { + "name": "Mohamed Anis Taktak (matio89)", + "homepage": "https://www.drupal.org/u/matio89", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/admin_toolbar", + "source": "http://cgit.drupalcode.org/admin_toolbar" + }, + "require": { + "drupal/admin_toolbar": "^1" + } +} diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/README.txt b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/README.txt index 4e40e8730..263cde93c 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/README.txt +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/README.txt @@ -33,15 +33,6 @@ This module requires the following modules: * Admin Toolbar (https://www.drupal.org/project/admin_toolbar) -RECOMMENDED MODULES -------------------- - - * Admin Toolbar (https://www.drupal.org/project/admin_toolbar): - Improve the default Drupal Toolbar (the administration menu at the top of - your site) to transform it into a drop-down menu, providing a fast access to - all administration pages. - - INSTALLATION ------------ diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml index 8166fe66e..a42c0ccbe 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml @@ -7,9 +7,10 @@ type: module dependencies: - admin_toolbar:admin_toolbar + - drupal:system (>=8.3) -# Information added by Drupal.org packaging script on 2018-02-06 -version: '8.x-1.23' +# Information added by Drupal.org packaging script on 2018-05-28 +version: '8.x-1.24' core: '8.x' project: 'admin_toolbar' -datestamp: 1517936588 +datestamp: 1527522484 diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml index cb8cc2587..fc7df7f1c 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml @@ -63,6 +63,12 @@ admin_toolbar_tools.flush_menu: parent: admin_toolbar_tools.flush menu_name: admin +admin_toolbar_tools.flush_twig: + title: 'Flush twig cache' + route_name: admin_toolbar_tools.flush_twig + parent: admin_toolbar_tools.flush + menu_name: admin + admin_toolbar_tools.flush_rendercache: title: 'Flush render cache' route_name: admin_toolbar_tools.flush_rendercache diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module index d08141db4..1db436a1d 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module @@ -5,7 +5,6 @@ * Provides extra menu links for the core drupal toolbar. */ -use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; @@ -34,21 +33,12 @@ function admin_toolbar_tools_toolbar() { function admin_toolbar_tools_help($route_name, RouteMatchInterface $route_match) { switch ($route_name) { case 'help.page.admin_toolbar_tools': - $url = Url::fromUri('https://www.drupal.org/project/admin_toolbar'); - $external_link = Link::fromTextAndUrl(t('Admin Toolbar'), $url); - $external_link2 = Link::fromTextAndUrl(t('Toolbar'), $url); - $url = Url::fromUri('https://www.drupal.org/node/2713693'); - $external_link3 = Link::fromTextAndUrl(t('online documentation for Admin Toolbar'), $url); $output = ''; $output .= '

'; - $output .= t('The Admin Toolbar Extra Tools module comes packaged with the :link module and adds functionality to it. The additional functionality is accessed thru extra links on the main administration :link2. Some links to Admin Toolbar Extra Tools administration pages are located at the bottom of this page. For more information, see the :link3', [ - ':link' => $external_link, - ':link2' => $external_link2, - ':link3' => $external_link3, - ]); + $output .= t('The Admin Toolbar Extra Tools module comes packaged with the Admin Toolbar module and adds functionality to it. The additional functionality is accessed thru extra links on the main administration Toolbar. Some links to Admin Toolbar Extra Tools administration pages are located at the bottom of this page.', [':admin-toolbar' => Url::fromRoute('help.page', ['name' => 'admin_toolbar'])->toString()]); $output .= '

'; $output .= '

' . t('Uses') . '

'; - $output .= '

' . t('To use Admin Toolbar Extra Tools just install it like any other module. There is no other configuration required. The Admin Toolbar functionality can be further extended by installing complimentary modules. See :link for a complete listing of these complimentary modules.', [':link' => $external_link]) . '

'; + $output .= '

' . t('To use Admin Toolbar Extra Tools just install it like any other module. There is no other configuration required.') . '

'; return $output; } @@ -85,6 +75,15 @@ function admin_toolbar_tools_menu_links_discovered_alter(&$links) { 'menu_name' => 'admin', 'parent' => 'admin_toolbar_tools.flush', ]; + // Adding a menu link to Files. + if ($moduleHandler->moduleExists('file') && in_array('view.files.page_1', $routes)) { + $links['admin_toolbar_tools.view.files'] = [ + 'title' => t('Files'), + 'route_name' => 'view.files.page_1', + 'menu_name' => 'admin', + 'parent' => 'system.admin_content', + ]; + } } // Adds common links to entities. @@ -568,7 +567,7 @@ function admin_toolbar_tools_menu_links_discovered_alter(&$links) { ]; // Add node links for each media type. foreach (\Drupal::entityTypeManager()->getStorage('media_type')->loadMultiple() as $type) { - $links['node.add.' . $type->id()] = [ + $links['media.add.' . $type->id()] = [ 'title' => t($type->label()), 'route_name' => 'entity.media.add_form', 'parent' => 'admin_toolbar_tools.add_media', diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml index 277808c8e..dc081c860 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml @@ -61,6 +61,15 @@ admin_toolbar_tools.flush_views: _permission: 'administer site configuration' _csrf_token: 'TRUE' +admin_toolbar_tools.flush_twig: + path: '/admin/flush/twig' + defaults: + _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::flushTwig' + _title: 'Twig' + requirements: + _permission: 'administer site configuration' + _csrf_token: 'TRUE' + admin_toolbar.run.cron: path: '/run-cron' defaults: diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/composer.json b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/composer.json new file mode 100644 index 000000000..49414fc2b --- /dev/null +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/composer.json @@ -0,0 +1,39 @@ +{ + "name": "drupal/admin_toolbar_tools", + "description": "Adds menu links to the Admin Toolbar.", + "type": "drupal-module", + "keywords": ["Drupal", "Toolbar"], + "homepage": "http://drupal.org/project/admin_toolbar", + "license": "GPL-2.0+", + "authors": [ + { + "name": "Wilfrid Roze (eme)", + "homepage": "https://www.drupal.org/u/eme", + "role": "Maintainer" + }, + { + "name": "Romain Jarraud (romainj)", + "homepage": "https://www.drupal.org/u/romainj", + "role": "Maintainer" + }, + { + "name": "Adrian Cid Almaguer (adriancid)", + "email": "adriancid@gmail.com", + "homepage": "https://www.drupal.org/u/adriancid", + "role": "Maintainer" + }, + { + "name": "Mohamed Anis Taktak (matio89)", + "homepage": "https://www.drupal.org/u/matio89", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/admin_toolbar", + "source": "http://cgit.drupalcode.org/admin_toolbar" + }, + "require": { + "drupal/admin_toolbar": "^1", + "drupal/core": "~8.3" + } +} diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php index b1f275a79..8b74436c8 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php @@ -2,18 +2,19 @@ namespace Drupal\admin_toolbar_tools\Controller; -use Drupal\Component\Datetime\Time; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\CronInterface; -use Drupal\Core\Menu\ContextualLinkManager; -use Drupal\Core\Menu\LocalActionManager; -use Drupal\Core\Menu\LocalTaskManager; -use Drupal\Core\Menu\MenuLinkManager; +use Drupal\Core\Menu\ContextualLinkManagerInterface; +use Drupal\Core\Menu\LocalActionManagerInterface; +use Drupal\Core\Menu\LocalTaskManagerInterface; +use Drupal\Core\Menu\MenuLinkManagerInterface; use Drupal\Core\Plugin\CachedDiscoveryClearerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RequestStack; +use Drupal\Core\PhpStorage\PhpStorageFactory; /** * Class ToolbarController. @@ -32,28 +33,28 @@ class ToolbarController extends ControllerBase { /** * A menu link manager instance. * - * @var \Drupal\Core\Menu\MenuLinkManager + * @var \Drupal\Core\Menu\MenuLinkManagerInterface */ protected $menuLinkManager; /** * A context link manager instance. * - * @var \Drupal\Core\Menu\ContextualLinkManager + * @var \Drupal\Core\Menu\ContextualLinkManagerInterface */ protected $contextualLinkManager; /** * A local task manager instance. * - * @var \Drupal\Core\Menu\LocalTaskManager + * @var \Drupal\Core\Menu\LocalTaskManagerInterface */ protected $localTaskLinkManager; /** * A local action manager instance. * - * @var \Drupal\Core\Menu\LocalActionManager + * @var \Drupal\Core\Menu\LocalActionManagerInterface */ protected $localActionLinkManager; @@ -67,7 +68,7 @@ class ToolbarController extends ControllerBase { /** * A date time instance. * - * @var \Drupal\Component\Datetime\Time + * @var \Drupal\Component\Datetime\TimeInterface */ protected $time; @@ -86,15 +87,34 @@ class ToolbarController extends ControllerBase { protected $pluginCacheClearer; /** - * {@inheritdoc} + * Constructs a ToolbarController object. + * + * @param \Drupal\Core\CronInterface $cron + * A cron instance. + * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager + * A menu link manager instance. + * @param \Drupal\Core\Menu\ContextualLinkManagerInterface $contextualLinkManager + * A context link manager instance. + * @param \Drupal\Core\Menu\LocalTaskManagerInterface $localTaskLinkManager + * A local task manager instance. + * @param \Drupal\Core\Menu\LocalActionManagerInterface $localActionLinkManager + * A local action manager instance. + * @param \Drupal\Core\Cache\CacheBackendInterface $cacheRender + * A cache backend interface instance. + * @param \Drupal\Component\Datetime\TimeInterface $time + * A date time instance. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * A request stack symfony instance. + * @param \Drupal\Core\Plugin\CachedDiscoveryClearerInterface $plugin_cache_clearer + * A plugin cache clear instance. */ public function __construct(CronInterface $cron, - MenuLinkManager $menuLinkManager, - ContextualLinkManager $contextualLinkManager, - LocalTaskManager $localTaskLinkManager, - LocalActionManager $localActionLinkManager, + MenuLinkManagerInterface $menuLinkManager, + ContextualLinkManagerInterface $contextualLinkManager, + LocalTaskManagerInterface $localTaskLinkManager, + LocalActionManagerInterface $localActionLinkManager, CacheBackendInterface $cacheRender, - Time $time, + TimeInterface $time, RequestStack $request_stack, CachedDiscoveryClearerInterface $plugin_cache_clearer) { $this->cron = $cron; @@ -197,6 +217,17 @@ class ToolbarController extends ControllerBase { return new RedirectResponse($this->reloadPage()); } + /** + * Clears the twig cache. + */ + public function flushTwig() { + // @todo Update once Drupal 8.6 will be released. + // @see https://www.drupal.org/node/2908461 + PhpStorageFactory::get('twig')->deleteAll(); + drupal_set_message($this->t('Twig cache cleared.')); + return new RedirectResponse($this->reloadPage()); + } + /** * Run the cron. */ diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Tests/AdminToolbarToolsAlterTest.php b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php similarity index 78% rename from web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Tests/AdminToolbarToolsAlterTest.php rename to web/modules/contrib/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php index 73035efb1..f61329c63 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Tests/AdminToolbarToolsAlterTest.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php @@ -1,22 +1,26 @@ theme_handler->listInfo(); $result = []; - foreach ($modules + $themes as $name => $data) { + foreach ($modules as $name => $data) { $result[$name] = $this->module_handler->getName($name); } + + foreach ($themes as $name => $data) { + $result[$name] = $this->theme_handler->getName($name); + } + return $result; } diff --git a/web/modules/contrib/blazy/blazy.info.yml b/web/modules/contrib/blazy/blazy.info.yml index c123745f3..20b936d10 100644 --- a/web/modules/contrib/blazy/blazy.info.yml +++ b/web/modules/contrib/blazy/blazy.info.yml @@ -7,8 +7,8 @@ package: Blazy dependencies: - drupal:image -# Information added by Drupal.org packaging script on 2017-05-25 -version: '8.x-1.0-rc2' +# Information added by Drupal.org packaging script on 2018-09-27 +version: '8.x-1.0-rc3' core: '8.x' project: 'blazy' -datestamp: 1495745286 +datestamp: 1538045584 diff --git a/web/modules/contrib/blazy/blazy.module b/web/modules/contrib/blazy/blazy.module index 252c38945..59b1ff753 100644 --- a/web/modules/contrib/blazy/blazy.module +++ b/web/modules/contrib/blazy/blazy.module @@ -80,8 +80,7 @@ function blazy_field_formatter_info_alter(array &$info) { 'quickedit' => ['editor' => 'disabled'], 'provider' => 'blazy', ]; - - if (function_exists('video_embed_media_media_bundle_insert')) { + if (\Drupal::moduleHandler()->moduleExists('video_embed_media')) { $info['blazy_file'] = $common + [ 'id' => 'blazy_file', 'label' => t('Blazy Image with Media'), diff --git a/web/modules/contrib/blazy/blazy.views.inc b/web/modules/contrib/blazy/blazy.views.inc index c1fbec3c8..8d1246a42 100644 --- a/web/modules/contrib/blazy/blazy.views.inc +++ b/web/modules/contrib/blazy/blazy.views.inc @@ -17,8 +17,7 @@ function blazy_views_data_alter(&$data) { 'click sortable' => FALSE, ], ]; - - if (function_exists('video_embed_media_media_bundle_insert')) { + if (\Drupal::moduleHandler()->moduleExists('video_embed_media')) { $data['media_field_data']['blazy_media'] = [ 'title' => 'Blazy', 'help' => t('Displays a preview of a Media using Blazy, if applicable.'), diff --git a/web/modules/contrib/blazy/blazy_ui/blazy_ui.info.yml b/web/modules/contrib/blazy/blazy_ui/blazy_ui.info.yml index b5a4dde54..63575981e 100644 --- a/web/modules/contrib/blazy/blazy_ui/blazy_ui.info.yml +++ b/web/modules/contrib/blazy/blazy_ui/blazy_ui.info.yml @@ -8,8 +8,8 @@ configure: blazy.settings dependencies: - blazy:blazy -# Information added by Drupal.org packaging script on 2017-05-25 -version: '8.x-1.0-rc2' +# Information added by Drupal.org packaging script on 2018-09-27 +version: '8.x-1.0-rc3' core: '8.x' project: 'blazy' -datestamp: 1495745286 +datestamp: 1538045584 diff --git a/web/modules/contrib/blazy/src/BlazyLightbox.php b/web/modules/contrib/blazy/src/BlazyLightbox.php index bd943996c..6d6470d33 100644 --- a/web/modules/contrib/blazy/src/BlazyLightbox.php +++ b/web/modules/contrib/blazy/src/BlazyLightbox.php @@ -36,6 +36,9 @@ class BlazyLightbox { $settings['box_width'] = isset($item->width) ? $item->width : NULL; $settings['box_height'] = isset($item->height) ? $item->height : NULL; + $settings['box_width'] = isset($settings['box_width']) ? $settings['box_width'] : $settings['width']; + $settings['box_height'] = isset($settings['box_height']) ? $settings['box_height'] : $settings['height']; + $dimensions = ['width' => $settings['box_width'], 'height' => $settings['box_height']]; if (!empty($settings['box_style'])) { $box_style = ImageStyle::load($settings['box_style']); @@ -77,7 +80,7 @@ class BlazyLightbox { $settings['box_url'] = $box_media_style->buildUrl($uri); // Allows custom work to override this without image style. - if (empty($settings['_box_width'])) { + if (empty($settings['box_width'])) { $settings['box_width'] = $dimensions['width']; $settings['box_height'] = $dimensions['height']; } diff --git a/web/modules/contrib/blazy/src/Dejavu/BlazyVideoTrait.php b/web/modules/contrib/blazy/src/Dejavu/BlazyVideoTrait.php index 1a0268131..443e64678 100644 --- a/web/modules/contrib/blazy/src/Dejavu/BlazyVideoTrait.php +++ b/web/modules/contrib/blazy/src/Dejavu/BlazyVideoTrait.php @@ -171,7 +171,7 @@ trait BlazyVideoTrait { $bundle = $media->bundle(); $fields = $media->getFields(); - $config = $media->getType()->getConfiguration(); + $config = method_exists($media, 'getSource') ? $media->getSource()->getConfiguration() : $media->getType()->getConfiguration(); $source = isset($config['source_url_field']) ? $config['source_url_field'] : ''; $source_field[$bundle] = isset($config['source_field']) ? $config['source_field'] : $source; diff --git a/web/modules/contrib/blazy/tests/modules/blazy_test/blazy_test.info.yml b/web/modules/contrib/blazy/tests/modules/blazy_test/blazy_test.info.yml index 5a4639157..ffd481b37 100644 --- a/web/modules/contrib/blazy/tests/modules/blazy_test/blazy_test.info.yml +++ b/web/modules/contrib/blazy/tests/modules/blazy_test/blazy_test.info.yml @@ -9,8 +9,8 @@ dependencies: - drupal:responsive_image - drupal:views -# Information added by Drupal.org packaging script on 2017-05-25 -version: '8.x-1.0-rc2' +# Information added by Drupal.org packaging script on 2018-09-27 +version: '8.x-1.0-rc3' core: '8.x' project: 'blazy' -datestamp: 1495745286 +datestamp: 1538045584 diff --git a/web/modules/contrib/block_class/README.txt b/web/modules/contrib/block_class/README.txt index 8bf7508af..5fe887806 100644 --- a/web/modules/contrib/block_class/README.txt +++ b/web/modules/contrib/block_class/README.txt @@ -1,12 +1,57 @@ -BLOCK CLASS ------------ -Project URL: http://drupal.org/project/block_class +CONTENTS OF THIS FILE +--------------------- + + * Introduction + * Requirements + * Installation + * Configuration + * Maintainers + + +INTRODUCTION +------------ + +Block Class allows users to add classes to any block through the block's +configuration interface. + + +REQUIREMENTS +------------ + +The Menu Block just requires the Block project: + + * Block (https://drupal.org/project/block) -===== -Installation ------ -1. Enable the module -2. Visit the block configuration page at Administration -> Structure -> Block Layout -and click on the Configure link for a block. -3. Enter the classes in the field provided and save the block. \ No newline at end of file +INSTALLATION +------------ + +Install as you would normally install a contributed Drupal module. See: +https://drupal.org/documentation/install/modules-themes/modules-8 for further +information. + + +CONFIGURATION +------------- + + * Visit the block configuration page at Administration » Structure » Block + Layout and click on the Configure link for a block. + + * Enter the classes in the field provided and save the block. + + +MAINTAINERS +----------- + +Current maintainers: + * Todd Nienkerk - https://www.drupal.org/user/92096 + * Renato Gonçalves (RenatoG) - https://www.drupal.org/user/3326031 + * Aaron Stanush - https://www.drupal.org/user/89718 + * David Suissa (DYdave) - https://www.drupal.org/user/467284 + * Four Kitchens - https://www.drupal.org/user/358502 + * berenddeboer - https://www.drupal.org/user/143552 + * elliotttf - https://www.drupal.org/user/61601 + * Michal Minecki (mirzu) - https://www.drupal.org/user/7710 + * Patrick Coffey (patrickcoffeyo) - https://www.drupal.org/user/2837945 + * Patrick Coffey (pcoffey) - https://www.drupal.org/user/1595818 + * Taylor Smith (tsmith512) - https://www.drupal.org/user/2031446 diff --git a/web/modules/contrib/block_class/block_class.info.yml b/web/modules/contrib/block_class/block_class.info.yml index 6a831f84f..75701e41a 100644 --- a/web/modules/contrib/block_class/block_class.info.yml +++ b/web/modules/contrib/block_class/block_class.info.yml @@ -4,10 +4,10 @@ description: 'Allows assigning classes to Blocks' package: Other # core: 8.x dependencies: - - block + - drupal:block -# Information added by Drupal.org packaging script on 2016-10-04 -version: '8.x-1.0-alpha1' +# Information added by Drupal.org packaging script on 2018-07-13 +version: '8.x-1.0' core: '8.x' project: 'block_class' -datestamp: 1475623440 +datestamp: 1531440825 diff --git a/web/modules/contrib/block_class/block_class.module b/web/modules/contrib/block_class/block_class.module index c43fde2fb..b3afb939f 100644 --- a/web/modules/contrib/block_class/block_class.module +++ b/web/modules/contrib/block_class/block_class.module @@ -2,15 +2,28 @@ /** * @file - * Module for adding classes to blocks. + * Adding classes to blocks. */ +use Drupal\Core\Form\FormStateInterface; use Drupal\block\Entity\Block; +use Drupal\Component\Utility\Html; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\block\BlockInterface; + +/** + * Implements hook_ENTITY_TYPE_presave(). + */ +function block_class_block_presave(BlockInterface $entity) { + if (empty($entity->getThirdPartySetting('block_class', 'classes'))) { + $entity->unsetThirdPartySetting('block_class', 'classes'); + } +} /** * Implements hook_form_FORM_ID_alter(). */ -function block_class_form_block_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { +function block_class_form_block_form_alter(&$form, FormStateInterface $form_state, $form_id) { if (\Drupal::currentUser()->hasPermission('administer block classes')) { /** @var \Drupal\block\BlockInterface $block */ @@ -18,12 +31,12 @@ function block_class_form_block_form_alter(&$form, \Drupal\Core\Form\FormStateIn // This will automatically be saved in the third party settings. $form['third_party_settings']['#tree'] = TRUE; - $form['third_party_settings']['block_class']['classes'] = array( + $form['third_party_settings']['block_class']['classes'] = [ '#type' => 'textfield', '#title' => t('CSS class(es)'), '#description' => t('Customize the styling of this block by adding CSS classes. Separate multiple classes by spaces.'), '#default_value' => $block->getThirdPartySetting('block_class', 'classes'), - ); + ]; } } @@ -35,8 +48,36 @@ function block_class_preprocess_block(&$variables) { // Blocks coming from page manager widget does not have id. if (!empty($variables['elements']['#id'])) { $block = Block::load($variables['elements']['#id']); - if ($classes = $block->getThirdPartySetting('block_class', 'classes')) { - $variables['attributes']['class'][] = $classes; + if ($block && $classes = $block->getThirdPartySetting('block_class', 'classes')) { + $classes_array = explode(' ', $classes); + foreach ($classes_array as $class) { + $variables['attributes']['class'][] = Html::cleanCssIdentifier($class, []); + } } } } + +/** + * Implements hook_help(). + */ +function block_class_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + // Main module help for the forms_to_email module. + case 'help.page.block_class': + $output = ''; + $output .= '

' . t('About') . '

'; + $output .= '

' . t("Block Class allows users to add classes to any block through the block's configuration interface. Hooray for more powerful block theming!") . '

'; + + $output .= '

' . t('Installation note') . '

'; + $output .= '
'; + $output .= '
' . t('Enable the module on extend menu.', [':extend_link' => \Drupal::url('system.modules_list')]) . '
'; + $output .= '
'; + + $output .= '

' . t('Usage') . '

'; + $output .= '
'; + $output .= '
' . t("To add a class to a block, simply visit that block's configuration page at Administration > Structure > Block Layout and click on Configure of the desired block.") . '
'; + $output .= '
'; + + return $output; + } +} diff --git a/web/modules/contrib/block_class/composer.json b/web/modules/contrib/block_class/composer.json new file mode 100644 index 000000000..474e3065c --- /dev/null +++ b/web/modules/contrib/block_class/composer.json @@ -0,0 +1,65 @@ +{ + "name": "drupal/block_class", + "description": "Allows assigning classes to Blocks.", + "type": "drupal-module", + "homepage": "https://www.drupal.org/project/block_class", + "authors": [ + { + "name": "Todd Nienkerk", + "homepage": "https://www.drupal.org/u/todd-nienkerk", + "role": "Maintainer" + }, + { + "name": "Renato Gonçalves (RenatoG)", + "email": "renatog@ciandt.com", + "homepage": "https://www.drupal.org/u/RenatoG", + "role": "Maintainer" + }, + { + "name": "Aaron Stanush", + "homepage": "https://www.drupal.org/u/aaron-stanush", + "role": "Maintainer" + }, + { + "name": "David Suissa (DYdave)", + "homepage": "https://www.drupal.org/u/DYdave", + "role": "Maintainer" + }, + { + "name": "Four Kitchens", + "homepage": "https://www.drupal.org/user/358502", + "role": "Maintainer" + }, + { + "name": "berenddeboer", + "homepage": "https://www.drupal.org/u/berenddeboer", + "role": "Maintainer" + }, + { + "name": "elliotttf", + "homepage": "https://www.drupal.org/u/elliotttf", + "role": "Maintainer" + }, + { + "name": "Michal Minecki (mirzu)", + "homepage": "https://www.drupal.org/u/mirzu", + "role": "Maintainer" + }, + { + "name": "Patrick Coffey (pcoffey)", + "homepage": "https://www.drupal.org/u/pcoffey", + "role": "Maintainer" + }, + { + "name": "Taylor Smith (tsmith512)", + "homepage": "https://www.drupal.org/u/tsmith512", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/block_class", + "irc": "irc://irc.freenode.org/drupal-contribute", + "source": "https://cgit.drupalcode.org/block_class" + }, + "license": "GPL-2.0+" +} \ No newline at end of file diff --git a/web/modules/contrib/block_class/src/Tests/BlockClassTest.php b/web/modules/contrib/block_class/src/Tests/BlockClassTest.php index 071d6346a..adf0a213a 100644 --- a/web/modules/contrib/block_class/src/Tests/BlockClassTest.php +++ b/web/modules/contrib/block_class/src/Tests/BlockClassTest.php @@ -1,8 +1,4 @@ drupalCreateUser(array( + $admin_user = $this->drupalCreateUser([ 'administer block classes', 'administer blocks', - )); + ]); $this->drupalLogin($admin_user); // Add a content block with custom CSS class. $this->drupalGet('admin/structure/block/add/system_main_block/classy', ['query' => ['region' => 'content']]); - $edit = array( + $edit = [ 'region' => 'content', - 'third_party_settings[block_class][classes]' => 'TestClass_content' - ); - $this->drupalPostForm(NULL, $edit, t('Save block')); + 'third_party_settings[block_class][classes]' => 'TestClass_content', + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Save block')); // Add a user account menu with a custom CSS class. $this->drupalGet('admin/structure/block/add/system_menu_block:account/classy', ['query' => ['region' => 'content']]); - $edit = array( + $edit = [ 'region' => 'secondary_menu', - 'third_party_settings[block_class][classes]' => 'TestClass_menu' - ); - $this->drupalPostForm(NULL, $edit, t('Save block')); + 'third_party_settings[block_class][classes]' => 'TestClass_menu', + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Save block')); // Go to the front page of the user. $this->drupalGet(''); diff --git a/web/modules/contrib/diff/config/optional/core.entity_view_mode.node.diff.yml b/web/modules/contrib/diff/config/optional/core.entity_view_mode.node.diff.yml index eb366aa31..0201aafe4 100644 --- a/web/modules/contrib/diff/config/optional/core.entity_view_mode.node.diff.yml +++ b/web/modules/contrib/diff/config/optional/core.entity_view_mode.node.diff.yml @@ -1,3 +1,4 @@ +langcode: en id: node.diff label: 'Revision comparison' status: false diff --git a/web/modules/contrib/diff/diff.info.yml b/web/modules/contrib/diff/diff.info.yml index f28f2c924..310355b13 100644 --- a/web/modules/contrib/diff/diff.info.yml +++ b/web/modules/contrib/diff/diff.info.yml @@ -4,8 +4,8 @@ description: Shows changes between content revisions. # core: 8.x configure: diff.general_settings -# Information added by Drupal.org packaging script on 2017-01-16 -version: '8.x-1.0-rc1' +# Information added by Drupal.org packaging script on 2018-06-28 +version: '8.x-1.0-rc2' core: '8.x' project: 'diff' -datestamp: 1484566687 +datestamp: 1530178427 diff --git a/web/modules/contrib/diff/diff.services.yml b/web/modules/contrib/diff/diff.services.yml index b670e5f70..ab533fbb0 100755 --- a/web/modules/contrib/diff/diff.services.yml +++ b/web/modules/contrib/diff/diff.services.yml @@ -25,6 +25,8 @@ services: diff.entity_comparison: class: Drupal\diff\DiffEntityComparison arguments: ['@config.factory', '@diff.diff.formatter','@plugin.manager.field.field_type', '@diff.entity_parser', '@plugin.manager.diff.builder'] + calls: + - [setModerationInformation, ['@?content_moderation.moderation_information']] diff.html_diff: class: HtmlDiffAdvanced diff --git a/web/modules/contrib/diff/src/Controller/PluginRevisionController.php b/web/modules/contrib/diff/src/Controller/PluginRevisionController.php index 7b6a53d9c..6205ae154 100644 --- a/web/modules/contrib/diff/src/Controller/PluginRevisionController.php +++ b/web/modules/contrib/diff/src/Controller/PluginRevisionController.php @@ -90,6 +90,7 @@ class PluginRevisionController extends ControllerBase { $result = $storage->getQuery() ->allRevisions() ->condition($storage->getEntityType()->getKey('id'), $entity_id) + ->accessCheck(FALSE) ->execute(); $result_array = array_keys($result); sort($result_array); @@ -166,7 +167,7 @@ class PluginRevisionController extends ControllerBase { if ($plugin = $this->diffLayoutManager->createInstance($filter)) { $build = array_merge_recursive($build, $plugin->build($left_revision, $right_revision, $entity)); $build['diff']['#prefix'] = '
'; - $build['diff']['#suffix'] = '
'; + $build['diff']['#suffix'] = '
'; $build['diff']['#attributes']['class'][] = 'diff-responsive-table'; } } diff --git a/web/modules/contrib/diff/src/DiffEntityComparison.php b/web/modules/contrib/diff/src/DiffEntityComparison.php index 65e15d008..401740272 100644 --- a/web/modules/contrib/diff/src/DiffEntityComparison.php +++ b/web/modules/contrib/diff/src/DiffEntityComparison.php @@ -2,6 +2,7 @@ namespace Drupal\diff; +use Drupal\content_moderation\ModerationInformationInterface; use Drupal\Core\Config\ConfigFactory; use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Entity\ContentEntityInterface; @@ -56,6 +57,13 @@ class DiffEntityComparison { */ protected $diffBuilderManager; + /** + * The content moderation service, if available. + * + * @var \Drupal\content_moderation\ModerationInformationInterface + */ + protected $moderationInformation; + /** * Constructs a DiffEntityComparison object. * @@ -257,48 +265,18 @@ class DiffEntityComparison { * The revision log message. */ public function getRevisionDescription(ContentEntityInterface $revision, ContentEntityInterface $previous_revision = NULL) { - $summary_elements = []; $revision_summary = ''; // Check if the revision has a revision log message. if ($revision instanceof RevisionLogInterface) { $revision_summary = Xss::filter($revision->getRevisionLogMessage()); } - // Auto generate the revision log. - if ($revision_summary == '') { - // If there is a previous revision, load values of both revisions, loop - // over the current revision fields. - if ($previous_revision) { - $left_values = $this->summary($previous_revision); - $right_values = $this->summary($revision); - foreach ($right_values as $key => $value) { - // Unset left values after comparing. Add right value label to the - // summary if it is changed or new. - if (isset($left_values[$key])) { - if ($value['value'] != $left_values[$key]['value']) { - $summary_elements[] = $value['label']; - } - unset($left_values[$key]); - } - else { - $summary_elements[] = $value['label']; - } - } - // Add the remaining left values if not present in the right entity. - foreach ($left_values as $key => $value) { - if (!isset($right_values[$key])) { - $summary_elements[] = $value['label']; - } - } - if (count($summary_elements) > 0) { - $revision_summary = 'Changes on: ' . implode(', ', $summary_elements); - } - else { - $revision_summary = 'No changes.'; - } - } - else { - $revision_summary = 'Initial revision.'; - } + + // @todo Autogenerate summary again. + // @see https://www.drupal.org/project/diff/issues/2880936 + + // Add workflow/content moderation state information. + if ($state = $this->getModerationState($revision)) { + $revision_summary .= " ($state)"; } return $revision_summary; @@ -351,4 +329,34 @@ class DiffEntityComparison { return $result; } + /** + * Gets the revision's content moderation state, if available. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity revision. + * + * @return string|bool + * Returns the label of the moderation state, if available, otherwise FALSE. + */ + protected function getModerationState(ContentEntityInterface $entity) { + if ($this->moderationInformation && $this->moderationInformation->isModeratedEntity($entity)) { + if ($state = $entity->moderation_state->value) { + $workflow = $this->moderationInformation->getWorkflowForEntity($entity); + return $workflow->getTypePlugin()->getState($state)->label(); + } + } + + return FALSE; + } + + /** + * Sets the content moderation service if available. + * + * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information + * The moderation information service. + */ + public function setModerationInformation(ModerationInformationInterface $moderation_information) { + $this->moderationInformation = $moderation_information; + } + } diff --git a/web/modules/contrib/diff/src/DiffLayoutBase.php b/web/modules/contrib/diff/src/DiffLayoutBase.php index 85c423a05..dcb20f039 100644 --- a/web/modules/contrib/diff/src/DiffLayoutBase.php +++ b/web/modules/contrib/diff/src/DiffLayoutBase.php @@ -106,14 +106,9 @@ abstract class DiffLayoutBase extends PluginBase implements DiffLayoutInterface, * Header link for a revision in the table. */ protected function buildRevisionLink(ContentEntityInterface $revision) { - $entity_type_id = $revision->getEntityTypeId(); if ($revision instanceof RevisionLogInterface) { $revision_date = $this->date->format($revision->getRevisionCreationTime(), 'short'); - $route_name = $entity_type_id != 'node' ? "entity.$entity_type_id.revisions_diff" : 'entity.node.revision'; - $revision_link = Link::fromTextAndUrl($revision_date, Url::fromRoute($route_name, [ - $entity_type_id => $revision->id(), - $entity_type_id . '_revision' => $revision->getRevisionId(), - ]))->toString(); + $revision_link = Link::fromTextAndUrl($revision_date, $revision->toUrl('revision'))->toString(); } else { $revision_link = Link::fromTextAndUrl($revision->label(), $revision->toUrl('revision')) @@ -151,7 +146,7 @@ abstract class DiffLayoutBase extends PluginBase implements DiffLayoutInterface, '#wrapper_attributes' => ['class' => 'diff-revision'], 'items' => [ '#prefix' => '
', - '#suffix' => '
', + '#suffix' => '', 'right_revision' => $right_revision, 'left_revision' => $left_revision, ], @@ -170,19 +165,14 @@ abstract class DiffLayoutBase extends PluginBase implements DiffLayoutInterface, * Revision data about author, creation date and log. */ protected function buildRevisionData(ContentEntityInterface $revision) { - $entity_type_id = $revision->getEntityTypeId(); if ($revision instanceof RevisionLogInterface) { $revision_log = Xss::filter($revision->getRevisionLogMessage()); $user_id = $revision->getRevisionUserId(); - $route_name = $entity_type_id != 'node' ? "entity.$entity_type_id.revisions_diff" : 'entity.node.revision'; $revision_link['date'] = [ '#type' => 'link', '#title' => $this->date->format($revision->getRevisionCreationTime(), 'short'), - '#url' => Url::fromRoute($route_name, [ - $entity_type_id => $revision->id(), - $entity_type_id . '_revision' => $revision->getRevisionId(), - ]), + '#url' => $revision->toUrl('revision'), '#prefix' => '
', '#suffix' => '
', ]; diff --git a/web/modules/contrib/diff/src/Form/RevisionOverviewForm.php b/web/modules/contrib/diff/src/Form/RevisionOverviewForm.php index fcb6d50ed..831619ac0 100755 --- a/web/modules/contrib/diff/src/Form/RevisionOverviewForm.php +++ b/web/modules/contrib/diff/src/Form/RevisionOverviewForm.php @@ -3,7 +3,6 @@ namespace Drupal\diff\Form; use Drupal\Component\Utility\Xss; -use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Language\LanguageManagerInterface; @@ -79,13 +78,6 @@ class RevisionOverviewForm extends FormBase { */ protected $entityComparison; - /** - * The entity query factory service. - * - * @var \Drupal\Core\Entity\Query\QueryFactory - */ - protected $entityQuery; - /** * Constructs a RevisionOverviewForm object. * @@ -103,10 +95,8 @@ class RevisionOverviewForm extends FormBase { * The diff layout service. * @param \Drupal\diff\DiffEntityComparison $entity_comparison * The diff entity comparison service. - * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query - * The entity query factory. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, DateFormatter $date, RendererInterface $renderer, LanguageManagerInterface $language_manager, DiffLayoutManager $diff_layout_manager, DiffEntityComparison $entity_comparison, QueryFactory $entity_query) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, DateFormatter $date, RendererInterface $renderer, LanguageManagerInterface $language_manager, DiffLayoutManager $diff_layout_manager, DiffEntityComparison $entity_comparison) { $this->entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; $this->date = $date; @@ -115,7 +105,6 @@ class RevisionOverviewForm extends FormBase { $this->config = $this->config('diff.settings'); $this->diffLayoutManager = $diff_layout_manager; $this->entityComparison = $entity_comparison; - $this->entityQuery = $entity_query; } /** @@ -129,8 +118,7 @@ class RevisionOverviewForm extends FormBase { $container->get('renderer'), $container->get('language_manager'), $container->get('plugin.manager.diff.layout'), - $container->get('diff.entity_comparison'), - $container->get('entity.query') + $container->get('diff.entity_comparison') ); } @@ -156,11 +144,15 @@ class RevisionOverviewForm extends FormBase { $pagerLimit = $this->config->get('general_settings.revision_pager_limit'); - $query = $this->entityQuery->get('node') + $query = $this->entityTypeManager->getStorage('node')->getQuery() ->condition($node->getEntityType()->getKey('id'), $node->id()) ->pager($pagerLimit) ->allRevisions() ->sort($node->getEntityType()->getKey('revision'), 'DESC') + // Access to the content has already been verified. Disable query-level + // access checking so that revisions for unpublished content still + //appear. + ->accessCheck(FALSE) ->execute(); $vids = array_keys($query); diff --git a/web/modules/contrib/diff/src/Tests/DiffAdminFormsTest.php b/web/modules/contrib/diff/src/Tests/DiffAdminFormsTest.php index 03e60d59c..bbbfd9260 100644 --- a/web/modules/contrib/diff/src/Tests/DiffAdminFormsTest.php +++ b/web/modules/contrib/diff/src/Tests/DiffAdminFormsTest.php @@ -1,6 +1,7 @@ 'great_title', 'body[0][value]' => '

great_body

', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); $this->clickLink('Edit'); $edit = [ 'title[0][value]' => 'greater_title', 'body[0][value]' => '

greater_body

', ]; - $this->drupalPostForm(NULL, $edit, t('Save and keep published')); + $this->drupalPostNodeForm(NULL, $edit, t('Save and keep published')); // Assert the diff display uses the classic layout. $node = $this->getNodeByTitle('greater_title'); diff --git a/web/modules/contrib/diff/src/Tests/DiffLocaleTest.php b/web/modules/contrib/diff/src/Tests/DiffLocaleTest.php index 15bd871d6..1779e26ef 100644 --- a/web/modules/contrib/diff/src/Tests/DiffLocaleTest.php +++ b/web/modules/contrib/diff/src/Tests/DiffLocaleTest.php @@ -1,6 +1,7 @@ 'English node', 'langcode[0][value]' => 'en', ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); $english_node = $this->drupalGetNodeByTitle('English node'); $this->drupalGet('node/' . $english_node->id() . '/translations'); @@ -71,7 +74,7 @@ class DiffLocaleTest extends DiffTestBase { 'title[0][value]' => 'French node', 'revision' => FALSE, ); - $this->drupalPostForm(NULL, $edit, t('Save and keep published (this translation)')); + $this->drupalPostNodeForm(NULL, $edit, t('Save and keep published (this translation)')); $this->rebuildContainer(); $english_node = $this->drupalGetNodeByTitle('English node'); $french_node = $english_node->getTranslation('fr'); @@ -81,12 +84,12 @@ class DiffLocaleTest extends DiffTestBase { 'title[0][value]' => 'Updated title', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $english_node->id() . '/edit', $edit, t('Save and keep published (this translation)')); + $this->drupalPostNodeForm('node/' . $english_node->id() . '/edit', $edit, t('Save and keep published (this translation)')); $edit = array( 'title[0][value]' => 'Le titre', 'revision' => TRUE, ); - $this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', $edit, t('Save and keep published (this translation)')); + $this->drupalPostNodeForm('fr/node/' . $english_node->id() . '/edit', $edit, t('Save and keep published (this translation)')); // View differences between revisions. Check that they don't mix up. $this->drupalGet('node/' . $english_node->id() . '/revisions'); diff --git a/web/modules/contrib/diff/src/Tests/DiffPluginEntityTest.php b/web/modules/contrib/diff/src/Tests/DiffPluginEntityTest.php index c9eb4ec9d..3e8096df3 100644 --- a/web/modules/contrib/diff/src/Tests/DiffPluginEntityTest.php +++ b/web/modules/contrib/diff/src/Tests/DiffPluginEntityTest.php @@ -3,6 +3,7 @@ namespace Drupal\diff\Tests; use Drupal\field_ui\Tests\FieldUiTestTrait; +use Drupal\Tests\diff\Functional\CoreVersionUiTestTrait; /** * Tests the Diff module entity plugins. @@ -12,6 +13,7 @@ use Drupal\field_ui\Tests\FieldUiTestTrait; class DiffPluginEntityTest extends DiffPluginTestBase { use FieldUiTestTrait; + use CoreVersionUiTestTrait; /** * Modules to enable. @@ -77,14 +79,14 @@ class DiffPluginEntityTest extends DiffPluginTestBase { 'field_reference[0][target_id]' => 'Article B (' . $node2->id() . ')', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node1->id() . '/edit', $edit, t('Save and keep published')); // Update article A so it points to article C instead of B. $edit = array( 'field_reference[0][target_id]' => 'Article C (' . $node3->id() . ')', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node1->id() . '/edit', $edit, t('Save and keep published')); // Check differences between revisions. $this->clickLink(t('Revisions')); diff --git a/web/modules/contrib/diff/src/Tests/DiffPluginFileTest.php b/web/modules/contrib/diff/src/Tests/DiffPluginFileTest.php index 1f41f5bfc..ecbbd9336 100644 --- a/web/modules/contrib/diff/src/Tests/DiffPluginFileTest.php +++ b/web/modules/contrib/diff/src/Tests/DiffPluginFileTest.php @@ -5,6 +5,7 @@ namespace Drupal\diff\Tests; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field_ui\Tests\FieldUiTestTrait; +use Drupal\Tests\diff\Functional\CoreVersionUiTestTrait; /** * Tests the Diff module entity plugins. @@ -14,6 +15,7 @@ use Drupal\field_ui\Tests\FieldUiTestTrait; class DiffPluginFileTest extends DiffPluginTestBase { use FieldUiTestTrait; + use CoreVersionUiTestTrait; /** * Modules to enable. @@ -89,17 +91,17 @@ class DiffPluginFileTest extends DiffPluginTestBase { $edit['files[field_file_0]'] = $this->fileSystem->realpath($test_files['0']->uri); $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Upload'); $edit['revision'] = TRUE; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); $node = $this->drupalGetNodeByTitle('Test article', TRUE); $revision2 = $node->getRevisionId(); // Replace the file by a different one. $this->drupalPostForm('node/' . $node->id() . '/edit', [], 'Remove'); - $this->drupalPostForm(NULL, ['revision' => FALSE], t('Save and keep published')); + $this->drupalPostNodeForm(NULL, ['revision' => FALSE], t('Save and keep published')); $edit['files[field_file_0]'] = $this->fileSystem->realpath($test_files['1']->uri); $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Upload'); $edit['revision'] = TRUE; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); $node = $this->drupalGetNodeByTitle('Test article', TRUE); $revision3 = $node->getRevisionId(); @@ -172,25 +174,25 @@ class DiffPluginFileTest extends DiffPluginTestBase { // Upload an image to the article. $test_files = $this->drupalGetTestFiles('image'); $edit = ['files[field_image_0]' => $this->fileSystem->realpath($test_files['1']->uri)]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); $edit = [ 'field_image[0][alt]' => 'Image alt', 'revision' => TRUE, ]; - $this->drupalPostForm(NULL, $edit, t('Save and keep published')); + $this->drupalPostNodeForm(NULL, $edit, t('Save and keep published')); $node = $this->drupalGetNodeByTitle('Test article', TRUE); $revision2 = $node->getRevisionId(); // Replace the image by a different one. $this->drupalPostForm('node/' . $node->id() . '/edit', [], 'Remove'); - $this->drupalPostForm(NULL, ['revision' => FALSE], t('Save and keep published')); + $this->drupalPostNodeForm(NULL, ['revision' => FALSE], t('Save and keep published')); $edit = ['files[field_image_0]' => $this->fileSystem->realpath($test_files['1']->uri)]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); $edit = [ 'field_image[0][alt]' => 'Image alt updated', 'revision' => TRUE, ]; - $this->drupalPostForm(NULL, $edit, t('Save and keep published')); + $this->drupalPostNodeForm(NULL, $edit, t('Save and keep published')); $node = $this->drupalGetNodeByTitle('Test article', TRUE); $revision3 = $node->getRevisionId(); @@ -217,7 +219,7 @@ class DiffPluginFileTest extends DiffPluginTestBase { 'revision' => TRUE, 'field_image[0][title]' => 'Image title updated', ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); $this->drupalPostForm('node/' . $node->id() . '/revisions', [], t('Compare selected revisions')); $rows = $this->xpath('//tbody/tr'); // Image title and alternative text must be shown. diff --git a/web/modules/contrib/diff/src/Tests/DiffPluginTest.php b/web/modules/contrib/diff/src/Tests/DiffPluginTest.php index 67c683b38..f01dc5fcc 100644 --- a/web/modules/contrib/diff/src/Tests/DiffPluginTest.php +++ b/web/modules/contrib/diff/src/Tests/DiffPluginTest.php @@ -5,6 +5,7 @@ namespace Drupal\diff\Tests; use Drupal\comment\Tests\CommentTestTrait; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Tests\diff\Functional\CoreVersionUiTestTrait; /** * Tests the Diff module plugins. @@ -14,6 +15,7 @@ use Drupal\field\Entity\FieldStorageConfig; class DiffPluginTest extends DiffPluginTestBase { use CommentTestTrait; + use CoreVersionUiTestTrait; /** * Modules to enable. @@ -78,10 +80,11 @@ class DiffPluginTest extends DiffPluginTestBase { // Update the article and add a new revision, the "changed" field should be // updated which does not have plugins provided by diff. - $edit = array( + $edit = [ 'revision' => TRUE, - ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + 'body[0][value]' => 'change', + ]; + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check the difference between the last two revisions. $this->clickLink(t('Revisions')); @@ -151,7 +154,7 @@ class DiffPluginTest extends DiffPluginTestBase { 'test_field_non_applicable[0][value]' => 'nicer_not_applicable', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check differences between revisions. $this->clickLink(t('Revisions')); @@ -195,13 +198,10 @@ class DiffPluginTest extends DiffPluginTestBase { 'body[0][value]' => '

body

', ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); - - // Assert the revision summary. - $this->drupalGet('node/' . $node->id() . '/revisions'); - $this->assertText('Changes on: Body'); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Assert the revision comparison. + $this->drupalGet('node/' . $node->id() . '/revisions'); $this->drupalPostForm(NULL, [], t('Compare selected revisions')); $this->assertNoText('No visible changes.'); $rows = $this->xpath('//tbody/tr'); @@ -217,7 +217,7 @@ class DiffPluginTest extends DiffPluginTestBase {

body_new

', ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); $this->drupalGet('node/' . $node->id() . '/revisions'); $this->drupalPostForm(NULL, [], t('Compare selected revisions')); $this->assertNoText('No visible changes.'); diff --git a/web/modules/contrib/diff/src/Tests/DiffPluginVariousTest.php b/web/modules/contrib/diff/src/Tests/DiffPluginVariousTest.php index fdbe2bb24..4c5b54127 100644 --- a/web/modules/contrib/diff/src/Tests/DiffPluginVariousTest.php +++ b/web/modules/contrib/diff/src/Tests/DiffPluginVariousTest.php @@ -7,6 +7,7 @@ use Drupal\comment\Tests\CommentTestTrait; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\link\LinkItemInterface; +use Drupal\Tests\diff\Functional\CoreVersionUiTestTrait; /** * Tests the Diff module plugins. @@ -16,6 +17,7 @@ use Drupal\link\LinkItemInterface; class DiffPluginVariousTest extends DiffPluginTestBase { use CommentTestTrait; + use CoreVersionUiTestTrait; /** * Modules to enable. @@ -88,7 +90,7 @@ class DiffPluginVariousTest extends DiffPluginTestBase { 'body[0][value]' => '

Revision 1

', 'comment[0][status]' => CommentItemInterface::OPEN, ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); $node = $this->drupalGetNodeByTitle($title); // Edit the article and close its comments. @@ -96,7 +98,7 @@ class DiffPluginVariousTest extends DiffPluginTestBase { 'comment[0][status]' => CommentItemInterface::CLOSED, 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check the difference between the last two revisions. $this->clickLink(t('Revisions')); @@ -148,7 +150,7 @@ class DiffPluginVariousTest extends DiffPluginTestBase { 'field_email[0][value]' => 'bar@example.com', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check the difference between the last two revisions. $this->clickLink(t('Revisions')); @@ -274,7 +276,7 @@ class DiffPluginVariousTest extends DiffPluginTestBase { 'field_link[0][uri]' => 'http://www.google.es', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check differences between revisions. $this->clickLink(t('Revisions')); @@ -335,7 +337,7 @@ class DiffPluginVariousTest extends DiffPluginTestBase { 'field_list' => 'value_b', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check differences between revisions. $this->clickLink(t('Revisions')); @@ -369,7 +371,7 @@ class DiffPluginVariousTest extends DiffPluginTestBase { 'field_text_long[0][value]' => 'Fly', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check differences between revisions. $this->clickLink(t('Revisions')); @@ -411,7 +413,7 @@ class DiffPluginVariousTest extends DiffPluginTestBase { 'body[0][summary]' => 'Bar summary', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check differences between revisions. $this->clickLink(t('Revisions')); diff --git a/web/modules/contrib/diff/src/Tests/DiffRevisionContentModerationTest.php b/web/modules/contrib/diff/src/Tests/DiffRevisionContentModerationTest.php new file mode 100644 index 000000000..c26923fa8 --- /dev/null +++ b/web/modules/contrib/diff/src/Tests/DiffRevisionContentModerationTest.php @@ -0,0 +1,104 @@ +getTypePlugin(); + $plugin->addEntityTypeAndBundle('node', 'article'); + $workflow->save(); + + // Add necessary admin permissions for moderated content. + $this->adminPermissions = array_merge([ + 'use editorial transition create_new_draft', + 'use editorial transition publish', + 'use editorial transition archive', + 'use editorial transition archived_draft', + 'use editorial transition archived_published', + 'view latest version', + 'view any unpublished content', + ], $this->adminPermissions); + } + + /** + * {@inheritdoc} + * + * Override form submission to work with content moderation. + */ + protected function drupalPostNodeForm($path, array $edit, $submit) { + // New revisions are automatically enabled, so remove the manual value. + unset($edit['revision']); + parent::drupalPostNodeForm($path, $edit, $submit); + } + + /** + * {@inheritdoc} + */ + public function testAll() { + // Ensure revision tab still works as expected. + parent::testAll(); + + // Specifically test for content moderation functionality. + $this->doTestContentModeration(); + } + + /** + * Test content moderation integration. + */ + protected function doTestContentModeration() { + $title = $this->randomString(); + $node = $this->createNode([ + 'type' => 'article', + 'title' => $title, + 'revision_log' => 'First revision', + ]); + + // Add another draft. + $node->title = $title . ' change 1'; + $node->revision_log = 'Second revision'; + $node->save(); + + // Publish. + $node->moderation_state = 'published'; + $node->revision_log = 'Third revision'; + $node->save(); + + // Another draft. + $node->title = $title . ' change 2'; + $node->moderation_state = 'draft'; + $node->revision_log = 'Fourth revision'; + $node->save(); + + // Verify moderation state information appears on revision overview. + $this->drupalGet($node->toUrl('version-history')); + + // Verify proper moderation states are displayed. + $diff_rows = $this->xpath('//tbody/tr/td[1]/p'); + $this->assertEqual('Fourth revision (Draft)', (string) $diff_rows[0]); + $this->assertEqual('Third revision (Published)', (string) $diff_rows[1]); + $this->assertEqual('Second revision (Draft)', (string) $diff_rows[2]); + $this->assertEqual('First revision (Draft)', (string) $diff_rows[3]); + } + +} diff --git a/web/modules/contrib/diff/src/Tests/DiffRevisionTest.php b/web/modules/contrib/diff/src/Tests/DiffRevisionTest.php index 41554026f..b49c20f67 100644 --- a/web/modules/contrib/diff/src/Tests/DiffRevisionTest.php +++ b/web/modules/contrib/diff/src/Tests/DiffRevisionTest.php @@ -4,6 +4,7 @@ namespace Drupal\diff\Tests; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\system\Tests\Menu\AssertBreadcrumbTrait; +use Drupal\Tests\diff\Functional\CoreVersionUiTestTrait; /** * Tests the diff revisions overview. @@ -13,6 +14,7 @@ use Drupal\system\Tests\Menu\AssertBreadcrumbTrait; class DiffRevisionTest extends DiffTestBase { use AssertBreadcrumbTrait; + use CoreVersionUiTestTrait; /** * Modules to enable. @@ -51,9 +53,12 @@ class DiffRevisionTest extends DiffTestBase {

first_unique_text

second_unique_text

', ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + // Set to published if content moderation is enabled. + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + $edit['moderation_state[0][state]'] = 'published'; + } + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); $node = $this->drupalGetNodeByTitle($title); - $created = $node->getCreatedTime(); $this->drupalGet('node/' . $node->id()); // Make sure the revision tab doesn't exist. @@ -68,7 +73,11 @@ class DiffRevisionTest extends DiffTestBase { 'revision' => TRUE, 'revision_log[0][value]' => 'Revision 2 comment', ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + // Set to published if content moderation is enabled. + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + $edit['moderation_state[0][state]'] = 'published'; + } + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); $this->drupalGet('node/' . $node->id()); // Check the revisions overview. @@ -78,7 +87,6 @@ class DiffRevisionTest extends DiffTestBase { $this->assertEqual(count($rows), 2); // Assert the revision summary. $this->assertUniqueText('Revision 2 comment'); - $this->assertText('Initial revision.'); // Compare the revisions in standard mode. $this->drupalPostForm(NULL, NULL, t('Compare selected revisions')); @@ -207,29 +215,39 @@ class DiffRevisionTest extends DiffTestBase { // Make sure we only have 1 revision now. $rows = $this->xpath('//tbody/tr'); - $this->assertEqual(count($rows), 1); + $this->assertEqual(count($rows), 0); // Assert that there are no radio buttons for revision selection. $this->assertNoFieldByXPath('//input[@type="radio"]'); // Assert that there is no submit button. - $this->assertNoFieldByXPath('//input[@type="submit"]'); + $this->assertNoFieldByXPath('//input[@type="submit" and text()="Compare selected revisions"]'); // Create two new revisions of node. $edit = [ 'title[0][value]' => 'new test title', 'body[0][value]' => '

new body

', + 'revision_log[0][value]' => 'this revision message will appear twice', ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save and keep published'); + // Set to published if content moderation is enabled. + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + $edit['moderation_state[0][state]'] = 'published'; + } + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, 'Save and keep published'); $edit = [ 'title[0][value]' => 'newer test title', 'body[0][value]' => '

newer body

', + 'revision_log[0][value]' => 'this revision message will appear twice', ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save and keep published'); + // Set to published if content moderation is enabled. + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + $edit['moderation_state[0][state]'] = 'published'; + } + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, 'Save and keep published'); $this->clickLink(t('Revisions')); // Assert the revision summary. - $this->assertNoUniqueText('Changes on: Title, Body'); + $this->assertNoUniqueText('this revision message will appear twice'); $this->assertText('Copy of the revision from'); $edit = [ 'radios_left' => 3, @@ -251,6 +269,10 @@ class DiffRevisionTest extends DiffTestBase { $node = $this->getNodeByTitle('newer test title'); $node->setNewRevision(TRUE); $node->isDefaultRevision(FALSE); + if ($node->hasField('moderation_state')) { + // If testing with content_moderation enabled, set as draft. + $node->moderation_state = 'draft'; + } $node->save(); $this->drupalGet('node/' . $node->id() . '/revisions'); @@ -263,10 +285,18 @@ class DiffRevisionTest extends DiffTestBase { $this->clickLink('Set as current revision'); $this->drupalPostForm(NULL, [], t('Revert')); - // Check the last revision is set as current. - $text = $this->xpath('//tbody/tr[1]/td[4]/em'); - $this->assertEqual($text[0], 'Current revision'); - $this->assertNoLink(t('Set as current revision')); + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + // With content moderation, the new revision will not be current. + // @see https://www.drupal.org/node/2899719 + $text = $this->xpath('//tbody/tr[1]/td[4]/div/div/ul/li/a'); + $this->assertEqual($text[0], 'Set as current revision'); + } + else { + // Check the last revision is set as current. + $text = $this->xpath('//tbody/tr[1]/td[4]/em'); + $this->assertEqual($text[0], 'Current revision'); + $this->assertNoLink(t('Set as current revision')); + } } /** @@ -281,11 +311,15 @@ class DiffRevisionTest extends DiffTestBase { $node = $this->drupalCreateNode([ 'type' => 'article', ]); + // Create 11 more revisions in order to trigger paging on the revisions // overview screen. for ($i = 0; $i < 11; $i++) { - $node->setNewRevision(TRUE); - $node->save(); + $edit = [ + 'revision' => TRUE, + 'body[0][value]' => 'change: ' . $i, + ]; + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); } // Check the number of elements on the first page. @@ -336,7 +370,7 @@ class DiffRevisionTest extends DiffTestBase { 'title[0][value]' => $title, 'body[0][value]' => '

Revision 1

', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); $node = $this->drupalGetNodeByTitle($title); $revision1 = $node->getRevisionId(); @@ -346,7 +380,7 @@ class DiffRevisionTest extends DiffTestBase { 'body[0][value]' => '

Revision 2

', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Check the revisions overview, ensure only one revisions is available. $this->clickLink(t('Revisions')); @@ -363,7 +397,7 @@ class DiffRevisionTest extends DiffTestBase { 'body[0][value]' => '

Revision 3

', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); $node = $this->drupalGetNodeByTitle($title, TRUE); $revision3 = $node->getRevisionId(); @@ -420,7 +454,10 @@ class DiffRevisionTest extends DiffTestBase { 'title[0][value]' => $title, 'body[0][value]' => '

First article

', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + $edit['moderation_state[0][state]'] = 'published'; + } + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); $node_one = $this->drupalGetNodeByTitle($title); // Create second article. @@ -429,7 +466,10 @@ class DiffRevisionTest extends DiffTestBase { 'title[0][value]' => $title, 'body[0][value]' => '

Second article

', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + $edit['moderation_state[0][state]'] = 'published'; + } + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); $node_two = $this->drupalGetNodeByTitle($title); // Create revision and add entity reference from second node to first. @@ -438,7 +478,10 @@ class DiffRevisionTest extends DiffTestBase { 'field_content[0][target_id]' => $node_two->getTitle(), 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node_one->id() . '/edit', $edit, t('Save and keep published')); + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + $edit['moderation_state[0][state]'] = 'published'; + } + $this->drupalPostNodeForm('node/' . $node_one->id() . '/edit', $edit, t('Save and keep published')); // Delete referenced node. $node_two->delete(); diff --git a/web/modules/contrib/diff/src/Tests/DiffViewModeTest.php b/web/modules/contrib/diff/src/Tests/DiffViewModeTest.php index e07555a4a..352f8b73c 100644 --- a/web/modules/contrib/diff/src/Tests/DiffViewModeTest.php +++ b/web/modules/contrib/diff/src/Tests/DiffViewModeTest.php @@ -1,6 +1,7 @@ 'Fighters', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); // Set the Body field to hidden in the diff view mode. $edit = [ diff --git a/web/modules/contrib/diff/tests/modules/diff_test/diff_test.info.yml b/web/modules/contrib/diff/tests/modules/diff_test/diff_test.info.yml index dc7b7be86..e02bf20d8 100644 --- a/web/modules/contrib/diff/tests/modules/diff_test/diff_test.info.yml +++ b/web/modules/contrib/diff/tests/modules/diff_test/diff_test.info.yml @@ -11,8 +11,8 @@ name: Diff tests package: diff type: module -# Information added by Drupal.org packaging script on 2017-01-16 -version: '8.x-1.0-rc1' +# Information added by Drupal.org packaging script on 2018-06-28 +version: '8.x-1.0-rc2' core: '8.x' project: 'diff' -datestamp: 1484566687 +datestamp: 1530178427 diff --git a/web/modules/contrib/diff/tests/src/Functional/CoreVersionUiTestTrait.php b/web/modules/contrib/diff/tests/src/Functional/CoreVersionUiTestTrait.php new file mode 100644 index 000000000..07e0ab18c --- /dev/null +++ b/web/modules/contrib/diff/tests/src/Functional/CoreVersionUiTestTrait.php @@ -0,0 +1,39 @@ +drupalPostForm($path, $edit, $submit); + } + +} \ No newline at end of file diff --git a/web/modules/contrib/diff/tests/src/Functional/NodeAccessTest.php b/web/modules/contrib/diff/tests/src/Functional/NodeAccessTest.php new file mode 100644 index 000000000..046d96470 --- /dev/null +++ b/web/modules/contrib/diff/tests/src/Functional/NodeAccessTest.php @@ -0,0 +1,71 @@ +createContentType(['type' => 'article']); + + // Dummy user 1. + $this->createUser(); + + // Rebuild access. + node_access_rebuild(); + } + + /** + * Tests that the revision overview form still works with node access. + */ + public function testOverview() { + // Create an unpublished node with 3 revisions. + $node = $this->createNode([ + 'type' => 'article', + 'status' => FALSE, + ]); + $node->setTitle($this->randomString()); + $node->setNewRevision(); + $node->save(); + $node->setTitle($this->randomString()); + $node->setNewRevision(); + $node->save(); + $user = $this->createUser(['access content', 'view all revisions']); + $this->drupalLogin($user); + + // Grant access via node_access_test. + // @see node_access_test_node_access + \Drupal::state()->set('node_access_test.allow_uid', $user->id()); + + $this->drupalGet($node->toUrl()); + $this->assertSession()->statusCodeEquals(200); + $this->drupalGet($node->toUrl('version-history')); + $this->assertSession()->statusCodeEquals(200); + + // There should be 3 diff rows. + $rows = $this->xpath('//tbody/tr'); + $this->assertCount(3, $rows, 'Did not find 3 diff rows.'); + + // Compare selected revisions should not time out. + $this->drupalGet('/node/' . $node->id(). '/revisions'); + $this->drupalPostForm(NULL, NULL, t('Compare selected revisions')); + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/web/modules/contrib/draggableviews/README.md b/web/modules/contrib/draggableviews/README.md new file mode 100755 index 000000000..d83bdb98f --- /dev/null +++ b/web/modules/contrib/draggableviews/README.md @@ -0,0 +1,59 @@ +INTRODUCTION +------------ + +This module provides dragging entities and saving their order. + + +REQUIREMENTS +------------- + +Views, Views UI, Filter, User, System module enabled. + + +INSTALLATION +------------ + +Enable this module from extend list page /admin/modules. + + +CONFIGURATION +------------- + +1) Activate Draggableviews module at /admin/modules. +2) Create a new view + - Goto '/admin/structure/views/add' on your site. + - Check off 'Create a page'. + - Check off 'Create a block'. + - Set the 'Display format' for the page to what you desire. + - Set the "'Display format' of" to fields. + - Set the 'Display format' for the block to table. + - Fill in the rest of the views information. + - Click Save & edit button. +3) Under the "FIELDS" section, do you see "Content: Title"? If you do not: + - Click 'add' button at the "Fields" section and choose field + "Content:title", add and apply. +4) Add the Draggableviews Field: + - Click Add button at the "FIELDS" section. + - At the top of the overlay, Change "For: 'All displays'" to 'This block + (override)'. + - If you do not do this then the field will be add to all displays and + will prevent your page display from using the block display to sort the + order. +5) Click Add button at the "SORT CRITERIA" section choose field +"Draggableviews: Weight", add and choose sort asc, then apply. +6) Under the "SORT CRITERIA" section, do you see "Content: Post date (asc)"? + If you do: + - Click on it. At the bottom, click the 'Remove' button. + - An alternative is to rearrange the "SORT CRITERIA" order, making sure + 'Draggableviews: Weight (asc) appears first (or on top). +7) Save the view and you're done. +*Things to confirm after you saved your new view. +- In the Administrative Views UI, Go back to your View's 'page' display. + -> Click 'Draggableviews: Weight (asc)' under 'SORT CRITERIA' + -> You should see: + Display sort as: + (<display title>) + + This should the view and block display you just create. + + FYI - This is also where you can change it to another view. diff --git a/web/modules/contrib/draggableviews/composer.json b/web/modules/contrib/draggableviews/composer.json new file mode 100644 index 000000000..c1fed0d98 --- /dev/null +++ b/web/modules/contrib/draggableviews/composer.json @@ -0,0 +1,39 @@ +{ + "name": "drupal/draggableviews", + "description": "DraggableViews module makes views draggable.", + "type": "drupal-module", + "homepage": "https://www.drupal.org/project/draggableviews", + "authors": [ + { + "name": "Tyler Struyk (iStryker)", + "email": "tyler.struyk@gmail.com", + "homepage": "https://www.drupal.org/u/istryker", + "role": "Maintainer" + }, + { + "name": "Andrii Podanenko (podarok)", + "email": "podarokua@gmail.com", + "homepage": "https://www.drupal.org/u/podarok", + "role": "Drupal 7 to 8 Porter" + }, + { + "name": "Yuriy Gerasimov (ygerasimov)", + "email": "yuriy.gerasimov@gmail.com", + "homepage": "https://www.drupal.org/u/ygerasimov", + "role": "Ex Maintainer (D7)" + }, + { + "name": "Severin Unger (sevi)", + "homepage": "https://www.drupal.org/u/sevi", + "role": "Ex Maintainer (D6)" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/draggableviews", + "source": "https://cgit.drupalcode.org/draggableviews" + }, + "license": "GPL-2.0+", + "minimum-stability": "dev", + "require": { + } +} diff --git a/web/modules/contrib/draggableviews/draggableviews.info.yml b/web/modules/contrib/draggableviews/draggableviews.info.yml index 1acec7669..803dec3ca 100644 --- a/web/modules/contrib/draggableviews/draggableviews.info.yml +++ b/web/modules/contrib/draggableviews/draggableviews.info.yml @@ -1,12 +1,15 @@ name: DraggableViews type: module -description: Complete rewrite of D7 draggableviews +description: 'Allows your views to be draggable and gives you the ability to set the order of how they appear.' # core: 8.x dependencies: - - views + - drupal:views +test_dependencies: + - draggableviews:draggableviews_demo +package: Views -# Information added by Drupal.org packaging script on 2016-10-21 -version: '8.x-1.0' +# Information added by Drupal.org packaging script on 2018-11-06 +version: '8.x-1.2' core: '8.x' project: 'draggableviews' -datestamp: 1477076053 +datestamp: 1541518687 diff --git a/web/modules/contrib/draggableviews/draggableviews.install b/web/modules/contrib/draggableviews/draggableviews.install index 364c65a6d..f05c12a16 100644 --- a/web/modules/contrib/draggableviews/draggableviews.install +++ b/web/modules/contrib/draggableviews/draggableviews.install @@ -5,68 +5,78 @@ * Install, update and uninstall functions for the draggableviews module. */ +use Drupal\views\Views; + /** * Implements hook_schema(). */ function draggableviews_schema() { - $schema['draggableviews_structure'] = array( - 'description' => 'Table that contains logs of all system events.', - 'fields' => array( - 'dvid' => array( + $schema['draggableviews_structure'] = [ + 'description' => 'Saves the order settings of a draggableview view.', + 'fields' => [ + 'dvid' => [ 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'The primary identifier.', - ), - 'view_name' => array( + ], + 'view_name' => [ 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => '', 'description' => 'Makes the order unique for each view.', - ), - 'view_display' => array( + ], + 'view_display' => [ 'type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => '', 'description' => 'Makes the order unique for each view display.', - ), - 'args' => array( + ], + 'args' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'default' => '', 'description' => 'Makes the order unique for a given set of arguments', - ), - 'entity_id' => array( + ], + 'entity_id' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Id of the entity that we are sorting (node, user, etc.).', - ), - 'weight' => array( + ], + 'weight' => [ 'type' => 'int', 'unsigned' => FALSE, 'not null' => TRUE, 'default' => 0, 'description' => 'The order weight.', - ), - 'parent' => array( + ], + 'parent' => [ 'type' => 'int', 'unsigned' => FALSE, 'not null' => TRUE, 'default' => 0, 'description' => 'The id of the parent.', - ), - ), - 'indexes' => array( - 'view' => array('view_name', 'view_display', 'args', 'entity_id'), - 'weight' => array('weight'), - 'entity_id' => array('entity_id'), - ), - 'primary key' => array('dvid'), - ); + ], + ], + 'indexes' => [ + 'view' => ['view_name', 'view_display', 'args', 'entity_id'], + 'weight' => ['weight'], + 'entity_id' => ['entity_id'], + ], + 'primary key' => ['dvid'], + ]; return $schema; } + +/** + * Implements hook_update(). + */ +function draggableviews_update_8104(&$sandbox) { + // the update hook here accidentally cropped in into 1.1 but it shouldn't be + // there, I belivee that removing it should be harmless. +} diff --git a/web/modules/contrib/draggableviews/draggableviews.module b/web/modules/contrib/draggableviews/draggableviews.module index d5d22eeaa..0950a5a87 100644 --- a/web/modules/contrib/draggableviews/draggableviews.module +++ b/web/modules/contrib/draggableviews/draggableviews.module @@ -5,96 +5,109 @@ * Contains draggableviews.module. */ +use Drupal\draggableviews\DraggableViews; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Database\Database; +use Drupal\Core\Cache\Cache; + /** * Implements hook_views_data_alter(). */ function draggableviews_views_data_alter(&$data) { - $data['draggableviews_structure']['weight'] = array( + $data['draggableviews_structure']['weight'] = [ 'title' => t('DraggableViews Weight'), - 'group' => t('Global'), + 'group' => t('Draggableviews'), 'help' => t('Display the weight value.'), - 'field' => array( + 'field' => [ 'id' => 'numeric', - ), - 'sort' => array( + ], + 'sort' => [ 'id' => 'standard', - ), - 'filter' => array( + ], + 'filter' => [ 'help' => t('Filter by the draggableviews weight value (Native handler only).'), 'id' => 'numeric', - ), - ); - $data['draggableviews_structure']['parent'] = array( + ], + 'argument' => [ + 'id' => 'numeric', + ], + ]; + $data['draggableviews_structure']['parent'] = [ 'title' => t('Parent'), 'help' => t('The parent entity id.'), 'group' => t('Draggableviews'), - 'field' => array( + 'field' => [ + 'id' => 'numeric', + ], + 'filter' => [ + 'help' => t("Filter by the draggableviews parent's entity id (Native handler only)."), 'id' => 'numeric', - ), - 'filter' => array( - 'help' => t('Filter by the draggableviews parent\'s entity id (Native handler only).'), + ], + 'argument' => [ 'id' => 'numeric', - ), - ); + ], + ]; - foreach (\Drupal::entityManager()->getDefinitions() as $entity_type_id => $entity_type) { + foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type) { $base_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); $entity_keys = $entity_type->getKeys(); if ($base_table && isset($data[$base_table]['table'])) { - $data[$base_table]['draggableviews'] = array( + $data[$base_table]['draggableviews'] = [ 'title' => $data[$base_table]['table']['group'], 'group' => t('Draggableviews'), 'help' => t('Provide a draggable functionality.'), 'entity field' => $entity_keys['id'], - 'field' => array( + 'field' => [ 'id' => 'draggable_views_field', 'click sortable' => FALSE, - ), - ); - // Explain to every entity how to join with draggableviews structure table. - $data['draggableviews_structure']['table']['join'][$base_table] = array( + ], + ]; + // Explain to every entity how to join with draggableviews_structure + // table. + $data['draggableviews_structure']['table']['join'][$base_table] = [ 'handler' => 'draggableviews_join_handler', // Because this is a direct link it could be left out. 'left_table' => $base_table, 'left_field' => $entity_keys['id'], 'field' => 'entity_id', - 'extra' => array( - array('field' => 'view_name', 'value' => '***VIEW_ID***'), - array('field' => 'view_display', 'value' => '***VIEW_DISPLAY***'), - ), - ); + ]; } } } -/** - * Implements hook_views_query_substitutions(). - * - * Allow replacement of current userid so we can cache these queries. - */ -function draggableviews_views_query_substitutions(\Drupal\views\ViewExecutable $view) { - return array('***VIEW_ID***' => $view->id(), '***VIEW_DISPLAY***' => $view->current_display); -} - /** * Implements hook_preprocess_views_view_table(). */ function draggableviews_preprocess_views_view_table(&$variables) { $view = $variables['view']; + + // If this view is not the sort view, then stop here. if (!isset($view->field['draggableviews'])) { return; } - $draggableviews = new \Drupal\draggableviews\DraggableViews($variables['view']); + $draggableviews = new DraggableViews($variables['view']); // Add hierarchy. foreach ($variables['rows'] as $key => $row) { - $title = $row['columns']['title']['content'][0]['field_output']['#markup']; + $columns = array_keys($row['columns']); + // Find the first column that is not the draggableviews field. + do { + $first_column = current($columns); + if ($first_column !== 'draggableviews') { + break; + } + // Set the first column. + $first_column = each($columns); + } while ($first_column); + + // Indent the first column that is not the draggableviews field. + $columns_title = $row['columns'][$first_column]['content'][0]['field_output']['#markup']; $indent = [ '#theme' => 'indentation', '#size' => $draggableviews->getDepth($key), ]; - $variables['rows'][$key]['columns']['title']['content'][0]['field_output']['#markup'] = render($indent) . $title; + $variables['rows'][$key]['columns'][$first_column]['content'][0]['field_output']['#markup'] = (string) (render($indent) . $columns_title); } // Add table attributes. @@ -110,7 +123,7 @@ function draggableviews_preprocess_views_view_table(&$variables) { /** * Implements hook_form_alter(). */ -function draggableviews_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { +function draggableviews_form_alter(&$form, FormStateInterface $form_state, $form_id) { // Filter the right form. if (strpos($form_id, 'views_form_') === FALSE) { return; @@ -127,10 +140,10 @@ function draggableviews_form_alter(&$form, \Drupal\Core\Form\FormStateInterface if (\Drupal::currentUser()->hasPermission('access draggableviews')) { // Create draggableviews save order button. - $form['actions']['save_order'] = array( + $form['actions']['save_order'] = [ '#value' => t('Save order'), '#type' => 'submit', - ); + ]; } // If there is no results remove the save-order button. @@ -145,7 +158,7 @@ function draggableviews_form_alter(&$form, \Drupal\Core\Form\FormStateInterface /** * Submit handler. */ -function draggableviews_views_submit(&$form, \Drupal\Core\Form\FormStateInterface $form_state) { +function draggableviews_views_submit(&$form, FormStateInterface $form_state) { $input = $form_state->getUserInput(); /** @var \Drupal\views\ViewExecutable $view */ @@ -155,7 +168,7 @@ function draggableviews_views_submit(&$form, \Drupal\Core\Form\FormStateInterfac $weight = 0; - $connection = \Drupal\Core\Database\Database::getConnection(); + $connection = Database::getConnection(); $transaction = $connection->startTransaction(); try { foreach ($input['draggableviews'] as $item) { @@ -185,12 +198,12 @@ function draggableviews_views_submit(&$form, \Drupal\Core\Form\FormStateInterfac // cache. $views_entity_table_info = $view->query->getEntityTableInfo(); // Find the entity type used by the view. - $result = array_keys(array_filter($views_entity_table_info, function($info) { + $result = array_keys(array_filter($views_entity_table_info, function ($info) { return $info['relationship_id'] == 'none'; })); $entity_type_id = reset($result); $list_cache_tags = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getListCacheTags(); - \Drupal\Core\Cache\Cache::invalidateTags($list_cache_tags); + Cache::invalidateTags($list_cache_tags); } catch (\Exception $e) { $transaction->rollback(); diff --git a/web/modules/contrib/draggableviews/draggableviews.module.rej b/web/modules/contrib/draggableviews/draggableviews.module.rej new file mode 100644 index 000000000..1c1c2f443 --- /dev/null +++ b/web/modules/contrib/draggableviews/draggableviews.module.rej @@ -0,0 +1,26 @@ +--- draggableviews.module (revision 76d7ca84425218e1e9f60633767bcbb5024a3d8c) ++++ draggableviews.module (revision ) +@@ -58,10 +58,23 @@ + 'left_table' => $base_table, + 'left_field' => $entity_keys['id'], + 'field' => 'entity_id', ++ 'extra' => array( ++ array('field' => 'view_name', 'value' => '***VIEW_ID***'), ++ array('field' => 'view_display', 'value' => '***VIEW_DISPLAY***'), ++ ), + ); + } + } + } ++ ++/** ++ * Implements hook_views_query_substitutions(). ++ * ++ * Allow replacement of current userid so we can cache these queries. ++ */ ++function draggableviews_views_query_substitutions(\Drupal\views\ViewExecutable $view) { ++ return array('***VIEW_ID***' => $view->id(), '***VIEW_DISPLAY***' => $view->current_display); ++} + + /** + * Implements hook_preprocess_views_view_table(). diff --git a/web/modules/contrib/draggableviews/draggableviews.permissions.yml b/web/modules/contrib/draggableviews/draggableviews.permissions.yml index 7a1a06f09..43e7afbb9 100644 --- a/web/modules/contrib/draggableviews/draggableviews.permissions.yml +++ b/web/modules/contrib/draggableviews/draggableviews.permissions.yml @@ -1,4 +1,3 @@ access draggableviews: title: 'Access Draggableviews' description: 'Give users a permission to sort draggableviews.' - restrict access: true diff --git a/web/modules/contrib/draggableviews/modules/draggableviews_demo/config/install/node.type.draggableviews_demo.yml b/web/modules/contrib/draggableviews/modules/draggableviews_demo/config/install/node.type.draggableviews_demo.yml new file mode 100644 index 000000000..0fa539c83 --- /dev/null +++ b/web/modules/contrib/draggableviews/modules/draggableviews_demo/config/install/node.type.draggableviews_demo.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - draggableviews_demo +third_party_settings: + menu_ui: + available_menus: + - main + parent: 'main:' +name: 'Draggableviews Demo' +type: draggableviews_demo +description: '' +help: '' +new_revision: true +preview_mode: 1 +display_submitted: true diff --git a/web/modules/contrib/draggableviews/modules/draggableviews_demo/config/install/views.view.draggableviews_demo.yml b/web/modules/contrib/draggableviews/modules/draggableviews_demo/config/install/views.view.draggableviews_demo.yml new file mode 100644 index 000000000..4bb7c4d0a --- /dev/null +++ b/web/modules/contrib/draggableviews/modules/draggableviews_demo/config/install/views.view.draggableviews_demo.yml @@ -0,0 +1,519 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.node.teaser + - node.type.draggableviews_demo + module: + - draggableviews + - node + - user +_core: + default_config_hash: jNovwA94yRL5frztAOmdSCWDJHEa4UJhryQKRWYwJ98 +id: draggableviews_demo +label: 'Draggableviews Demo' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + row: + type: fields + options: + default_field_elements: true + inline: { } + separator: '' + hide_empty: false + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + draggableviews: + id: draggableviews + table: node_field_data + field: draggableviews + relationship: none + group_type: group + admin_label: '' + label: Content + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: node + entity_field: nid + plugin_id: draggable_views_field + weight: + id: weight + table: draggableviews_structure + field: weight + relationship: none + group_type: group + admin_label: '' + label: 'DraggableViews Weight' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + set_precision: false + precision: 0 + decimal: . + separator: ',' + format_plural: false + format_plural_string: "1\x03@count" + prefix: '' + suffix: '' + plugin_id: numeric + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + relationship: none + group_type: group + admin_label: '' + operator: in + value: + draggableviews_demo: draggableviews_demo + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + weight: + id: weight + table: draggableviews_structure + field: weight + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + plugin_id: standard + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: 'Draggableviews Demo Display Page' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + draggableviews_demo_display: + display_plugin: page + id: draggableviews_demo_display + display_title: 'Display Page' + position: 1 + display_options: + display_extenders: { } + path: draggableviews-demo + display_description: '' + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + weight: + id: weight + table: draggableviews_structure + field: weight + relationship: none + group_type: group + admin_label: '' + label: 'DraggableViews Weight' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + set_precision: false + precision: 0 + decimal: . + separator: ',' + format_plural: false + format_plural_string: "1\x03@count" + prefix: '' + suffix: '' + plugin_id: numeric + defaults: + fields: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + draggableviews_demo_order: + display_plugin: page + id: draggableviews_demo_order + display_title: 'Order Page' + position: 2 + display_options: + display_extenders: { } + path: draggableviews-demo/order + menu: + type: tab + title: 'Order Draggableviews Demo' + description: '' + expanded: false + parent: '' + weight: 0 + context: '0' + menu_name: main + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + title: title + info: + title: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: '-1' + empty_table: false + defaults: + style: false + row: false + title: false + access: false + row: + type: 'entity:node' + options: + view_mode: teaser + title: 'Draggableviews Demo Order Page' + access: + type: perm + options: + perm: 'access draggableviews' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/web/modules/contrib/draggableviews/modules/draggableviews_demo/draggableviews_demo.info.yml b/web/modules/contrib/draggableviews/modules/draggableviews_demo/draggableviews_demo.info.yml new file mode 100644 index 000000000..10a562c19 --- /dev/null +++ b/web/modules/contrib/draggableviews/modules/draggableviews_demo/draggableviews_demo.info.yml @@ -0,0 +1,22 @@ +name: Draggableviews Demo +type: module +description: 'Demo module for Draggableviews.' +# core: 8.x +dependencies: + - drupal:block + - drupal:draggableviews + - drupal:menu_ui + - drupal:node + - drupal:user + - drupal:views + - drupal:system + - drupal:filter + - drupal:field +hidden: false +package: Views + +# Information added by Drupal.org packaging script on 2018-11-06 +version: '8.x-1.2' +core: '8.x' +project: 'draggableviews' +datestamp: 1541518687 diff --git a/web/modules/contrib/draggableviews/modules/draggableviews_demo/draggableviews_demo.install b/web/modules/contrib/draggableviews/modules/draggableviews_demo/draggableviews_demo.install new file mode 100644 index 000000000..89a70d38d --- /dev/null +++ b/web/modules/contrib/draggableviews/modules/draggableviews_demo/draggableviews_demo.install @@ -0,0 +1,6 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the draggableviews demo module. + */ diff --git a/web/modules/contrib/draggableviews/src/DraggableViews.php b/web/modules/contrib/draggableviews/src/DraggableViews.php index da5e99dcb..7cfd4f79f 100644 --- a/web/modules/contrib/draggableviews/src/DraggableViews.php +++ b/web/modules/contrib/draggableviews/src/DraggableViews.php @@ -1,10 +1,5 @@ <?php -/** - * @file - * Contains \Drupal\draggableviews\DraggableViews class. - */ - namespace Drupal\draggableviews; use Drupal\views\ViewExecutable; @@ -18,7 +13,7 @@ class DraggableViews { /** * The view. * - * @var \Drupal\views\ViewExecutable $view + * @var \Drupal\views\ViewExecutable */ public $view; diff --git a/web/modules/contrib/draggableviews/src/Plugin/migrate/destination/DraggableViews.php b/web/modules/contrib/draggableviews/src/Plugin/migrate/destination/DraggableViews.php index 92f5e9f04..1dfac33d3 100644 --- a/web/modules/contrib/draggableviews/src/Plugin/migrate/destination/DraggableViews.php +++ b/web/modules/contrib/draggableviews/src/Plugin/migrate/destination/DraggableViews.php @@ -1,10 +1,5 @@ <?php -/** - * @file - * Contains destination plugin for Draggable Views database table. - */ - namespace Drupal\draggableviews\Plugin\migrate\destination; use Drupal\Core\Database\Database; @@ -21,22 +16,6 @@ use Drupal\migrate\Row; */ class DraggableViews extends DestinationBase { - /** - * Constructs an entity destination plugin. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param MigrationInterface $migration - * The migration. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $migration); - } - /** * {@inheritdoc} */ @@ -50,7 +29,7 @@ class DraggableViews extends DestinationBase { 'parent' => $row->getDestinationProperty('parent'), ]; $result = Database::getConnection()->insert('draggableviews_structure')->fields($record)->execute(); - return array($result); + return [$result]; } /** diff --git a/web/modules/contrib/draggableviews/src/Plugin/views/field/DraggableViewsField.php b/web/modules/contrib/draggableviews/src/Plugin/views/field/DraggableViewsField.php index 4361e398a..8f1e6fff0 100755 --- a/web/modules/contrib/draggableviews/src/Plugin/views/field/DraggableViewsField.php +++ b/web/modules/contrib/draggableviews/src/Plugin/views/field/DraggableViewsField.php @@ -1,16 +1,15 @@ <?php -/** - * @file - * Contains \Drupal\draggableviews\Plugin\views\field\DraggableViewsField. - */ - namespace Drupal\draggableviews\Plugin\views\field; use Drupal\Core\Form\FormStateInterface; use Drupal\draggableviews\DraggableViews; use Drupal\system\Plugin\views\field\BulkForm; use Drupal\Core\Render\Markup; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Session\AccountInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a draggableviews form element. @@ -19,19 +18,79 @@ use Drupal\Core\Render\Markup; */ class DraggableViewsField extends BulkForm { + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * The action storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $actionStorage; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + /** + * The Current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * Sets the current_user service. + * + * @param \Drupal\Core\Session\AccountInterface $current_user + * Current user. + * + * @return $this + */ + public function setCurrentUser(AccountInterface $current_user) { + $this->currentUser = $current_user; + return $this; + } + /** * {@inheritdoc} */ - protected function defineOptions() { - $options = parent::defineOptions(); - return $options; + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + /** @var static $datasource */ + $bulk_form = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $bulk_form->setCurrentUser($container->get('current_user')); + return $bulk_form; } /** * {@inheritdoc} */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $form['draggableview_help'] = [ + '#markup' => $this->t("A draggable element will be added to the first table column. You do not have to set this field as the first column in your View."), + ]; parent::buildOptionsForm($form, $form_state); + // Remove all the fields that would break this or are completely ignored + // when rendering the drag interface. + unset($form['custom_label']); + unset($form['label']); + unset($form['element_label_colon']); + unset($form['action_title']); + unset($form['include_exclude']); + unset($form['selected_actions']); + unset($form['exclude']); + unset($form['alter']); + unset($form['empty_field_behavior']); + unset($form['empty']); + unset($form['empty_zero']); + unset($form['hide_empty']); + unset($form['hide_alter_empty']); } /** @@ -55,33 +114,33 @@ class DraggableViewsField extends BulkForm { $draggableviews = new DraggableViews($this->view); foreach ($this->view->result as $row_index => $row) { - $form[$this->options['id']][$row_index] = array( + $form[$this->options['id']][$row_index] = [ '#tree' => TRUE, - ); + ]; // Item to keep id of the entity. - $form[$this->options['id']][$row_index]['id'] = array( + $form[$this->options['id']][$row_index]['id'] = [ '#type' => 'hidden', - '#value' => $row->{$this->definition['entity field']}, - '#attributes' => array('class' => array('draggableviews-id')), - ); + '#value' => $this->getEntity($row)->id(), + '#attributes' => ['class' => ['draggableviews-id']], + ]; // Add parent. - $form[$this->options['id']][$row_index]['parent'] = array( + $form[$this->options['id']][$row_index]['parent'] = [ '#type' => 'hidden', '#default_value' => $draggableviews->getParent($row_index), - '#attributes' => array('class' => array('draggableviews-parent')), - ); + '#attributes' => ['class' => ['draggableviews-parent']], + ]; } - if (\Drupal::currentUser()->hasPermission('access draggableviews')) { + if ($this->currentUser->hasPermission('access draggableviews')) { $options = [ 'table_id' => $draggableviews->getHtmlId(), 'action' => 'match', - 'relationship' => 'parent', + 'relationship' => 'group', 'group' => 'draggableviews-parent', 'subgroup' => 'draggableviews-parent', - 'source' => 'draggableviews-id' + 'source' => 'draggableviews-id', ]; drupal_attach_tabledrag($form, $options); } diff --git a/web/modules/contrib/draggableviews/src/Plugin/views/field/DraggableViewsField.php.rej b/web/modules/contrib/draggableviews/src/Plugin/views/field/DraggableViewsField.php.rej new file mode 100644 index 000000000..61481c06d --- /dev/null +++ b/web/modules/contrib/draggableviews/src/Plugin/views/field/DraggableViewsField.php.rej @@ -0,0 +1,19 @@ +--- src/Plugin/views/field/DraggableViewsField.php ++++ src/Plugin/views/field/DraggableViewsField.php +@@ -63,14 +63,14 @@ class DraggableViewsField extends BulkForm { + $form[$this->options['id']][$row_index]['id'] = array( + '#type' => 'hidden', + '#value' => $row->{$this->definition['entity field']}, +- '#attributes' => array('class' => 'draggableviews-id'), ++ '#attributes' => array('class' => array('draggableviews-id')), + ); + + // Add parent. + $form[$this->options['id']][$row_index]['parent'] = array( + '#type' => 'hidden', + '#default_value' => $draggableviews->getParent($row_index), +- '#attributes' => array('class' => 'draggableviews-parent'), ++ '#attributes' => array('class' => array('draggableviews-parent')), + ); + } + diff --git a/web/modules/contrib/draggableviews/tests/src/Functional/DraggableviewsTest.php b/web/modules/contrib/draggableviews/tests/src/Functional/DraggableviewsTest.php new file mode 100644 index 000000000..ee3c1fd51 --- /dev/null +++ b/web/modules/contrib/draggableviews/tests/src/Functional/DraggableviewsTest.php @@ -0,0 +1,123 @@ +<?php + +namespace Drupal\Tests\draggableviews\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * Tests sortability of Draggableviewws. + * + * @group draggableviews + */ +class DraggableviewsTest extends BrowserTestBase { + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'node', + 'views', + 'draggableviews', + 'draggableviews_demo', + ]; + + /** + * The installation profile to use with this test. + * + * @var string + */ + protected $profile = 'minimal'; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + // Create users. + $this->adminUser = $this->drupalCreateUser([ + 'access administration pages', + 'view the administration theme', + 'administer permissions', + 'administer nodes', + 'administer content types', + 'access draggableviews', + ]); + $this->authUser = $this->drupalCreateUser([], 'authuser'); + + // Gather the test data. + $dataContent = $this->providerTestDataContent(); + + // Create nodes. + foreach ($dataContent as $datumContent) { + $node = $this->drupalCreateNode([ + 'type' => 'draggableviews_demo', + 'title' => $datumContent[0], + ]); + $node->save(); + } + } + + /** + * Data provider for setUp. + * + * @return array + * Nested array of testing data, Arranged like this: + * - Title + * - Body + */ + protected function providerTestDataContent() { + return [ + [ + 'Draggable Content 1', + 'Draggable Content Body 1', + ], + [ + 'Draggable Content 2', + 'Draggable Content Body 2', + ], + [ + 'Draggable Content 3', + 'Draggable Content Body 3', + ], + [ + 'Draggable Content 4', + 'Draggable Content Body 4', + ], + [ + 'Draggable Content 5', + 'Draggable Content Body 5', + ], + ]; + } + + /** + * A simple test. + */ + public function testDraggableviewsContent() { + $assert_session = $this->assertSession(); + + $this->drupalGet('draggableviews-demo'); + $this->assertSession()->statusCodeEquals(200); + // Verify that anonymous useres cannot access the order page. + $this->drupalGet('draggableviews-demo/order'); + $this->assertSession()->statusCodeEquals(403); + + // Verify that authorized user has access to display page. + $this->drupalLogin($this->adminUser); + $this->drupalGet('draggableviews-demo'); + $this->assertSession()->statusCodeEquals(200); + + // Verify that the page contains generated content. + $assert_session->pageTextContains(t('Draggable Content 4')); + + // Verify that authorized user has access to order page. + $this->drupalGet('draggableviews-demo/order'); + $this->assertSession()->statusCodeEquals(200); + + // Verify that the page contains generated content. + $assert_session->pageTextContains(t('Draggable Content 5')); + } + +} diff --git a/web/modules/contrib/entity/.travis.yml b/web/modules/contrib/entity/.travis.yml deleted file mode 100644 index e48d5d7d3..000000000 --- a/web/modules/contrib/entity/.travis.yml +++ /dev/null @@ -1,58 +0,0 @@ -language: php -sudo: false - -php: - - 5.5 - - 5.6 - - 7 - - hhvm - -matrix: - allow_failures: - # We cannot use hhvm-nightly since that does not work in Travis CI's old - # Ubuntu 12.04. - - php: hhvm - # Don't wait for the allowed failures to build. - fast_finish: true - -mysql: - database: entity - username: root - encoding: utf8 - -before_script: - # Remove Xdebug as we don't need it and it causes - # PHP Fatal error: Maximum function nesting level of '256' reached. - # We also don't care if that file exists or not on PHP 7. - - phpenv config-rm xdebug.ini || true - - # Remember the current entity test directory for later use in the Drupal - # installation. - - TESTDIR=$(pwd) - # Navigate out of module directory to prevent blown stack by recursive module - # lookup. - - cd .. - - # Create database. - - mysql -e 'create database entity' - # Export database variable for kernel tests. - - export SIMPLETEST_DB=mysql://root:@127.0.0.1/entity - # Download Drupal 8 core. - - travis_retry git clone --branch 8.5.x --depth 1 http://git.drupal.org/project/drupal.git - - cd drupal - - composer self-update - - composer install -n - - # Reference entity in build site. - - ln -s $TESTDIR modules/entity - - # Start a web server on port 8888, run in the background; wait for - # initialization. - - nohup php -S localhost:8888 > /dev/null 2>&1 & - - # Export web server URL for browser tests. - - export SIMPLETEST_BASE_URL=http://localhost:8888 - -script: - # Run the PHPUnit tests which also include the kernel tests. - - ./vendor/phpunit/phpunit/phpunit -c ./core/phpunit.xml.dist --verbose ./modules/entity diff --git a/web/modules/contrib/entity/README.txt b/web/modules/contrib/entity/README.txt deleted file mode 100644 index c19422cd3..000000000 --- a/web/modules/contrib/entity/README.txt +++ /dev/null @@ -1,11 +0,0 @@ -Entity API module ------------------ - -This module contains improvements and extensions to the Drupal 8 Entity system. -The goal is to bring useful improvements to upcoming Drupal 8 minor releases -(e.g. 8.1, 8.2, ..) while maintaining backwards compatibility in future Entity -API module versions (based on the improvements which went into core). - -@todo: Explain version compatibility pattern. -@todo: Add overview of all items and maintainers. - diff --git a/web/modules/contrib/entity/composer.json b/web/modules/contrib/entity/composer.json deleted file mode 100644 index 538e0e559..000000000 --- a/web/modules/contrib/entity/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "drupal/entity", - "type": "drupal-module", - "description": "Provides expanded entity APIs, which will be moved to Drupal core one day.", - "homepage": "http://drupal.org/project/entity", - "license": "GPL-2.0+", - "require": { - "drupal/core": "~8.5" - } -} diff --git a/web/modules/contrib/entity/config/schema/entity.schema.yml b/web/modules/contrib/entity/config/schema/entity.schema.yml deleted file mode 100644 index b3c192358..000000000 --- a/web/modules/contrib/entity/config/schema/entity.schema.yml +++ /dev/null @@ -1,11 +0,0 @@ -action.configuration.entity_delete_action:*: - type: action_configuration_default - label: 'Delete entity configuration' - -views.field.rendered_entity: - type: views_field - label: 'Rendered entity' - mapping: - view_mode: - type: string - label: 'View mode' diff --git a/web/modules/contrib/entity/entity.info.yml b/web/modules/contrib/entity/entity.info.yml deleted file mode 100644 index 54ca9636b..000000000 --- a/web/modules/contrib/entity/entity.info.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Entity -description: Provides expanded entity APIs, which will be moved to Drupal core one day. -type: module -# core: 8.x -dependencies: - - drupal:system (>=8.5.0) - -# Information added by Drupal.org packaging script on 2018-03-13 -version: '8.x-1.0-beta3' -core: '8.x' -project: 'entity' -datestamp: 1520958515 diff --git a/web/modules/contrib/entity/entity.links.action.yml b/web/modules/contrib/entity/entity.links.action.yml deleted file mode 100644 index 5e02edcd7..000000000 --- a/web/modules/contrib/entity/entity.links.action.yml +++ /dev/null @@ -1,2 +0,0 @@ -entity.entity_actions: - deriver: Drupal\entity\Plugin\Derivative\EntityActionsDeriver diff --git a/web/modules/contrib/entity/entity.links.task.yml b/web/modules/contrib/entity/entity.links.task.yml deleted file mode 100644 index 8116ae326..000000000 --- a/web/modules/contrib/entity/entity.links.task.yml +++ /dev/null @@ -1,3 +0,0 @@ -entity.revisions_overview: - deriver: 'Drupal\entity\Plugin\Derivative\RevisionsOverviewDeriver' - weight: 100 diff --git a/web/modules/contrib/entity/entity.module b/web/modules/contrib/entity/entity.module deleted file mode 100644 index c15e3f7ed..000000000 --- a/web/modules/contrib/entity/entity.module +++ /dev/null @@ -1,88 +0,0 @@ -<?php - -/** - * @file - * Provides expanded entity APIs. - */ - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\entity\BundlePlugin\BundlePluginHandler; - -/** - * Gets the entity types which use bundle plugins. - * - * @return \Drupal\Core\Entity\EntityTypeInterface[] - * The entity types. - */ -function entity_get_bundle_plugin_entity_types() { - $entity_types = \Drupal::entityTypeManager()->getDefinitions(); - $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) { - return $entity_type->hasHandlerClass('bundle_plugin'); - }); - - return $entity_types; -} - -/** - * Implements hook_entity_type_build(). - */ -function entity_entity_type_build(array &$entity_types) { - foreach ($entity_types as $entity_type) { - if ($entity_type->get('bundle_plugin_type')) { - $entity_type->setHandlerClass('bundle_plugin', BundlePluginHandler::class); - } - } -} - -/** - * Implements hook_entity_bundle_info(). - */ -function entity_entity_bundle_info() { - $bundles = []; - foreach (entity_get_bundle_plugin_entity_types() as $entity_type) { - /** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */ - $bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin'); - $bundles[$entity_type->id()] = $bundle_handler->getBundleInfo(); - } - return $bundles; -} - -/** - * Implements hook_entity_field_storage_info(). - */ -function entity_entity_field_storage_info(EntityTypeInterface $entity_type) { - if ($entity_type->hasHandlerClass('bundle_plugin')) { - /** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */ - $bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin'); - return $bundle_handler->getFieldStorageDefinitions(); - } -} - -/** - * Implements hook_entity_bundle_field_info(). - */ -function entity_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) { - if ($entity_type->hasHandlerClass('bundle_plugin')) { - /** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */ - $bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin'); - return $bundle_handler->getFieldDefinitions($bundle); - } -} - -/** - * Implements hook_modules_installed(). - */ -function entity_modules_installed($modules) { - foreach (entity_get_bundle_plugin_entity_types() as $entity_type) { - \Drupal::service('entity.bundle_plugin_installer')->installBundles($entity_type, $modules); - } -} - -/** - * Implements hook_module_preuninstall(). - */ -function entity_module_preuninstall($module) { - foreach (entity_get_bundle_plugin_entity_types() as $entity_type) { - \Drupal::service('entity.bundle_plugin_installer')->uninstallBundles($entity_type, [$module]); - } -} diff --git a/web/modules/contrib/entity/entity.permissions.yml b/web/modules/contrib/entity/entity.permissions.yml deleted file mode 100644 index 1676e880b..000000000 --- a/web/modules/contrib/entity/entity.permissions.yml +++ /dev/null @@ -1,2 +0,0 @@ -permission_callbacks: - - \Drupal\entity\EntityPermissions::buildPermissions diff --git a/web/modules/contrib/entity/entity.services.yml b/web/modules/contrib/entity/entity.services.yml deleted file mode 100644 index c4e5f83f5..000000000 --- a/web/modules/contrib/entity/entity.services.yml +++ /dev/null @@ -1,22 +0,0 @@ -services: - access_check.entity_delete_multiple: - class: Drupal\entity\Access\EntityDeleteMultipleAccessCheck - arguments: ['@entity_type.manager', '@tempstore.private', '@request_stack'] - tags: - - { name: access_check, applies_to: _entity_delete_multiple_access } - - access_checker.entity_revision: - class: \Drupal\entity\Access\EntityRevisionRouteAccessChecker - arguments: ['@entity_type.manager', '@current_route_match'] - tags: - - { name: access_check, applies_to: _entity_access_revision } - - entity.bundle_plugin_installer: - class: Drupal\entity\BundlePlugin\BundlePluginInstaller - arguments: ['@entity_type.manager', '@entity_bundle.listener', '@field_storage_definition.listener', '@field_definition.listener'] - - entity.bundle_plugin.uninstall_validator: - class: \Drupal\entity\BundlePlugin\BundlePluginUninstallValidator - tags: - - { name: module_install.uninstall_validator } - arguments: ['@entity_type.manager', '@string_translation'] diff --git a/web/modules/contrib/entity/entity.views.inc b/web/modules/contrib/entity/entity.views.inc deleted file mode 100644 index 3074175c6..000000000 --- a/web/modules/contrib/entity/entity.views.inc +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityTypeInterface; - -/** - * Implements hook_views_data(). - */ -function entity_views_data() { - $entity_types = \Drupal::entityTypeManager()->getDefinitions(); - $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) { - return $entity_type->entityClassImplements(ContentEntityInterface::class); - }); - - $data = []; - foreach ($entity_types as $entity_type) { - /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ - $base_table = $entity_type->getBaseTable() ?: $entity_type->id(); - - if ($entity_type->hasViewBuilderClass()) { - $data[$base_table]['rendered_entity'] = [ - 'field' => [ - 'title' => t('Rendered entity'), - 'help' => t('Renders an entity in a view mode.'), - 'id' => 'rendered_entity', - ], - ]; - } - } - - return $data; -} diff --git a/web/modules/contrib/entity/src/Access/EntityDeleteMultipleAccessCheck.php b/web/modules/contrib/entity/src/Access/EntityDeleteMultipleAccessCheck.php deleted file mode 100644 index a04cda68c..000000000 --- a/web/modules/contrib/entity/src/Access/EntityDeleteMultipleAccessCheck.php +++ /dev/null @@ -1,87 +0,0 @@ -<?php - -namespace Drupal\entity\Access; - -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Routing\Access\AccessInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\TempStore\PrivateTempStoreFactory; -use Symfony\Component\HttpFoundation\RequestStack; - -/** - * Checks if the current user has delete access to the items of the tempstore. - */ -class EntityDeleteMultipleAccessCheck implements AccessInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityManagerInterface - */ - protected $entityTypeManager; - - /** - * The tempstore service. - * - * @var \Drupal\Core\TempStore\PrivateTempStoreFactory - */ - protected $tempStore; - - /** - * Request stack service. - * - * @var \Symfony\Component\HttpFoundation\RequestStack - */ - protected $requestStack; - - /** - * Constructs a new EntityDeleteMultipleAccessCheck. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory - * The tempstore service. - * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack - * The request stack service. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, RequestStack $request_stack) { - $this->entityTypeManager = $entity_type_manager; - $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm'); - $this->requestStack = $request_stack; - } - - /** - * Checks if the user has delete access for at least one item of the store. - * - * @param \Drupal\Core\Session\AccountInterface $account - * Run access checks for this account. - * @param string $entity_type_id - * Entity type ID. - * - * @return \Drupal\Core\Access\AccessResult - * Allowed or forbidden, neutral if tempstore is empty. - */ - public function access(AccountInterface $account, $entity_type_id) { - if (!$this->requestStack->getCurrentRequest()->getSession()) { - return AccessResult::neutral(); - } - $selection = $this->tempStore->get($account->id() . ':' . $entity_type_id); - if (empty($selection) || !is_array($selection)) { - return AccessResult::neutral(); - } - - $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_keys($selection)); - foreach ($entities as $entity) { - // As long as the user has access to delete one entity allow access to the - // delete form. Access will be checked again in - // Drupal\Core\Entity\Form\DeleteMultipleForm::submit() in case it has - // changed in the meantime. - if ($entity->access('delete', $account)) { - return AccessResult::allowed(); - } - } - return AccessResult::forbidden(); - } - -} diff --git a/web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php b/web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php deleted file mode 100644 index 84f431707..000000000 --- a/web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php +++ /dev/null @@ -1,162 +0,0 @@ -<?php - -namespace Drupal\entity\Access; - -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Routing\Access\AccessInterface; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Session\AccountInterface; -use Symfony\Component\Routing\Route; - -/** - * Checks access to a entity revision. - */ -class EntityRevisionRouteAccessChecker implements AccessInterface { - - /** - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Stores calculated access check results. - * - * @var array - */ - protected $accessCache = array(); - - /** - * The currently active route match object. - * - * @var \Drupal\Core\Routing\RouteMatchInterface - */ - protected $routeMatch; - - /** - * Creates a new EntityRevisionRouteAccessChecker instance. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity manager. - * @param \Drupal\Core\Routing\RouteMatchInterface $route_match - * The currently active route match object. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match) { - $this->entityTypeManager = $entity_type_manager; - $this->routeMatch = $route_match; - } - - /** - * {@inheritdoc} - */ - public function access(Route $route, AccountInterface $account, RouteMatchInterface $route_match = NULL) { - if (empty($route_match)) { - $route_match = $this->routeMatch; - } - - $operation = $route->getRequirement('_entity_access_revision'); - list($entity_type_id, $operation) = explode('.', $operation, 2); - - if ($operation === 'list') { - $_entity = $route_match->getParameter($entity_type_id); - return AccessResult::allowedIf($this->checkAccess($_entity, $account, $operation))->cachePerPermissions(); - } - else { - $_entity_revision = $route_match->getParameter($entity_type_id . '_revision'); - return AccessResult::allowedIf($_entity_revision && $this->checkAccess($_entity_revision, $account, $operation))->cachePerPermissions(); - } - } - - protected function checkAccess(ContentEntityInterface $entity, AccountInterface $account, $operation = 'view') { - $entity_type = $entity->getEntityType(); - $entity_type_id = $entity->getEntityTypeId(); - $entity_access = $this->entityTypeManager->getAccessControlHandler($entity_type_id); - - /** @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */ - $entity_storage = $this->entityTypeManager->getStorage($entity_type_id); - - $map = [ - 'view' => "view all $entity_type_id revisions", - 'list' => "view all $entity_type_id revisions", - 'update' => "revert all $entity_type_id revisions", - 'delete' => "delete all $entity_type_id revisions", - ]; - $bundle = $entity->bundle(); - $type_map = [ - 'view' => "view $entity_type_id $bundle revisions", - 'list' => "view $entity_type_id $bundle revisions", - 'update' => "revert $entity_type_id $bundle revisions", - 'delete' => "delete $entity_type_id $bundle revisions", - ]; - - if (!$entity || !isset($map[$operation]) || !isset($type_map[$operation])) { - // If there was no node to check against, or the $op was not one of the - // supported ones, we return access denied. - return FALSE; - } - - // Statically cache access by revision ID, language code, user account ID, - // and operation. - $langcode = $entity->language()->getId(); - $cid = $entity->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $operation; - - if (!isset($this->accessCache[$cid])) { - $admin_permission = $entity_type->getAdminPermission(); - - // Perform basic permission checks first. - if (!$account->hasPermission($map[$operation]) && !$account->hasPermission($type_map[$operation]) && ($admin_permission && !$account->hasPermission($admin_permission))) { - $this->accessCache[$cid] = FALSE; - return FALSE; - } - - if (($admin_permission = $entity_type->getAdminPermission()) && $account->hasPermission($admin_permission)) { - $this->accessCache[$cid] = TRUE; - } - else { - // Entity access handlers are generally not aware of the "list" operation. - $operation = $operation == 'list' ? 'view' : $operation; - // First check the access to the default revision and finally, if the - // node passed in is not the default revision then access to that, too. - $this->accessCache[$cid] = $entity_access->access($entity_storage->load($entity->id()), $operation, $account) && ($entity->isDefaultRevision() || $entity_access->access($entity, $operation, $account)); - } - } - - return $this->accessCache[$cid]; - } - - - /** - * Counts the number of revisions in the default language. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The entity. - * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage - * The entity storage. - * - * @return int - * The number of revisions in the default language. - */ - protected function countDefaultLanguageRevisions(ContentEntityInterface $entity, EntityStorageInterface $entity_storage) { - $entity_type = $entity->getEntityType(); - $count = $entity_storage->getQuery() - ->allRevisions() - ->condition($entity_type->getKey('id'), $entity->id()) - ->condition($entity_type->getKey('default_langcode'), 1) - ->count() - ->execute(); - return $count; - } - - /** - * Resets the access cache. - * - * @return $this - */ - public function resetAccessCache() { - $this->accessCache = []; - return $this; - } - -} diff --git a/web/modules/contrib/entity/src/BundleFieldDefinition.php b/web/modules/contrib/entity/src/BundleFieldDefinition.php deleted file mode 100644 index 9da74c87b..000000000 --- a/web/modules/contrib/entity/src/BundleFieldDefinition.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\Field\BaseFieldDefinition; - -/** - * Provides a field definition class for bundle fields. - * - * Core currently doesn't provide one, the hook_entity_bundle_field_info() - * example uses BaseFieldDefinition, which is wrong. Tracked in #2346347. - * - * Note that this class implements both FieldStorageDefinitionInterface and - * FieldDefinitionInterface. This is a simplification for DX reasons, - * allowing code to return just the bundle definitions instead of having to - * return both storage definitions and bundle definitions. - */ -class BundleFieldDefinition extends BaseFieldDefinition { - - /** - * {@inheritdoc} - */ - public function isBaseField() { - return FALSE; - } - -} diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandler.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandler.php deleted file mode 100644 index e92233263..000000000 --- a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandler.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php - -namespace Drupal\entity\BundlePlugin; - -use Drupal\Component\Plugin\PluginManagerInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -class BundlePluginHandler implements BundlePluginHandlerInterface { - - /** - * The entity type. - * - * @var \Drupal\Core\Entity\EntityTypeInterface - */ - protected $entityType; - - /** - * The bundle plugin manager. - * - * @var \Drupal\Component\Plugin\PluginManagerInterface - */ - protected $pluginManager; - - /** - * Constructs a new BundlePluginHandler object. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager - * The bundle plugin manager. - */ - public function __construct(EntityTypeInterface $entity_type, PluginManagerInterface $plugin_manager) { - $this->entityType = $entity_type; - $this->pluginManager = $plugin_manager; - } - - /** - * {@inheritdoc} - */ - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( - $entity_type, - $container->get('plugin.manager.' . $entity_type->get('bundle_plugin_type')) - ); - } - - /** - * {@inheritdoc} - */ - public function getBundleInfo() { - $bundles = []; - foreach ($this->pluginManager->getDefinitions() as $plugin_id => $definition) { - $bundles[$plugin_id] = [ - 'label' => $definition['label'], - 'description' => isset($definition['description']) ? $definition['description'] : '', - 'translatable' => $this->entityType->isTranslatable(), - 'provider' => $definition['provider'], - ]; - } - return $bundles; - } - - /** - * {@inheritdoc} - */ - public function getFieldStorageDefinitions() { - $definitions = []; - foreach (array_keys($this->pluginManager->getDefinitions()) as $plugin_id) { - /** @var \Drupal\entity\BundlePlugin\BundlePluginInterface $plugin */ - $plugin = $this->pluginManager->createInstance($plugin_id); - $definitions += $plugin->buildFieldDefinitions(); - } - // Ensure the presence of required keys which aren't set by the plugin. - foreach ($definitions as $field_name => $definition) { - $definition->setName($field_name); - $definition->setTargetEntityTypeId($this->entityType->id()); - $definitions[$field_name] = $definition; - } - - return $definitions; - } - - /** - * {@inheritdoc} - */ - public function getFieldDefinitions($bundle) { - /** @var \Drupal\entity\BundlePlugin\BundlePluginInterface $plugin */ - $plugin = $this->pluginManager->createInstance($bundle); - $definitions = $plugin->buildFieldDefinitions(); - // Ensure the presence of required keys which aren't set by the plugin. - foreach ($definitions as $field_name => $definition) { - $definition->setName($field_name); - $definition->setTargetEntityTypeId($this->entityType->id()); - $definition->setTargetBundle($bundle); - $definitions[$field_name] = $definition; - } - - return $definitions; - } - -} diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php deleted file mode 100644 index 5702fb58c..000000000 --- a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php - -namespace Drupal\entity\BundlePlugin; - -use Drupal\Core\Entity\EntityHandlerInterface; - -/** - * Handles plugin-provided bundles. - */ -interface BundlePluginHandlerInterface extends EntityHandlerInterface { - - /** - * Gets the bundle info. - * - * @return array - * An array of bundle information keyed by the bundle name. - * The format expected by hook_entity_bundle_info(). - */ - public function getBundleInfo(); - - /** - * Gets the field storage definitions. - */ - public function getFieldStorageDefinitions(); - - /** - * Gets the field definitions for a specific bundle. - * - * @param string $bundle - * The bundle name. - * - * @return \Drupal\entity\BundleFieldDefinition[] - * An array of bundle field definitions, keyed by field name. - */ - public function getFieldDefinitions($bundle); - -} diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstaller.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstaller.php deleted file mode 100644 index 555407fc6..000000000 --- a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstaller.php +++ /dev/null @@ -1,94 +0,0 @@ -<?php - -namespace Drupal\entity\BundlePlugin; - -use Drupal\Core\Entity\EntityBundleListenerInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Field\FieldDefinitionListenerInterface; -use Drupal\Core\Field\FieldStorageDefinitionListenerInterface; - -class BundlePluginInstaller implements BundlePluginInstallerInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * The entity bundle listener. - * - * @var \Drupal\Core\Entity\EntityBundleListenerInterface - */ - protected $entityBundleListener; - - /** - * The field storage definition listener. - * - * @var \Drupal\Core\Field\FieldStorageDefinitionListenerInterface - */ - protected $fieldStorageDefinitionListener; - - /** - * The field definition listener. - * - * @var \Drupal\Core\Field\FieldDefinitionListenerInterface - */ - protected $fieldDefinitionListener; - - /** - * Constructs a new BundlePluginInstaller object. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\Entity\EntityBundleListenerInterface $entity_bundle_listener - * The entity bundle listener. - * @param \Drupal\Core\Field\FieldStorageDefinitionListenerInterface $field_storage_definition_listener - * The field storage definition listener. - * @param \Drupal\Core\Field\FieldDefinitionListenerInterface $field_definition_listener - * The field definition listener. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityBundleListenerInterface $entity_bundle_listener, FieldStorageDefinitionListenerInterface $field_storage_definition_listener, FieldDefinitionListenerInterface $field_definition_listener) { - $this->entityTypeManager = $entity_type_manager; - $this->entityBundleListener = $entity_bundle_listener; - $this->fieldStorageDefinitionListener = $field_storage_definition_listener; - $this->fieldDefinitionListener = $field_definition_listener; - } - - /** - * {@inheritdoc} - */ - public function installBundles(EntityTypeInterface $entity_type, array $modules) { - $bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin'); - $bundles = array_filter($bundle_handler->getBundleInfo(), function ($bundle_info) use ($modules) { - return in_array($bundle_info['provider'], $modules, TRUE); - }); - foreach (array_keys($bundles) as $bundle) { - $this->entityBundleListener->onBundleCreate($bundle, $entity_type->id()); - foreach ($bundle_handler->getFieldDefinitions($bundle) as $definition) { - $this->fieldStorageDefinitionListener->onFieldStorageDefinitionCreate($definition); - $this->fieldDefinitionListener->onFieldDefinitionCreate($definition); - } - } - } - - /** - * {@inheritdoc} - */ - public function uninstallBundles(EntityTypeInterface $entity_type, array $modules) { - $bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin'); - $bundles = array_filter($bundle_handler->getBundleInfo(), function ($bundle_info) use ($modules) { - return in_array($bundle_info['provider'], $modules, TRUE); - }); - foreach (array_keys($bundles) as $bundle) { - $this->entityBundleListener->onBundleDelete($bundle, $entity_type->id()); - foreach ($bundle_handler->getFieldDefinitions($bundle) as $definition) { - $this->fieldDefinitionListener->onFieldDefinitionDelete($definition); - $this->fieldStorageDefinitionListener->onFieldStorageDefinitionDelete($definition); - } - } - } - -} diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php deleted file mode 100644 index afeacee48..000000000 --- a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace Drupal\entity\BundlePlugin; - -use Drupal\Core\Entity\EntityTypeInterface; - -/** - * Installs and uninstalls bundle plugins. - * - * Ensures that the fields provided by the bundle plugins are created/deleted. - */ -interface BundlePluginInstallerInterface { - - /** - * Installs the bundle plugins provided by the specified modules. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * @param array $modules - * The modules. - */ - public function installBundles(EntityTypeInterface $entity_type, array $modules); - - /** - * Uninstalls the bundle plugins provided by the specified modules. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * @param array $modules - * The modules. - */ - public function uninstallBundles(EntityTypeInterface $entity_type, array $modules); - -} diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInterface.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInterface.php deleted file mode 100644 index b79e65f40..000000000 --- a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInterface.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php - -namespace Drupal\entity\BundlePlugin; - -use Drupal\Component\Plugin\PluginInspectionInterface; - -/** - * Interface for plugins which act as entity bundles. - */ -interface BundlePluginInterface extends PluginInspectionInterface { - - /** - * Builds the field definitions for entities of this bundle. - * - * Important: - * Field names must be unique across all bundles. - * It is recommended to prefix them with the bundle name (plugin ID). - * - * @return \Drupal\entity\BundleFieldDefinition[] - * An array of bundle field definitions, keyed by field name. - */ - public function buildFieldDefinitions(); - -} diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginUninstallValidator.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginUninstallValidator.php deleted file mode 100644 index 09531df84..000000000 --- a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginUninstallValidator.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php - -namespace Drupal\entity\BundlePlugin; - -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Extension\ModuleUninstallValidatorInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\Core\StringTranslation\TranslationInterface; - -/** - * Prevents uninstalling modules with bundle plugins in case of found data. - */ -class BundlePluginUninstallValidator implements ModuleUninstallValidatorInterface { - - use StringTranslationTrait; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs the object. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation - * The string translation service. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) { - $this->entityTypeManager = $entity_type_manager; - $this->stringTranslation = $string_translation; - } - - /** - * {@inheritdoc} - */ - public function validate($module) { - $reasons = []; - - foreach (entity_get_bundle_plugin_entity_types() as $entity_type) { - /** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */ - $bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin'); - $bundles = $bundle_handler->getBundleInfo(); - - // We find all bundles which have to be removed due to the uninstallation. - $bundles_filtered_by_module = array_filter($bundles, function ($bundle_info) use ($module) { - return $module === $bundle_info['provider']; - }); - - if (!empty($bundles_filtered_by_module)) { - $bundle_keys_with_content = array_filter(array_keys($bundles_filtered_by_module), function ($bundle) use ($entity_type) { - $result = $this->entityTypeManager->getStorage($entity_type->id())->getQuery() - ->condition($entity_type->getKey('bundle'), $bundle) - ->range(0, 1) - ->execute(); - return !empty($result); - }); - - $bundles_with_content = array_intersect_key($bundles_filtered_by_module, array_flip($bundle_keys_with_content)); - - foreach ($bundles_with_content as $bundle) { - $reasons[] = $this->t('There is data for the bundle @bundle on the entity type @entity_type. Please remove all content before uninstalling the module.', [ - '@bundle' => $bundle['label'], - '@entity_type' => $entity_type->getLabel(), - ]); - } - } - } - - return $reasons; - } - -} diff --git a/web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php b/web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php deleted file mode 100644 index 6be025a6e..000000000 --- a/web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php +++ /dev/null @@ -1,192 +0,0 @@ -<?php - -namespace Drupal\entity\Controller; - -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Entity\EntityInterface; - -/** - * Defines a trait for common revision UI functionality. - */ -trait RevisionControllerTrait { - - /** - * Returns the entity type manager. - * - * @return \Drupal\Core\Entity\EntityTypeManagerInterface - */ - abstract protected function entityTypeManager(); - - /** - * Returns the langauge manager. - * - * @return \Drupal\Core\Language\LanguageManagerInterface - */ - abstract public function languageManager(); - - /** - * Determines if the user has permission to revert revisions. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to check revert access for. - * - * @return bool - * TRUE if the user has revert access. - */ - abstract protected function hasRevertRevisionAccess(EntityInterface $entity); - - /** - * Determines if the user has permission to delete revisions. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to check delete revision access for. - * - * @return bool - * TRUE if the user has delete revision access. - */ - abstract protected function hasDeleteRevisionAccess(EntityInterface $entity); - - /** - * Builds a link to revert an entity revision. - * - * @param \Drupal\Core\Entity\EntityInterface $entity_revision - * The entity to build a revert revision link for. - * - * @return array - * A link render array. - */ - abstract protected function buildRevertRevisionLink(EntityInterface $entity_revision); - - /** - * Builds a link to delete an entity revision. - * - * @param \Drupal\Core\Entity\EntityInterface $entity_revision - * The entity to build a delete revision link for. - * - * @return array - * A link render array. - */ - abstract protected function buildDeleteRevisionLink(EntityInterface $entity_revision); - - /** - * Returns a string providing details of the revision. - * - * E.g. Node describes its revisions using {date} by {username}. For the - * non-current revision, it also provides a link to view that revision. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $revision - * The entity revision. - * @param bool $is_current - * TRUE if the revision is the current revision. - * - * @return string - * Returns a string to provide the details of the revision. - */ - abstract protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE); - - /** - * Loads all revision IDs of an entity sorted by revision ID descending. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The entity. - * - * @return mixed[] - */ - protected function revisionIds(ContentEntityInterface $entity) { - $entity_type = $entity->getEntityType(); - $result = $this->entityTypeManager()->getStorage($entity_type->id())->getQuery() - ->allRevisions() - ->condition($entity_type->getKey('id'), $entity->id()) - ->sort($entity_type->getKey('revision'), 'DESC') - ->execute(); - return array_keys($result); - } - - /** - * Generates an overview table of older revisions of an entity. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * An entity object. - * - * @return array - * A render array. - */ - protected function revisionOverview(ContentEntityInterface $entity) { - $langcode = $this->languageManager() - ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) - ->getId(); - /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $entity_storage */ - $entity_storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId()); - $revision_ids = $this->revisionIds($entity); - $entity_revisions = $entity_storage->loadMultipleRevisions($revision_ids); - - $header = [$this->t('Revision'), $this->t('Operations')]; - $rows = []; - foreach ($entity_revisions as $revision) { - $row = []; - /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ - if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) { - $row[] = $this->getRevisionDescription($revision, $revision->isDefaultRevision()); - - if ($revision->isDefaultRevision()) { - $row[] = [ - 'data' => [ - '#prefix' => '<em>', - '#markup' => $this->t('Current revision'), - '#suffix' => '</em>', - ], - ]; - foreach ($row as &$current) { - $current['class'] = ['revision-current']; - } - } - else { - $links = $this->getOperationLinks($revision); - $row[] = [ - 'data' => [ - '#type' => 'operations', - '#links' => $links, - ], - ]; - } - } - - $rows[] = $row; - } - - $build[$entity->getEntityTypeId() . '_revisions_table'] = [ - '#theme' => 'table', - '#rows' => $rows, - '#header' => $header, - ]; - - // We have no clue about caching yet. - $build['#cache']['max-age'] = 0; - - return $build; - } - - /** - * Get the links of the operations for an entity revision. - * - * @param \Drupal\Core\Entity\EntityInterface $entity_revision - * The entity to build the revision links for. - * - * @return array - * The operation links. - */ - protected function getOperationLinks(EntityInterface $entity_revision) { - $links = []; - if ($this->hasRevertRevisionAccess($entity_revision)) { - $links['revert'] = $this->buildRevertRevisionLink($entity_revision); - } - - if ($this->hasDeleteRevisionAccess($entity_revision)) { - $links['delete'] = $this->buildDeleteRevisionLink($entity_revision); - } - - return array_filter($links); - } - -} diff --git a/web/modules/contrib/entity/src/Controller/RevisionOverviewController.php b/web/modules/contrib/entity/src/Controller/RevisionOverviewController.php deleted file mode 100644 index 495b6a040..000000000 --- a/web/modules/contrib/entity/src/Controller/RevisionOverviewController.php +++ /dev/null @@ -1,160 +0,0 @@ -<?php - -namespace Drupal\entity\Controller; - -use Drupal\Component\Utility\Xss; -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Controller\ControllerBase; -use Drupal\Core\Datetime\DateFormatterInterface; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Render\RendererInterface; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Entity\RevisionLogInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Provides a controller which shows the revision history. - * - * This controller leverages the revision controller trait, which is agnostic to - * any entity type, by using \Drupal\Core\Entity\RevisionLogInterface. - */ -class RevisionOverviewController extends ControllerBase { - - use RevisionControllerTrait; - - /** - * The date formatter. - * - * @var \Drupal\Core\Datetime\DateFormatterInterface - */ - protected $dateFormatter; - - /** - * The renderer. - * - * @var \Drupal\Core\Render\RendererInterface - */ - protected $renderer; - - /** - * Creates a new RevisionOverviewController instance. - * - * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter - * The date formatter. - */ - public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer) { - $this->dateFormatter = $date_formatter; - $this->renderer = $renderer; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static($container->get('date.formatter'), $container->get('renderer')); - } - - /** - * {@inheritdoc} - */ - protected function hasDeleteRevisionAccess(EntityInterface $entity) { - return $this->currentUser()->hasPermission("delete all {$entity->id()} revisions"); - } - - /** - * {@inheritdoc} - */ - protected function buildRevertRevisionLink(EntityInterface $entity_revision) { - if ($entity_revision->hasLinkTemplate('revision-revert-form')) { - return [ - 'title' => t('Revert'), - 'url' => $entity_revision->toUrl('revision-revert-form'), - ]; - } - } - - /** - * {@inheritdoc} - */ - protected function buildDeleteRevisionLink(EntityInterface $entity_revision) { - if ($entity_revision->hasLinkTemplate('revision-delete-form')) { - return [ - 'title' => t('Delete'), - 'url' => $entity_revision->toUrl('revision-delete-form'), - ]; - } - } - - /** - * Generates an overview table of older revisions of an entity. - * - * @param \Drupal\Core\Routing\RouteMatchInterface $route_match - * The route match. - * - * @return array - * A render array. - */ - public function revisionOverviewController(RouteMatchInterface $route_match) { - return $this->revisionOverview($route_match->getParameter($route_match->getRouteObject()->getOption('entity_type_id'))); - } - - /** - * {@inheritdoc} - */ - protected function getRevisionDescription(ContentEntityInterface $revision, $is_default = FALSE) { - /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\user\EntityOwnerInterface|\Drupal\Core\Entity\RevisionLogInterface $revision */ - if ($revision instanceof RevisionLogInterface) { - // Use revision link to link to revisions that are not active. - $date = $this->dateFormatter->format($revision->getRevisionCreationTime(), 'short'); - $link = $revision->toLink($date, 'revision'); - - // @todo: Simplify this when https://www.drupal.org/node/2334319 lands. - $username = [ - '#theme' => 'username', - '#account' => $revision->getRevisionUser(), - ]; - $username = $this->renderer->render($username); - } - else { - $link = $revision->toLink($revision->label(), 'revision'); - $username = ''; - - } - - $markup = ''; - if ($revision instanceof RevisionLogInterface) { - $markup = $revision->getRevisionLogMessage(); - } - - if ($username) { - $template = '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}'; - } - else { - $template = '{% trans %} {{ date }} {% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}'; - } - - $column = [ - 'data' => [ - '#type' => 'inline_template', - '#template' => $template, - '#context' => [ - 'date' => $link->toString(), - 'username' => $username, - 'message' => ['#markup' => $markup, '#allowed_tags' => Xss::getHtmlTagList()], - ], - ], - ]; - return $column; - } - - /** - * {@inheritdoc} - */ - protected function hasRevertRevisionAccess(EntityInterface $entity) { - return AccessResult::allowedIfHasPermission($this->currentUser(), "revert all {$entity->getEntityTypeId()} revisions")->orIf( - AccessResult::allowedIfHasPermission($this->currentUser(), "revert {$entity->bundle()} {$entity->getEntityTypeId()} revisions") - ); - } - -} diff --git a/web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php b/web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php deleted file mode 100644 index 18d392a06..000000000 --- a/web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -<?php - -namespace Drupal\entity\Entity; - -use Drupal\Core\Entity\RevisionableEntityBundleInterface as CoreRevisionableEntityBundleInterface; - -@trigger_error('\Drupal\entity\Entity\RevisionableEntityBundleInterface has been deprecated in favor of \Drupal\Core\Entity\RevisionableEntityBundleInterface. Use that instead.'); - -/** - * @deprecated in favor of - * \Drupal\Core\Entity\RevisionableEntityBundleInterface. Use that instead. - */ -interface RevisionableEntityBundleInterface extends CoreRevisionableEntityBundleInterface { -} diff --git a/web/modules/contrib/entity/src/EntityAccessControlHandler.php b/web/modules/contrib/entity/src/EntityAccessControlHandler.php deleted file mode 100644 index 36dc4a9cc..000000000 --- a/web/modules/contrib/entity/src/EntityAccessControlHandler.php +++ /dev/null @@ -1,152 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityPublishedInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\user\EntityOwnerInterface; - -/** - * Controls access based on the generic entity permissions. - * - * @see \Drupal\entity\UncacheableEntityPermissionProvider - */ -class EntityAccessControlHandler extends CoreEntityAccessControlHandler { - - /** - * {@inheritdoc} - */ - public function __construct(EntityTypeInterface $entity_type) { - parent::__construct($entity_type); - - if (!$entity_type->hasHandlerClass('permission_provider') || !is_a($entity_type->getHandlerClass('permission_provider'), EntityPermissionProvider::class, TRUE)) { - throw new \Exception("This entity access control handler requires the entity permissions provider: {EntityPermissionProvider::class}"); - } - } - - - /** - * {@inheritdoc} - */ - protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { - $account = $this->prepareUser($account); - /** @var \Drupal\Core\Access\AccessResult $result */ - $result = parent::checkAccess($entity, $operation, $account); - - if ($result->isNeutral()) { - if ($entity instanceof EntityOwnerInterface) { - $result = $this->checkEntityOwnerPermissions($entity, $operation, $account); - } - else { - $result = $this->checkEntityPermissions($entity, $operation, $account); - } - } - - // Ensure that access is evaluated again when the entity changes. - return $result->addCacheableDependency($entity); - } - - /** - * Checks the entity operation and bundle permissions. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for which to check access. - * @param string $operation - * The entity operation. Usually one of 'view', 'view label', 'update' or - * 'delete'. - * @param \Drupal\Core\Session\AccountInterface $account - * The user for which to check access. - * - * @return \Drupal\Core\Access\AccessResultInterface - * The access result. - */ - protected function checkEntityPermissions(EntityInterface $entity, $operation, AccountInterface $account) { - if ($operation === 'view') { - $permissions = [ - "view {$entity->getEntityTypeId()}" - ]; - } - else { - $permissions = [ - "$operation {$entity->getEntityTypeId()}", - "$operation {$entity->bundle()} {$entity->getEntityTypeId()}", - ]; - } - return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR'); - } - - /** - * Checks the entity operation and bundle permissions, with owners. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for which to check access. - * @param string $operation - * The entity operation. Usually one of 'view', 'view label', 'update' or - * 'delete'. - * @param \Drupal\Core\Session\AccountInterface $account - * The user for which to check access. - * - * @return \Drupal\Core\Access\AccessResultInterface - * The access result. - */ - protected function checkEntityOwnerPermissions(EntityInterface $entity, $operation, AccountInterface $account) { - if ($operation === 'view') { - if ($entity instanceof EntityPublishedInterface && !$entity->isPublished()) { - if (($account->id() == $entity->getOwnerId())) { - $permissions = [ - "view own unpublished {$entity->getEntityTypeId()}", - ]; - return AccessResult::allowedIfHasPermissions($account, $permissions)->cachePerUser(); - } - return AccessResult::neutral()->cachePerUser(); - } - else { - return AccessResult::allowedIfHasPermissions($account, [ - "view {$entity->getEntityTypeId()}", - ]); - } - } - else { - if (($account->id() == $entity->getOwnerId())) { - $result = AccessResult::allowedIfHasPermissions($account, [ - "$operation own {$entity->getEntityTypeId()}", - "$operation any {$entity->getEntityTypeId()}", - "$operation own {$entity->bundle()} {$entity->getEntityTypeId()}", - "$operation any {$entity->bundle()} {$entity->getEntityTypeId()}", - ], 'OR'); - } - else { - $result = AccessResult::allowedIfHasPermissions($account, [ - "$operation any {$entity->getEntityTypeId()}", - "$operation any {$entity->bundle()} {$entity->getEntityTypeId()}", - ], 'OR'); - } - return $result; - } - } - - /** - * {@inheritdoc} - */ - protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - $result = parent::checkCreateAccess($account, $context, $entity_bundle); - if ($result->isNeutral()) { - $permissions = [ - 'administer ' . $this->entityTypeId, - 'create ' . $this->entityTypeId, - ]; - if ($entity_bundle) { - $permissions[] = 'create ' . $entity_bundle . ' ' . $this->entityTypeId; - } - - $result = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR'); - } - - return $result; - } - -} diff --git a/web/modules/contrib/entity/src/EntityPermissionProvider.php b/web/modules/contrib/entity/src/EntityPermissionProvider.php deleted file mode 100644 index 88c06ee10..000000000 --- a/web/modules/contrib/entity/src/EntityPermissionProvider.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\Entity\EntityTypeInterface; - -/** - * Provides generic entity permissions which are still cacheable. - * - * This includes: - * - * - administer $entity_type - * - access $entity_type overview - * - view $entity_type - * - view own unpublished $entity_type - * - update (own|any) ($bundle) $entity_type - * - delete (own|any) ($bundle) $entity_type - * - create $bundle $entity_type - * - * This class does not support "view own ($bundle) $entity_type", because this - * results in caching per user. If you need this use case, please use - * \Drupal\entity\UncacheableEntityPermissionProvider instead. - * - * Intended for content entity types, since config entity types usually rely - * on a single "administer" permission. - * Example annotation: - * @code - * handlers = { - * "access" = "Drupal\entity\EntityAccessControlHandler", - * "permission_provider" = "Drupal\entity\EntityPermissionProvider", - * } - * @endcode - * - * @see \Drupal\entity\EntityAccessControlHandler - * @see \Drupal\entity\EntityPermissions - */ -class EntityPermissionProvider extends EntityPermissionProviderBase { - - /** - * {@inheritdoc} - */ - public function buildPermissions(EntityTypeInterface $entity_type) { - $entity_type_id = $entity_type->id(); - $plural_label = $entity_type->getPluralLabel(); - - $permissions = parent::buildPermissions($entity_type); - - // View permissions are the same for both granularities. - $permissions["view {$entity_type_id}"] = [ - 'title' => $this->t('View @type', [ - '@type' => $plural_label, - ]), - ]; - - return $this->processPermissions($permissions, $entity_type); - } - - -} diff --git a/web/modules/contrib/entity/src/EntityPermissionProviderBase.php b/web/modules/contrib/entity/src/EntityPermissionProviderBase.php deleted file mode 100644 index 53d83f8ac..000000000 --- a/web/modules/contrib/entity/src/EntityPermissionProviderBase.php +++ /dev/null @@ -1,231 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\Entity\EntityHandlerInterface; -use Drupal\Core\Entity\EntityPublishedInterface; -use Drupal\Core\Entity\EntityTypeBundleInfoInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\user\EntityOwnerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * @internal - */ -class EntityPermissionProviderBase implements EntityPermissionProviderInterface, EntityHandlerInterface { - - use StringTranslationTrait; - - /** - * The entity type bundle info. - * - * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface - */ - protected $entityTypeBundleInfo; - - /** - * Constructs a new EntityPermissionProvider object. - * - * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info - * The entity type bundle info. - */ - public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info) { - $this->entityTypeBundleInfo = $entity_type_bundle_info; - } - - /** - * {@inheritdoc} - */ - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( - $container->get('entity_type.bundle.info') - ); - } - - /** - * {@inheritdoc} - */ - public function buildPermissions(EntityTypeInterface $entity_type) { - $entity_type_id = $entity_type->id(); - $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); - $plural_label = $entity_type->getPluralLabel(); - - $permissions = []; - $permissions["administer {$entity_type_id}"] = [ - 'title' => $this->t('Administer @type', ['@type' => $plural_label]), - 'restrict access' => TRUE, - ]; - $permissions["access {$entity_type_id} overview"] = [ - 'title' => $this->t('Access the @type overview page', ['@type' => $plural_label]), - ]; - if ($has_owner && $entity_type->entityClassImplements(EntityPublishedInterface::class)) { - $permissions["view own unpublished {$entity_type_id}"] = [ - 'title' => $this->t('View own unpublished @type', [ - '@type' => $plural_label, - ]), - ]; - } - - // Generate the other permissions based on granularity. - if ($entity_type->getPermissionGranularity() === 'entity_type') { - $permissions += $this->buildEntityTypePermissions($entity_type); - } - else { - $permissions += $this->buildBundlePermissions($entity_type); - } - - return $this->processPermissions($permissions, $entity_type); - } - - /** - * Adds the provider and converts the titles to strings to allow sorting. - * - * @param array $permissions - * The array of permissions - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return array - * An array of processed permissions. - */ - protected function processPermissions(array $permissions, EntityTypeInterface $entity_type) { - foreach ($permissions as $name => $permission) { - // Permissions are grouped by provider on admin/people/permissions. - $permissions[$name]['provider'] = $entity_type->getProvider(); - // TranslatableMarkup objects don't sort properly. - $permissions[$name]['title'] = (string) $permission['title']; - } - return $permissions; - } - - /** - * Builds permissions for the entity_type granularity. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return array - * The permissions. - */ - protected function buildEntityTypePermissions(EntityTypeInterface $entity_type) { - $entity_type_id = $entity_type->id(); - $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); - $singular_label = $entity_type->getSingularLabel(); - $plural_label = $entity_type->getPluralLabel(); - - $permissions = []; - $permissions["create {$entity_type_id}"] = [ - 'title' => $this->t('Create @type', [ - '@type' => $plural_label, - ]), - ]; - if ($has_owner) { - $permissions["update any {$entity_type_id}"] = [ - 'title' => $this->t('Update any @type', [ - '@type' => $singular_label, - ]), - ]; - $permissions["update own {$entity_type_id}"] = [ - 'title' => $this->t('Update own @type', [ - '@type' => $plural_label, - ]), - ]; - $permissions["delete any {$entity_type_id}"] = [ - 'title' => $this->t('Delete any @type', [ - '@type' => $singular_label, - ]), - ]; - $permissions["delete own {$entity_type_id}"] = [ - 'title' => $this->t('Delete own @type', [ - '@type' => $plural_label, - ]), - ]; - } - else { - $permissions["update {$entity_type_id}"] = [ - 'title' => $this->t('Update @type', [ - '@type' => $plural_label, - ]), - ]; - $permissions["delete {$entity_type_id}"] = [ - 'title' => $this->t('Delete @type', [ - '@type' => $plural_label, - ]), - ]; - } - - return $permissions; - } - - /** - * Builds permissions for the bundle granularity. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return array - * The permissions. - */ - protected function buildBundlePermissions(EntityTypeInterface $entity_type) { - $entity_type_id = $entity_type->id(); - $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); - $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); - $singular_label = $entity_type->getSingularLabel(); - $plural_label = $entity_type->getPluralLabel(); - - $permissions = []; - foreach ($bundles as $bundle_name => $bundle_info) { - $permissions["create {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: Create @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $plural_label, - ]), - ]; - - if ($has_owner) { - $permissions["update any {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: Update any @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $singular_label, - ]), - ]; - $permissions["update own {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: Update own @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $plural_label, - ]), - ]; - $permissions["delete any {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: Delete any @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $singular_label, - ]), - ]; - $permissions["delete own {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: Delete own @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $plural_label, - ]), - ]; - } - else { - $permissions["update {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: Update @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $plural_label, - ]), - ]; - $permissions["delete {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: Delete @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $plural_label, - ]), - ]; - } - } - - return $permissions; - } - -} diff --git a/web/modules/contrib/entity/src/EntityPermissionProviderInterface.php b/web/modules/contrib/entity/src/EntityPermissionProviderInterface.php deleted file mode 100644 index 50f3ccaa9..000000000 --- a/web/modules/contrib/entity/src/EntityPermissionProviderInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\Entity\EntityTypeInterface; - -/** - * Allows entity types to provide permissions. - */ -interface EntityPermissionProviderInterface { - - /** - * Builds permissions for the given entity type. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return array - * The permissions. - */ - public function buildPermissions(EntityTypeInterface $entity_type); - -} diff --git a/web/modules/contrib/entity/src/EntityPermissions.php b/web/modules/contrib/entity/src/EntityPermissions.php deleted file mode 100644 index c2c13a562..000000000 --- a/web/modules/contrib/entity/src/EntityPermissions.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Generates entity permissions via their permission providers. - * - * @see \Drupal\entity\EntityPermissionProvider - * @see \Drupal\entity\UncacheableEntityPermissionProvider - */ -class EntityPermissions implements ContainerInjectionInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a new EntityPermissions object. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager') - ); - } - - /** - * Builds a list of permissions for the participating entity types. - * - * @return array - * The permissions. - */ - public function buildPermissions() { - $permissions = []; - /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ - foreach ($this->entityTypeManager->getDefinitions() as $entity_type) { - if ($entity_type->hasHandlerClass('permission_provider')) { - $permission_provider_class = $entity_type->getHandlerClass('permission_provider'); - $permission_provider = $this->entityTypeManager->createHandlerInstance($permission_provider_class, $entity_type); - $permissions += $permission_provider->buildPermissions($entity_type); - } - } - - return $permissions; - } - -} diff --git a/web/modules/contrib/entity/src/EntityViewBuilder.php b/web/modules/contrib/entity/src/EntityViewBuilder.php deleted file mode 100644 index 1efe0ebe6..000000000 --- a/web/modules/contrib/entity/src/EntityViewBuilder.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\Entity\EntityViewBuilder as CoreEntityViewBuilder; - -@trigger_error('\Drupal\entity\EntityViewBuilder has been deprecated in favor of \Drupal\Core\Entity\EntityViewBuilder. Use that instead.'); - -/** - * Provides a entity view builder with contextual links support. - * - * @deprecated in favor of \Drupal\Core\Entity\EntityViewBuilder. Use that - * instead. - */ -class EntityViewBuilder extends CoreEntityViewBuilder { - -} diff --git a/web/modules/contrib/entity/src/Form/DeleteMultipleForm.php b/web/modules/contrib/entity/src/Form/DeleteMultipleForm.php deleted file mode 100644 index aed711dfc..000000000 --- a/web/modules/contrib/entity/src/Form/DeleteMultipleForm.php +++ /dev/null @@ -1,323 +0,0 @@ -<?php - -namespace Drupal\entity\Form; - -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Form\BaseFormIdInterface; -use Drupal\Core\Form\ConfirmFormBase; -use Drupal\Core\Messenger\MessengerInterface; -use Drupal\Core\TypedData\TranslatableInterface; -use Drupal\Core\Url; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\TempStore\PrivateTempStoreFactory; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Provides an entities deletion confirmation form. - */ -class DeleteMultipleForm extends ConfirmFormBase implements BaseFormIdInterface { - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $currentUser; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * The tempstore. - * - * @var \Drupal\Core\TempStore\SharedTempStore - */ - protected $tempStore; - - /** - * The messenger service. - * - * @var \Drupal\Core\Messenger\MessengerInterface - */ - protected $messenger; - - /** - * The entity type ID. - * - * @var string - */ - protected $entityTypeId; - - /** - * The selection, in the entity_id => langcodes format. - * - * @var array - */ - protected $selection = []; - - /** - * The entity type definition. - * - * @var \Drupal\Core\Entity\EntityTypeInterface - */ - protected $entityType; - - /** - * Constructs a new DeleteMultiple object. - * - * @param \Drupal\Core\Session\AccountInterface $current_user - * The current user. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory - * The tempstore factory. - * @param \Drupal\Core\Messenger\MessengerInterface $messenger - * The messenger service. - */ - public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, MessengerInterface $messenger) { - $this->currentUser = $current_user; - $this->entityTypeManager = $entity_type_manager; - $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm'); - $this->messenger = $messenger; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('current_user'), - $container->get('entity_type.manager'), - $container->get('tempstore.private'), - $container->get('messenger') - ); - } - - /** - * {@inheritdoc} - */ - public function getBaseFormId() { - return 'entity_delete_multiple_confirm_form'; - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - // Get entity type ID from the route because ::buildForm has not yet been - // called. - $entity_type_id = $this->getRouteMatch()->getParameter('entity_type_id'); - return $entity_type_id . '_delete_multiple_confirm_form'; - } - - /** - * {@inheritdoc} - */ - public function getQuestion() { - return $this->formatPlural(count($this->selection), 'Are you sure you want to delete this @item?', 'Are you sure you want to delete these @items?', [ - '@item' => $this->entityType->getSingularLabel(), - '@items' => $this->entityType->getPluralLabel(), - ]); - } - - /** - * {@inheritdoc} - */ - public function getCancelUrl() { - if ($this->entityType->hasLinkTemplate('collection')) { - return new Url('entity.' . $this->entityTypeId . '.collection'); - } - else { - return new Url('<front>'); - } - } - - /** - * {@inheritdoc} - */ - public function getConfirmText() { - return $this->t('Delete'); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) { - $this->entityTypeId = $entity_type_id; - $this->entityType = $this->entityTypeManager->getDefinition($this->entityTypeId); - $this->selection = $this->tempStore->get($this->currentUser->id() . ':' . $entity_type_id); - if (empty($this->entityTypeId) || empty($this->selection)) { - return new RedirectResponse($this->getCancelUrl() - ->setAbsolute() - ->toString()); - } - - $items = []; - $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_keys($this->selection)); - foreach ($this->selection as $id => $selected_langcodes) { - $entity = $entities[$id]; - foreach ($selected_langcodes as $langcode) { - $key = $id . ':' . $langcode; - if ($entity instanceof TranslatableInterface) { - $entity = $entity->getTranslation($langcode); - $default_key = $id . ':' . $entity->getUntranslated()->language()->getId(); - - // Build a nested list of translations that will be deleted if the - // entity has multiple translations. - $entity_languages = $entity->getTranslationLanguages(); - if (count($entity_languages) > 1 && $entity->isDefaultTranslation()) { - $names = []; - foreach ($entity_languages as $translation_langcode => $language) { - $names[] = $language->getName(); - unset($items[$id . ':' . $translation_langcode]); - } - $items[$default_key] = [ - 'label' => [ - '#markup' => $this->t('@label (Original translation) - <em>The following @entity_type translations will be deleted:</em>', - [ - '@label' => $entity->label(), - '@entity_type' => $this->entityType->getSingularLabel(), - ]), - ], - 'deleted_translations' => [ - '#theme' => 'item_list', - '#items' => $names, - ], - ]; - } - elseif (!isset($items[$default_key])) { - $items[$key] = $entity->label(); - } - } - elseif (!isset($items[$key])) { - $items[$key] = $entity->label(); - } - } - } - - $form['entities'] = [ - '#theme' => 'item_list', - '#items' => $items, - ]; - $form = parent::buildForm($form, $form_state); - - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $total_count = 0; - $delete_entities = []; - $delete_translations = []; - $inaccessible_entities = []; - $storage = $this->entityTypeManager->getStorage($this->entityTypeId); - - $entities = $storage->loadMultiple(array_keys($this->selection)); - foreach ($this->selection as $id => $selected_langcodes) { - $entity = $entities[$id]; - if (!$entity->access('delete', $this->currentUser)) { - $inaccessible_entities[] = $entity; - continue; - } - foreach ($selected_langcodes as $langcode) { - if ($entity instanceof TranslatableInterface) { - $entity = $entity->getTranslation($langcode); - // If the entity is the default translation then deleting it will - // delete all the translations. - if ($entity->isDefaultTranslation()) { - $delete_entities[$id] = $entity; - // If there are translations already marked for deletion then remove - // them as they will be deleted anyway. - unset($delete_translations[$id]); - // Update the total count. Since a single delete will delete all - // translations, we need to add the number of translations to the - // count. - $total_count += count($entity->getTranslationLanguages()); - } - // Add the translation to the list of translations to be deleted - // unless the default translation is being deleted. - elseif (!isset($delete_entities[$id])) { - $delete_translations[$id][] = $entity; - } - } - elseif (!isset($delete_entities[$id])) { - $delete_entities[$id] = $entity; - $total_count++; - } - } - } - - if ($delete_entities) { - $storage->delete($delete_entities); - foreach ($delete_entities as $entity) { - $this->logger($entity->getEntityType()->getProvider())->notice('The @entity-type %label has been deleted.', [ - '@entity-type' => $entity->getEntityType()->getLowercaseLabel(), - '%label' => $entity->label(), - ]); - } - } - - if ($delete_translations) { - /** @var \Drupal\Core\Entity\TranslatableInterface[][] $delete_translations */ - foreach ($delete_translations as $id => $translations) { - $entity = $entities[$id]->getUntranslated(); - foreach ($translations as $translation) { - $entity->removeTranslation($translation->language()->getId()); - } - $entity->save(); - foreach ($translations as $translation) { - $this->logger($entity->getEntityType()->getProvider())->notice('The @entity-type %label @language translation has been deleted.', [ - '@entity-type' => $entity->getEntityType()->getLowercaseLabel(), - '%label' => $entity->label(), - '@language' => $translation->language()->getName(), - ]); - } - $total_count += count($translations); - } - } - - if ($total_count) { - $this->messenger->addStatus($this->getDeletedMessage($total_count)); - } - if ($inaccessible_entities) { - $this->messenger->addWarning($this->getInaccessibleMessage(count($inaccessible_entities))); - } - $this->tempStore->delete($this->currentUser->id()); - $form_state->setRedirectUrl($this->getCancelUrl()); - } - - /** - * Returns the message to show the user after an item was deleted. - * - * @param int $count - * Count of deleted translations. - * - * @return \Drupal\Core\StringTranslation\TranslatableMarkup - * The item deleted message. - */ - protected function getDeletedMessage($count) { - return $this->formatPlural($count, 'Deleted @count item.', 'Deleted @count items.'); - } - - /** - * Returns the message to show the user when an item has not been deleted. - * - * @param int $count - * Count of deleted translations. - * - * @return \Drupal\Core\StringTranslation\TranslatableMarkup - * The item inaccessible message. - */ - protected function getInaccessibleMessage($count) { - return $this->formatPlural($count, "@count item has not been deleted because you do not have the necessary permissions.", "@count items have not been deleted because you do not have the necessary permissions."); - } - -} diff --git a/web/modules/contrib/entity/src/Form/RevisionRevertForm.php b/web/modules/contrib/entity/src/Form/RevisionRevertForm.php deleted file mode 100644 index c94c53a0c..000000000 --- a/web/modules/contrib/entity/src/Form/RevisionRevertForm.php +++ /dev/null @@ -1,167 +0,0 @@ -<?php - -namespace Drupal\entity\Form; - -use Drupal\Core\Datetime\DateFormatterInterface; -use Drupal\Core\Entity\EntityTypeBundleInfoInterface; -use Drupal\Core\Entity\RevisionableInterface; -use Drupal\Core\Form\ConfirmFormBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Entity\RevisionLogInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; - -class RevisionRevertForm extends ConfirmFormBase { - - /** - * The entity revision. - * - * @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\RevisionLogInterface - */ - protected $revision; - - /** - * The date formatter. - * - * @var \Drupal\Core\Datetime\DateFormatterInterface - */ - protected $dateFormatter; - - /** - * The entity bundle information. - * - * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface - */ - protected $bundleInformation; - - /** - * Creates a new RevisionRevertForm instance. - * - * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter - * The date formatter. - * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_information - * The bundle information. - */ - public function __construct(DateFormatterInterface $date_formatter, EntityTypeBundleInfoInterface $bundle_information) { - $this->dateFormatter = $date_formatter; - $this->bundleInformation = $bundle_information; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('date.formatter'), - $container->get('entity_type.bundle.info') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'entity_revision_revert_confirm'; - } - - /** - * {@inheritdoc} - */ - public function getQuestion() { - if ($this->revision instanceof RevisionLogInterface) { - return $this->t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]); - } - return $this->t('Are you sure you want to revert the revision?'); - } - - /** - * {@inheritdoc} - */ - public function getCancelUrl() { - if ($this->revision->getEntityType()->hasLinkTemplate('version-history')) { - return $this->revision->toUrl('version-history'); - } - return $this->revision->toUrl(); - } - - /** - * {@inheritdoc} - */ - public function getConfirmText() { - return t('Revert'); - } - - /** - * {@inheritdoc} - */ - public function getDescription() { - return ''; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $_entity_revision = NULL, Request $request = NULL) { - $this->revision = $_entity_revision; - $form = parent::buildForm($form, $form_state); - - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - // The revision timestamp will be updated when the revision is saved. Keep - // the original one for the confirmation message. - $this->revision = $this->prepareRevision($this->revision); - if ($this->revision instanceof RevisionLogInterface) { - $original_revision_timestamp = $this->revision->getRevisionCreationTime(); - - $this->revision->setRevisionLogMessage($this->t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)])); - drupal_set_message(t('@type %title has been reverted to the revision from %revision-date.', ['@type' => $this->getBundleLabel($this->revision), '%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp)])); - } - else { - drupal_set_message(t('@type %title has been reverted', ['@type' => $this->getBundleLabel($this->revision), '%title' => $this->revision->label()])); - } - - $this->revision->save(); - - $this->logger('content')->notice('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]); - $form_state->setRedirect( - "entity.{$this->revision->getEntityTypeId()}.version_history", - [$this->revision->getEntityTypeId() => $this->revision->id()] - ); - } - - /** - * Prepares a revision to be reverted. - * - * @param \Drupal\Core\Entity\RevisionableInterface $revision - * The revision to be reverted. - * - * @return \Drupal\Core\Entity\RevisionableInterface - * The prepared revision ready to be stored. - */ - protected function prepareRevision(RevisionableInterface $revision) { - $revision->setNewRevision(); - $revision->isDefaultRevision(TRUE); - - return $revision; - } - - /** - * Returns a bundle label. - * - * @param \Drupal\Core\Entity\RevisionableInterface $revision - * The entity revision. - * - * @return string - */ - protected function getBundleLabel(RevisionableInterface $revision) { - /** @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface $revision */ - $bundle_info = $this->bundleInformation->getBundleInfo($revision->getEntityTypeId()); - return $bundle_info[$revision->bundle()]['label']; - } - -} diff --git a/web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php b/web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php deleted file mode 100644 index 9437aabcd..000000000 --- a/web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php - -namespace Drupal\entity\Form; - -use Drupal\Core\Entity\ContentEntityForm; -use Drupal\Core\Entity\RevisionableEntityBundleInterface; -use Drupal\Core\Form\FormStateInterface; - -@trigger_error('\Drupal\entity\Form\RevisionableContentEntityForm has been deprecated in favor of \Drupal\Core\Entity\ContentEntityForm. Use that instead.'); - -/** - * Extends the base entity form with revision support in the UI. - * - * @deprecated in favor of \Drupal\Core\Entity\ContentEntityForm. Use that - * instead. - */ -class RevisionableContentEntityForm extends ContentEntityForm { - - /** - * The entity being used by this form. - * - * @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface|\Drupal\entity\Revision\EntityRevisionLogInterface - */ - protected $entity; - - /** - * {@inheritdoc} - */ - protected function prepareEntity() { - parent::prepareEntity(); - - $bundle_entity = $this->getBundleEntity(); - - // Set up default values, if required. - if (!$this->entity->isNew()) { - $this->entity->setRevisionLogMessage(NULL); - } - - if ($bundle_entity instanceof RevisionableEntityBundleInterface) { - // Always use the default revision setting. - $this->entity->setNewRevision($bundle_entity && $bundle_entity->shouldCreateNewRevision()); - } - } - - /** - * Returns the bundle entity of the entity, or NULL if there is none. - * - * @return \Drupal\Core\Entity\EntityInterface|null - */ - protected function getBundleEntity() { - if ($bundle_key = $this->entity->getEntityType()->getKey('bundle')) { - return $this->entity->{$bundle_key}->referencedEntities()[0]; - } - return NULL; - } - - /** - * {@inheritdoc} - */ - public function form(array $form, FormStateInterface $form_state) { - $entity_type = $this->entity->getEntityType(); - $bundle_entity = $this->getBundleEntity(); - $account = $this->currentUser(); - - if ($this->operation == 'edit') { - $form['#title'] = $this->t('Edit %bundle_label @label', [ - '%bundle_label' => $bundle_entity ? $bundle_entity->label() : '', - '@label' => $this->entity->label(), - ]); - } - - $form['advanced'] = [ - '#type' => 'vertical_tabs', - '#weight' => 99, - ]; - - // Add a log field if the "Create new revision" option is checked, or if the - // current user has the ability to check that option. - // @todo Could we autogenerate this form by using some widget on the - // revision info field. - $form['revision_information'] = [ - '#type' => 'details', - '#title' => $this->t('Revision information'), - // Open by default when "Create new revision" is checked. - '#open' => $this->entity->isNewRevision(), - '#group' => 'advanced', - '#weight' => 20, - '#access' => $this->entity->isNewRevision() || $account->hasPermission($entity_type->get('admin_permission')), - ]; - - $form['revision_information']['revision'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Create new revision'), - '#default_value' => $this->entity->isNewRevision(), - '#access' => $account->hasPermission($entity_type->get('admin_permission')), - ]; - - // Check the revision log checkbox when the log textarea is filled in. - // This must not happen if "Create new revision" is enabled by default, - // since the state would auto-disable the checkbox otherwise. - if (!$this->entity->isNewRevision()) { - $form['revision_information']['revision']['#states'] = [ - 'checked' => [ - 'textarea[name="revision_log"]' => ['empty' => FALSE], - ], - ]; - } - - $form['revision_information']['revision_log'] = [ - '#type' => 'textarea', - '#title' => $this->t('Revision log message'), - '#rows' => 4, - '#default_value' => $this->entity->getRevisionLogMessage(), - '#description' => $this->t('Briefly describe the changes you have made.'), - ]; - - return parent::form($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function save(array $form, FormStateInterface $form_state) { - // Save as a new revision if requested to do so. - if (!$form_state->isValueEmpty('revision')) { - $this->entity->setNewRevision(); - } - - $insert = $this->entity->isNew(); - $this->entity->save(); - $context = ['@type' => $this->entity->bundle(), '%info' => $this->entity->label()]; - $logger = $this->logger('content'); - $bundle_entity = $this->getBundleEntity(); - $t_args = ['@type' => $bundle_entity ? $bundle_entity->label() : 'None', '%info' => $this->entity->label()]; - - if ($insert) { - $logger->notice('@type: added %info.', $context); - drupal_set_message($this->t('@type %info has been created.', $t_args)); - } - else { - $logger->notice('@type: updated %info.', $context); - drupal_set_message($this->t('@type %info has been updated.', $t_args)); - } - - if ($this->entity->id()) { - $form_state->setValue('id', $this->entity->id()); - $form_state->set('id', $this->entity->id()); - - if ($this->entity->getEntityType()->hasLinkTemplate('collection')) { - $form_state->setRedirectUrl($this->entity->toUrl('collection')); - } - else { - $form_state->setRedirectUrl($this->entity->toUrl('canonical')); - } - } - else { - // In the unlikely case something went wrong on save, the entity will be - // rebuilt and entity form redisplayed. - drupal_set_message($this->t('The entity could not be saved.'), 'error'); - $form_state->setRebuild(); - } - } - -} diff --git a/web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php b/web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php deleted file mode 100644 index 8b7a04e7a..000000000 --- a/web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php - -namespace Drupal\entity\Menu; - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Menu\LocalActionDefault; -use Drupal\Core\Routing\RouteProviderInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\Core\StringTranslation\TranslationInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; - -/** - * Provides a local action to add an entity. - */ -class EntityAddLocalAction extends LocalActionDefault { - - use StringTranslationTrait; - - /** - * The entity type. - * - * @var \Drupal\Core\Entity\EntityTypeInterface - */ - protected $entityType; - - /** - * Constructs a EntityAddLocalAction object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider - * The route provider to load routes by name. - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation - * The string translation service. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, EntityTypeInterface $entity_type, TranslationInterface $string_translation) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider); - - $this->entityType = $entity_type; - $this->setStringTranslation($string_translation); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - /* @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - $entity_type_manager = $container->get('entity_type.manager'); - // The plugin ID is of the form - // "entity.entity_actions:entity.$entity_type_id.collection". - // @see entity.links.action.yml - // @see \Drupal\entity\Menu\EntityCollectionLocalActionProvider::buildLocalActions() - list(, $derivate_id) = explode(':', $plugin_id); - list(, $entity_type_id, ) = explode('.', $derivate_id); - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('router.route_provider'), - $entity_type_manager->getDefinition($entity_type_id), - $container->get('string_translation') - ); - } - - /** - * {@inheritdoc} - */ - public function getTitle(Request $request = NULL) { - return (string) $this->t('Add @entity', [ - '@entity' => (string) $this->entityType->getSingularLabel(), - ]); - } - -} diff --git a/web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php b/web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php deleted file mode 100644 index b0103709d..000000000 --- a/web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php - -namespace Drupal\entity\Menu; - -use Drupal\Core\Entity\EntityTypeInterface; - -/** - * Provides a action link to the add page or add form on the collection. - */ -class EntityCollectionLocalActionProvider implements EntityLocalActionProviderInterface { - - /** - * {@inheritdoc} - */ - public function buildLocalActions(EntityTypeInterface $entity_type) { - $actions = []; - if ($entity_type->hasLinkTemplate('collection')) { - $entity_type_id = $entity_type->id(); - - if ($entity_type->hasLinkTemplate('add-page')) { - $route_name = "entity.$entity_type_id.add_page"; - } - elseif ($entity_type->hasLinkTemplate('add-form')) { - $route_name = "entity.$entity_type_id.add_form"; - } - - if (isset($route_name)) { - $actions[$route_name] = [ - // The title is translated at runtime by EntityAddLocalAction. - /* @see \Drupal\entity\Menu\EntityAddLocalAction::getTitle() */ - 'title' => 'Add ' . $entity_type->getSingularLabel(), - 'route_name' => $route_name, - 'appears_on' => ["entity.$entity_type_id.collection"], - 'class' => EntityAddLocalAction::class, - ]; - } - } - return $actions; - } - -} diff --git a/web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php b/web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php deleted file mode 100644 index bba16c328..000000000 --- a/web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -namespace Drupal\entity\Menu; - -use Drupal\Core\Entity\EntityTypeInterface; - -/** - * Provides an interface for entity local action providers. - */ -interface EntityLocalActionProviderInterface { - - /** - * Builds local actions for the given entity type. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return array[] - * An array of local action definitions. - */ - public function buildLocalActions(EntityTypeInterface $entity_type); - -} diff --git a/web/modules/contrib/entity/src/Plugin/Action/DeleteAction.php b/web/modules/contrib/entity/src/Plugin/Action/DeleteAction.php deleted file mode 100644 index c99d4c9ec..000000000 --- a/web/modules/contrib/entity/src/Plugin/Action/DeleteAction.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php - -namespace Drupal\entity\Plugin\Action; - -use Drupal\Core\Action\ActionBase; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\TempStore\PrivateTempStoreFactory; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Redirects to an entity deletion form. - * - * @Action( - * id = "entity_delete_action", - * label = @Translation("Delete entity"), - * deriver = "Drupal\entity\Plugin\Action\Derivative\DeleteActionDeriver", - * ) - */ -class DeleteAction extends ActionBase implements ContainerFactoryPluginInterface { - - /** - * The tempstore object. - * - * @var \Drupal\Core\TempStore\SharedTempStore - */ - protected $tempStore; - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $currentUser; - - /** - * Constructs a new DeleteAction object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin ID for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory - * The tempstore factory. - * @param AccountInterface $current_user - * Current user. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) { - $this->currentUser = $current_user; - $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm'); - - parent::__construct($configuration, $plugin_id, $plugin_definition); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('tempstore.private'), - $container->get('current_user') - ); - } - - /** - * {@inheritdoc} - */ - public function executeMultiple(array $entities) { - /** @var \Drupal\Core\Entity\EntityInterface[] $entities */ - $selection = []; - foreach ($entities as $entity) { - $langcode = $entity->language()->getId(); - $selection[$entity->id()][$langcode] = $langcode; - } - $this->tempStore->set($this->currentUser->id() . ':' . $this->getPluginDefinition()['type'], $selection); - } - - /** - * {@inheritdoc} - */ - public function execute($object = NULL) { - $this->executeMultiple([$object]); - } - - /** - * {@inheritdoc} - */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { - return $object->access('delete', $account, $return_as_object); - } - -} diff --git a/web/modules/contrib/entity/src/Plugin/Action/Derivative/DeleteActionDeriver.php b/web/modules/contrib/entity/src/Plugin/Action/Derivative/DeleteActionDeriver.php deleted file mode 100644 index 1f7d76b8d..000000000 --- a/web/modules/contrib/entity/src/Plugin/Action/Derivative/DeleteActionDeriver.php +++ /dev/null @@ -1,78 +0,0 @@ -<?php - -namespace Drupal\entity\Plugin\Action\Derivative; - -use Drupal\Component\Plugin\Derivative\DeriverBase; -use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Provides a delete action for each content entity type. - */ -class DeleteActionDeriver extends DeriverBase implements ContainerDeriverInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a new DeleteActionDeriver object. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, $base_plugin_id) { - return new static($container->get('entity_type.manager')); - } - - /** - * {@inheritdoc} - */ - public function getDerivativeDefinitions($base_plugin_definition) { - if (empty($this->derivatives)) { - $definitions = []; - foreach ($this->getParticipatingEntityTypes() as $entity_type_id => $entity_type) { - $definition = $base_plugin_definition; - $definition['label'] = t('Delete @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]); - $definition['type'] = $entity_type_id; - $definition['confirm_form_route_name'] = 'entity.' . $entity_type_id . '.delete_multiple_form'; - $definitions[$entity_type_id] = $definition; - } - $this->derivatives = $definitions; - } - - return parent::getDerivativeDefinitions($base_plugin_definition); - } - - /** - * Gets a list of participating entity types. - * - * The list consists of all content entity types with a delete-multiple-form - * link template. - * - * @return \Drupal\Core\Entity\EntityTypeInterface[] - * The participating entity types, keyed by entity type id. - */ - protected function getParticipatingEntityTypes() { - $entity_types = $this->entityTypeManager->getDefinitions(); - $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) { - return $entity_type->entityClassImplements(ContentEntityInterface::class) && $entity_type->hasLinkTemplate('delete-multiple-form'); - }); - - return $entity_types; - } - -} diff --git a/web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php b/web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php deleted file mode 100644 index 568ad7e87..000000000 --- a/web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php - -namespace Drupal\entity\Plugin\Derivative; - -use Drupal\Component\Plugin\Derivative\DeriverBase; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Derives local actions for entity types. - */ -class EntityActionsDeriver extends DeriverBase implements ContainerDeriverInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs an entity local actions deriver. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, $base_plugin_id) { - return new static($container->get('entity_type.manager')); - } - - /** - * {@inheritdoc} - */ - public function getDerivativeDefinitions($base_plugin_definition) { - if (!$this->derivatives) { - foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { - $handlers = $entity_type->getHandlerClasses(); - if (isset($handlers['local_action_provider'])) { - foreach ($handlers['local_action_provider'] as $class) { - /** @var \Drupal\entity\Menu\EntityLocalActionProviderInterface $handler */ - $handler = $this->entityTypeManager->createHandlerInstance($class, $entity_type); - $this->derivatives += $handler->buildLocalActions($entity_type); - } - } - } - } - return $this->derivatives; - } - -} diff --git a/web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php b/web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php deleted file mode 100644 index b00b8ceb4..000000000 --- a/web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php +++ /dev/null @@ -1,68 +0,0 @@ -<?php - -namespace Drupal\entity\Plugin\Derivative; - -use Drupal\Component\Plugin\Derivative\DeriverBase; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Provides local tasks for the revision overview. - */ -class RevisionsOverviewDeriver extends DeriverBase implements ContainerDeriverInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Creates a new RevisionsOverviewDeriver instance. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager - * The entity type manager. - */ - public function __construct(EntityTypeManagerInterface $entityTypeManager) { - $this->entityTypeManager = $entityTypeManager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, $base_plugin_id) { - return new static( - $container->get('entity_type.manager') - ); - } - - /** - * {@inheritdoc} - */ - public function getDerivativeDefinitions($base_plugin_definition) { - $exclude = ['node']; - - $this->derivatives = []; - foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { - if (in_array($entity_type_id, $exclude)) { - continue; - } - - if (!$entity_type->hasLinkTemplate('version-history')) { - continue; - } - - $this->derivatives[$entity_type_id] = [ - 'route_name' => "entity.$entity_type_id.version_history", - 'title' => t('Revisions'), - 'base_route' => "entity.$entity_type_id.canonical", - 'weight' => 20, - ] + $base_plugin_definition; - } - - return parent::getDerivativeDefinitions($base_plugin_definition); - } - -} diff --git a/web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php b/web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php deleted file mode 100644 index f6dc11cb3..000000000 --- a/web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -namespace Drupal\entity\Revision; - -use Drupal\Core\Entity\RevisionableContentEntityBase as BaseRevisionableContentEntityBase; - -/** - * Improves the url route handling of core's revisionable content entity base. - */ -abstract class RevisionableContentEntityBase extends BaseRevisionableContentEntityBase { - - /** - * {@inheritdoc} - */ - protected function urlRouteParameters($rel) { - $uri_route_parameters = parent::urlRouteParameters($rel); - - if (strpos($this->getEntityType()->getLinkTemplate($rel), $this->getEntityTypeId() . '_revision') !== FALSE) { - $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId(); - } - - return $uri_route_parameters; - } - -} diff --git a/web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php b/web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php deleted file mode 100644 index 7ef2f6e18..000000000 --- a/web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php - -namespace Drupal\entity\Routing; - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider as CoreAdminHtmlRouteProvider; - -/** - * Provides HTML routes for entities with administrative add/edit/delete pages. - */ -class AdminHtmlRouteProvider extends CoreAdminHtmlRouteProvider { - - /** - * {@inheritdoc} - */ - protected function getCollectionRoute(EntityTypeInterface $entity_type) { - $route = parent::getCollectionRoute($entity_type); - if ($route && $entity_type->hasHandlerClass('permission_provider')) { - $admin_permission = $entity_type->getAdminPermission(); - $overview_permission = "access {$entity_type->id()} overview"; - $route->setRequirement('_permission', "$admin_permission+$overview_permission"); - } - return $route; - } - -} diff --git a/web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php b/web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php deleted file mode 100644 index 06a350400..000000000 --- a/web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php - -namespace Drupal\entity\Routing; - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider as CoreDefaultHtmlRouteProvider; - -/** - * Provides HTML routes for entities. - */ -class DefaultHtmlRouteProvider extends CoreDefaultHtmlRouteProvider { - - /** - * {@inheritdoc} - */ - protected function getCollectionRoute(EntityTypeInterface $entity_type) { - $route = parent::getCollectionRoute($entity_type); - if ($route && $entity_type->hasHandlerClass('permission_provider')) { - $admin_permission = $entity_type->getAdminPermission(); - $overview_permission = "access {$entity_type->id()} overview"; - $route->setRequirement('_permission', "$admin_permission+$overview_permission"); - } - return $route; - } - -} diff --git a/web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php b/web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php deleted file mode 100644 index b8dced03f..000000000 --- a/web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php - -namespace Drupal\entity\Routing; - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\Routing\EntityRouteProviderInterface; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; - -/** - * Provides the HTML route for deleting multiple entities. - */ -class DeleteMultipleRouteProvider implements EntityRouteProviderInterface { - - /** - * {@inheritdoc} - */ - public function getRoutes(EntityTypeInterface $entity_type) { - $routes = new RouteCollection(); - if ($route = $this->deleteMultipleFormRoute($entity_type)) { - $routes->add('entity.' . $entity_type->id() . '.delete_multiple_form', $route); - } - - return $routes; - } - - /** - * Returns the delete multiple form route. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return \Symfony\Component\Routing\Route|null - * The generated route, if available. - */ - protected function deleteMultipleFormRoute(EntityTypeInterface $entity_type) { - if ($entity_type->hasLinkTemplate('delete-multiple-form')) { - $route = new Route($entity_type->getLinkTemplate('delete-multiple-form')); - $route->setDefault('_form', '\Drupal\entity\Form\DeleteMultipleForm'); - $route->setDefault('entity_type_id', $entity_type->id()); - $route->setRequirement('_entity_delete_multiple_access', $entity_type->id()); - - return $route; - } - } - -} diff --git a/web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php b/web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php deleted file mode 100644 index e3412958c..000000000 --- a/web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php +++ /dev/null @@ -1,127 +0,0 @@ -<?php - -namespace Drupal\entity\Routing; - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\Routing\EntityRouteProviderInterface; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; - -/** - * Provides revision routes. - */ -class RevisionRouteProvider implements EntityRouteProviderInterface { - - /** - * {@inheritdoc} - */ - public function getRoutes(EntityTypeInterface $entity_type) { - $collection = new RouteCollection(); - $entity_type_id = $entity_type->id(); - if ($view_route = $this->getRevisionViewRoute($entity_type)) { - $collection->add("entity.$entity_type_id.revision", $view_route); - } - if ($view_route = $this->getRevisionRevertRoute($entity_type)) { - $collection->add("entity.$entity_type_id.revision_revert_form", $view_route); - } - - if ($view_route = $this->getRevisionHistoryRoute($entity_type)) { - $collection->add("entity.$entity_type_id.version_history", $view_route); - } - - return $collection; - } - - /** - * Gets the entity revision view route. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return \Symfony\Component\Routing\Route|null - * The generated route, if available. - */ - protected function getRevisionViewRoute(EntityTypeInterface $entity_type) { - if ($entity_type->hasLinkTemplate('revision')) { - $entity_type_id = $entity_type->id(); - $route = new Route($entity_type->getLinkTemplate('revision')); - $route->addDefaults([ - '_controller' => '\Drupal\Core\Entity\Controller\EntityViewController::viewRevision', - '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title', - ]); - $route->addRequirements([ - '_entity_access_revision' => "$entity_type_id.view", - ]); - $route->setOption('parameters', [ - $entity_type->id() => [ - 'type' => 'entity:' . $entity_type->id(), - ], - $entity_type->id() . '_revision' => [ - 'type' => 'entity_revision:' . $entity_type->id(), - ], - ]); - return $route; - } - } - - /** - * Gets the entity revision revert route. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return \Symfony\Component\Routing\Route|null - * The generated route, if available. - */ - protected function getRevisionRevertRoute(EntityTypeInterface $entity_type) { - if ($entity_type->hasLinkTemplate('revision-revert-form')) { - $entity_type_id = $entity_type->id(); - $route = new Route($entity_type->getLinkTemplate('revision-revert-form')); - $route->addDefaults([ - '_form' => '\Drupal\entity\Form\RevisionRevertForm', - 'title' => 'Revert to earlier revision', - ]); - $route->addRequirements([ - '_entity_access_revision' => "$entity_type_id.update", - ]); - $route->setOption('parameters', [ - $entity_type->id() => [ - 'type' => 'entity:' . $entity_type->id(), - ], - $entity_type->id() . '_revision' => [ - 'type' => 'entity_revision:' . $entity_type->id(), - ], - ]); - return $route; - } - } - - /** - * Gets the entity revision version history route. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return \Symfony\Component\Routing\Route|null - * The generated route, if available. - */ - protected function getRevisionHistoryRoute($entity_type) { - if ($entity_type->hasLinkTemplate('version-history')) { - $entity_type_id = $entity_type->id(); - $route = new Route($entity_type->getLinkTemplate('version-history')); - $route->addDefaults([ - '_controller' => '\Drupal\entity\Controller\RevisionOverviewController::revisionOverviewController', - '_title' => 'Revisions', - ]); - $route->setRequirement('_entity_access_revision', "$entity_type_id.list"); - $route->setOption('entity_type_id', $entity_type->id()); - $route->setOption('parameters', [ - $entity_type->id() => [ - 'type' => 'entity:' . $entity_type->id(), - ], - ]); - return $route; - } - } - -} diff --git a/web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php b/web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php deleted file mode 100644 index df6a4001b..000000000 --- a/web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityPublishedInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\user\EntityOwnerInterface; - -/** - * Controls access based on the uncacheable entity permissions. - * - * @see \Drupal\entity\UncacheableEntityPermissionProvider - * - * Note: this access control handler will cause pages to be cached per user. - */ -class UncacheableEntityAccessControlHandler extends CoreEntityAccessControlHandler { - - /** - * {@inheritdoc} - */ - public function __construct(EntityTypeInterface $entity_type) { - parent::__construct($entity_type); - - if (!$entity_type->hasHandlerClass('permission_provider') || !is_a($entity_type->getHandlerClass('permission_provider'), UncacheableEntityPermissionProvider::class, TRUE)) { - throw new \Exception("This entity access control handler requires the entity permissions provider: {EntityPermissionProvider::class}"); - } - } - - - /** - * {@inheritdoc} - */ - protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { - $account = $this->prepareUser($account); - /** @var \Drupal\Core\Access\AccessResult $result */ - $result = parent::checkAccess($entity, $operation, $account); - - if ($result->isNeutral()) { - if ($entity instanceof EntityOwnerInterface) { - $result = $this->checkEntityOwnerPermissions($entity, $operation, $account); - } - else { - $result = $this->checkEntityPermissions($entity, $operation, $account); - } - } - - // Ensure that access is evaluated again when the entity changes. - return $result->addCacheableDependency($entity); - } - - /** - * Checks the entity operation and bundle permissions. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for which to check access. - * @param string $operation - * The entity operation. Usually one of 'view', 'view label', 'update' or - * 'delete'. - * @param \Drupal\Core\Session\AccountInterface $account - * The user for which to check access. - * - * @return \Drupal\Core\Access\AccessResultInterface - * The access result. - */ - protected function checkEntityPermissions(EntityInterface $entity, $operation, AccountInterface $account) { - return AccessResult::allowedIfHasPermissions($account, [ - "$operation {$entity->getEntityTypeId()}", - "$operation {$entity->bundle()} {$entity->getEntityTypeId()}", - ], 'OR'); - } - - /** - * Checks the entity operation and bundle permissions, with owners. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for which to check access. - * @param string $operation - * The entity operation. Usually one of 'view', 'view label', 'update' or - * 'delete'. - * @param \Drupal\Core\Session\AccountInterface $account - * The user for which to check access. - * - * @return \Drupal\Core\Access\AccessResultInterface - * The access result. - */ - protected function checkEntityOwnerPermissions(EntityInterface $entity, $operation, AccountInterface $account) { - /** @var \Drupal\Core\Entity\EntityInterface|\Drupal\user\EntityOwnerInterface $entity */ - if (($account->id() == $entity->getOwnerId())) { - if ($operation === 'view' && $entity instanceof EntityPublishedInterface && !$entity->isPublished()) { - $permissions = [ - "view own unpublished {$entity->getEntityTypeId()}", - ]; - } - else { - $permissions = [ - "$operation own {$entity->getEntityTypeId()}", - "$operation any {$entity->getEntityTypeId()}", - "$operation own {$entity->bundle()} {$entity->getEntityTypeId()}", - "$operation any {$entity->bundle()} {$entity->getEntityTypeId()}", - ]; - } - $result = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR'); - } - else { - $result = AccessResult::allowedIfHasPermissions($account, [ - "$operation any {$entity->getEntityTypeId()}", - "$operation any {$entity->bundle()} {$entity->getEntityTypeId()}", - ], 'OR'); - } - - return $result->cachePerUser(); - } - - /** - * {@inheritdoc} - */ - protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - $result = parent::checkCreateAccess($account, $context, $entity_bundle); - if ($result->isNeutral()) { - $permissions = [ - 'administer ' . $this->entityTypeId, - 'create ' . $this->entityTypeId, - ]; - if ($entity_bundle) { - $permissions[] = 'create ' . $entity_bundle . ' ' . $this->entityTypeId; - } - - $result = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR'); - } - - return $result; - } - -} diff --git a/web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php b/web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php deleted file mode 100644 index 4d105c0ae..000000000 --- a/web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php - -namespace Drupal\entity; - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\user\EntityOwnerInterface; - -/** - * Provides generic entity permissions which are cached per user. - * - * This includes: - * - * - administer $entity_type - * - access $entity_type overview - * - view an ($bundle) $entity_type - * - view own ($bundle) $entity_type - * - view own unpublished $entity_type - * - update (own|any) ($bundle) $entity_type - * - delete (own|any) ($bundle) $entity_type - * - create $bundle $entity_type - * - * As this class supports "view own ($bundle) $entity_type" it is just cacheable - * per user, which might harm performance of sites. Given that please use - * \Drupal\entity\EntityPermissionProvider unless you need the feature, or your - * entity type is not really user facing (commerce orders for example). - * - * Intended for content entity types, since config entity types usually rely - * on a single "administer" permission. - * Example annotation: - * @code - * handlers = { - * "access" = "Drupal\entity\UncacheableEntityAccessControlHandler", - * "permission_provider" = "Drupal\entity\UncacheableEntityPermissionProvider", - * } - * @endcode - * - * @see \Drupal\entity\EntityAccessControlHandler - * @see \Drupal\entity\EntityPermissions - */ -class UncacheableEntityPermissionProvider extends EntityPermissionProviderBase { - - /** - * Builds permissions for the entity_type granularity. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return array - * The permissions. - */ - protected function buildEntityTypePermissions(EntityTypeInterface $entity_type) { - $permissions = parent::buildEntityTypePermissions($entity_type); - - $entity_type_id = $entity_type->id(); - $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); - $plural_label = $entity_type->getPluralLabel(); - - if ($has_owner) { - $permissions["view any {$entity_type_id}"] = [ - 'title' => $this->t('View any @type', [ - '@type' => $plural_label, - ]), - ]; - $permissions["view own {$entity_type_id}"] = [ - 'title' => $this->t('View own @type', [ - '@type' => $plural_label, - ]), - ]; - } - else { - $permissions["view any {$entity_type_id}"] = [ - 'title' => $this->t('View any @type', [ - '@type' => $plural_label, - ]), - ]; - } - - return $permissions; - } - - /** - * Builds permissions for the bundle granularity. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return array - * The permissions. - */ - protected function buildBundlePermissions(EntityTypeInterface $entity_type) { - $permissions = parent::buildBundlePermissions($entity_type); - $entity_type_id = $entity_type->id(); - $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); - $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); - $plural_label = $entity_type->getPluralLabel(); - - $permissions["view any {$entity_type_id}"] = [ - 'title' => $this->t('View any @type', [ - '@type' => $plural_label, - ]), - ]; - if ($has_owner) { - $permissions["view own {$entity_type_id}"] = [ - 'title' => $this->t('View own @type', [ - '@type' => $plural_label, - ]), - ]; - } - - foreach ($bundles as $bundle_name => $bundle_info) { - if ($has_owner) { - $permissions["view any {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: View any @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $plural_label, - ]), - ]; - $permissions["view own {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: View own @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $plural_label, - ]), - ]; - } - else { - $permissions["view any {$bundle_name} {$entity_type_id}"] = [ - 'title' => $this->t('@bundle: View any @type', [ - '@bundle' => $bundle_info['label'], - '@type' => $plural_label, - ]), - ]; - } - } - - return $permissions; - } - -} diff --git a/web/modules/contrib/entity/tests/Kernel/RevisionOverviewIntegrationTest.php b/web/modules/contrib/entity/tests/Kernel/RevisionOverviewIntegrationTest.php deleted file mode 100644 index 118fea62f..000000000 --- a/web/modules/contrib/entity/tests/Kernel/RevisionOverviewIntegrationTest.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Kernel; - -use Drupal\KernelTests\KernelTestBase; -use Symfony\Component\Routing\Route; - -/** - * Tests some integration of the revision overview: - * - * - Are the routes added properly. - * - Are the local tasks added properly. - */ -class RevisionOverviewIntegrationTest extends KernelTestBase { - - /** - * {@inheritdoc} - */ - public static $modules = ['node', 'entity_module_test', 'entity', 'user', 'system']; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->installSchema('system', 'router'); - - \Drupal::service('router.builder')->rebuild(); - } - - public function testIntegration() { - /** @var \Drupal\Core\Menu\LocalTaskManagerInterface $local_tasks_manager */ - $local_tasks_manager = \Drupal::service('plugin.manager.menu.local_task'); - - $tasks = $local_tasks_manager->getDefinitions(); - $this->assertArrayHasKey('entity.revisions_overview:entity_test_enhanced', $tasks); - $this->assertArrayNotHasKey('entity.revisions_overview:node', $tasks, 'Node should have been excluded because it provides their own'); - - $this->assertEquals('entity.entity_test_enhanced.version_history', $tasks['entity.revisions_overview:entity_test_enhanced']['route_name']); - $this->assertEquals('entity.entity_test_enhanced.canonical', $tasks['entity.revisions_overview:entity_test_enhanced']['base_route']); - - /** @var \Drupal\Core\Routing\RouteProviderInterface $route_provider */ - $route_provider = \Drupal::service('router.route_provider'); - - $route = $route_provider->getRouteByName('entity.entity_test_enhanced.version_history'); - $this->assertInstanceOf(Route::class, $route); - $this->assertEquals('\Drupal\entity\Controller\RevisionOverviewController::revisionOverviewController', $route->getDefault('_controller')); - } - -} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml deleted file mode 100644 index cd263ae9e..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: 'Entity bundle plugin examples test' -type: module -description: 'Module for testing bundle plugins.' -package: Testing -# core: 8.x -dependencies: - - entity - -# Information added by Drupal.org packaging script on 2018-03-13 -version: '8.x-1.0-beta3' -core: '8.x' -project: 'entity' -datestamp: 1520958515 diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php deleted file mode 100644 index a047b6e81..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php - -namespace Drupal\entity_module_bundle_plugin_examples_test\Plugin\BundlePluginTest; - -use Drupal\entity\BundleFieldDefinition; -use Drupal\Core\Plugin\PluginBase; -use Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest\BundlePluginTestInterface; - -/** - * Provides the second bundle plugin. - * - * @BundlePluginTest( - * id = "second", - * label = @Translation("Second"), - * ) - */ -class Second extends PluginBase implements BundlePluginTestInterface { - - /** - * {@inheritdoc} - */ - public function buildFieldDefinitions() { - $fields = []; - $fields['second_mail'] = BundleFieldDefinition::create('email') - ->setLabel(t('Email')) - ->setRequired(TRUE); - - return $fields; - } - -} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml deleted file mode 100644 index f4fad5344..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: 'Entity bundle plugin test' -type: module -description: 'Module for testing bundle plugins.' -package: Testing -# core: 8.x -dependencies: - - entity - -# Information added by Drupal.org packaging script on 2018-03-13 -version: '8.x-1.0-beta3' -core: '8.x' -project: 'entity' -datestamp: 1520958515 diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml deleted file mode 100644 index ead5cd39d..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - plugin.manager.bundle_plugin_test: - class: Drupal\entity_module_bundle_plugin_test\BundlePluginTestManager - parent: default_plugin_manager diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php deleted file mode 100644 index f09f70eb2..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace Drupal\entity_module_bundle_plugin_test\Annotation; - -use Drupal\Component\Annotation\Plugin; - -/** - * Defines the BundlePluginTest annotation object. - * - * Plugin namespace: Plugin\BundlePluginTest. - * - * @see plugin_api - * - * @Annotation - */ -class BundlePluginTest extends Plugin { - - /** - * The plugin ID. - * - * @var string - */ - public $id; - - /** - * The plugin label. - * - * @ingroup plugin_translatable - * - * @var \Drupal\Core\Annotation\Translation - */ - public $label; - -} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/BundlePluginTestManager.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/BundlePluginTestManager.php deleted file mode 100644 index 68cd398ef..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/BundlePluginTestManager.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php - -namespace Drupal\entity_module_bundle_plugin_test; - -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Plugin\DefaultPluginManager; - -/** - * Manages discovery and instantiation of BundlePluginTest plugins. - * - * @see \Drupal\entity_module_bundle_plugin_test\Annotation\BundlePluginTest - * @see plugin_api - */ -class BundlePluginTestManager extends DefaultPluginManager { - - /** - * Constructs a new BundlePluginTestManager object. - * - * @param \Traversable $namespaces - * An object that implements \Traversable which contains the root paths - * keyed by the corresponding namespace to look for plugin implementations. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend - * The cache backend. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - */ - public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { - parent::__construct('Plugin/BundlePluginTest', $namespaces, $module_handler, 'Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest\BundlePluginTestInterface', 'Drupal\entity_module_bundle_plugin_test\Annotation\BundlePluginTest'); - - $this->alterInfo('bundle_plugin_test_info'); - $this->setCacheBackend($cache_backend, 'bundle_plugin_test_plugins'); - } - -} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php deleted file mode 100644 index 1c75a477a..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -namespace Drupal\entity_module_bundle_plugin_test\Entity; - -use Drupal\Core\Entity\ContentEntityBase; - -/** - * Defines the 'entity_test_bundle_plugin' entity class. - * - * @ContentEntityType( - * id = "entity_test_bundle_plugin", - * label = @Translation("Entity test bundle plugin"), - * bundle_label = @Translation("Bundle Plugin Test"), - * bundle_plugin_type = "bundle_plugin_test", - * base_table = "entity_test_bundle_plugin", - * admin_permission = "administer content", - * fieldable = TRUE, - * entity_keys = { - * "id" = "method_id", - * "uuid" = "uuid", - * "bundle" = "type" - * }, - * ) - */ -class EntityTestBundlePlugin extends ContentEntityBase { - -} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/BundlePluginTestInterface.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/BundlePluginTestInterface.php deleted file mode 100644 index 2b25a02db..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/BundlePluginTestInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php - -namespace Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest; - -use Drupal\entity\BundlePlugin\BundlePluginInterface; - -/** - * Defines the interface for BundlePluginTest plugins. - */ -interface BundlePluginTestInterface extends BundlePluginInterface { - -} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/First.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/First.php deleted file mode 100644 index 728a90b69..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/First.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php - -namespace Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest; - -use Drupal\entity\BundleFieldDefinition; -use Drupal\Core\Plugin\PluginBase; - -/** - * Provides the first bundle plugin. - * - * @BundlePluginTest( - * id = "first", - * label = @Translation("First"), - * description = @Translation("Some description"), - * ) - */ -class First extends PluginBase implements BundlePluginTestInterface { - - /** - * {@inheritdoc} - */ - public function buildFieldDefinitions() { - $fields = []; - $fields['first_mail'] = BundleFieldDefinition::create('email') - ->setLabel(t('Email')) - ->setRequired(TRUE); - - return $fields; - } - -} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml deleted file mode 100644 index eb1c5a089..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml +++ /dev/null @@ -1,16 +0,0 @@ -entity_module_test.entity_test_enhanced_bundle.*: - type: config_entity - label: 'Entity test with enhancements - Bundle' - mapping: - id: - type: string - label: 'Type' - label: - type: label - label: 'Label' - description: - type: text - label: 'Description' - new_revision: - type: boolean - label: 'New revision' diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml deleted file mode 100644 index 486850130..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Entity test -type: module -package: Testing -# core: 8.x - -# Information added by Drupal.org packaging script on 2018-03-13 -version: '8.x-1.0-beta3' -core: '8.x' -project: 'entity' -datestamp: 1520958515 diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml deleted file mode 100644 index e306c10c9..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml +++ /dev/null @@ -1,9 +0,0 @@ -entity.entity_test_enhanced.canonical: - title: View - route_name: entity.entity_test_enhanced.canonical - base_route: entity.entity_test_enhanced.canonical - -entity.entity_module_test.edit_form: - title: Edit - route_name: entity.entity_test_enhanced.edit_form - base_route: entity.entity_test_enhanced.canonical diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.permissions.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.permissions.yml deleted file mode 100644 index 8aa844ed4..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.permissions.yml +++ /dev/null @@ -1,3 +0,0 @@ -'view all entity_test_enhanced revisions': - title: 'View all entity_test_enhanced revisions' - 'restrict access': TRUE diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php b/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php deleted file mode 100644 index f9f7b9b6c..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php - -namespace Drupal\entity_module_test\Entity; - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\entity\Revision\RevisionableContentEntityBase; - -/** - * Provides a test entity which uses all the capabilities of entity module. - * - * @ContentEntityType( - * id = "entity_test_enhanced", - * label = @Translation("Enhanced entity"), - * label_collection = @Translation("Enhanced entities"), - * label_singular = @Translation("enhanced entity"), - * label_plural = @Translation("enhanced entities"), - * label_count = @PluralTranslation( - * singular = "@count enhanced entity", - * plural = "@count enhanced entities", - * ), - * handlers = { - * "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage", - * "access" = "\Drupal\Core\Entity\EntityAccessControlHandler", - * "permission_provider" = "\Drupal\entity\EntityPermissionProvider", - * "form" = { - * "add" = "\Drupal\entity\Form\RevisionableContentEntityForm", - * "edit" = "\Drupal\entity\Form\RevisionableContentEntityForm", - * "delete" = "\Drupal\Core\Entity\EntityDeleteForm", - * }, - * "route_provider" = { - * "html" = "\Drupal\entity\Routing\DefaultHtmlRouteProvider", - * "revision" = "\Drupal\entity\Routing\RevisionRouteProvider", - * "delete-multiple" = "\Drupal\entity\Routing\DeleteMultipleRouteProvider", - * }, - * "local_action_provider" = { - * "collection" = "\Drupal\entity\Menu\EntityCollectionLocalActionProvider", - * }, - * "list_builder" = "\Drupal\Core\Entity\EntityListBuilder", - * }, - * base_table = "entity_test_enhanced", - * data_table = "entity_test_enhanced_field_data", - * revision_table = "entity_test_enhanced_revision", - * revision_data_table = "entity_test_enhanced_field_revision", - * translatable = TRUE, - * revisionable = TRUE, - * admin_permission = "administer entity_test_enhanced", - * permission_granularity = "bundle", - * entity_keys = { - * "id" = "id", - * "bundle" = "type", - * "revision" = "vid", - * "langcode" = "langcode", - * }, - * links = { - * "add-page" = "/entity_test_enhanced/add", - * "add-form" = "/entity_test_enhanced/add/{type}", - * "edit-form" = "/entity_test_enhanced/{entity_test_enhanced}/edit", - * "canonical" = "/entity_test_enhanced/{entity_test_enhanced}", - * "collection" = "/entity_test_enhanced", - * "delete-multiple-form" = "/entity_test_enhanced/delete", - * "revision" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/view", - * "revision-revert-form" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/revert", - * "version-history" = "/entity_test_enhanced/{entity_test_enhanced}/revisions", - * }, - * bundle_entity_type = "entity_test_enhanced_bundle", - * ) - */ -class EnhancedEntity extends RevisionableContentEntityBase { - - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields = parent::baseFieldDefinitions($entity_type); - - $fields['name'] = BaseFieldDefinition::create('string') - ->setLabel('Name') - ->setRevisionable(TRUE) - ->setDisplayOptions('view', [ - 'label' => 'hidden', - 'type' => 'string', - 'weight' => -5, - ]); - - return $fields; - } - -} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityBundle.php b/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityBundle.php deleted file mode 100644 index 48524a828..000000000 --- a/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityBundle.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php - -namespace Drupal\entity_module_test\Entity; - -use Drupal\Core\Config\Entity\ConfigEntityBundleBase; -use Drupal\Core\Entity\EntityDescriptionInterface; -use Drupal\Core\Entity\RevisionableEntityBundleInterface; - -/** - * Provides bundles for the test entity. - * - * @ConfigEntityType( - * id = "entity_test_enhanced_bundle", - * label = @Translation("Entity test with enhancments - Bundle"), - * admin_permission = "administer entity_test_enhanced", - * config_prefix = "entity_test_enhanced_bundle", - * bundle_of = "entity_test_enhanced", - * entity_keys = { - * "id" = "id", - * "label" = "label" - * }, - * config_export = { - * "id", - * "label", - * "description" - * }, - * ) - */ -class EnhancedEntityBundle extends ConfigEntityBundleBase implements EntityDescriptionInterface, RevisionableEntityBundleInterface { - - /** - * The bundle ID. - * - * @var string - */ - protected $id; - - /** - * The bundle label. - * - * @var string - */ - protected $label; - - /** - * The bundle description. - * - * @var string - */ - protected $description; - - /** - * Should new entities of this bundle have a new revision by default. - * - * @var bool - */ - protected $new_revision = FALSE; - - /** - * {@inheritdoc} - */ - public function getDescription() { - return $this->description; - } - - /** - * {@inheritdoc} - */ - public function setDescription($description) { - $this->description = $description; - return $this; - } - - /** - * {@inheritdoc} - */ - public function shouldCreateNewRevision() { - return $this->new_revision; - } - -} diff --git a/web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php b/web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php deleted file mode 100644 index 5160ed402..000000000 --- a/web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Functional; - -use Drupal\entity_module_test\Entity\EnhancedEntity; -use Drupal\entity_module_test\Entity\EnhancedEntityBundle; -use Drupal\Tests\block\Traits\BlockCreationTrait; -use Drupal\Tests\BrowserTestBase; - -/** - * Tests the collection route access check. - * - * @group entity - * - * @runTestsInSeparateProcesses - * - * @preserveGlobalState disabled - */ -class CollectionRouteAccessTest extends BrowserTestBase { - - use BlockCreationTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = ['entity_module_test', 'user', 'entity', 'block']; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - EnhancedEntityBundle::create([ - 'id' => 'default', - 'label' => 'Default', - ])->save(); - - $this->placeBlock('local_tasks_block'); - $this->placeBlock('system_breadcrumb_block'); - } - - /** - * Test the collection route access. - */ - public function testCollectionRouteAccess() { - $entity = EnhancedEntity::create([ - 'name' => 'rev 1', - 'type' => 'default', - ]); - $entity->save(); - - // User without any relevant permissions. - $account = $this->drupalCreateUser(['access administration pages']); - $this->drupalLogin($account); - - $this->drupalGet($entity->toUrl('collection')); - $this->assertSession()->statusCodeEquals(403); - - // User with "access overview" permissions. - $account = $this->drupalCreateUser(['access entity_test_enhanced overview']); - $this->drupalLogin($account); - - $this->drupalGet($entity->toUrl('collection')); - $this->assertSession()->statusCodeEquals(200); - - // User with full administration permissions. - $account = $this->drupalCreateUser(['administer entity_test_enhanced']); - $this->drupalLogin($account); - - $this->drupalGet($entity->toUrl('collection')); - $this->assertSession()->statusCodeEquals(200); - } - -} diff --git a/web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php b/web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php deleted file mode 100644 index 1dc6931c6..000000000 --- a/web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Functional; - -use Drupal\entity_module_test\Entity\EnhancedEntity; -use Drupal\entity_module_test\Entity\EnhancedEntityBundle; -use Drupal\Tests\BrowserTestBase; - -/** - * Tests the delete multiple confirmation form. - * - * @group entity - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled - */ -class DeleteMultipleFormTest extends BrowserTestBase { - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $account; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = ['entity_module_test', 'user', 'entity']; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - EnhancedEntityBundle::create([ - 'id' => 'default', - 'label' => 'Default', - ])->save(); - $this->account = $this->drupalCreateUser(['administer entity_test_enhanced']); - $this->drupalLogin($this->account); - } - - /** - * Tests the add page. - */ - public function testForm() { - $entities = []; - $selection = []; - for ($i = 0; $i < 2; $i++) { - $entity = EnhancedEntity::create([ - 'type' => 'default', - ]); - $entity->save(); - $entities[$entity->id()] = $entity; - - $langcode = $entity->language()->getId(); - $selection[$entity->id()][$langcode] = $langcode; - } - // Add the selection to the tempstore just like DeleteAction would. - $tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm'); - $tempstore->set($this->account->id() . ':entity_test_enhanced', $selection); - - $this->drupalGet('/entity_test_enhanced/delete'); - $assert = $this->assertSession(); - $assert->statusCodeEquals(200); - $assert->elementTextContains('css', '.page-title', 'Are you sure you want to delete these enhanced entities?'); - $delete_button = $this->getSession()->getPage()->findButton('Delete'); - $delete_button->click(); - $assert = $this->assertSession(); - $assert->addressEquals('/entity_test_enhanced'); - $assert->responseContains('Deleted 2 items.'); - - \Drupal::entityTypeManager()->getStorage('entity_test_enhanced')->resetCache(); - $remaining_entities = EnhancedEntity::loadMultiple(array_keys($selection)); - $this->assertEmpty($remaining_entities); - } - -} diff --git a/web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php b/web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php deleted file mode 100644 index 4fb4faabe..000000000 --- a/web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Functional\Menu; - -use Drupal\Tests\BrowserTestBase; - -/** - * Tests that entity local actions are generated correctly. - * - * @group entity - * - * @runTestsInSeparateProcesses - * - * @preserveGlobalState disabled - */ -class EntityLocalActionTest extends BrowserTestBase { - - /** - * {@inheritdoc} - */ - protected static $modules = ['block', 'entity', 'entity_module_test']; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->drupalPlaceBlock('local_actions_block'); - - $account = $this->drupalCreateUser(['administer entity_test_enhanced']); - $this->drupalLogin($account); - } - - /** - * Tests the local action on the collection is provided correctly. - */ - public function testCollectionLocalAction() { - $this->drupalGet('/entity_test_enhanced'); - $this->assertSession()->linkByHrefExists('/entity_test_enhanced/add'); - $this->assertSession()->linkExists('Add enhanced entity'); - } - -} diff --git a/web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php b/web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php deleted file mode 100644 index f3c205a20..000000000 --- a/web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Functional; - -use Drupal\entity_module_test\Entity\EnhancedEntity; -use Drupal\entity_module_test\Entity\EnhancedEntityBundle; -use Drupal\Tests\block\Traits\BlockCreationTrait; -use Drupal\Tests\BrowserTestBase; - -/** - * Tests the revision route access check. - * - * @group entity - * - * @runTestsInSeparateProcesses - * - * @preserveGlobalState disabled - */ -class RevisionRouteAccessTest extends BrowserTestBase { - - use BlockCreationTrait; - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface; - */ - protected $account; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = ['entity_module_test', 'user', 'entity', 'block']; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - EnhancedEntityBundle::create([ - 'id' => 'default', - 'label' => 'Default', - ])->save(); - - $this->placeBlock('local_tasks_block'); - $this->placeBlock('system_breadcrumb_block'); - - $this->account = $this->drupalCreateUser([ - 'administer entity_test_enhanced', - 'view all entity_test_enhanced revisions', - ]); - - $this->drupalLogin($this->account); - } - - /** - * Test enhanced entity revision routes access. - */ - public function testRevisionRouteAccess() { - $entity = EnhancedEntity::create([ - 'name' => 'rev 1', - 'type' => 'default', - ]); - $entity->save(); - - $revision = clone $entity; - $revision->name->value = 'rev 2'; - $revision->setNewRevision(TRUE); - $revision->isDefaultRevision(FALSE); - $revision->save(); - - $this->drupalGet('/entity_test_enhanced/1/revisions'); - $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->responseContains('Revisions'); - $collection_link = $this->getSession()->getPage()->findLink('Enhanced entities'); - $collection_link->click(); - $this->assertSession()->addressEquals('/entity_test_enhanced'); - $this->assertSession()->responseContains('Edit'); - $edit_link = $this->getSession()->getPage()->findLink('Edit'); - $edit_link->click(); - $this->assertSession()->addressEquals('/entity_test_enhanced/1/edit'); - // Check if we have revision tab link on edit page. - $this->getSession()->getPage()->findLink('Revisions')->click(); - $this->assertSession()->addressEquals('/entity_test_enhanced/1/revisions'); - $this->drupalGet('/entity_test_enhanced/1/revisions/2/view'); - $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->responseContains('rev 2'); - $revisions_link = $this->getSession()->getPage()->findLink('Revisions'); - $revisions_link->click(); - $this->assertSession()->addressEquals('/entity_test_enhanced/1/revisions'); - $this->assertSession()->statusCodeEquals(200); - } - -} diff --git a/web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php b/web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php deleted file mode 100644 index eeeb92384..000000000 --- a/web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php +++ /dev/null @@ -1,146 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Kernel; - -use Drupal\entity_module_bundle_plugin_test\Entity\EntityTestBundlePlugin; -use Drupal\KernelTests\KernelTestBase; - -/** - * Tests the bundle plugin API. - * - * @group entity - */ -class BundlePluginTest extends KernelTestBase { - - /** - * {@inheritdoc} - */ - public static $modules = [ - 'system', - 'entity', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->installSchema('system', 'router'); - - // Install the modules properly. Putting them into static::$modules doesn't trigger the install - // hooks, like hook_modules_installed, so entity_modules_installed is not triggered(). - /** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */ - $module_installer = \Drupal::service('module_installer'); - $module_installer->install(['entity_module_bundle_plugin_test', 'entity_module_bundle_plugin_examples_test']); - } - - /** - * Tests the bundle plugins. - */ - public function testPluginBundles() { - $bundled_entity_types = entity_get_bundle_plugin_entity_types(); - /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ - $entity_type = $bundled_entity_types['entity_test_bundle_plugin']; - $this->assertEquals('entity_test_bundle_plugin', $entity_type->id()); - $this->assertTrue($entity_type->hasHandlerClass('bundle_plugin')); - - /** @var \Drupal\Core\Entity\EntityTypeBundleInfo $entity_type_bundle_info */ - $entity_type_bundle_info = \Drupal::service('entity_type.bundle.info'); - $bundle_info = $entity_type_bundle_info->getBundleInfo('entity_test_bundle_plugin'); - $this->assertEquals(2, count($bundle_info)); - $this->assertArrayHasKey('first', $bundle_info); - $this->assertArrayHasKey('second', $bundle_info); - $this->assertEquals('First', $bundle_info['first']['label']); - $this->assertEquals('Some description', $bundle_info['first']['description']); - - /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ - $entity_field_manager = \Drupal::service('entity_field.manager'); - $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions('entity_test_bundle_plugin'); - $this->assertArrayHasKey('first_mail', $field_storage_definitions); - $this->assertArrayHasKey('second_mail', $field_storage_definitions); - $first_field_definitions = $entity_field_manager->getFieldDefinitions('entity_test_bundle_plugin', 'first'); - $this->assertArrayHasKey('first_mail', $first_field_definitions); - $this->assertArrayNotHasKey('second_mail', $first_field_definitions); - $second_field_definitions = $entity_field_manager->getFieldDefinitions('entity_test_bundle_plugin', 'second'); - $this->assertArrayNotHasKey('first_mail', $second_field_definitions); - $this->assertArrayHasKey('second_mail', $second_field_definitions); - - $first_entity = EntityTestBundlePlugin::create([ - 'type' => 'first', - 'first_mail' => 'admin@test.com', - ]); - $first_entity->save(); - $first_entity = EntityTestBundlePlugin::load($first_entity->id()); - $this->assertEquals('admin@test.com', $first_entity->first_mail->value); - - $second_entity = EntityTestBundlePlugin::create([ - 'type' => 'second', - 'second_mail' => 'admin@example.com', - ]); - $second_entity->save(); - $second_entity = EntityTestBundlePlugin::load($second_entity->id()); - $this->assertEquals('admin@example.com', $second_entity->second_mail->value); - - // Also test entity queries. - $result = \Drupal::entityTypeManager()->getStorage('entity_test_bundle_plugin') - ->getQuery() - ->condition('second_mail', 'admin@example.com') - ->execute(); - $this->assertEquals([$second_entity->id() => $second_entity->id()], $result); - - $result = \Drupal::entityTypeManager()->getStorage('entity_test_bundle_plugin') - ->getQuery() - ->condition('type', 'first') - ->execute(); - $this->assertEquals([$first_entity->id() => $first_entity->id()], $result); - - } - - /** - * Tests the uninstallation for a bundle provided by a module. - */ - public function testBundlePluginModuleUninstallation() { - /** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */ - $module_installer = \Drupal::service('module_installer'); - - // One should be possible to uninstall without any actual content. - $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']); - $this->assertEmpty($violations); - - $first_entity = EntityTestBundlePlugin::create([ - 'type' => 'first', - 'first_mail' => 'admin@test.com', - ]); - $first_entity->save(); - $second_entity = EntityTestBundlePlugin::create([ - 'type' => 'second', - 'second_mail' => 'admin@example.com', - ]); - $second_entity->save(); - - $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']); - $this->assertCount(1, $violations); - $this->assertCount(1, $violations['entity_module_bundle_plugin_examples_test']); - $this->assertEquals('There is data for the bundle Second on the entity type Entity test bundle plugin. Please remove all content before uninstalling the module.', $violations['entity_module_bundle_plugin_examples_test'][0]); - - $second_entity->delete(); - - // The first entity is defined by entity_module_bundle_plugin_test, so it should be possible - // to uninstall the module providing the second bundle plugin. - $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']); - $this->assertEmpty($violations); - - $module_installer->uninstall(['entity_module_bundle_plugin_examples_test']); - - // The first entity is provided by entity_module_bundle_plugin_test which we cannot uninstall, - // until all the entities are deleted. - $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_test']); - $this->assertNotEmpty($violations); - - $first_entity->delete(); - $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_test']); - $this->assertEmpty($violations); - } - -} diff --git a/web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php b/web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php deleted file mode 100644 index 68680f09d..000000000 --- a/web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Kernel; - -use Drupal\entity\Plugin\Action\DeleteAction; -use Drupal\entity_module_test\Entity\EnhancedEntity; -use Drupal\entity_module_test\Entity\EnhancedEntityBundle; -use Drupal\system\Entity\Action; -use Drupal\user\Entity\User; -use Drupal\KernelTests\KernelTestBase; - -/** - * Tests the delete entity action. - * @group entity - */ -class DeleteActionTest extends KernelTestBase { - - /** - * The current user. - * - * @var \Drupal\user\UserInterface - */ - protected $user; - - /** - * {@inheritdoc} - */ - public static $modules = ['action', 'node', 'entity_module_test', 'entity', - 'user', 'system']; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->installEntitySchema('user'); - $this->installEntitySchema('entity_test_enhanced'); - $this->installSchema('system', ['key_value_expire', 'sequences']); - - $bundle = EnhancedEntityBundle::create([ - 'id' => 'default', - 'label' => 'Default', - ]); - $bundle->save(); - - $this->user = User::create([ - 'name' => 'username', - 'status' => 1, - ]); - $this->user->save(); - \Drupal::service('current_user')->setAccount($this->user); - } - - public function testAction() { - /** @var \Drupal\system\ActionConfigEntityInterface $action */ - $action = Action::create([ - 'id' => 'enhanced_entity_delete_action', - 'label' => 'Delete enhanced entity', - 'plugin' => 'entity_delete_action:entity_test_enhanced', - ]); - $status = $action->save(); - $this->assertEquals(SAVED_NEW, $status); - $this->assertInstanceOf(DeleteAction::class, $action->getPlugin()); - - $entities = []; - for ($i = 0; $i < 2; $i++) { - $entity = EnhancedEntity::create([ - 'type' => 'default', - ]); - $entity->save(); - $entities[$entity->id()] = $entity; - } - - $action->execute($entities); - // Confirm that the entity ids and langcodes are now in the tempstore. - $tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm'); - $selection = $tempstore->get($this->user->id() . ':entity_test_enhanced'); - $this->assertEquals(array_keys($entities), array_keys($selection)); - $this->assertEquals([['en' => 'en'], ['en' => 'en']], array_values($selection)); - } - -} diff --git a/web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php b/web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php deleted file mode 100644 index 1342848da..000000000 --- a/web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php +++ /dev/null @@ -1,192 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Kernel; - -use Drupal\entity_module_test\Entity\EnhancedEntity; -use Drupal\entity_module_test\Entity\EnhancedEntityBundle; -use Drupal\KernelTests\KernelTestBase; -use Drupal\user\Entity\Role; -use Drupal\user\Entity\User; -use Symfony\Component\HttpFoundation\Request; - -/** - * @group entity - */ -class RevisionBasicUITest extends KernelTestBase { - - /** - * {@inheritdoc} - */ - public static $modules = ['entity_module_test', 'system', 'user', 'entity']; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->installEntitySchema('user'); - $this->installEntitySchema('entity_test_enhanced'); - $this->installSchema('system', 'router'); - $this->installConfig(['system']); - - $bundle = EnhancedEntityBundle::create([ - 'id' => 'default', - 'label' => 'Default', - ]); - $bundle->save(); - - \Drupal::service('router.builder')->rebuild(); - } - - /** - * Tests the revision history controller. - */ - public function testRevisionHistory() { - $entity = EnhancedEntity::create([ - 'name' => 'rev 1', - 'type' => 'default', - ]); - $entity->save(); - - $revision = clone $entity; - $revision->name->value = 'rev 2'; - $revision->setNewRevision(TRUE); - $revision->isDefaultRevision(FALSE); - $revision->save(); - - /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */ - $http_kernel = \Drupal::service('http_kernel'); - $request = Request::create($revision->toUrl('version-history')->toString()); - $response = $http_kernel->handle($request); - $this->assertEquals(403, $response->getStatusCode()); - - $role_admin = Role::create(['id' => 'test_role_admin']); - $role_admin->grantPermission('administer entity_test_enhanced'); - $role_admin->save(); - - $role = Role::create(['id' => 'test_role']); - $role->grantPermission('view all entity_test_enhanced revisions'); - $role->grantPermission('administer entity_test_enhanced'); - $role->save(); - - $user_admin = User::create([ - 'name' => 'Test user admin', - ]); - $user_admin->addRole($role_admin->id()); - \Drupal::service('account_switcher')->switchTo($user_admin); - - $request = Request::create($revision->toUrl('version-history')->toString()); - $response = $http_kernel->handle($request); - $this->assertEquals(200, $response->getStatusCode()); - - $user = User::create([ - 'name' => 'Test user', - ]); - $user->addRole($role->id()); - \Drupal::service('account_switcher')->switchTo($user); - - $request = Request::create($revision->toUrl('version-history')->toString()); - $response = $http_kernel->handle($request); - $this->assertEquals(200, $response->getStatusCode()); - - // This ensures that the default revision is still the first revision. - $this->assertTrue(strpos($response->getContent(), 'entity_test_enhanced/1/revisions/2/view') !== FALSE); - $this->assertTrue(strpos($response->getContent(), 'entity_test_enhanced/1') !== FALSE); - - // Publish a new revision. - $revision = clone $entity; - $revision->name->value = 'rev 3'; - $revision->setNewRevision(TRUE); - $revision->isDefaultRevision(TRUE); - $revision->save(); - - $request = Request::create($revision->toUrl('version-history')->toString()); - $response = $http_kernel->handle($request); - $this->assertEquals(200, $response->getStatusCode()); - - // The first revision row should now include a revert link. - $this->assertTrue(strpos($response->getContent(), 'entity_test_enhanced/1/revisions/1/revert') !== FALSE); - } - - public function testRevisionView() { - $entity = EnhancedEntity::create([ - 'name' => 'rev 1', - 'type' => 'default', - ]); - $entity->save(); - - $revision = clone $entity; - $revision->name->value = 'rev 2'; - $revision->setNewRevision(TRUE); - $revision->isDefaultRevision(FALSE); - $revision->save(); - - /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */ - $http_kernel = \Drupal::service('http_kernel'); - $request = Request::create($revision->toUrl('revision')->toString()); - $response = $http_kernel->handle($request); - $this->assertEquals(403, $response->getStatusCode()); - - $role_admin = Role::create(['id' => 'test_role_admin']); - $role_admin->grantPermission('administer entity_test_enhanced'); - $role_admin->save(); - - $role = Role::create(['id' => 'test_role']); - $role->grantPermission('view all entity_test_enhanced revisions'); - $role->grantPermission('administer entity_test_enhanced'); - $role->save(); - - $user_admin = User::create([ - 'name' => 'Test user admin', - ]); - $user_admin->addRole($role_admin->id()); - \Drupal::service('account_switcher')->switchTo($user_admin); - - $request = Request::create($revision->toUrl('version-history')->toString()); - $response = $http_kernel->handle($request); - $this->assertEquals(200, $response->getStatusCode()); - - $user = User::create([ - 'name' => 'Test user', - ]); - $user->addRole($role->id()); - \Drupal::service('account_switcher')->switchTo($user); - - $request = Request::create($revision->toUrl('revision')->toString()); - $response = $http_kernel->handle($request); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertNotContains('rev 1', $response->getContent()); - $this->assertContains('rev 2', $response->getContent()); - } - - public function testRevisionRevert() { - $entity = EnhancedEntity::create([ - 'name' => 'rev 1', - 'type' => 'entity_test_enhance', - ]); - $entity->save(); - $entity->name->value = 'rev 2'; - $entity->setNewRevision(TRUE); - $entity->isDefaultRevision(TRUE); - $entity->save(); - - $role = Role::create(['id' => 'test_role']); - $role->grantPermission('administer entity_test_enhanced'); - $role->grantPermission('revert all entity_test_enhanced revisions'); - $role->save(); - - $user = User::create([ - 'name' => 'Test user', - ]); - $user->addRole($role->id()); - \Drupal::service('account_switcher')->switchTo($user); - - /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */ - $http_kernel = \Drupal::service('http_kernel'); - $request = Request::create($entity->toUrl('revision-revert-form')->toString()); - $response = $http_kernel->handle($request); - $this->assertEquals(200, $response->getStatusCode()); - } - -} diff --git a/web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php b/web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php deleted file mode 100644 index f90ade56a..000000000 --- a/web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php +++ /dev/null @@ -1,257 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Unit; - -use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\Context\CacheContextsManager; -use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityPublishedInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Language\Language; -use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\entity\EntityAccessControlHandler; -use Drupal\entity\EntityPermissionProvider; -use Drupal\Tests\UnitTestCase; -use Drupal\user\EntityOwnerInterface; -use Prophecy\Argument; - -/** - * @coversDefaultClass \Drupal\entity\EntityAccessControlHandler - * @group entity - */ -class EntityAccessControlHandlerTest extends UnitTestCase { - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $module_handler = $this->prophesize(ModuleHandlerInterface::class); - $module_handler->invokeAll(Argument::any(), Argument::any())->willReturn([]); - $cache_contexts_manager = $this->prophesize(CacheContextsManager::class); - $cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE); - - $container = new ContainerBuilder(); - $container->set('module_handler', $module_handler->reveal()); - $container->set('cache_contexts_manager', $cache_contexts_manager->reveal()); - \Drupal::setContainer($container); - } - - /** - * @covers ::checkAccess - * @covers ::checkEntityPermissions - * @covers ::checkEntityOwnerPermissions - * @covers ::checkCreateAccess - * - * @dataProvider accessProvider - */ - public function testAccess(EntityInterface $entity, $operation, $account, $allowed) { - $handler = new EntityAccessControlHandler($entity->getEntityType()); - $handler->setStringTranslation($this->getStringTranslationStub()); - $result = $handler->access($entity, $operation, $account); - $this->assertEquals($allowed, $result); - } - - /** - * @covers ::checkCreateAccess - * - * @dataProvider createAccessProvider - */ - public function testCreateAccess(EntityTypeInterface $entity_type, $bundle, $account, $allowed) { - $handler = new EntityAccessControlHandler($entity_type); - $handler->setStringTranslation($this->getStringTranslationStub()); - $result = $handler->createAccess($bundle, $account); - $this->assertEquals($allowed, $result); - } - - /** - * Data provider for testAccess(). - * - * @return array - * A list of testAccess method arguments. - */ - public function accessProvider() { - $data = []; - - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->id()->willReturn('green_entity'); - $entity_type->getAdminPermission()->willReturn('administer green_entity'); - $entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE); - $entity_type->getHandlerClass('permission_provider')->willReturn(EntityPermissionProvider::class); - - // User with the admin permission can do anything. - $entity = $this->buildMockEntity($entity_type->reveal()); - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission('administer green_entity')->willReturn(TRUE); - $data[] = [$entity->reveal(), 'view', $account->reveal(), TRUE]; - $data[] = [$entity->reveal(), 'update', $account->reveal(), TRUE]; - $data[] = [$entity->reveal(), 'delete', $account->reveal(), TRUE]; - - // Entity with no owner. - $entity = $this->buildMockEntity($entity_type->reveal()); - // User who has access. - $first_account = $this->prophesize(AccountInterface::class); - $first_account->id()->willReturn(6); - $first_account->hasPermission('view green_entity')->willReturn(TRUE); - $first_account->hasPermission(Argument::any())->willReturn(FALSE); - // User who doesn't have access. - $second_account = $this->prophesize(AccountInterface::class); - $second_account->id()->willReturn(7); - $second_account->hasPermission('view green_entity')->willReturn(FALSE); - $second_account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity->reveal(), 'view', $first_account->reveal(), TRUE]; - $data[] = [$entity->reveal(), 'view', $second_account->reveal(), FALSE]; - - // Entity with owner. - $entity = $this->buildMockEntity($entity_type->reveal(), 6); - // Owner. - $first_account = $this->prophesize(AccountInterface::class); - $first_account->id()->willReturn(6); - $first_account->hasPermission('update own green_entity')->willReturn(TRUE); - $first_account->hasPermission(Argument::any())->willReturn(FALSE); - // Non-owner. - $second_account = $this->prophesize(AccountInterface::class); - $second_account->id()->willReturn(7); - $second_account->hasPermission('update own green_entity')->willReturn(TRUE); - $second_account->hasPermission(Argument::any())->willReturn(FALSE); - // User who can update any. - $third_account = $this->prophesize(AccountInterface::class); - $third_account->id()->willReturn(8); - $third_account->hasPermission('update any green_entity')->willReturn(TRUE); - $third_account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity->reveal(), 'update', $first_account->reveal(), TRUE]; - $data[] = [$entity->reveal(), 'update', $second_account->reveal(), FALSE]; - $data[] = [$entity->reveal(), 'update', $third_account->reveal(), TRUE]; - - // Test the unpublished permissions. - $entity_first_other_up = $this->buildMockEntity($entity_type->reveal(), 9999, 'first', FALSE); - $entity_first_own_up = $this->buildMockEntity($entity_type->reveal(), 14, 'first', FALSE); - $entity_first_own_bundle_up = $this->buildMockEntity($entity_type->reveal(), 15, 'first', FALSE); - - $entity_second_other_up = $this->buildMockEntity($entity_type->reveal(), 9999, 'second', FALSE); - $entity_second_own_up = $this->buildMockEntity($entity_type->reveal(), 14, 'second', FALSE); - $entity_second_own_bundle_up = $this->buildMockEntity($entity_type->reveal(), 15, 'second', FALSE); - - $user_view_own_up = $this->buildMockUser(14, 'view own unpublished green_entity'); - $user_view_other = $this->buildMockUser(15, 'view green_entity'); - - $data['entity_first_other_up user_view_own_up'] = [$entity_first_other_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE]; - $data['entity_first_own_up user_view_own_up'] = [$entity_first_own_up->reveal(), 'view', $user_view_own_up->reveal(), TRUE]; - $data['entity_first_own_bundle_up user_view_own_up'] = [$entity_first_own_bundle_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE]; - $data['entity_second_other_up user_view_own_up'] = [$entity_second_other_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE]; - $data['entity_second_own_up user_view_own_up'] = [$entity_second_own_up->reveal(), 'view', $user_view_own_up->reveal(), TRUE]; - $data['entity_second_own_bundle_up user_view_own_up'] = [$entity_second_own_bundle_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE]; - - $data['entity_first_other_up user_view_other'] = [$entity_first_other_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_first_own_up user_view_other'] = [$entity_first_own_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_first_own_bundle_up user_view_other'] = [$entity_first_own_bundle_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_second_other_up user_view_other'] = [$entity_second_other_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_second_own_up user_view_other'] = [$entity_second_own_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_second_own_bundle_up user_view_other'] = [$entity_second_own_bundle_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - - return $data; - } - - /** - * Data provider for testCreateAccess(). - * - * @return array - * A list of testCreateAccess method arguments. - */ - public function createAccessProvider() { - $data = []; - - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->id()->willReturn('green_entity'); - $entity_type->getAdminPermission()->willReturn('administer green_entity'); - $entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE); - $entity_type->getHandlerClass('permission_provider')->willReturn(EntityPermissionProvider::class); - - // User with the admin permission. - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission('administer green_entity')->willReturn(TRUE); - $data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE]; - - // Ordinary user. - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission('create green_entity')->willReturn(TRUE); - $account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE]; - - // Ordinary user, entity with a bundle. - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission('create first_bundle green_entity')->willReturn(TRUE); - $account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity_type->reveal(), 'first_bundle', $account->reveal(), TRUE]; - - // User with no permissions. - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity_type->reveal(), NULL, $account->reveal(), FALSE]; - - return $data; - } - - /** - * Builds a mock entity. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * @param string $owner_id - * The owner ID. - * - * @return \Prophecy\Prophecy\ObjectProphecy - * The entity mock. - */ - protected function buildMockEntity(EntityTypeInterface $entity_type, $owner_id = NULL, $bundle = NULL, $published = NULL) { - $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED; - $entity = $this->prophesize(ContentEntityInterface::class); - if (isset($published)) { - $entity->willImplement(EntityPublishedInterface::class); - } - if ($owner_id) { - $entity->willImplement(EntityOwnerInterface::class); - } - if (isset($published)) { - $entity->isPublished()->willReturn($published); - } - if ($owner_id) { - $entity->getOwnerId()->willReturn($owner_id); - } - - $entity->bundle()->willReturn($bundle ?: $entity_type->id()); - $entity->isNew()->willReturn(FALSE); - $entity->uuid()->willReturn('fake uuid'); - $entity->id()->willReturn('fake id'); - $entity->getRevisionId()->willReturn(NULL); - $entity->language()->willReturn(new Language(['id' => $langcode])); - $entity->getEntityTypeId()->willReturn($entity_type->id()); - $entity->getEntityType()->willReturn($entity_type); - $entity->getCacheContexts()->willReturn([]); - $entity->getCacheTags()->willReturn([]); - $entity->getCacheMaxAge()->willReturn(Cache::PERMANENT); - - - return $entity; - } - - protected function buildMockUser($uid, $permission) { - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn($uid); - $account->hasPermission($permission)->willReturn(TRUE); - $account->hasPermission(Argument::any())->willReturn(FALSE); - return $account; - } - -} diff --git a/web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php b/web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php deleted file mode 100644 index e38021438..000000000 --- a/web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php +++ /dev/null @@ -1,177 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Unit; - -use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\EntityPublishedInterface; -use Drupal\Core\Entity\EntityTypeBundleInfoInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\entity\EntityPermissionProvider; -use Drupal\Tests\UnitTestCase; -use Drupal\user\EntityOwnerInterface; - -/** - * @coversDefaultClass \Drupal\entity\EntityPermissionProvider - * @group entity - */ -class EntityPermissionProviderTest extends UnitTestCase { - - /** - * The entity permission provider. - * - * @var \Drupal\entity\EntityPermissionProviderInterface - */ - protected $permissionProvider; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $entity_type_bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class); - $entity_type_bundle_info->getBundleInfo('white_entity')->willReturn([ - 'first' => ['label' => 'First'], - 'second' => ['label' => 'Second'], - ]); - $entity_type_bundle_info->getBundleInfo('black_entity')->willReturn([ - 'third' => ['label' => 'Third'], - ]); - $entity_type_bundle_info->getBundleInfo('pink_entity')->willReturn([ - 'third' => ['label' => 'Third'], - ]); - $this->permissionProvider = new EntityPermissionProvider($entity_type_bundle_info->reveal()); - $this->permissionProvider->setStringTranslation($this->getStringTranslationStub()); - } - - /** - * @covers ::buildPermissions - * - * @dataProvider entityTypeProvider - */ - public function testBuildPermissions(EntityTypeInterface $entity_type, array $expected_permissions) { - $permissions = $this->permissionProvider->buildPermissions($entity_type); - $this->assertEquals(array_keys($expected_permissions), array_keys($permissions)); - foreach ($permissions as $name => $permission) { - $this->assertEquals('entity_module_test', $permission['provider']); - $this->assertEquals($expected_permissions[$name], $permission['title']); - } - } - - /** - * Data provider for testBuildPermissions(). - * - * @return array - * A list of testBuildPermissions method arguments. - */ - public function entityTypeProvider() { - $data = []; - // Content entity type. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('green_entity'); - $entity_type->getSingularLabel()->willReturn('green entity'); - $entity_type->getPluralLabel()->willReturn('green entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); - $entity_type->getPermissionGranularity()->willReturn('entity_type'); - $expected_permissions = [ - 'administer green_entity' => 'Administer green entities', - 'access green_entity overview' => 'Access the green entities overview page', - 'create green_entity' => 'Create green entities', - 'update green_entity' => 'Update green entities', - 'delete green_entity' => 'Delete green entities', - 'view green_entity' => 'View green entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - // Content entity type with owner. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('blue_entity'); - $entity_type->getSingularLabel()->willReturn('blue entity'); - $entity_type->getPluralLabel()->willReturn('blue entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); - $entity_type->getPermissionGranularity()->willReturn('entity_type'); - $expected_permissions = [ - 'administer blue_entity' => 'Administer blue entities', - 'access blue_entity overview' => 'Access the blue entities overview page', - 'create blue_entity' => 'Create blue entities', - 'update any blue_entity' => 'Update any blue entity', - 'update own blue_entity' => 'Update own blue entities', - 'delete any blue_entity' => 'Delete any blue entity', - 'delete own blue_entity' => 'Delete own blue entities', - 'view blue_entity' => 'View blue entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - // Content entity type with bundles. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('white_entity'); - $entity_type->getSingularLabel()->willReturn('white entity'); - $entity_type->getPluralLabel()->willReturn('white entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); - $entity_type->getPermissionGranularity()->willReturn('bundle'); - $expected_permissions = [ - 'administer white_entity' => 'Administer white entities', - 'access white_entity overview' => 'Access the white entities overview page', - 'create first white_entity' => 'First: Create white entities', - 'update first white_entity' => 'First: Update white entities', - 'delete first white_entity' => 'First: Delete white entities', - 'create second white_entity' => 'Second: Create white entities', - 'update second white_entity' => 'Second: Update white entities', - 'delete second white_entity' => 'Second: Delete white entities', - 'view white_entity' => 'View white entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - // Content entity type with bundles and owner. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('black_entity'); - $entity_type->getSingularLabel()->willReturn('black entity'); - $entity_type->getPluralLabel()->willReturn('black entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); - $entity_type->getPermissionGranularity()->willReturn('bundle'); - $expected_permissions = [ - 'administer black_entity' => 'Administer black entities', - 'access black_entity overview' => 'Access the black entities overview page', - 'create third black_entity' => 'Third: Create black entities', - 'update any third black_entity' => 'Third: Update any black entity', - 'update own third black_entity' => 'Third: Update own black entities', - 'delete any third black_entity' => 'Third: Delete any black entity', - 'delete own third black_entity' => 'Third: Delete own black entities', - 'view black_entity' => 'View black entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - // Content entity type with bundles and owner and entity published. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('pink_entity'); - $entity_type->getSingularLabel()->willReturn('pink entity'); - $entity_type->getPluralLabel()->willReturn('pink entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(TRUE); - $entity_type->getPermissionGranularity()->willReturn('bundle'); - $expected_permissions = [ - 'administer pink_entity' => 'Administer pink entities', - 'access pink_entity overview' => 'Access the pink entities overview page', - 'view own unpublished pink_entity' => 'View own unpublished pink entities', - 'create third pink_entity' => 'Third: Create pink entities', - 'update any third pink_entity' => 'Third: Update any pink entity', - 'update own third pink_entity' => 'Third: Update own pink entities', - 'delete any third pink_entity' => 'Third: Delete any pink entity', - 'delete own third pink_entity' => 'Third: Delete own pink entities', - 'view pink_entity' => 'View pink entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - return $data; - } - -} diff --git a/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php b/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php deleted file mode 100644 index c788a5b48..000000000 --- a/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php +++ /dev/null @@ -1,299 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Unit; - -use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\Context\CacheContextsManager; -use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityPublishedInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Language\Language; -use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\entity\UncacheableEntityAccessControlHandler; -use Drupal\entity\UncacheableEntityPermissionProvider; -use Drupal\Tests\UnitTestCase; -use Drupal\user\EntityOwnerInterface; -use Prophecy\Argument; - -/** - * @coversDefaultClass \Drupal\entity\EntityAccessControlHandler - * @group entity - */ -class UncacheableEntityAccessControlHandlerTest extends UnitTestCase { - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $module_handler = $this->prophesize(ModuleHandlerInterface::class); - $module_handler->invokeAll(Argument::any(), Argument::any())->willReturn([]); - $cache_contexts_manager = $this->prophesize(CacheContextsManager::class); - $cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE); - - $container = new ContainerBuilder(); - $container->set('module_handler', $module_handler->reveal()); - $container->set('cache_contexts_manager', $cache_contexts_manager->reveal()); - \Drupal::setContainer($container); - } - - /** - * @covers ::checkAccess - * @covers ::checkEntityPermissions - * @covers ::checkEntityOwnerPermissions - * @covers ::checkCreateAccess - * - * @dataProvider accessProvider - */ - public function testAccess(EntityInterface $entity, $operation, $account, $allowed) { - $handler = new UncacheableEntityAccessControlHandler($entity->getEntityType()); - $handler->setStringTranslation($this->getStringTranslationStub()); - $result = $handler->access($entity, $operation, $account); - $this->assertEquals($allowed, $result); - } - - /** - * @covers ::checkCreateAccess - * - * @dataProvider createAccessProvider - */ - public function testCreateAccess(EntityTypeInterface $entity_type, $bundle, $account, $allowed) { - $handler = new UncacheableEntityAccessControlHandler($entity_type); - $handler->setStringTranslation($this->getStringTranslationStub()); - $result = $handler->createAccess($bundle, $account); - $this->assertEquals($allowed, $result); - } - - /** - * Data provider for testAccess(). - * - * @return array - * A list of testAccess method arguments. - */ - public function accessProvider() { - $data = []; - - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->id()->willReturn('green_entity'); - $entity_type->getAdminPermission()->willReturn('administer green_entity'); - $entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE); - $entity_type->getHandlerClass('permission_provider')->willReturn(UncacheableEntityPermissionProvider::class); - - // User with the admin permission can do anything. - $entity = $this->buildMockEntity($entity_type->reveal()); - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission('administer green_entity')->willReturn(TRUE); - $data[] = [$entity->reveal(), 'view', $account->reveal(), TRUE]; - $data[] = [$entity->reveal(), 'update', $account->reveal(), TRUE]; - $data[] = [$entity->reveal(), 'delete', $account->reveal(), TRUE]; - - // Entity with no owner. - $entity = $this->buildMockEntity($entity_type->reveal()); - // User who has access. - $first_account = $this->prophesize(AccountInterface::class); - $first_account->id()->willReturn(6); - $first_account->hasPermission('view green_entity')->willReturn(TRUE); - $first_account->hasPermission(Argument::any())->willReturn(FALSE); - // User who doesn't have access. - $second_account = $this->prophesize(AccountInterface::class); - $second_account->id()->willReturn(7); - $second_account->hasPermission('view green_entity')->willReturn(FALSE); - $second_account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity->reveal(), 'view', $first_account->reveal(), TRUE]; - $data[] = [$entity->reveal(), 'view', $second_account->reveal(), FALSE]; - - // Entity with owner. - $entity = $this->buildMockEntity($entity_type->reveal(), 6); - // Owner. - $first_account = $this->prophesize(AccountInterface::class); - $first_account->id()->willReturn(6); - $first_account->hasPermission('update own green_entity')->willReturn(TRUE); - $first_account->hasPermission(Argument::any())->willReturn(FALSE); - // Non-owner. - $second_account = $this->prophesize(AccountInterface::class); - $second_account->id()->willReturn(7); - $second_account->hasPermission('update own green_entity')->willReturn(TRUE); - $second_account->hasPermission(Argument::any())->willReturn(FALSE); - // User who can update any. - $third_account = $this->prophesize(AccountInterface::class); - $third_account->id()->willReturn(8); - $third_account->hasPermission('update any green_entity')->willReturn(TRUE); - $third_account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity->reveal(), 'update', $first_account->reveal(), TRUE]; - $data[] = [$entity->reveal(), 'update', $second_account->reveal(), FALSE]; - $data[] = [$entity->reveal(), 'update', $third_account->reveal(), TRUE]; - - // Per bundle permissions. - $entity_first_other = $this->buildMockEntity($entity_type->reveal(), 9999, 'first'); - $entity_first_own = $this->buildMockEntity($entity_type->reveal(), 10, 'first'); - $entity_first_own_bundle = $this->buildMockEntity($entity_type->reveal(), 12, 'first'); - - $entity_second_other = $this->buildMockEntity($entity_type->reveal(), 9999, 'second'); - $entity_second_own = $this->buildMockEntity($entity_type->reveal(), 10, 'second'); - $entity_second_own_bundle = $this->buildMockEntity($entity_type->reveal(), 12, 'second'); - - $user_view_any = $this->buildMockUser(9, 'view any green_entity'); - $user_view_own = $this->buildMockUser(10, 'view own green_entity'); - $user_view_bundle_any = $this->buildMockUser(11, 'view any first green_entity'); - $user_view_bundle_own = $this->buildMockUser(12, 'view own first green_entity'); - - $data['entity_first_other user_view_any'] = [$entity_first_other->reveal(), 'view', $user_view_any->reveal(), TRUE]; - $data['entity_first_own user_view_any'] = [$entity_first_own->reveal(), 'view', $user_view_any->reveal(), TRUE]; - $data['entity_first_own_bundle user_view_any'] = [$entity_first_own_bundle->reveal(), 'view', $user_view_any->reveal(), TRUE]; - $data['entity_second_other user_view_any'] = [$entity_second_other->reveal(), 'view', $user_view_any->reveal(), TRUE]; - $data['entity_second_own user_view_any'] = [$entity_second_own->reveal(), 'view', $user_view_any->reveal(), TRUE]; - $data['entity_second_own_bundle user_view_any'] = [$entity_second_own_bundle->reveal(), 'view', $user_view_any->reveal(), TRUE]; - - $data['entity_first_other user_view_own'] = [$entity_first_other->reveal(), 'view', $user_view_own->reveal(), FALSE]; - $data['entity_first_own user_view_own'] = [$entity_first_own->reveal(), 'view', $user_view_own->reveal(), TRUE]; - $data['entity_first_own_bundle user_view_own'] = [$entity_first_own_bundle->reveal(), 'view', $user_view_own->reveal(), FALSE]; - $data['entity_second_other user_view_own'] = [$entity_second_other->reveal(), 'view', $user_view_own->reveal(), FALSE]; - $data['entity_second_own user_view_own'] = [$entity_second_own->reveal(), 'view', $user_view_own->reveal(), TRUE]; - $data['entity_second_own_bundle user_view_own'] = [$entity_second_own_bundle->reveal(), 'view', $user_view_own->reveal(), FALSE]; - - $data['entity_first_other user_view_bundle_any'] = [$entity_first_other->reveal(), 'view', $user_view_bundle_any->reveal(), TRUE]; - $data['entity_first_own user_view_bundle_any'] = [$entity_first_own->reveal(), 'view', $user_view_bundle_any->reveal(), TRUE]; - $data['entity_first_own_bundle user_view_bundle_any'] = [$entity_first_own_bundle->reveal(), 'view', $user_view_bundle_any->reveal(), TRUE]; - $data['entity_second_other user_view_bundle_any'] = [$entity_second_other->reveal(), 'view', $user_view_bundle_any->reveal(), FALSE]; - $data['entity_second_own user_view_bundle_any'] = [$entity_second_own->reveal(), 'view', $user_view_bundle_any->reveal(), FALSE]; - $data['entity_second_own_bundle user_view_bundle_any'] = [$entity_second_own_bundle->reveal(), 'view', $user_view_bundle_any->reveal(), FALSE]; - - $data['entity_first_other user_view_bundle_any'] = [$entity_first_other->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE]; - $data['entity_first_own user_view_bundle_any'] = [$entity_first_own->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE]; - $data['entity_first_own_bundle user_view_bundle_any'] = [$entity_first_own_bundle->reveal(), 'view', $user_view_bundle_own->reveal(), TRUE]; - $data['entity_second_other user_view_bundle_any'] = [$entity_second_other->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE]; - $data['entity_second_own user_view_bundle_any'] = [$entity_second_own->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE]; - $data['entity_second_own_bundle user_view_bundle_any'] = [$entity_second_own_bundle->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE]; - - // Test the unpublished permissions. - $entity_first_other_up = $this->buildMockEntity($entity_type->reveal(), 9999, 'first', FALSE); - $entity_first_own_up = $this->buildMockEntity($entity_type->reveal(), 14, 'first', FALSE); - $entity_first_own_bundle_up = $this->buildMockEntity($entity_type->reveal(), 15, 'first', FALSE); - - $entity_second_other_up = $this->buildMockEntity($entity_type->reveal(), 9999, 'second', FALSE); - $entity_second_own_up = $this->buildMockEntity($entity_type->reveal(), 14, 'second', FALSE); - $entity_second_own_bundle_up = $this->buildMockEntity($entity_type->reveal(), 15, 'second', FALSE); - - $user_view_own_up = $this->buildMockUser(14, 'view own unpublished green_entity'); - $user_view_other = $this->buildMockUser(15, 'view green_entity'); - - $data['entity_first_other_up user_view_own_up'] = [$entity_first_other_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE]; - $data['entity_first_own_up user_view_own_up'] = [$entity_first_own_up->reveal(), 'view', $user_view_own_up->reveal(), TRUE]; - $data['entity_first_own_bundle_up user_view_own_up'] = [$entity_first_own_bundle_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE]; - $data['entity_second_other_up user_view_own_up'] = [$entity_second_other_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE]; - $data['entity_second_own_up user_view_own_up'] = [$entity_second_own_up->reveal(), 'view', $user_view_own_up->reveal(), TRUE]; - $data['entity_second_own_bundle_up user_view_own_up'] = [$entity_second_own_bundle_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE]; - - $data['entity_first_other_up user_view_other'] = [$entity_first_other_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_first_own_up user_view_other'] = [$entity_first_own_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_first_own_bundle_up user_view_other'] = [$entity_first_own_bundle_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_second_other_up user_view_other'] = [$entity_second_other_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_second_own_up user_view_other'] = [$entity_second_own_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - $data['entity_second_own_bundle_up user_view_other'] = [$entity_second_own_bundle_up->reveal(), 'view', $user_view_other->reveal(), FALSE]; - - return $data; - } - - /** - * Data provider for testCreateAccess(). - * - * @return array - * A list of testCreateAccess method arguments. - */ - public function createAccessProvider() { - $data = []; - - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->id()->willReturn('green_entity'); - $entity_type->getAdminPermission()->willReturn('administer green_entity'); - $entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE); - $entity_type->getHandlerClass('permission_provider')->willReturn(UncacheableEntityPermissionProvider::class); - - // User with the admin permission. - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission('administer green_entity')->willReturn(TRUE); - $data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE]; - - // Ordinary user. - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission('create green_entity')->willReturn(TRUE); - $account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE]; - - // Ordinary user, entity with a bundle. - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission('create first_bundle green_entity')->willReturn(TRUE); - $account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity_type->reveal(), 'first_bundle', $account->reveal(), TRUE]; - - // User with no permissions. - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn(6); - $account->hasPermission(Argument::any())->willReturn(FALSE); - $data[] = [$entity_type->reveal(), NULL, $account->reveal(), FALSE]; - - return $data; - } - - /** - * Builds a mock entity. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * @param string $owner_id - * The owner ID. - * - * @return \Prophecy\Prophecy\ObjectProphecy - * The entity mock. - */ - protected function buildMockEntity(EntityTypeInterface $entity_type, $owner_id = NULL, $bundle = NULL, $published = NULL) { - $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED; - $entity = $this->prophesize(ContentEntityInterface::class); - if (isset($published)) { - $entity->willImplement(EntityPublishedInterface::class); - } - if ($owner_id) { - $entity->willImplement(EntityOwnerInterface::class); - } - if (isset($published)) { - $entity->isPublished()->willReturn($published); - } - if ($owner_id) { - $entity->getOwnerId()->willReturn($owner_id); - } - - $entity->bundle()->willReturn($bundle ?: $entity_type->id()); - $entity->isNew()->willReturn(FALSE); - $entity->uuid()->willReturn('fake uuid'); - $entity->id()->willReturn('fake id'); - $entity->getRevisionId()->willReturn(NULL); - $entity->language()->willReturn(new Language(['id' => $langcode])); - $entity->getEntityTypeId()->willReturn($entity_type->id()); - $entity->getEntityType()->willReturn($entity_type); - $entity->getCacheContexts()->willReturn([]); - $entity->getCacheTags()->willReturn([]); - $entity->getCacheMaxAge()->willReturn(Cache::PERMANENT); - - - return $entity; - } - - protected function buildMockUser($uid, $permission) { - $account = $this->prophesize(AccountInterface::class); - $account->id()->willReturn($uid); - $account->hasPermission($permission)->willReturn(TRUE); - $account->hasPermission(Argument::any())->willReturn(FALSE); - return $account; - } - -} diff --git a/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php b/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php deleted file mode 100644 index afaab7ce2..000000000 --- a/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php +++ /dev/null @@ -1,186 +0,0 @@ -<?php - -namespace Drupal\Tests\entity\Unit; - -use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\EntityPublishedInterface; -use Drupal\Core\Entity\EntityTypeBundleInfoInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\entity\UncacheableEntityPermissionProvider; -use Drupal\Tests\UnitTestCase; -use Drupal\user\EntityOwnerInterface; - -/** - * @coversDefaultClass \Drupal\entity\UncacheableEntityPermissionProvider - * @group entity - */ -class UncacheableEntityPermissionProviderTest extends UnitTestCase { - - /** - * The entity permission provider. - * - * @var \Drupal\entity\EntityPermissionProviderInterface - */ - protected $permissionProvider; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $entity_type_bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class); - $entity_type_bundle_info->getBundleInfo('white_entity')->willReturn([ - 'first' => ['label' => 'First'], - 'second' => ['label' => 'Second'], - ]); - $entity_type_bundle_info->getBundleInfo('black_entity')->willReturn([ - 'third' => ['label' => 'Third'], - ]); - $entity_type_bundle_info->getBundleInfo('pink_entity')->willReturn([ - 'third' => ['label' => 'Third'], - ]); - $this->permissionProvider = new UncacheableEntityPermissionProvider($entity_type_bundle_info->reveal()); - $this->permissionProvider->setStringTranslation($this->getStringTranslationStub()); - } - - /** - * @covers ::buildPermissions - * - * @dataProvider entityTypeProvider - */ - public function testBuildPermissions(EntityTypeInterface $entity_type, array $expected_permissions) { - $permissions = $this->permissionProvider->buildPermissions($entity_type); - $this->assertEquals(array_keys($expected_permissions), array_keys($permissions)); - foreach ($permissions as $name => $permission) { - $this->assertEquals('entity_module_test', $permission['provider']); - $this->assertEquals($expected_permissions[$name], $permission['title']); - } - } - - /** - * Data provider for testBuildPermissions(). - * - * @return array - * A list of testBuildPermissions method arguments. - */ - public function entityTypeProvider() { - $data = []; - // Content entity type. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('green_entity'); - $entity_type->getSingularLabel()->willReturn('green entity'); - $entity_type->getPluralLabel()->willReturn('green entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); - $entity_type->getPermissionGranularity()->willReturn('entity_type'); - $expected_permissions = [ - 'administer green_entity' => 'Administer green entities', - 'access green_entity overview' => 'Access the green entities overview page', - 'create green_entity' => 'Create green entities', - 'update green_entity' => 'Update green entities', - 'delete green_entity' => 'Delete green entities', - 'view any green_entity' => 'View any green entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - // Content entity type with owner. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('blue_entity'); - $entity_type->getSingularLabel()->willReturn('blue entity'); - $entity_type->getPluralLabel()->willReturn('blue entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); - $entity_type->getPermissionGranularity()->willReturn('entity_type'); - $expected_permissions = [ - 'administer blue_entity' => 'Administer blue entities', - 'access blue_entity overview' => 'Access the blue entities overview page', - 'create blue_entity' => 'Create blue entities', - 'update any blue_entity' => 'Update any blue entity', - 'update own blue_entity' => 'Update own blue entities', - 'delete any blue_entity' => 'Delete any blue entity', - 'delete own blue_entity' => 'Delete own blue entities', - 'view any blue_entity' => 'View any blue entities', - 'view own blue_entity' => 'View own blue entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - // Content entity type with bundles. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('white_entity'); - $entity_type->getSingularLabel()->willReturn('white entity'); - $entity_type->getPluralLabel()->willReturn('white entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); - $entity_type->getPermissionGranularity()->willReturn('bundle'); - $expected_permissions = [ - 'administer white_entity' => 'Administer white entities', - 'access white_entity overview' => 'Access the white entities overview page', - 'create first white_entity' => 'First: Create white entities', - 'update first white_entity' => 'First: Update white entities', - 'delete first white_entity' => 'First: Delete white entities', - 'create second white_entity' => 'Second: Create white entities', - 'update second white_entity' => 'Second: Update white entities', - 'delete second white_entity' => 'Second: Delete white entities', - 'view any white_entity' => 'View any white entities', - 'view any first white_entity' => 'First: View any white entities', - 'view any second white_entity' => 'Second: View any white entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - // Content entity type with bundles and owner. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('black_entity'); - $entity_type->getSingularLabel()->willReturn('black entity'); - $entity_type->getPluralLabel()->willReturn('black entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); - $entity_type->getPermissionGranularity()->willReturn('bundle'); - $expected_permissions = [ - 'administer black_entity' => 'Administer black entities', - 'access black_entity overview' => 'Access the black entities overview page', - 'create third black_entity' => 'Third: Create black entities', - 'update any third black_entity' => 'Third: Update any black entity', - 'update own third black_entity' => 'Third: Update own black entities', - 'delete any third black_entity' => 'Third: Delete any black entity', - 'delete own third black_entity' => 'Third: Delete own black entities', - 'view any black_entity' => 'View any black entities', - 'view own black_entity' => 'View own black entities', - 'view any third black_entity' => 'Third: View any black entities', - 'view own third black_entity' => 'Third: View own black entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - // Content entity type with bundles and owner and entity published. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_type->getProvider()->willReturn('entity_module_test'); - $entity_type->id()->willReturn('pink_entity'); - $entity_type->getSingularLabel()->willReturn('pink entity'); - $entity_type->getPluralLabel()->willReturn('pink entities'); - $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); - $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(TRUE); - $entity_type->getPermissionGranularity()->willReturn('bundle'); - $expected_permissions = [ - 'administer pink_entity' => 'Administer pink entities', - 'access pink_entity overview' => 'Access the pink entities overview page', - 'view own unpublished pink_entity' => 'View own unpublished pink entities', - 'create third pink_entity' => 'Third: Create pink entities', - 'update any third pink_entity' => 'Third: Update any pink entity', - 'update own third pink_entity' => 'Third: Update own pink entities', - 'delete any third pink_entity' => 'Third: Delete any pink entity', - 'delete own third pink_entity' => 'Third: Delete own pink entities', - 'view any pink_entity' => 'View any pink entities', - 'view own pink_entity' => 'View own pink entities', - 'view any third pink_entity' => 'Third: View any pink entities', - 'view own third pink_entity' => 'Third: View own pink entities', - ]; - $data[] = [$entity_type->reveal(), $expected_permissions]; - - return $data; - } - -} diff --git a/web/modules/contrib/entity_browser/config/schema/entity_browser.schema.yml b/web/modules/contrib/entity_browser/config/schema/entity_browser.schema.yml index 741679929..cf2c9ec95 100644 --- a/web/modules/contrib/entity_browser/config/schema/entity_browser.schema.yml +++ b/web/modules/contrib/entity_browser/config/schema/entity_browser.schema.yml @@ -169,6 +169,9 @@ field.widget.settings.entity_browser_entity_reference: field_widget_remove: type: boolean label: 'Field widget remove' + field_widget_replace: + type: boolean + label: 'Field widget replace' open: type: boolean label: 'Open' @@ -211,6 +214,9 @@ field.widget.settings.entity_browser_file: field_widget_remove: type: boolean label: 'Field widget remove' + field_widget_replace: + type: boolean + label: 'Field widget replace' open: type: boolean label: 'Open' diff --git a/web/modules/contrib/entity_browser/entity_browser.info.yml b/web/modules/contrib/entity_browser/entity_browser.info.yml index 5b6653b66..a2f8d429b 100644 --- a/web/modules/contrib/entity_browser/entity_browser.info.yml +++ b/web/modules/contrib/entity_browser/entity_browser.info.yml @@ -11,8 +11,8 @@ test_dependencies: - paragraphs:paragraphs configure: entity.entity_browser.collection -# Information added by Drupal.org packaging script on 2017-11-30 -version: '8.x-1.4' +# Information added by Drupal.org packaging script on 2018-09-07 +version: '8.x-1.6' core: '8.x' project: 'entity_browser' -datestamp: 1512033788 +datestamp: 1536328688 diff --git a/web/modules/contrib/entity_browser/entity_browser.views.inc b/web/modules/contrib/entity_browser/entity_browser.views.inc index 934fde06a..b2bb86de3 100644 --- a/web/modules/contrib/entity_browser/entity_browser.views.inc +++ b/web/modules/contrib/entity_browser/entity_browser.views.inc @@ -22,4 +22,19 @@ function entity_browser_views_data_alter(&$data) { ]; } } + if (\Drupal::moduleHandler()->moduleExists('search_api')) { + /** @var \Drupal\search_api\IndexInterface $index */ + foreach (\Drupal\search_api\Entity\Index::loadMultiple() as $index) { + $key = 'search_api_index_' . $index->id(); + $data[$key]['entity_browser_select'] = [ + 'title' => t('Entity browser bulk select form for Search API views'), + 'help' => t('Add a form element that lets you use a view as a base to select entities in entity browser.'), + 'field' => [ + 'id' => 'entity_browser_search_api_select', + 'real field' => 'id', + ], + ]; + } + } + } diff --git a/web/modules/contrib/entity_browser/js/entity_browser.entity_reference.js b/web/modules/contrib/entity_browser/js/entity_browser.entity_reference.js index 882799afb..9034de6ad 100644 --- a/web/modules/contrib/entity_browser/js/entity_browser.entity_reference.js +++ b/web/modules/contrib/entity_browser/js/entity_browser.entity_reference.js @@ -19,6 +19,21 @@ stop: Drupal.entityBrowserEntityReference.entitiesReordered }); }); + // The AJAX callback will give us a flag when we need to re-open the + // browser, most likely due to a "Replace" button being clicked. + if (typeof drupalSettings.entity_browser_reopen_browser !== 'undefined' && drupalSettings.entity_browser_reopen_browser) { + var data_drupal_selector = '[data-drupal-selector^="edit-' + drupalSettings.entity_browser_reopen_browser.replace(/_/g, '-') + '-entity-browser-entity-browser-' + '"]'; + var $launch_browser_element = $(context).find(data_drupal_selector); + if (!drupalSettings.entity_browser.iframe[$launch_browser_element.attr('data-uuid')].auto_open) { + $launch_browser_element.click(); + } + // In case this is inside a fieldset closed by default, open it so the + // user doesn't need to guess the browser is open but hidden there. + var $fieldset_summary = $launch_browser_element.closest('details').find('summary'); + if ($fieldset_summary.length && $fieldset_summary.attr('aria-expanded') === 'false') { + $fieldset_summary.click(); + } + } } }; diff --git a/web/modules/contrib/entity_browser/modules/entity_form/entity_browser_entity_form.info.yml b/web/modules/contrib/entity_browser/modules/entity_form/entity_browser_entity_form.info.yml index 3cf4ba739..10500a3aa 100644 --- a/web/modules/contrib/entity_browser/modules/entity_form/entity_browser_entity_form.info.yml +++ b/web/modules/contrib/entity_browser/modules/entity_form/entity_browser_entity_form.info.yml @@ -7,8 +7,8 @@ dependencies: - entity_browser:entity_browser - inline_entity_form:inline_entity_form -# Information added by Drupal.org packaging script on 2017-11-30 -version: '8.x-1.4' +# Information added by Drupal.org packaging script on 2018-09-07 +version: '8.x-1.6' core: '8.x' project: 'entity_browser' -datestamp: 1512033788 +datestamp: 1536328688 diff --git a/web/modules/contrib/entity_browser/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php b/web/modules/contrib/entity_browser/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php index 6ecfa7561..7901925a2 100644 --- a/web/modules/contrib/entity_browser/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php +++ b/web/modules/contrib/entity_browser/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php @@ -239,4 +239,13 @@ class EntityForm extends WidgetBase { $this->configuration['form_mode'] = $this->configuration['form_mode']['form_select']; } + /** + * {@inheritdoc} + */ + public function access() { + return $this->entityTypeManager + ->getAccessControlHandler($this->configuration['entity_type']) + ->createAccess($this->configuration['bundle'], NULL, [], TRUE); + } + } diff --git a/web/modules/contrib/entity_browser/modules/entity_form/tests/modules/entity_browser_entity_form_test/entity_browser_entity_form_test.info.yml b/web/modules/contrib/entity_browser/modules/entity_form/tests/modules/entity_browser_entity_form_test/entity_browser_entity_form_test.info.yml index b8919bf9f..13f683a81 100644 --- a/web/modules/contrib/entity_browser/modules/entity_form/tests/modules/entity_browser_entity_form_test/entity_browser_entity_form_test.info.yml +++ b/web/modules/contrib/entity_browser/modules/entity_form/tests/modules/entity_browser_entity_form_test/entity_browser_entity_form_test.info.yml @@ -8,8 +8,8 @@ dependencies: - entity_browser_entity_form - views -# Information added by Drupal.org packaging script on 2017-11-30 -version: '8.x-1.4' +# Information added by Drupal.org packaging script on 2018-09-07 +version: '8.x-1.6' core: '8.x' project: 'entity_browser' -datestamp: 1512033788 +datestamp: 1536328688 diff --git a/web/modules/contrib/entity_browser/modules/entity_form/tests/src/FunctionalJavascript/EntityFormWidgetTest.php b/web/modules/contrib/entity_browser/modules/entity_form/tests/src/FunctionalJavascript/EntityFormWidgetTest.php index aef8ebb34..75913914f 100644 --- a/web/modules/contrib/entity_browser/modules/entity_form/tests/src/FunctionalJavascript/EntityFormWidgetTest.php +++ b/web/modules/contrib/entity_browser/modules/entity_form/tests/src/FunctionalJavascript/EntityFormWidgetTest.php @@ -75,6 +75,7 @@ class EntityFormWidgetTest extends JavascriptTestBase { $account = $this->drupalCreateUser([ 'access entity_browser_test_entity_form entity browser pages', 'create foo content', + 'create article content', 'access content', ]); $this->drupalLogin($account); @@ -148,6 +149,16 @@ class EntityFormWidgetTest extends JavascriptTestBase { $parent_node = current($parent_node); $this->assertEquals(1, $parent_node->get('field_reference')->count(), 'There is one child node.'); $this->assertEquals('War is peace', $parent_node->field_reference->entity->label(), 'Child node has correct title.'); + + // Make sure entity create access is respected. + $account = $this->drupalCreateUser([ + 'access entity_browser_test_entity_form entity browser pages', + 'create foo content', + 'access content', + ]); + $this->drupalLogin($account); + $this->drupalGet('entity-browser/iframe/entity_browser_test_entity_form'); + $this->assertSession()->pageTextContains('No widgets are available.'); } } diff --git a/web/modules/contrib/entity_browser/modules/example/config/install/core.entity_form_display.node.entity_browser_test.default.yml b/web/modules/contrib/entity_browser/modules/example/config/install/core.entity_form_display.node.entity_browser_test.default.yml index de7d83cbc..0bf7b37fd 100644 --- a/web/modules/contrib/entity_browser/modules/example/config/install/core.entity_form_display.node.entity_browser_test.default.yml +++ b/web/modules/contrib/entity_browser/modules/example/config/install/core.entity_form_display.node.entity_browser_test.default.yml @@ -44,6 +44,7 @@ content: open: false field_widget_edit: true field_widget_remove: true + field_widget_replace: false third_party_settings: { } field_files1: type: entity_browser_entity_reference @@ -55,6 +56,7 @@ content: open: false field_widget_edit: true field_widget_remove: true + field_widget_replace: false third_party_settings: { } field_files_over_ajax: weight: 36 @@ -63,6 +65,7 @@ content: field_widget_display: label field_widget_edit: true field_widget_remove: true + field_widget_replace: false selection_mode: selection_edit open: false field_widget_display_settings: { } @@ -74,6 +77,7 @@ content: entity_browser: test_files field_widget_edit: true field_widget_remove: true + field_widget_replace: false open: false third_party_settings: { } type: entity_browser_file @@ -87,6 +91,7 @@ content: open: false field_widget_edit: true field_widget_remove: true + field_widget_replace: false third_party_settings: { } path: type: path diff --git a/web/modules/contrib/entity_browser/modules/example/entity_browser_example.info.yml b/web/modules/contrib/entity_browser/modules/example/entity_browser_example.info.yml index efc31b4f5..019ee6d19 100644 --- a/web/modules/contrib/entity_browser/modules/example/entity_browser_example.info.yml +++ b/web/modules/contrib/entity_browser/modules/example/entity_browser_example.info.yml @@ -11,8 +11,8 @@ dependencies: - drupal:node - drupal:image -# Information added by Drupal.org packaging script on 2017-11-30 -version: '8.x-1.4' +# Information added by Drupal.org packaging script on 2018-09-07 +version: '8.x-1.6' core: '8.x' project: 'entity_browser' -datestamp: 1512033788 +datestamp: 1536328688 diff --git a/web/modules/contrib/entity_browser/src/Controllers/EntityBrowserController.php b/web/modules/contrib/entity_browser/src/Controllers/EntityBrowserController.php index aebb4be80..c3788809d 100644 --- a/web/modules/contrib/entity_browser/src/Controllers/EntityBrowserController.php +++ b/web/modules/contrib/entity_browser/src/Controllers/EntityBrowserController.php @@ -29,8 +29,16 @@ class EntityBrowserController extends ControllerBase { * containing the edit form. */ public function entityBrowserEdit(EntityInterface $entity, Request $request) { + + // Use edit form class if it exists, otherwise use default form class. + $operation = 'default'; + $entity_type = $entity->getEntityType(); + if ($entity_type->getFormClass('edit')) { + $operation = 'edit'; + } + // Build the entity edit form. - $form_object = $this->entityTypeManager()->getFormObject($entity->getEntityTypeId(), 'edit'); + $form_object = $this->entityTypeManager()->getFormObject($entity->getEntityTypeId(), $operation); $form_object->setEntity($entity); $form_state = (new FormState()) ->setFormObject($form_object) diff --git a/web/modules/contrib/entity_browser/src/Entity/EntityBrowser.php b/web/modules/contrib/entity_browser/src/Entity/EntityBrowser.php index 53bc9f2af..be75e6c2d 100644 --- a/web/modules/contrib/entity_browser/src/Entity/EntityBrowser.php +++ b/web/modules/contrib/entity_browser/src/Entity/EntityBrowser.php @@ -303,6 +303,14 @@ class EntityBrowser extends ConfigEntityBase implements EntityBrowserInterface, */ public function getFirstWidget() { $instance_ids = $this->getWidgets()->getInstanceIds(); + $instance_ids = array_filter($instance_ids, function ($id) { + return $this->getWidget($id)->access()->isAllowed(); + }); + + if (empty($instance_ids)) { + return NULL; + } + return reset($instance_ids); } diff --git a/web/modules/contrib/entity_browser/src/EntityBrowserFormInterface.php b/web/modules/contrib/entity_browser/src/EntityBrowserFormInterface.php index 335fe96ab..d274b6ec1 100644 --- a/web/modules/contrib/entity_browser/src/EntityBrowserFormInterface.php +++ b/web/modules/contrib/entity_browser/src/EntityBrowserFormInterface.php @@ -17,4 +17,12 @@ interface EntityBrowserFormInterface extends FormInterface { */ public function setEntityBrowser(EntityBrowserInterface $entity_browser); + /** + * Gets entity browser entity, + * + * @return \Drupal\entity_browser\EntityBrowserInterface + * Entity browser entity. + */ + public function getEntityBrowser(); + } diff --git a/web/modules/contrib/entity_browser/src/EntityBrowserInterface.php b/web/modules/contrib/entity_browser/src/EntityBrowserInterface.php index cfad3254a..09a5e8673 100644 --- a/web/modules/contrib/entity_browser/src/EntityBrowserInterface.php +++ b/web/modules/contrib/entity_browser/src/EntityBrowserInterface.php @@ -123,8 +123,8 @@ interface EntityBrowserInterface extends ConfigEntityInterface { /** * Gets first widget based on weights. * - * @return string - * First widget instance ID. + * @return string|null + * First widget instance ID or NULL if no widgets are available. */ public function getFirstWidget(); diff --git a/web/modules/contrib/entity_browser/src/Form/EntityBrowserForm.php b/web/modules/contrib/entity_browser/src/Form/EntityBrowserForm.php index 340e425fd..bcecac1be 100644 --- a/web/modules/contrib/entity_browser/src/Form/EntityBrowserForm.php +++ b/web/modules/contrib/entity_browser/src/Form/EntityBrowserForm.php @@ -11,6 +11,7 @@ use Drupal\entity_browser\DisplayAjaxInterface; use Drupal\entity_browser\EntityBrowserFormInterface; use Drupal\entity_browser\EntityBrowserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Render\RendererInterface; /** * The entity browser form. @@ -38,6 +39,13 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { */ protected $selectionStorage; + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + /** * Constructs a EntityBrowserForm object. * @@ -45,10 +53,13 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { * The UUID generator service. * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $selection_storage * Selection storage. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. */ - public function __construct(UuidInterface $uuid_generator, KeyValueStoreExpirableInterface $selection_storage) { + public function __construct(UuidInterface $uuid_generator, KeyValueStoreExpirableInterface $selection_storage, RendererInterface $renderer) { $this->uuidGenerator = $uuid_generator; $this->selectionStorage = $selection_storage; + $this->renderer = $renderer; } /** @@ -57,7 +68,8 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { public static function create(ContainerInterface $container) { return new static( $container->get('uuid'), - $container->get('entity_browser.selection_storage') + $container->get('entity_browser.selection_storage'), + $container->get('renderer') ); } @@ -75,6 +87,13 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { $this->entityBrowser = $entity_browser; } + /** + * {@inheritdoc} + */ + public function getEntityBrowser() { + return $this->entityBrowser; + } + /** * Initializes form state. * @@ -124,16 +143,33 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { 'widget' => 'widget', 'selection_display' => 'selection_display', ]; + + if (!($current_widget_id = $this->getCurrentWidget($form_state))) { + drupal_set_message($this->t('No widgets are available.'), 'warning'); + return $form; + } + $this->entityBrowser ->getWidgetSelector() - ->setDefaultWidget($this->getCurrentWidget($form_state)); + ->setDefaultWidget($current_widget_id); $form[$form['#browser_parts']['widget_selector']] = $this->entityBrowser ->getWidgetSelector() ->getForm($form, $form_state); - $form[$form['#browser_parts']['widget']] = $this->entityBrowser - ->getWidgets() - ->get($this->getCurrentWidget($form_state)) - ->getForm($form, $form_state, $this->entityBrowser->getAdditionalWidgetParameters()); + + $widget = $this->entityBrowser->getWidget($current_widget_id); + if ($widget->access()->isAllowed()) { + $form[$form['#browser_parts']['widget']] = $widget->getForm($form, $form_state, $this->entityBrowser->getAdditionalWidgetParameters()); + } + else { + drupal_set_message($this->t('Access to the widget forbidden.'), 'warning'); + } + + // Add cache access cache metadata from the widgets to the form directly as + // it is affected. + foreach ($this->entityBrowser->getWidgets() as $widget) { + /** @var \Drupal\entity_browser\WidgetInterface $widget */ + $this->renderer->addCacheableDependency($form, $widget->access()); + } $form[$form['#browser_parts']['selection_display']] = $this->entityBrowser ->getSelectionDisplay() diff --git a/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/Widget/View.php b/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/Widget/View.php index f0aaa19e3..111f9b777 100644 --- a/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/Widget/View.php +++ b/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/Widget/View.php @@ -3,6 +3,7 @@ namespace Drupal\entity_browser\Plugin\EntityBrowser\Widget; use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\entity_browser\WidgetBase; @@ -135,7 +136,7 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { // When rebuilding makes no sense to keep checkboxes that were previously // selected. - if (!empty($form['view']['entity_browser_select']) && $form_state->isRebuilding()) { + if (!empty($form['view']['entity_browser_select'])) { foreach (Element::children($form['view']['entity_browser_select']) as $child) { $form['view']['entity_browser_select'][$child]['#process'][] = ['\Drupal\entity_browser\Plugin\EntityBrowser\Widget\View', 'processCheckbox']; $form['view']['entity_browser_select'][$child]['#process'][] = ['\Drupal\Core\Render\Element\Checkbox', 'processAjaxForm']; @@ -158,7 +159,10 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { * @see \Drupal\Core\Render\Element\Checkbox::processCheckbox() */ public static function processCheckbox(&$element, FormStateInterface $form_state, &$complete_form) { - $element['#checked'] = FALSE; + if ($form_state->isRebuilding()) { + $element['#checked'] = FALSE; + } + return $element; } @@ -279,4 +283,20 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { return $dependencies; } + /** + * {@inheritdoc} + */ + public function access() { + // Mark the widget as not visible if the user has no access to the view. + /** @var \Drupal\views\ViewExecutable $view */ + $view = $this->entityTypeManager + ->getStorage('view') + ->load($this->configuration['view']) + ->getExecutable(); + + + // Check if the current user has access to this view. + return AccessResult::allowedIf($view->access($this->configuration['view_display'])); + } + } diff --git a/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/WidgetSelector/DropDown.php b/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/WidgetSelector/DropDown.php index d5d32d518..ef3315a58 100644 --- a/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/WidgetSelector/DropDown.php +++ b/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/WidgetSelector/DropDown.php @@ -24,9 +24,19 @@ class DropDown extends WidgetSelectorBase { $form['#prefix'] = '<div id="entity-browser-form">'; $form['#suffix'] = '</div>'; + /** @var \Drupal\entity_browser\EntityBrowserInterface $browser */ + $browser = $form_state->getFormObject()->getEntityBrowser(); + + $widget_ids = []; + foreach ($this->widget_ids as $widget_id => $widget_name) { + if ($browser->getWidget($widget_id)->access()->isAllowed()) { + $widget_ids[$widget_id] = $widget_name; + } + } + $element['widget'] = [ '#type' => 'select', - '#options' => $this->widget_ids, + '#options' => $widget_ids, '#default_value' => $this->getDefaultWidget(), '#executes_submit_callback' => TRUE, '#limit_validation_errors' => [['widget']], diff --git a/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/WidgetSelector/Tabs.php b/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/WidgetSelector/Tabs.php index e6d511d44..0128b2907 100644 --- a/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/WidgetSelector/Tabs.php +++ b/web/modules/contrib/entity_browser/src/Plugin/EntityBrowser/WidgetSelector/Tabs.php @@ -21,6 +21,8 @@ class Tabs extends WidgetSelectorBase { */ public function getForm(array &$form = [], FormStateInterface &$form_state = NULL) { $element = []; + /** @var \Drupal\entity_browser\EntityBrowserInterface $browser */ + $browser = $form_state->getFormObject()->getEntityBrowser(); foreach ($this->widget_ids as $id => $label) { $name = 'tab_selector_' . $id; $element[$name] = [ @@ -34,6 +36,7 @@ class Tabs extends WidgetSelectorBase { '#submit' => [], '#name' => $name, '#widget_id' => $id, + '#access' => $browser->getWidget($id)->access(), ]; } diff --git a/web/modules/contrib/entity_browser/src/Plugin/Field/FieldWidget/EntityReferenceBrowserWidget.php b/web/modules/contrib/entity_browser/src/Plugin/Field/FieldWidget/EntityReferenceBrowserWidget.php index c2096b556..719869ca6 100644 --- a/web/modules/contrib/entity_browser/src/Plugin/Field/FieldWidget/EntityReferenceBrowserWidget.php +++ b/web/modules/contrib/entity_browser/src/Plugin/Field/FieldWidget/EntityReferenceBrowserWidget.php @@ -136,6 +136,7 @@ class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactor 'field_widget_display' => 'label', 'field_widget_edit' => TRUE, 'field_widget_remove' => TRUE, + 'field_widget_replace' => FALSE, 'field_widget_display_settings' => [], 'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND, ] + parent::defaultSettings(); @@ -201,6 +202,13 @@ class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactor '#default_value' => $this->getSetting('field_widget_remove'), ]; + $element['field_widget_replace'] = [ + '#title' => $this->t('Display Replace button'), + '#description' => $this->t('This button will only be displayed if there is a single entity in the current selection.'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('field_widget_replace'), + ]; + $element['open'] = [ '#title' => $this->t('Show widget details as open by default'), '#description' => $this->t('If marked, the fieldset container that wraps the browser on the entity form will be loaded initially expanded.'), @@ -420,18 +428,29 @@ class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactor */ public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) { $trigger = $form_state->getTriggeringElement(); + $reopen_browser = FALSE; // AJAX requests can be triggered by hidden "target_id" element when // entities are added or by one of the "Remove" buttons. Depending on that // we need to figure out where root of the widget is in the form structure // and use this information to return correct part of the form. + $parents = []; if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') { $parents = array_slice($trigger['#array_parents'], 0, -1); } elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) { $parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth); } + elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_replace_')) { + $parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth); + // We need to re-open the browser. Instead of just passing "TRUE", send + // to the JS the unique part of the button's name that needs to be clicked + // on to relaunch the browser. + $reopen_browser = implode("-", array_slice($trigger['#parents'], 0, -static::$deleteDepth)); + } - return NestedArray::getValue($form, $parents); + $parents = NestedArray::getValue($form, $parents); + $parents['#attached']['drupalSettings']['entity_browser_reopen_browser'] = $reopen_browser; + return $parents; } /** @@ -504,11 +523,15 @@ class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactor $classes[] = 'sortable'; } + // The "Replace" button will only be shown if this setting is enabled in the + // widget, and there is only one entity in the current selection. + $replace_button_access = $this->getSetting('field_widget_replace') && (count($entities) === 1); + return [ '#theme_wrappers' => ['container'], '#attributes' => ['class' => $classes], 'items' => array_map( - function (ContentEntityInterface $entity, $row_id) use ($field_widget_display, $details_id, $field_parents) { + function (ContentEntityInterface $entity, $row_id) use ($field_widget_display, $details_id, $field_parents, $replace_button_access) { $display = $field_widget_display->view($entity); $edit_button_access = $this->getSetting('field_widget_edit') && $entity->access('update', $this->currentUser); if ($entity->getEntityTypeId() == 'file') { @@ -541,9 +564,27 @@ class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactor '#attributes' => [ 'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(), 'data-row-id' => $row_id, + 'class' => ['remove-button'], ], '#access' => (bool) $this->getSetting('field_widget_remove'), ], + 'replace_button' => [ + '#type' => 'submit', + '#value' => $this->t('Replace'), + '#ajax' => [ + 'callback' => [get_class($this), 'updateWidgetCallback'], + 'wrapper' => $details_id, + ], + '#submit' => [[get_class($this), 'removeItemSubmit']], + '#name' => $this->fieldDefinition->getName() . '_replace_' . $entity->id() . '_' . $row_id . '_' . md5(json_encode($field_parents)), + '#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])], + '#attributes' => [ + 'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(), + 'data-row-id' => $row_id, + 'class' => ['replace-button'], + ], + '#access' => $replace_button_access, + ], 'edit_button' => [ '#type' => 'submit', '#value' => $this->t('Edit'), @@ -560,6 +601,9 @@ class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactor ], ], ], + '#attributes' => [ + 'class' => ['edit-button'], + ], '#access' => $edit_button_access, ], ]; diff --git a/web/modules/contrib/entity_browser/src/Plugin/Field/FieldWidget/FileBrowserWidget.php b/web/modules/contrib/entity_browser/src/Plugin/Field/FieldWidget/FileBrowserWidget.php index 6db9c8d47..d883f1569 100644 --- a/web/modules/contrib/entity_browser/src/Plugin/Field/FieldWidget/FileBrowserWidget.php +++ b/web/modules/contrib/entity_browser/src/Plugin/Field/FieldWidget/FileBrowserWidget.php @@ -239,7 +239,7 @@ class FileBrowserWidget extends EntityReferenceBrowserWidget { // Add the remaining columns. $current['#header'][] = $this->t('Metadata'); - $current['#header'][] = ['data' => $this->t('Operations'), 'colspan' => 2]; + $current['#header'][] = ['data' => $this->t('Operations'), 'colspan' => 3]; $current['#header'][] = $this->t('Order', [], ['context' => 'Sort order']); /** @var \Drupal\file\FileInterface[] $entities */ @@ -253,6 +253,10 @@ class FileBrowserWidget extends EntityReferenceBrowserWidget { $edit_button_access = $can_edit && $entity->access('update', $this->currentUser); } + // The "Replace" button will only be shown if this setting is enabled in + // the widget, and there is only one entity in the current selection. + $replace_button_access = $this->getSetting('field_widget_replace') && (count($entities) === 1); + $entity_id = $entity->id(); // Find the default description. @@ -356,9 +360,27 @@ class FileBrowserWidget extends EntityReferenceBrowserWidget { '#attributes' => [ 'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(), 'data-row-id' => $delta, + 'class' => ['edit-button'], ], '#access' => $edit_button_access, ], + 'replace_button' => [ + '#type' => 'submit', + '#value' => $this->t('Replace'), + '#ajax' => [ + 'callback' => [get_class($this), 'updateWidgetCallback'], + 'wrapper' => $details_id, + ], + '#submit' => [[get_class($this), 'removeItemSubmit']], + '#name' => $field_machine_name . '_replace_' . $entity_id . '_' . md5(json_encode($field_parents)), + '#limit_validation_errors' => [array_merge($field_parents, [$field_machine_name, 'target_id'])], + '#attributes' => [ + 'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(), + 'data-row-id' => $delta, + 'class' => ['replace-button'], + ], + '#access' => $replace_button_access, + ], 'remove_button' => [ '#type' => 'submit', '#value' => $this->t('Remove'), @@ -372,6 +394,7 @@ class FileBrowserWidget extends EntityReferenceBrowserWidget { '#attributes' => [ 'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(), 'data-row-id' => $delta, + 'class' => ['remove-button'], ], '#access' => (bool) $widget_settings['field_widget_remove'], ], diff --git a/web/modules/contrib/entity_browser/src/Plugin/views/field/SearchApiSelectForm.php b/web/modules/contrib/entity_browser/src/Plugin/views/field/SearchApiSelectForm.php new file mode 100644 index 000000000..a75ef1ee8 --- /dev/null +++ b/web/modules/contrib/entity_browser/src/Plugin/views/field/SearchApiSelectForm.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\entity_browser\Plugin\views\field; + +use Drupal\views\ResultRow; + +/** + * Defines a bulk operation form element that works with entity browser. + * + * @ViewsField("entity_browser_search_api_select") + */ +class SearchApiSelectForm extends SelectForm { + + /** + * {@inheritdoc} + */ + public function getRowId(ResultRow $row) { + $entity = $row->_object->getValue(); + return $entity->getEntityTypeId() . ':' . $entity->id(); + } + +} diff --git a/web/modules/contrib/entity_browser/src/Tests/ConfigUITest.php b/web/modules/contrib/entity_browser/src/Tests/ConfigUITest.php index da58a79d6..335843941 100644 --- a/web/modules/contrib/entity_browser/src/Tests/ConfigUITest.php +++ b/web/modules/contrib/entity_browser/src/Tests/ConfigUITest.php @@ -55,7 +55,7 @@ class ConfigUITest extends WebTestBase { $this->drupalLogin($this->adminUser); $this->drupalGet('/admin/config/content/entity_browser'); $this->assertResponse(200, 'Admin user is able to navigate to the entity browser listing page.'); - $this->assertText('There is no Entity browser yet.', 'Entity browsers table is empty.'); + $this->assertText('There are no entity browser entities yet.', 'Entity browsers table is empty.'); // Add page. $this->clickLink('Add Entity browser'); @@ -275,7 +275,7 @@ class ConfigUITest extends WebTestBase { $this->drupalPostForm(NULL, [], 'Delete Entity Browser'); $this->assertText('Entity browser Test entity browser was deleted.', 'Confirmation message found.'); - $this->assertText('There is no Entity browser yet.', 'Entity browsers table is empty.'); + $this->assertText('There are no entity browser entities yet.', 'Entity browsers table is empty.'); $this->drupalLogout(); } diff --git a/web/modules/contrib/entity_browser/src/WidgetBase.php b/web/modules/contrib/entity_browser/src/WidgetBase.php index da094e4cf..f5d2ea7a8 100644 --- a/web/modules/contrib/entity_browser/src/WidgetBase.php +++ b/web/modules/contrib/entity_browser/src/WidgetBase.php @@ -3,6 +3,7 @@ namespace Drupal\entity_browser; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Form\FormStateInterface; @@ -372,4 +373,11 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain } } + /** + * {@inheritdoc} + */ + public function access() { + return AccessResult::allowed(); + } + } diff --git a/web/modules/contrib/entity_browser/src/WidgetInterface.php b/web/modules/contrib/entity_browser/src/WidgetInterface.php index 0a1140f7a..c88f4b82d 100644 --- a/web/modules/contrib/entity_browser/src/WidgetInterface.php +++ b/web/modules/contrib/entity_browser/src/WidgetInterface.php @@ -118,4 +118,12 @@ interface WidgetInterface extends PluginInspectionInterface, ConfigurablePluginI */ public function requiresJsCommands(); + /** + * Defines if the widget is visible / accessible in a given context. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function access(); + } diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_ief_test/config/install/core.entity_form_display.media.ief_media_bundle.default.yml b/web/modules/contrib/entity_browser/tests/modules/entity_browser_ief_test/config/install/core.entity_form_display.media.ief_media_bundle.default.yml index 6288723e9..f2ab14433 100644 --- a/web/modules/contrib/entity_browser/tests/modules/entity_browser_ief_test/config/install/core.entity_form_display.media.ief_media_bundle.default.yml +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_ief_test/config/install/core.entity_form_display.media.ief_media_bundle.default.yml @@ -18,6 +18,7 @@ content: field_widget_display: label field_widget_edit: true field_widget_remove: true + field_widget_replace: false open: true selection_mode: selection_edit field_widget_display_settings: { } diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_ief_test/entity_browser_ief_test.info.yml b/web/modules/contrib/entity_browser/tests/modules/entity_browser_ief_test/entity_browser_ief_test.info.yml index e332fec4c..f6de948d3 100644 --- a/web/modules/contrib/entity_browser/tests/modules/entity_browser_ief_test/entity_browser_ief_test.info.yml +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_ief_test/entity_browser_ief_test.info.yml @@ -12,8 +12,8 @@ dependencies: - inline_entity_form - views -# Information added by Drupal.org packaging script on 2017-11-30 -version: '8.x-1.4' +# Information added by Drupal.org packaging script on 2018-09-07 +version: '8.x-1.6' core: '8.x' project: 'entity_browser' -datestamp: 1512033788 +datestamp: 1536328688 diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/config/install/entity_browser.browser.test_entity_browser_file.yml b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/config/install/entity_browser.browser.test_entity_browser_file.yml index 65178f0e2..d75ba00ea 100644 --- a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/config/install/entity_browser.browser.test_entity_browser_file.yml +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/config/install/entity_browser.browser.test_entity_browser_file.yml @@ -35,3 +35,10 @@ widgets: weight: -10 label: view id: view + cbc59500-04ab-4395-b063-c561f0e3bf80: + id: dummy + label: dummy + weight: -8 + uuid: cbc59500-04ab-4395-b063-c561f0e3bf80 + settings: + text: 'This is dummy widget.' diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/entity_browser_test.info.yml b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/entity_browser_test.info.yml index 97dfafe3d..391f392e2 100644 --- a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/entity_browser_test.info.yml +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/entity_browser_test.info.yml @@ -10,8 +10,8 @@ dependencies: - node - views -# Information added by Drupal.org packaging script on 2017-11-30 -version: '8.x-1.4' +# Information added by Drupal.org packaging script on 2018-09-07 +version: '8.x-1.6' core: '8.x' project: 'entity_browser' -datestamp: 1512033788 +datestamp: 1536328688 diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/entity_browser_test.services.yml b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/entity_browser_test.services.yml new file mode 100644 index 000000000..17e1f17d6 --- /dev/null +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/entity_browser_test.services.yml @@ -0,0 +1,5 @@ +services: + cache_context.eb_dummy: + class: Drupal\entity_browser_test\Cache\Context\DummyCacheContext + tags: + - { name: cache.context} diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/src/Cache/Context/DummyCacheContext.php b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/src/Cache/Context/DummyCacheContext.php new file mode 100644 index 000000000..ee539059d --- /dev/null +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/src/Cache/Context/DummyCacheContext.php @@ -0,0 +1,36 @@ +<?php + +namespace Drupal\entity_browser_test\Cache\Context; + +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\Context\CacheContextInterface; + +/** + * Dummy cache context for Entity browser test purposes. + * + * Cache context ID: 'eb_dummy'. + */ +class DummyCacheContext implements CacheContextInterface { + + /** + * {@inheritdoc} + */ + public static function getLabel() { + return t('Dummy context'); + } + + /** + * {@inheritdoc} + */ + public function getContext() { + return "dummy"; + } + + /** + * {@inheritdoc} + */ + public function getCacheableMetadata() { + return new CacheableMetadata(); + } + +} diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/DummyWidget.php b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/DummyWidget.php index 7d19edfd8..820d27149 100644 --- a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/DummyWidget.php +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/DummyWidget.php @@ -2,6 +2,7 @@ namespace Drupal\entity_browser_test\Plugin\EntityBrowser\Widget; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Form\FormStateInterface; use Drupal\entity_browser\WidgetBase; @@ -55,4 +56,19 @@ class DummyWidget extends WidgetBase { return $form_state->getValue('dummy_entities', []); } + /** + * {@inheritdoc} + */ + public function access() { + if (\Drupal::state()->get('eb_test_dummy_widget_access', TRUE)) { + $access = AccessResult::allowed(); + $access->addCacheContexts(['eb_dummy']); + } + else { + $access = AccessResult::forbidden(); + $access->addCacheContexts(['eb_dummy']); + } + return $access; + } + } diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.content_embed.default.yml b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.content_embed.default.yml index 5732c92ea..d09edbbd8 100644 --- a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.content_embed.default.yml +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.content_embed.default.yml @@ -20,6 +20,7 @@ content: field_widget_display: label field_widget_edit: true field_widget_remove: true + field_widget_replace: false selection_mode: selection_append open: true field_widget_display_settings: { } diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.nested_paragrah.default.yml b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.nested_paragraph.default.yml similarity index 95% rename from web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.nested_paragrah.default.yml rename to web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.nested_paragraph.default.yml index 50848f71c..2a817a9c4 100644 --- a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.nested_paragrah.default.yml +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/config/install/core.entity_form_display.paragraph.nested_paragraph.default.yml @@ -19,6 +19,7 @@ content: field_widget_display: label field_widget_edit: true field_widget_remove: true + field_widget_replace: false selection_mode: selection_append open: true field_widget_display_settings: { } diff --git a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/entity_browser_test_paragraphs.info.yml b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/entity_browser_test_paragraphs.info.yml index 11882beba..cfba3cfd9 100644 --- a/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/entity_browser_test_paragraphs.info.yml +++ b/web/modules/contrib/entity_browser/tests/modules/entity_browser_test_paragraphs/entity_browser_test_paragraphs.info.yml @@ -14,8 +14,8 @@ dependencies: - views - user -# Information added by Drupal.org packaging script on 2017-11-30 -version: '8.x-1.4' +# Information added by Drupal.org packaging script on 2018-09-07 +version: '8.x-1.6' core: '8.x' project: 'entity_browser' -datestamp: 1512033788 +datestamp: 1536328688 diff --git a/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityBrowserTest.php b/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityBrowserTest.php index e8add2cef..857d2c17c 100644 --- a/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityBrowserTest.php +++ b/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityBrowserTest.php @@ -78,6 +78,7 @@ class EntityBrowserTest extends EntityBrowserJavascriptTestBase { $this->getSession()->switchToIFrame('entity_browser_iframe_test_entity_browser_file'); + $this->assertSession()->linkExists('dummy'); $this->assertSession()->linkExists('view'); $this->assertSession()->linkExists('upload'); @@ -102,6 +103,26 @@ class EntityBrowserTest extends EntityBrowserJavascriptTestBase { // 'files[upload][]' => $this->container->get('file_system')->realpath($image2->getFileUri()), //]; // $this->drupalPostForm(NULL, $edit, 'Select files');. + + \Drupal::state()->set('eb_test_dummy_widget_access', FALSE); + $this->drupalGet('entity-browser/iframe/test_entity_browser_file'); + $this->assertSession()->linkNotExists('dummy'); + $this->assertSession()->linkExists('view'); + $this->assertSession()->linkExists('upload'); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'eb_dummy'); + + // Move dummy widget to the first place and make sure it does not appear. + $browser = $this->container->get('entity_type.manager') + ->getStorage('entity_browser') + ->load('test_entity_browser_file'); + $browser->getWidget('cbc59500-04ab-4395-b063-c561f0e3bf80')->setWeight(-15); + $browser->save(); + $this->drupalGet('entity-browser/iframe/test_entity_browser_file'); + $this->assertSession()->linkNotExists('dummy'); + $this->assertSession()->linkExists('view'); + $this->assertSession()->linkExists('upload'); + $this->assertSession()->pageTextNotContains('This is dummy widget.'); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'eb_dummy'); } /** @@ -130,6 +151,9 @@ class EntityBrowserTest extends EntityBrowserJavascriptTestBase { $this->getSession()->switchToIFrame('entity_browser_iframe_test_entity_browser_file'); $this->assertSession()->selectExists('widget'); + $this->assertSession()->optionExists('widget', 'cbc59500-04ab-4395-b063-c561f0e3bf80'); // Dummy + $this->assertSession()->optionExists('widget', '2dc1ab07-2f8f-42c9-aab7-7eef7f8b7d87'); // Upload + $this->assertSession()->optionExists('widget', '774798f1-5ec5-4b63-84bd-124cd51ec07d'); // View // Selects the view widget. $this->getSession()->getPage()->selectFieldOption('widget', '774798f1-5ec5-4b63-84bd-124cd51ec07d'); @@ -148,10 +172,30 @@ class EntityBrowserTest extends EntityBrowserJavascriptTestBase { // Causes a fatal. // Selects the upload widget. // $this->getSession()->getPage()->selectFieldOption('widget', '2dc1ab07-2f8f-42c9-aab7-7eef7f8b7d87');. + + \Drupal::state()->set('eb_test_dummy_widget_access', FALSE); + $this->drupalGet('entity-browser/iframe/test_entity_browser_file'); + $this->assertSession()->optionNotExists('widget', 'cbc59500-04ab-4395-b063-c561f0e3bf80'); // Dummy + $this->assertSession()->optionExists('widget', '2dc1ab07-2f8f-42c9-aab7-7eef7f8b7d87'); // Upload + $this->assertSession()->optionExists('widget', '774798f1-5ec5-4b63-84bd-124cd51ec07d'); // View + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'eb_dummy'); + + // Move dummy widget to the first place and make sure it does not appear. + $browser = $this->container->get('entity_type.manager') + ->getStorage('entity_browser') + ->load('test_entity_browser_file'); + $browser->getWidget('cbc59500-04ab-4395-b063-c561f0e3bf80')->setWeight(-15); + $browser->save(); + $this->drupalGet('entity-browser/iframe/test_entity_browser_file'); + $this->assertSession()->optionNotExists('widget', 'cbc59500-04ab-4395-b063-c561f0e3bf80'); // Dummy + $this->assertSession()->optionExists('widget', '2dc1ab07-2f8f-42c9-aab7-7eef7f8b7d87'); // Upload + $this->assertSession()->optionExists('widget', '774798f1-5ec5-4b63-84bd-124cd51ec07d'); // View + $this->assertSession()->pageTextNotContains('This is dummy widget.'); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'eb_dummy'); } /** - * Tests wievs selection display. + * Tests views selection display. */ public function testViewsSelectionDisplayWidget() { diff --git a/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityBrowserViewsWidgetTest.php b/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityBrowserViewsWidgetTest.php index f6e8d6d31..5f4c8c130 100644 --- a/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityBrowserViewsWidgetTest.php +++ b/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityBrowserViewsWidgetTest.php @@ -86,6 +86,21 @@ class EntityBrowserViewsWidgetTest extends EntityBrowserJavascriptTestBase { $check_new = $this->assertSession()->fieldExists($new_field); // Compare value attributes of checkboxes and assert they not equal. $this->assertNotEquals($check_old->getAttribute('value'), $check_new->getAttribute('value')); + + $uuid = \Drupal::service('uuid')->generate(); + \Drupal::service('entity_browser.selection_storage')->setWithExpire( + $uuid, + ['validators' => ['cardinality' => ['cardinality' => 1]]], + 21600 + ); + $this->drupalGet('/entity-browser/iframe/test_entity_browser_file', ['query' => ['uuid' => $uuid]]); + $this->getSession()->getPage()->fillField('entity_browser_select[file:1]', TRUE); + $this->getSession()->getPage()->fillField('entity_browser_select[file:2]', TRUE); + $this->getSession()->getPage()->pressButton('Select entities'); + + $this->assertSession()->pageTextContains('You can not select more than 1 entity.'); + $this->assertSession()->checkboxNotChecked('entity_browser_select[file:1]'); + $this->assertSession()->checkboxNotChecked('entity_browser_select[file:2]'); } } diff --git a/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php b/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php index c760cce7d..1fe6d17ff 100644 --- a/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php +++ b/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php @@ -24,8 +24,11 @@ class EntityReferenceWidgetTest extends EntityBrowserJavascriptTestBase { /** @var \Drupal\user\RoleInterface $role */ $role = Role::load('authenticated'); - $this->grantPermissions($role, ['access test_entity_browser_iframe_node_view entity browser pages']); - $this->grantPermissions($role, ['bypass node access']); + $this->grantPermissions($role, [ + 'access test_entity_browser_iframe_node_view entity browser pages', + 'bypass node access', + 'administer node form display', + ]); } @@ -33,12 +36,12 @@ class EntityReferenceWidgetTest extends EntityBrowserJavascriptTestBase { * Tests Entity Reference widget. */ public function testEntityReferenceWidget() { - - $page = $this->getSession()->getPage(); + $session = $this->getSession(); + $page = $session->getPage(); $assert_session = $this->assertSession(); // Create an entity_reference field to test the widget. - FieldStorageConfig::create([ + $field_storage = FieldStorageConfig::create([ 'field_name' => 'field_entity_reference1', 'type' => 'entity_reference', 'entity_type' => 'node', @@ -46,15 +49,17 @@ class EntityReferenceWidgetTest extends EntityBrowserJavascriptTestBase { 'settings' => [ 'target_type' => 'node', ], - ])->save(); + ]); + $field_storage->save(); - FieldConfig::create([ + $field = FieldConfig::create([ 'field_name' => 'field_entity_reference1', 'entity_type' => 'node', 'bundle' => 'article', 'label' => 'Referenced articles', 'settings' => [], - ])->save(); + ]); + $field->save(); /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ $form_display = $this->container->get('entity_type.manager') @@ -68,6 +73,7 @@ class EntityReferenceWidgetTest extends EntityBrowserJavascriptTestBase { 'open' => TRUE, 'field_widget_edit' => TRUE, 'field_widget_remove' => TRUE, + 'field_widget_replace' => FALSE, 'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND, 'field_widget_display' => 'label', 'field_widget_display_settings' => [], @@ -83,11 +89,11 @@ class EntityReferenceWidgetTest extends EntityBrowserJavascriptTestBase { $this->drupalGet('/node/add/article'); $page->fillField('title[0][value]', 'Referencing node 1'); - $this->getSession()->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); $this->waitForAjaxToFinish(); $page->checkField('edit-entity-browser-select-node1'); $page->pressButton('Select entities'); - $this->getSession()->switchToIFrame(); + $session->switchToIFrame(); $this->waitForAjaxToFinish(); $page->pressButton('Save'); @@ -110,6 +116,7 @@ class EntityReferenceWidgetTest extends EntityBrowserJavascriptTestBase { 'open' => TRUE, 'field_widget_edit' => FALSE, 'field_widget_remove' => FALSE, + 'field_widget_replace' => FALSE, 'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND, 'field_widget_display' => 'label', 'field_widget_display_settings' => [], @@ -127,31 +134,174 @@ class EntityReferenceWidgetTest extends EntityBrowserJavascriptTestBase { 'open' => TRUE, 'field_widget_edit' => TRUE, 'field_widget_remove' => TRUE, + 'field_widget_replace' => FALSE, 'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND, 'field_widget_display' => 'label', 'field_widget_display_settings' => [], ], ])->save(); $this->drupalGet('node/' . $nid . '/edit'); - $assert_session->buttonExists('edit-field-entity-reference1-current-items-0-remove-button'); - $assert_session->buttonExists('edit-field-entity-reference1-current-items-0-edit-button'); + $remove_button = $assert_session->buttonExists('edit-field-entity-reference1-current-items-0-remove-button'); + $this->assertEquals('Remove', $remove_button->getValue()); + $this->assertTrue($remove_button->hasClass('remove-button')); + $edit_button = $assert_session->buttonExists('edit-field-entity-reference1-current-items-0-edit-button'); + $this->assertEquals('Edit', $edit_button->getValue()); + $this->assertTrue($edit_button->hasClass('edit-button')); + // Make sure the "Replace" button is not there. + $assert_session->buttonNotExists('edit-field-entity-reference1-current-items-0-replace-button'); // Test the "Remove" button on the widget works. $page->pressButton('Remove'); $this->waitForAjaxToFinish(); $assert_session->pageTextNotContains('Target example node 1'); + // Test the "Replace" button functionality. + $form_display->setComponent('field_entity_reference1', [ + 'type' => 'entity_browser_entity_reference', + 'settings' => [ + 'entity_browser' => 'test_entity_browser_iframe_node_view', + 'open' => TRUE, + 'field_widget_edit' => TRUE, + 'field_widget_remove' => TRUE, + 'field_widget_replace' => TRUE, + 'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND, + 'field_widget_display' => 'label', + 'field_widget_display_settings' => [], + ], + ])->save(); + // In order to ensure the replace button opens the browser, it needs to be + // closed. + /** @var \Drupal\entity_browser\EntityBrowserInterface $browser */ + $browser = $this->container->get('entity_type.manager') + ->getStorage('entity_browser') + ->load('test_entity_browser_iframe_node_view'); + $browser->getDisplay() + ->setConfiguration([ + 'width' => 650, + 'height' => 500, + 'link_text' => 'Select entities', + 'auto_open' => FALSE, + ]); + $browser->save(); + // We'll need a third node to be able to make a new selection. + $target_node2 = Node::create([ + 'title' => 'Target example node 2', + 'type' => 'article', + ]); + $target_node2->save(); + $this->drupalGet('node/' . $nid . '/edit'); + // If there is only one entity in the current selection the button should + // show up. + $replace_button = $assert_session->buttonExists('edit-field-entity-reference1-current-items-0-replace-button'); + $this->assertEquals('Replace', $replace_button->getValue()); + $this->assertTrue($replace_button->hasClass('replace-button')); + // Clicking on the button should empty the selection and automatically + // open the browser again. + $replace_button->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $page->checkField('edit-entity-browser-select-node3'); + $page->pressButton('Select entities'); + $session->wait(1000); + $session->switchToIFrame(); + $this->waitForAjaxToFinish(); + // Even in the AJAX-built markup for the newly selected element, the replace + // button should be there. + $assert_session->elementExists('css', 'input[data-drupal-selector="edit-field-entity-reference1-current-items-0-replace-button"]'); + // Adding a new node to the selection, however, should make it disappear. + $open_iframe_link = $assert_session->elementExists('css', 'a[data-drupal-selector="edit-field-entity-reference1-entity-browser-entity-browser-link"]'); + $open_iframe_link->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $page->checkField('edit-entity-browser-select-node1'); + $page->pressButton('Select entities'); + $session->wait(1000); + $session->switchToIFrame(); + $this->waitForAjaxToFinish(); + $assert_session->elementNotExists('css', 'input[data-drupal-selector="edit-field-entity-reference1-current-items-0-replace-button"]'); + $page->pressButton('Save'); + $assert_session->pageTextContains('Article Referencing node 1 has been updated.'); + + // Test the replace button again with different field cardinalities. + FieldStorageConfig::load('node.field_entity_reference1')->setCardinality(1)->save(); + $this->drupalGet('/node/add/article'); + $page->fillField('title[0][value]', 'Referencing node 2'); + $open_iframe_link = $assert_session->elementExists('css', 'a[data-drupal-selector="edit-field-entity-reference1-entity-browser-entity-browser-link"]'); + $open_iframe_link->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $page->checkField('edit-entity-browser-select-node1'); + $page->pressButton('Select entities'); + $session->wait(1000); + $session->switchToIFrame(); + $this->waitForAjaxToFinish(); + $assert_session->elementContains('css', '#edit-field-entity-reference1-wrapper', 'Target example node 1'); + // All three buttons should be visible. + $assert_session->elementExists('css', 'input[data-drupal-selector="edit-field-entity-reference1-current-items-0-remove-button"]'); + $assert_session->elementExists('css', 'input[data-drupal-selector="edit-field-entity-reference1-current-items-0-edit-button"]'); + $replace_button = $assert_session->elementExists('css', 'input[data-drupal-selector="edit-field-entity-reference1-current-items-0-replace-button"]'); + // Clicking on the button should empty the selection and automatically + // open the browser again. + $replace_button->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $page->checkField('edit-entity-browser-select-node2'); + $page->pressButton('Select entities'); + $session->wait(1000); + $session->switchToIFrame(); + $this->waitForAjaxToFinish(); + $assert_session->elementContains('css', '#edit-field-entity-reference1-wrapper', 'Referencing node 1'); + + // Do the same as above but now with cardinality 2. + FieldStorageConfig::load('node.field_entity_reference1')->setCardinality(2)->save(); + $this->drupalGet('/node/add/article'); + $page->fillField('title[0][value]', 'Referencing node 3'); + $open_iframe_link = $assert_session->elementExists('css', 'a[data-drupal-selector="edit-field-entity-reference1-entity-browser-entity-browser-link"]'); + $open_iframe_link->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $page->checkField('edit-entity-browser-select-node1'); + $page->pressButton('Select entities'); + $session->wait(1000); + $session->switchToIFrame(); + $this->waitForAjaxToFinish(); + $assert_session->elementContains('css', '#edit-field-entity-reference1-wrapper', 'Target example node 1'); + // All three buttons should be visible. + $assert_session->elementExists('css', 'input[data-drupal-selector="edit-field-entity-reference1-current-items-0-remove-button"]'); + $assert_session->elementExists('css', 'input[data-drupal-selector="edit-field-entity-reference1-current-items-0-edit-button"]'); + $replace_button = $assert_session->elementExists('css', 'input[data-drupal-selector="edit-field-entity-reference1-current-items-0-replace-button"]'); + // Clicking on the button should empty the selection and automatically + // open the browser again. + $replace_button->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $page->checkField('edit-entity-browser-select-node2'); + $page->pressButton('Select entities'); + $session->wait(1000); + $session->switchToIFrame(); + $this->waitForAjaxToFinish(); + $assert_session->elementContains('css', '#edit-field-entity-reference1-wrapper', 'Referencing node 1'); + // Verify that if the user cannot edit the entity, the "Edit" button does // not show up, even if configured to. /** @var \Drupal\user\RoleInterface $role */ $role = Role::load('authenticated'); $role->revokePermission('bypass node access')->trustData()->save(); $this->drupalGet('node/add/article'); - $this->getSession()->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $open_iframe_link = $assert_session->elementExists('css', 'a[data-drupal-selector="edit-field-entity-reference1-entity-browser-entity-browser-link"]'); + $open_iframe_link->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); $this->waitForAjaxToFinish(); $page->checkField('edit-entity-browser-select-node1'); $page->pressButton('Select entities'); - $this->getSession()->switchToIFrame(); + $session->switchToIFrame(); $this->waitForAjaxToFinish(); $assert_session->buttonNotExists('edit-field-entity-reference1-current-items-0-edit-button'); diff --git a/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/ImageFieldTest.php b/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/ImageFieldTest.php index a08781a48..40bab0893 100644 --- a/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/ImageFieldTest.php +++ b/web/modules/contrib/entity_browser/tests/src/FunctionalJavascript/ImageFieldTest.php @@ -70,6 +70,7 @@ class ImageFieldTest extends EntityBrowserJavascriptTestBase { 'open' => TRUE, 'field_widget_edit' => FALSE, 'field_widget_remove' => TRUE, + 'field_widget_replace' => TRUE, 'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND, 'view_mode' => 'default', 'preview_image_style' => 'thumbnail', @@ -149,6 +150,25 @@ class ImageFieldTest extends EntityBrowserJavascriptTestBase { // Image filename should not be present. $this->assertSession()->pageTextNotContains('example.jpg'); $this->assertSession()->linkExists('Select entities'); + // Test the Replace functionality. + file_unmanaged_copy(\Drupal::root() . '/core/modules/simpletest/files/image-test.jpg', 'public://example2.jpg'); + $image2 = File::create(['uri' => 'public://example2.jpg']); + $image2->save(); + \Drupal::service('file.usage')->add($image2, 'entity_browser', 'test', '1'); + $this->drupalGet('node/1/edit'); + $this->assertSession()->buttonExists('Replace'); + $this->getSession()->getPage()->pressButton('Replace'); + $this->waitForAjaxToFinish(); + $this->getSession()->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_view'); + $this->getSession()->getPage()->checkField('entity_browser_select[file:' . $image2->id() . ']'); + $this->getSession()->getPage()->pressButton('Select entities'); + $this->getSession()->getPage()->pressButton('Use selected'); + $this->getSession()->wait(1000); + $this->getSession()->switchToIFrame(); + $this->waitForAjaxToFinish(); + // Initial image should not be present, the new one should be there instead. + $this->assertSession()->pageTextNotContains('example.jpg'); + $this->assertSession()->pageTextContains('example2.jpg'); } /** diff --git a/web/modules/contrib/entity_browser/tests/src/Kernel/Extension/EntityBrowserTest.php b/web/modules/contrib/entity_browser/tests/src/Kernel/Extension/EntityBrowserTest.php index d737f04e8..018f17dbc 100644 --- a/web/modules/contrib/entity_browser/tests/src/Kernel/Extension/EntityBrowserTest.php +++ b/web/modules/contrib/entity_browser/tests/src/Kernel/Extension/EntityBrowserTest.php @@ -13,6 +13,7 @@ use Drupal\entity_browser\WidgetInterface; use Drupal\entity_browser\WidgetSelectorInterface; use Drupal\entity_browser\SelectionDisplayInterface; use Drupal\KernelTests\KernelTestBase; +use Drupal\user\Entity\User; use Drupal\views\Entity\View; /** @@ -467,4 +468,41 @@ class EntityBrowserTest extends KernelTestBase { $this->assertEmpty($form_state->getErrors(), t('Validation succeeded where expected')); } + /** + * Tests view widget access. + */ + public function testViewWidgetAccess() { + $this->installConfig(['entity_browser_test']); + $this->installEntitySchema('user'); + $this->installEntitySchema('user_role'); + + /** @var \Drupal\entity_browser\EntityBrowserInterface $entity */ + $entity = $this->controller->load('test_entity_browser_file'); + + $this->assertFalse($entity->getWidget('774798f1-5ec5-4b63-84bd-124cd51ec07d')->access()->isAllowed()); + + // Create a user that has permission to access the view and try with it. + /** @var \Drupal\user\RoleInterface $role */ + $role = $this->container->get('entity_type.manager') + ->getStorage('user_role') + ->create([ + 'name' => $this->randomString(), + 'id' => $this->randomMachineName(), + ]); + $role->grantPermission('access content'); + $role->save(); + + $user = $this->container->get('entity_type.manager') + ->getStorage('user') + ->create([ + 'name' => $this->randomString(), + 'mail' => 'info@example.com', + 'roles' => $role->id(), + ]); + $user->save(); + \Drupal::currentUser()->setAccount($user); + + $this->assertTrue($entity->getWidget('774798f1-5ec5-4b63-84bd-124cd51ec07d')->access()->isAllowed()); + } + } diff --git a/web/modules/contrib/entity_reference_revisions/LICENSE.txt b/web/modules/contrib/entity_reference_revisions/LICENSE.txt deleted file mode 100644 index d159169d1..000000000 --- a/web/modules/contrib/entity_reference_revisions/LICENSE.txt +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - <signature of Ty Coon>, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml b/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml deleted file mode 100644 index 75a216aec..000000000 --- a/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml +++ /dev/null @@ -1,69 +0,0 @@ -# Schema for the configuration files of the Entity Reference module. - -entity_reference_revisions.default.handler_settings: - type: mapping - label: 'View handler settings' - mapping: - target_bundles: - type: sequence - label: 'types' - sequence: - - type: string - label: 'Type' - sort: - type: mapping - label: 'Sort settings' - mapping: - field: - type: string - label: 'Sort by' - direction: - type: string - label: 'Sort direction' - filter: - type: mapping - label: 'Filter settings' - mapping: - type: - type: string - label: 'Filter by' - role: - type: sequence - label: 'Restrict to the selected roles' - sequence: - - type: string - label: 'Role' - auto_create: - type: boolean - label: 'Create referenced entities if they don''t already exist' - -field.storage_settings.entity_reference_revisions: - type: field.storage_settings.entity_reference - -field.value.entity_reference_revisions: - type: field.value.entity_reference - label: 'Default value' - mapping: - target_id: - type: integer - label: 'Value' - target_revision_id: - type: integer - label: 'Revision ID' - target_uuid: - type: string - label: 'Target UUID' - -field.field_settings.entity_reference_revisions: - type: field.field_settings.entity_reference - -field.widget.settings.entity_reference_revisions_autocomplete: - type: field.widget.settings.entity_reference_autocomplete - -field.formatter.settings.entity_reference_revisions_entity_view: - type: mapping - mapping: - view_mode: - type: string - link: - type: string diff --git a/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml b/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml deleted file mode 100644 index afbad6375..000000000 --- a/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Schema for the views plugins of the Entity Reference module. - -views.display.entity_reference_revisions: - type: views_display - label: 'Entity Reference Revisions' - -views.row.entity_reference_revisions: - type: views.row.fields - label: 'Entity Reference Revisions inline fields' - -views.style.entity_reference_revisions: - type: views_style - label: 'Entity Reference Revisions list' - mapping: - search_fields: - type: sequence - label: 'Search fields' - sequence: - - type: string - label: 'Search field' diff --git a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml deleted file mode 100644 index 9110e1db1..000000000 --- a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Entity Reference Revisions -type: module -description: Adds a Entity Reference field type with revision support. -# core: 8.x -package: Field types - -test_dependencies: - - diff:diff - -# Information added by Drupal.org packaging script on 2018-01-05 -version: '8.x-1.4' -core: '8.x' -project: 'entity_reference_revisions' -datestamp: 1515143887 diff --git a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module deleted file mode 100644 index ba9ac496f..000000000 --- a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module +++ /dev/null @@ -1,220 +0,0 @@ -<?php - -/** - * @file - * Provides a field that can reference other entities. - */ - -use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Render\Element; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\FieldStorageConfigInterface; -use Drupal\Core\Url; - -/** - * Implements hook_help(). - */ -function entity_reference_revisions_help($route_name, RouteMatchInterface $route_match) { - switch ($route_name) { - case 'help.page.entity_reference_revisions': - $output = ''; - $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Entity Reference Revisions module allows you to create fields that contain links to other entities (such as content items, taxonomy terms, etc.) within the site. This allows you, for example, to include a link to a user within a content item. For more information, see <a href=":er_do">the online documentation for the Entity Reference Revisions module</a> and the <a href=":field_help">Field module help page</a>.', [':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':er_do' => 'https://drupal.org/documentation/modules/entity_reference_revisions']) . '</p>'; - $output .= '<h3>' . t('Uses') . '</h3>'; - $output .= '<dl>'; - $output .= '<dt>' . t('Managing and displaying entity reference fields') . '</dt>'; - $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the entity reference field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => Url::fromRoute('help.page', ['name' => 'field_ui'])->toString()]) . '</dd>'; - $output .= '<dt>' . t('Selecting reference type') . '</dt>'; - $output .= '<dd>' . t('In the field settings you can select which entity type you want to create a reference to.') . '</dd>'; - $output .= '<dt>' . t('Filtering and sorting reference fields') . '</dt>'; - $output .= '<dd>' . t('Depending on the chosen entity type, additional filtering and sorting options are available for the list of entities that can be referred to, in the field settings. For example, the list of users can be filtered by role and sorted by name or ID.') . '</dd>'; - $output .= '<dt>' . t('Displaying a reference') . '</dt>'; - $output .= '<dd>' . t('An entity reference can be displayed as a simple label with or without a link to the entity. Alternatively, the referenced entity can be displayed as a teaser (or any other available view mode) inside the referencing entity.') . '</dd>'; - $output .= '</dl>'; - return $output; - } -} - -/** - * Implements hook_field_widget_info_alter(). - */ -function entity_reference_revisions_field_widget_info_alter(&$info) { - if (isset($info['options_select'])) { - $info['options_select']['field_types'][] = 'entity_reference_revisions'; - } - if (isset($info['options_buttons'])) { - $info['options_buttons']['field_types'][] = 'entity_reference_revisions'; - } -} - -/** - * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'. - * - * Reset the instance handler settings, when the target type is changed. - */ -function entity_reference_revisions_field_storage_config_update(FieldStorageConfigInterface $field_storage) { - if ($field_storage->getType() != 'entity_reference_revisions') { - // Only act on entity reference fields. - return; - } - - if ($field_storage->isSyncing()) { - // Don't change anything during a configuration sync. - return; - } - - if ($field_storage->getSetting('target_type') == $field_storage->original->getSetting('target_type')) { - // Target type didn't change. - return; - } - - if (empty($field_storage->bundles)) { - // Field storage has no fields. - return; - } - - $field_name = $field_storage->getName(); - - foreach ($field_storage->bundles() as $entity_type => $bundles) { - foreach ($bundles as $bundle) { - $field = FieldConfig::loadByName($entity_type, $bundle, $field_name); - $field->setSetting('handler_settings', []); - $field->save(); - } - } -} - -/** - * Render API callback: Processes the field settings form and allows access to - * the form state. - * - * @see entity_reference_revisions_field_field_settings_form() - */ -function _entity_reference_revisions_field_field_settings_ajax_process($form, FormStateInterface $form_state) { - _entity_reference_revisions_field_field_settings_ajax_process_element($form, $form); - return $form; -} - -/** - * Adds entity_reference specific properties to AJAX form elements from the - * field settings form. - * - * @see _entity_reference_revisions_field_field_settings_ajax_process() - */ -function _entity_reference_revisions_field_field_settings_ajax_process_element(&$element, $main_form) { - if (!empty($element['#ajax'])) { - $element['#ajax'] = [ - 'callback' => 'entity_reference_revisions_settings_ajax', - 'wrapper' => $main_form['#id'], - 'element' => $main_form['#array_parents'], - ]; - } - - foreach (Element::children($element) as $key) { - _entity_reference_revisions_field_field_settings_ajax_process_element($element[$key], $main_form); - } -} - -/** - * Render API callback: Moves entity_reference specific Form API elements - * (i.e. 'handler_settings') up a level for easier processing by the validation - * and submission handlers. - * - * @see _entity_reference_revisions_field_settings_process() - */ -function _entity_reference_revisions_form_process_merge_parent($element) { - $parents = $element['#parents']; - array_pop($parents); - $element['#parents'] = $parents; - return $element; -} - -/** - * Form element validation handler; Filters the #value property of an element. - */ -function _entity_reference_revisions_element_validate_filter(&$element, FormStateInterface $form_state) { - $element['#value'] = array_filter($element['#value']); - $form_state->setValueForElement($element, $element['#value']); -} - -/** - * Ajax callback for the handler settings form. - * - * @see entity_reference_revisions_field_field_settings_form() - */ -function entity_reference_revisions_settings_ajax($form, FormStateInterface $form_state) { - return NestedArray::getValue($form, $form_state->getTriggeringElement()['#ajax']['element']); -} - -/** - * Submit handler for the non-JS case. - * - * @see entity_reference_revisions_field_field_settings_form() - */ -function entity_reference_revisions_settings_ajax_submit($form, FormStateInterface $form_state) { - $form_state->setRebuild(); -} - -/** - * Creates a field of an entity reference revisions field storage on the specified bundle. - * - * @param string $entity_type - * The type of entity the field will be attached to. - * @param string $bundle - * The bundle name of the entity the field will be attached to. - * @param string $field_name - * The name of the field; if it already exists, a new instance of the existing - * field will be created. - * @param string $field_label - * The label of the field. - * @param string $target_entity_type - * The type of the referenced entity. - * @param string $selection_handler - * The selection handler used by this field. - * @param array $selection_handler_settings - * An array of settings supported by the selection handler specified above. - * (e.g. 'target_bundles', 'sort', 'auto_create', etc). - * @param int $cardinality - * The cardinality of the field. - * - * @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\SelectionBase::buildConfigurationForm() - */ -function entity_reference_revisions_create_field($entity_type, $bundle, $field_name, $field_label, $target_entity_type, $selection_handler = 'default', $selection_handler_settings = [], $cardinality = 1) { - // Look for or add the specified field to the requested entity bundle. - if (!FieldStorageConfig::loadByName($entity_type, $field_name)) { - \Drupal::entityTypeManager()->getStorage('field_storage_config')->create([ - 'field_name' => $field_name, - 'type' => 'entity_reference_revisions', - 'entity_type' => $entity_type, - 'cardinality' => $cardinality, - 'settings' => [ - 'target_type' => $target_entity_type, - ], - ])->save(); - } - if (!FieldConfig::loadByName($entity_type, $bundle, $field_name)) { - \Drupal::entityTypeManager()->getStorage('field_config')->create([ - 'field_name' => $field_name, - 'entity_type' => $entity_type, - 'bundle' => $bundle, - 'label' => $field_label, - 'settings' => [ - 'handler' => $selection_handler, - 'handler_settings' => $selection_handler_settings, - ], - ])->save(); - } -} - -/** - * Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'. - */ -function entity_reference_revisions_form_field_ui_field_storage_add_form_alter(array &$form) { - // Move the "Entity reference revisions" option to the end of the list and rename it to - // "Other". - unset($form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions']); - $form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions'] = t('Other…'); -} diff --git a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc deleted file mode 100644 index 9d351b376..000000000 --- a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc +++ /dev/null @@ -1,73 +0,0 @@ -<?php - -/** - * @file - * Provides views data for the entity_reference_revisions module. - */ - -use Drupal\field\FieldStorageConfigInterface; - -/** - * Implements hook_field_views_data(). - */ -function entity_reference_revisions_field_views_data(FieldStorageConfigInterface $field_storage) { - $data = views_field_default_views_data($field_storage); - $entity_manager = \Drupal::entityTypeManager(); - foreach ($data as $table_name => $table_data) { - // Add a relationship to the target entity type. - $target_entity_type_id = $field_storage->getSetting('target_type'); - $target_entity_type = $entity_manager->getDefinition($target_entity_type_id); - $entity_type_id = $field_storage->getTargetEntityTypeId(); - $entity_type = $entity_manager->getDefinition($entity_type_id); - $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable(); - $field_name = $field_storage->getName(); - - // Provide a relationship for the entity type with the entity reference - // revisions field. - $args = array( - '@label' => $target_entity_type->getLabel(), - '@field_name' => $field_name, - ); - $data[$table_name][$field_name]['relationship'] = array( - 'title' => t('@label referenced from @field_name', $args), - 'label' => t('@field_name: @label', $args), - 'group' => $entity_type->getLabel(), - 'help' => t('Appears in: @bundles.', array('@bundles' => implode(', ', $field_storage->getBundles()))), - 'id' => 'standard', - 'base' => $target_base_table, - 'entity type' => $target_entity_type_id, - 'base field' => $target_entity_type->getKey('revision'), - 'relationship field' => $field_name . '_target_revision_id', - ); - - // Provide a reverse relationship for the entity type that is referenced by - // the field. - $args['@entity'] = $entity_type->getLabel(); - $args['@label'] = $target_entity_type->getLowercaseLabel(); - $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name; - /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ - $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); - $data[$target_base_table][$pseudo_field_name]['relationship'] = array( - 'title' => t('@entity using @field_name', $args), - 'label' => t('@field_name', array('@field_name' => $field_name)), - 'group' => $target_entity_type->getLabel(), - 'help' => t('Relate each @entity with a @field_name set to the @label.', $args), - 'id' => 'entity_reverse', - 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(), - 'entity_type' => $entity_type_id, - 'base field' => $entity_type->getKey('revision'), - 'field_name' => $field_name, - 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), - 'field field' => $field_name . '_target_revision_id', - 'join_extra' => array( - array( - 'field' => 'deleted', - 'value' => 0, - 'numeric' => TRUE, - ), - ), - ); - } - - return $data; -} diff --git a/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php b/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php deleted file mode 100644 index 1513aa3dd..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions; - -/** - * Allows an entity to define whether it needs to be saved. - */ -interface EntityNeedsSaveInterface { - - /** - * Checks whether the entity needs to be saved. - * - * @return bool - * TRUE if the entity needs to be saved. - */ - public function needsSave(); -} diff --git a/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveTrait.php b/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveTrait.php deleted file mode 100644 index 330401049..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveTrait.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions; -use Drupal\Core\Entity\EntityStorageInterface; - -/** - * Trait for EntityNeedsSaveInterface. - */ -trait EntityNeedsSaveTrait { - - /** - * Whether the entity needs to be saved or not. - * - * @var bool - */ - protected $needsSave = FALSE; - - /** - * {@inheritdoc} - */ - public function needsSave() { - return $this->needsSave; - } - - /** - * {@inheritdoc} - */ - public function setNeedsSave($needs_save) { - $this->needsSave = $needs_save; - } - - /** - * {@inheritdoc} - */ - public function postSave(EntityStorageInterface $storage, $update = TRUE) { - parent::postSave($storage, $update); - $this->setNeedsSave(FALSE); - } -} diff --git a/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php b/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php deleted file mode 100644 index ba907277a..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php +++ /dev/null @@ -1,125 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions; - -use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\EntityReferenceFieldItemList; -use Drupal\Core\Field\EntityReferenceFieldItemListInterface; - -/** - * Defines a item list class for entity reference fields. - */ -class EntityReferenceRevisionsFieldItemList extends EntityReferenceFieldItemList implements EntityReferenceFieldItemListInterface { - - /** - * {@inheritdoc} - */ - public function referencedEntities() { - if (empty($this->list)) { - return array(); - } - - // Collect the IDs of existing entities to load, and directly grab the - // "autocreate" entities that are already populated in $item->entity. - $target_entities = $ids = array(); - foreach ($this->list as $delta => $item) { - if ($item->hasNewEntity()) { - $target_entities[$delta] = $item->entity; - } - elseif ($item->target_revision_id !== NULL) { - $ids[$delta] = $item->target_revision_id; - } - } - - // Load and add the existing entities. - if ($ids) { - $target_type = $this->getFieldDefinition()->getSetting('target_type'); - foreach ($ids as $delta => $target_id) { - $entity = \Drupal::entityTypeManager()->getStorage($target_type)->loadRevision($target_id); - if ($entity) { - $target_entities[$delta] = $entity; - } - } - // Ensure the returned array is ordered by deltas. - ksort($target_entities); - } - - return $target_entities; - } - - /** - * {@inheritdoc} - */ - public static function processDefaultValue($default_value, FieldableEntityInterface $entity, FieldDefinitionInterface $definition) { - $default_value = parent::processDefaultValue($default_value, $entity, $definition); - - if ($default_value) { - // Convert UUIDs to numeric IDs. - $uuids = array(); - foreach ($default_value as $delta => $properties) { - if (isset($properties['target_uuid'])) { - $uuids[$delta] = $properties['target_uuid']; - } - } - if ($uuids) { - $target_type = $definition->getSetting('target_type'); - $entity_ids = \Drupal::entityQuery($target_type) - ->condition('uuid', $uuids, 'IN') - ->execute(); - $entities = \Drupal::entityTypeManager() - ->getStorage($target_type) - ->loadMultiple($entity_ids); - - $entity_uuids = array(); - foreach ($entities as $id => $entity) { - $entity_uuids[$entity->uuid()] = $id; - } - foreach ($uuids as $delta => $uuid) { - if (isset($entity_uuids[$uuid])) { - $default_value[$delta]['target_id'] = $entity_uuids[$uuid]; - unset($default_value[$delta]['target_uuid']); - } - else { - unset($default_value[$delta]); - } - } - } - - // Ensure we return consecutive deltas, in case we removed unknown UUIDs. - $default_value = array_values($default_value); - } - - return $default_value; - } - - /** - * {@inheritdoc} - */ - public function defaultValuesFormSubmit(array $element, array &$form, FormStateInterface $form_state) { - $default_value = parent::defaultValuesFormSubmit($element, $form, $form_state); - - // Convert numeric IDs to UUIDs to ensure config deployability. - $ids = array(); - foreach ($default_value as $delta => $properties) { - $ids[] = $properties['target_revision_id']; - } - - $entities = array(); - foreach($ids as $id) { - $entities[$id] = \Drupal::entityTypeManager() - ->getStorage($this->getSetting('target_type')) - ->loadRevision($id); - } - - foreach ($default_value as $delta => $properties) { - $default_value[$delta] = array( - 'target_uuid' => $entities[$properties['target_revision_id']]->uuid(), - 'target_revision_id' => $properties['target_revision_id'], - ); - } - return $default_value; - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php b/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php deleted file mode 100644 index 65db78508..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions; - -use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\Core\DependencyInjection\ServiceProviderBase; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Reference; - -/** - * Service Provider for Entity Reference Revisions. - */ -class EntityReferenceRevisionsServiceProvider extends ServiceProviderBase { - - /** - * {@inheritdoc} - */ - public function alter(ContainerBuilder $container) { - $modules = $container->getParameter('container.modules'); - if (isset($modules['hal'])) { - // Hal module is enabled, add our new normalizer for entity reference - // revision items. - $service_definition = new Definition('Drupal\entity_reference_revisions\Normalizer\EntityReferenceRevisionItemNormalizer', array( - new Reference('hal.link_manager'), - new Reference('serializer.entity_resolver'), - )); - // The priority must be higher than that of - // serializer.normalizer.entity_reference_item.hal in - // hal.services.yml. - $service_definition->addTag('normalizer', array('priority' => 20)); - $container->setDefinition('serializer.normalizer.entity_reference_revision_item', $service_definition); - } - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php b/web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php deleted file mode 100644 index 5c63cf2df..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Normalizer; - -use Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem; -use Drupal\hal\Normalizer\EntityReferenceItemNormalizer; - -/** - * Defines a class for normalizing EntityReferenceRevisionItems. - */ -class EntityReferenceRevisionItemNormalizer extends EntityReferenceItemNormalizer { - - /** - * The interface or class that this Normalizer supports. - * - * @var string - */ - protected $supportedInterfaceOrClass = EntityReferenceRevisionsItem::class; - - /** - * {@inheritdoc} - */ - protected function constructValue($data, $context) { - $value = parent::constructValue($data, $context); - if ($value) { - $value['target_revision_id'] = $data['target_revision_id']; - } - return $value; - } - - /** - * {@inheritdoc} - */ - public function normalize($field_item, $format = NULL, array $context = array()) { - $data = parent::normalize($field_item, $format, $context); - $field_name = $field_item->getParent()->getName(); - $entity = $field_item->getEntity(); - $field_uri = $this->linkManager->getRelationUri($entity->getEntityTypeId(), $entity->bundle(), $field_name, $context); - $data['_embedded'][$field_uri][0]['target_revision_id'] = $field_item->target_revision_id; - return $data; - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php deleted file mode 100644 index c9e66ac66..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php +++ /dev/null @@ -1,123 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\DataType; - -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\Plugin\DataType\EntityReference; - -/** - * Defines an 'entity_reference_revisions' data type. - * - * This serves as 'entity' property of entity reference field items and gets - * its value set from the parent, i.e. LanguageItem. - * - * The plain value of this reference is the entity object, i.e. an instance of - * \Drupal\Core\Entity\EntityInterface. For setting the value the entity object - * or the entity ID may be passed. - * - * Note that the definition of the referenced entity's type is required, whereas - * defining referencable entity bundle(s) is optional. A reference defining the - * type and bundle of the referenced entity can be created as following: - * @code - * $definition = \Drupal\Core\Entity\EntityDefinition::create($entity_type) - * ->addConstraint('Bundle', $bundle); - * \Drupal\Core\TypedData\DataReferenceDefinition::create('entity_revision') - * ->setTargetDefinition($definition); - * @endcode - * - * @DataType( - * id = "entity_revision_reference", - * label = @Translation("Entity reference revisions"), - * definition_class = "\Drupal\Core\TypedData\DataReferenceDefinition" - * ) - */ -class EntityReferenceRevisions extends EntityReference { - - /** - * The entity revision ID. - * - * @var integer|string - */ - protected $revision_id; - - /** - * The entity ID. - * - * @var integer|string - */ - protected $id; - - /** - * Returns the definition of the referenced entity. - * - * @return \Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface - * The reference target's definition. - */ - public function getTargetDefinition() { - return $this->definition->getTargetDefinition(); - } - - /** - * Checks whether the target entity has not been saved yet. - * - * @return bool - * TRUE if the entity is new, FALSE otherwise. - */ - public function isTargetNew() { - // If only an ID is given, the reference cannot be a new entity. - return !isset($this->id) && isset($this->target) && $this->target->getValue()->isNew(); - } - - /** - * {@inheritdoc} - */ - public function getTarget() { - if (!isset($this->target) && isset($this->revision_id)) { - // If we have a valid reference, return the entity's TypedData adapter. - $entity = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId())->loadRevision($this->revision_id); - $this->target = isset($entity) ? $entity->getTypedData() : NULL; - } - return $this->target; - } - - /** - * {@inheritdoc} - */ - public function getTargetIdentifier() { - if (isset($this->id)) { - return $this->id; - } - elseif ($entity = $this->getValue()) { - return $entity->id(); - } - } - - /** - * {@inheritdoc} - */ - public function setValue($value, $notify = TRUE) { - unset($this->target); - unset($this->id); - unset($this->revision_id); - - // Both the entity ID and the entity object may be passed as value. The - // reference may also be unset by passing NULL as value. - if (!isset($value)) { - $this->target = NULL; - } - elseif (is_object($value) && $value instanceof EntityInterface) { - $this->target = $value->getTypedData(); - } - elseif (!is_scalar($value['target_id']) || !is_scalar($value['target_revision_id']) || $this->getTargetDefinition()->getEntityTypeId() === NULL) { - throw new \InvalidArgumentException('Value is not a valid entity.'); - } - else { - $this->id = $value['target_id']; - $this->revision_id = $value['target_revision_id']; - } - // Notify the parent of any changes. - if ($notify && isset($this->parent)) { - $this->parent->onChange($this->name); - } - } -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php deleted file mode 100644 index 461fd539e..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\DataType; - -use Drupal\Core\TypedData\ComplexDataInterface; -use Drupal\Core\Entity\Plugin\DataType\EntityAdapter; - -/** - * Defines the "entity" data type. - * - * Instances of this class wrap entity objects and allow to deal with entities - * based upon the Typed Data API. - * - * In addition to the "entity" data type, this exposes derived - * "entity:$entity_type" and "entity:$entity_type:$bundle" data types. - * - * @DataType( - * id = "entity_revision", - * label = @Translation("Entity Revision"), - * description = @Translation("All kind of entities with revision information, e.g. nodes, comments or users."), - * deriver = "\Drupal\Core\Entity\Plugin\DataType\Deriver\EntityDeriver", - * definition_class = "\Drupal\entity_reference_revisions\TypedData\EntityRevisionDataDefinition" - * ) - */ -class EntityRevisionsAdapter extends EntityAdapter implements \IteratorAggregate, ComplexDataInterface { - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php deleted file mode 100644 index bb7d0c256..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\Derivative; - -use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions; -use Drupal\migrate\Plugin\Derivative\MigrateEntityRevision; - -/** - * Class MigrateEntityReferenceRevisions - */ -class MigrateEntityReferenceRevisions extends MigrateEntityRevision { - - /** - * {@inheritdoc} - */ - public function getDerivativeDefinitions($basePluginDefinition) { - foreach ($this->entityDefinitions as $entityType => $entityInfo) { - if ($entityInfo->getKey('revision')) { - $this->derivatives[$entityType] = [ - 'id' => "entity_reference_revisions:$entityType", - 'class' => EntityReferenceRevisions::class, - 'requirements_met' => 1, - 'provider' => $entityInfo->getProvider(), - ]; - } - } - return $this->derivatives; - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php deleted file mode 100644 index 5d23ec47e..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter; - -use Drupal\Core\Entity\EntityDisplayRepositoryInterface; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldItemListInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Logger\LoggerChannelFactoryInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Plugin implementation of the 'entity reference rendered entity' formatter. - * - * @FieldFormatter( - * id = "entity_reference_revisions_entity_view", - * label = @Translation("Rendered entity"), - * description = @Translation("Display the referenced entities rendered by entity_view()."), - * field_types = { - * "entity_reference_revisions" - * } - * ) - */ -class EntityReferenceRevisionsEntityFormatter extends EntityReferenceRevisionsFormatterBase implements ContainerFactoryPluginInterface { - - /** - * The logger factory. - * - * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface - */ - protected $loggerFactory; - - /** - * The entity display repository. - * - * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface - */ - protected $entityDisplayRepository; - - /** - * Constructs a StringFormatter instance. - * - * @param string $plugin_id - * The plugin_id for the formatter. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The definition of the field to which the formatter is associated. - * @param array $settings - * The formatter settings. - * @param string $label - * The formatter label display setting. - * @param string $view_mode - * The view mode. - * @param array $third_party_settings - * Any third party settings settings. - * @param LoggerChannelFactoryInterface $logger_factory - * The logger factory. - * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository - * The entity display repository. - */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, LoggerChannelFactoryInterface $logger_factory, EntityDisplayRepositoryInterface $entity_display_repository) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); - $this->loggerFactory = $logger_factory; - $this->entityDisplayRepository = $entity_display_repository; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $plugin_id, - $plugin_definition, - $configuration['field_definition'], - $configuration['settings'], - $configuration['label'], - $configuration['view_mode'], - $configuration['third_party_settings'], - $container->get('logger.factory'), - $container->get('entity_display.repository') - ); - } - - /** - * {@inheritdoc} - */ - public static function defaultSettings() { - return array( - 'view_mode' => 'default', - 'link' => FALSE, - ) + parent::defaultSettings(); - } - - /** - * {@inheritdoc} - */ - public function settingsForm(array $form, FormStateInterface $form_state) { - $elements['view_mode'] = array( - '#type' => 'select', - '#options' => $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type')), - '#title' => $this->t('View mode'), - '#default_value' => $this->getSetting('view_mode'), - '#required' => TRUE, - ); - - return $elements; - } - - /** - * {@inheritdoc} - */ - public function settingsSummary() { - $summary = array(); - - $view_modes = $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type')); - $view_mode = $this->getSetting('view_mode'); - $summary[] = $this->t('Rendered as @mode', array('@mode' => isset($view_modes[$view_mode]) ? $view_modes[$view_mode] : $view_mode)); - - return $summary; - } - - /** - * {@inheritdoc} - */ - public function viewElements(FieldItemListInterface $items, $langcode) { - $view_mode = $this->getSetting('view_mode'); - $elements = array(); - - foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) { - // Protect ourselves from recursive rendering. - static $depth = 0; - $depth++; - if ($depth > 20) { - $this->loggerFactory->get('entity')->error('Recursive rendering detected when rendering entity @entity_type @entity_id. Aborting rendering.', array('@entity_type' => $entity->getEntityTypeId(), '@entity_id' => $entity->id())); - return $elements; - } - $view_builder = \Drupal::entityTypeManager()->getViewBuilder($entity->getEntityTypeId()); - $elements[$delta] = $view_builder->view($entity, $view_mode, $entity->language()->getId()); - - // Add a resource attribute to set the mapping property's value to the - // entity's url. Since we don't know what the markup of the entity will - // be, we shouldn't rely on it for structured data such as RDFa. - if (!empty($items[$delta]->_attributes) && !$entity->isNew() && $entity->hasLinkTemplate('canonical')) { - $items[$delta]->_attributes += array('resource' => $entity->toUrl()->toString()); - } - $depth = 0; - } - - return $elements; - } - - /** - * {@inheritdoc} - */ - public static function isApplicable(FieldDefinitionInterface $field_definition) { - // This formatter is only available for entity types that have a view - // builder. - $target_type = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); - return \Drupal::entityTypeManager()->getDefinition($target_type)->hasViewBuilderClass(); - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php deleted file mode 100644 index e098f5280..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter; - -use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase; - -/** - * Parent plugin for entity reference formatters. - */ -abstract class EntityReferenceRevisionsFormatterBase extends EntityReferenceFormatterBase { - - /** - * {@inheritdoc} - */ - public function prepareView(array $entities_items) { - // Entity revision loading currently has no static/persistent cache and no - // multiload. As entity reference checks _loaded, while we don't want to - // indicate a loaded entity, when there is none, as it could cause errors, - // we actually load the entity and set the flag. - foreach ($entities_items as $items) { - foreach ($items as $item) { - - if ($item->entity) { - $item->_loaded = TRUE; - } - } - } - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php deleted file mode 100644 index 9146df4ea..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php +++ /dev/null @@ -1,477 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\Field\FieldType; - -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\TypedData\EntityDataDefinition; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; -use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\TypedData\DataReferenceDefinition; -use Drupal\Core\TypedData\DataReferenceTargetDefinition; -use Drupal\Core\TypedData\OptionsProviderInterface; -use Drupal\entity_reference_revisions\EntityNeedsSaveInterface; - -/** - * Defines the 'entity_reference_revisions' entity field type. - * - * Supported settings (below the definition's 'settings' key) are: - * - target_type: The entity type to reference. Required. - * - target_bundle: (optional): If set, restricts the entity bundles which may - * may be referenced. May be set to an single bundle, or to an array of - * allowed bundles. - * - * @FieldType( - * id = "entity_reference_revisions", - * label = @Translation("Entity reference revisions"), - * description = @Translation("An entity field containing an entity reference to a specific revision."), - * category = @Translation("Reference revisions"), - * no_ui = FALSE, - * class = "\Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem", - * list_class = "\Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList", - * default_formatter = "entity_reference_revisions_entity_view", - * default_widget = "entity_reference_revisions_autocomplete" - * ) - */ -class EntityReferenceRevisionsItem extends EntityReferenceItem implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface { - - /** - * {@inheritdoc} - */ - public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { - - $entity_types = \Drupal::entityTypeManager()->getDefinitions(); - $options = array(); - foreach ($entity_types as $entity_type) { - if ($entity_type->isRevisionable()) { - $options[$entity_type->id()] = $entity_type->getLabel(); - } - } - - $element['target_type'] = array( - '#type' => 'select', - '#title' => $this->t('Type of item to reference'), - '#options' => $options, - '#default_value' => $this->getSetting('target_type'), - '#required' => TRUE, - '#disabled' => $has_data, - '#size' => 1, - ); - - return $element; - } - - /** - * {@inheritdoc} - */ - public static function getPreconfiguredOptions() { - $options = array(); - - // Add all the commonly referenced entity types as distinct pre-configured - // options. - $entity_types = \Drupal::entityTypeManager()->getDefinitions(); - $common_references = array_filter($entity_types, function (EntityTypeInterface $entity_type) { - return $entity_type->get('common_reference_revisions_target') && $entity_type->isRevisionable(); - }); - - /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ - foreach ($common_references as $entity_type) { - - $options[$entity_type->id()] = [ - 'label' => $entity_type->getLabel(), - 'field_storage_config' => [ - 'settings' => [ - 'target_type' => $entity_type->id(), - ] - ] - ]; - $default_reference_settings = $entity_type->get('default_reference_revision_settings'); - if (is_array($default_reference_settings)) { - $options[$entity_type->id()] = array_merge($options[$entity_type->id()], $default_reference_settings); - } - } - - return $options; - } - - /** - * {@inheritdoc} - */ - public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { - $settings = $field_definition->getSettings(); - $target_type_info = \Drupal::entityTypeManager()->getDefinition($settings['target_type']); - - $properties = parent::propertyDefinitions($field_definition); - - if ($target_type_info->getKey('revision')) { - $target_revision_id_definition = DataReferenceTargetDefinition::create('integer') - ->setLabel(t('@label revision ID', array('@label' => $target_type_info->getLabel()))) - ->setSetting('unsigned', TRUE); - - $target_revision_id_definition->setRequired(TRUE); - $properties['target_revision_id'] = $target_revision_id_definition; - } - - $properties['entity'] = DataReferenceDefinition::create('entity_revision') - ->setLabel($target_type_info->getLabel()) - ->setDescription(t('The referenced entity revision')) - // The entity object is computed out of the entity ID. - ->setComputed(TRUE) - ->setReadOnly(FALSE) - ->setTargetDefinition(EntityDataDefinition::create($settings['target_type'])); - - return $properties; - } - - /** - * {@inheritdoc} - */ - public static function schema(FieldStorageDefinitionInterface $field_definition) { - $target_type = $field_definition->getSetting('target_type'); - $target_type_info = \Drupal::entityTypeManager()->getDefinition($target_type); - - $schema = parent::schema($field_definition); - - if ($target_type_info->getKey('revision')) { - $schema['columns']['target_revision_id'] = array( - 'description' => 'The revision ID of the target entity.', - 'type' => 'int', - 'unsigned' => TRUE, - ); - $schema['indexes']['target_revision_id'] = array('target_revision_id'); - } - - return $schema; - } - - /** - * {@inheritdoc} - */ - public function setValue($values, $notify = TRUE) { - if (isset($values) && !is_array($values)) { - // If either a scalar or an object was passed as the value for the item, - // assign it to the 'entity' property since that works for both cases. - $this->set('entity', $values, $notify); - } - else { - parent::setValue($values, FALSE); - // Support setting the field item with only one property, but make sure - // values stay in sync if only property is passed. - // NULL is a valid value, so we use array_key_exists(). - if (is_array($values) && array_key_exists('target_id', $values) && !isset($values['entity'])) { - $this->onChange('target_id', FALSE); - } - elseif (is_array($values) && array_key_exists('target_revision_id', $values) && !isset($values['entity'])) { - $this->onChange('target_revision_id', FALSE); - } - elseif (is_array($values) && !array_key_exists('target_id', $values) && !array_key_exists('target_revision_id', $values) && isset($values['entity'])) { - $this->onChange('entity', FALSE); - } - elseif (is_array($values) && array_key_exists('target_id', $values) && isset($values['entity'])) { - // If both properties are passed, verify the passed values match. The - // only exception we allow is when we have a new entity: in this case - // its actual id and target_id will be different, due to the new entity - // marker. - $entity_id = $this->get('entity')->getTargetIdentifier(); - // If the entity has been saved and we're trying to set both the - // target_id and the entity values with a non-null target ID, then the - // value for target_id should match the ID of the entity value. - if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id != $values['target_id'])) { - throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.'); - } - } - // Notify the parent if necessary. - if ($notify && $this->getParent()) { - $this->getParent()->onChange($this->getName()); - } - } - - } - - /** - * {@inheritdoc} - */ - public function getValue() { - $values = parent::getValue(); - if ($this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave()) { - $values['entity'] = $this->entity; - } - return $values; - } - - /** - * {@inheritdoc} - */ - public function onChange($property_name, $notify = TRUE) { - // Make sure that the target ID and the target property stay in sync. - if ($property_name == 'entity') { - $property = $this->get('entity'); - $target_id = $property->isTargetNew() ? NULL : $property->getTargetIdentifier(); - $this->writePropertyValue('target_id', $target_id); - $this->writePropertyValue('target_revision_id', $property->getValue()->getRevisionId()); - } - elseif ($property_name == 'target_id' && $this->target_id != NULL && $this->target_revision_id) { - $this->writePropertyValue('entity', array( - 'target_id' => $this->target_id, - 'target_revision_id' => $this->target_revision_id, - )); - } - elseif ($property_name == 'target_revision_id' && $this->target_revision_id && $this->target_id) { - $this->writePropertyValue('entity', array( - 'target_id' => $this->target_id, - 'target_revision_id' => $this->target_revision_id, - )); - } - if ($notify && isset($this->parent)) { - $this->parent->onChange($this->name); - } - } - - /** - * {@inheritdoc} - */ - public function isEmpty() { - // Avoid loading the entity by first checking the 'target_id'. - if ($this->target_id !== NULL && $this->target_revision_id !== NULL) { - return FALSE; - } - if ($this->entity && $this->entity instanceof EntityInterface) { - return FALSE; - } - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function preSave() { - $has_new = $this->hasNewEntity(); - - // If it is a new entity, parent will save it. - parent::preSave(); - - if (!$has_new) { - // Create a new revision if it is a composite entity in a host with a new - // revision. - - $host = $this->getEntity(); - $needs_save = $this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave(); - if (!$host->isNew() && $host->isNewRevision() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { - $this->entity->setNewRevision(); - if ($host->isDefaultRevision()) { - $this->entity->isDefaultRevision(TRUE); - } - $needs_save = TRUE; - } - if ($needs_save) { - $this->entity->save(); - } - } - if ($this->entity) { - $this->target_revision_id = $this->entity->getRevisionId(); - } - } - - /** - * {@inheritdoc} - */ - public function postSave($update) { - parent::postSave($update); - - $needs_save = FALSE; - // If any of entity, parent type or parent id is missing then return. - if (!$this->entity || !$this->entity->getEntityType()->get('entity_revision_parent_type_field') || !$this->entity->getEntityType()->get('entity_revision_parent_id_field')) { - return; - } - - $entity = $this->entity; - $parent_entity = $this->getEntity(); - - // If the entity has a parent field name get the key. - if ($entity->getEntityType()->get('entity_revision_parent_field_name_field')) { - $parent_field_name = $entity->getEntityType()->get('entity_revision_parent_field_name_field'); - - // If parent field name has changed then set it. - if ($entity->get($parent_field_name)->value != $this->getFieldDefinition()->getName()) { - $entity->set($parent_field_name, $this->getFieldDefinition()->getName()); - $needs_save = TRUE; - } - } - - $parent_type = $entity->getEntityType()->get('entity_revision_parent_type_field'); - $parent_id = $entity->getEntityType()->get('entity_revision_parent_id_field'); - - // If the parent type has changed then set it. - if ($entity->get($parent_type)->value != $parent_entity->getEntityTypeId()) { - $entity->set($parent_type, $parent_entity->getEntityTypeId()); - $needs_save = TRUE; - } - // If the parent id has changed then set it. - if ($entity->get($parent_id)->value != $parent_entity->id()) { - $entity->set($parent_id, $parent_entity->id()); - $needs_save = TRUE; - } - - if ($needs_save) { - // Check if any of the keys has changed, save it, do not create a new - // revision. - $entity->setNewRevision(FALSE); - $entity->save(); - } - } - - /** - * {@inheritdoc} - */ - public function deleteRevision() { - $child = $this->entity; - if ($child->isDefaultRevision()) { - // Do not delete if it is the default revision. - return; - } - - $host = $this->getEntity(); - $field_name = $this->getFieldDefinition()->getName() . '.target_revision_id'; - $all_revisions = \Drupal::entityQuery($host->getEntityTypeId()) - ->condition($field_name, $child->getRevisionId()) - ->allRevisions() - ->execute(); - - if (count($all_revisions) > 1) { - // Do not delete if there is more than one usage of this revision. - return; - } - - \Drupal::entityTypeManager()->getStorage($child->getEntityTypeId())->deleteRevision($child->getRevisionId()); - } - - /** - * {@inheritdoc} - */ - public function delete() { - parent::delete(); - - if ($this->entity && $this->entity->getEntityType()->get('entity_revision_parent_type_field') && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { - // Only delete composite entities if the host field is not translatable. - if (!$this->getFieldDefinition()->isTranslatable()) { - $this->entity->delete(); - } - } - } - - /** - * {@inheritdoc} - */ - public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) { - $changed = FALSE; - $entity_manager = \Drupal::entityManager(); - $target_entity_type = $entity_manager->getDefinition($field_definition->getFieldStorageDefinition() - ->getSetting('target_type')); - $handler_settings = $field_definition->getSetting('handler_settings'); - - // Update the 'target_bundles' handler setting if a bundle config dependency - // has been removed. - if (!empty($handler_settings['target_bundles'])) { - if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) { - if ($storage = $entity_manager->getStorage($bundle_entity_type_id)) { - foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) { - if (isset($dependencies[$bundle->getConfigDependencyKey()][$bundle->getConfigDependencyName()])) { - unset($handler_settings['target_bundles'][$bundle->id()]); - $changed = TRUE; - - // In case we deleted the only target bundle allowed by the field - // we can log a message because the behaviour of the field will - // have changed. - if ($handler_settings['target_bundles'] === []) { - \Drupal::logger('entity_reference_revisions') - ->notice('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference revisions field (entity_type: %entity_type, bundle: %bundle) no longer specifies a specific target bundle. The field will now accept any bundle and may need to be adjusted.', [ - '%target_bundle' => $bundle->label(), - '%target_entity_type' => $bundle->getEntityType() - ->getBundleOf(), - '%field_name' => $field_definition->getName(), - '%entity_type' => $field_definition->getTargetEntityTypeId(), - '%bundle' => $field_definition->getTargetBundle() - ]); - } - } - } - } - } - } - - if ($changed) { - $field_definition->setSetting('handler_settings', $handler_settings); - } - - return $changed; - } - - /** - * {@inheritdoc} - */ - public static function generateSampleValue(FieldDefinitionInterface $field_definition) { - $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection'); - $entity_manager = \Drupal::entityTypeManager(); - - // Bail if there are no referenceable entities. - if (!$selection_manager->getSelectionHandler($field_definition)->getReferenceableEntities()) { - return; - } - - // ERR field values are never cross referenced so we need to generate new - // target entities. First, find the target entity type. - $target_type_id = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); - $target_type = $entity_manager->getDefinition($target_type_id); - $handler_settings = $field_definition->getSetting('handler_settings'); - - // Determine referenceable bundles. - $bundle_manager = \Drupal::service('entity_type.bundle.info'); - if (isset($handler_settings['target_bundles']) && is_array($handler_settings['target_bundles'])) { - $bundles = $handler_settings['target_bundles']; - } - else { - $bundles = $bundle_manager->getBundleInfo($target_type_id); - } - $bundle = array_rand($bundles); - - $label = NULL; - if ($label_key = $target_type->getKey('label')) { - $random = new Random(); - // @TODO set the length somehow less arbitrary. - $label = $random->word(mt_rand(1, 10)); - } - - // Create entity stub. - $entity = $selection_manager->getSelectionHandler($field_definition)->createNewEntity($target_type_id, $bundle, $label, 0); - - // Populate entity values and save. - $instances = $entity_manager - ->getStorage('field_config') - ->loadByProperties([ - 'entity_type' => $target_type_id, - 'bundle' => $bundle, - ]); - - foreach ($instances as $instance) { - $field_storage = $instance->getFieldStorageDefinition(); - $max = $cardinality = $field_storage->getCardinality(); - if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { - // Just an arbitrary number for 'unlimited' - $max = rand(1, 5); - } - $field_name = $field_storage->getName(); - $entity->{$field_name}->generateSampleItems($max); - } - - $entity->save(); - - return [ - 'target_id' => $entity->id(), - 'target_revision_id' => $entity->getRevisionId(), - ]; - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php deleted file mode 100644 index faad139ab..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\Field\FieldWidget; - -use Drupal\Core\Entity\Entity; -use Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget; -use Drupal\Core\Form\FormStateInterface; - -/** - * Plugin implementation of the 'entity_reference_autocomplete' widget. - * - * @FieldWidget( - * id = "entity_reference_revisions_autocomplete", - * label = @Translation("Autocomplete"), - * description = @Translation("An autocomplete text field."), - * field_types = { - * "entity_reference_revisions" - * } - * ) - */ -class EntityReferenceRevisionsAutocompleteWidget extends EntityReferenceAutocompleteWidget { - - /** - * {@inheritdoc} - */ - public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { - $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type'); - foreach ($values as $key => $value) { - if($value['target_id']) { - $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($value['target_id']); - // Add the current revision ID. - $values[$key]['target_revision_id'] = $entity->getRevisionId(); - } - // The entity_autocomplete form element returns an array when an entity - // was "autocreated", so we need to move it up a level. - if (is_array($value['target_id'])) { - unset($values[$key]['target_id']); - $values[$key] += $value['target_id']; - } - } - return $values; - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php deleted file mode 100644 index 0df05077a..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\diff\Field; - -use Drupal\diff\FieldDiffBuilderBase; -use Drupal\Core\Field\FieldItemListInterface; -use Drupal\diff\FieldReferenceInterface; - -/** - * This plugins offers the possibility to compare ERR fields. - * - * @FieldDiffBuilder( - * id = "entity_reference_revisions_field_diff_builder", - * label = @Translation("Field Diff for Paragraphs"), - * field_types = { - * "entity_reference_revisions" - * }, - * ) - */ -class EntityReferenceRevisionsFieldDiffBuilder extends FieldDiffBuilderBase implements FieldReferenceInterface { - - /** - * {@inheritdoc} - */ - public function build(FieldItemListInterface $field_items) { - $result_text = array(); - $item_counter = 0; - /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item */ - foreach ($field_items as $field_key => $field_item) { - if (!$field_item->isEmpty() && $field_item->entity) { - $parsed_text = $this->entityParser->parseEntity($field_item->entity); - if (is_array($parsed_text)) { - foreach ($parsed_text as $field_id => $field) { - foreach ($field as $id => $text) { - $result_text[$item_counter + $id] = $text; - } - $item_counter = $item_counter + $id + 1; - } - } - } - } - return $result_text; - } - - /** - * {@inheritdoc} - */ - public function getEntitiesToDiff(FieldItemListInterface $field_items) { - /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item */ - $entities = []; - foreach ($field_items as $field_key => $field_item) { - if (!$field_item->isEmpty() && $field_item->entity) { - $entities[$field_key] = $field_item->entity; - } - } - return $entities; - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php deleted file mode 100644 index a50f5a767..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php +++ /dev/null @@ -1,153 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\migrate\destination; - -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\TypedData\TranslatableInterface; -use Drupal\migrate\MigrateException; -use Drupal\migrate\Plugin\migrate\destination\EntityRevision; -use Drupal\migrate\Plugin\MigrateIdMapInterface; -use Drupal\migrate\Row; - -/** - * Provides entity_reference_revisions destination plugin. - * - * @MigrateDestination( - * id = "entity_reference_revisions", - * deriver = "Drupal\entity_reference_revisions\Plugin\Derivative\MigrateEntityReferenceRevisions" - * ) - */ -class EntityReferenceRevisions extends EntityRevision { - - /** - * {@inheritdoc} - */ - protected static function getEntityTypeId($pluginId) { - // Remove "entity_reference_revisions:". - // Ideally, we would call getDerivativeId(), but since this is static - // that is not possible so we follow the same pattern as core. - return substr($pluginId, 27); - } - - /** - * {@inheritdoc} - */ - protected function save(ContentEntityInterface $entity, array $oldDestinationIdValues = array()) { - $entity->save(); - - return [ - $this->getKey('id') => $entity->id(), - $this->getKey('revision') => $entity->getRevisionId(), - ]; - } - - /** - * {@inheritdoc} - */ - public function getIds() { - if ($revision_key = $this->getKey('revision')) { - $id_key = $this->getKey('id'); - $ids[$id_key]['type'] = 'integer'; - - // TODO: Improve after https://www.drupal.org/node/2783715 is finished. - $ids[$revision_key]['type'] = 'integer'; - - if ($this->isTranslationDestination()) { - if ($revision_key = $this->getKey('langcode')) { - $ids[$revision_key]['type'] = 'string'; - } - else { - throw new MigrateException('This entity type does not support translation.'); - } - } - - return $ids; - } - throw new MigrateException('This entity type does not support revisions.'); - } - - /** - * {@inheritdoc} - */ - protected function getEntity(Row $row, array $oldDestinationIdValues) { - $revision_id = $oldDestinationIdValues ? - array_pop($oldDestinationIdValues) : - $row->getDestinationProperty($this->getKey('revision')); - if (!empty($revision_id) && ($entity = $this->storage->loadRevision($revision_id))) { - $entity->setNewRevision(FALSE); - } - else { - // Attempt to ensure we always have a bundle. - if ($bundle = $this->getBundle($row)) { - $row->setDestinationProperty($this->getKey('bundle'), $bundle); - } - - // Stubs might need some required fields filled in. - if ($row->isStub()) { - $this->processStubRow($row); - } - $entity = $this->storage->create($row->getDestination()) - ->enforceIsNew(TRUE); - $entity->setNewRevision(TRUE); - } - $entity = $this->updateEntity($entity, $row) ?: $entity; - $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE; - return $entity; - } - - /** - * {@inheritdoc} - */ - public function rollback(array $destination_identifiers) { - if ($this->isTranslationDestination()) { - $this->rollbackTranslation($destination_identifiers); - } - else { - $this->rollbackNonTranslation($destination_identifiers); - } - } - - /** - * Rollback translation destinations. - * - * @param array $destination_identifiers - * The IDs of the destination object to delete. - */ - protected function rollbackTranslation(array $destination_identifiers) { - $entity = $this->storage->loadRevision(array_pop($destination_identifiers)); - if ($entity && $entity instanceof TranslatableInterface) { - if ($key = $this->getKey('langcode')) { - if (isset($destination_identifier[$key])) { - $langcode = $destination_identifier[$key]; - if ($entity->hasTranslation($langcode)) { - // Make sure we don't remove the default translation. - $translation = $entity->getTranslation($langcode); - if (!$translation->isDefaultTranslation()) { - $entity->removeTranslation($langcode); - $entity->save(); - } - } - } - } - } - } - - /** - * Rollback non-translation destinations. - * - * @param array $destination_identifiers - * The IDs of the destination object to delete. - */ - protected function rollbackNonTranslation(array $destination_identifiers) { - $revision_id = array_pop($destination_identifiers); - $entity = $this->storage->loadRevision($revision_id); - if ($entity) { - if ($entity->isDefaultRevision()) { - $entity->delete(); - } - else { - $this->storage->deleteRevision($revision_id); - } - } - } -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php deleted file mode 100644 index 3c738564d..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php +++ /dev/null @@ -1,176 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\views\display; - -use Drupal\views\Plugin\views\display\DisplayPluginBase; - -/** - * The plugin that handles an EntityReferenceRevisions display. - * - * "entity_reference_revisions_display" is a custom property, used with - * \Drupal\views\Views::getApplicableViews() to retrieve all views with a - * 'Entity Reference Revisions' display. - * - * @ingroup views_display_plugins - * - * @ViewsDisplay( - * id = "entity_reference_revisions", - * title = @Translation("Entity Reference Revisions"), - * admin = @Translation("Entity Reference Revisions Source"), - * help = @Translation("Selects referenceable entities for an entity reference revisions field."), - * theme = "views_view", - * register_theme = FALSE, - * uses_menu_links = FALSE, - * entity_reference_revisions_display = TRUE - * ) - */ -class EntityReferenceRevisions extends DisplayPluginBase { - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$useAJAX. - */ - protected $usesAJAX = FALSE; - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesPager. - */ - protected $usesPager = FALSE; - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesAttachments. - */ - protected $usesAttachments = FALSE; - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::defineOptions(). - */ - protected function defineOptions() { - $options = parent::defineOptions(); - - // Force the style plugin to 'entity_reference_style' and the row plugin to - // 'fields'. - $options['style']['contains']['type'] = array('default' => 'entity_reference_revisions'); - $options['defaults']['default']['style'] = FALSE; - $options['row']['contains']['type'] = array('default' => 'entity_reference_revisions'); - $options['defaults']['default']['row'] = FALSE; - - // Make sure the query is not cached. - $options['defaults']['default']['cache'] = FALSE; - - // Set the display title to an empty string (not used in this display type). - $options['title']['default'] = ''; - $options['defaults']['default']['title'] = FALSE; - - return $options; - } - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::optionsSummary(). - * - * Disable 'cache' and 'title' so it won't be changed. - */ - public function optionsSummary(&$categories, &$options) { - parent::optionsSummary($categories, $options); - unset($options['query']); - unset($options['title']); - } - - /** - * {@inheritdoc} - */ - public function getType() { - return 'entity_reference_revisions'; - } - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::execute(). - */ - public function execute() { - return $this->view->render($this->display['id']); - } - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::render(). - */ - public function render() { - if (!empty($this->view->result) && $this->view->style_plugin->evenEmpty()) { - return $this->view->style_plugin->render($this->view->result); - } - return ''; - } - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::usesExposed(). - */ - public function usesExposed() { - return FALSE; - } - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::query(). - */ - public function query() { - if (!empty($this->view->live_preview)) { - return; - } - - // Make sure the id field is included in the results. - $id_field = $this->view->storage->get('base_field'); - $this->id_field_alias = $this->view->query->addField($this->view->storage->get('base_table'), $id_field); - - $options = $this->getOption('entity_reference_revisions_options'); - - // Restrict the autocomplete options based on what's been typed already. - if (isset($options['match'])) { - $style_options = $this->getOption('style'); - $value = db_like($options['match']) . '%'; - if ($options['match_operator'] != 'STARTS_WITH') { - $value = '%' . $value; - } - - // Multiple search fields are OR'd together. - $conditions = db_or(); - - // Build the condition using the selected search fields. - foreach ($style_options['options']['search_fields'] as $field_alias) { - if (!empty($field_alias)) { - // Get the table and field names for the checked field. - $field = $this->view->query->fields[$this->view->field[$field_alias]->field_alias]; - // Add an OR condition for the field. - $conditions->condition($field['table'] . '.' . $field['field'], $value, 'LIKE'); - } - } - - $this->view->query->addWhere(0, $conditions); - } - - // Add an IN condition for validation. - if (!empty($options['ids'])) { - $this->view->query->addWhere(0, $id_field, $options['ids']); - } - - $this->view->setItemsPerPage($options['limit']); - } - - /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::validate(). - */ - public function validate() { - $errors = parent::validate(); - // Verify that search fields are set up. - $style = $this->getOption('style'); - if (!isset($style['options']['search_fields'])) { - $errors[] = $this->t('Display "@display" needs a selected search fields to work properly. See the settings for the Entity Reference Revisions list format.', array('@display' => $this->display['display_title'])); - } - else { - // Verify that the search fields used actually exist. - $fields = array_keys($this->handlers['field']); - foreach ($style['options']['search_fields'] as $field_alias => $enabled) { - if ($enabled && !in_array($field_alias, $fields)) { - $errors[] = $this->t('Display "@display" uses field %field as search field, but the field is no longer present. See the settings for the Entity Reference Revisions list format.', array('@display' => $this->display['display_title'], '%field' => $field_alias)); - } - } - } - return $errors; - } -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php deleted file mode 100644 index fe25de202..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\views\row; - -use Drupal\Core\Form\FormStateInterface; -use Drupal\views\Plugin\views\row\Fields; - -/** - * EntityReferenceRevisions row plugin. - * - * @ingroup views_row_plugins - * - * @ViewsRow( - * id = "entity_reference_revisions", - * title = @Translation("Entity Reference inline fields"), - * help = @Translation("Displays the fields with an optional template."), - * theme = "views_view_fields", - * register_theme = FALSE, - * display_types = {"entity_reference"} - * ) - */ -class EntityReferenceRevisions extends Fields { - - /** - * Overrides \Drupal\views\Plugin\views\row\Fields::defineOptions(). - */ - protected function defineOptions() { - $options = parent::defineOptions(); - $options['separator'] = array('default' => '-'); - - return $options; - } - - /** - * Overrides \Drupal\views\Plugin\views\row\Fields::buildOptionsForm(). - */ - public function buildOptionsForm(&$form, FormStateInterface $form_state) { - parent::buildOptionsForm($form, $form_state); - - // Expand the description of the 'Inline field' checkboxes. - $form['inline']['#description'] .= '<br />' . $this->t("<strong>Note:</strong> In 'Entity Reference' displays, all fields will be displayed inline unless an explicit selection of inline fields is made here." ); - } - - /** - * {@inheritdoc} - */ - public function preRender($row) { - // Force all fields to be inline by default. - if (empty($this->options['inline'])) { - $fields = $this->view->getHandlers('field', $this->displayHandler->display['id']); - $names = array_keys($fields); - $this->options['inline'] = array_combine($names, $names); - } - - return parent::preRender($row); - } -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php deleted file mode 100644 index 376491ee7..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php +++ /dev/null @@ -1,103 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Plugin\views\style; - -use Drupal\Component\Utility\Xss; -use Drupal\Core\Form\FormStateInterface; -use Drupal\views\Plugin\views\style\StylePluginBase; - -/** - * EntityReferenceRevisions style plugin. - * - * @ingroup views_style_plugins - * - * @ViewsStyle( - * id = "entity_reference_revisions", - * title = @Translation("Entity Reference Revisions list"), - * help = @Translation("Returns results as a PHP array of labels and rendered rows."), - * theme = "views_view_unformatted", - * register_theme = FALSE, - * display_types = {"entity_reference_revisions"} - * ) - */ -class EntityReferenceRevisions extends StylePluginBase { - - /** - * Overrides \Drupal\views\Plugin\views\style\StylePluginBase::usesRowPlugin. - */ - protected $usesRowPlugin = TRUE; - - /** - * Overrides \Drupal\views\Plugin\views\style\StylePluginBase::usesFields. - */ - protected $usesFields = TRUE; - - /** - * Overrides \Drupal\views\Plugin\views\style\StylePluginBase::usesGrouping. - */ - protected $usesGrouping = FALSE; - - /** - * Overrides \Drupal\views\Plugin\views\style\StylePluginBase\StylePluginBase::defineOptions(). - */ - protected function defineOptions() { - $options = parent::defineOptions(); - $options['search_fields'] = array('default' => NULL); - - return $options; - } - - /** - * Overrides \Drupal\views\Plugin\views\style\StylePluginBase\StylePluginBase::buildOptionsForm(). - */ - public function buildOptionsForm(&$form, FormStateInterface $form_state) { - parent::buildOptionsForm($form, $form_state); - - $options = $this->displayHandler->getFieldLabels(TRUE); - $form['search_fields'] = array( - '#type' => 'checkboxes', - '#title' => $this->t('Search fields'), - '#options' => $options, - '#required' => TRUE, - '#default_value' => $this->options['search_fields'], - '#description' => $this->t('Select the field(s) that will be searched when using the autocomplete widget.'), - '#weight' => -3, - ); - } - - /** - * Overrides \Drupal\views\Plugin\views\style\StylePluginBase\StylePluginBase::render(). - */ - public function render() { - if (!empty($this->view->live_preview)) { - return parent::render(); - } - - // Group the rows according to the grouping field, if specified. - $sets = $this->renderGrouping($this->view->result, $this->options['grouping']); - - // Grab the alias of the 'id' field added by - // entity_reference_plugin_display. - $id_field_alias = $this->view->storage->get('base_field'); - - // @todo We don't display grouping info for now. Could be useful for select - // widget, though. - $results = array(); - foreach ($sets as $records) { - foreach ($records as $values) { - // Sanitize HTML, remove line breaks and extra whitespace. - $output = $this->view->rowPlugin->render($values); - $output = \Drupal::service('renderer')->render($output); - $results[$values->{$id_field_alias}] = Xss::filterAdmin(preg_replace('/\s\s+/', ' ', str_replace("\n", '', $output))); - } - } - return $results; - } - - /** - * {@inheritdoc} - */ - public function evenEmpty() { - return TRUE; - } -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php deleted file mode 100644 index cc7caeddb..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php +++ /dev/null @@ -1,184 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Tests; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; - -/** - * Tests the entity_reference_revisions configuration. - * - * @group entity_reference_revisions - */ -class EntityReferenceRevisionsAdminTest extends WebTestBase { - - use FieldUiTestTrait; - use EntityReferenceRevisionsCoreVersionUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'node', - 'field', - 'entity_reference_revisions', - 'field_ui', - 'block', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create paragraphs and article content types. - $this->drupalCreateContentType(array('type' => 'entity_revisions', 'name' => 'Entity revisions')); - $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); - // Place the breadcrumb, tested in fieldUIAddNewField(). - $this->drupalPlaceBlock('system_breadcrumb_block'); - $admin_user = $this->drupalCreateUser(array( - 'administer site configuration', - 'administer nodes', - 'create article content', - 'administer content types', - 'administer node fields', - 'administer node display', - 'administer node form display', - 'edit any article content', - )); - $this->drupalLogin($admin_user); - } - - /** - * Tests the entity reference revisions configuration. - */ - public function testEntityReferenceRevisions() { - // Create a test target node used as entity reference by another test node. - $node_target = Node::create([ - 'title' => 'Target node', - 'type' => 'article', - 'body' => 'Target body text', - 'uuid' => '2d04c2b4-9c3d-4fa6-869e-ecb6fa5c9410', - ]); - $node_target->save(); - - // Add an entity reference revisions field to entity_revisions content type - // with $node_target as default value. - $storage_edit = ['settings[target_type]' => 'node', 'cardinality' => '-1']; - $field_edit = [ - 'settings[handler_settings][target_bundles][article]' => TRUE, - 'default_value_input[field_entity_reference_revisions][0][target_id]' => $node_target->label() . ' (' . $node_target->id() . ')', - ]; - static::fieldUIAddNewField('admin/structure/types/manage/entity_revisions', 'entity_reference_revisions', 'Entity reference revisions', 'entity_reference_revisions', $storage_edit, $field_edit); - \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); - $this->assertText('Saved Entity reference revisions configuration.'); - - // Resave the target node, so that the default revision is not the one we - // want to use. - $revision_id = $node_target->getRevisionId(); - $node_target_after = Node::load($node_target->id()); - $node_target_after->setNewRevision(); - $node_target_after->save(); - $this->assertTrue($node_target_after->getRevisionId() != $revision_id); - - // Create an article. - $title = $this->randomMachineName(); - $edit = array( - 'title[0][value]' => $title, - 'body[0][value]' => 'Revision 1', - ); - $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); - $this->assertText($title); - $this->assertText('Revision 1'); - $node = $this->drupalGetNodeByTitle($title); - - // Check if when creating an entity revisions content the default entity - // reference is set, add also the above article as a new reference. - $this->drupalGet('node/add/entity_revisions'); - $this->assertFieldByName('field_entity_reference_revisions[0][target_id]', $node_target->label() . ' (' . $node_target->id() . ')'); - $edit = [ - 'title[0][value]' => 'Entity reference revision content', - 'field_entity_reference_revisions[1][target_id]' => $node->label() . ' (' . $node->id() . ')', - ]; - $this->drupalPostNodeForm(NULL, $edit, t('Save and publish')); - $this->assertLinkByHref('node/' . $node_target->id()); - $this->assertText('Entity revisions Entity reference revision content has been created.'); - $this->assertText('Entity reference revision content'); - $this->assertText($title); - $this->assertText('Revision 1'); - - // Create 2nd revision of the article. - $edit = array( - 'body[0][value]' => 'Revision 2', - 'revision' => TRUE, - ); - $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); - $this->assertText($title); - $this->assertText('Revision 2'); - - // View the Entity reference content and make sure it still has revision 1. - $node = $this->drupalGetNodeByTitle('Entity reference revision content'); - $this->drupalGet('node/' . $node->id()); - $this->assertText($title); - $this->assertText('Revision 1'); - $this->assertNoText('Revision 2'); - - // Make sure the non-revisionable entities are not selectable as referenced - // entities. - $edit = array( - 'new_storage_type' => 'entity_reference_revisions', - 'label' => 'Entity reference revisions field', - 'field_name' => 'entity_ref_revisions_field', - ); - $this->drupalPostForm('admin/structure/types/manage/entity_revisions/fields/add-field', $edit, t('Save and continue')); - $this->assertNoOption('edit-settings-target-type', 'user'); - $this->assertOption('edit-settings-target-type', 'node'); - - // Check ERR default value and property definitions label are set properly. - $field_definition = $node->getFieldDefinition('field_entity_reference_revisions'); - $default_value = $field_definition->toArray()['default_value']; - $this->assertEqual($default_value[0]['target_uuid'], $node_target->uuid()); - $this->assertEqual($default_value[0]['target_revision_id'], $revision_id); - $properties = $field_definition->getFieldStorageDefinition()->getPropertyDefinitions(); - $this->assertEqual((string) $properties['target_revision_id']->getLabel(), 'Content revision ID'); - $this->assertEqual((string) $properties['target_id']->getLabel(), 'Content ID'); - $this->assertEqual((string) $properties['entity']->getLabel(), 'Content'); - } - - /** - * Tests target bundle settings for an entity reference revisions field. - */ - public function testMultipleTargetBundles() { - // Create a couple of content types for the ERR field to point to. - $target_types = []; - for ($i = 0; $i < 2; $i++) { - $target_types[$i] = $this->drupalCreateContentType([ - 'type' => strtolower($this->randomMachineName()), - 'name' => 'Test type ' . $i - ]); - } - - // Create a new field that can point to either target content type. - $node_type_path = 'admin/structure/types/manage/entity_revisions'; - - // Generate a random field name, must be only lowercase characters. - $field_name = strtolower($this->randomMachineName()); - - $field_edit = []; - $storage_edit = ['settings[target_type]' => 'node', 'cardinality' => '-1']; - $field_edit['settings[handler_settings][target_bundles][' . $target_types[0]->id() . ']'] = TRUE; - $field_edit['settings[handler_settings][target_bundles][' . $target_types[1]->id() . ']'] = TRUE; - - $this->fieldUIAddNewField($node_type_path, $field_name, 'Entity reference revisions', 'entity_reference_revisions', $storage_edit, $field_edit); - - // Deleting one of these content bundles at this point should only delete - // that bundle's body field. Test that there is no second field that will - // be deleted. - $this->drupalGet('/admin/structure/types/manage/' . $target_types[0]->id() . '/delete'); - $this->assertNoFieldByXPath('(//details[@id="edit-entity-deletes"]//ul[@data-drupal-selector="edit-field-config"]/li)[2]'); - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php deleted file mode 100644 index a80c615e5..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php +++ /dev/null @@ -1,147 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Tests; - -use Drupal\block_content\Entity\BlockContent; -use Drupal\Component\Utility\Html; -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; - -/** - * Tests the entity_reference_revisions autocomplete. - * - * @group entity_reference_revisions - */ -class EntityReferenceRevisionsAutocompleteTest extends WebTestBase { - - use FieldUiTestTrait; - use EntityReferenceRevisionsCoreVersionUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'block_content', - 'node', - 'field', - 'entity_reference_revisions', - 'field_ui', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create article content type. - $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); - // Place the breadcrumb, tested in fieldUIAddNewField(). - $this->drupalPlaceBlock('system_breadcrumb_block'); - } - - /** - * Test for autocomplete processing. - * - * Tests that processing does not crash when the entity types of the - * referenced entity and of the entity the field is attached to are different. - */ - public function testEntityReferenceRevisionsAutocompleteProcessing() { - $admin_user = $this->drupalCreateUser(array( - 'administer site configuration', - 'administer nodes', - 'administer blocks', - 'create article content', - 'administer content types', - 'administer node fields', - 'administer node display', - 'administer node form display', - 'edit any article content', - )); - $this->drupalLogin($admin_user); - - // Create a custom block content bundle. - $this->createBlockContentType(array('type' => 'customblockcontent', 'name' => 'Custom Block Content')); - - // Create entity reference revisions field attached to article. - static::fieldUIAddNewField( - 'admin/structure/types/manage/article', - 'entity_reference_revisions', - 'Entity reference revisions', - 'entity_reference_revisions', - array('settings[target_type]' => 'block_content', 'cardinality' => '-1'), - array('settings[handler_settings][target_bundles][customblockcontent]' => TRUE) - ); - - // Create custom block. - $block_label = $this->randomMachineName(); - $block_content = $this->randomString(); - $edit = array( - 'info[0][value]' => $block_label, - 'body[0][value]' => $block_content, - ); - $this->drupalPostForm('block/add', $edit, t('Save')); - $block = $this->drupalGetBlockByInfo($block_label); - - // Create an article. - $title = $this->randomMachineName(); - $edit = array( - 'title[0][value]' => $title, - 'body[0][value]' => 'Revision 1', - 'field_entity_reference_revisions[0][target_id]' => $block_label . ' (' . $block->id() . ')', - ); - $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); - $this->assertText($title); - $this->assertText(Html::escape($block_content)); - - // Check if the block content is not deleted since there is no composite - // relationship. - $node = $this->drupalGetNodeByTitle($edit['title[0][value]']); - $node = Node::load($node->id()); - $node->delete(); - $this->assertNotNull(BlockContent::load($block->id())); - } - - /** - * Get a custom block from the database based on its title. - * - * @param $info - * A block title, usually generated by $this->randomMachineName(). - * @param $reset - * (optional) Whether to reset the entity cache. - * - * @return \Drupal\block\BlockInterface - * A block entity matching $info. - */ - function drupalGetBlockByInfo($info, $reset = FALSE) { - if ($reset) { - \Drupal::entityTypeManager()->getStorage('block_content')->resetCache(); - } - $blocks = \Drupal::entityTypeManager()->getStorage('block_content')->loadByProperties(array('info' => $info)); - // Get the first block returned from the database. - $returned_block = reset($blocks); - return $returned_block; - } - - /** - * Create a block_content bundle. - * - * @param $parameters - * An assoc array with name (human readable) and type (bundle machine name) - * as keys. - */ - function createBlockContentType($parameters) { - $label = $parameters['name']; - $machine_name = $parameters['type']; - $edit = array( - 'label' => $label, - 'id' => $machine_name, - 'revision' => TRUE, - ); - $this->drupalPostForm('admin/structure/block/block-content/types/add', $edit, t('Save')); - $this->assertText($label); - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php deleted file mode 100644 index 596d9416f..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Tests; - -/** - * Provides helper methods for Drupal 8.3.x and 8.4.x versions. - */ -trait EntityReferenceRevisionsCoreVersionUiTestTrait { - - /** - * An adapter for 8.3 > 8.4 Save (and (un)publish) node button change. - * - * @see \Drupal\simpletest\WebTestBase::drupalPostForm - * @see https://www.drupal.org/node/2847274 - */ - protected function drupalPostNodeForm($path, $edit, $submit, array $options = [], array $headers = [], $form_html_id = NULL, $extra_post = NULL) { - $drupal_version = (float) substr(\Drupal::VERSION, 0, 3); - if ($drupal_version > 8.3) { - - switch ($submit) { - case t('Save and unpublish'): - $edit['status[value]'] = FALSE; - break; - - case t('Save and publish'): - $edit['status[value]'] = TRUE; - break; - } - - $submit = t('Save'); - } - parent::drupalPostForm($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post); - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php deleted file mode 100644 index d4f236abe..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php +++ /dev/null @@ -1,121 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Tests; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\simpletest\WebTestBase; - -/** - * Tests the entity_reference_revisions diff plugin. - * - * @group entity_reference_revisions - * - * @dependencies diff - */ -class EntityReferenceRevisionsDiffTest extends WebTestBase { - - use FieldUiTestTrait; - use EntityReferenceRevisionsCoreVersionUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'block_content', - 'node', - 'field', - 'entity_reference_revisions', - 'field_ui', - 'diff', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create article content type. - $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); - - // Disable visual inline diff. - $config = $this->config('diff.settings') - ->set('general_settings.layout_plugins.visual_inline.enabled', FALSE); - $config->save(); - - $admin_user = $this->drupalCreateUser([ - 'administer site configuration', - 'administer nodes', - 'administer content types', - 'administer node fields', - 'administer node display', - 'administer node form display', - 'view all revisions', - 'edit any article content', - ]); - $this->drupalLogin($admin_user); - $this->drupalPlaceBlock('system_breadcrumb_block'); - } - - /** - * Test for diff plugin of ERR. - * - * Tests that the diff is displayed when changes are made in an ERR field. - */ - public function testEntityReferenceRevisionsDiff() { - // Add an entity_reference_revisions field. - static::fieldUIAddNewField('admin/structure/types/manage/article', 'err_field', 'err_field', 'entity_reference_revisions', [ - 'settings[target_type]' => 'node', - 'cardinality' => '-1', - ], [ - 'settings[handler_settings][target_bundles][article]' => TRUE, - ]); - - // Create first referenced node. - $title_node_1 = 'referenced_node_1'; - $edit = [ - 'title[0][value]' => $title_node_1, - 'body[0][value]' => 'body_node_1', - ]; - $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); - - // Create second referenced node. - $title_node_2 = 'referenced_node_2'; - $edit = [ - 'title[0][value]' => $title_node_2, - 'body[0][value]' => 'body_node_2', - ]; - $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); - - // Create referencing node. - $title = 'referencing_node'; - $node = $this->drupalGetNodeByTitle($title_node_1); - $edit = [ - 'title[0][value]' => $title, - 'field_err_field[0][target_id]' => $title_node_1 . ' (' . $node->id() . ')', - ]; - $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); - - // Check the plugin is set. - $this->drupalGet('admin/config/content/diff/fields'); - $this->drupalPostForm(NULL, ['fields[node.field_err_field][plugin][type]' => 'entity_reference_revisions_field_diff_builder'], t('Save')); - - // Update the referenced node of the err field and create a new revision. - $node = $this->drupalGetNodeByTitle($title); - $referenced_node_new = $this->drupalGetNodeByTitle($title_node_2); - $edit = [ - 'field_err_field[0][target_id]' => $title_node_2 . ' (' . $referenced_node_new->id() . ')', - 'revision' => TRUE, - ]; - $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); - - // Compare the revisions of the referencing node. - $this->drupalPostForm('node/' . $node->id() . '/revisions', [], t('Compare selected revisions')); - - // Assert the field changes. - $this->assertRaw('class="diff-context diff-deletedline">' . $title_node_1); - $this->assertRaw('class="diff-context diff-addedline">' . $title_node_2); - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php deleted file mode 100644 index ea5bcaf9e..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php +++ /dev/null @@ -1,106 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\Tests; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; - -/** - * Tests the entity_reference_revisions configuration. - * - * @group entity_reference_revisions - */ -class EntityReferenceRevisionsNormalizerTest extends WebTestBase { - - use FieldUiTestTrait; - use EntityReferenceRevisionsCoreVersionUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'node', - 'field', - 'entity_reference_revisions', - 'field_ui', - 'block', - 'hal', - 'serialization', - 'rest', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create paragraphs and article content types. - $this->drupalCreateContentType(array('type' => 'entity_revisions', 'name' => 'Entity revisions')); - $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); - // Place the breadcrumb, tested in fieldUIAddNewField(). - $this->drupalPlaceBlock('system_breadcrumb_block'); - } - - /** - * Tests the entity reference revisions configuration. - */ - public function testEntityReferenceRevisions() { - $admin_user = $this->drupalCreateUser(array( - 'administer site configuration', - 'administer nodes', - 'create article content', - 'administer content types', - 'administer node fields', - 'administer node display', - 'administer node form display', - 'edit any article content', - )); - $this->drupalLogin($admin_user); - // Create entity reference revisions field. - static::fieldUIAddNewField('admin/structure/types/manage/entity_revisions', 'entity_reference_revisions', 'Entity reference revisions', 'entity_reference_revisions', array('settings[target_type]' => 'node', 'cardinality' => '-1'), array('settings[handler_settings][target_bundles][article]' => TRUE)); - $this->assertText('Saved Entity reference revisions configuration.'); - - // Create an article. - $title = $this->randomMachineName(); - $edit = array( - 'title[0][value]' => $title, - 'body[0][value]' => 'Revision 1', - ); - $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); - $this->assertText($title); - $this->assertText('Revision 1'); - $node = $this->drupalGetNodeByTitle($title); - - // Create entity revisions content that includes the above article. - $err_title = 'Entity reference revision content'; - $edit = array( - 'title[0][value]' => $err_title, - 'field_entity_reference_revisions[0][target_id]' => $node->label() . ' (' . $node->id() . ')', - ); - $this->drupalPostNodeForm('node/add/entity_revisions', $edit, t('Save and publish')); - $this->assertText('Entity revisions Entity reference revision content has been created.'); - $err_node = $this->drupalGetNodeByTitle($err_title); - - $this->assertText($err_title); - $this->assertText($title); - $this->assertText('Revision 1'); - - // Create 2nd revision of the article. - $edit = array( - 'body[0][value]' => 'Revision 2', - 'revision' => TRUE, - ); - $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); - $serializer = $this->container->get('serializer'); - $normalized = $serializer->normalize($err_node, 'hal_json'); - $request = \Drupal::request(); - $link_domain = $request->getSchemeAndHttpHost() . $request->getBasePath(); - $this->assertEqual($err_node->field_entity_reference_revisions->target_revision_id, $normalized['_embedded'][$link_domain . '/rest/relation/node/entity_revisions/field_entity_reference_revisions'][0]['target_revision_id']); - $new_err_node = $serializer->denormalize($normalized, Node::class, 'hal_json'); - $this->assertEqual($err_node->field_entity_reference_revisions->target_revision_id, $new_err_node->field_entity_reference_revisions->target_revision_id); - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php b/web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php deleted file mode 100644 index 7ffac7d8f..000000000 --- a/web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -namespace Drupal\entity_reference_revisions\TypedData; - -use Drupal\Core\Entity\TypedData\EntityDataDefinition; -use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface; - -/** - * A typed data definition class for describing entities. - */ -class EntityRevisionDataDefinition extends EntityDataDefinition implements EntityDataDefinitionInterface { - - /** - * {@inheritdoc} - */ - public static function createFromDataType($data_type) { - $parts = explode(':', $data_type); - if ($parts[0] != 'entity_revision') { - throw new \InvalidArgumentException('Data type must be in the form of "entity_revision:ENTITY_TYPE:BUNDLE."'); - } - $definition = static::create(); - // Set the passed entity type and bundle. - if (isset($parts[1])) { - $definition->setEntityTypeId($parts[1]); - } - if (isset($parts[2])) { - $definition->setBundles(array($parts[2])); - } - return $definition; - } - - /** - * {@inheritdoc} - */ - public function getDataType() { - $type = 'entity_revision'; - if ($entity_type = $this->getEntityTypeId()) { - $type .= ':' . $entity_type; - // Append the bundle only if we know it for sure and it is not the default - // bundle. - if (($bundles = $this->getBundles()) && count($bundles) == 1) { - $bundle = reset($bundles); - if ($bundle != $entity_type) { - $type .= ':' . $bundle; - } - } - } - return $type; - } -} diff --git a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml deleted file mode 100644 index f0054190a..000000000 --- a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: 'ERR Composite relationship test' -type: module -description: 'Entity with parent type and ID.' -package: Testing -# core: 8.x - -dependencies: - - entity_reference_revisions - - entity_test -# Information added by Drupal.org packaging script on 2018-01-05 -version: '8.x-1.4' -core: '8.x' -project: 'entity_reference_revisions' -datestamp: 1515143887 diff --git a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml deleted file mode 100644 index c9be8a03b..000000000 --- a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml +++ /dev/null @@ -1,2 +0,0 @@ -administer entity_test composite relationship: - title: administer entity_test composite relationship diff --git a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php deleted file mode 100644 index fb61ad159..000000000 --- a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php +++ /dev/null @@ -1,60 +0,0 @@ -<?php - -namespace Drupal\entity_composite_relationship_test\Entity; - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\entity_reference_revisions\EntityNeedsSaveInterface; -use Drupal\entity_reference_revisions\EntityNeedsSaveTrait; -use Drupal\entity_test\Entity\EntityTestMulRev; - -/** - * Defines the test entity class. - * - * @ContentEntityType( - * id = "entity_test_composite", - * label = @Translation("Test entity - composite relationship"), - * base_table = "entity_test_composite", - * revision_table = "entity_test_composite_revision", - * data_table = "entity_test_composite_field_data", - * revision_data_table = "entity_test_composite_field_revision", - * translatable = TRUE, - * entity_revision_parent_type_field = "parent_type", - * entity_revision_parent_id_field = "parent_id", - * entity_revision_parent_field_name_field = "parent_field_name", - * admin_permission = "administer entity_test composite relationship", - * entity_keys = { - * "id" = "id", - * "uuid" = "uuid", - * "revision" = "revision_id", - * "bundle" = "type", - * "label" = "name", - * "langcode" = "langcode", - * } - * ) - */ -class EntityTestCompositeRelationship extends EntityTestMulRev implements EntityNeedsSaveInterface { - - use EntityNeedsSaveTrait; - - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields = parent::baseFieldDefinitions($entity_type); - $fields['parent_id'] = BaseFieldDefinition::create('string') - ->setLabel(t('Parent ID')) - ->setDescription(t('The ID of the parent entity of which this entity is referenced.')); - - $fields['parent_type'] = BaseFieldDefinition::create('string') - ->setLabel(t('Parent type')) - ->setDescription(t('The entity parent type to which this entity is referenced.')); - - $fields['parent_field_name'] = BaseFieldDefinition::create('string') - ->setLabel(t('Parent field name')) - ->setDescription(t('The entity parent field name to which this entity is referenced.')); - - return $fields; - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php deleted file mode 100644 index e92bfbe2b..000000000 --- a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php +++ /dev/null @@ -1,407 +0,0 @@ -<?php - -namespace Drupal\Tests\entity_reference_revisions\Kernel; - -use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; -use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\node\Entity\Node; -use Drupal\node\Entity\NodeType; -use Drupal\simpletest\ContentTypeCreationTrait; -use Drupal\simpletest\NodeCreationTrait; - -/** - * Tests the entity_reference_revisions composite relationship. - * - * @group entity_reference_revisions - */ -class EntityReferenceRevisionsCompositeTest extends EntityKernelTestBase { - - use ContentTypeCreationTrait; - use NodeCreationTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'node', - 'field', - 'entity_reference_revisions', - 'entity_composite_relationship_test', - 'language' - ); - - /** - * The current database connection. - * - * @var \Drupal\Core\Database\Connection - */ - protected $database; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - * - */ - protected $entityTypeManager; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->installEntitySchema('entity_test_composite'); - $this->installSchema('node', ['node_access']); - - // Create article content type. - NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); - - // Create the reference to the composite entity test. - $field_storage = FieldStorageConfig::create(array( - 'field_name' => 'composite_reference', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'settings' => array( - 'target_type' => 'entity_test_composite' - ), - )); - $field_storage->save(); - $field = FieldConfig::create(array( - 'field_storage' => $field_storage, - 'bundle' => 'article', - 'translatable' => FALSE, - )); - $field->save(); - - // Inject database connection and entity type manager for the tests. - $this->database = \Drupal::database(); - $this->entityTypeManager = \Drupal::entityTypeManager(); - } - - /** - * Test for maintaining composite relationship. - * - * Tests that the referenced entity saves the parent type and id when saving. - */ - public function testEntityReferenceRevisionsCompositeRelationship() { - // Create the test composite entity. - $composite = EntityTestCompositeRelationship::create(array( - 'uuid' => $this->randomMachineName(), - 'name' => $this->randomMachineName(), - )); - $composite->save(); - - // Assert that there is only 1 revision of the composite entity. - $composite_revisions_count = \Drupal::entityQuery('entity_test_composite')->condition('uuid', $composite->uuid())->allRevisions()->count()->execute(); - $this->assertEquals(1, $composite_revisions_count); - - // Create a node with a reference to the test composite entity. - $node = Node::create(array( - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $composite, - )); - $node->save(); - - // Assert that there is only 1 revision when creating a node. - $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); - $this->assertEqual($node_revisions_count, 1); - // Assert there is no new composite revision after creating a host entity. - $composite_revisions_count = \Drupal::entityQuery('entity_test_composite')->condition('uuid', $composite->uuid())->allRevisions()->count()->execute(); - $this->assertEquals(1, $composite_revisions_count); - - // Verify the value of parent type and id after create a node. - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); - // Create second revision of the node. - $original_composite_revision = $node->composite_reference[0]->target_revision_id; - $original_node_revision = $node->getRevisionId(); - $node->setTitle('2nd revision'); - $node->setNewRevision(); - $node->save(); - $node = node_load($node->id(), TRUE); - // Check the revision of the node. - $this->assertEqual('2nd revision', $node->getTitle(), 'New node revision has changed data.'); - $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host did.'); - - // Make sure that there are only 2 revisions. - $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); - $this->assertEqual($node_revisions_count, 2); - - // Revert to first revision of the node. - $node = $this->entityTypeManager->getStorage('node')->loadRevision($original_node_revision); - $node->setNewRevision(); - $node->isDefaultRevision(TRUE); - $node->save(); - $node = node_load($node->id(), TRUE); - // Check the revision of the node. - $this->assertNotEqual('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.'); - $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.'); - - // Test that the composite entity is deleted when its parent is deleted. - $node->delete(); - $this->assertNull(EntityTestCompositeRelationship::load($composite->id())); - } - - /** - * Tests composite relationship with translations and an untranslatable field. - */ - function testCompositeRelationshipWithTranslationNonTranslatableField() { - - ConfigurableLanguage::createFromLangcode('de')->save(); - - // Create the test composite entity with a translation. - $composite = EntityTestCompositeRelationship::create(array( - 'uuid' => $this->randomMachineName(), - 'name' => $this->randomMachineName(), - )); - $composite->addTranslation('de', $composite->toArray()); - $composite->save(); - - - // Create a node with a reference to the test composite entity. - $node = Node::create(array( - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $composite, - )); - $node->addTranslation('de', $node->toArray()); - $node->save(); - - // Verify the value of parent type and id after create a node. - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); - $this->assertTrue($composite->hasTranslation('de')); - - // Test that the composite entity is not when the german translation of the - // parent is deleted. - $node->removeTranslation('de'); - $node->save(); - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertNotNull($composite); - // @todo Support deleting translations of a composite reference. - // @see https://www.drupal.org/node/2834314. - //$this->assertFalse($composite->hasTranslation('de')); - - // Test that the composite entity is deleted when its parent is deleted. - $node->delete(); - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertNull($composite); - } - - /** - * Tests composite relationship with translations and a translatable field. - */ - function testCompositeRelationshipWithTranslationTranslatableField() { - $field_config = FieldConfig::loadByName('node', 'article', 'composite_reference'); - $field_config->setTranslatable(TRUE); - $field_config->save(); - - ConfigurableLanguage::createFromLangcode('de')->save(); - - // Create the test composite entity with a translation. - $composite = EntityTestCompositeRelationship::create(array( - 'uuid' => $this->randomMachineName(), - 'name' => $this->randomMachineName(), - )); - $composite->addTranslation('de', $composite->toArray()); - $composite->save(); - - // Create a node with a reference to the test composite entity. - $node = Node::create(array( - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $composite, - )); - $node->addTranslation('de', $node->toArray()); - $node->save(); - - // Verify the value of parent type and id after create a node. - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); - - // Test that the composite entity is not when the german translation of the parent is deleted. - $node->removeTranslation('de'); - $node->save(); - //$this->entityTypeManager->getStorage('entity_test_composite')->resetCache(); - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertNotNull($composite); - - // Test that the composite entity is deleted when its parent is deleted. - $node->delete(); - $composite = EntityTestCompositeRelationship::load($composite->id()); - // @todo Support deletions for translatable fields. - // @see https://www.drupal.org/node/2834374 - // $this->assertNull($composite); - } - - /** - * Tests composite relationship with revisions. - */ - function testCompositeRelationshipWithRevisions() { - - // Create the test composite entity with a translation. - $composite = EntityTestCompositeRelationship::create(array( - 'uuid' => $this->randomMachineName(), - 'name' => $this->randomMachineName(), - )); - $composite->save(); - - // Create a node with a reference to the test composite entity. - $node = Node::create(array( - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $composite, - )); - $node->save(); - - - // Verify the value of parent type and id after create a node. - $composite = EntityTestCompositeRelationship::load($composite->id()); - $composite_original_revision_id = $composite->getRevisionId(); - $node_original_revision_id = $node->getRevisionId(); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); - - $node->setNewRevision(TRUE); - $node->save(); - // Ensure that we saved a new revision ID. - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertNotEqual($composite->getRevisionId(), $composite_original_revision_id); - - // Test that deleting the first revision does not delete the composite. - $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id); - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertNotNull($composite); - - // Ensure that the composite revision was deleted as well. - $composite_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite_original_revision_id); - $this->assertNull($composite_revision); - - // Test that the composite entity is deleted when its parent is deleted. - $node->delete(); - $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertNull($composite); - } - - /** - * Tests that the composite revision is not deleted if it is the default one. - */ - function testCompositeRelationshipDefaultRevision() { - // Create a node with a reference to a test composite entity. - $composite = EntityTestCompositeRelationship::create([ - 'uuid' => $this->randomMachineName(), - 'name' => $this->randomMachineName(), - ]); - $composite->save(); - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $composite, - ]); - $node->save(); - - $composite = EntityTestCompositeRelationship::load($composite->id()); - $composite_original_revision_id = $composite->getRevisionId(); - $node_original_revision_id = $node->getRevisionId(); - - // Set a new revision, composite entity should have a new revision as well. - $node->setNewRevision(TRUE); - $node->save(); - // Ensure that we saved a new revision ID. - $composite2 = EntityTestCompositeRelationship::load($composite->id()); - $composite2_rev_id = $composite2->getRevisionId(); - $this->assertNotEquals($composite2_rev_id, $composite_original_revision_id); - - // Revert default composite entity revision to the original revision. - $composite_original = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite_original_revision_id); - $composite_original->isDefaultRevision(TRUE); - $composite_original->save(); - // Check the default composite revision is the original composite revision. - $this->assertEquals($composite_original_revision_id, $composite_original->getrevisionId()); - - // Test deleting the first node revision, referencing to the default - // composite revision, does not delete the default composite revision. - $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id); - $composite_default = EntityTestCompositeRelationship::load($composite_original->id()); - $this->assertNotNull($composite_default); - $composite_default_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite_original->getrevisionId()); - $this->assertNotNull($composite_default_revision); - // Ensure the second revision still exists. - $composite2_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite2_rev_id); - $this->assertNotNull($composite2_revision); - } - - /** - * Tests that the composite revision is not deleted if it is still in use. - */ - function testCompositeRelationshipDuplicatedRevisions() { - // Create a node with a reference to a test composite entity. - $composite = EntityTestCompositeRelationship::create([ - 'uuid' => $this->randomMachineName(), - 'name' => $this->randomMachineName(), - ]); - $composite->save(); - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $composite, - ]); - $node->save(); - - $composite = EntityTestCompositeRelationship::load($composite->id()); - $composite_original_revision_id = $composite->getRevisionId(); - $node_original_revision_id = $node->getRevisionId(); - - // Set a new revision, composite entity should have a new revision as well. - $node->setNewRevision(TRUE); - $node->save(); - // Ensure that we saved a new revision ID. - $composite2 = EntityTestCompositeRelationship::load($composite->id()); - $composite2_rev_id = $composite2->getRevisionId(); - $this->assertNotEquals($composite2_rev_id, $composite_original_revision_id); - - // Set the new node revision to reference to the original composite - // revision as well to test this composite revision will not be deleted. - $this->database->update('node__composite_reference') - ->fields(['composite_reference_target_revision_id' => $composite_original_revision_id]) - ->condition('revision_id', $node->getRevisionId()) - ->execute(); - $this->database->update('node_revision__composite_reference') - ->fields(['composite_reference_target_revision_id' => $composite_original_revision_id]) - ->condition('revision_id', $node->getRevisionId()) - ->execute(); - - // Test deleting the first revision does not delete the composite. - $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id); - $composite2 = EntityTestCompositeRelationship::load($composite2->id()); - $this->assertNotNull($composite2); - - // Ensure the original composite revision is not deleted because it is - // still referenced by the second node revision. - $composite_original_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite_original_revision_id); - $this->assertNotNull($composite_original_revision); - // Ensure the second revision still exists. - $composite2_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite2_rev_id); - $this->assertNotNull($composite2_revision); - - // Test that the composite entity is deleted when its parent is deleted. - $node->delete(); - $composite = EntityTestCompositeRelationship::load($composite2->id()); - $this->assertNull($composite); - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php deleted file mode 100644 index 430d06e62..000000000 --- a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php +++ /dev/null @@ -1,106 +0,0 @@ -<?php - -namespace Drupal\Tests\entity_reference_revisions\Kernel; - -use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\KernelTests\KernelTestBase; -use Drupal\node\Entity\Node; -use Drupal\node\Entity\NodeType; -use Drupal\simpletest\UserCreationTrait; - -/** - * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter\EntityReferenceRevisionsEntityFormatter - * @group entity_reference_revisions - */ -class EntityReferenceRevisionsFormatterTest extends KernelTestBase { - - use UserCreationTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'node', - 'user', - 'system', - 'field', - 'entity_reference_revisions', - 'entity_composite_relationship_test', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create article content type. - $values = ['type' => 'article', 'name' => 'Article']; - $node_type = NodeType::create($values); - $node_type->save(); - $this->installEntitySchema('user'); - $this->installEntitySchema('node'); - $this->installEntitySchema('entity_test_composite'); - $this->installSchema('system', ['sequences']); - $this->installSchema('node', ['node_access']); - - // Add the entity_reference_revisions field to article. - $field_storage = FieldStorageConfig::create([ - 'field_name' => 'composite_reference', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'settings' => [ - 'target_type' => 'entity_test_composite' - ], - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'article', - ]); - $field->save(); - - $user = $this->createUser(['administer entity_test composite relationship']); - \Drupal::currentUser()->setAccount($user); - } - - public function testFormatterWithDeletedReference() { - // Create the test composite entity. - $text = 'Dummy text'; - $entity_test = EntityTestCompositeRelationship::create([ - 'uuid' => $text, - 'name' => $text, - ]); - $entity_test->save(); - - $text = 'Clever text'; - // Set the name to a new text. - /** @var \Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship $entity_test */ - $entity_test->name = $text; - $entity_test->setNeedsSave(TRUE); - - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $entity_test, - ]); - $node->save(); - - // entity_reference_revisions_entity_view - $result = $node->composite_reference->view(['type' => 'entity_reference_revisions_entity_view']); - $this->setRawContent($this->render($result)); - $this->assertText('Clever text'); - - // Remove the referenced entity. - $entity_test->delete(); - - $node = Node::load($node->id()); - $result = $node->composite_reference->view(['type' => 'entity_reference_revisions_entity_view']); - $this->render($result); - $this->assertNoText('Clever text'); - } - -} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php deleted file mode 100644 index e73a2dddc..000000000 --- a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php +++ /dev/null @@ -1,266 +0,0 @@ -<?php - -namespace Drupal\Tests\entity_reference_revisions\Kernel; - -use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\KernelTests\KernelTestBase; -use Drupal\node\Entity\Node; -use Drupal\node\Entity\NodeType; - -/** - * Tests the entity_reference_revisions NeedsSaveInterface. - * - * @group entity_reference_revisions - */ -class EntityReferenceRevisionsSaveTest extends KernelTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'node', - 'user', - 'system', - 'field', - 'entity_reference_revisions', - 'entity_composite_relationship_test', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create article content type. - $values = ['type' => 'article', 'name' => 'Article']; - $node_type = NodeType::create($values); - $node_type->save(); - $this->installEntitySchema('user'); - $this->installEntitySchema('node'); - $this->installEntitySchema('entity_test_composite'); - $this->installSchema('system', ['sequences']); - $this->installSchema('node', ['node_access']); - } - - /** - * Test for NeedsSaveInterface implementation. - * - * Tests that the referenced entity is saved when needsSave() is TRUE. - */ - public function testNeedsSave() { - - // Add the entity_reference_revisions field to article. - $field_storage = FieldStorageConfig::create(array( - 'field_name' => 'composite_reference', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'settings' => array( - 'target_type' => 'entity_test_composite' - ), - )); - $field_storage->save(); - $field = FieldConfig::create(array( - 'field_storage' => $field_storage, - 'bundle' => 'article', - )); - $field->save(); - - $text = 'Dummy text'; - // Create the test composite entity. - $entity_test = EntityTestCompositeRelationship::create(array( - 'uuid' => $text, - 'name' => $text, - )); - $entity_test->save(); - - $text = 'Clever text'; - // Set the name to a new text. - /** @var \Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship $entity_test */ - $entity_test->name = $text; - $entity_test->setNeedsSave(TRUE); - // Create a node with a reference to the test entity and save. - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $entity_test, - ]); - // Check the name is properly set. - $values = $node->composite_reference->getValue(); - $this->assertTrue(isset($values[0]['entity'])); - static::assertEquals($values[0]['entity']->name->value, $text); - $node->composite_reference->setValue($values); - static::assertEquals($node->composite_reference->entity->name->value, $text); - $node->save(); - - // Check that the name has been updated when the parent has been saved. - /** @var \Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship $entity_test_after */ - $entity_test_after = EntityTestCompositeRelationship::load($entity_test->id()); - static::assertEquals($entity_test_after->name->value, $text); - - $new_text = 'Dummy text again'; - // Set the name again. - $entity_test->name = $new_text; - $entity_test->setNeedsSave(FALSE); - - // Load the Node and check the composite reference field is not set. - $node = Node::load($node->id()); - $values = $node->composite_reference->getValue(); - $this->assertFalse(isset($values[0]['entity'])); - $node->composite_reference = $entity_test; - $node->save(); - - // Check the name is not updated. - $entity_test_after = EntityTestCompositeRelationship::load($entity_test->id()); - static::assertEquals($entity_test_after->name->value, $text); - - // Test if after delete the referenced entity there are no problems setting - // the referencing values to the parent. - $entity_test->delete(); - $node = Node::load($node->id()); - $node->save(); - - // Test if the needs save variable is set as false after saving. - $entity_needs_save = EntityTestCompositeRelationship::create([ - 'uuid' => $text, - 'name' => $text, - ]); - $entity_needs_save->setNeedsSave(TRUE); - $entity_needs_save->save(); - $this->assertFalse($entity_needs_save->needsSave()); - } - - /** - * Test for NeedsSaveInterface implementation. - * - * Tests that the fields in the parent are properly updated. - */ - public function testSaveNewEntity() { - // Add the entity_reference_revisions field to article. - $field_storage = FieldStorageConfig::create(array( - 'field_name' => 'composite_reference', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'settings' => array( - 'target_type' => 'entity_test_composite' - ), - )); - $field_storage->save(); - $field = FieldConfig::create(array( - 'field_storage' => $field_storage, - 'bundle' => 'article', - )); - $field->save(); - - $text = 'Dummy text'; - // Create the test entity. - $entity_test = EntityTestCompositeRelationship::create(array( - 'uuid' => $text, - 'name' => $text, - )); - - // Create a node with a reference to the test entity and save. - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'composite_reference' => $entity_test, - ]); - $validate = $node->validate(); - $this->assertEmpty($validate); - $node->save(); - - // Test that the fields on node are properly set. - $node_after = Node::load($node->id()); - static::assertEquals($node_after->composite_reference[0]->target_id, $entity_test->id()); - static::assertEquals($node_after->composite_reference[0]->target_revision_id, $entity_test->getRevisionId()); - // Check that the entity is not new after save parent. - $this->assertFalse($entity_test->isNew()); - - // Create a new test entity. - $text = 'Smart text'; - $second_entity_test = EntityTestCompositeRelationship::create(array( - 'uuid' => $text, - 'name' => $text, - )); - $second_entity_test->save(); - - // Set the new test entity to the node field. - $node_after->composite_reference = $second_entity_test; - // Check the fields have been updated. - static::assertEquals($node_after->composite_reference[0]->target_id, $second_entity_test->id()); - static::assertEquals($node_after->composite_reference[0]->target_revision_id, $second_entity_test->getRevisionId()); - } - - /** - * Tests entity_reference_revisions default value and config dependencies. - */ - public function testEntityReferenceRevisionsDefaultValue() { - - // Create a test target node used as entity reference by another test node. - $node_target = Node::create([ - 'title' => 'Target node', - 'type' => 'article', - 'body' => 'Target body text', - 'uuid' => '2d04c2b4-9c3d-4fa6-869e-ecb6fa5c9410', - ]); - $node_target->save(); - - // Create an entity reference field to reference to the test target node. - /** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */ - $field_storage = FieldStorageConfig::create([ - 'field_name' => 'target_node_reference', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'settings' => ['target_type' => 'node'], - ]); - $field_storage->save(); - /** @var \Drupal\field\Entity\FieldConfig $field */ - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'article', - 'required' => FALSE, - 'settings' => ['handler_settings' => ['target_bundles' => ['article' => 'article']]], - ]); - // Add reference values to field config that will be used as default value. - $default_value = [ - [ - 'target_id' => $node_target->id(), - 'target_revision_id' => $node_target->getRevisionId(), - 'target_uuid' => $node_target->uuid(), - ], - ]; - $field->setDefaultValue($default_value)->save(); - - // Resave the target node, so that the default revision is not the one we - // want to use. - $revision_id = $node_target->getRevisionId(); - $node_target_after = Node::load($node_target->id()); - $node_target_after->setNewRevision(); - $node_target_after->save(); - $this->assertTrue($node_target_after->getRevisionId() != $revision_id); - - // Create another node. - $node_host = Node::create([ - 'title' => 'Host node', - 'type' => 'article', - 'body' => 'Host body text', - 'target_node_reference' => $node_target, - ]); - $node_host->save(); - - // Check if the ERR default values are properly created. - $node_host_after = Node::load($node_host->id()); - $this->assertEquals($node_host_after->target_node_reference->target_id, $node_target->id()); - $this->assertEquals($node_host_after->target_node_reference->target_revision_id, $revision_id); - - // Check if the configuration dependencies are properly created. - $dependencies = $field->calculateDependencies()->getDependencies(); - $this->assertEquals($dependencies['content'][0], 'node:article:2d04c2b4-9c3d-4fa6-869e-ecb6fa5c9410'); - $this->assertEquals($dependencies['config'][0], 'field.storage.node.target_node_reference'); - $this->assertEquals($dependencies['config'][1], 'node.type.article'); - $this->assertEquals($dependencies['module'][0], 'entity_reference_revisions'); - } -} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php deleted file mode 100644 index e68cf058c..000000000 --- a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php - -namespace Drupal\Tests\entity_reference_revisions\Kernel\Plugin\Derivative; - -use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions; -use Drupal\KernelTests\KernelTestBase; -use Drupal\migrate\Plugin\MigrationPluginManager; -use Drupal\migrate\Plugin\MigrateDestinationPluginManager; - -/** - * Tests the migration deriver. - * - * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\Derivative\MigrateEntityReferenceRevisions - * @group entity_reference_revisions - */ -class EntityReferenceRevisionsDeriverTest extends KernelTestBase { - - /** - * {@inheritdoc} - */ - public static $modules = ['migrate', 'entity_reference_revisions', 'entity_composite_relationship_test']; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->installConfig($this->modules); - } - - /** - * Tests deriver. - * - * @covers ::getDerivativeDefinitions - */ - public function testDestinationDeriver() { - /** @var MigrateDestinationPluginManager $migrationDestinationManager */ - $migrationDestinationManager = \Drupal::service('plugin.manager.migrate.destination'); - - $destination = $migrationDestinationManager->getDefinition('entity_reference_revisions:entity_test_composite'); - $this->assertEquals(EntityReferenceRevisions::class, $destination['class']); - } - - - -} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php deleted file mode 100644 index 02e02ec58..000000000 --- a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php +++ /dev/null @@ -1,484 +0,0 @@ -<?php - -namespace Drupal\Tests\entity_reference_revisions\Kernel\Plugin\migrate\destination; - -use Drupal\Core\Entity\EntityStorageBase; -use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\KernelTests\KernelTestBase; -use Drupal\migrate\MigrateExecutable; -use Drupal\migrate\MigrateMessageInterface; -use Drupal\migrate\Plugin\Migration; -use Drupal\node\Entity\NodeType; - -/** - * Tests the migration destination plugin. - * - * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions - * @group entity_reference_revisions - */ -class EntityReferenceRevisionsDestinationTest extends KernelTestBase implements MigrateMessageInterface { - - /** - * @var \Drupal\migrate\Plugin\MigrationPluginManager $migrationManager - * - * The migration plugin manager. - */ - protected $migrationPluginManager; - - /** - * {@inheritdoc} - */ - public static $modules = [ - 'migrate', - 'entity_reference_revisions', - 'entity_composite_relationship_test', - 'user', - 'system', - 'text', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->installEntitySchema('entity_test_composite'); - $this->installSchema('system', ['sequences']); - $this->installConfig($this->modules); - - $this->migrationPluginManager = \Drupal::service('plugin.manager.migration'); - } - - /** - * Tests get entity type id. - * - * @dataProvider getEntityTypeIdDataProvider - * - * @covers ::getEntityTypeId - */ - public function testGetEntityTypeId(array $definition, $expected) { - /** @var Migration $migration */ - $migration = $this->migrationPluginManager->createStubMigration($definition); - /** @var EntityReferenceRevisions $destination */ - $destination = $migration->getDestinationPlugin(); - - /** @var EntityStorageBase $storage */ - $storage = $this->readAttribute($destination, 'storage'); - $actual = $this->readAttribute($storage, 'entityTypeId'); - - $this->assertEquals($expected, $actual); - } - - /** - * Provides multiple migration definitions for "getEntityTypeId" test. - */ - public function getEntityTypeIdDataProvider() { - $datas = $this->getEntityDataProvider(); - - foreach ($datas as &$data) { - $data['expected'] = 'entity_test_composite'; - } - - return $datas; - } - - /** - * Tests get entity. - * - * @dataProvider getEntityDataProvider - * - * @covers ::getEntity - * @covers ::rollback - * @covers ::rollbackNonTranslation - */ - public function testGetEntity(array $definition, array $expected) { - /** @var Migration $migration */ - $migration = $this->migrationPluginManager->createStubMigration($definition); - $migrationExecutable = (new MigrateExecutable($migration, $this)); - /** @var EntityStorageBase $storage */ - $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); - // Test inserting and updating by looping twice. - for ($i = 0; $i < 2; $i++) { - $migrationExecutable->import(); - $migration->getIdMap()->prepareUpdate(); - foreach ($expected as $data) { - $entity = $storage->loadRevision($data['id']); - $this->assertEquals($data['label'], $entity->label()); - } - } - $migrationExecutable->rollback(); - foreach ($expected as $data) { - $entity = $storage->loadRevision($data['id']); - $this->assertEmpty($entity); - } - } - - /** - * Provides multiple migration definitions for "getEntity" test. - */ - public function getEntityDataProvider() { - return [ - 'without keys' => [ - 'definition' => [ - 'source' => [ - 'plugin' => 'embedded_data', - 'data_rows' => [ - ['id' => 1, 'name' => 'content item 1a'], - ['id' => 1, 'name' => 'content item 1b'], - ['id' => 2, 'name' => 'content item 2'], - ], - 'ids' => [ - 'id' => ['type' => 'integer'], - 'name' => ['type' => 'text'], - ], - ], - 'process' => [ - 'name' => 'name', - ], - 'destination' => [ - 'plugin' => 'entity_reference_revisions:entity_test_composite', - ], - ], - 'expected' => [ - ['id' => 1, 'label' => 'content item 1a'], - ['id' => 2, 'label' => 'content item 1b'], - ['id' => 3, 'label' => 'content item 2'], - ], - ], - 'with keys' => [ - 'definition' => [ - 'source' => [ - 'plugin' => 'embedded_data', - 'data_rows' => [ - ['id' => 1, 'revision_id' => 1, 'name' => 'content item 1'], - ['id' => 2, 'revision_id' => 2, 'name' => 'content item 2'], - ['id' => 3, 'revision_id' => 3, 'name' => 'content item 3'], - ], - 'ids' => [ - 'id' => ['type' => 'integer'], - 'name' => ['type' => 'text'], - ], - ], - 'process' => [ - 'id' => 'id', - 'revision_id' => 'revision_id', - 'name' => 'name', - ], - 'destination' => [ - 'plugin' => 'entity_reference_revisions:entity_test_composite', - ], - ], - 'expected' => [ - ['id' => 1, 'label' => 'content item 1'], - ['id' => 2, 'label' => 'content item 2'], - ['id' => 3, 'label' => 'content item 3'], - ], - ], - ]; - } - - /** - * Tests multi-value and single-value destination field linkage. - * - * @dataProvider destinationFieldMappingDataProvider - * - */ - public function testDestinationFieldMapping(array $datas) { - $this->enableModules(['node', 'field']); - $this->installEntitySchema('node'); - $this->installEntitySchema('user'); - $this->installSchema('node', ['node_access']); - - // Create new content type. - $values = ['type' => 'article', 'name' => 'Article']; - $node_type = NodeType::create($values); - $node_type->save(); - - // Add the field_err_single field to the node type. - $field_storage = FieldStorageConfig::create([ - 'field_name' => 'field_err_single', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'settings' => [ - 'target_type' => 'entity_test_composite' - ], - 'cardinality' => 1, - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'article', - ]); - $field->save(); - - // Add the field_err_multiple field to the node type. - $field_storage = FieldStorageConfig::create([ - 'field_name' => 'field_err_multiple', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'settings' => [ - 'target_type' => 'entity_test_composite' - ], - 'cardinality' => -1, - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'article', - ]); - $field->save(); - - $definitions = []; - $instances = []; - foreach ($datas as $data) { - $definitions[$data['definition']['id']] = $data['definition']; - $instances[$data['definition']['id']] = $this->migrationPluginManager->createStubMigration($data['definition']); - } - - // Reflection is easier than mocking. We need to use createInstance for - // purposes of registering the migration for the migration process plugin. - $reflector = new \ReflectionObject($this->migrationPluginManager); - $property = $reflector->getProperty('definitions'); - $property->setAccessible(TRUE); - $property->setValue($this->migrationPluginManager, $definitions); - $this->container->set('plugin.manager.migration', $this->migrationPluginManager); - - foreach ($datas as $data) { - $migration = $this->migrationPluginManager->createInstance($data['definition']['id']); - $migrationExecutable = (new MigrateExecutable($migration, $this)); - /** @var EntityStorageBase $storage */ - $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); - $migrationExecutable->import(); - foreach ($data['expected'] as $expected) { - $entity = $storage->loadRevision($expected['id']); - $properties = array_diff_key($expected, array_flip(['id'])); - foreach ($properties as $property => $value) { - if (is_array($value)) { - foreach ($value as $delta => $text) { - $this->assertNotEmpty($entity->{$property}[$delta]->entity, "Entity property $property with $delta is empty"); - $this->assertEquals($text, $entity->{$property}[$delta]->entity->label()); - } - } - else { - $this->assertNotEmpty($entity, 'Entity with label ' . $expected[$property] .' is empty'); - $this->assertEquals($expected[$property], $entity->label()); - } - } - } - } - } - - /** - * Provides multiple migration definitions for "getEntity" test. - */ - public function destinationFieldMappingDataProvider() { - return [ - 'scenario 1' => [ - [ - 'single err' => [ - 'definition' => [ - 'id' => 'single_err', - 'class' => Migration::class, - 'source' => [ - 'plugin' => 'embedded_data', - 'data_rows' => [ - [ - 'id' => 1, - 'photo' => 'Photo1 here', - ], - [ - 'id' => 2, - 'photo' => 'Photo2 here', - ], - ], - 'ids' => [ - 'id' => ['type' => 'integer'], - ], - ], - 'process' => [ - 'name' => 'photo', - ], - 'destination' => [ - 'plugin' => 'entity_reference_revisions:entity_test_composite', - ], - ], - 'expected' => [ - ['id' => 1, 'name' => 'Photo1 here'], - ['id' => 2, 'name' => 'Photo2 here'], - ], - ], - 'multiple err author1' => [ - 'definition' => [ - 'id' => 'multiple_err_author1', - 'class' => Migration::class, - 'source' => [ - 'plugin' => 'embedded_data', - 'data_rows' => [ - [ - 'id' => 1, - 'author' => 'Author 1', - ], - [ - 'id' => 2, - 'author' => 'Author 2', - ], - ], - 'ids' => [ - 'author' => ['type' => 'text'], - ], - ], - 'process' => [ - 'name' => 'author', - ], - 'destination' => [ - 'plugin' => 'entity_reference_revisions:entity_test_composite', - ], - ], - 'expected' => [ - ['id' => 3, 'name' => 'Author 1'], - ['id' => 4, 'name' => 'Author 2'], - ], - ], - 'multiple err author 2' => [ - 'definition' => [ - 'id' => 'multiple_err_author2', - 'class' => Migration::class, - 'source' => [ - 'plugin' => 'embedded_data', - 'data_rows' => [ - [ - 'id' => 1, - 'author' => 'Author 3', - ], - [ - 'id' => 2, - 'author' => 'Author 4', - ], - ], - 'ids' => [ - 'author' => ['type' => 'text'], - ], - ], - 'process' => [ - 'name' => 'author', - ], - 'destination' => [ - 'plugin' => 'entity_reference_revisions:entity_test_composite', - ], - ], - 'expected' => [ - ['id' => 5, 'name' => 'Author 3'], - ['id' => 6, 'name' => 'Author 4'], - ], - ], - 'destination entity' => [ - 'definition' => [ - 'id' => 'node_migration', - 'class' => Migration::class, - 'source' => [ - 'plugin' => 'embedded_data', - 'data_rows' => [ - [ - 'id' => 1, - 'title' => 'Article 1', - 'photo' => 'Photo1 here', - 'author' => ['Author 1', 'Author 3'], - ], - [ - 'id' => 2, - 'title' => 'Article 2', - 'photo' => 'Photo2 here', - 'author' => ['Author 2', 'Author 4'], - ], - ], - 'ids' => [ - 'id' => ['type' => 'integer'], - ], - ], - 'process' => [ - 'title' => 'title', - 'type' => [ - 'plugin' => 'default_value', - 'default_value' => 'article', - ], - 'field_err_single/target_id' => [ - [ - 'plugin' => 'migration', - 'migration' => ['single_err'], - 'no_stub' => TRUE, - 'source' => 'id', - ], - [ - 'plugin' => 'extract', - 'index' => [ - '0', - ], - ], - ], - 'field_err_single/target_revision_id' => [ - [ - 'plugin' => 'migration', - 'migration' => ['single_err'], - 'no_stub' => TRUE, - 'source' => 'id', - ], - [ - 'plugin' => 'extract', - 'index' => [ - 1, - ], - ], - ], - 'field_err_multiple' => [ - [ - 'plugin' => 'migration', - 'migration' => [ - 'multiple_err_author1', - 'multiple_err_author2', - ], - 'no_stub' => TRUE, - 'source' => 'author', - ], - [ - 'plugin' => 'iterator', - 'process' => [ - 'target_id' => '0', - 'target_revision_id' => '1', - ], - ], - ], - ], - 'destination' => [ - 'plugin' => 'entity:node', - ], - ], - 'expected' => [ - [ - 'id' => 1, - 'title' => 'Article 1', - 'field_err_single' => ['Photo1 here'], - 'field_err_multiple' => ['Author 1', 'Author 3'], - ], - [ - 'id' => 2, - 'title' => 'Article 2', - 'field_err_single' => ['Photo2 here'], - 'field_err_multiple' => ['Author 2', 'Author 4'], - ], - ], - ], - ], - ], - ]; - } - - /** - * {@inheritdoc} - */ - public function display($message, $type = 'status') { - $this->assertTrue($type == 'status', $message); - } - -} diff --git a/web/modules/contrib/entityqueue/composer.json b/web/modules/contrib/entityqueue/composer.json new file mode 100644 index 000000000..f51e48ad8 --- /dev/null +++ b/web/modules/contrib/entityqueue/composer.json @@ -0,0 +1,26 @@ +{ + "name": "drupal/entityqueue", + "description": "The Entityqueue module allows users to create queues of any entity type.", + "type": "drupal-module", + "license": "GPL-2.0+", + "homepage": "https://www.drupal.org/project/entityqueue", + "minimum-stability": "dev", + "authors": [ + { + "name": "Andrei Mateescu", + "homepage": "https://www.drupal.org/u/amateescu", + "role": "Maintainer" + }, + { + "name": "Jonathan Jordan", + "homepage": "https://www.drupal.org/u/jojonaloha", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/entityqueue", + "irc": "irc://irc.freenode.org/drupal-contribute", + "source": "http://cgit.drupalcode.org/entityqueue" + }, + "require": { } +} diff --git a/web/modules/contrib/entityqueue/config/schema/entityqueue.schema.yml b/web/modules/contrib/entityqueue/config/schema/entityqueue.schema.yml index fd21d8560..7024d9c81 100644 --- a/web/modules/contrib/entityqueue/config/schema/entityqueue.schema.yml +++ b/web/modules/contrib/entityqueue/config/schema/entityqueue.schema.yml @@ -11,9 +11,8 @@ entityqueue.entity_queue.*: handler: type: string label: 'Queue handler' -#TODO: Provide a schema for queue handler plugins. handler_configuration: - type: ignore + type: entityqueue_handler_configuration.[%parent.handler] label: 'Queue handler configuration' entity_settings: type: mapping @@ -44,3 +43,20 @@ entityqueue.entity_queue.*: reverse_in_admin: type: boolean label: 'Reverse order in admin view' + +# Base schema for all entity queue handler schemas. +entityqueue_handler_configuration: + type: mapping + label: 'Entity queue handler configuration' + +# Schema for all entity queue handlers that are not providing a specific schema. +entityqueue_handler_configuration.*: + type: entityqueue_handler_configuration + +views.relationship.entity_queue: + type: views_relationship + label: 'Entity queue' + mapping: + limit_queue: + type: string + label: 'Limit to a specific entity queue' diff --git a/web/modules/contrib/entityqueue/entityqueue.info.yml b/web/modules/contrib/entityqueue/entityqueue.info.yml index bc428d9c6..9955a589a 100644 --- a/web/modules/contrib/entityqueue/entityqueue.info.yml +++ b/web/modules/contrib/entityqueue/entityqueue.info.yml @@ -5,8 +5,8 @@ package: Entityqueue # core: 8.x configure: entity.entity_queue.collection -# Information added by Drupal.org packaging script on 2017-10-01 -version: '8.x-1.0-alpha7' +# Information added by Drupal.org packaging script on 2018-09-05 +version: '8.x-1.0-alpha8' core: '8.x' project: 'entityqueue' -datestamp: 1506839349 +datestamp: 1536140287 diff --git a/web/modules/contrib/entityqueue/entityqueue.links.task.yml b/web/modules/contrib/entityqueue/entityqueue.links.task.yml new file mode 100644 index 000000000..598915770 --- /dev/null +++ b/web/modules/contrib/entityqueue/entityqueue.links.task.yml @@ -0,0 +1,3 @@ +entityqueue.entities: + class: \Drupal\Core\Menu\LocalTaskDefault + deriver: \Drupal\entityqueue\Plugin\Derivative\EntityqueueLocalTask diff --git a/web/modules/contrib/entityqueue/entityqueue.module b/web/modules/contrib/entityqueue/entityqueue.module index 117a264fb..9a76f70cb 100644 --- a/web/modules/contrib/entityqueue/entityqueue.module +++ b/web/modules/contrib/entityqueue/entityqueue.module @@ -39,11 +39,6 @@ function entityqueue_views_pre_render(ViewExecutable $view) { return; } - // Proceed only if there is entityqueue sort criteria available. - if (!$sort_key = entityqueue_get_entityqueue_sort($view)) { - return; - } - // Allow to disable the contextual links. if (!$view->display_handler->getOption('show_admin_links')) { return; @@ -76,37 +71,16 @@ function entityqueue_views_pre_render(ViewExecutable $view) { * Change Entityqueue on views into offcanvas links if available. */ function entityqueue_contextual_links_view_alter(&$element, $items) { - if (\Drupal::moduleHandler()->moduleExists('outside_in') && isset($element['#links']['entityentity-subqueueedit-form'])) { + if (\Drupal::moduleHandler()->moduleExists('settings_tray') && isset($element['#links']['entityentity-subqueueedit-form'])) { $element['#links']['entityentity-subqueueedit-form']['attributes'] = [ 'class' => ['use-ajax'], 'data-dialog-type' => 'dialog', - 'data-dialog-renderer' => 'offcanvas', - 'data-outside-in-edit' => TRUE, + 'data-dialog-renderer' => 'off_canvas', + 'data-settings-tray-edit' => TRUE, ]; - - $element['#attached']['library'][] = 'outside_in/drupal.off_canvas'; } } -/** - * Get the entityqueue position sort of a view if there is one and return its - * ID. If there are multiple of these sorts the first is returned. - * - * @param $view - * The view object. - * - * @return - * The ID of the sort or FALSE if there isn't one. - */ -function entityqueue_get_entityqueue_sort($view) { - foreach ($view->sort as $id => $sort) { - if ($sort->definition['id'] == 'entity_queue_position') { - return $id; - } - } - return FALSE; -} - /** * Implements hook_entity_delete(). * @@ -116,23 +90,20 @@ function entityqueue_entity_delete(EntityInterface $entity) { // Get all the entity queues referencing the targets entity type. $queues = EntityQueue::loadMultipleByTargetType($entity->getEntityTypeId()); foreach ($queues as $queue) { - $entity_settings = $queue->getEntitySettings(); - - // Check if the queue's bundle also matches that of the target entity. - if (is_array($entity_settings['handler_settings']['target_bundles']) && in_array($entity->bundle(), $entity_settings['handler_settings']['target_bundles'])) { - // Get subqueues. - $query = \Drupal::entityQuery('entity_subqueue')->condition('queue', $queue->id()); - $result = $query->execute(); - $subqueues = EntitySubqueue::loadMultiple($result); + // Get all the subqueues which are referencing the deleted entity. + $query = \Drupal::entityQuery('entity_subqueue') + ->condition('queue', $queue->id()) + ->condition('items', $entity->id()); + $result = $query->execute(); + $subqueues = EntitySubqueue::loadMultiple($result); - // Check if the entity is referenced in a subqueue. - foreach ($subqueues as $subqueue) { - $items = $subqueue->get('items')->getValue(); - if (($item_key = array_search($entity->id(), array_column($items, 'target_id'))) !== FALSE) { - unset($items[$item_key]); - $subqueue->set('items', $items); - $subqueue->save(); - } + // Check if the entity is referenced in a subqueue. + foreach ($subqueues as $subqueue) { + $items = $subqueue->get('items')->getValue(); + if (($item_key = array_search($entity->id(), array_column($items, 'target_id'))) !== FALSE) { + unset($items[$item_key]); + $subqueue->set('items', $items); + $subqueue->save(); } } } diff --git a/web/modules/contrib/entityqueue/entityqueue.routing.yml b/web/modules/contrib/entityqueue/entityqueue.routing.yml index 91c445940..8d23d8434 100644 --- a/web/modules/contrib/entityqueue/entityqueue.routing.yml +++ b/web/modules/contrib/entityqueue/entityqueue.routing.yml @@ -20,7 +20,7 @@ entity.entity_queue.edit_form: _entity_form: 'entity_queue.edit' _title: 'Edit Entity Queue' requirements: - _permission: 'administer entityqueue+manipulate entityqueues+manipulate all entityqueues' + _permission: 'administer entityqueue' entity.entity_queue.delete_form: path: '/admin/structure/entityqueue/{entity_queue}/delete' @@ -28,7 +28,7 @@ entity.entity_queue.delete_form: _entity_form: 'entity_queue.delete' _title: 'Delete Entity Queue' requirements: - _permission: 'administer entityqueue+manipulate entityqueues+manipulate all entityqueues' + _permission: 'administer entityqueue' entity.entity_queue.enable: path: '/admin/structure/entityqueue/{entity_queue}/enable' @@ -63,3 +63,21 @@ entity.entity_subqueue.add_form: _title: 'Add subqueue' requirements: _entity_create_access: 'entity_subqueue:{entity_queue}' + +entity.entity_subqueue.add_item: + path: '/admin/structure/entityqueue/{entity_queue}/{entity_subqueue}/{entity}/add-item' + defaults: + _controller: '\Drupal\entityqueue\Controller\EntityQueueUIController::subqueueAjaxOperation' + op: addItem + requirements: + _permission: 'administer entityqueue+manipulate entityqueues+manipulate all entityqueues' + _csrf_token: 'TRUE' + +entity.entity_subqueue.remove_item: + path: '/admin/structure/entityqueue/{entity_queue}/{entity_subqueue}/{entity}/remove-item' + defaults: + _controller: '\Drupal\entityqueue\Controller\EntityQueueUIController::subqueueAjaxOperation' + op: removeItem + requirements: + _permission: 'administer entityqueue+manipulate entityqueues+manipulate all entityqueues' + _csrf_token: 'TRUE' diff --git a/web/modules/contrib/entityqueue/entityqueue.services.yml b/web/modules/contrib/entityqueue/entityqueue.services.yml index 4d8893593..e314177b0 100644 --- a/web/modules/contrib/entityqueue/entityqueue.services.yml +++ b/web/modules/contrib/entityqueue/entityqueue.services.yml @@ -2,3 +2,8 @@ services: plugin.manager.entityqueue.handler: class: Drupal\entityqueue\EntityQueueHandlerManager arguments: ['@container.namespaces', '@cache.default', '@module_handler'] + entityqueue.route_subscriber: + class: Drupal\entityqueue\Routing\RouteSubscriber + arguments: ['@entity_type.manager'] + tags: + - { name: event_subscriber } diff --git a/web/modules/contrib/entityqueue/entityqueue.views.inc b/web/modules/contrib/entityqueue/entityqueue.views.inc index aa2026ca6..a4f63efe6 100644 --- a/web/modules/contrib/entityqueue/entityqueue.views.inc +++ b/web/modules/contrib/entityqueue/entityqueue.views.inc @@ -57,38 +57,55 @@ function entityqueue_views_data_alter(array &$data) { 'field field' => $columns['target_id'], ]; - $data[$target_base_table]['entityqueue_relationship']['sort'] = array( + $data[$target_base_table]['entityqueue_relationship_in_queue']['sort'] = [ + 'id' => 'entity_queue_in_queue', + 'group' => t('Entityqueue'), + 'title' => t('In @target_label queue', [ + '@target_label' => $entity_type->getLabel(), + ]), + 'label' => t('In @target_label queue', [ + '@target_label' => $entity_type->getLabel(), + ]), + 'help' => t('Filter to ensure a(n) @target_label IS or IS NOT in the related queue', [ + '@target_label' => $entity_type->getLabel(), + ]), + 'field' => 'delta', + 'field table' => $subqueue_items_table_name, + 'field_name' => $field_name, + ]; + + $data[$target_base_table]['entityqueue_relationship']['sort'] = [ 'id' => 'entity_queue_position', 'group' => t('Entityqueue'), - 'title' => t('@target_label Queue Position', array( + 'title' => t('@target_label Queue Position', [ '@target_label' => $entity_type->getLabel(), - )), - 'label' => t('@target_label Queue Position', array( + ]), + 'label' => t('@target_label Queue Position', [ '@target_label' => $entity_type->getLabel(), - )), - 'help' => t('Position of item in the @target_label queue.', array( + ]), + 'help' => t('Position of item in the @target_label queue.', [ '@target_label' => $entity_type->getLabel(), - )), + ]), 'field' => 'delta', 'field table' => $subqueue_items_table_name, 'field_name' => $field_name, - ); + ]; - $data[$target_base_table]['entityqueue_relationship']['filter'] = array( + $data[$target_base_table]['entityqueue_relationship']['filter'] = [ 'id' => 'entity_queue_in_queue', 'type' => 'yes-no', 'group' => t('Entityqueue'), - 'title' => t('@target_label In Queue', array( + 'title' => t('@target_label In Queue', [ '@target_label' => $entity_type->getLabel(), - )), - 'label' => t('@target_label In Queue', array( + ]), + 'label' => t('@target_label In Queue', [ '@target_label' => $entity_type->getLabel(), - )), - 'help' => t('Filter for entities that are available or not in the @target_label entity queue.', array( + ]), + 'help' => t('Filter for entities that are available or not in the @target_label entity queue.', [ '@target_label' => $entity_type->getLabel(), - )), + ]), 'field table' => $subqueue_items_table_name, 'field field' => $columns['target_id'], - ); + ]; } } diff --git a/web/modules/contrib/entityqueue/plugins/entityreference/selection/EntityReference_SelectionHandler_EntityQueue.class.php b/web/modules/contrib/entityqueue/plugins/entityreference/selection/EntityReference_SelectionHandler_EntityQueue.class.php deleted file mode 100644 index 026976216..000000000 --- a/web/modules/contrib/entityqueue/plugins/entityreference/selection/EntityReference_SelectionHandler_EntityQueue.class.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php - -/** - * @file - * Definition of EntityReference_SelectionHandler_EntityQueue. - */ - -/** - * Defines a Entityreference selection handler for Entityqueue. - */ -class EntityReference_SelectionHandler_EntityQueue extends EntityReference_SelectionHandler_Generic { - - /** - * Overrides EntityReference_SelectionHandler_Generic::getInstance(). - */ - public static function getInstance($field, $instance = NULL, $entity_type = NULL, $entity = NULL) { - return new EntityReference_SelectionHandler_EntityQueue($field, $instance, $entity_type, $entity); - } - - /** - * Constructs the EntityQueue selection handler. - */ - protected function __construct($field, $instance = NULL, $entity_type = NULL, $entity = NULL) { - parent::__construct($field, $instance, $entity_type, $entity); - - $queue_name = NULL; - if (!empty($entity->queue)) { - $queue_name = $entity->queue; - } - elseif (!empty($instance['bundle'])) { - $queue_name = $instance['bundle']; - } - if (!empty($queue_name)) { - $this->queue = entityqueue_queue_load($queue_name); - } - - // Override the entityreference settings with our own. - $this->field['settings']['handler_settings']['target_bundles'] = NULL; - } - - /** - * Overrides EntityReference_SelectionHandler_Generic::buildEntityFieldQuery(). - */ - public function buildEntityFieldQuery($match = NULL, $match_operator = 'CONTAINS') { - $handler = EntityReference_SelectionHandler_Generic::getInstance($this->field, $this->instance, $this->entity_type, $this->entity); - $query = $handler->buildEntityFieldQuery($match, $match_operator); - - if (!empty($this->queue->settings['target_bundles'])) { - $query->entityCondition('bundle', $this->queue->settings['target_bundles'], 'IN'); - } - - return $query; - } - - /** - * Implements EntityReferenceHandler::validateReferencableEntities(). - */ - public function validateReferencableEntities(array $ids) { - $referencable = parent::validateReferencableEntities($ids); - // Allow users to save the queue even if they don't have access to an - // existing entity in the queue. See https://www.drupal.org/node/2383903 - $existing = $this->getCurrentlyReferencedEntityIds(); - - return array_unique(array_merge($referencable, $existing)); - } - - /** - * Gets ids of existing entities in the queue. - * - * @return array - * Entity ids that are currently referenced by the entity. - */ - public function getCurrentlyReferencedEntityIds() { - $ret = array(); - if (isset($this->entity) && isset($this->field)) { - $entity_type = $this->entity_type; - $field_name = $this->field['field_name']; - $wrapper = entity_metadata_wrapper($entity_type, $this->entity); - $ret = $wrapper->{$field_name}->raw(); - } - - return $ret; - } - -} diff --git a/web/modules/contrib/entityqueue/plugins/entityreference/selection/entityqueue.inc b/web/modules/contrib/entityqueue/plugins/entityreference/selection/entityqueue.inc deleted file mode 100644 index c43b6e114..000000000 --- a/web/modules/contrib/entityqueue/plugins/entityreference/selection/entityqueue.inc +++ /dev/null @@ -1,13 +0,0 @@ -<?php - -/** - * @file - * Ctools plugin that provides a special Entity reference selection for - * Entityqueue. - */ - -$plugin = array( - 'title' => t('Entityqueue'), - 'class' => 'EntityReference_SelectionHandler_EntityQueue', - 'weight' => 0, -); diff --git a/web/modules/contrib/entityqueue/src/Controller/EntityQueueUIController.php b/web/modules/contrib/entityqueue/src/Controller/EntityQueueUIController.php index 58be72fd2..b32630611 100644 --- a/web/modules/contrib/entityqueue/src/Controller/EntityQueueUIController.php +++ b/web/modules/contrib/entityqueue/src/Controller/EntityQueueUIController.php @@ -3,13 +3,19 @@ namespace Drupal\entityqueue\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Routing\RouteMatch; use Drupal\entityqueue\EntityQueueInterface; use Symfony\Component\HttpFoundation\Request; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\entityqueue\EntitySubqueueInterface; +use Drupal\Core\Url; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Access\AccessResult; /** - * Returns responses for Views UI routes. + * Returns responses for Entityqueue UI routes. */ class EntityQueueUIController extends ControllerBase { @@ -29,6 +35,87 @@ class EntityQueueUIController extends ControllerBase { return $list_builder->render(); } + /** + * Provides a list of subqueues where an entity can be added. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The route match. + * @param string $entity_type_id + * (optional) The entity type ID. + * @param \Drupal\Core\Entity\EntityInterface $entity + * (optional) An entity object. + * + * @return array + * Array of page elements to render. + */ + public function subqueueListForEntity(RouteMatchInterface $route_match, $entity_type_id = NULL, EntityInterface $entity = NULL) { + if (!$entity) { + $entity = $route_match->getParameter($entity_type_id); + } + + $queues = $this->getAvailableQueuesForEntity($entity); + $subqueues = $this->entityTypeManager()->getStorage('entity_subqueue')->loadByProperties(['queue' => array_keys($queues)]); + $list_builder = $this->entityTypeManager()->getListBuilder('entity_subqueue'); + + $build['#title'] = $this->t('Entityqueues for %title', ['%title' => $entity->label()]); + $build['#type'] = 'container'; + $build['#attributes']['id'] = 'entity-subqueue-list'; + $build['#attached']['library'][] = 'core/drupal.ajax'; + $build['table'] = [ + '#type' => 'table', + '#header' => $list_builder->buildHeader(), + '#rows' => [], + '#cache' => [], + '#empty' => $this->t('There are no queues available.'), + ]; + + /** @var \Drupal\entityqueue\EntitySubqueueInterface $subqueue */ + foreach ($subqueues as $subqueue_id => $subqueue) { + $row = $list_builder->buildRow($subqueue); + + // Check if entity is in queue + $subqueue_items = $subqueue->get('items')->getValue(); + if (in_array($entity->id(), array_column($subqueue_items, 'target_id'), TRUE)) { + $row['operations']['data']['#links'] = [ + 'remove-item' => [ + 'title' => $this->t('Remove from queue'), + 'url' => Url::fromRoute('entity.entity_subqueue.remove_item', ['entity_queue' => $queues[$subqueue->bundle()]->id(), 'entity_subqueue' => $subqueue_id, 'entity' => $entity->id()]), + 'attributes' => [ + 'class' => ['use-ajax'], + ], + ], + ]; + } + else { + $row['operations']['data']['#links'] = [ + 'add-item' => [ + 'title' => $this->t('Add to queue'), + 'url' => Url::fromRoute('entity.entity_subqueue.add_item', ['entity_queue' => $queues[$subqueue->bundle()]->id(), 'entity_subqueue' => $subqueue_id, 'entity' => $entity->id()]), + 'attributes' => [ + 'class' => ['use-ajax'], + ], + ], + ]; + } + + // Add an operation for editing the subqueue items. + // First, compute the destination to send the user back to the + // entityqueue tab they're currently on. We can't rely on <current> + // since if any of the AJAX links are used and the page is rebuilt, + // <current> will point to the most recent AJAX callback, not the + // original entityqueue tab. + $destination = Url::fromRoute("entity.$entity_type_id.entityqueue", [$entity_type_id => $entity->id()])->toString(); + $row['operations']['data']['#links']['edit-subqueue-items'] = [ + 'title' => $this->t('Edit subqueue items'), + 'url' => $subqueue->toUrl('edit-form', ['query' => ['destination' => $destination]]), + ]; + + $build['table']['#rows'][$subqueue->id()] = $row; + } + + return $build; + } + /** * Returns a form to add a new subqeue. * @@ -73,4 +160,88 @@ class EntityQueueUIController extends ControllerBase { return $this->redirect('entity.entity_queue.collection'); } + /** + * Calls a method on an entity subqueue page and reloads the page. + * + * @param \Drupal\entityqueue\EntitySubqueueInterface $entity_subqueue + * The subqueue being acted upon. + * @param string $op + * The operation to perform, e.g., 'addItem' or 'removeItem'. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @return \Drupal\Core\Ajax\AjaxResponse|\Symfony\Component\HttpFoundation\RedirectResponse + * Either returns a rebuilt listing page as an AJAX response, or redirects + * back to the current page. + */ + public function subqueueAjaxOperation(EntitySubqueueInterface $entity_subqueue, $op, Request $request) { + $entity_id = $request->get('entity'); + $entity = $this->entityTypeManager()->getStorage($entity_subqueue->getQueue()->getTargetEntityTypeId())->load($entity_id); + + // Perform the operation. + $entity_subqueue->$op($entity)->save(); + + // If the request is via AJAX, return the rendered list as JSON. + if ($request->request->get('js')) { + $route_match = RouteMatch::createFromRequest($request); + $list = $this->subqueueListForEntity($route_match, $entity->getEntityTypeId(), $entity); + $response = new AjaxResponse(); + $response->addCommand(new ReplaceCommand('#entity-subqueue-list', $list)); + return $response; + } + + // Otherwise, redirect back to the page. + return $this->redirect('<current>'); + } + + /** + * Checks access for a specific request. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The route match. + * @param string $entity_type_id + * (optional) The entity type ID. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function access(RouteMatchInterface $route_match, $entity_type_id = NULL) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $route_match->getParameter($entity_type_id); + + if ($this->getAvailableQueuesForEntity($entity)) { + return AccessResult::allowed(); + } + + return AccessResult::forbidden(); + } + + /** + * Gets a list of queues which can hold this entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * An entity object. + * + * @return \Drupal\entityqueue\EntityQueueInterface[] + * An array of entity queues which can hold this entity. + */ + protected function getAvailableQueuesForEntity(EntityInterface $entity) { + $storage = $this->entityTypeManager()->getStorage('entity_queue'); + + $queue_ids = $storage->getQuery() + ->condition('entity_settings.target_type', $entity->getEntityTypeId(), '=') + ->condition('status', TRUE) + ->execute(); + + $queues = $storage->loadMultiple($queue_ids); + $queues = array_filter($queues, function ($queue) use ($entity) { + /** @var \Drupal\entityqueue\EntityQueueInterface $queue */ + $queue_settings = $queue->getEntitySettings(); + $target_bundles = &$queue_settings['handler_settings']['target_bundles']; + return ($target_bundles === NULL || in_array($entity->bundle(), $target_bundles, TRUE)); + }); + + return $queues; + } + } diff --git a/web/modules/contrib/entityqueue/src/Entity/EntityQueue.php b/web/modules/contrib/entityqueue/src/Entity/EntityQueue.php index 706bd14c0..8455bac7d 100644 --- a/web/modules/contrib/entityqueue/src/Entity/EntityQueue.php +++ b/web/modules/contrib/entityqueue/src/Entity/EntityQueue.php @@ -22,7 +22,8 @@ use Drupal\entityqueue\EntityQueueInterface; * "add" = "Drupal\entityqueue\Form\EntityQueueForm", * "edit" = "Drupal\entityqueue\Form\EntityQueueForm", * "delete" = "Drupal\Core\Entity\EntityDeleteForm" - * } + * }, + * "access" = "Drupal\entityqueue\EntityQueueAccessControlHandler", * }, * admin_permission = "administer entityqueue", * config_prefix = "entity_queue", @@ -174,9 +175,16 @@ class EntityQueue extends ConfigEntityBundleBase implements EntityQueueInterface /** * {@inheritdoc} */ - public function setHandler($handler) { - $this->handler = $handler; - $this->getPluginCollection()->addInstanceID($handler, []); + public function getHandlerConfiguration() { + return $this->handler_configuration; + } + + /** + * {@inheritdoc} + */ + public function setHandler($handler_id) { + $this->handler = $handler_id; + $this->getPluginCollection()->addInstanceID($handler_id, []); return $this; } @@ -188,6 +196,15 @@ class EntityQueue extends ConfigEntityBundleBase implements EntityQueueInterface return $this->getPluginCollection()->get($this->handler); } + /** + * {@inheritdoc} + */ + public function setHandlerPlugin($handler) { + $this->getPluginCollection()->set($handler->getPluginId(), $handler); + + return $this; + } + /** * {@inheritdoc} */ diff --git a/web/modules/contrib/entityqueue/src/Entity/EntitySubqueue.php b/web/modules/contrib/entityqueue/src/Entity/EntitySubqueue.php index 349ab962e..ba7b5fdd9 100644 --- a/web/modules/contrib/entityqueue/src/Entity/EntitySubqueue.php +++ b/web/modules/contrib/entityqueue/src/Entity/EntitySubqueue.php @@ -5,6 +5,7 @@ namespace Drupal\entityqueue\Entity; use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -168,15 +169,16 @@ class EntitySubqueue extends ContentEntityBase implements EntitySubqueueInterfac ->setLabel(t('Title')) ->setRequired(TRUE) ->setSetting('max_length', 191) - ->setDisplayOptions('view', array( + ->setDisplayOptions('view', [ 'label' => 'hidden', 'type' => 'string', 'weight' => -10, - )) - ->setDisplayOptions('form', array( + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayOptions('form', [ 'type' => 'string_textfield', 'weight' => -10, - )) + ]) ->setDisplayConfigurable('form', TRUE); $fields['items'] = BaseFieldDefinition::create('entity_reference') @@ -187,33 +189,33 @@ class EntitySubqueue extends ContentEntityBase implements EntitySubqueueInterfac // entity type that uses strings IDs, in order to allow both integers and // strings to be stored by the default entity reference field storage. ->setSetting('target_type', 'entity_subqueue') - ->setDisplayOptions('view', array( + ->setDisplayOptions('view', [ 'label' => 'hidden', 'type' => 'entity_reference_label', 'weight' => 0, - )) - ->setDisplayOptions('form', array( + ]) + ->setDisplayOptions('form', [ 'type' => 'entity_reference_autocomplete', 'weight' => 5, - 'settings' => array( + 'settings' => [ 'match_operator' => 'CONTAINS', 'size' => '60', 'placeholder' => '', - ), - )) + ], + ]) ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE); $fields['langcode'] = BaseFieldDefinition::create('language') ->setLabel(t('Language')) ->setDescription(t('The subqueue language code.')) - ->setDisplayOptions('view', array( + ->setDisplayOptions('view', [ 'type' => 'hidden', - )) - ->setDisplayOptions('form', array( + ]) + ->setDisplayOptions('form', [ 'type' => 'language_select', 'weight' => 2, - )); + ]); $fields['uid'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Authored by')) @@ -277,6 +279,28 @@ class EntitySubqueue extends ContentEntityBase implements EntitySubqueueInterfac return $this; } + /** + * {@inheritdoc} + */ + public function addItem(EntityInterface $entity) { + $this->get('items')->appendItem($entity->id()); + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeItem(EntityInterface $entity) { + $subqueue_items = $this->get('items')->getValue(); + foreach ($subqueue_items as $key => $item) { + if ($item['target_id'] == $entity->id()) { + unset($subqueue_items[$key]); + } + } + $this->get('items')->setValue($subqueue_items); + return $this; + } + /** * Default value callback for 'uid' base field definition. * @@ -286,7 +310,7 @@ class EntitySubqueue extends ContentEntityBase implements EntitySubqueueInterfac * An array of default values. */ public static function getCurrentUserId() { - return array(\Drupal::currentUser()->id()); + return [\Drupal::currentUser()->id()]; } /** diff --git a/web/modules/contrib/entityqueue/src/EntityQueueAccessControlHandler.php b/web/modules/contrib/entityqueue/src/EntityQueueAccessControlHandler.php new file mode 100644 index 000000000..6151dc07b --- /dev/null +++ b/web/modules/contrib/entityqueue/src/EntityQueueAccessControlHandler.php @@ -0,0 +1,48 @@ +<?php + +namespace Drupal\entityqueue; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\EntityAccessControlHandler; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Session\AccountInterface; + +/** + * Defines the access control handler for the entity_queue entity type. + * + * @see \Drupal\entityqueue\Entity\EntityQueue + */ +class EntityQueueAccessControlHandler extends EntityAccessControlHandler { + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + /** @var \Drupal\entityqueue\EntitySubqueueInterface $entity */ + switch ($operation) { + case 'view': + return AccessResult::allowedIfHasPermission($account, 'access content'); + break; + + case 'update': + return AccessResult::allowedIfHasPermissions($account, ["update {$entity->id()} entityqueue", 'manipulate all entityqueues', 'administer entityqueue'], 'OR'); + break; + + case 'delete': + return AccessResult::allowedIfHasPermissions($account, ["delete {$entity->id()} entityqueue", 'manipulate all entityqueues', 'administer entityqueue'], 'OR'); + break; + + default: + // No opinion. + return AccessResult::neutral(); + } + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + return AccessResult::allowedIfHasPermission($account, 'administer entityqueue'); + } + +} diff --git a/web/modules/contrib/entityqueue/src/EntityQueueHandlerBase.php b/web/modules/contrib/entityqueue/src/EntityQueueHandlerBase.php index 94cc9d2c7..cd989a153 100644 --- a/web/modules/contrib/entityqueue/src/EntityQueueHandlerBase.php +++ b/web/modules/contrib/entityqueue/src/EntityQueueHandlerBase.php @@ -3,6 +3,7 @@ namespace Drupal\entityqueue; use Drupal\Component\Plugin\PluginBase; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Form\FormStateInterface; @@ -28,7 +29,7 @@ abstract class EntityQueueHandlerBase extends PluginBase implements EntityQueueH */ public function __construct(array $configuration, $plugin_id, array $plugin_definition) { parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->configuration += $this->defaultConfiguration(); + $this->setConfiguration($configuration); } /** @@ -42,7 +43,7 @@ abstract class EntityQueueHandlerBase extends PluginBase implements EntityQueueH * {@inheritdoc} */ public function setConfiguration(array $configuration) { - $this->configuration = $configuration; + $this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration); } /** @@ -106,26 +107,26 @@ abstract class EntityQueueHandlerBase extends PluginBase implements EntityQueueH /** * {@inheritdoc} */ - public function onQueuePreSave(EntityQueueInterface $queue, EntityStorageInterface $storage) { } + public function onQueuePreSave(EntityQueueInterface $queue, EntityStorageInterface $storage) {} /** * {@inheritdoc} */ - public function onQueuePostSave(EntityQueueInterface $queue, EntityStorageInterface $storage, $update = TRUE) { } + public function onQueuePostSave(EntityQueueInterface $queue, EntityStorageInterface $storage, $update = TRUE) {} /** * {@inheritdoc} */ - public function onQueuePreDelete(EntityQueueInterface $queue, EntityStorageInterface $storage) { } + public function onQueuePreDelete(EntityQueueInterface $queue, EntityStorageInterface $storage) {} /** * {@inheritdoc} */ - public function onQueuePostDelete(EntityQueueInterface $queue, EntityStorageInterface $storage) { } + public function onQueuePostDelete(EntityQueueInterface $queue, EntityStorageInterface $storage) {} /** * {@inheritdoc} */ - public function onQueuePostLoad(EntityQueueInterface $queue, EntityStorageInterface $storage) { } + public function onQueuePostLoad(EntityQueueInterface $queue, EntityStorageInterface $storage) {} } diff --git a/web/modules/contrib/entityqueue/src/EntityQueueHandlerInterface.php b/web/modules/contrib/entityqueue/src/EntityQueueHandlerInterface.php index 0731c0886..650c22467 100644 --- a/web/modules/contrib/entityqueue/src/EntityQueueHandlerInterface.php +++ b/web/modules/contrib/entityqueue/src/EntityQueueHandlerInterface.php @@ -2,6 +2,8 @@ namespace Drupal\entityqueue; +use Drupal\Component\Plugin\DerivativeInspectionInterface; +use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Component\Plugin\ConfigurablePluginInterface; @@ -14,7 +16,7 @@ use Drupal\Component\Plugin\ConfigurablePluginInterface; * @see \Drupal\entityqueue\EntityQueueHandlerBase * @see plugin_api */ -interface EntityQueueHandlerInterface extends PluginFormInterface, ConfigurablePluginInterface { +interface EntityQueueHandlerInterface extends PluginFormInterface, ConfigurablePluginInterface, PluginInspectionInterface, DerivativeInspectionInterface { /** * Sets the entity queue that is using this plugin. @@ -55,7 +57,7 @@ interface EntityQueueHandlerInterface extends PluginFormInterface, ConfigurableP /** * Acts on an entity queue before the presave hook is invoked. * - * @param \Drupal\entityqueue\EntityQueueInterface + * @param \Drupal\entityqueue\EntityQueueInterface $queue * The entity queue object. * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The entity storage object. @@ -65,7 +67,7 @@ interface EntityQueueHandlerInterface extends PluginFormInterface, ConfigurableP /** * Acts on an entity queue before the insert or update hook is invoked. * - * @param \Drupal\entityqueue\EntityQueueInterface + * @param \Drupal\entityqueue\EntityQueueInterface $queue * The entity queue object. * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The entity storage object. @@ -77,7 +79,7 @@ interface EntityQueueHandlerInterface extends PluginFormInterface, ConfigurableP /** * Acts on entity queues before they are deleted and before hooks are invoked. * - * @param \Drupal\entityqueue\EntityQueueInterface + * @param \Drupal\entityqueue\EntityQueueInterface $queue * The entity queue object. * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The entity storage object. @@ -87,7 +89,7 @@ interface EntityQueueHandlerInterface extends PluginFormInterface, ConfigurableP /** * Acts on deleted entity queues before the delete hook is invoked. * - * @param \Drupal\entityqueue\EntityQueueInterface + * @param \Drupal\entityqueue\EntityQueueInterface $queue * The entity queue object. * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The entity storage object. @@ -97,7 +99,7 @@ interface EntityQueueHandlerInterface extends PluginFormInterface, ConfigurableP /** * Acts on loaded entity queues. * - * @param \Drupal\entityqueue\EntityQueueInterface + * @param \Drupal\entityqueue\EntityQueueInterface $queue * The entity queue object. * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The entity storage object. diff --git a/web/modules/contrib/entityqueue/src/EntityQueueInterface.php b/web/modules/contrib/entityqueue/src/EntityQueueInterface.php index d861287bb..630e5e2fb 100644 --- a/web/modules/contrib/entityqueue/src/EntityQueueInterface.php +++ b/web/modules/contrib/entityqueue/src/EntityQueueInterface.php @@ -10,21 +10,29 @@ use Drupal\Core\Config\Entity\ConfigEntityInterface; interface EntityQueueInterface extends ConfigEntityInterface { /** - * Gets the EntityQueueHandler plugin id. + * Gets the EntityQueueHandler plugin ID. * * @return string */ public function getHandler(); + /** + * Gets the handler plugin configuration for this queue. + * + * @return mixed[] + * The handler plugin configuration. + */ + public function getHandlerConfiguration(); + /** * Sets the EntityQueueHandler. * - * @param string $handler + * @param string $handler_id * The handler name. * * @return $this */ - public function setHandler($handler); + public function setHandler($handler_id); /** * Gets the EntityQueueHandler plugin object. @@ -33,6 +41,16 @@ interface EntityQueueInterface extends ConfigEntityInterface { */ public function getHandlerPlugin(); + /** + * Sets the EntityQueueHandler plugin object. + * + * @param \Drupal\entityqueue\EntityQueueHandlerInterface $handler + * A queue handler plugin. + * + * @return $this + */ + public function setHandlerPlugin($handler); + /** * Gets the ID of the target entity type. * diff --git a/web/modules/contrib/entityqueue/src/EntityQueueListBuilder.php b/web/modules/contrib/entityqueue/src/EntityQueueListBuilder.php index 76a13df3f..77fa85da0 100644 --- a/web/modules/contrib/entityqueue/src/EntityQueueListBuilder.php +++ b/web/modules/contrib/entityqueue/src/EntityQueueListBuilder.php @@ -2,10 +2,12 @@ namespace Drupal\entityqueue; +use Drupal\Core\Cache\Cache; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\entityqueue\Entity\EntitySubqueue; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -21,6 +23,11 @@ class EntityQueueListBuilder extends ConfigEntityListBuilder { */ protected $entityTypeManager; + /** + * {@inheritdoc} + */ + protected $limit = FALSE; + /** * Constructs a new class instance. * @@ -49,11 +56,16 @@ class EntityQueueListBuilder extends ConfigEntityListBuilder { * {@inheritdoc} */ public function load() { - $entities = array( - 'enabled' => array(), - 'disabled' => array(), - ); + $entities = [ + 'enabled' => [], + 'disabled' => [], + ]; + /** @var \Drupal\entityqueue\EntityQueueInterface $entity */ foreach (parent::load() as $entity) { + // Don't display queues which can not be edited by the user. + if (!$entity->access('update')) { + continue; + } if ($entity->status()) { $entities['enabled'][] = $entity; } @@ -87,7 +99,7 @@ class EntityQueueListBuilder extends ConfigEntityListBuilder { 'handler' => $entity->getHandlerPlugin()->getPluginDefinition()['title'], 'items' => $this->getQueueItemsStatus($entity), ] + parent::buildRow($entity), - 'title' => $this->t('Machine name: @name', array('@name' => $entity->id())), + 'title' => $this->t('Machine name: @name', ['@name' => $entity->id()]), ]; return $row; @@ -102,25 +114,29 @@ class EntityQueueListBuilder extends ConfigEntityListBuilder { $build['#type'] = 'container'; $build['#attributes']['id'] = 'entity-queue-list'; $build['#attached']['library'][] = 'core/drupal.ajax'; + $build['#cache'] = [ + 'contexts' => Cache::mergeContexts($this->entityType->getListCacheContexts(), ['user.permissions']), + 'tags' => $this->entityType->getListCacheTags(), + ]; - $build['enabled']['heading']['#markup'] = '<h2>' . $this->t('Enabled', array(), array('context' => 'Plural')) . '</h2>'; - $build['disabled']['heading']['#markup'] = '<h2>' . $this->t('Disabled', array(), array('context' => 'Plural')) . '</h2>'; + $build['enabled']['heading']['#markup'] = '<h2>' . $this->t('Enabled', [], ['context' => 'Plural']) . '</h2>'; + $build['disabled']['heading']['#markup'] = '<h2>' . $this->t('Disabled', [], ['context' => 'Plural']) . '</h2>'; - foreach (array('enabled', 'disabled') as $status) { + foreach (['enabled', 'disabled'] as $status) { $build[$status]['#type'] = 'container'; - $build[$status]['#attributes'] = array('class' => array('entity-queue-list-section', $status)); - $build[$status]['table'] = array( + $build[$status]['#attributes'] = ['class' => ['entity-queue-list-section', $status]]; + $build[$status]['table'] = [ '#type' => 'table', - '#attributes' => array( - 'class' => array('entity-queue-listing-table'), - ), + '#attributes' => [ + 'class' => ['entity-queue-listing-table'], + ], '#header' => $this->buildHeader(), - '#rows' => array(), + '#rows' => [], '#cache' => [ 'contexts' => $this->entityType->getListCacheContexts(), 'tags' => $this->entityType->getListCacheTags(), ], - ); + ]; foreach ($entities[$status] as $entity) { $build[$status]['table']['#rows'][$entity->id()] = $this->buildRow($entity); } @@ -144,7 +160,7 @@ class EntityQueueListBuilder extends ConfigEntityListBuilder { } // Add AJAX functionality to enable/disable operations. - foreach (array('enable', 'disable') as $op) { + foreach (['enable', 'disable'] as $op) { if (isset($operations[$op])) { $operations[$op]['url'] = $entity->toUrl($op); // Enable and disable operations should use AJAX. diff --git a/web/modules/contrib/entityqueue/src/EntityQueuePermissions.php b/web/modules/contrib/entityqueue/src/EntityQueuePermissions.php index 67692065b..d848b502f 100644 --- a/web/modules/contrib/entityqueue/src/EntityQueuePermissions.php +++ b/web/modules/contrib/entityqueue/src/EntityQueuePermissions.php @@ -13,7 +13,7 @@ class EntityQueuePermissions { * @return array */ public function permissions() { - $perms = array(); + $perms = []; // Generate queue permissions for all queues. foreach (EntityQueue::loadMultiple() as $queue) { $perms += $this->buildPermissions($queue); @@ -31,20 +31,20 @@ class EntityQueuePermissions { $queue_id = $queue->id(); if ($queue->getHandlerPlugin()->supportsMultipleSubqueues()) { - $permissions["create $queue_id entityqueue"] = array( - 'title' => $this->t('Add %queue subqueues', array('%queue' => $queue->label())), - 'description' => $this->t('Access to create new subqueue to the %queue queue.', array('%queue' => $queue->label())), - ); - $permissions["delete $queue_id entityqueue"] = array( - 'title' => $this->t('Delete %queue subqueues', array('%queue' => $queue->label())), - 'description' => $this->t('Access to delete subqueues of the %queue queue.', array('%queue' => $queue->label())), - ); + $permissions["create $queue_id entityqueue"] = [ + 'title' => $this->t('Add %queue subqueues', ['%queue' => $queue->label()]), + 'description' => $this->t('Access to create new subqueue to the %queue queue.', ['%queue' => $queue->label()]), + ]; + $permissions["delete $queue_id entityqueue"] = [ + 'title' => $this->t('Delete %queue subqueues', ['%queue' => $queue->label()]), + 'description' => $this->t('Access to delete subqueues of the %queue queue.', ['%queue' => $queue->label()]), + ]; } - $permissions["update $queue_id entityqueue"] = array( - 'title' => $this->t('Manipulate %queue queue', array('%queue' => $queue->label())), - 'description' => $this->t('Access to update the %queue queue.', array('%queue' => $queue->label())), - ); + $permissions["update $queue_id entityqueue"] = [ + 'title' => $this->t('Manipulate %queue queue', ['%queue' => $queue->label()]), + 'description' => $this->t('Access to update the %queue queue.', ['%queue' => $queue->label()]), + ]; return $permissions; } diff --git a/web/modules/contrib/entityqueue/src/EntitySubqueueAccessControlHandler.php b/web/modules/contrib/entityqueue/src/EntitySubqueueAccessControlHandler.php index f5a81290d..a8a0cb8ad 100644 --- a/web/modules/contrib/entityqueue/src/EntitySubqueueAccessControlHandler.php +++ b/web/modules/contrib/entityqueue/src/EntitySubqueueAccessControlHandler.php @@ -32,9 +32,9 @@ class EntitySubqueueAccessControlHandler extends EntityAccessControlHandler { case 'delete': $can_delete_subqueue = AccessResult::allowedIf(!$entity->getQueue()->getHandlerPlugin()->hasAutomatedSubqueues()); - $access_result = AccessResult - ::allowedIfHasPermissions($account, ["delete {$entity->bundle()} entityqueue", 'manipulate all entityqueues', 'administer entityqueue'], 'OR') - ->andIf($can_delete_subqueue); + $access_result = AccessResult::allowedIfHasPermissions($account, ["delete {$entity->bundle()} entityqueue", 'manipulate all entityqueues', 'administer entityqueue'], 'OR') + ->andIf($can_delete_subqueue) + ->addCacheableDependency($entity->getQueue()); return $access_result; break; @@ -54,6 +54,7 @@ class EntitySubqueueAccessControlHandler extends EntityAccessControlHandler { if ($entity_bundle) { $queue = EntityQueue::load($entity_bundle); $access_result = AccessResult::allowedIf(!$queue->getHandlerPlugin()->hasAutomatedSubqueues()); + $access_result->addCacheableDependency($queue); } return $access_result; diff --git a/web/modules/contrib/entityqueue/src/EntitySubqueueInterface.php b/web/modules/contrib/entityqueue/src/EntitySubqueueInterface.php index e4eaca90b..2aebf9821 100644 --- a/web/modules/contrib/entityqueue/src/EntitySubqueueInterface.php +++ b/web/modules/contrib/entityqueue/src/EntitySubqueueInterface.php @@ -4,6 +4,7 @@ namespace Drupal\entityqueue; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityChangedInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\user\EntityOwnerInterface; /** @@ -29,6 +30,26 @@ interface EntitySubqueueInterface extends ContentEntityInterface, EntityChangedI */ public function setQueue(EntityQueueInterface $queue); + /** + * Adds an entity to a subqueue + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * An entity object. + * + * @return $this + */ + public function addItem(EntityInterface $entity); + + /** + * Removes an entity from a subqueue + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * An entity object. + * + * @return $this + */ + public function removeItem(EntityInterface $entity); + /** * Gets the subqueue title. * diff --git a/web/modules/contrib/entityqueue/src/Form/EntityQueueForm.php b/web/modules/contrib/entityqueue/src/Form/EntityQueueForm.php index 44db509c3..3b4421666 100644 --- a/web/modules/contrib/entityqueue/src/Form/EntityQueueForm.php +++ b/web/modules/contrib/entityqueue/src/Form/EntityQueueForm.php @@ -9,6 +9,8 @@ use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; use Drupal\Core\Entity\EntityTypeRepositoryInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformState; +use Drupal\entityqueue\EntityQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -34,7 +36,7 @@ class EntityQueueForm extends BundleEntityFormBase { /** * The entity queue handler plugin manager. * - * @var \Drupal\Component\Plugin\PluginManagerInterface + * @var \Drupal\entityqueue\EntityQueueHandlerManager */ protected $entityQueueHandlerManager; @@ -67,9 +69,9 @@ class EntityQueueForm extends BundleEntityFormBase { /** * Constructs a EntityQueueForm. * - * @param \Drupal\Core\Entity\EntityTypeRepositoryInterface + * @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository * The entity type repository. - * @param \Drupal\Component\Plugin\PluginManagerInterface + * @param \Drupal\Component\Plugin\PluginManagerInterface $entity_queue_handler_manager * The entity queue handler plugin manager. * @param \Psr\Log\LoggerInterface $logger * A logger instance. @@ -110,16 +112,43 @@ class EntityQueueForm extends BundleEntityFormBase { '#disabled' => !$queue->isNew(), ]; - $handlers = $this->entityQueueHandlerManager->getAllEntityQueueHandlers(); + $handler_plugin = $this->getHandlerPlugin($this->getEntity(), $form_state); $form['handler'] = [ '#type' => 'radios', '#title' => $this->t('Type'), - '#options' => $handlers, - '#default_value' => $queue->getHandler(), + '#options' => $this->entityQueueHandlerManager->getAllEntityQueueHandlers(), + '#default_value' => $handler_plugin->getPluginId(), '#required' => TRUE, '#disabled' => !$queue->isNew(), + '#ajax' => [ + 'callback' => '::settingsAjax', + 'wrapper' => 'entityqueue-handler-settings-wrapper', + 'trigger_as' => ['name' => 'handler_change'], + ], + ]; + $form['handler_change'] = [ + '#type' => 'submit', + '#name' => 'handler_change', + '#value' => $this->t('Change type'), + '#limit_validation_errors' => [], + '#submit' => [[get_called_class(), 'settingsAjaxSubmit']], + '#attributes' => ['class' => ['js-hide']], + '#ajax' => [ + 'callback' => '::settingsAjax', + 'wrapper' => 'entityqueue-handler-settings-wrapper', + ], + ]; + + $form['handler_settings_wrapper'] = [ + '#type' => 'container', + '#id' => 'entityqueue-handler-settings-wrapper', + '#tree' => TRUE, ]; + $form['handler_settings_wrapper']['handler_settings'] = []; + $subform_state = SubformState::createForSubform($form['handler_settings_wrapper']['handler_settings'], $form, $form_state); + $form['handler_settings_wrapper']['handler_settings'] = $handler_plugin->buildConfigurationForm($form['handler_settings_wrapper']['handler_settings'], $subform_state); + $form['settings'] = [ '#type' => 'vertical_tabs', ]; @@ -255,6 +284,47 @@ class EntityQueueForm extends BundleEntityFormBase { return $form; } + /** + * Gets the handler plugin for the currently selected queue handler. + * + * @param \Drupal\entityqueue\EntityQueueInterface $entity + * The current form entity. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\entityqueue\EntityQueueHandlerInterface + * The queue handler plugin. + */ + protected function getHandlerPlugin(EntityQueueInterface $entity, FormStateInterface $form_state) { + if (!$handler_plugin = $form_state->get('handler_plugin')) { + $stored_handler_id = $entity->getHandler(); + // Use selected handler if it exists, falling back to the stored handler. + $handler_id = $form_state->getValue('handler', $stored_handler_id); + // If the current handler is the stored handler, use the stored handler + // settings. Otherwise leave the settings empty. + $handler_configuration = $handler_id === $stored_handler_id ? $entity->getHandlerConfiguration() : []; + + $handler_plugin = $this->entityQueueHandlerManager->createInstance($handler_id, $handler_configuration); + $form_state->set('handler_plugin', $handler_plugin); + } + return $handler_plugin; + } + + /** + * Ajax callback for the queue settings form. + */ + public static function settingsAjax($form, FormStateInterface $form_state) { + return $form['handler_settings_wrapper']; + } + + /** + * Submit handler for the non-JS case. + */ + public static function settingsAjaxSubmit($form, FormStateInterface $form_state) { + $form_state->set('handler_plugin', NULL); + $form_state->setRebuild(); + } + /** * Form element validation handler; Invokes selection plugin's validation. * @@ -285,12 +355,27 @@ class EntityQueueForm extends BundleEntityFormBase { /** * {@inheritdoc} */ - public function buildEntity(array $form, FormStateInterface $form_state) { - $entity = parent::buildEntity($form, $form_state); - if ($handler = $entity->get('handler')) { - $entity->setHandler($handler); - } - return $entity; + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $handler_plugin = $this->getHandlerPlugin($this->getEntity(), $form_state); + $subform_state = SubformState::createForSubform($form['handler_settings_wrapper']['handler_settings'], $form, $form_state); + $handler_plugin->validateConfigurationForm($form['handler_settings_wrapper']['handler_settings'], $subform_state); + } + + /** + * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::submitForm(). + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + /** @var \Drupal\entityqueue\EntityQueueInterface $queue */ + $queue = $this->getEntity(); + $handler_plugin = $this->getHandlerPlugin($queue, $form_state); + $subform_state = SubformState::createForSubform($form['handler_settings_wrapper']['handler_settings'], $form, $form_state); + $handler_plugin->submitConfigurationForm($form['handler_settings_wrapper']['handler_settings'], $subform_state); + + $queue->setHandlerPlugin($handler_plugin); } /** @@ -307,7 +392,7 @@ class EntityQueueForm extends BundleEntityFormBase { } else { drupal_set_message($this->t('The entity queue %label has been added.', ['%label' => $queue->label()])); - $this->logger->notice('The entity queue %label has been added.', ['%label' => $queue->label(), 'link' => $edit_link]); + $this->logger->notice('The entity queue %label has been added.', ['%label' => $queue->label(), 'link' => $edit_link]); } $form_state->setRedirectUrl($queue->toUrl('collection')); diff --git a/web/modules/contrib/entityqueue/src/Form/EntitySubqueueDeleteForm.php b/web/modules/contrib/entityqueue/src/Form/EntitySubqueueDeleteForm.php index f14c45f41..39ac23d04 100644 --- a/web/modules/contrib/entityqueue/src/Form/EntitySubqueueDeleteForm.php +++ b/web/modules/contrib/entityqueue/src/Form/EntitySubqueueDeleteForm.php @@ -5,7 +5,7 @@ namespace Drupal\entityqueue\Form; use Drupal\Core\Entity\ContentEntityDeleteForm; /** - * Provides the comment delete confirmation form. + * Provides the entity subqueue delete confirmation form. */ class EntitySubqueueDeleteForm extends ContentEntityDeleteForm { diff --git a/web/modules/contrib/entityqueue/src/Form/EntitySubqueueForm.php b/web/modules/contrib/entityqueue/src/Form/EntitySubqueueForm.php index 7837333dc..04b71fdb1 100644 --- a/web/modules/contrib/entityqueue/src/Form/EntitySubqueueForm.php +++ b/web/modules/contrib/entityqueue/src/Form/EntitySubqueueForm.php @@ -6,11 +6,11 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\ContentEntityForm; -use Drupal\Core\Entity\EntityChangedInterface; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\inline_entity_form\Plugin\Field\FieldWidget\InlineEntityFormBase; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -27,6 +27,13 @@ class EntitySubqueueForm extends ContentEntityForm { */ protected $entity; + /** + * The element info manager. + * + * @var \Drupal\Core\Render\ElementInfoManagerInterface + */ + protected $elementInfo; + /** * A logger instance. * @@ -40,6 +47,9 @@ class EntitySubqueueForm extends ContentEntityForm { public static function create(ContainerInterface $container) { return new static( $container->get('entity.manager'), + $container->get('entity_type.bundle.info'), + $container->get('datetime.time'), + $container->get('element_info'), $container->get('logger.factory')->get('entityqueue') ); } @@ -47,12 +57,21 @@ class EntitySubqueueForm extends ContentEntityForm { /** * Constructs a EntitySubqueueForm. * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle service. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info + * The element info manager. * @param \Psr\Log\LoggerInterface $logger * A logger instance. */ - public function __construct(EntityManagerInterface $entity_manager, LoggerInterface $logger) { - parent::__construct($entity_manager); + public function __construct(EntityManagerInterface $entity_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, TimeInterface $time, ElementInfoManagerInterface $element_info, LoggerInterface $logger) { + parent::__construct($entity_manager, $entity_type_bundle_info, $time); + $this->elementInfo = $element_info; $this->logger = $logger; } @@ -87,14 +106,17 @@ class EntitySubqueueForm extends ContentEntityForm { $form['#prefix'] = '<div id="' . $wrapper_id . '">'; $form['#suffix'] = '</div>'; - // @todo Consider creating a 'Machine name' field widget. + // @todo Use the 'Machine name' field widget when + // https://www.drupal.org/node/2685749 is committed. + $element_info = $this->elementInfo->getInfo('machine_name'); $form['name'] = [ '#type' => 'machine_name', '#default_value' => $this->entity->id(), - '#machine_name' => array( + '#source_field' => 'title', + '#process' => array_merge([[get_class($this), 'processMachineNameSource']], $element_info['#process']), + '#machine_name' => [ 'exists' => '\Drupal\entityqueue\Entity\EntitySubqueue::load', - 'source' => ['title', 'widget', 0, 'value'], - ), + ], '#disabled' => !$this->entity->isNew(), '#weight' => -5, '#access' => !$this->entity->getQueue()->getHandlerPlugin()->hasAutomatedSubqueues(), @@ -103,6 +125,27 @@ class EntitySubqueueForm extends ContentEntityForm { return $form; } + /** + * Form API callback: Sets the 'source' property of a machine_name element. + * + * This method is assigned as a #process callback in formElement() method. + */ + public static function processMachineNameSource($element, FormStateInterface $form_state, $form) { + $source_field_state = WidgetBase::getWidgetState($form['#parents'], $element['#source_field'], $form_state); + + // Hide the field widget if the source field is not configured properly or + // if it doesn't exist in the form. + if (empty($element['#source_field']) || empty($source_field_state['array_parents'])) { + $element['#access'] = FALSE; + } + else { + $source_field_element = NestedArray::getValue($form_state->getCompleteForm(), $source_field_state['array_parents']); + $element['#machine_name']['source'] = $source_field_element[0]['value']['#array_parents']; + } + + return $element; + } + /** * {@inheritdoc} */ @@ -259,7 +302,7 @@ class EntitySubqueueForm extends ContentEntityForm { } else { drupal_set_message($this->t('The entity subqueue %label has been added.', ['%label' => $subqueue->label()])); - $this->logger->notice('The entity subqueue %label has been added.', ['%label' => $subqueue->label(), 'link' => $edit_link]); + $this->logger->notice('The entity subqueue %label has been added.', ['%label' => $subqueue->label(), 'link' => $edit_link]); } $queue = $subqueue->getQueue(); @@ -271,14 +314,4 @@ class EntitySubqueueForm extends ContentEntityForm { } } - /** - * {@inheritdoc} - */ - public function updateChangedTime(EntityInterface $entity) { - // @todo Remove this method when Drupal 8.2.x is no longer supported. - if ($entity->getEntityType()->isSubclassOf(EntityChangedInterface::class)) { - $entity->setChangedTime(REQUEST_TIME); - } - } - } diff --git a/web/modules/contrib/entityqueue/src/Plugin/Derivative/EntityqueueLocalTask.php b/web/modules/contrib/entityqueue/src/Plugin/Derivative/EntityqueueLocalTask.php new file mode 100644 index 000000000..49bb23c53 --- /dev/null +++ b/web/modules/contrib/entityqueue/src/Plugin/Derivative/EntityqueueLocalTask.php @@ -0,0 +1,72 @@ +<?php + +namespace Drupal\entityqueue\Plugin\Derivative; + +use Drupal\Component\Plugin\Derivative\DeriverBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides local task definitions for all entity bundles. + */ +class EntityqueueLocalTask extends DeriverBase implements ContainerDeriverInterface { + + use StringTranslationTrait; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Creates an EntityqueueLocalTask object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity manager. + * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation + * The translation manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) { + $this->entityTypeManager = $entity_type_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $this->derivatives = []; + + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical')) { + $entityqueue_route_name = "entity.$entity_type_id.entityqueue"; + $this->derivatives[$entityqueue_route_name] = [ + 'entity_type' => $entity_type_id, + 'title' => $this->t('Entityqueue'), + 'route_name' => $entityqueue_route_name, + 'base_route' => "entity.$entity_type_id.canonical", + // Ensure that the entityqueue tab is at the end of the list. + 'weight' => 21, + ] + $base_plugin_definition; + } + } + + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/web/modules/contrib/entityqueue/src/Plugin/EntityQueueHandler/Simple.php b/web/modules/contrib/entityqueue/src/Plugin/EntityQueueHandler/Simple.php index c84db326d..da2120a23 100644 --- a/web/modules/contrib/entityqueue/src/Plugin/EntityQueueHandler/Simple.php +++ b/web/modules/contrib/entityqueue/src/Plugin/EntityQueueHandler/Simple.php @@ -51,15 +51,20 @@ class Simple extends EntityQueueHandlerBase { */ public function onQueuePostSave(EntityQueueInterface $queue, EntityStorageInterface $storage, $update = TRUE) { // Make sure that every simple queue has a subqueue. - if (!$update) { + if ($update) { + $subqueue = EntitySubqueue::load($queue->id()); + $subqueue->setTitle($queue->label()); + } + else { $subqueue = EntitySubqueue::create([ 'queue' => $queue->id(), 'name' => $queue->id(), 'title' => $queue->label(), 'langcode' => $queue->language()->getId(), ]); - $subqueue->save(); } + + $subqueue->save(); } /** diff --git a/web/modules/contrib/entityqueue/src/Plugin/Field/FieldWidget/EntityqueueDragtableWidget.php b/web/modules/contrib/entityqueue/src/Plugin/Field/FieldWidget/EntityqueueDragtableWidget.php index 8be25f4e5..c7aa12f4b 100644 --- a/web/modules/contrib/entityqueue/src/Plugin/Field/FieldWidget/EntityqueueDragtableWidget.php +++ b/web/modules/contrib/entityqueue/src/Plugin/Field/FieldWidget/EntityqueueDragtableWidget.php @@ -31,6 +31,44 @@ class EntityqueueDragtableWidget extends EntityReferenceAutocompleteWidget { */ protected $wrapperId; + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'link_to_entity' => FALSE, + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $elements = parent::settingsForm($form, $form_state); + + $elements['link_to_entity'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Link label to the referenced entity'), + '#default_value' => $this->getSetting('link'), + ]; + + return $elements; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + $settings = $this->getSettings(); + if (!empty($settings['link_to_entity'])) { + $summary[] = $this->t('Link to the referenced entity'); + } + + return $summary; + } + /** * {@inheritdoc} */ @@ -40,7 +78,12 @@ class EntityqueueDragtableWidget extends EntityReferenceAutocompleteWidget { $referenced_entities = $items->referencedEntities(); if (isset($referenced_entities[$delta])) { - $entity_label = EntityAutocomplete::getEntityLabels([$referenced_entities[$delta]]); + if ($this->getSetting('link_to_entity') && !$referenced_entities[$delta]->isNew()) { + $entity_label = $referenced_entities[$delta]->toLink()->toString(); + } + else { + $entity_label = $referenced_entities[$delta]->label(); + } $id_prefix = implode('-', array_merge($parents, [$field_name, $delta])); $element += [ @@ -169,7 +212,7 @@ class EntityqueueDragtableWidget extends EntityReferenceAutocompleteWidget { $submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -2)); // Check submitted values for empty items. - $new_values = array(); + $new_values = []; foreach ($submitted_values as $delta => $submitted_value) { if ($delta !== 'add_more' && (isset($submitted_value['target_id']) || isset($submitted_value['entity']))) { $new_values[] = $submitted_value; diff --git a/web/modules/contrib/entityqueue/src/Plugin/Validation/Constraint/QueueSizeConstraintValidator.php b/web/modules/contrib/entityqueue/src/Plugin/Validation/Constraint/QueueSizeConstraintValidator.php index 049d32a72..2d60009a1 100644 --- a/web/modules/contrib/entityqueue/src/Plugin/Validation/Constraint/QueueSizeConstraintValidator.php +++ b/web/modules/contrib/entityqueue/src/Plugin/Validation/Constraint/QueueSizeConstraintValidator.php @@ -33,7 +33,6 @@ class QueueSizeConstraintValidator extends ConstraintValidator { $this->context->buildViolation($constraint->messageMaxSize, ['%max_size' => $max_size]) ->addViolation(); } - } } diff --git a/web/modules/contrib/entityqueue/src/Plugin/views/relationship/EntityQueueRelationship.php b/web/modules/contrib/entityqueue/src/Plugin/views/relationship/EntityQueueRelationship.php index 9b8e86d2d..691a7662b 100644 --- a/web/modules/contrib/entityqueue/src/Plugin/views/relationship/EntityQueueRelationship.php +++ b/web/modules/contrib/entityqueue/src/Plugin/views/relationship/EntityQueueRelationship.php @@ -33,17 +33,17 @@ class EntityQueueRelationship extends EntityReverse implements CacheableDependen */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $queues = EntityQueue::loadMultipleByTargetType($this->getEntityType()); - $options = array(); + $options = []; foreach ($queues as $queue) { $options[$queue->id()] = $queue->label(); } - $form['limit_queue'] = array( + $form['limit_queue'] = [ '#type' => 'radios', '#title' => $this->t('Limit to a specific entity queue'), '#options' => $options, '#default_value' => $this->options['limit_queue'], - ); + ]; parent::buildOptionsForm($form, $form_state); } diff --git a/web/modules/contrib/entityqueue/src/Plugin/views/sort/EntityQueueInQueue.php b/web/modules/contrib/entityqueue/src/Plugin/views/sort/EntityQueueInQueue.php new file mode 100644 index 000000000..b679675a8 --- /dev/null +++ b/web/modules/contrib/entityqueue/src/Plugin/views/sort/EntityQueueInQueue.php @@ -0,0 +1,103 @@ +<?php + +namespace Drupal\entityqueue\Plugin\views\sort; + +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\entityqueue\Plugin\views\relationship\EntityQueueRelationship; +use Drupal\views\Plugin\views\sort\SortPluginBase; +use Drupal\Core\Messenger\Messenger; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Sort handler for ordering the results based on their queue position. + * + * @ingroup views_sort_handlers + * + * @ViewsSort("entity_queue_in_queue") + */ +class EntityQueueInQueue extends SortPluginBase { + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * Constructs a new EntityQueueInQueue object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, AccountInterface $current_user, MessengerInterface $messenger) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->currentUser = $current_user; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_user'), + $container->get('messenger') + ); + } + + /** + * {@inheritdoc}. + */ + public function query() { + $this->ensureMyTable(); + + // Try to find an entity queue relationship in this view, and pick the first + // one available. + $entity_queue_relationship = NULL; + foreach ($this->view->relationship as $id => $relationship) { + if ($relationship instanceof EntityQueueRelationship) { + $entity_queue_relationship = $relationship; + $this->options['relationship'] = $id; + $this->setRelationship(); + + break; + } + } + + if ($entity_queue_relationship) { + // Add the field. + $subqueue_items_table_alias = $entity_queue_relationship->first_alias; + $this->query->addOrderBy($subqueue_items_table_alias, 'bundle', $this->options['order']); + } + else { + if ($this->currentUser->hasPermission('administer views')) { + $this->messenger->addMessage($this->t('In order to sort by in queue, you need to add the Entityqueue: Queue relationship on View: @view with display: @display', + ['@view' => $this->view->storage->label(), + '@display' => $this->view->current_display] + ), Messenger::TYPE_ERROR); + } + } + } + +} diff --git a/web/modules/contrib/entityqueue/src/Routing/RouteSubscriber.php b/web/modules/contrib/entityqueue/src/Routing/RouteSubscriber.php new file mode 100644 index 000000000..11e630be6 --- /dev/null +++ b/web/modules/contrib/entityqueue/src/Routing/RouteSubscriber.php @@ -0,0 +1,98 @@ +<?php + +namespace Drupal\entityqueue\Routing; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Routing\RouteSubscriberBase; +use Drupal\Core\Routing\RoutingEvents; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * Subscriber for entityqueue routes. + */ +class RouteSubscriber extends RouteSubscriberBase { + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a new RouteSubscriber object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entity_manager) { + $this->entityTypeManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + protected function alterRoutes(RouteCollection $collection) { + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + // Try to get the route from the current collection. + $link_template = $entity_type->getLinkTemplate('canonical'); + if (strpos($link_template, '/') !== FALSE) { + $base_path = '/' . $link_template; + } + else { + if (!$entity_route = $collection->get("entity.$entity_type_id.canonical")) { + continue; + } + $base_path = $entity_route->getPath(); + } + + // Inherit admin route status from edit route, if exists. + $is_admin = FALSE; + $route_name = "entity.$entity_type_id.edit_form"; + if ($edit_route = $collection->get($route_name)) { + $is_admin = (bool) $edit_route->getOption('_admin_route'); + } + + $path = $base_path . '/entityqueue'; + + $route = new Route( + $path, + [ + '_controller' => '\Drupal\entityqueue\Controller\EntityQueueUIController::subqueueListForEntity', + 'entity_type_id' => $entity_type_id, + '_title' => 'Entityqueues', + ], + [ + '_permission' => 'administer entityqueue+manipulate entityqueues+manipulate all entityqueues', + '_custom_access' => 'Drupal\entityqueue\Controller\EntityQueueUIController::access', + ], + [ + 'parameters' => [ + $entity_type_id => [ + 'type' => 'entity:' . $entity_type_id, + ], + ], + '_admin_route' => $is_admin, + ] + ); + $route_name = "entity.$entity_type_id.entityqueue"; + $collection->add($route_name, $route); + + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = parent::getSubscribedEvents(); + + // Should run after AdminRouteSubscriber so the routes can inherit admin + // status of the edit routes on entities. Therefore priority -210. + $events[RoutingEvents::ALTER] = ['onAlterRoutes', -210]; + + return $events; + } + +} diff --git a/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/entityqueue.entity_queue.simple_queue.yml b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/entityqueue.entity_queue.simple_queue.yml new file mode 100644 index 000000000..412d988ab --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/entityqueue.entity_queue.simple_queue.yml @@ -0,0 +1,24 @@ +uuid: 2cd581bc-8dbd-488c-978d-e774f4397950 +langcode: en +status: true +dependencies: + module: + - node +id: simple_queue +label: simple_queue +handler: simple +handler_configuration: { } +entity_settings: + target_type: node + handler: 'default:node' + handler_settings: + target_bundles: null + sort: + field: _none + auto_create: false + auto_create_bundle: '' +queue_settings: + min_size: 0 + max_size: 0 + act_as_queue: false + reverse_in_admin: false diff --git a/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/entityqueue.entity_queue.test_queue.yml b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/entityqueue.entity_queue.test_queue.yml new file mode 100644 index 000000000..f10dce2d4 --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/entityqueue.entity_queue.test_queue.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + module: + - entityqueue_test + - node +id: test_queue +label: 'Test queue' +handler: test +handler_configuration: + shape: round +entity_settings: + target_type: node + handler: 'default:node' + handler_settings: + target_bundles: null + sort: + field: _none + auto_create: false + auto_create_bundle: '' +queue_settings: + min_size: 0 + max_size: 0 + act_as_queue: false + reverse_in_admin: false diff --git a/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/views.view.entityqueue_test.yml b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/views.view.entityqueue_test.yml new file mode 100644 index 000000000..cbbecf766 --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/views.view.entityqueue_test.yml @@ -0,0 +1,181 @@ +langcode: en +status: true +dependencies: + module: + - entityqueue + - node + - user +id: entityqueue_test +label: 'Entity Queue Test' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: + entityqueue_relationship: + id: entityqueue_relationship + table: node_field_data + field: entityqueue_relationship + relationship: none + group_type: group + admin_label: 'Content queue' + required: false + limit_queue: null + entity_type: node + plugin_id: entity_queue + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/views.view.simple_queue_listing.yml b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/views.view.simple_queue_listing.yml new file mode 100644 index 000000000..341016f4c --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/install/views.view.simple_queue_listing.yml @@ -0,0 +1,185 @@ +uuid: 778ffedc-10b7-4aaa-a5b7-0073a0408b49 +langcode: en +status: true +dependencies: + config: + - entityqueue.entity_queue.simple_queue + module: + - entityqueue + - node + - user +id: simple_queue_listing +label: simple_queue_listing +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Filter + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: + entityqueue_relationship: + id: entityqueue_relationship + table: node_field_data + field: entityqueue_relationship + relationship: none + group_type: group + admin_label: 'Content queue' + required: true + limit_queue: simple_queue + entity_type: node + plugin_id: entity_queue + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: + - 'config:entityqueue.entity_queue.simple_queue' + - entity_field_info + - views_data diff --git a/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/schema/entityqueue_test.schema.yml b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/schema/entityqueue_test.schema.yml new file mode 100644 index 000000000..bcc0bb851 --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/config/schema/entityqueue_test.schema.yml @@ -0,0 +1,8 @@ +# Schema for the entity queue 'test' handler configuration. +entityqueue_handler_configuration.test: + type: entityqueue_handler_configuration + label: 'Test handler configuration' + mapping: + shape: + type: string + label: 'A test property.' diff --git a/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/entityqueue_test.info.yml b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/entityqueue_test.info.yml new file mode 100644 index 000000000..58c87e42b --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/entityqueue_test.info.yml @@ -0,0 +1,15 @@ +name: Entityqueue test +type: module +description: 'Test content for entityqueue module.' +package: Testing +# core: 8.x +dependencies: + - drupal:node + - drupal:views + - entityqueue:entityqueue + +# Information added by Drupal.org packaging script on 2018-09-05 +version: '8.x-1.0-alpha8' +core: '8.x' +project: 'entityqueue' +datestamp: 1536140287 diff --git a/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/src/Plugin/EntityQueueHandler/Test.php b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/src/Plugin/EntityQueueHandler/Test.php new file mode 100644 index 000000000..056b1ae04 --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/modules/entityqueue_test/src/Plugin/EntityQueueHandler/Test.php @@ -0,0 +1,69 @@ +<?php + +namespace Drupal\entityqueue_test\Plugin\EntityQueueHandler; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\entityqueue\EntityQueueHandlerBase; + +/** + * Defines an entity queue handler for testing. + * + * @EntityQueueHandler( + * id = "test", + * title = @Translation("Test handler") + * ) + */ +class Test extends EntityQueueHandlerBase { + + /** + * {@inheritdoc} + */ + public function supportsMultipleSubqueues() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function hasAutomatedSubqueues() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'shape' => 'round', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['shape'] = [ + '#type' => 'textfield', + '#title' => 'Shape', + '#default_value' => $this->configuration['shape'], + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + if ($form_state->getValue('shape') === 'square') { + $form_state->setErrorByName('shape', $this->t('The shape can not be square.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['shape'] = $form_state->getValue('shape'); + } + +} diff --git a/web/modules/contrib/entityqueue/tests/src/Functional/EntityQueueUiTest.php b/web/modules/contrib/entityqueue/tests/src/Functional/EntityQueueUiTest.php new file mode 100644 index 000000000..09c2b16e9 --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/src/Functional/EntityQueueUiTest.php @@ -0,0 +1,46 @@ +<?php + +namespace Drupal\Tests\entityqueue\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * Tests the user interface for entityqueue module. + * + * @group entityqueue + */ +class EntityQueueUiTest extends BrowserTestBase { + + /** + * Modules to install. + * + * @var array + */ + public static $modules = ['entityqueue_test']; + + /** + * A user with the 'administer entityqueue' permission. + * + * @var \Drupal\user\UserInterface + */ + protected $webUser; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->webUser = $this->drupalCreateUser(['administer entityqueue']); + $this->drupalLogin($this->webUser); + } + + /** + * Tests entity queue list page. + */ + public function testListPage() { + $this->drupalGet('/admin/structure/entityqueue'); + $this->assertText('There are no disabled queues'); + } + +} diff --git a/web/modules/contrib/entityqueue/tests/src/Kernel/EntityQueueCacheTagsTest.php b/web/modules/contrib/entityqueue/tests/src/Kernel/EntityQueueCacheTagsTest.php new file mode 100644 index 000000000..86b9af43f --- /dev/null +++ b/web/modules/contrib/entityqueue/tests/src/Kernel/EntityQueueCacheTagsTest.php @@ -0,0 +1,87 @@ +<?php + +namespace Drupal\Tests\entityqueue\Kernel; + +use Drupal\entityqueue\Entity\EntitySubqueue; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; + +/** + * Tests cache tags of entity queues. + * + * @group entityqueue + */ +class EntityQueueCacheTagsTest extends KernelTestBase { + + use ContentTypeCreationTrait; + use NodeCreationTrait; + + /** + * {@inheritdoc} + */ + public static $modules = ['field', 'filter', 'node', 'text', 'user', 'system', 'views', 'entityqueue', 'entityqueue_test']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installSchema('node', ['node_access']); + + $this->installEntitySchema('node'); + $this->installEntitySchema('entity_subqueue'); + $this->installEntitySchema('user'); + + $this->installConfig(['filter', 'node', 'system', 'entityqueue_test']); + + $this->createContentType(['type' => 'article', 'name' => 'Article']); + } + + /** + * Tests the cache tags of a view with a entity queue relationship. + */ + public function testViewWithRelationship() { + $nodes = []; + + $node = $this->createNode([ + 'type' => 'article', + 'title' => 'Test article (1)', + ]); + $node->save(); + $nodes[] = $node; + + $node = $this->createNode([ + 'type' => 'article', + 'title' => 'Test article (2)', + ]); + $node->save(); + $nodes[] = $node; + + $entity_subqueue = EntitySubqueue::load('simple_queue'); + $entity_subqueue->set('items', $nodes); + $entity_subqueue->save(); + + $build = [ + '#type' => 'view', + '#name' => 'simple_queue_listing', + ]; + + $renderer = $this->container->get('bare_html_page_renderer'); + $response = $renderer->renderBarePage($build, '', 'maintenance_page'); + + $this->assertEquals([ + 'config:entityqueue.entity_queue.simple_queue', + 'config:views.view.simple_queue_listing', + 'entity_field_info', + 'entity_subqueue:simple_queue', + 'entity_subqueue_list', + 'node:1', + 'node:2', + 'node_list', + 'views_data', + ], $response->getCacheableMetadata()->getCacheTags()); + } + +} diff --git a/web/modules/contrib/eu_cookie_compliance/README.txt b/web/modules/contrib/eu_cookie_compliance/README.txt index 2bca240fb..8a65deb85 100644 --- a/web/modules/contrib/eu_cookie_compliance/README.txt +++ b/web/modules/contrib/eu_cookie_compliance/README.txt @@ -14,7 +14,7 @@ The module displays a banner at the bottom or the top of website to make users aware of the fact that cookies are being set. The user may then give his/her consent or move to a page that provides more details. Consent is given by user pressing the agree buttons or by continuing browsing the website. Once -consent is given another banner appears with a “Thank you” message. +consent is given another banner appears with a "Thank you" message. The module provides a settings page where the banner can be customized. There are also template files for the banners that can be overridden by your theme. diff --git a/web/modules/contrib/eu_cookie_compliance/composer.json b/web/modules/contrib/eu_cookie_compliance/composer.json new file mode 100644 index 000000000..8a359a81e --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/composer.json @@ -0,0 +1,43 @@ +{ + "name": "drupal/eu_cookie_compliance", + "description": "This module aims at making the website compliant with the new EU cookie regulation.", + "type": "drupal-module", + "license": "GPL-2.0+", + "minimum-stability": "dev", + "homepage": "https://drupal.org/project/eu_cookie_compliance", + "keywords": [ + "Drupal", + "Cookie", + "CookieCompliance", + "GDPR" + ], + "authors": [ + { + "name": "Marcin Pajdzik", + "homepage": "https://www.drupal.org/u/marcin-pajdzik", + "role": "Maintainer" + }, + { + "name": "Sven Berg Ryen", + "homepage": "https://www.drupal.org/u/svenryen", + "role": "Maintainer" + }, + { + "name": "See other contributors", + "homepage":"https://www.drupal.org/node/1538032/committers" + } + ], + "support": { + "docs": "https://www.drupal.org/project/eu_cookie_compliance", + "forum": "https://drupal.stackexchange.com/search?q=eu+cookie+compliance", + "issues": "https://www.drupal.org/project/issues/eu_cookie_compliance?version=8.x", + "irc": "irc://irc.freenode.org/drupal-contribute", + "source": "https://cgit.drupalcode.org/eu-cookie-compliance" + }, + "repositories": [ + { + "type": "composer", + "url": "https://packages.drupal.org/8" + } + ] +} diff --git a/web/modules/contrib/eu_cookie_compliance/config/install/eu_cookie_compliance.settings.yml b/web/modules/contrib/eu_cookie_compliance/config/install/eu_cookie_compliance.settings.yml index 36cb71bcd..7f57606bf 100644 --- a/web/modules/contrib/eu_cookie_compliance/config/install/eu_cookie_compliance.settings.yml +++ b/web/modules/contrib/eu_cookie_compliance/config/install/eu_cookie_compliance.settings.yml @@ -17,7 +17,7 @@ popup_clicking_confirmation: true popup_scrolling_confirmation: false popup_delay: 1000 show_disagree_button: true -popup_disagree_button_message: 'Give me more info' +popup_disagree_button_message: 'More info' popup_enabled: true popup_find_more_button_message: 'More info' popup_height: 0 @@ -43,3 +43,14 @@ cookie_name: '' exclude_uid_1: false better_support_for_screen_readers: false use_mobile_message: false +method: 'opt_in' +disagree_button_label: 'No, thanks' +disabled_javascripts: '' +whitelisted_cookies: '' +consent_storage_method: 'do_not_store' +withdraw_message: + value: '<h2>We use cookies on this site to enhance your user experience</h2><p>You have given your consent for us to set cookies.</p>' + format: 'restricted_html' +withdraw_action_button_label: 'Withdraw consent' +withdraw_tab_button_label: 'Privacy settings' +withdraw_enabled: false diff --git a/web/modules/contrib/eu_cookie_compliance/config/schema/eu_cookie_compliance.schema.yml b/web/modules/contrib/eu_cookie_compliance/config/schema/eu_cookie_compliance.schema.yml index 0cf99fa7b..155401a30 100644 --- a/web/modules/contrib/eu_cookie_compliance/config/schema/eu_cookie_compliance.schema.yml +++ b/web/modules/contrib/eu_cookie_compliance/config/schema/eu_cookie_compliance.schema.yml @@ -15,22 +15,16 @@ eu_cookie_compliance.settings: label: 'Consent by scrolling' eu_only: type: boolean - label: 'Only display banner in EU countries' + label: 'Only display banner in EU countries.' eu_only_js: type: boolean - label: 'Only display banner in EU countries (Javascript based)' + label: 'Only display banner in EU countries (Javascript based).' popup_position: type: boolean label: 'Place the banner at the top of the website' fixed_top_position: type: boolean - label: 'If the banner is at the top, don’t scroll the banner with the page' - popup_agree_button_message: - type: label - label: 'Agree button message' - popup_disagree_button_message: - type: label - label: 'Cookie Policy button message' + label: "If the banner is at the top, don't scroll the banner with the page." popup_info: type: mapping label: 'Cookie information banner message' @@ -51,21 +45,24 @@ eu_cookie_compliance.settings: format: type: string label: 'Cookie information banner message - mobile - Format' + popup_agree_button_message: + type: label + label: 'Agree button message' + popup_disagree_button_message: + type: label + label: 'Cookie Policy button message' mobile_breakpoint: type: integer label: 'Mobile breakpoint' popup_agreed_enabled: type: boolean - label: 'Enable “Thank you” banner' + label: 'Enable "Thank you" banner.' popup_hide_agreed: type: boolean - label: 'Clicking hides “Thank you” banner' - popup_find_more_button_message: - type: label - label: 'More info button label' - popup_hide_button_message: + label: 'Clicking hides "Thank you" banner.' + disagree_button_label: type: label - label: 'Hide button label' + label: 'Disagree button label' popup_agreed: type: mapping label: 'Thank you banner message' @@ -76,12 +73,18 @@ eu_cookie_compliance.settings: format: type: string label: 'Thank you banner message - Format' + popup_find_more_button_message: + type: label + label: 'More info button label' + popup_hide_button_message: + type: label + label: 'Hide button label' popup_link: type: label label: 'Privacy policy link' popup_link_new_window: type: boolean - label: 'Open privacy policy link in a new window' + label: 'Open privacy policy link in a new window.' popup_height: type: integer label: 'Banner height in pixels' @@ -96,10 +99,10 @@ eu_cookie_compliance.settings: label: 'Show "Cookie Policy" and "More info" buttons' popup_bg_hex: type: string - label: 'Background Color' + label: 'Background color' popup_text_hex: type: string - label: 'Text Color' + label: 'Text color' domain: type: string label: 'Domain' @@ -107,14 +110,17 @@ eu_cookie_compliance.settings: type: integer label: 'Add/Remove banner on specified domains' domains_list: - type: text + type: string label: 'Domains list' exclude_paths: - type: text + type: string label: 'Exclude paths' exclude_admin_theme: type: boolean - label: 'Exclude admin pages' + label: 'Exclude admin pages.' + cookie_session: + type: integer + label: 'Cookie session' cookie_lifetime: type: integer label: 'Cookie lifetime' @@ -126,16 +132,47 @@ eu_cookie_compliance.settings: label: 'Include minimal CSS, I want to style the overlay in the theme CSS' disagree_do_not_show_popup: type: boolean - label: 'Don’t show cookie policy when the user clicks the “Cookie Policy” button.' + label: 'Do not show cookie policy when the user clicks the "Cookie Policy" button.' reload_page: type: boolean - label: 'Reload page after user clicks the “Agree” button.' + label: 'Reload page after user clicks the "Agree" button.' cookie_name: type: string label: 'Cookie name' exclude_uid_1: type: boolean - label: 'Don’t show the banner for UID 1.' + label: "Don't show the banner for UID 1." better_support_for_screen_readers: type: boolean label: 'Let screen readers see the banner before other links on the page' + method: + type: string + label: 'Consent method' + disabled_javascripts: + type: string + label: 'Disable JavaScripts' + whitelisted_cookies: + type: string + label: 'Whitelisted cookies' + consent_storage_method: + type: string + label: 'Consent storage method' + withdraw_message: + type: mapping + label: 'Withdraw consent banner message' + mapping: + value: + type: text + label: 'Withdraw consent banner message - Value' + format: + type: string + label: 'Withdraw consent banner message - Format' + withdraw_tab_button_label: + type: label + label: 'Privacy settings tab label' + withdraw_action_button_label: + type: label + label: 'Withdraw consent action button label' + withdraw_enabled: + type: boolean + label: 'Enable floating privacy settings tab and withdraw consent banner' diff --git a/web/modules/contrib/eu_cookie_compliance/css/eu_cookie_compliance.bare.css b/web/modules/contrib/eu_cookie_compliance/css/eu_cookie_compliance.bare.css index 396d20927..5f78a0f96 100644 --- a/web/modules/contrib/eu_cookie_compliance/css/eu_cookie_compliance.bare.css +++ b/web/modules/contrib/eu_cookie_compliance/css/eu_cookie_compliance.bare.css @@ -29,9 +29,11 @@ float: left; } -.eu-cookie-compliance-agree-button, +.eu-cookie-compliance-default-button, +.eu-cookie-compliance-secondary-button, .eu-cookie-compliance-more-button, -.eu-cookie-compliance-hide-button { +.eu-cookie-compliance-hide-button, +.eu-cookie-withdraw-tab { cursor: pointer; } @@ -40,6 +42,22 @@ max-width: 60%; } +.eu-cookie-withdraw-wrapper.sliding-popup-top, +.eu-cookie-withdraw-wrapper.sliding-popup-top .eu-cookie-withdraw-banner { + transform: scaleY(-1); +} + +.eu-cookie-withdraw-tab { + position: absolute; + top: 0; + transform: translate(-50%, -100%); + left: 50%; +} + +.eu-cookie-withdraw-wrapper.sliding-popup-top .eu-cookie-withdraw-tab { + transform: translate(-50%, -100%) scaleY(-1); +} + [dir="rtl"] .eu-cookie-compliance-message { float: right; text-align: right; diff --git a/web/modules/contrib/eu_cookie_compliance/css/eu_cookie_compliance.css b/web/modules/contrib/eu_cookie_compliance/css/eu_cookie_compliance.css index e80940a5a..0af246dbe 100644 --- a/web/modules/contrib/eu_cookie_compliance/css/eu_cookie_compliance.css +++ b/web/modules/contrib/eu_cookie_compliance/css/eu_cookie_compliance.css @@ -38,9 +38,30 @@ float: left; } -.eu-cookie-compliance-agree-button, -.eu-cookie-compliance-more-button, -.eu-cookie-compliance-hide-button { +.eu-cookie-compliance-more-button { + cursor: pointer; + display: inline; + height: auto; + margin: 0; + padding: 0; + border: none; + text-decoration: underline; + background: none; +} + +.eu-cookie-compliance-secondary-button { + cursor: pointer; + border: none; + background: none; + margin-top: 1em; + padding: 0 8px; + vertical-align: middle; +} + +.eu-cookie-compliance-default-button, +.eu-cookie-compliance-hide-button, +.eu-cookie-compliance-more-button-thank-you, +.eu-cookie-withdraw-button { cursor: pointer; margin-right: 5px; margin-top: 1em; @@ -75,9 +96,10 @@ box-shadow: inset 0 0 0 1px #ffffff; } -.eu-cookie-compliance-agree-button:hover, -.eu-cookie-compliance-more-button:hover, -.eu-cookie-compliance-hide-button:hover { +.eu-cookie-compliance-default-button:hover, +.eu-cookie-compliance-hide-button:hover, +.eu-cookie-compliance-more-button-thank-you:hover, +.eu-cookie-withdraw-button:hover { background-color: #dfdfdf; background-image: -moz-linear-gradient(top, #dfdfdf 5%, #ededed 100%); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(5%, #dfdfdf), color-stop(100%, #ededed)); @@ -87,9 +109,10 @@ background-image: linear-gradient(to bottom, #dfdfdf 5%, #ededed 100%); } -.eu-cookie-compliance-agree-button:active, -.eu-cookie-compliance-more-button:active, -.eu-cookie-compliance-hide-button:active { +.eu-cookie-compliance-default-button:active, +.eu-cookie-compliance-hide-button:active, +.eu-cookie-compliance-more-button-thank-you:active, +.eu-cookie-withdraw-button:active { position: relative; top: 1px; } @@ -129,6 +152,38 @@ .eu-cookie-compliance-message p { font-size: 12px; + display: inline; +} + +.eu-cookie-withdraw-tab { + padding: 4px 7px; + cursor: pointer; +} + +.sliding-popup-bottom .eu-cookie-withdraw-tab { + border-width: 2px 2px 0; + border-radius: 5px 5px 0 0; +} + +.sliding-popup-top .eu-cookie-withdraw-tab { + border-width: 0 2px 2px; + border-radius: 0 0 5px 5px; +} + +.eu-cookie-withdraw-wrapper.sliding-popup-top, +.eu-cookie-withdraw-wrapper.sliding-popup-top .eu-cookie-withdraw-banner { + transform: scaleY(-1); +} + +.eu-cookie-withdraw-tab { + position: absolute; + top: 0; + transform: translate(-50%, -100%); + left: 50%; +} + +.eu-cookie-withdraw-wrapper.sliding-popup-top .eu-cookie-withdraw-tab { + transform: translate(-50%, -100%) scaleY(-1); } @media screen and (max-width: 600px) { diff --git a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.info.yml b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.info.yml index e05273c53..9272a38f6 100644 --- a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.info.yml +++ b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.info.yml @@ -5,10 +5,10 @@ package: User interface # core: 8.x configure: eu_cookie_compliance.settings dependencies: - - filter + - drupal:filter -# Information added by Drupal.org packaging script on 2018-04-01 -version: '8.x-1.0' +# Information added by Drupal.org packaging script on 2018-07-12 +version: '8.x-1.2' core: '8.x' project: 'eu_cookie_compliance' -datestamp: 1522572790 +datestamp: 1531429147 diff --git a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.install b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.install index 47c89c11b..974bd0bc6 100644 --- a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.install +++ b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.install @@ -6,11 +6,76 @@ */ use Drupal\user\Entity\Role; +use Drupal\Core\Database\Database; +use Drupal\filter\Entity\FilterFormat; + +/** + * Implements hook_schema(). + */ +function eu_cookie_compliance_schema() { + $schema['eu_cookie_compliance_basic_consent'] = [ + 'description' => 'Basic consent storage for EU Cookie Compliance / GDPR.', + 'fields' => [ + 'cid' => [ + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique consent storage ID.', + ], + 'uid' => [ + 'description' => '{users}.uid for user.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + 'timestamp' => [ + 'description' => 'Time of consent.', + 'type' => 'int', + 'unsigned' => FALSE, + 'not null' => TRUE, + 'default' => 0, + ], + 'ip_address' => [ + 'description' => 'The IP address.', + 'type' => 'varchar', + // Maximum length of an ipv6 IP address. + 'length' => 45, + 'not null' => TRUE, + 'default' => '', + ], + 'consent_type' => [ + 'description' => 'The type of consent, such as "banner" for the banner and form_id for forms.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + 'revision_id' => [ + 'description' => 'Revision of the privacy policy at the time of consent.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + ], + 'primary key' => ['cid'], + 'indexes' => [ + 'uid' => ['uid'], + ], + 'foreign keys' => [ + 'uid' => ['users' => 'uid'], + ], + ]; + + return $schema; +} /** * Implements hook_install(). */ function eu_cookie_compliance_install() { + module_load_include('module', 'eu_cookie_compliance', 'eu_cookie_compliance'); + $roles = Role::loadMultiple(); $permission = 'display eu cookie compliance popup'; foreach ($roles as $rid => $role) { @@ -24,26 +89,27 @@ function eu_cookie_compliance_install() { ->set('popup_link', $cookie_policy) ->save(); } + + eu_cookie_compliance_module_set_weight(); } /** * Implements hook_requirements(). */ function eu_cookie_compliance_requirements($phase) { - $requirements = array(); - $popup_link = Drupal::config('eu_cookie_compliance.settings')->get('popup_link'); - $show_policy = Drupal::config('eu_cookie_compliance.settings')->get('show_disagree_button'); - - if ($popup_link == '<front>' && $show_policy) { - $requirements['eu_cookie_compliance'] = array( - 'title' => t('EU Cookie Compliance'), - 'severity' => REQUIREMENT_ERROR, - 'description' => t('Your privacy policy link is pointing at the front page. This is the default value after installation, and unless your privacy policy is actually posted at the front page, you will need to create a separate page for the privacy policy and link to that page.'), - ); - if ($phase == 'runtime') { - $requirements['eu_cookie_compliance'] += array( - 'value' => t('Privacy Policy link not provided'), - ); + $requirements = []; + + if ($phase == 'runtime') { + $popup_link = Drupal::config('eu_cookie_compliance.settings')->get('popup_link'); + $show_policy = Drupal::config('eu_cookie_compliance.settings')->get('show_disagree_button'); + + if ($popup_link == '<front>' && $show_policy) { + $requirements['eu_cookie_compliance'] = [ + 'title' => t('EU Cookie Compliance'), + 'severity' => REQUIREMENT_WARNING, + 'description' => t('Your privacy policy link is pointing at the front page. This is the default value after installation, and unless your privacy policy is actually posted at the front page, you will need to create a separate page for the privacy policy and link to that page.'), + 'value' => t('Privacy policy link not provided'), + ]; } } @@ -111,3 +177,130 @@ function eu_cookie_compliance_update_8105() { ->set('show_disagree_button', TRUE) ->save(); } + +/** + * Create new config value to handle consent options. + */ +function eu_cookie_compliance_update_8106() { + \Drupal::configFactory() + ->getEditable('eu_cookie_compliance.settings') + ->set('method', 'default') + ->set('disagree_button_label', 'No, thanks') + ->set('disabled_javascripts', '') + ->set('whitelisted_cookies', '') + ->save(); +} + +/** + * Add table to handle basic consent. + */ +function eu_cookie_compliance_update_8107() { + $schema['eu_cookie_compliance_basic_consent'] = [ + 'description' => 'Basic consent storage for EU Cookie Compliance / GDPR.', + 'fields' => [ + 'cid' => [ + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique consent storage ID.', + ], + 'uid' => [ + 'description' => '{users}.uid for user.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + 'timestamp' => [ + 'description' => 'Time of consent.', + 'type' => 'int', + 'unsigned' => FALSE, + 'not null' => TRUE, + 'default' => 0, + ], + 'ip_address' => [ + 'description' => 'The IP address.', + 'type' => 'varchar', + // Maximum length of an ipv6 IP address. + 'length' => 45, + 'not null' => TRUE, + 'default' => '', + ], + 'consent_type' => [ + 'description' => 'The type of consent, such as "banner" for the banner and form_id for forms.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + 'revision_id' => [ + 'description' => 'Revision of the privacy policy at the time of consent.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + ], + 'primary key' => ['cid'], + 'indexes' => [ + 'uid' => ['uid'], + ], + 'foreign keys' => [ + 'uid' => ['users' => 'uid'], + ], + ]; + + Database::getConnection()->schema()->createTable('eu_cookie_compliance_basic_consent', $schema['eu_cookie_compliance_basic_consent']); + \Drupal::configFactory() + ->getEditable('eu_cookie_compliance.settings') + ->set('consent_storage_method', 'do_not_store') + ->save(); +} + +/** + * Change module weight to load after other modules, ensure all JSs are handled. + */ +function eu_cookie_compliance_update_8108() { + module_load_include('module', 'eu_cookie_compliance', 'eu_cookie_compliance'); + eu_cookie_compliance_module_set_weight(); +} + +/** + * Add config variables for the withdraw banner. + */ +function eu_cookie_compliance_update_8109() { + $default_filter_format = filter_default_format(); + $full_html_format = FilterFormat::load('full_html'); + if ($default_filter_format == 'restricted_html' && !empty($full_html_format) && $full_html_format->get('status')) { + $default_filter_format = 'full_html'; + } + + \Drupal::configFactory() + ->getEditable('eu_cookie_compliance.settings') + ->set('withdraw_message', [ + 'value' => '<h2>We use cookies on this site to enhance your user experience</h2><p>You have given your consent for us to set cookies.</p>', + 'format' => $default_filter_format, + ]) + ->set('withdraw_action_button_label', 'Withdraw consent') + ->set('withdraw_tab_button_label', 'Privacy settings') + ->set('withdraw_enabled', TRUE) + ->save(); +} + +/** + * Disable withdraw tab and banner in the consent method "Consent by default". + */ +function eu_cookie_compliance_update_8110() { + $withdraw_enabled = \Drupal::configFactory() + ->get('eu_cookie_compliance.settings') + ->get('withdraw_enabled'); + $method = \Drupal::configFactory() + ->get('eu_cookie_compliance.settings') + ->get('method'); + + if ($method == 'default' && $withdraw_enabled == 1) { + \Drupal::configFactory() + ->getEditable('eu_cookie_compliance.settings') + ->set('withdraw_enabled', FALSE) + ->save(); + } +} diff --git a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.module b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.module index e2421377b..b7e2aa719 100644 --- a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.module +++ b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.module @@ -16,6 +16,8 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\smart_ip\SmartIp; use Drupal\Core\Database\Database; +use Drupal\Core\Asset\AttachedAssetsInterface; +use Drupal\Component\Serialization\Json; /** * Implements hook_help(). @@ -25,15 +27,9 @@ function eu_cookie_compliance_help($route_name, RouteMatchInterface $route_match case 'help.page.eu_cookie_compliance': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('This module intends to deal with the EU Directive on Privacy and Electronic Communications that comes into effect on 26th May 2012. - From that date, if you are not compliant or visibly working towards compliance, - you run the risk of enforcement action, which can include a fine of up to - half a million pounds for a serious breach.') . '</p>'; + $output .= '<p>' . t('This module intends to deal with the EU Directive on Privacy and Electronic Communications that comes into effect on 26th May 2012. From that date, if you are not compliant or visibly working towards compliance, you run the risk of enforcement action, which can include a fine of up to half a million pounds for a serious breach.') . '</p>'; $output .= '<h3>' . t('How it works') . '</h3>'; - $output .= '<p>' . t('The module displays a banner at the bottom or the top of website to make users aware of the fact that cookies are being set. The user may then give - his/her consent or move to a page that provides more details. Consent is given - by user pressing the agree buttons or by continuing browsing the website. Once - consent is given another banner appears with a “Thank you” message.') . '</p>'; + $output .= '<p>' . t('The module displays a banner at the bottom or the top of website to make users aware of the fact that cookies are being set. The user may then give his/her consent or move to a page that provides more details. Consent is given by user pressing the agree buttons or by continuing browsing the website. Once consent is given another banner appears with a "Thank you" message.') . '</p>'; $output .= '<p>' . t('The module provides a settings page where the banner can be customized. There are also template files for the banners that can be overridden by your theme.') . '</p>'; $output .= '<h3>' . t('Installation') . '</h3>'; $output .= '<ol><p><li>' . t('Unzip the files to the "sites/all/modules" OR "modules" directory and enable the module.') . '</li></p>'; @@ -67,7 +63,7 @@ function eu_cookie_compliance_page_attachments(&$attachments) { if (!empty($config->get('domains_list'))) { global $base_url; - $domains_list = str_replace(array("\r\n", "\r"), "\n", $config->get('domains_list')); + $domains_list = str_replace(["\r\n", "\r"], "\n", $config->get('domains_list')); $domains_list = explode("\n", $domains_list); $domains_list = preg_replace('{/$}', '', $domains_list); $domain_match = in_array($base_url, $domains_list); @@ -85,8 +81,12 @@ function eu_cookie_compliance_page_attachments(&$attachments) { $path_match = FALSE; if (!empty($config->get('exclude_paths'))) { + // Check both the URL path and the URL alias against the list to exclude. $path = Drupal::service('path.current')->getPath(); + $url_alias_path = \Drupal::service('path.alias_manager')->getAliasByPath($path); $path_match = Drupal::service('path.matcher')->matchPath($path, $config->get('exclude_paths')); + $path_match_url_alias = Drupal::service('path.matcher')->matchPath($url_alias_path, $config->get('exclude_paths')); + $path_match = $path_match || $path_match_url_alias; $exclude_paths = $config->get('exclude_paths'); Drupal::moduleHandler()->alter('eu_cookie_compliance_path_match', $path_match, $path, $exclude_paths); } @@ -102,7 +102,7 @@ function eu_cookie_compliance_page_attachments(&$attachments) { } } - $geoip_match = TRUE; + $geoip_match = ['in_eu' => TRUE]; if (!empty($config->get('eu_only')) && $config->get('eu_only')) { $geoip_match = eu_cookie_compliance_user_in_eu(); } @@ -119,39 +119,93 @@ function eu_cookie_compliance_page_attachments(&$attachments) { $modules_allow_popup = TRUE; Drupal::moduleHandler()->alter('eu_cookie_compliance_show_popup', $modules_allow_popup); - if ($config->get('popup_enabled') && Drupal::currentUser()->hasPermission('display eu cookie compliance popup') && $geoip_match && $domain_allow && !$path_match && !$admin_theme_match && $uid1_match && $modules_allow_popup) { + if ($config->get('popup_enabled') && Drupal::currentUser()->hasPermission('display eu cookie compliance popup') && $geoip_match['in_eu'] && $domain_allow && !$path_match && !$admin_theme_match && $uid1_match && $modules_allow_popup) { $language = Drupal::languageManager()->getCurrentLanguage(); $data['css'] = ''; // Color overrides. if ($config->get('popup_bg_hex') !== '' && $config->get('popup_text_hex') !== '') { - $data['css'] = 'div#sliding-popup {background:#' . Html::escape($config->get('popup_bg_hex')) . '} #sliding-popup h1, #sliding-popup h2, #sliding-popup h3, #sliding-popup p { color:#' . Html::escape($config->get('popup_text_hex')) . ';}'; + $data['css'] = 'div#sliding-popup, div#sliding-popup .eu-cookie-withdraw-banner, .eu-cookie-withdraw-tab {background: #' . Html::escape($config->get('popup_bg_hex')) . '} div#sliding-popup.eu-cookie-withdraw-wrapper { background: transparent; } #sliding-popup h1, #sliding-popup h2, #sliding-popup h3, #sliding-popup p, .eu-cookie-compliance-more-button, .eu-cookie-compliance-secondary-button, .eu-cookie-withdraw-tab { color: #' . Html::escape($config->get('popup_text_hex')) . ';} .eu-cookie-withdraw-tab { border-color: #' . Html::escape($config->get('popup_text_hex')) . ';}'; } if (!empty($config->get('popup_position')) && $config->get('popup_position') && !empty($config->get('fixed_top_position')) && $config->get('fixed_top_position')) { $data['css'] .= '#sliding-popup.sliding-popup-top { position: fixed; }'; } - $popup_text_info = str_replace(array("\r", "\n"), '', $config->get('popup_info.value')); - $popup_text_agreed = str_replace(array("\r", "\n"), '', $config->get('popup_agreed.value')); - $html_info = array( + $method = $config->get('method'); + + if ($method == 'auto') { + $dnt = isset($_SERVER['HTTP_DNT']) ? $_SERVER['HTTP_DNT'] : NULL; + if ((int) $dnt === 0 && $dnt !== NULL) { + $method = 'default'; + } + else { + $method = 'opt_in'; + } + } + + switch ($method) { + case 'default': + $click_confirmation = $config->get('popup_clicking_confirmation'); + $scroll_confirmation = $config->get('popup_scrolling_confirmation'); + $primary_button_label = $config->get('popup_agree_button_message'); + $primary_button_class = 'agree-button eu-cookie-compliance-default-button'; + $secondary_button_label = ''; + $secondary_button_class = ''; + break; + + case 'opt_in': + $click_confirmation = FALSE; + $scroll_confirmation = FALSE; + $primary_button_label = $config->get('popup_agree_button_message'); + $primary_button_class = 'agree-button eu-cookie-compliance-secondary-button'; + $secondary_button_label = $config->get('disagree_button_label'); + $secondary_button_class = 'decline-button eu-cookie-compliance-default-button'; + break; + + case 'opt_out': + $click_confirmation = FALSE; + $scroll_confirmation = FALSE; + $primary_button_label = $config->get('disagree_button_label'); + $primary_button_class = 'decline-button eu-cookie-compliance-secondary-button'; + $secondary_button_label = $config->get('popup_agree_button_message'); + $secondary_button_class = 'agree-button eu-cookie-compliance-default-button'; + break; + } + + $popup_text_info = str_replace(["\r", "\n"], '', $config->get('popup_info.value')); + $popup_text_agreed = str_replace(["\r", "\n"], '', $config->get('popup_agreed.value')); + $withdraw_markup = str_replace(["\r", "\n"], '', $config->get('withdraw_message.value')); + $html_info = [ '#theme' => 'eu_cookie_compliance_popup_info', '#message' => check_markup($popup_text_info, $config->get('popup_info.format'), FALSE), - '#agree_button' => $config->get('popup_agree_button_message'), + '#agree_button' => $primary_button_label, '#disagree_button' => ($config->get('show_disagree_button') == TRUE) ? $config->get('popup_disagree_button_message') : FALSE, - ); - $mobile_popup_text_info = str_replace(array("\r", "\n"), '', $config->get('mobile_popup_info.value')); - $mobile_html_info = array( + '#secondary_button_label' => $secondary_button_label, + '#primary_button_class' => $primary_button_class, + '#secondary_button_class' => $secondary_button_class, + ]; + $mobile_popup_text_info = str_replace(["\r", "\n"], '', $config->get('mobile_popup_info.value')); + $mobile_html_info = [ '#theme' => 'eu_cookie_compliance_popup_info', '#message' => check_markup($mobile_popup_text_info, $config->get('popup_info.format'), FALSE), - '#agree_button' => $config->get('popup_agree_button_message'), + '#agree_button' => $primary_button_label, '#disagree_button' => ($config->get('show_disagree_button') == TRUE) ? $config->get('popup_disagree_button_message') : FALSE, - ); - $html_agreed = array( + '#secondary_button_label' => $secondary_button_label, + '#primary_button_class' => $primary_button_class, + '#secondary_button_class' => $secondary_button_class, + ]; + $html_agreed = [ '#theme' => 'eu_cookie_compliance_popup_agreed', '#message' => check_markup($popup_text_agreed, $config->get('popup_agreed.format'), FALSE), '#hide_button' => $config->get('popup_hide_button_message'), '#find_more_button' => ($config->get('show_disagree_button') == TRUE) ? $config->get('popup_find_more_button_message') : FALSE, - ); + ]; + $withdraw_markup = [ + '#theme' => 'eu_cookie_compliance_withdraw', + '#message' => check_markup($withdraw_markup, $config->get('withdraw_message.format'), FALSE), + '#withdraw_tab_button_label' => $config->get('withdraw_tab_button_label'), + '#withdraw_action_button_label' => $config->get('withdraw_action_button_label'), + ]; $was_debugging = FALSE; @@ -168,6 +222,7 @@ function eu_cookie_compliance_page_attachments(&$attachments) { $html_info = trim(Drupal::service('renderer')->renderRoot($html_info)->__toString()); $mobile_html_info = trim(Drupal::service('renderer')->renderRoot($mobile_html_info)->__toString()); $html_agreed = trim(Drupal::service('renderer')->renderRoot($html_agreed)->__toString()); + $withdraw_markup = trim(Drupal::service('renderer')->renderRoot($withdraw_markup)->__toString()); if ($was_debugging) { $twig_service->enableDebug(); @@ -178,17 +233,21 @@ function eu_cookie_compliance_page_attachments(&$attachments) { $popup_link = Url::fromUri($popup_link); } else { + // Guard against translations being entered without leading slash. + if (substr( $popup_link, 0, 1) != '/' && substr( $popup_link, 0, 1) != '?' && substr( $popup_link, 0, 1) != '#') { + $popup_link = '/' . $popup_link; + } $popup_link = $popup_link === '<front>' ? '/' : $popup_link; $popup_link = Url::fromUserInput($popup_link); } $popup_link = $popup_link->toString(); - $data['variables'] = array( + $data['variables'] = [ 'popup_enabled' => $config->get('popup_enabled'), 'popup_agreed_enabled' => $config->get('popup_agreed_enabled'), 'popup_hide_agreed' => $config->get('popup_hide_agreed'), - 'popup_clicking_confirmation' => $config->get('popup_clicking_confirmation'), - 'popup_scrolling_confirmation' => $config->get('popup_scrolling_confirmation'), + 'popup_clicking_confirmation' => $click_confirmation, + 'popup_scrolling_confirmation' => $scroll_confirmation, 'popup_html_info' => $config->get('popup_enabled') ? $html_info : FALSE, 'use_mobile_message' => !empty($config->get('use_mobile_message')) ? $config->get('use_mobile_message') : FALSE, 'mobile_popup_html_info' => $config->get('popup_enabled') ? $mobile_html_info : FALSE, @@ -202,14 +261,20 @@ function eu_cookie_compliance_page_attachments(&$attachments) { 'popup_link_new_window' => $config->get('popup_link_new_window'), 'popup_position' => $config->get('popup_position'), 'popup_language' => $language->getId(), + 'store_consent' => $config->get('consent_storage_method') != 'do_not_store', 'better_support_for_screen_readers' => !empty($config->get('better_support_for_screen_readers')) ? $config->get('better_support_for_screen_readers') : FALSE, - 'cookie_name' => !empty($config->get('cookie_name')) ? $config->get('cookie_name') : '', - 'reload_page' => !empty($config->get('reload_page')) ? $config->get('reload_page') : FALSE, + 'cookie_name' => !empty($config->get('cookie_name')) ? $config->get('cookie_name') : '', + 'reload_page' => !empty($config->get('reload_page')) ? $config->get('reload_page') : FALSE, 'domain' => $config->get('domain'), 'popup_eu_only_js' => !empty($config->get('eu_only_js')) ? $config->get('eu_only_js') : FALSE, 'cookie_lifetime' => $config->get('cookie_lifetime'), - 'disagree_do_not_show_popup' => !empty($config->get('disagree_do_not_show_popup')) ? $config->get('disagree_do_not_show_popup') : FALSE, - ); + 'cookie_session' => $config->get('cookie_session'), + 'disagree_do_not_show_popup' => !empty($config->get('disagree_do_not_show_popup')) ? $config->get('disagree_do_not_show_popup') : FALSE, + 'method' => $method, + 'whitelisted_cookies' => !empty($config->get('whitelisted_cookies')) ? $config->get('whitelisted_cookies') : '', + 'withdraw_markup' => $withdraw_markup, + 'withdraw_enabled' => $config->get('withdraw_enabled'), + ]; $attachments['#attached']['drupalSettings']['eu_cookie_compliance'] = $data['variables']; if ($config->get('use_bare_css')) { @@ -218,6 +283,31 @@ function eu_cookie_compliance_page_attachments(&$attachments) { else { $attachments['#attached']['library'][] = 'eu_cookie_compliance/eu_cookie_compliance'; } + // Add inline javascript. + $disabled_javascripts = $config->get('disabled_javascripts'); + $load_disabled_scripts = ''; + if ($disabled_javascripts != '') { + $load_disabled_scripts = ''; + $disabled_javascripts = _eu_cookie_compliance_explode_multiple_lines($disabled_javascripts); + foreach ($disabled_javascripts as $script) { + if (substr($script, 0, 4) !== 'http' && substr($script, 0, 2) !== '//') { + $script = '/' . $script; + } + $load_disabled_scripts .= 'var scriptTag = document.createElement("script");'; + $load_disabled_scripts .= 'scriptTag.src = ' . Json::encode($script) . ';'; + $load_disabled_scripts .= 'document.body.appendChild(scriptTag);'; + } + } + + $attachments['#attached']['html_head'][] = [ + [ + '#type' => 'html_tag', + '#tag' => 'script', + '#value' => 'function euCookieComplianceLoadScripts() {' . $load_disabled_scripts . '}', + ], + 'eu-cookie-compliance-js', + ]; + // Add inline css. $attachments['#attached']['html_head'][] = [ [ @@ -228,6 +318,27 @@ function eu_cookie_compliance_page_attachments(&$attachments) { ]; $cache_tags = isset($attachments['#cache']['tags']) ? $attachments['#cache']['tags'] : []; $attachments['#cache']['tags'] = Cache::mergeTags($cache_tags, $config->getCacheTags()); + + // Check if disabled scripts are in page html_head. + $disabled_javascripts = $config->get('disabled_javascripts'); + $disabled_javascripts = _eu_cookie_compliance_explode_multiple_lines($disabled_javascripts); + $disabled_javascripts = array_filter($disabled_javascripts); + + if (!empty($disabled_javascripts)) { + foreach ($attachments['#attached']['html_head'] as $index => $asset) { + $is_script = !empty($asset[0]['#type']) && $asset[0]['#type'] == 'html_tag' && $asset[0]['#tag'] == 'script'; + $is_src = !empty($asset[0]['#attributes']['src']); + if (!$is_script || !$is_src) { + continue; + } + $src = $asset[0]['#attributes']['src']; + $src = preg_replace('/\.js\?[^"]+/', '.js', $src); + $src = preg_replace('/^\//', '', $src); + if (in_array($src, $disabled_javascripts)) { + $attachments['#attached']['html_head'][$index][0]['#access'] = FALSE; + } + } + } } } @@ -235,24 +346,35 @@ function eu_cookie_compliance_page_attachments(&$attachments) { * Implements hook_theme(). */ function eu_cookie_compliance_theme($existing, $type, $theme, $path) { - return array( - 'eu_cookie_compliance_popup_info' => array( + return [ + 'eu_cookie_compliance_popup_info' => [ 'template' => 'eu_cookie_compliance_popup_info', - 'variables' => array( + 'variables' => [ 'message' => NULL, 'agree_button' => NULL, 'disagree_button' => NULL, - ), - ), - 'eu_cookie_compliance_popup_agreed' => array( + 'secondary_button_label' => NULL, + 'primary_button_class' => NULL, + 'secondary_button_class' => NULL, + ], + ], + 'eu_cookie_compliance_popup_agreed' => [ 'template' => 'eu_cookie_compliance_popup_agreed', - 'variables' => array( + 'variables' => [ 'message' => NULL, 'hide_button' => NULL, 'find_more_button' => NULL, - ), - ), - ); + ], + ], + 'eu_cookie_compliance_withdraw' => [ + 'template' => 'eu_cookie_compliance_withdraw', + 'variables' => [ + 'withdraw_tab_button_label' => NULL, + 'message' => NULL, + 'withdraw_action_button_label' => NULL, + ], + ], + ]; } /** @@ -263,9 +385,9 @@ function eu_cookie_compliance_theme($existing, $type, $theme, $path) { * @param \Drupal\Core\Form\FormStateInterface $form_state * Form State Interface. */ -function eu_cookie_compliance_validate_hex($element, FormStateInterface &$form_state) { +function eu_cookie_compliance_validate_hex(array $element, FormStateInterface &$form_state) { if (!empty($element['#value']) && !preg_match('/^[0-9a-fA-F]{3,6}$/', $element['#value'])) { - $form_state->setError($element, t('%name must be a HEX value (without leading #) or empty.', array('%name' => $element['#title']))); + $form_state->setError($element, t('%name must be a HEX value (without leading #) or empty.', ['%name' => $element['#title']])); } } @@ -274,11 +396,11 @@ function eu_cookie_compliance_validate_hex($element, FormStateInterface &$form_s */ function eu_cookie_compliance_user_in_eu() { $geoip_match = FALSE; - $eu_countries_default = array( + $eu_countries_default = [ NULL, 'BE', 'BG', 'CZ', 'DK', 'DE', 'EE', 'IE', 'EL', 'ES', 'FR', 'HR', 'IT', 'CY', 'LV', 'LT', 'LU', 'HU', 'MT', 'NL', 'AT', 'PL', 'PT', 'RO', 'SI', 'SK', 'FI', 'SE', 'UK', 'GB', 'NO', - ); + ]; // Allow custom array of countries to be loaded from settings.php, defaulting // to the array above. $config = Drupal::config('eu_cookie_compliance.settings'); @@ -295,10 +417,43 @@ function eu_cookie_compliance_user_in_eu() { $geoip_match = TRUE; } - return array( + return [ 'country' => $country_code, 'in_eu' => $geoip_match, - ); + ]; +} + +/** + * Implements hook_js_alter(). + */ +function eu_cookie_compliance_js_alter(&$javascript, AttachedAssetsInterface $assets) { + $config = Drupal::config('eu_cookie_compliance.settings'); + $disabled_javascripts = $config->get('disabled_javascripts'); + $disabled_javascripts = _eu_cookie_compliance_explode_multiple_lines($disabled_javascripts); + + foreach ($disabled_javascripts as $script) { + unset($javascript[$script]); + } +} + +/** + * Splits a return delimited text string into an array. + * + * @param string $text + * Text to split. + * + * @return array + * Text split into an array. + */ +function _eu_cookie_compliance_explode_multiple_lines($text) { + $text = explode("\r\n", $text); + if (count($text) == 1) { + $text = explode("\r", $text[0]); + } + if (count($text) == 1) { + $text = explode("\n", $text[0]); + } + return $text; } /** @@ -308,6 +463,10 @@ function eu_cookie_compliance_user_in_eu() { * URL to the node if found, otherwise FALSE. */ function _eu_cookie_compliance_find_privacy_policy() { + if (!\Drupal::entityTypeManager()->hasDefinition('node')) { + return FALSE; + } + $pattern = 'privacy|privacy +policy|cookie +policy|terms +of +use|terms +of +service|terms +and +conditions'; $connection = Database::getConnection(); @@ -334,3 +493,22 @@ function _eu_cookie_compliance_find_privacy_policy() { } return FALSE; } + +/** + * Helper function to set module weight. + */ +function eu_cookie_compliance_module_set_weight() { + $weight = 1; + $exclude_modules = [ + 'eu_cookie_compliance', + ]; + + $extension_config = \Drupal::configFactory()->get('core.extension'); + // Loop through all installed modules to find the highest weight. + foreach ($extension_config->get('module') as $module_name => $module_weight) { + if ($module_weight > $weight && !in_array($module_name, $exclude_modules)) { + $weight = $module_weight + 1; + } + } + module_set_weight('eu_cookie_compliance', $weight); +} diff --git a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.routing.yml b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.routing.yml index d6e1007b2..b80c65299 100644 --- a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.routing.yml +++ b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.routing.yml @@ -5,5 +5,12 @@ eu_cookie_compliance.settings: _title: 'EU Cookie Compliance' requirements: _permission: 'administer eu cookie compliance popup' +eu_cookie_compliance.store_consent: + path: '/eu-cookie-compliance/store_consent/{target}' + defaults: + _controller: \Drupal\eu_cookie_compliance\Controller\StoreConsent::store + _title: 'EU Cookie Compliance store consent' + requirements: + _access: 'TRUE' route_callbacks: - '\Drupal\eu_cookie_compliance\Routing\CheckIfEuCountryJs::routes' diff --git a/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.services.yml b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.services.yml new file mode 100644 index 000000000..db0c0a044 --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/eu_cookie_compliance.services.yml @@ -0,0 +1,4 @@ +services: + plugin.manager.eu_cookie_compliance.consent_storage: + class: Drupal\eu_cookie_compliance\Plugin\ConsentStorageManager + parent: default_plugin_manager diff --git a/web/modules/contrib/eu_cookie_compliance/js/eu_cookie_compliance.js b/web/modules/contrib/eu_cookie_compliance/js/eu_cookie_compliance.js index 8b6201f3f..bcb574ffb 100644 --- a/web/modules/contrib/eu_cookie_compliance/js/eu_cookie_compliance.js +++ b/web/modules/contrib/eu_cookie_compliance/js/eu_cookie_compliance.js @@ -14,7 +14,7 @@ // If configured, check JSON callback to determine if in EU. if (drupalSettings.eu_cookie_compliance.popup_eu_only_js) { if (Drupal.eu_cookie_compliance.showBanner()) { - var url = drupalSettings.path.baseUrl + 'eu-cookie-compliance-check'; + var url = drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + 'eu-cookie-compliance-check'; var data = {}; $.getJSON(url, data, function (data) { // If in the EU, show the compliance banner. @@ -51,7 +51,7 @@ } var status = Drupal.eu_cookie_compliance.getCurrentStatus(); - if (status === 0 || status === null) { + if ((status === 0 && drupalSettings.eu_cookie_compliance.method === 'default') || status === null) { if (!drupalSettings.eu_cookie_compliance.disagree_do_not_show_popup || status === null) { // Detect mobile here and use mobile_popup_html_info, if we have a mobile device. if (window.matchMedia('(max-width: ' + drupalSettings.eu_cookie_compliance.mobile_breakpoint + 'px)').matches && drupalSettings.eu_cookie_compliance.use_mobile_message) { @@ -65,12 +65,89 @@ } else if (status === 1 && drupalSettings.eu_cookie_compliance.popup_agreed_enabled) { Drupal.eu_cookie_compliance.createPopup(drupalSettings.eu_cookie_compliance.popup_html_agreed); Drupal.eu_cookie_compliance.attachHideEvents(); + } else if (status === 2 && drupalSettings.eu_cookie_compliance.withdraw_enabled) { + Drupal.eu_cookie_compliance.createWithdrawBanner(drupalSettings.eu_cookie_compliance.withdraw_markup); + Drupal.eu_cookie_compliance.attachWithdrawEvents(); } } catch (e) { } }; + Drupal.eu_cookie_compliance.createWithdrawBanner = function (html) { + var $html = $('<div></div>').html(html); + var $banner = $('.eu-cookie-withdraw-banner', $html); + $html.attr('id', 'sliding-popup'); + $html.addClass('eu-cookie-withdraw-wrapper'); + + if (!drupalSettings.eu_cookie_compliance.popup_use_bare_css) { + $banner.height(drupalSettings.eu_cookie_compliance.popup_height) + .width(drupalSettings.eu_cookie_compliance.popup_width); + } + $html.hide(); + var height = 0; + if (drupalSettings.eu_cookie_compliance.popup_position) { + $html.prependTo('body'); + height = $html.outerHeight(); + + $html.show() + .addClass('sliding-popup-top') + .addClass('clearfix') + .css({ top: -1 * height }); + // For some reason, the tab outerHeight is -10 if we don't use a timeout + // function to reveal the tab. + setTimeout(function () { + var height = $html.outerHeight(); + + $html.animate({ top: -1 * (height) }, drupalSettings.eu_cookie_compliance.popup_delay, null, function () { + $html.trigger('eu_cookie_compliance_popup_open'); + }); + }.bind($html), 0); + } else { + if (drupalSettings.eu_cookie_compliance.better_support_for_screen_readers) { + $html.prependTo('body'); + } else { + $html.appendTo('body'); + } + height = $html.outerHeight(); + $html.show() + .addClass('sliding-popup-bottom') + .css({ bottom: -1 * height }); + // For some reason, the tab outerHeight is -10 if we don't use a timeout + // function to reveal the tab. + setTimeout(function () { + var height = $html.outerHeight(); + + $html.animate({ bottom: -1 * (height) }, drupalSettings.eu_cookie_compliance.popup_delay, null, function () { + $html.trigger('eu_cookie_compliance_popup_open'); + }); + }.bind($html), 0); + } + }; + + Drupal.eu_cookie_compliance.toggleWithdrawBanner = function () { + var $wrapper = $('#sliding-popup'); + var $tab = $('.eu-cookie-withdraw-tab'); + var $bannerIsShowing = drupalSettings.eu_cookie_compliance.popup_position ? parseInt($wrapper.css('top')) === 0 : parseInt($wrapper.css('bottom')) === 0; + var height = $wrapper.outerHeight(); + if (drupalSettings.eu_cookie_compliance.popup_position) { + if ($bannerIsShowing) { + $wrapper.animate({'top' : -1 * (height)}, drupalSettings.eu_cookie_compliance.popup_delay); + } + else { + $wrapper.animate({'top' : 0}, drupalSettings.eu_cookie_compliance.popup_delay); + } + } + else { + if ($bannerIsShowing) { + $wrapper.animate({'bottom' : -1 * (height)}, drupalSettings.eu_cookie_compliance.popup_delay); + } + else { + $wrapper.animate({'bottom' : 0}, drupalSettings.eu_cookie_compliance.popup_delay); + } + } + }; + Drupal.eu_cookie_compliance.createPopup = function (html) { // This fixes a problem with jQuery 1.9. var popup = $('<div></div>').html(html); @@ -113,6 +190,7 @@ var scrollConfirms = drupalSettings.eu_cookie_compliance.popup_scrolling_confirmation; $('.agree-button').click(Drupal.eu_cookie_compliance.acceptAction); + $('.decline-button').click(Drupal.eu_cookie_compliance.declineAction); if (clickingConfirms) { $('a, input[type=submit], button[type=submit]').bind('click.euCookieCompliance', Drupal.eu_cookie_compliance.acceptAction); @@ -155,6 +233,11 @@ $('.find-more-button').not('.find-more-button-processed').addClass('find-more-button-processed').click(Drupal.eu_cookie_compliance.moreInfoAction); }; + Drupal.eu_cookie_compliance.attachWithdrawEvents = function () { + $('.eu-cookie-withdraw-button').click(Drupal.eu_cookie_compliance.withdrawAction); + $('.eu-cookie-withdraw-tab').click(Drupal.eu_cookie_compliance.toggleWithdrawBanner); + }; + Drupal.eu_cookie_compliance.acceptAction = function () { var agreedEnabled = drupalSettings.eu_cookie_compliance.popup_agreed_enabled; var nextStatus = 1; @@ -163,13 +246,37 @@ nextStatus = 2; } + if (!euCookieComplianceHasLoadedScripts) { + euCookieComplianceLoadScripts(); + } + + if (typeof euCookieComplianceBlockCookies !== 'undefined') { + clearInterval(euCookieComplianceBlockCookies); + } + Drupal.eu_cookie_compliance.changeStatus(nextStatus); }; + Drupal.eu_cookie_compliance.declineAction = function () { + Drupal.eu_cookie_compliance.setStatus(0); + let popup = $('#sliding-popup'); + if (popup.hasClass('sliding-popup-top')) { + popup.animate({ top: popup.outerHeight() * -1 }).trigger('eu_cookie_compliance_popup_close'); + } + else { + popup.animate({ bottom: popup.outerHeight() * -1 }).trigger('eu_cookie_compliance_popup_close'); + } + }; + + Drupal.eu_cookie_compliance.withdrawAction = function () { + Drupal.eu_cookie_compliance.setStatus(null); + location.reload(); + }; + Drupal.eu_cookie_compliance.moreInfoAction = function () { if (drupalSettings.eu_cookie_compliance.disagree_do_not_show_popup) { Drupal.eu_cookie_compliance.setStatus(0); - $('#sliding-popup').remove().trigger('eu_cookie_compliance_popup_close'); + $('#sliding-popup').trigger('eu_cookie_compliance_popup_close').remove(); } else { if (drupalSettings.eu_cookie_compliance.popup_link_new_window) { window.open(drupalSettings.eu_cookie_compliance.popup_link); @@ -203,7 +310,7 @@ $('#sliding-popup').html(drupalSettings.eu_cookie_compliance.popup_html_agreed).animate({ top: 0 }, drupalSettings.eu_cookie_compliance.popup_delay); Drupal.eu_cookie_compliance.attachHideEvents(); } else if (status === 1) { - $('#sliding-popup').remove().trigger('eu_cookie_compliance_popup_close'); + $('#sliding-popup').trigger('eu_cookie_compliance_popup_close').remove(); } }); } else { @@ -212,7 +319,7 @@ $('#sliding-popup').html(drupalSettings.eu_cookie_compliance.popup_html_agreed).animate({ bottom: 0 }, drupalSettings.eu_cookie_compliance.popup_delay); Drupal.eu_cookie_compliance.attachHideEvents(); } else if (status === 1) { - $('#sliding-popup').remove().trigger('eu_cookie_compliance_popup_close'); + $('#sliding-popup').trigger('eu_cookie_compliance_popup_close').remove(); } }); } @@ -221,6 +328,11 @@ location.reload(); } + if (value === 2 && drupalSettings.eu_cookie_compliance.withdraw_enabled) { + Drupal.eu_cookie_compliance.createWithdrawBanner(drupalSettings.eu_cookie_compliance.withdraw_markup); + Drupal.eu_cookie_compliance.attachWithdrawEvents(); + } + Drupal.eu_cookie_compliance.setStatus(value); }; @@ -236,9 +348,21 @@ } } - date.setDate(date.getDate() + parseInt(drupalSettings.eu_cookie_compliance.cookie_lifetime)); - $.cookie(cookieName, status, { expires: date, path: path, domain: domain }); + var cookie_session = parseInt(drupalSettings.eu_cookie_compliance.cookie_session); + if (cookie_session) { + $.cookie(cookieName, status, { path: path, domain: domain }); + } else { + var lifetime = parseInt(drupalSettings.eu_cookie_compliance.cookie_lifetime); + date.setDate(date.getDate() + lifetime); + $.cookie(cookieName, status, { expires: date, path: path, domain: domain }); + } $(document).trigger('eu_cookie_compliance.changeStatus', [status]); + + // Store consent if applicable. + if (drupalSettings.eu_cookie_compliance.store_consent && ((status === 1 && drupalSettings.eu_cookie_compliance.popup_agreed_enabled) || (status === 2 && !drupalSettings.eu_cookie_compliance.popup_agreed_enabled))) { + var url = drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + 'eu-cookie-compliance/store_consent/banner'; + $.post(url, {}, function (data) { }); + } }; Drupal.eu_cookie_compliance.hasAgreed = function () { @@ -249,7 +373,7 @@ Drupal.eu_cookie_compliance.showBanner = function () { var showBanner = false; var status = Drupal.eu_cookie_compliance.getCurrentStatus(); - if (status === 0 || status === null) { + if ((status === 0 && drupalSettings.eu_cookie_compliance.method === 'default') || status === null) { if (!drupalSettings.eu_cookie_compliance.disagree_do_not_show_popup || status === null) { showBanner = true; } @@ -270,8 +394,66 @@ return (cookieEnabled); }; - Drupal.eu_cookie_compliance.reloadPage = function () { + // Load blocked scripts if the user has agreed to being tracked. + var euCookieComplianceHasLoadedScripts = false; + $(function () { + if (Drupal.eu_cookie_compliance.hasAgreed() + || (Drupal.eu_cookie_compliance.getCurrentStatus() === null && drupalSettings.eu_cookie_compliance.method !== 'opt_in') + ) { + euCookieComplianceLoadScripts(); + euCookieComplianceHasLoadedScripts = true; + } + }); + + // Block cookies when the user hasn't agreed. + if ((drupalSettings.eu_cookie_compliance.method === 'opt_in' && (Drupal.eu_cookie_compliance.getCurrentStatus() === null || !Drupal.eu_cookie_compliance.hasAgreed())) + || (drupalSettings.eu_cookie_compliance.method === 'opt_out' && !Drupal.eu_cookie_compliance.hasAgreed() && Drupal.eu_cookie_compliance.getCurrentStatus() !== null) + ) { + // Split the white-listed cookies. + var euCookieComplianceWhitelist = drupalSettings.eu_cookie_compliance.whitelisted_cookies.split(/\r\n|\n|\r/g); + + // Add the EU Cookie Compliance cookie. + euCookieComplianceWhitelist.push((drupalSettings.eu_cookie_compliance.cookie_name === '') ? 'cookie-agreed' : drupalSettings.eu_cookie_compliance.cookie_name); + var euCookieComplianceBlockCookies = setInterval(function () { + // Load all cookies from jQuery. + var cookies = $.cookie(); + + // Check each cookie and try to remove it if it's not white-listed. + for (var i in cookies) { + var remove = true; + var hostname = window.location.hostname; + var cookieRemoved = false; + var index = 0; + + // Skip the PHP session cookie. + if (i.indexOf('SESS') === 0 || i.indexOf('SSESS') === 0) { + remove = false; + } - }; + // Check if the cookie is white-listed. + for (var item in euCookieComplianceWhitelist) { + if (i === euCookieComplianceWhitelist[item]) { + remove = false; + } + } + + // Remove the cookie if it's not white-listed. + if (remove) { + while (!cookieRemoved && hostname !== '') { + // Attempt to remove. + cookieRemoved = $.removeCookie(i, { domain: '.' + hostname, path: '/' }); + if (!cookieRemoved) { + cookieRemoved = $.removeCookie(i, { domain: hostname, path: '/' }); + } + + index = hostname.indexOf('.'); + + // We can be on a sub-domain, so keep checking the main domain as well. + hostname = (index === -1) ? '' : hostname.substring(index + 1); + } + } + } + }, 5000); + } })(jQuery, Drupal, drupalSettings); diff --git a/web/modules/contrib/eu_cookie_compliance/src/Annotation/ConsentStorage.php b/web/modules/contrib/eu_cookie_compliance/src/Annotation/ConsentStorage.php new file mode 100644 index 000000000..a64437723 --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/src/Annotation/ConsentStorage.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\eu_cookie_compliance\Annotation; + +use Drupal\Component\Annotation\Plugin; + +/** + * Defines a consent storage annotation object. + * + * Plugin Namespace: Plugin\ConsentStorage. + * + * For a working example, see + * \Drupal\eu_cookie_compliance\Plugin\ConsentStorage\BasicConsentStorage/registerConsent + * + * @see hook_eu_cookie_compliance_consent_storage_info_alter() + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageInterface + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageBase + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageManager + * @see plugin_api + * + * @Annotation + */ +class ConsentStorage extends Plugin { + + /** + * The plugin ID. + * + * @var string + */ + public $id; + + /** + * The human-readable name of the consent storage. + * + * @var \Drupal\Core\Annotation\Translation + * + * @ingroup plugin_translatable + */ + public $label; + + /** + * A brief description of the consent storage. + * + * This will be shown when adding or configuring this consent storage. + * + * @var \Drupal\Core\Annotation\Translation + * + * @ingroup plugin_translatable + */ + public $description = ''; + +} diff --git a/web/modules/contrib/eu_cookie_compliance/src/Controller/CheckIfEuCountryJsController.php b/web/modules/contrib/eu_cookie_compliance/src/Controller/CheckIfEuCountryJsController.php index a27b55a25..2d91f6f0d 100644 --- a/web/modules/contrib/eu_cookie_compliance/src/Controller/CheckIfEuCountryJsController.php +++ b/web/modules/contrib/eu_cookie_compliance/src/Controller/CheckIfEuCountryJsController.php @@ -15,6 +15,10 @@ class CheckIfEuCountryJsController extends ControllerBase { */ public function content() { $data = eu_cookie_compliance_user_in_eu(); + + // Allow other modules to alter the geo IP matching logic. + \Drupal::moduleHandler()->alter('eu_cookie_compliance_geoip_match', $data); + return new JsonResponse($data, 200, ['Cache-Control' => 'private']); } diff --git a/web/modules/contrib/eu_cookie_compliance/src/Controller/StoreConsent.php b/web/modules/contrib/eu_cookie_compliance/src/Controller/StoreConsent.php new file mode 100644 index 000000000..8d5ca4428 --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/src/Controller/StoreConsent.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\eu_cookie_compliance\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Symfony\Component\HttpFoundation\JsonResponse; + +/** + * Controller for JS call that stores consent. + */ +class StoreConsent extends ControllerBase { + + /** + * {@inheritdoc} + */ + public function store($target) { + // Get list of all plugins. + $consent_storages = \Drupal::service('plugin.manager.eu_cookie_compliance.consent_storage'); + // Get the currently active plugin. + $consent_storage_method = \Drupal::configFactory() + ->get('eu_cookie_compliance.settings') + ->get('consent_storage_method'); + // If we're not going to log consent, return NULL. + if (!$consent_storage_method || $consent_storage_method == 'do_not_store') { + return new JsonResponse(NULL); + } + + // Get plugin. + /* @var \Drupal\eu_cookie_compliance\Plugin\ConsentStorageInterface $consent_storage */ + $consent_storage = $consent_storages->createInstance($consent_storage_method); + // Register consent. + $result = $consent_storage->registerConsent($target); + // Return value. + return new JsonResponse($result); + } + +} diff --git a/web/modules/contrib/eu_cookie_compliance/src/Form/EuCookieComplianceConfigForm.php b/web/modules/contrib/eu_cookie_compliance/src/Form/EuCookieComplianceConfigForm.php index 728f358d2..d6f7a899d 100644 --- a/web/modules/contrib/eu_cookie_compliance/src/Form/EuCookieComplianceConfigForm.php +++ b/web/modules/contrib/eu_cookie_compliance/src/Form/EuCookieComplianceConfigForm.php @@ -12,6 +12,8 @@ use Drupal\Core\Routing\RequestContext; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\user\RoleStorageInterface; +use Drupal\filter\Entity\FilterFormat; +use Drupal\Core\Cache\Cache; /** * Provides settings for eu_cookie_compliance module. @@ -116,15 +118,27 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { $config = $this->config('eu_cookie_compliance.settings'); $default_filter_format = filter_default_format(); - if ($default_filter_format == 'restricted_html') { + $full_html_format = FilterFormat::load('full_html'); + if ($default_filter_format == 'restricted_html' && !empty($full_html_format) && $full_html_format->get('status')) { $default_filter_format = 'full_html'; } - $form['popup_enabled'] = array( + $consent_storages = \Drupal::service('plugin.manager.eu_cookie_compliance.consent_storage'); + $plugin_definitions = $consent_storages->getDefinitions(); + + $consent_storage_options = []; + $consent_storage_options['do_not_store'] = $this->t('Do not store'); + foreach ($plugin_definitions as $plugin_name => $plugin_definition) { + /* @var \Drupal\Core\StringTranslation\TranslatableMarkup $plugin_definition_name */ + $plugin_definition_name = $plugin_definition['name']; + $consent_storage_options[$plugin_name] = $plugin_definition_name->render(); + } + + $form['popup_enabled'] = [ '#type' => 'checkbox', '#title' => $this->t('Enable banner'), '#default_value' => $config->get('popup_enabled'), - ); + ]; // List of checkbox values. $role_names = []; @@ -137,6 +151,7 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { foreach ($this->getRoles() as $role_name => $role) { // Exclude Admin roles. + /* @var \Drupal\user\Entity\Role $role */ if (!$role->isAdmin()) { $role_names[$role_name] = $role->label(); // Fetch permissions for the roles. @@ -161,169 +176,356 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { '#default_value' => $role_values, ]; - $form['popup_message'] = array( + $form['consent_option'] = [ + '#type' => 'details', + '#title' => $this->t('Consent for processing of personal information'), + '#open' => TRUE, + ]; + + $form['consent_option']['info'] = [ + '#type' => 'markup', + '#markup' => $this->t("The EU General Data Protection Regulation (GDPR) (see <a href=\"https://www.eugdpr.org/\" target=\"_blank\">https://www.eugdpr.org/</a>) comes into enforcement from 25 May 2018 and introduces new requirements for web sites which handle information that can be used to identify individuals. The regulation underlines that consent must be <strong>unambiguous</strong> and involve a <strong>clear affirmative action</strong>. When evaluating how to best handle the requirements in the GDPR, remember that if you have a basic web site where the visitors don't log in, you always have the option to <strong>not process data that identifies individuals</strong>, in which case you may not need this module. Also note that GDPR applies to any electronic processing or storage of personal data that your organization may do, and simply installing a module may not be enough to become fully GDPR compliant."), + ]; + + $form['consent_option']['method'] = [ + '#type' => 'radios', + '#title' => $this->t('Consent method'), + '#options' => [ + 'default' => $this->t("Consent by default. Don't provide any option to opt out."), + 'opt_in' => $this->t("Opt-in. Don't track visitors unless they specifically give consent. (GDPR compliant)"), + 'opt_out' => $this->t('Opt-out. Track visitors by default, unless they choose to opt out.'), + 'auto' => $this->t('Automatic. Respect the DNT (Do not track) setting in the browser, if present. Uses opt-in when DNT is 1 or not set, and consent by default when DNT is 0.'), + ], + '#default_value' => $config->get('method'), + ]; + + $form['javascripts'] = [ + '#type' => 'details', + '#title' => $this->t("Disable the following JavaScripts when consent isn't given"), + '#open' => TRUE, + '#states' => [ + 'visible' => [ + "input[name='method']" => ['!value' => 'default'], + ], + ], + ]; + + $form['javascripts']['disabled_javascripts'] = [ + '#type' => 'textarea', + '#title' => $this->t('Disable JavaScripts'), + '#default_value' => $config->get('disabled_javascripts'), + '#description' => $this->t("Include the full path of JavaScripts, each on a separate line. When using the opt-in or opt-out consent options, you can block certain JavaScript files from being loaded when consent isn't given. The on-site JavaScripts should be written as root relative paths <strong>without the leading slash</strong>, and off-site JavaScripts should be written as complete URLs <strong>with the leading http(s)://</strong>. Note that after the user gives consent, the scripts will be executed in the order you enter here."), + ]; + + $form['cookies'] = [ + '#type' => 'details', + '#title' => $this->t('Cookie handling'), + '#open' => TRUE, + '#states' => [ + 'visible' => [ + "input[name='method']" => ['!value' => 'default'], + ], + ], + ]; + + $form['cookies']['whitelisted_cookies'] = [ + '#type' => 'textarea', + '#title' => $this->t('Whitelisted cookies'), + '#default_value' => $config->get('whitelisted_cookies'), + '#description' => $this->t("Include the name of cookies, each on a separate line. When using the opt-in or opt-out consent options, this module will <strong>prevent cookies that are not on the whitelist</strong> from being stored in the browser when consent isn't given. PHP session cookies and the cookie for this module are always whitelisted."), + ]; + + $form['consent_storage'] = [ + '#type' => 'details', + '#title' => $this->t('Store record of consent'), + '#open' => TRUE, + '#states' => [ + 'visible' => [ + "input[name='method']" => ['!value' => 'default'], + ], + ], + ]; + + $form['consent_storage']['info'] = [ + '#type' => 'markup', + '#markup' => $this->t('Depending on your implementation of GDPR, you may have to store a record when the user consents. This module comes with a basic consent storage plugin that writes a record to the database. Note that if your site has significant traffic, the basic consent storage may become a bottleneck, as every consent action will require a write to the database. You can easily create your own module with a ConsentStorage Plugin that extends ConsentStorageBase, using BasicConsentStorage from this module as a template. If you create a highly performant consent storage plugin, please consider contributing it back to the Drupal community as a contrib module.'), + ]; + + $form['consent_storage']['consent_storage_method'] = [ + '#type' => 'radios', + '#title' => $this->t('Consent storage method'), + '#default_value' => $config->get('consent_storage_method'), + '#options' => $consent_storage_options, + ]; + + $form['popup_message'] = [ '#type' => 'details', '#title' => $this->t('Cookie information banner'), '#open' => TRUE, - ); + ]; - $form['popup_message']['popup_clicking_confirmation'] = array( + $form['popup_message']['popup_clicking_confirmation'] = [ '#type' => 'checkbox', '#title' => $this->t('Consent by clicking'), '#default_value' => $config->get('popup_clicking_confirmation'), '#description' => $this->t('By default by clicking any link or button on the website the visitor accepts the cookie policy. Uncheck this box if you don’t require this functionality. You may want to edit the banner message below accordingly.'), - ); + '#states' => [ + 'visible' => [ + 'input[name="method"]' => ['value' => 'default'], + ], + ], + ]; - $form['popup_message']['popup_info'] = array( + $config_format = $config->get('popup_info.format'); + if (!empty($config_format)) { + $filter_format = FilterFormat::load($config_format); + if (empty($filter_format) || !$filter_format->get('status')) { + $config_format = $default_filter_format; + } + } + + $form['popup_message']['popup_info'] = [ '#type' => 'text_format', '#title' => $this->t('Cookie information banner message'), '#default_value' => $config->get('popup_info.value'), '#required' => TRUE, - '#format' => !empty($config->get('popup_info.format')) ? $config->get('popup_info.format') : $default_filter_format, - ); + '#format' => $config_format, + ]; - $form['popup_message']['use_mobile_message'] = array( + $form['popup_message']['use_mobile_message'] = [ '#type' => 'checkbox', - '#title' => $this->t('Use a different message for mobile phones'), + '#title' => $this->t('Use a different message for mobile phones.'), '#default_value' => !empty($config->get('use_mobile_message')) ? $config->get('use_mobile_message') : FALSE, - ); + ]; - $form['popup_message']['container'] = array( + $form['popup_message']['container'] = [ '#type' => 'container', - '#states' => array('visible' => array('input[name="use_mobile_message"]' => array('checked' => TRUE))), - ); + '#states' => ['visible' => ['input[name="use_mobile_message"]' => ['checked' => TRUE]]], + ]; + + $config_format = $config->get('mobile_popup_info.format'); + if (!empty($config_format)) { + $filter_format = FilterFormat::load($config_format); + if (empty($filter_format) || !$filter_format->get('status')) { + $config_format = $default_filter_format; + } + } - $form['popup_message']['container']['mobile_popup_info'] = array( + $form['popup_message']['container']['mobile_popup_info'] = [ '#type' => 'text_format', '#title' => $this->t('Cookie information banner message - mobile'), '#default_value' => $config->get('mobile_popup_info.value'), '#required' => FALSE, - '#format' => !empty($config->get('mobile_popup_info.format')) ? $config->get('mobile_popup_info.format') : $default_filter_format, - ); + '#format' => $config_format, + ]; - $form['popup_message']['mobile_breakpoint'] = array( + $form['popup_message']['mobile_breakpoint'] = [ '#type' => 'number', '#title' => $this->t('Mobile breakpoint'), '#default_value' => !empty($config->get('mobile_breakpoint')) ? $config->get('mobile_breakpoint') : '768', - '#field_suffix' => ' ' . $this->t('pixels'), + '#field_suffix' => $this->t('px'), '#size' => 4, '#maxlength' => 4, '#required' => FALSE, '#description' => $this->t('The mobile message will be used when the window width is below or equal to the given value.'), - '#states' => array( - 'visible' => array( - "input[name='use_mobile_message']" => array('checked' => TRUE), - ), - ), - ); + '#states' => [ + 'visible' => [ + "input[name='use_mobile_message']" => ['checked' => TRUE], + ], + ], + ]; - $form['popup_message']['popup_agree_button_message'] = array( + $form['popup_message']['popup_agree_button_message'] = [ '#type' => 'textfield', '#title' => $this->t('Agree button label'), '#default_value' => $config->get('popup_agree_button_message'), '#size' => 30, '#required' => TRUE, - ); + ]; $form['popup_message']['disagree_button'] = [ '#type' => 'checkbox', - '#title' => $this->t('Show “Cookie Policy” and “More info” buttons'), - '#description' => $this->t('If this option is checked, the cookie policy button will be shown on the site. Disabling this option will hide both the “Cookie Policy” button on the information banner and the “More info” button on the “Thank you” banner.'), + '#title' => $this->t('Show "Cookie Policy" and "More info" buttons'), + '#description' => $this->t('If this option is checked, the cookie policy button will be shown on the site. Disabling this option will hide both the "Cookie Policy" button on the information banner and the "More info" button on the "Thank you" banner.'), '#default_value' => $config->get('show_disagree_button'), + '#states' => [ + 'visible' => [ + "input[name='method']" => ['value' => 'default'], + ], + ], ]; - $form['popup_message']['popup_disagree_button_message'] = array( + $form['popup_message']['popup_disagree_button_message'] = [ '#type' => 'textfield', '#title' => $this->t('Cookie policy button label'), '#default_value' => $config->get('popup_disagree_button_message'), '#size' => 30, '#states' => [ 'visible' => [ - 'input[name="disagree_button"]' => ['checked' => TRUE], + ['input[name="disagree_button"]' => ['checked' => TRUE]], + ['input[name="method"]' => ['!value' => 'default']], ], 'required' => [ - 'input[name="disagree_button"]' => ['checked' => TRUE], + ['input[name="disagree_button"]' => ['checked' => TRUE]], + ['input[name="method"]' => ['!value' => 'default']], ], ], - ); + ]; + + $form['popup_message']['disagree_button_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Disagree button label'), + '#default_value' => $config->get('disagree_button_label'), + '#size' => 30, + '#states' => [ + 'visible' => [ + 'input[name="method"]' => ['!value' => 'default'], + ], + 'required' => [ + 'input[name="method"]' => ['!value' => 'default'], + ], + ], + ]; + + $form['withdraw_consent'] = [ + '#type' => 'details', + '#title' => $this->t('Withdraw consent'), + '#open' => TRUE, + '#states' => [ + 'visible' => [ + "input[name='method']" => ['!value' => 'default'], + ], + ], + ]; + + $form['withdraw_consent']['info'] = [ + '#type' => 'markup', + '#markup' => t('GDPR requires that withdrawing consent for handling personal information should be as easy as giving consent. This module offers a tab button that when clicked brings up a message and a button that can be used to withdraw consent.'), + ]; + + $form['withdraw_consent']['withdraw_enabled'] = [ + '#type' => 'checkbox', + '#title' => t('Enable floating privacy settings tab and withdraw consent banner'), + '#default_value' => $config->get('withdraw_enabled'), + ]; + + $config_format = $config->get('popup_info.format'); + if (!empty($config_format)) { + $filter_format = FilterFormat::load($config_format); + if (empty($filter_format) || !$filter_format->get('status')) { + $config_format = $default_filter_format; + } + } + + $form['withdraw_consent']['withdraw_message'] = [ + '#type' => 'text_format', + '#title' => t('Withdraw consent banner message'), + '#default_value' => isset($config->get('withdraw_message')['value']) ? $config->get('withdraw_message')['value'] : '', + '#description' => t('Text that will be displayed in the banner that appears when the privacy settings tab is clicked.'), + '#format' => $config_format, + ]; - $form['thank_you'] = array( + $form['withdraw_consent']['withdraw_tab_button_label'] = [ + '#type' => 'textfield', + '#title' => t('Privacy settings tab label'), + '#default_value' => $config->get('withdraw_tab_button_label'), + '#description' => t('Tab button that reveals/hides the withdraw message and action button when clicked.'), + ]; + + $form['withdraw_consent']['withdraw_action_button_label'] = [ + '#type' => 'textfield', + '#title' => t('Withdraw consent action button label'), + '#default_value' => $config->get('withdraw_action_button_label'), + '#description' => t('This button will withdraw consent when clicked.'), + ]; + + $form['thank_you'] = [ '#type' => 'details', '#open' => TRUE, '#title' => $this->t('Thank you banner'), - ); + ]; - $form['thank_you']['popup_agreed_enabled'] = array( + $form['thank_you']['popup_agreed_enabled'] = [ '#type' => 'checkbox', - '#title' => $this->t('Enable “Thank you” banner'), + '#title' => $this->t('Enable "Thank you" banner'), '#default_value' => $config->get('popup_agreed_enabled'), - ); + ]; - $form['thank_you']['popup_hide_agreed'] = array( + $form['thank_you']['popup_hide_agreed'] = [ '#type' => 'checkbox', - '#title' => $this->t('Clicking hides “Thank you” banner'), + '#title' => $this->t('Clicking hides "Thank you" banner.'), '#default_value' => $config->get('popup_hide_agreed'), - '#description' => $this->t('Clicking a link or button hides the “Thank you” message automatically.'), - ); + '#description' => $this->t('Clicking a link or button hides the "Thank you" message automatically.'), + ]; + + $config_format = $config->get('popup_info.format'); + if (!empty($config_format)) { + $filter_format = FilterFormat::load($config_format); + if (empty($filter_format) || !$filter_format->get('status')) { + $config_format = $default_filter_format; + } + } - $form['thank_you']['popup_agreed'] = array( + $form['thank_you']['popup_agreed'] = [ '#type' => 'text_format', - '#title' => $this->t('“Thank you” banner message'), + '#title' => $this->t('"Thank you" banner message'), '#default_value' => !empty($config->get('popup_agreed')['value']) ? $config->get('popup_agreed')['value'] : '', '#required' => TRUE, - '#format' => !empty($config->get('popup_agreed')['format']) ? $config->get('popup_agreed')['format'] : $default_filter_format, - ); + '#format' => $config_format, + ]; - $form['thank_you']['popup_find_more_button_message'] = array( + $form['thank_you']['popup_find_more_button_message'] = [ '#type' => 'textfield', '#title' => $this->t('More info button label'), '#default_value' => $config->get('popup_find_more_button_message'), '#size' => 30, '#states' => [ 'visible' => [ - 'input[name="disagree_button"]' => ['checked' => TRUE], + ['input[name="disagree_button"]' => ['checked' => TRUE]], + ['input[name="method"]' => ['!value' => 'default']], ], 'required' => [ - 'input[name="disagree_button"]' => ['checked' => TRUE], + ['input[name="disagree_button"]' => ['checked' => TRUE]], + ['input[name="method"]' => ['!value' => 'default']], ], ], - ); + ]; - $form['thank_you']['popup_hide_button_message'] = array( + $form['thank_you']['popup_hide_button_message'] = [ '#type' => 'textfield', '#title' => $this->t('Hide button label'), '#default_value' => $config->get('popup_hide_button_message'), '#size' => 30, '#required' => TRUE, - ); + ]; - $form['privacy'] = array( + $form['privacy'] = [ '#type' => 'details', '#open' => TRUE, - '#title' => $this->t('Privacy Policy'), - ); + '#title' => $this->t('Privacy policy'), + ]; - $form['privacy']['popup_link'] = array( + $form['privacy']['popup_link'] = [ '#type' => 'textfield', '#title' => $this->t('Privacy policy link'), '#default_value' => $config->get('popup_link'), '#maxlength' => 1024, '#required' => TRUE, '#description' => $this->t('Enter link to your privacy policy or other page that will explain cookies to your users, external links should start with http:// or https://.'), - '#element_validate' => array(array($this, 'validatePopupLink')), - ); + '#element_validate' => [[$this, 'validatePopupLink']], + ]; - $form['privacy']['popup_link_new_window'] = array( + $form['privacy']['popup_link_new_window'] = [ '#type' => 'checkbox', - '#title' => $this->t('Open privacy policy link in a new window'), + '#title' => $this->t('Open privacy policy link in a new window.'), '#default_value' => $config->get('popup_link_new_window'), - ); + ]; - $form['appearance'] = array( + $form['appearance'] = [ '#type' => 'details', '#open' => TRUE, '#title' => $this->t('Appearance'), - ); + ]; $form_color_picker_type = 'textfield'; @@ -338,224 +540,254 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { $popup_position_value = ($config->get('popup_position') === TRUE ? 'top' : ($config->get('popup_position') === FALSE ? 'bottom' : $config->get('popup_position'))); - $form['appearance']['popup_position'] = array( + $form['appearance']['popup_position'] = [ '#type' => 'radios', '#title' => $this->t('Position'), '#default_value' => $popup_position_value, '#options' => $popup_position_options, - ); + ]; - $form['appearance']['use_bare_css'] = array( + $form['appearance']['use_bare_css'] = [ '#type' => 'checkbox', '#title' => $this->t('Include minimal CSS, I want to style the banner in the theme CSS.'), '#default_value' => !empty($config->get('use_bare_css')) ? $config->get('use_bare_css') : 0, '#description' => $this->t('This may be useful if you want the banner to share the button style of your theme. Note that you will have to configure values like the banner width, text color and background color in your CSS file.'), - ); + ]; - $form['appearance']['popup_text_hex'] = array( + $form['appearance']['popup_text_hex'] = [ '#type' => $form_color_picker_type, '#title' => $this->t('Text color'), '#default_value' => $config->get('popup_text_hex'), '#description' => $this->t('Change the text color of the banner. Provide HEX value without the #.'), - '#element_validate' => array('eu_cookie_compliance_validate_hex'), - '#states' => array( - 'visible' => array( - "input[name='use_bare_css']" => array('checked' => FALSE), - ), - ), - ); + '#element_validate' => ['eu_cookie_compliance_validate_hex'], + '#states' => [ + 'visible' => [ + "input[name='use_bare_css']" => ['checked' => FALSE], + ], + ], + ]; - $form['appearance']['popup_bg_hex'] = array( + $form['appearance']['popup_bg_hex'] = [ '#type' => $form_color_picker_type, '#title' => $this->t('Background color'), '#default_value' => $config->get('popup_bg_hex'), '#description' => $this->t('Change the background color of the banner. Provide HEX value without the #.'), - '#element_validate' => array('eu_cookie_compliance_validate_hex'), - '#states' => array( - 'visible' => array( - "input[name='use_bare_css']" => array('checked' => FALSE), - ), - ), - ); + '#element_validate' => ['eu_cookie_compliance_validate_hex'], + '#states' => [ + 'visible' => [ + "input[name='use_bare_css']" => ['checked' => FALSE], + ], + ], + ]; - $form['appearance']['popup_height'] = array( + $form['appearance']['popup_height'] = [ '#type' => 'number', - '#title' => $this->t('Banner height in pixels'), + '#title' => $this->t('Banner height'), '#default_value' => !empty($config->get('popup_height')) ? $config->get('popup_height') : '', - '#field_suffix' => ' ' . $this->t('pixels'), + '#field_suffix' => $this->t('px'), '#size' => 5, '#maxlength' => 5, '#required' => FALSE, '#description' => $this->t('Enter an integer value for a desired height in pixels or leave empty for automatically adjusted height.'), - '#states' => array( - 'visible' => array( - "input[name='use_bare_css']" => array('checked' => FALSE), - ), - ), - ); + '#states' => [ + 'visible' => [ + "input[name='use_bare_css']" => ['checked' => FALSE], + ], + ], + ]; - $form['appearance']['popup_width'] = array( + $form['appearance']['popup_width'] = [ '#type' => 'textfield', '#title' => $this->t('Banner width in pixels or a percentage value'), '#default_value' => $config->get('popup_width'), - '#field_suffix' => ' ' . $this->t('px or %'), + '#field_suffix' => $this->t('px or %'), '#size' => 5, '#maxlength' => 5, '#description' => $this->t('Set the width of the banner. This can be either an integer value or percentage of the screen width. For example: 200 or 50%.'), - '#states' => array( - 'visible' => array("input[name='use_bare_css']" => array('checked' => FALSE)), - 'required' => array("input[name='use_bare_css']" => array('checked' => FALSE)), - ), - ); + '#states' => [ + 'visible' => ["input[name='use_bare_css']" => ['checked' => FALSE]], + 'required' => ["input[name='use_bare_css']" => ['checked' => FALSE]], + ], + ]; - $form['eu_only'] = array( + $form['eu_only'] = [ '#type' => 'details', '#open' => TRUE, '#title' => t('EU countries'), - ); + ]; if ($this->moduleHandler->moduleExists('smart_ip') || extension_loaded('geoip')) { - $form['eu_only']['eu_only'] = array( + $form['eu_only']['eu_only'] = [ '#type' => 'checkbox', - '#title' => $this->t('Only display banner in EU countries'), + '#title' => $this->t('Only display banner in EU countries.'), '#default_value' => !empty($config->get('eu_only')) ? $config->get('eu_only') : 0, '#description' => $this->t('You can limit the number of countries for which the banner is displayed by checking this option. If you want to provide a list of countries other than current EU states, you may place an array in <code>$config[\'eu_cookie_compliance.settings\'][\'eu_countries\']</code> in your <code>settings.php</code> file. Using the <a href="http://drupal.org/project/smart_ip">smart_ip</a> module or the <a href="http://www.php.net/manual/en/function.geoip-country-code-by-name.php">geoip_country_code_by_name()</a> PHP function.'), - ); - $form['eu_only']['eu_only_js'] = array( + ]; + $form['eu_only']['eu_only_js'] = [ '#type' => 'checkbox', - '#title' => $this->t('JavaScript-based (for Varnish): Only display banner in EU countries'), + '#title' => $this->t('JavaScript-based (for Varnish): Only display banner in EU countries.'), '#default_value' => !empty($config->get('eu_only_js')) ? $config->get('eu_only_js') : 0, '#description' => $this->t('This option also works for visitors that bypass Varnish. You can limit the number of countries for which the banner is displayed by checking this option. If you want to provide a list of countries other than current EU states, you may place an array in <code>$config[\'eu_cookie_compliance.settings\'][\'eu_countries\']</code> in your <code>settings.php</code> file. Using the <a href="http://drupal.org/project/smart_ip">smart_ip</a> module or the <a href="http://www.php.net/manual/en/function.geoip-country-code-by-name.php">geoip_country_code_by_name()</a> PHP function.'), - ); + ]; } else { - $form['eu_only']['info'] = array( + $form['eu_only']['info'] = [ '#markup' => t('You can choose to show the banner only to visitors from EU countries. In order to achieve this, you need to install the <a href="http://drupal.org/project/smart_ip">smart_ip</a> module or enable the <a href="http://www.php.net/manual/en/function.geoip-country-code-by-name.php">geoip_country_code_by_name()</a> PHP function.'), - ); + ]; } - $form['advanced'] = array( + $form['advanced'] = [ '#type' => 'details', '#open' => FALSE, '#title' => $this->t('Advanced'), - ); + ]; - $form['advanced']['fixed_top_position'] = array( + $form['advanced']['fixed_top_position'] = [ '#type' => 'checkbox', - '#title' => $this->t('If the banner is at the top, don’t scroll the banner with the page'), + '#title' => $this->t("If the banner is at the top, don't scroll the banner with the page."), '#default_value' => $config->get('fixed_top_position'), '#description' => $this->t('Use position:fixed for the banner when displayed at the top.'), - ); + ]; - $form['advanced']['popup_delay'] = array( + $form['advanced']['popup_delay'] = [ '#type' => 'number', - '#title' => $this->t('Banner sliding animation time in milliseconds'), + '#title' => $this->t('Banner sliding animation time'), '#default_value' => $config->get('popup_delay'), - '#field_suffix' => ' ' . $this->t('milliseconds'), + '#field_suffix' => $this->t('ms'), '#size' => 5, '#maxlength' => 5, '#required' => TRUE, - ); + ]; - $form['advanced']['disagree_do_not_show_popup'] = array( + $form['advanced']['disagree_do_not_show_popup'] = [ '#type' => 'checkbox', - '#title' => $this->t('Don’t show cookie policy when the user clicks the “Cookie Policy” button.'), + '#title' => $this->t('Do not show cookie policy when the user clicks the "Cookie Policy" button.'), '#default_value' => !empty($config->get('disagree_do_not_show_popup')) ? $config->get('disagree_do_not_show_popup') : 0, '#description' => $this->t('Enabling this will make it possible to record the fact that the user disagrees without the user having to see the privacy policy.'), - ); + ]; - $form['advanced']['reload_page'] = array( + $form['advanced']['reload_page'] = [ '#type' => 'checkbox', - '#title' => $this->t('Reload page after user clicks the “Agree” button.'), + '#title' => $this->t('Reload page after user clicks the "Agree" button.'), '#default_value' => !empty($config->get('reload_page')) ? $config->get('reload_page') : 0, - ); + ]; - $form['advanced']['popup_scrolling_confirmation'] = array( + $form['advanced']['popup_scrolling_confirmation'] = [ '#type' => 'checkbox', '#title' => $this->t('Consent by scrolling'), '#default_value' => $config->get('popup_scrolling_confirmation'), '#description' => $this->t('Scrolling makes the visitors to accept the cookie policy. In some countries, like Italy, it is permitted.'), - ); + '#states' => [ + 'visible' => [ + ['input[name="method"]' => ['value' => 'default']], + ], + ], + ]; - $form['advanced']['cookie_name'] = array( + $form['advanced']['cookie_name'] = [ '#type' => 'textfield', '#title' => $this->t('Cookie name'), '#default_value' => !empty($config->get('cookie_name')) ? $config->get('cookie_name') : '', - '#description' => $this->t('Sets the cookie name that is used to check whether the user has agreed or not. This option is useful when policies change and the user needs to agree again.'), - ); + '#description' => $this->t('Sets the cookie name that is used to check whether the user has agreed or not. This option is useful when policies change and the user needs to agree again.'), + ]; // Adding option to add/remove banner on specified domains. - $exclude_domains_option_active = array( + $exclude_domains_option_active = [ 0 => $this->t('Add'), 1 => $this->t('Remove'), - ); + ]; - $form['advanced']['domains_option'] = array( + $form['advanced']['domains_option'] = [ '#type' => 'radios', '#title' => $this->t('Add/remove banner on specified domains'), '#default_value' => $config->get('domains_option'), '#options' => $exclude_domains_option_active, '#description' => $this->t('Specify if you want to add or remove banner on the listed below domains.'), - ); + ]; - $form['advanced']['domains_list'] = array( + $form['advanced']['domains_list'] = [ '#type' => 'textarea', '#title' => $this->t('Domains list'), '#default_value' => $config->get('domains_list'), '#description' => $this->t('Specify domains with protocol (e.g., http or https). Enter one domain per line.'), - ); + ]; - $form['advanced']['exclude_paths'] = array( + $form['advanced']['exclude_paths'] = [ '#type' => 'textarea', '#title' => $this->t('Exclude paths'), '#default_value' => !empty($config->get('exclude_paths')) ? $config->get('exclude_paths') : '', - '#description' => $this->t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page.", array( + '#description' => $this->t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page.", [ '%blog' => '/blog', '%blog-wildcard' => '/blog/*', '%front' => '<front>', - )), - ); + ]), + ]; - $form['advanced']['exclude_admin_theme'] = array( + $form['advanced']['exclude_admin_theme'] = [ '#type' => 'checkbox', - '#title' => $this->t('Exclude admin pages'), + '#title' => $this->t('Exclude admin pages.'), '#default_value' => $config->get('exclude_admin_theme'), - ); + ]; - $form['advanced']['exclude_uid_1'] = array( + $form['advanced']['exclude_uid_1'] = [ '#type' => 'checkbox', - '#title' => $this->t('Don’t show the banner for UID 1.'), + '#title' => $this->t("Don't show the banner for UID 1."), '#default_value' => !empty($config->get('exclude_uid_1')) ? $config->get('exclude_uid_1') : 0, - ); + ]; - $form['advanced']['better_support_for_screen_readers'] = array( + $form['advanced']['better_support_for_screen_readers'] = [ '#type' => 'checkbox', '#title' => $this->t('Let screen readers see the banner before other links on the page.'), '#default_value' => !empty($config->get('better_support_for_screen_readers')) ? $config->get('better_support_for_screen_readers') : 0, '#description' => $this->t('Enable this if you want to place the banner as the first HTML element on the page. This will make it possible for screen readers to close the banner without tabbing through all links on the page.'), - ); + ]; - $form['advanced']['domain'] = array( + $form['advanced']['domain'] = [ '#type' => 'textfield', '#title' => $this->t('Domain'), '#default_value' => $config->get('domain'), '#description' => $this->t('Sets the domain of the cookie to a specific url. Used when you need consistency across domains. This is language independent. Note: Make sure you actually enter a domain that the browser can make use of. For example if your site is accessible at both www.domain.com and domain.com, you will not be able to hide the banner at domain.com if your value for this field is www.domain.com.'), - ); + ]; + + $form['advanced']['cookie_session'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Prompt for consent (from the same user) at every new browser session.'), + '#description' => $this->t("This sets cookie lifetime to 0, invalidating the cookie at the end of the browser session. To set a cookie lifetime greater than 0, uncheck this option. Note that some users will find this behavior highly annoying, and it's recommended to double-check with the legal advisor whether you really need this option enabled."), + '#default_value' => $config->get('cookie_session'), + ]; - $form['advanced']['cookie_lifetime'] = array( + $form['advanced']['cookie_lifetime'] = [ '#type' => 'number', '#title' => $this->t('Cookie lifetime'), - '#description' => $this->t("How long does the system remember the user's choice, in days."), + '#description' => $this->t("How many days the system remember the user's choice."), '#default_value' => $config->get('cookie_lifetime'), - '#field_suffix' => ' ' . $this->t('days'), + '#field_suffix' => $this->t('days'), '#size' => 5, '#maxlength' => 5, '#required' => TRUE, - ); + '#states' => [ + 'enabled' => [ + "input[name='cookie_session']" => ['checked' => FALSE], + ], + ], + ]; return parent::buildForm($form, $form_state); } + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + // Validate cookie name against valid characters. + if (preg_match('/[&\'\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5b-\x5d\x7b\x7d\x7f]/', $form_state->getValue('cookie_name'))) { + $form_state->setErrorByName('cookie_name', $this->t('Invalid cookie name, please remove any special characters and try again.')); + } + + } + /** * {@inheritdoc} */ @@ -581,6 +813,7 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { $permission_name = 'display eu cookie compliance popup'; foreach ($this->getRoles() as $role_name => $role) { + /* @var \Drupal\user\Entity\Role $role */ if (!$role->isAdmin()) { if (array_key_exists($role_name, $form_state->getValue('see_the_banner')) && $form_state->getValue('see_the_banner')[$role_name]) { user_role_grant_permissions($role_name, [$permission_name]); @@ -599,6 +832,24 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { $form_state->setValue('popup_position', FALSE); } + $method = $form_state->getValue('method'); + + if ($method != 'default') { + $form_state->setValue('disagree_button', TRUE); + $form_state->setValue('popup_clicking_confirmation', FALSE); + $form_state->setValue('popup_scrolling_confirmation', FALSE); + + } + else { + $form_state->setValue('whitelisted_cookies', ''); + $form_state->setValue('disabled_javascripts', ''); + } + + // Clear cached javascript. + Cache::invalidateTags(['library_info']); + + eu_cookie_compliance_module_set_weight(); + // Save settings. $this->config('eu_cookie_compliance.settings') ->set('domain', $form_state->getValue('domain')) @@ -611,7 +862,7 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { ->set('popup_disagree_button_message', $form_state->getValue('popup_disagree_button_message')) ->set('popup_info', $form_state->getValue('popup_info')) ->set('use_mobile_message', $form_state->getValue('use_mobile_message')) - ->set('mobile_popup_info', $form_state->getValue('use_mobile_message') ? $form_state->getValue('mobile_popup_info') : array('value' => '', 'format' => filter_default_format())) + ->set('mobile_popup_info', $form_state->getValue('use_mobile_message') ? $form_state->getValue('mobile_popup_info') : ['value' => '', 'format' => filter_default_format()]) ->set('mobile_breakpoint', $form_state->getValue('mobile_breakpoint')) ->set('popup_agreed_enabled', $form_state->getValue('popup_agreed_enabled')) ->set('popup_hide_agreed', $form_state->getValue('popup_hide_agreed')) @@ -630,6 +881,7 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { ->set('exclude_paths', $form_state->getValue('exclude_paths')) ->set('exclude_admin_theme', $form_state->getValue('exclude_admin_theme')) ->set('cookie_lifetime', $form_state->getValue('cookie_lifetime')) + ->set('cookie_session', $form_state->getValue('cookie_session')) ->set('eu_only', $form_state->getValue('eu_only')) ->set('eu_only_js', $form_state->getValue('eu_only_js')) ->set('use_bare_css', $form_state->getValue('use_bare_css')) @@ -639,6 +891,15 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { ->set('exclude_uid_1', $form_state->getValue('exclude_uid_1')) ->set('better_support_for_screen_readers', $form_state->getValue('better_support_for_screen_readers')) ->set('fixed_top_position', $form_state->getValue('fixed_top_position')) + ->set('method', $form_state->getValue('method')) + ->set('disagree_button_label', $form_state->getValue('disagree_button_label')) + ->set('whitelisted_cookies', $form_state->getValue('whitelisted_cookies')) + ->set('disabled_javascripts', $form_state->getValue('disabled_javascripts')) + ->set('consent_storage_method', $form_state->getValue('consent_storage_method')) + ->set('withdraw_message', $form_state->getValue('withdraw_message')) + ->set('withdraw_action_button_label', $form_state->getValue('withdraw_action_button_label')) + ->set('withdraw_tab_button_label', $form_state->getValue('withdraw_tab_button_label')) + ->set('withdraw_enabled', $form_state->getValue('withdraw_enabled')) ->save(); parent::submitForm($form, $form_state); @@ -652,7 +913,7 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state. */ - public function validatePopupLink($element, FormStateInterface &$form_state) { + public function validatePopupLink(array $element, FormStateInterface &$form_state) { if (empty($element['#value'])) { return; } @@ -661,17 +922,17 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { if (UrlHelper::isExternal($input)) { $allowed_protocols = ['http', 'https']; if (!in_array(parse_url($input, PHP_URL_SCHEME), $allowed_protocols)) { - $form_state->setError($element, $this->t('Invalid protocol specified for the %name (valid protocols: %protocols).', array( + $form_state->setError($element, $this->t('Invalid protocol specified for the %name (valid protocols: %protocols).', [ '%name' => $element['#title'], '%protocols' => implode(', ', $allowed_protocols), - ))); + ])); } else { try { Url::fromUri($input); } catch (\Exception $exc) { - $form_state->setError($element, $this->t('Invalid %name (:message).', array('%name' => $element['#title'], ':message' => $exc->getMessage()))); + $form_state->setError($element, $this->t('Invalid %name (:message).', ['%name' => $element['#title'], ':message' => $exc->getMessage()])); } } } @@ -684,7 +945,7 @@ class EuCookieComplianceConfigForm extends ConfigFormBase { Url::fromUserInput($input); } catch (\Exception $exc) { - $form_state->setError($element, $this->t('Invalid URL in %name field (:message).', array('%name' => $element['#title'], ':message' => $exc->getMessage()))); + $form_state->setError($element, $this->t('Invalid URL in %name field (:message).', ['%name' => $element['#title'], ':message' => $exc->getMessage()])); } } } diff --git a/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorage/BasicConsentStorage.php b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorage/BasicConsentStorage.php new file mode 100644 index 000000000..ce1368ad2 --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorage/BasicConsentStorage.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\eu_cookie_compliance\Plugin\ConsentStorage; + +use Drupal\eu_cookie_compliance\Plugin\ConsentStorageBase; +use Drupal\Core\Config\ConfigFactoryInterface; + +/** + * Provides a database storage for cookie consents. + * + * @ConsentStorage( + * id = "basic", + * name = @Translation("Basic storage"), + * description = @Translation("Basic storage") + * ) + */ +class BasicConsentStorage extends ConsentStorageBase { + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id = NULL, $plugin_definition = NULL, ConfigFactoryInterface $config_factory = NULL) { + return parent::__construct($configuration, $plugin_id, $plugin_definition, $config_factory); + } + + /** + * {@inheritdoc} + */ + public function registerConsent($consent_type) { + $revision_id = $this->getCurrentPolicyNodeRevision(); + $timestamp = time(); + $ip_address = \Drupal::request()->getClientIp(); + $uid = \Drupal::currentUser()->id(); + + \Drupal::database()->insert('eu_cookie_compliance_basic_consent')->fields( + [ + 'uid' => $uid, + 'ip_address' => $ip_address, + 'timestamp' => $timestamp, + 'revision_id' => $revision_id ? $revision_id : 0 , + 'consent_type' => $consent_type, + ] + )->execute(); + return TRUE; + } + +} diff --git a/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageBase.php b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageBase.php new file mode 100644 index 000000000..9871422bc --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageBase.php @@ -0,0 +1,122 @@ +<?php + +namespace Drupal\eu_cookie_compliance\Plugin; + +use Drupal\Core\Plugin\PluginBase; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Config\ConfigFactoryInterface; + +/** + * Provides a base class for a consent storage. + * + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageInterface + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageManager + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageManagerInterface + * @see plugin_api + */ +abstract class ConsentStorageBase extends PluginBase implements ConsentStorageInterface { + + /** + * The config factory. + * + * Subclasses should use the self::config() method, which may be overridden to + * address specific needs when loading config, rather than this property + * directly. See \Drupal\Core\Form\ConfigFormBase::config() for an example of + * this. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Constructs a ConsentStorageBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->pluginDefinition['label']; + } + + /** + * {@inheritdoc} + */ + public function description() { + return $this->pluginDefinition['description']; + } + + /** + * {@inheritdoc} + */ + public function getStatus() { + return TRUE; + } + + /** + * Get the current revision of the privacy policy node. + * + * @return bool|int + * Returns the latest revision ID of the curreny privacy policy node, or + * FALSE if no priacy policy exists. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + public function getCurrentPolicyNodeRevision() { + $config = $this->configFactory->get('eu_cookie_compliance.settings'); + $cookie_policy_link = $config->get('popup_link'); + $cookie_policy_drupal_path = \Drupal::service('path.alias_manager')->getPathByAlias($cookie_policy_link, \Drupal::languageManager()->getCurrentLanguage()->getId()); + if (substr($cookie_policy_drupal_path, 0, 6) == '/node/') { + $node_id = explode('/', $cookie_policy_drupal_path)[2]; + /* @var \Drupal\node\Entity\Node $node */ + $node = \Drupal::entityTypeManager()->getStorage('node')->load($node_id); + return $node->getRevisionId(); + } + else { + return FALSE; + } + } + + /** + * Register consent. + * + * In addition to the parameters passed to the function, consider storing the + * uid, ip address, timestamp and the current revision of the cookie policy. + * + * @param string $consent_type + * "banner" if the consent is given to the cookie banner or the form ID when + * consent is given on a form. + * + * @return bool + * Returns TRUE when the consent has been stored successfully, FALSE on + * error. + */ + abstract protected function registerConsent($consent_type); + +} diff --git a/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageInterface.php b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageInterface.php new file mode 100644 index 000000000..bf7d24a90 --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageInterface.php @@ -0,0 +1,34 @@ +<?php + +namespace Drupal\eu_cookie_compliance\Plugin; + +use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; + +/** + * Defines the interface for consent storages. + * + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageBase + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageManager + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageManagerInterface + * @see plugin_api + */ +interface ConsentStorageInterface extends PluginInspectionInterface, ContainerFactoryPluginInterface { + + /** + * Returns the consent storage label. + * + * @return string + * The consent storage label. + */ + public function label(); + + /** + * Returns the consent storage description. + * + * @return string + * The consent storage description. + */ + public function description(); + +} diff --git a/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageManager.php b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageManager.php new file mode 100644 index 000000000..b6b7f8317 --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageManager.php @@ -0,0 +1,49 @@ +<?php + +namespace Drupal\eu_cookie_compliance\Plugin; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Component\Plugin\FallbackPluginManagerInterface; + +/** + * Provides an ConsentStorage plugin manager. + * + * @see \Drupal\eu_cookie_compliance\Annotation\ConsentStorage + * @see \Drupal\eu_cookie_compliance\Plugin\ConsentStorageInterface + * @see plugin_api + */ +class ConsentStorageManager extends DefaultPluginManager implements FallbackPluginManagerInterface { + + /** + * Constructs a ConsentStorageManager object. + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke the alter hook with. + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + parent::__construct( + 'Plugin/ConsentStorage', + $namespaces, + $module_handler, + 'Drupal\eu_cookie_compliance\Plugin\ConsentStorageInterface', + 'Drupal\eu_cookie_compliance\Annotation\ConsentStorage' + ); + $this->alterInfo('consent_storage_info'); + $this->setCacheBackend($cache_backend, 'consent_storage_info_plugins'); + } + + /** + * {@inheritdoc} + */ + public function getFallbackPluginId($plugin_id, array $configuration = []) { + return 'basic'; + } + +} diff --git a/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageManagerInterface.php b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageManagerInterface.php new file mode 100644 index 000000000..c32845a61 --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/src/Plugin/ConsentStorageManagerInterface.php @@ -0,0 +1,31 @@ +<?php + +namespace Drupal\eu_cookie_compliance\Plugin; + +use Drupal\Component\Plugin\PluginManagerInterface; + +/** + * Collects available consent storages. + */ +interface ConsentStorageManagerInterface extends PluginManagerInterface { + + /** + * Get all available eu_cookie_compliance storage plugin instances. + * + * @param array $configuration + * Export configuration (aka export options). + * + * @return \Drupal\eu_cookie_compliance\Plugin\ConsentStorageInterface[] + * An array of all available eu_cookie_compliance consent plugin instances. + */ + public function getInstances(array $configuration = []); + + /** + * Get consent storage plugins as options. + * + * @return array + * An associative array of options keyed by plugin id. + */ + public function getOptions(); + +} diff --git a/web/modules/contrib/eu_cookie_compliance/src/Routing/CheckIfEuCountryJs.php b/web/modules/contrib/eu_cookie_compliance/src/Routing/CheckIfEuCountryJs.php index 23868e265..7ffafc111 100644 --- a/web/modules/contrib/eu_cookie_compliance/src/Routing/CheckIfEuCountryJs.php +++ b/web/modules/contrib/eu_cookie_compliance/src/Routing/CheckIfEuCountryJs.php @@ -13,8 +13,8 @@ class CheckIfEuCountryJs { * {@inheritdoc} */ public function routes() { - $routes = array(); - if (\Drupal::moduleHandler()->moduleExists('smart_ip')) { + $routes = []; + if (\Drupal::config('eu_cookie_compliance.settings')->get('eu_only_js')) { $routes['eu_cookie_compliance.check_if_eu_country_js'] = new Route( '/eu-cookie-compliance-check', [ diff --git a/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_popup_agreed.html.twig b/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_popup_agreed.html.twig index 608988525..abd78b175 100644 --- a/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_popup_agreed.html.twig +++ b/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_popup_agreed.html.twig @@ -11,7 +11,7 @@ * find-more-button - link to an information page * * Variables available: - * - $message: Contains the text that will be display whithin the banner + * - $message: Contains the text that will be display within the banner * - $hide_button: Label for the hide button * - $find_more_button: Label for the find out more button */ @@ -25,7 +25,7 @@ <div id="popup-buttons" class="eu-cookie-compliance-buttons"> <button type="button" class="hide-popup-button eu-cookie-compliance-hide-button">{{ hide_button }}</button> {% if find_more_button %} - <button type="button" class="find-more-button eu-cookie-compliance-more-button" >{{ find_more_button }}</button> + <button type="button" class="find-more-button eu-cookie-compliance-more-button-thank-you" >{{ find_more_button }}</button> {% endif %} </div> </div> diff --git a/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_popup_info.html.twig b/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_popup_info.html.twig index 4317b1635..f9f1a4dec 100644 --- a/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_popup_info.html.twig +++ b/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_popup_info.html.twig @@ -12,8 +12,17 @@ * * Variables available: * - message: Contains the text that will be display whithin the banner - * - agree_button: Label for the agree button - * - disagree_button: Label for the disagree button + * - agree_button: Label for the primary/agree button. Note that this is the + * primary button. For backwards compatibility, the name remains agree_button. + * - disagree_button: Contains Cookie policy button title. (Note: for historical + * reasons, this label is called "disagree" even though it just displays the + * privacy policy.) + * - secondary_button_label: Contains the secondary button label. The current + * action depends on whether you're running the module in Opt-out or Opt-in + * mode. + * - primary_button_class: Contains class names for the primary button. + * - secondary_button_class: Contains class names for the secondary button + * (if visible). */ #} @@ -21,12 +30,15 @@ <div class ="popup-content info eu-cookie-compliance-content"> <div id="popup-text" class="eu-cookie-compliance-message"> {{ message }} - </div> - <div id="popup-buttons" class="eu-cookie-compliance-buttons"> - <button type="button" class="agree-button eu-cookie-compliance-agree-button">{{ agree_button }}</button> {% if disagree_button %} <button type="button" class="find-more-button eu-cookie-compliance-more-button">{{ disagree_button }}</button> {% endif %} </div> + <div id="popup-buttons" class="eu-cookie-compliance-buttons"> + <button type="button" class="{{ primary_button_class }}">{{ agree_button }}</button> + {% if secondary_button_label %} + <button type="button" class="{{ secondary_button_class }}" >{{ secondary_button_label }}</button> + {% endif %} + </div> </div> </div> diff --git a/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_withdraw.html.twig b/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_withdraw.html.twig new file mode 100755 index 000000000..1b63b23df --- /dev/null +++ b/web/modules/contrib/eu_cookie_compliance/templates/eu_cookie_compliance_withdraw.html.twig @@ -0,0 +1,28 @@ +{# +/** + * @file + * This is a template file for the cookie consent withdraw button. + * + * When overriding this template it is important to note that jQuery will use + * the following classes to assign actions to the button: + * + * eu-cookie-withdraw-button - withdraw button + * eu-cookie-withdraw-tab - tab that reveals the withdraw interface + * + * Variables available: + * - message: Contains the text that will be displayed within the banner. + * - withdraw_tab_button_label: Label for the tab that sits at the window edge. + * - withdraw_action_button_label: Label for the withdraw button. + */ +#} +<button type="button" class="eu-cookie-withdraw-tab">{{ withdraw_tab_button_label }}</button> +<div class="eu-cookie-withdraw-banner"> + <div class="popup-content info eu-cookie-compliance-content"> + <div id="popup-text" class="eu-cookie-compliance-message"> + {{ message }} + </div> + <div id="popup-buttons" class="eu-cookie-compliance-buttons"> + <button type="button" class="eu-cookie-withdraw-button">{{ withdraw_action_button_label }}</button> + </div> + </div> +</div> diff --git a/web/modules/contrib/entity/LICENSE.txt b/web/modules/contrib/file_mdm/LICENSE.txt similarity index 100% rename from web/modules/contrib/entity/LICENSE.txt rename to web/modules/contrib/file_mdm/LICENSE.txt diff --git a/web/modules/contrib/file_mdm/README.md b/web/modules/contrib/file_mdm/README.md new file mode 100644 index 000000000..aa26f1054 --- /dev/null +++ b/web/modules/contrib/file_mdm/README.md @@ -0,0 +1,115 @@ +# File metadata manager + +A Drupal 8 module providing a file metadata manager service and API. Allows to +get, via an unified API, information stored in files like EXIF photo +information, TrueType font information, etc. + +Metadata protocols are pluggable. Developers can implement a plugin and use the +service framework to get the metadata required. + +The following plugins are provided by the module: + +Plugin | Read | Write | Description | +--------------|:----:|:-----:|--------------------------------------------------------------| +exif | X | X | Uses the [PHP Exif Library](https://github.com/lsolesen/pel) to read/write EXIF information to image files, bypassing the limitations of the standard PHP Exif extensions which only provides read capabilities. Enable the _file_mdm_exif_ submodule to enable this plugin. | +font | X | | Uses the [PHP Font Lib](https://github.com/PhenX/php-font-lib) to read font information from TTF/OTF/WOFF font files. Enable the _file_mdm_exif_ submodule to enable this plugin. | +getimagesize | X | | Caches calls to the PHP ```getimagesize()``` function. | + +The module is inspired by discussions at [#2630242 Provide methods to retrieve EXIF image information via the Image object](https://www.drupal.org/node/2630242). + + +## Features: + +1. Load from, and save to, file embedded metadata directly from the files. +2. Metadata for a file is statically cached during a request's lifetime. This + avoids different modules all repeat I/O on the same file. +3. Metadata can be cached in a Drupal cache bin to avoid repeating I/O on the + files in successive requests. +4. Metadata standards (EXIF, TTF, etc.) are implemented as plugins. The service + loads the metadata plugin needed based on the calling code request. +5. Manages copying to/from local temporary storage files stored in remote file + systems, to allow PHP functions that do not support remote stream wrappers + access the file locally. + + +## Installing: + +The module requires [using Composer to manage Drupal site dependencies](https://www.drupal.org/node/2718229). +Once you have setup building your code base using composer, require the module +via + +```$ composer require drupal/file_mdm``` + +then enable the module as usual. Also enable the EXIF or font submodules if +needed. + + +## Configuration: + +- Go to _Manage > Configuration > System > File Metadata Manager_ and specify + the cache retention requirements, in general and/or per each metadata plugin. + + +## Usage examples: + +Metadata are retrieved by specifying the protocol plugin required, and the +specific _metadata key_ needed. + +For the 'getimagesize' protocol, the metadata keys supported correspond to the +array keys returned by the PHP ```getimagesize()``` function: + +Key | Description | +---------|--------------------------------------------------------------| +0 | Width of the image | +1 | Height of the image | +2 | The _IMAGETYPE_*_ constant indicating the type of the image | +3 | Text string with the correct _height="yyy" width="xxx"_ string that can be used directly in an IMG tag | +mime | The MIME type of the image | +channels | 3 for RGB pictures and 4 for CMYK pictures | +bits | The number of bits for each color | + +1. __Basic usage:__ + + a. Use the _file_metadata_manager_ service to prepare collecting metadata for + the file located at a desired URI: + ```php + $fmdm = \Drupal::service('file_metadata_manager'); + $my_file_metadata = $fmdm->uri('public::/my_directory/test-image.jpeg'); + ``` + b. Get the metadata for the specific protocol, identified by the _plugin_, and + the metadata _key_ required: + ```php + $mime = $my_file_metadata->getMetadata('getimagesize', 'mime'); + ``` + c. Summarizing, in the context of a controller returning a render array: + ```php + $fmdm = \Drupal::service('file_metadata_manager'); + $my_file_metadata = $fmdm->uri('public::/my_directory/test-image.jpeg'); + $mime = $my_file_metadata->getMetadata('getimagesize', 'mime'); + return ['#markup' => 'MIME type: ' . $mime]; + ``` + + will return something like + ``` + MIME type: image/jpeg + ``` + +2. __Use a known local temp copy of the remote file to avoid remote file access:__ + + ```php + $fmdm = \Drupal::service('file_metadata_manager'); + $my_file_metadata = $fmdm->uri('remote_wrapper::/my_directory/test-image.jpeg'); + $my_file_metadata->setLocalTempPath($temp_path); + $mime = $my_file_metadata->getMetadata('getimagesize', 'mime'); + ... + ``` + +3. __Make a local temp copy of the remote file to avoid remote file access:__ + + ```php + $fmdm = \Drupal::service('file_metadata_manager'); + $my_file_metadata = $fmdm->uri('remote_wrapper::/my_directory/test-image.jpeg'); + $my_file_metadata->copyUriToTemp(); + $mime = $my_file_metadata->getMetadata('getimagesize', 'mime'); + ... + ``` diff --git a/web/modules/contrib/file_mdm/composer.json b/web/modules/contrib/file_mdm/composer.json new file mode 100644 index 000000000..3872c5ec7 --- /dev/null +++ b/web/modules/contrib/file_mdm/composer.json @@ -0,0 +1,10 @@ +{ + "name": "drupal/file_mdm", + "type": "drupal-module", + "description": "Provides a service to manage file metadata.", + "require": { + "php": ">=5.6", + "phenx/php-font-lib": "0.5", + "lsolesen/pel": "0.9.6" + } +} diff --git a/web/modules/contrib/file_mdm/config/install/file_mdm.file_metadata_plugin.getimagesize.yml b/web/modules/contrib/file_mdm/config/install/file_mdm.file_metadata_plugin.getimagesize.yml new file mode 100644 index 000000000..394780947 --- /dev/null +++ b/web/modules/contrib/file_mdm/config/install/file_mdm.file_metadata_plugin.getimagesize.yml @@ -0,0 +1,7 @@ +configuration: + cache: + override: FALSE + settings: + enabled: TRUE + expiration: 172800 + disallowed_paths: { } diff --git a/web/modules/contrib/file_mdm/config/install/file_mdm.settings.yml b/web/modules/contrib/file_mdm/config/install/file_mdm.settings.yml new file mode 100644 index 000000000..3ac296f8e --- /dev/null +++ b/web/modules/contrib/file_mdm/config/install/file_mdm.settings.yml @@ -0,0 +1,4 @@ +metadata_cache: + enabled: TRUE + expiration: 172800 + disallowed_paths: { } diff --git a/web/modules/contrib/file_mdm/config/schema/file_mdm.data_types.schema.yml b/web/modules/contrib/file_mdm/config/schema/file_mdm.data_types.schema.yml new file mode 100644 index 000000000..37bf4e24a --- /dev/null +++ b/web/modules/contrib/file_mdm/config/schema/file_mdm.data_types.schema.yml @@ -0,0 +1,32 @@ +# Basic data types for file_mdm module. + +file_mdm.cache_settings: + type: mapping + label: 'Cache settings' + mapping: + enabled: + type: boolean + label: 'Enable metadata caching' + expiration: + type: integer + label: 'Cache entry duration' + disallowed_paths: + type: sequence + label: 'Paths to be excluded from caching' + sequence: + type: string + +file_mdm.plugin.configuration: + type: mapping + label: 'FileMetadata plugin settings' + mapping: + cache: + type: mapping + label: 'Cache settings' + mapping: + override: + type: boolean + label: 'Override general settings' + settings: + type: file_mdm.cache_settings + label: 'Plugin overridden cache settings' diff --git a/web/modules/contrib/file_mdm/config/schema/file_mdm.schema.yml b/web/modules/contrib/file_mdm/config/schema/file_mdm.schema.yml new file mode 100644 index 000000000..91bed16c8 --- /dev/null +++ b/web/modules/contrib/file_mdm/config/schema/file_mdm.schema.yml @@ -0,0 +1,18 @@ +# file_mdm module schema + +# Schema for settings. +file_mdm.settings: + type: config_object + label: 'file_mdm settings' + mapping: + metadata_cache: + type: file_mdm.cache_settings + label: 'Main metadata caching' + +file_mdm.file_metadata_plugin.getimagesize: + type: config_object + label: 'getimagesize file metadata plugin settings' + mapping: + configuration: + type: file_mdm.plugin.configuration + label: 'getimagesize plugin settings' diff --git a/web/modules/contrib/file_mdm/file_mdm.info.yml b/web/modules/contrib/file_mdm/file_mdm.info.yml new file mode 100644 index 000000000..da246ee07 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm.info.yml @@ -0,0 +1,15 @@ +type: module +# core: 8.x +name: 'File metadata manager' +description: 'Provides a service to manage file metadata.' +package: File metadata +configure: file_mdm.settings +php: 5.6 +test_dependencies: + - image_effects + +# Information added by Drupal.org packaging script on 2017-02-28 +version: '8.x-1.1' +core: '8.x' +project: 'file_mdm' +datestamp: 1488273795 diff --git a/web/modules/contrib/file_mdm/file_mdm.links.menu.yml b/web/modules/contrib/file_mdm/file_mdm.links.menu.yml new file mode 100644 index 000000000..471a909d6 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm.links.menu.yml @@ -0,0 +1,5 @@ +file_mdm.settings: + title: 'File metadata manager' + route_name: file_mdm.settings + description: 'Configure file metadata settings.' + parent: system.admin_config_system diff --git a/web/modules/contrib/file_mdm/file_mdm.module b/web/modules/contrib/file_mdm/file_mdm.module new file mode 100644 index 000000000..34e6a70d5 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm.module @@ -0,0 +1,26 @@ +<?php + +/** + * @file + * Provides a service to manage file metadata. + */ + +use \Drupal\Core\Entity\EntityInterface; + +/** + * Implements hook_cache_flush(). + */ +function file_mdm_cache_flush() { + return array('file_mdm'); +} + +/** + * Implements hook_file_delete(). + */ +function file_mdm_file_delete(EntityInterface $entity) { + // Deletes any cached file metadata information upon deletion of a file + // entity. + $fmdm = \Drupal::service('file_metadata_manager'); + $fmdm->deleteCachedMetadata($entity->getFileUri()); + $fmdm->release($entity->getFileUri()); +} diff --git a/web/modules/contrib/file_mdm/file_mdm.routing.yml b/web/modules/contrib/file_mdm/file_mdm.routing.yml new file mode 100644 index 000000000..7f4a17a4c --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm.routing.yml @@ -0,0 +1,9 @@ +# file_mdm module routes + +file_mdm.settings: + path: '/admin/config/system/file_mdm' + defaults: + _form: '\Drupal\file_mdm\Form\SettingsForm' + _title: 'File metadata manager' + requirements: + _permission: 'administer site configuration' diff --git a/web/modules/contrib/file_mdm/file_mdm.services.yml b/web/modules/contrib/file_mdm/file_mdm.services.yml new file mode 100644 index 000000000..bd8829155 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm.services.yml @@ -0,0 +1,17 @@ +services: + cache.file_mdm: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory: cache_factory:get + arguments: ['file_mdm'] + logger.channel.file_mdm: + parent: logger.channel_base + arguments: ['file_mdm'] + file_metadata_manager: + class: Drupal\file_mdm\FileMetadataManager + arguments: ['@plugin.manager.file_metadata', '@logger.channel.file_mdm', '@config.factory', '@file_system', '@cache.file_mdm'] + plugin.manager.file_metadata: + class: Drupal\file_mdm\Plugin\FileMetadataPluginManager + parent: default_plugin_manager + arguments: ['@config.factory'] diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/README.md b/web/modules/contrib/file_mdm/file_mdm_exif/README.md new file mode 100644 index 000000000..9f50240c4 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/README.md @@ -0,0 +1,134 @@ +# File metadata EXIF + +A Drupal 8 module providing a file metadata plugin for the EXIF protocol. + + +## Features: + +Uses the [PHP Exif Library](https://github.com/lsolesen/pel) to read/write EXIF +information to image files, so bypassing the limitations of the standard PHP +Exif extensions which only provides read capabilities. + + +## Usage examples: + +1. __Get EXIF information from a file:__ + + a. Prepare collecting metadata for the file located at a desired URI: + + ```php + $fmdm = \Drupal::service('file_metadata_manager'); + $my_file_metadata = $fmdm->uri('public::/my_directory/test-exif.jpeg'); + ... + ``` + + b. Get the metadata for the metadata _$key_ required. The value returned is an + associative array with two keys: + - 'text': the string representation of the EXIF tag, suitable for + presentation; + - 'value': the EXIF tag value in PEL internal format. + + ```php + ... + $val = $my_file_metadata->getMetadata('exif', $key); + ... + ``` + + c. EXIF metadata is organized in 'headers' (IFDs) and 'tags'. For this reason, + the metadata _$key_ can be specified in the ```getMetadata``` method: + - as a string: in this case, it is assumed that a TAG is specified, and the + default IFD for that TAG will be used to fetch the information: + - as an array: in this case, the first and the second array elements + specify respectively the IFD and the TAG requested. IFD and TAG can be + strings, or integers. + The following statements all are equivalent in returning the same + information about the 'ApertureValue' TAG in the 'Exif' IFD: + + ```php + ... + $aperture = $my_file_metadata->getMetadata('exif', 'ApertureValue'); + $aperture = $my_file_metadata->getMetadata('exif', ['Exif', 'ApertureValue']); + $aperture = $my_file_metadata->getMetadata('exif', [2, 'ApertureValue']); + $aperture = $my_file_metadata->getMetadata('exif', ['Exif', 0x9202]); + $aperture = $my_file_metadata->getMetadata('exif', [2, 0x9202]); + ... + ``` + + d. Get a list of IFDs: + + ```php + ... + $my_file_metadata->getSupportedKeys('exif', ['ifds' => TRUE]); + ... + ``` + + e. Get a list of TAGs for a given IFD: + + ```php + ... + $my_file_metadata->getSupportedKeys('exif', ['ifd' => 'GPS']); + ... + ``` + + f. Walk through all possible IFDs/TAGs and build a table with results: + + ```php + ... + $header = [ + ['data' => 'key'], + ['data' => 'text'], + ['data' => 'value'], + ]; + $rows = []; + foreach ($my_file_metadata->getSupportedKeys('exif', ['ifds' => TRUE]) as $ifd) { + $rows[] = [['data' => $ifd[0], 'colspan' => 3]]; + $keys = $my_file_metadata->getSupportedKeys('exif', ['ifd' => $ifd[0]]); + foreach ($keys as $key) { + $x = $my_file_metadata->getMetadata('exif', $key); + if ($x) { + $rows[] = ['data' => [$key[1], $x ? $x['text'] : NULL, $x ? var_export($x['value'], TRUE) : NULL]]; + } + } + } + return [ + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + ]; + ``` + +2. __Change EXIF information and save to file:__ + +Changing EXIF information and saving it back to the file requires some +understanding of how the PEL library manages EXIF entries. + +a. If you are changing information that is _already_ existing in the source + file, then you can use the plugin ```setMetadata``` method, passing the value + that the PEL Exif entry expects: + + ```php + ... + $my_file_metadata->setMetadata('exif', 'Orientation', 7); + ... + ``` + +b. If you are _adding_ a TAG that was not existing before, you need to pass a + new PEL Exif entry, as expected for that entry. This can also be done as an + alternative to change an existing entry: + + ```php + ... + $artist_tag = \Drupal::service('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag('Artist'); + $value = 'MEEeeee!'; + $artist = new PelEntryAscii($artist_tag['tag'], $value); + $my_file_metadata->setMetadata('exif', 'Artist', $artist); + ... + ``` + +c. Save changed metadata to file: + + ```php + ... + $my_file_metadata->saveMetadataToFile('exif'); + ... + ``` diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/config/install/file_mdm_exif.file_metadata_plugin.exif.yml b/web/modules/contrib/file_mdm/file_mdm_exif/config/install/file_mdm_exif.file_metadata_plugin.exif.yml new file mode 100644 index 000000000..9f1433b56 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/config/install/file_mdm_exif.file_metadata_plugin.exif.yml @@ -0,0 +1,33 @@ +ifd_map: + 0: + type: 0 + aliases: + - '0' + - 'IFD0' + - 'Main' + 1: + type: 1 + aliases: + - '1' + - 'IFD1' + - 'Thumbnail' + Exif: + type: 2 + aliases: + - 'Exif' + GPS: + type: 3 + aliases: + - 'GPS' + Interoperability: + type: 4 + aliases: + - 'Interoperability' + - 'Interop' +configuration: + cache: + override: FALSE + settings: + enabled: TRUE + expiration: 172800 + disallowed_paths: { } diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/config/schema/file_mdm_exif.data_types.schema.yml b/web/modules/contrib/file_mdm/file_mdm_exif/config/schema/file_mdm_exif.data_types.schema.yml new file mode 100644 index 000000000..a594ee4be --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/config/schema/file_mdm_exif.data_types.schema.yml @@ -0,0 +1,14 @@ +# Basic data types for file_mdf_exif module. + +file_mdm_exif.ifd: + type: mapping + label: 'EXIF IFD' + mapping: + type: + type: integer + label: 'IFD type' + aliases: + type: sequence + sequence: + type: string + label: 'Literal representation of IFD' diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/config/schema/file_mdm_exif.schema.yml b/web/modules/contrib/file_mdm/file_mdm_exif/config/schema/file_mdm_exif.schema.yml new file mode 100644 index 000000000..6403814c8 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/config/schema/file_mdm_exif.schema.yml @@ -0,0 +1,13 @@ +# file_mdm_exif module schema + +file_mdm_exif.file_metadata_plugin.exif: + type: config_object + label: 'File metadata EXIF plugin settings' + mapping: + ifd_map: + type: sequence + sequence: + type: file_mdm_exif.ifd + configuration: + type: file_mdm.plugin.configuration + label: 'exif plugin settings' diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/file_mdm_exif.info.yml b/web/modules/contrib/file_mdm/file_mdm_exif/file_mdm_exif.info.yml new file mode 100644 index 000000000..878b487d1 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/file_mdm_exif.info.yml @@ -0,0 +1,17 @@ +type: module +# core: 8.x +name: File metadata - EXIF +description: 'Provides a file metadata plugin for EXIF image information.' +package: File metadata +configure: file_mdm.settings +php: 5.6 +dependencies: + - file_mdm +test_dependencies: + - image_effects + +# Information added by Drupal.org packaging script on 2017-02-28 +version: '8.x-1.1' +core: '8.x' +project: 'file_mdm' +datestamp: 1488273795 diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/file_mdm_exif.services.yml b/web/modules/contrib/file_mdm/file_mdm_exif/file_mdm_exif.services.yml new file mode 100644 index 000000000..ecf2b24b4 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/file_mdm_exif.services.yml @@ -0,0 +1,4 @@ +services: + file_mdm_exif.tag_mapper: + class: Drupal\file_mdm_exif\ExifTagMapper + arguments: ['@logger.channel.file_mdm', '@config.factory', '@cache.file_mdm'] diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/src/ExifTagMapper.php b/web/modules/contrib/file_mdm/file_mdm_exif/src/ExifTagMapper.php new file mode 100644 index 000000000..c7b39cc14 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/src/ExifTagMapper.php @@ -0,0 +1,350 @@ +<?php + +namespace Drupal\file_mdm_exif; + +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\file_mdm\FileMetadataException; +use Psr\Log\LoggerInterface; +use lsolesen\pel\PelIfd; +use lsolesen\pel\PelTag; + +/** + * Provides a mapping service for EXIF ifds and tags. + */ +class ExifTagMapper implements ExifTagMapperInterface { + + /** + * The cache service. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * The file_mdm logger. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The string to IFD map. + * + * Maps IFDs or their aliases expressed as literals to the EXIF integer + * identifier. + * + * @var array + */ + protected $stringToIfdMap; + + /** + * The string to TAG map. + * + * Maps TAGs expressed as literals to the EXIF integer IFD/TAG identifiers. + * + * @var array + */ + protected $stringToTagMap; + + /** + * The supported metadata 'keys'. + * + * A simple array of IFD/TAG combinations, expressed as literals. + * + * @var array + */ + protected $supportedKeysMap; + + /** + * The supported IFDs. + * + * A simple array of IFDs, expressed as literal/integer combinations. + * + * @var array + */ + protected $supportedIfdsMap; + + /** + * Constructs a ExifTagMapper object. + * + * @param \Psr\Log\LoggerInterface $logger + * The file_mdm logger. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_service + * The cache service. + */ + public function __construct(LoggerInterface $logger, ConfigFactoryInterface $config_factory, CacheBackendInterface $cache_service) { + $this->logger = $logger; + $this->configFactory = $config_factory; + $this->cache = $cache_service; + } + + /** + * {@inheritdoc} + */ + public function resolveKeyToIfdAndTag($key) { + if ($key === NULL) { + throw new FileMetadataException('Missing $key argument', NULL, __METHOD__); + } + if (is_string($key)) { + $tag = $this->stringToTag($key); + return ['ifd' => $tag[0], 'tag' => $tag[1]]; + } + if (is_array($key)) { + if (!isset($key[0]) || !isset($key[1])) { + throw new FileMetadataException('Invalid $key array specified, must have two values', NULL, __METHOD__); + } + // Deal with ifd. + if (is_int($key[0])) { + $ifd = $key[0]; + } + elseif (is_string($key[0])) { + $ifd = $this->stringToIfd($key[0]); + } + else { + throw new FileMetadataException('Invalid EXIF IFD specified, must be a string or an integer', NULL, __METHOD__); + } + // Deal with tag. + if (is_string($key[1])) { + $tag = $this->stringToTag($key[1])[1]; + } + elseif (is_int($key[1])) { + $tag = $key[1]; + } + else { + throw new FileMetadataException('Invalid EXIF TAG specified, must be a string or an integer', NULL, __METHOD__); + } + return ['ifd' => $ifd, 'tag' => $tag]; + } + throw new FileMetadataException('Invalid $key argument, must be a string or an array', NULL, __METHOD__); + } + + /** + * {@inheritdoc} + */ + public function getSupportedKeys(array $options = NULL) { + if (isset($options['ifds'])) { + return $this->getSupportedIfdsMap(); + } + elseif (isset($options['ifd'])) { + return array_filter($this->getSupportedKeysMap(), function ($a) use ($options) { + return strtolower($options['ifd']) === strtolower($a[0]); + }); + } + else { + return $this->getSupportedKeysMap(); + } + } + + /** + * Returns the list of supported IFDs. + * + * Builds and caches the list as needed. + * + * @return array + * A simple array of IFDs, expressed as literal/integer combinations. + */ + protected function getSupportedIfdsMap() { + if (!$this->supportedIfdsMap) { + $cache_id = 'supportedIfds'; + if ($cache = $this->getCache($cache_id)) { + $this->supportedIfdsMap = $cache->data; + } + else { + $this->supportedIfdsMap = []; + $ifd_types = [ + PelIfd::IFD0, + PelIfd::IFD1, + PelIfd::EXIF, + PelIfd::GPS, + PelIfd::INTEROPERABILITY, + ]; + foreach ($ifd_types as $type) { + $this->supportedIfdsMap[] = [PelIfd::getTypeName($type), $type]; + } + $this->setCache($cache_id, $this->supportedIfdsMap); + } + } + return $this->supportedIfdsMap; + } + + /** + * Returns the list of supported metadata 'keys'. + * + * Builds and caches the list as needed. + * + * @return array + * A simple array of IFD/TAG combinations, expressed as literals. + */ + protected function getSupportedKeysMap() { + if (!$this->supportedKeysMap) { + $cache_id = 'supportedKeys'; + if ($cache = $this->getCache($cache_id)) { + $this->supportedKeysMap = $cache->data; + } + else { + $this->supportedKeysMap = []; + foreach ($this->getSupportedIfdsMap() as $ifd) { + $ifd_obj = new PelIfd($ifd[1]); + $valid_tags = $ifd_obj->getValidTags(); + foreach ($valid_tags as $tag) { + $this->supportedKeysMap[] = [ + $ifd[0], + PelTag::getName($ifd[1], $tag), + ]; + } + } + $this->setCache($cache_id, $this->supportedKeysMap); + } + } + return $this->supportedKeysMap; + } + + /** + * Returns the IFD/TAG integers for a TAG literal. + * + * @param string $value + * A TAG literal. + * + * @return array + * A simple array of with IFD and TAG, expressed as integers. + * + * @throws \Drupal\file_mdm\FileMetadataException + * When the IFD/TAG combination could not be found. + */ + protected function stringToTag($value) { + $v = strtolower($value); + $tag = isset($this->getStringToTagMap()[$v]) ? $this->getStringToTagMap()[$v] : NULL; + if ($tag) { + return $tag; + } + throw new FileMetadataException("No EXIF TAG found for key '{$value}'", "EXIF"); + } + + /** + * Returns the map of TAG strings to IFD/TAG integers. + * + * Builds and caches the list as needed. + * + * @return array + * An associative array where keys are TAG literals, and values a simple + * array of IFD/TAG integer identifiers. + */ + protected function getStringToTagMap() { + if (!$this->stringToTagMap) { + $cache_id = 'stringToTag'; + if ($cache = $this->getCache($cache_id)) { + $this->stringToTagMap = $cache->data; + } + else { + foreach ($this->getSupportedIfdsMap() as $ifd) { + $ifd_obj = new PelIfd($ifd[1]); + $valid_tags = $ifd_obj->getValidTags(); + foreach ($valid_tags as $tag) { + $tag_name = strtolower(PelTag::getName($ifd[1], $tag)); + if (!isset($this->stringToTagMap[$tag_name])) { + $this->stringToTagMap[$tag_name] = [$ifd[1], $tag]; + } + } + } + $this->setCache($cache_id, $this->stringToTagMap); + } + } + return $this->stringToTagMap; + } + + /** + * Returns the IFD integer for an IFD literal. + * + * @param string $value + * An IFD literal. + * + * @return int + * The IFD identifier. + * + * @throws \Drupal\file_mdm\FileMetadataException + * When the IFD could not be found. + */ + protected function stringToIfd($value) { + $v = strtolower($value); + if (isset($this->getStringToIfdMap()[$v])) { + return $this->getStringToIfdMap()[$v]; + } + throw new FileMetadataException("Invalid EXIF IFD '{$value}' specified", "EXIF"); + } + + /** + * Returns the map of IFD strings to IFD integers. + * + * Builds and caches the list as needed. + * + * @return array + * An associative array where keys are IFD literals, and values the IFD + * integer identifiers. + */ + protected function getStringToIfdMap() { + if (!$this->stringToIfdMap) { + $cache_id = 'stringToIfd'; + if ($cache = $this->getCache($cache_id)) { + $this->stringToIfdMap = $cache->data; + } + else { + $config_map = $this->configFactory->get('file_mdm_exif.file_metadata_plugin.exif')->get('ifd_map'); + $this->stringToIfdMap = []; + foreach ($config_map as $value) { + foreach ($value['aliases'] as $alias) { + $k = strtolower($alias); + $this->stringToIfdMap[$k] = $value['type']; + } + } + $this->setCache($cache_id, $this->stringToIfdMap); + } + } + return $this->stringToIfdMap; + } + + /** + * Gets a cache entry. + * + * @param string $id + * The cache id to get. + * + * @return object|null + * The cache item or NULL on failure. + */ + protected function getCache($id) { + if ($cache = $this->cache->get("map:exif:{$id}")) { + return $cache; + } + else { + return NULL; + } + } + + /** + * Sets a cache entry. + * + * @param string $id + * The cache id to set. + * @param mixed $value + * The value to cache. + * + * @return $this + */ + protected function setCache($id, $value) { + $config = $this->configFactory->get('file_mdm_exif.file_metadata_plugin.exif'); + $this->cache->set("map:exif:{$id}", $value, Cache::PERMANENT, $config->getCacheTags()); + return $this; + } + +} diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/src/ExifTagMapperInterface.php b/web/modules/contrib/file_mdm/file_mdm_exif/src/ExifTagMapperInterface.php new file mode 100644 index 000000000..9b7d645fa --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/src/ExifTagMapperInterface.php @@ -0,0 +1,54 @@ +<?php + +namespace Drupal\file_mdm_exif; + +/** + * Provides an interface for EXIF metadata ifds and tags mapper. + */ +interface ExifTagMapperInterface { + + /** + * Resolves a metadata 'key' to the default EXIF IFD and TAG. + * + * @param string|array $key + * A metadata 'key' as passed in by the file metadata manager. It can be a + * string, in which case the default IFD and TAG are returned. If it is an + * array, then the first and the second array elements define respectively + * the IFD and the TAG requested. IFD and TAG can be strings, in which case + * they are converted to EXIF integer identifiers, or integers, in which + * case they are returned as such. + * + * @return array + * An associative array with the following keys: + * 'ifd' - the IFD EXIF integer identifier. + * 'tag' - the TAG EXIF integer identifier. + * + * @throws \Drupal\file_mdm\FileMetadataException + * When wrong argument is passed, or if the IFD/TAG could not be found. + */ + public function resolveKeyToIfdAndTag($key); + + /** + * Returns a list of default metadata 'keys' supported. + * + * @param array $options + * (optional) If specified, restricts the results returned. By default, all + * the available EXIF IFD/TAG combinations for any IFD are returned. + * If $options contains ['ifds' => TRUE], the supported IFDs are returned. + * If $options contains ['ifd' => $value], the IFD/TAG combinations + * supported by the IFD specified by $value are returned. + * + * @return array + * A simple array. + * When returning a list of supported IFDs, each array element is a simple + * array with: + * 0 => the default string identifier of the IFD. + * 1 => the integer identifier of the IFD. + * When returning a list of supported IFD/TAGs, each array element is a + * simple array with: + * 0 => the string identifier of the IFD. + * 1 => the string identifier of the TAG. + */ + public function getSupportedKeys(array $options = NULL); + +} diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/src/Plugin/FileMetadata/Exif.php b/web/modules/contrib/file_mdm/file_mdm_exif/src/Plugin/FileMetadata/Exif.php new file mode 100644 index 000000000..ce5320454 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/src/Plugin/FileMetadata/Exif.php @@ -0,0 +1,399 @@ +<?php + +namespace Drupal\file_mdm_exif\Plugin\FileMetadata; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\file_mdm\Plugin\FileMetadata\FileMetadataPluginBase; +use Drupal\file_mdm_exif\ExifTagMapperInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; +use lsolesen\pel\PelEntry; +use lsolesen\pel\PelExif; +use lsolesen\pel\PelIfd; +use lsolesen\pel\PelJpeg; +use lsolesen\pel\PelTiff; + +/** + * FileMetadata plugin for EXIF. + * + * @FileMetadata( + * id = "exif", + * title = @Translation("EXIF"), + * help = @Translation("File metadata plugin for EXIF image information, using the PHP Exif Library (PEL)."), + * ) + */ +class Exif extends FileMetadataPluginBase { + + /** + * The MIME type guessing service. + * + * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface + */ + protected $mimeTypeGuesser; + + /** + * The EXIF tag mapping service. + * + * @var \Drupal\file_mdm_exif\ExifTagMapperInterface + */ + protected $tagMapper; + + /** + * The PEL file object being processed. + * + * @var \lsolesen\pel\PelJpeg|\lsolesen\pel\PelTiff + */ + protected $pelFile; + + /** + * Constructs an Exif file metadata plugin. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_service + * The cache service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser + * The MIME type mapping service. + * @param \Drupal\file_mdm_exif\ExifTagMapperInterface $tag_mapper + * The EXIF tag mapping service. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, CacheBackendInterface $cache_service, ConfigFactoryInterface $config_factory, MimeTypeGuesserInterface $mime_type_guesser, ExifTagMapperInterface $tag_mapper) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $cache_service, $config_factory); + $this->mimeTypeGuesser = $mime_type_guesser; + $this->tagMapper = $tag_mapper; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('cache.file_mdm'), + $container->get('config.factory'), + $container->get('file.mime_type.guesser'), + $container->get('file_mdm_exif.tag_mapper') + ); + } + + /** + * {@inheritdoc} + */ + public function getSupportedKeys($options = NULL) { + return $this->tagMapper->getSupportedKeys($options); + } + + /** + * Returns the PEL file object for the image file. + * + * @return \lsolesen\pel\PelJpeg|\lsolesen\pel\PelTiff + * A PEL file object. + */ + protected function getFile() { + if ($this->pelFile !== NULL) { + return $this->pelFile; + } + else { + switch ($this->mimeTypeGuesser->guess($this->getUri())) { + case 'image/jpeg': + $this->pelFile = new PelJpeg($this->getLocalTempPath()); + return $this->pelFile !== NULL ? $this->pelFile : FALSE; + + case 'image/tiff': + $this->pelFile = new PelTiff($this->getLocalTempPath()); + return $this->pelFile !== NULL ? $this->pelFile : FALSE; + + default: + return FALSE; + + } + } + } + + /** + * {@inheritdoc} + */ + protected function doGetMetadataFromFile() { + // Get the file as a PelJpeg or PelTiff object. + $file = $this->getFile(); + if (!$file) { + return []; + } + + // Get the TIFF section if existing, or return if not. + if ($file instanceof PelJpeg) { + $exif = $file->getExif(); + if ($exif === NULL) { + return []; + } + $tiff = $exif->getTiff(); + if ($tiff === NULL) { + return []; + } + } + elseif ($file instanceof PelTiff) { + $tiff = $file; + } + + // Scans metadata for entries of supported tags. + $metadata = []; + $keys = $this->tagMapper->getSupportedKeys(); + foreach ($keys as $key) { + $ifd_tag = $this->tagMapper->resolveKeyToIfdAndTag($key); + if ($entry = $this->getEntry($tiff, $ifd_tag['ifd'], $ifd_tag['tag'])) { + $metadata[$ifd_tag['ifd']][$ifd_tag['tag']] = $entry; + } + } + return $metadata; + } + + /** + * Returns a PelEntry. + * + * @param \lsolesen\pel\PelTiff $tiff + * A PelTiff object. + * @param int $ifd_tag + * The IFD EXIF integer identifier. + * @param int $key_tag + * The TAG EXIF integer identifier. + * + * @return \lsolesen\pel\PelEntry + * The PelEntry for the specified IFD and TAG. + */ + protected function getEntry(PelTiff $tiff, $ifd_tag, $key_tag) { + $ifd = $tiff->getIfd(); + switch ($ifd_tag) { + case PelIfd::IFD0: + return $ifd->getEntry($key_tag); + + case PelIfd::IFD1: + $ifd1 = $ifd->getNextIfd(); + if (!$ifd1) { + return NULL; + } + return $ifd1->getEntry($key_tag); + + case PelIfd::EXIF: + $exif = $ifd->getSubIfd(PelIfd::EXIF); + if (!$exif) { + return NULL; + } + return $exif->getEntry($key_tag); + + case PelIfd::INTEROPERABILITY: + $exif = $ifd->getSubIfd(PelIfd::EXIF); + if (!$exif) { + return NULL; + } + $interop = $exif->getSubIfd(PelIfd::INTEROPERABILITY); + if (!$interop) { + return NULL; + } + return $interop->getEntry($key_tag); + + case PelIfd::GPS: + $gps = $ifd->getSubIfd(PelIfd::GPS); + if (!$gps) { + return NULL; + } + return $gps->getEntry($key_tag); + + } + } + + /** + * {@inheritdoc} + */ + public function isSaveToFileSupported() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function doSaveMetadataToFile() { + // Get the file as a PelJpeg or PelTiff object. + $file = $this->getFile(); + if (!$file) { + return FALSE; + } + + // Get the TIFF section if existing, or create one if not. + if ($file instanceof PelJpeg) { + $exif = $file->getExif(); + if ($exif === NULL) { + // If EXIF section is missing we simply create a new APP1 section + // (a PelExif object) and add it to the PelJpeg object. + $exif = new PelExif(); + $file->setExif($exif); + } + $tiff = $exif->getTiff(); + if ($tiff === NULL) { + // Same for TIFF section. + $tiff = new PelTiff(); + $exif->setTiff($tiff); + } + } + elseif ($file instanceof PelTiff) { + $tiff = $file; + } + + // Get IFD0 if existing, or create it if not. + $ifd0 = $tiff->getIfd(); + if ($ifd0 === NULL) { + // No IFD in the TIFF data, we just create and insert an empty PelIfd + // object. + $ifd0 = new PelIfd(PelIfd::IFD0); + $tiff->setIfd($ifd0); + } + + // Loops through in-memory metadata and update tag entries accordingly. + foreach ($this->metadata as $ifd_id => $entries) { + switch ($ifd_id) { + case PelIfd::IFD0: + $this->setIfdEntries($ifd0, $entries); + break; + + case PelIfd::IFD1: + $ifd1 = $ifd0->getNextIfd(); + if ($ifd1 === NULL) { + $ifd1 = new PelIfd(PelIfd::IFD1); + $ifd0->setNextIfd($ifd1); + } + $this->setIfdEntries($ifd1, $entries); + break; + + case PelIfd::EXIF: + $exif = $ifd0->getSubIfd(PelIfd::EXIF); + if ($exif === NULL) { + $exif = new PelIfd(PelIfd::EXIF); + $ifd0->addSubIfd($exif); + } + $this->setIfdEntries($exif, $entries); + break; + + case PelIfd::INTEROPERABILITY: + $exif = $ifd0->getSubIfd(PelIfd::EXIF); + if ($exif === NULL) { + $exif = new PelIfd(PelIfd::EXIF); + $ifd0->addSubIfd($exif); + } + $interop = $exif->getSubIfd(PelIfd::INTEROPERABILITY); + if ($interop === NULL) { + $interop = new PelIfd(PelIfd::INTEROPERABILITY); + $exif->addSubIfd($interop); + } + $this->setIfdEntries($interop, $entries); + break; + + case PelIfd::GPS: + $gps = $ifd0->getSubIfd(PelIfd::GPS); + if ($gps === NULL) { + $gps = new PelIfd(PelIfd::GPS); + $ifd0->addSubIfd($gps); + } + $this->setIfdEntries($gps, $entries); + break; + + } + } + + return $file->saveFile($this->getLocalTempPath()) === FALSE ? FALSE : TRUE; + } + + /** + * Adds or changes entries for an IFD. + * + * @param lsolesen\pel\PelIfd $ifd + * A PelIfd object. + * @param lsolesen\pel\PelEntry[] $entries + * An array of PelEntry objects. + * + * @return bool + * TRUE if entries were added/changed successfully, FALSE otherwise. + */ + protected function setIfdEntries(PelIfd $ifd, array $entries) { + foreach ($entries as $tag => $input_entry) { + if ($c = $ifd->getEntry($tag)) { + if ($input_entry === 'deleted') { + unset($ifd[$tag]); + } + else { + $c->setValue($input_entry->getValue()); + } + } + else { + if ($input_entry !== 'deleted') { + $ifd->addEntry($input_entry); + } + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function doGetMetadata($key = NULL) { + if (!$this->metadata) { + return NULL; + } + if (!$key) { + return $this->metadata; + } + else { + $ifd_tag = $this->tagMapper->resolveKeyToIfdAndTag($key); + if (!isset($this->metadata[$ifd_tag['ifd']][$ifd_tag['tag']]) || $this->metadata[$ifd_tag['ifd']][$ifd_tag['tag']] === 'deleted') { + return NULL; + } + $entry = $this->metadata[$ifd_tag['ifd']][$ifd_tag['tag']]; + return [ + 'value' => $entry->getValue(), + 'text' => $entry->getText(), + ]; + } + } + + /** + * {@inheritdoc} + */ + protected function doSetMetadata($key, $value) { + $ifd_tag = $this->tagMapper->resolveKeyToIfdAndTag($key); + if ($value instanceof PelEntry) { + $this->metadata[$ifd_tag['ifd']][$ifd_tag['tag']] = $value; + return TRUE; + } + elseif (isset($this->metadata[$ifd_tag['ifd']][$ifd_tag['tag']])) { + $this->metadata[$ifd_tag['ifd']][$ifd_tag['tag']]->setValue($value); + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + protected function doRemoveMetadata($key) { + if (!$this->metadata || !$key) { + return FALSE; + } + else { + $ifd_tag = $this->tagMapper->resolveKeyToIfdAndTag($key); + if (isset($this->metadata[$ifd_tag['ifd']][$ifd_tag['tag']])) { + $this->metadata[$ifd_tag['ifd']][$ifd_tag['tag']] = 'deleted'; + return TRUE; + } + return FALSE; + } + } + +} diff --git a/web/modules/contrib/file_mdm/file_mdm_exif/tests/src/Kernel/FileMetadataExifTest.php b/web/modules/contrib/file_mdm/file_mdm_exif/tests/src/Kernel/FileMetadataExifTest.php new file mode 100644 index 000000000..ce9a81883 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_exif/tests/src/Kernel/FileMetadataExifTest.php @@ -0,0 +1,350 @@ +<?php + +namespace Drupal\Tests\file_mdm_exif\Kernel; + +use Drupal\file_mdm\FileMetadataInterface; +use Drupal\Tests\file_mdm\Kernel\FileMetadataManagerTestBase; +use lsolesen\pel\PelEntryAscii; +use lsolesen\pel\PelEntryRational; +use lsolesen\pel\PelEntrySRational; + +/** + * Tests that File Metadata EXIF plugin works properly. + * + * @group File Metadata + */ +class FileMetadataExifTest extends FileMetadataManagerTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'system', + 'simpletest', + 'file_mdm', + 'file_mdm_exif', + 'file_test', + 'image_effects', + ]; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->installConfig(['file_mdm_exif']); + } + + /** + * Test EXIF plugin. + */ + public function testExifPlugin() { + // Prepare a copy of test files. + file_unmanaged_copy(drupal_get_path('module', 'simpletest') . '/files/image-test.jpg', 'public://', FILE_EXISTS_REPLACE); + file_unmanaged_copy(drupal_get_path('module', 'simpletest') . '/files/image-test.png', 'public://', FILE_EXISTS_REPLACE); + file_unmanaged_copy(drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', 'public://', FILE_EXISTS_REPLACE); + file_unmanaged_copy(drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', 'temporary://', FILE_EXISTS_REPLACE); + // The image files that will be tested. + $image_files = [ + [ + // Pass a path instead of the URI. + 'uri' => drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', + 'count_keys' => 48, + 'test_keys' => [ + ['Orientation', 8], + ['orientation', 8], + ['OrIeNtAtIoN', 8], + ['ShutterSpeedValue', [106, 32]], + ['ApertureValue', [128, 32]], + [['exif', 'aperturevalue'], [128, 32]], + [[2, 'aperturevalue'], [128, 32]], + [['exif', 0x9202], [128, 32]], + [[2, 0x9202], [128, 32]], + ], + ], + [ + // Pass a URI. + 'uri' => 'public://test-exif.jpeg', + 'count_keys' => 48, + 'test_keys' => [ + ['Orientation', 8], + ['ShutterSpeedValue', [106, 32]], + ], + ], + [ + // Remote storage file. Let the file be copied to a local temp copy. + 'uri' => 'dummy-remote://test-exif.jpeg', + 'copy_to_temp' => TRUE, + 'count_keys' => 48, + 'test_keys' => [ + ['Orientation', 8], + ['ShutterSpeedValue', [106, 32]], + ], + ], + [ + // JPEG Image with GPS data. + 'uri' => drupal_get_path('module', 'file_mdm') . '/tests/files/1024-2006_1011_093752.jpg', + 'count_keys' => 59, + 'test_keys' => [ + ['Orientation', 1], + ['FocalLength', [8513, 256]], + ['GPSLatitudeRef', 'S'], + ['GPSLatitude', [[33, 1], [51, 1], [2191, 100]]], + ['GPSLongitudeRef', 'E'], + ['GPSLongitude', [[151, 1], [13, 1], [1173, 100]]], + ], + ], + [ + // JPEG Image with no EXIF data. + 'uri' => 'public://image-test.jpg', + 'count_keys' => 0, + 'test_keys' => [], + ], + [ + // TIFF image. + 'uri' => drupal_get_path('module', 'file_mdm') . '/tests/files/sample-1.tiff', + 'count_keys' => 11, + 'test_keys' => [ + ['Orientation', 1], + ['BitsPerSample', [8, 8, 8, 8]], + ], + ], + [ + // PNG should not have any data. + 'uri' => 'public://image-test.png', + 'count_keys' => 0, + 'test_keys' => [], + ], + ]; + + $fmdm = $this->container->get('file_metadata_manager'); + + // Walk through test files. + foreach ($image_files as $image_file) { + $file_metadata = $fmdm->uri($image_file['uri']); + if (!$file_metadata) { + $this->fail("File not found: {$image_file['uri']}"); + continue; + } + if (isset($image_file['copy_to_temp'])) { + $file_metadata->copyUriToTemp(); + } + $this->assertEqual($image_file['count_keys'], $this->countMetadataKeys($file_metadata, 'exif')); + foreach ($image_file['test_keys'] as $test) { + $entry = $file_metadata->getMetadata('exif', $test[0]); + $this->assertEqual($test[1], $entry ? $entry['value'] : NULL); + } + } + + // Test loading metadata from an in-memory object. + $file_metadata_from = $fmdm->uri($image_files[0]['uri']); + $metadata = $file_metadata_from->getMetadata('exif'); + $new_file_metadata = $fmdm->uri('public://test-output.jpeg'); + $new_file_metadata->loadMetadata('exif', $metadata); + $this->assertEqual($image_files[0]['count_keys'], $this->countMetadataKeys($new_file_metadata, 'exif')); + foreach ($image_files[0]['test_keys'] as $test) { + $entry = $file_metadata->getMetadata('exif', $test[0]); + $this->assertEqual($test[1], $new_file_metadata->getMetadata('exif', $test[0])['value']); + } + + // Test removing metadata. + $fmdm->release($image_files[0]['uri']); + $this->assertFalse($fmdm->has($image_files[0]['uri'])); + $file_metadata = $fmdm->uri($image_files[0]['uri']); + $this->assertEqual($image_files[0]['count_keys'], $this->countMetadataKeys($file_metadata, 'exif')); + $this->assertTrue($file_metadata->removeMetadata('exif', 'shutterspeedValue')); + $this->assertTrue($file_metadata->removeMetadata('exif', 'apertureValue')); + $this->assertFalse($file_metadata->removeMetadata('exif', 'bar')); + $this->assertEqual($image_files[0]['count_keys'] - 2, $this->countMetadataKeys($file_metadata, 'exif')); + $this->assertNull($file_metadata->getMetadata('exif', 'shutterspeedValue')); + $this->assertNull($file_metadata->getMetadata('exif', 'apertureValue')); + $this->assertNotNull($file_metadata->getMetadata('exif', 'orientation')); + } + + /** + * Test writing metadata to JPEG file. + */ + public function testJpegExifSaveToFile() { + $fmdm = $this->container->get('file_metadata_manager'); + + // Copy test file to public://. + file_unmanaged_copy(drupal_get_path('module', 'image_effects') . '/tests/images/portrait-painting.jpg', 'public://', FILE_EXISTS_REPLACE); + $file_uri = 'public://portrait-painting.jpg'; + $file_metadata = $fmdm->uri($file_uri); + + // Check values via exif_read_data before operations. + $data = @exif_read_data($file_uri); + $this->assertEqual(8, $data['Orientation']); + $this->assertFalse(isset($data['Artist'])); + $this->assertEqual('Canon', $data['Make']); + $this->assertEqual(800, $data['ISOSpeedRatings']); + + // Change the Orientation tag from IFD0. + $this->assertEqual(8, $file_metadata->getMetadata('exif', 'orientation')['value']); + $this->assertTrue($file_metadata->setMetadata('exif', 'orientation', 4)); + $this->assertEqual(4, $file_metadata->getMetadata('exif', 'orientation')['value']); + // Add the Artist tag to IFD0. + $this->assertEqual(48, $this->countMetadataKeys($file_metadata, 'exif')); + $this->assertNull($file_metadata->getMetadata('exif', 'artist')); + $artist_tag = $this->container->get('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag('artist'); + $artist = new PelEntryAscii($artist_tag['tag'], 'shot by foo!'); + $file_metadata->setMetadata('exif', 'artist', $artist); + $this->assertEqual('shot by foo!', $file_metadata->getMetadata('exif', 'artist')['value']); + $this->assertEqual(49, $this->countMetadataKeys($file_metadata, 'exif')); + // Setting an unknown tag leads to failure. + $this->assertFalse($file_metadata->setMetadata('exif', 'bar', 'qux')); + // Remove the Make tag from IFD0. + $this->assertEqual('Canon', $file_metadata->getMetadata('exif', 'make')['value']); + $this->assertTrue($file_metadata->removeMetadata('exif', 'make')); + $this->assertNull($file_metadata->getMetadata('exif', 'make')); + $this->assertEqual(48, $this->countMetadataKeys($file_metadata, 'exif')); + + // Add the ImageDescription tag to IFD1. + $this->assertNull($file_metadata->getMetadata('exif', [1, 'imagedescription'])); + $desc_tag = $this->container->get('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag([1, 'imagedescription']); + $desc = new PelEntryAscii($desc_tag['tag'], 'awesome!'); + $file_metadata->setMetadata('exif', [1, 'imagedescription'], $desc); + $this->assertEqual('awesome!', $file_metadata->getMetadata('exif', [1, 'imagedescription'])['value']); + $this->assertEqual(49, $this->countMetadataKeys($file_metadata, 'exif')); + // Remove the Compression tag from IFD1. + $this->assertEqual(6, $file_metadata->getMetadata('exif', [1, 'compression'])['value']); + $this->assertTrue($file_metadata->removeMetadata('exif', [1, 'compression'])); + $this->assertNull($file_metadata->getMetadata('exif', [1, 'compression'])); + $this->assertEqual(48, $this->countMetadataKeys($file_metadata, 'exif')); + + // Add the BrightnessValue tag to EXIF. + $this->assertNull($file_metadata->getMetadata('exif', ['exif', 'brightnessvalue'])); + $brightness_tag = $this->container->get('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag(['exif', 'brightnessvalue']); + $brightness = new PelEntrySRational($brightness_tag['tag'], [12, 4]); + $file_metadata->setMetadata('exif', ['exif', 'brightnessvalue'], $brightness); + $this->assertEqual('12/4', $file_metadata->getMetadata('exif', ['exif', 'brightnessvalue'])['text']); + $this->assertEqual(49, $this->countMetadataKeys($file_metadata, 'exif')); + // Remove the ISOSpeedRatings tag from EXIF. + $this->assertEqual(800, $file_metadata->getMetadata('exif', ['exif', 'isospeedratings'])['value']); + $this->assertTrue($file_metadata->removeMetadata('exif', ['exif', 'isospeedratings'])); + $this->assertNull($file_metadata->getMetadata('exif', ['exif', 'isospeedratings'])); + $this->assertEqual(48, $this->countMetadataKeys($file_metadata, 'exif')); + + // Add the RelatedImageFileFormat tag to INTEROP. + $this->assertNull($file_metadata->getMetadata('exif', ['interop', 'RelatedImageFileFormat'])); + $ff_tag = $this->container->get('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag(['interop', 'RelatedImageFileFormat']); + $ff = new PelEntryAscii($ff_tag['tag'], 'qux'); + $file_metadata->setMetadata('exif', ['interop', 'RelatedImageFileFormat'], $ff); + $this->assertEqual('qux', $file_metadata->getMetadata('exif', ['interop', 'RelatedImageFileFormat'])['text']); + $this->assertEqual(49, $this->countMetadataKeys($file_metadata, 'exif')); + // Remove the InteroperabilityIndex tag from INTEROP. + $this->assertEqual('R98', $file_metadata->getMetadata('exif', ['interop', 'InteroperabilityIndex'])['value']); + $this->assertTrue($file_metadata->removeMetadata('exif', ['interop', 'InteroperabilityIndex'])); + $this->assertNull($file_metadata->getMetadata('exif', ['interop', 'InteroperabilityIndex'])); + $this->assertEqual(48, $this->countMetadataKeys($file_metadata, 'exif')); + + // Add Longitude/Latitude tags to GPS. + $this->assertNull($file_metadata->getMetadata('exif', 'GPSLatitudeRef')); + $this->assertNull($file_metadata->getMetadata('exif', 'GPSLatitude')); + $this->assertNull($file_metadata->getMetadata('exif', 'GPSLongitudeRef')); + $this->assertNull($file_metadata->getMetadata('exif', 'GPSLongitude')); + $atr_tag = $this->container->get('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag('GPSLatitudeRef'); + $at_tag = $this->container->get('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag('GPSLatitude'); + $otr_tag = $this->container->get('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag('GPSLongitudeRef'); + $ot_tag = $this->container->get('file_mdm_exif.tag_mapper')->resolveKeyToIfdAndTag('GPSLongitude'); + $atr = new PelEntryAscii($atr_tag['tag'], 'N'); + $at = new PelEntryRational($at_tag['tag'], [46, 1], [37, 1], [59448, 10000]); + $otr = new PelEntryAscii($otr_tag['tag'], 'E'); + $ot = new PelEntryRational($ot_tag['tag'], [12, 1], [17, 1], [488112, 10000]); + $file_metadata->setMetadata('exif', 'GPSLatitudeRef', $atr); + $file_metadata->setMetadata('exif', 'GPSLatitude', $at); + $file_metadata->setMetadata('exif', 'GPSLongitudeRef', $otr); + $file_metadata->setMetadata('exif', 'GPSLongitude', $ot); + $this->assertEqual('N', $file_metadata->getMetadata('exif', 'GPSLatitudeRef')['text']); + $this->assertNotNull($file_metadata->getMetadata('exif', 'GPSLatitude')['text']); + $this->assertEqual('E', $file_metadata->getMetadata('exif', 'GPSLongitudeRef')['text']); + $this->assertNotNull($file_metadata->getMetadata('exif', 'GPSLongitude')['text']); + $this->assertEqual(52, $this->countMetadataKeys($file_metadata, 'exif')); + + // Save metadata to file. + $this->assertTrue($file_metadata->saveMetadataToFile('exif')); + + // Check results via exif_read_data. + $data = @exif_read_data($file_uri); + $this->assertEqual(4, $data['Orientation']); + $this->assertEqual('shot by foo!', $data['Artist']); + $this->assertFalse(isset($data['Make'])); + $this->assertEqual('12/4', $data['BrightnessValue']); + $this->assertFalse(isset($data['ISOSpeedRatings'])); + $this->assertEqual('qux', $data['RelatedFileFormat']); + $this->assertFalse(isset($data['InterOperabilityIndex'])); + $this->assertEqual('N', $data['GPSLatitudeRef']); + $this->assertEqual(['46/1', '37/1', '59448/10000'], $data['GPSLatitude']); + $this->assertEqual('E', $data['GPSLongitudeRef']); + $this->assertEqual(['12/1', '17/1', '488112/10000'], $data['GPSLongitude']); + + // Test writing metadata to a file that has no EXIF info. + file_unmanaged_copy(drupal_get_path('module', 'simpletest') . '/files/image-2.jpg', 'public://', FILE_EXISTS_REPLACE); + $test_2 = $fmdm->uri('public://image-2.jpg'); + $this->assertEqual(0, $this->countMetadataKeys($test_2, 'exif')); + // Load EXIF metadata from previous file processed. + $test_2->loadMetadata('exif', $file_metadata->getMetadata('exif')); + // Save metadata to file. + $this->assertTrue($test_2->saveMetadataToFile('exif')); + $this->assertEqual(52, $this->countMetadataKeys($test_2, 'exif')); + // Check results via exif_read_data. + $data = @exif_read_data('public://image-2.jpg'); + $this->assertEqual(4, $data['Orientation']); + $this->assertEqual('shot by foo!', $data['Artist']); + $this->assertEqual('12/4', $data['BrightnessValue']); + $this->assertEqual('qux', $data['RelatedFileFormat']); + $this->assertEqual('N', $data['GPSLatitudeRef']); + $this->assertEqual(['46/1', '37/1', '59448/10000'], $data['GPSLatitude']); + $this->assertEqual('E', $data['GPSLongitudeRef']); + $this->assertEqual(['12/1', '17/1', '488112/10000'], $data['GPSLongitude']); + + // Check that after save, file metadata is retrieved from file first time, + // then from cache in further requests. + $file_metadata = NULL; + $this->assertTrue($fmdm->release($file_uri)); + $file_metadata = $fmdm->uri($file_uri); + $this->assertEqual(52, $this->countMetadataKeys($file_metadata, 'exif')); + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $file_metadata->isMetadataLoaded('exif')); + $file_metadata = NULL; + $this->assertTrue($fmdm->release($file_uri)); + $file_metadata = $fmdm->uri($file_uri); + $this->assertEqual(52, $this->countMetadataKeys($file_metadata, 'exif')); + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $file_metadata->isMetadataLoaded('exif')); + } + + /** + * Test writing metadata to TIFF file. + */ + public function testTiffExifSaveToFile() { + $fmdm = $this->container->get('file_metadata_manager'); + + // Copy test file to public://. + file_unmanaged_copy(drupal_get_path('module', 'file_mdm') . '/tests/files/sample-1.tiff', 'public://', FILE_EXISTS_REPLACE); + $file_uri = 'public://sample-1.tiff'; + $file_metadata = $fmdm->uri($file_uri); + + // Check values via exif_read_data before operations. + $data = @exif_read_data($file_uri); + $this->assertEqual(1, $data['Orientation']); + $this->assertEqual(2, $data['PhotometricInterpretation']); + + // Change tags from IFD0. + $this->assertEqual(1, $file_metadata->getMetadata('exif', 'orientation')['value']); + $this->assertTrue($file_metadata->setMetadata('exif', 'orientation', 4)); + $this->assertEqual(4, $file_metadata->getMetadata('exif', 'orientation')['value']); + $this->assertEqual(2, $file_metadata->getMetadata('exif', 'PhotometricInterpretation')['value']); + $this->assertTrue($file_metadata->setMetadata('exif', 'PhotometricInterpretation', 4)); + $this->assertEqual(4, $file_metadata->getMetadata('exif', 'PhotometricInterpretation')['value']); + + // Save metadata to file. + $this->assertTrue($file_metadata->saveMetadataToFile('exif')); + + // Check results via exif_read_data. + $data = @exif_read_data($file_uri); + $this->assertEqual(4, $data['Orientation']); + $this->assertEqual(4, $data['PhotometricInterpretation']); + } + +} diff --git a/web/modules/contrib/file_mdm/file_mdm_font/README.md b/web/modules/contrib/file_mdm/file_mdm_font/README.md new file mode 100644 index 000000000..05036276c --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_font/README.md @@ -0,0 +1,68 @@ +# File metadata Font + +A Drupal 8 module providing a file metadata plugin to retrieve information from +font files. + + +## Features: + +Uses the [PHP Font Lib](https://github.com/PhenX/php-font-lib) to read font +information from TTF/OTF/WOFF font files. + + +## Usage examples: + +1. Use the _file_metadata_manager_ service to prepare collecting metadata for + the font located at a desired URI: + + ```php + $fmdm = \Drupal::service('file_metadata_manager'); + $my_font_metadata = $fmdm->uri('public::/my_font_directory/arial.ttf'); + ... + ``` + +2. Get the value of a key: + + ```php + ... + $font_name = $my_font_metadata->getMetadata('font', 'FontName'); + ... + ``` + +3. Get an array with all the metadata values: + + ```php + ... + $my_font_info = []; + foreach ($my_font_metadata->getSupportedKeys('font') as $key) { + $my_font_info[$key] = $my_font_metadata->getMetadata('font', $key); + } + ... + ``` + + +## Available metadata keys: + +Key | +--------------------| +FontType | +FontWeight | +Copyright | +FontName | +FontSubfamily | +UniqueID | +FullName | +Version | +PostScriptName | +Trademark | +Manufacturer | +Designer | +Description | +FontVendorURL | +FontDesignerURL | +LicenseDescription | +LicenseURL | +PreferredFamily | +PreferredSubfamily | +CompatibleFullName | +SampleText | diff --git a/web/modules/contrib/file_mdm/file_mdm_font/config/install/file_mdm_font.file_metadata_plugin.font.yml b/web/modules/contrib/file_mdm/file_mdm_font/config/install/file_mdm_font.file_metadata_plugin.font.yml new file mode 100644 index 000000000..394780947 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_font/config/install/file_mdm_font.file_metadata_plugin.font.yml @@ -0,0 +1,7 @@ +configuration: + cache: + override: FALSE + settings: + enabled: TRUE + expiration: 172800 + disallowed_paths: { } diff --git a/web/modules/contrib/file_mdm/file_mdm_font/config/schema/file_mdm_font.schema.yml b/web/modules/contrib/file_mdm/file_mdm_font/config/schema/file_mdm_font.schema.yml new file mode 100644 index 000000000..b9ac21334 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_font/config/schema/file_mdm_font.schema.yml @@ -0,0 +1,9 @@ +# file_mdm_font module schema + +file_mdm_font.file_metadata_plugin.font: + type: config_object + label: 'font file metadata plugin settings' + mapping: + configuration: + type: file_mdm.plugin.configuration + label: 'font plugin settings' diff --git a/web/modules/contrib/file_mdm/file_mdm_font/file_mdm_font.info.yml b/web/modules/contrib/file_mdm/file_mdm_font/file_mdm_font.info.yml new file mode 100644 index 000000000..45171ea53 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_font/file_mdm_font.info.yml @@ -0,0 +1,17 @@ +type: module +# core: 8.x +name: File metadata - Font +description: 'Provides a file metadata plugin for TTF/OTF/WOFF font information.' +package: File metadata +configure: file_mdm.settings +php: 5.6 +dependencies: + - file_mdm +test_dependencies: + - image_effects + +# Information added by Drupal.org packaging script on 2017-02-28 +version: '8.x-1.1' +core: '8.x' +project: 'file_mdm' +datestamp: 1488273795 diff --git a/web/modules/contrib/file_mdm/file_mdm_font/src/Plugin/FileMetadata/Font.php b/web/modules/contrib/file_mdm/file_mdm_font/src/Plugin/FileMetadata/Font.php new file mode 100644 index 000000000..93f9ca564 --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_font/src/Plugin/FileMetadata/Font.php @@ -0,0 +1,113 @@ +<?php + +namespace Drupal\file_mdm_font\Plugin\FileMetadata; + +use Drupal\file_mdm\FileMetadataException; +use Drupal\file_mdm\Plugin\FileMetadata\FileMetadataPluginBase; +use FontLib\Font as LibFont; +use FontLib\Table\Type\name; + +/** + * FileMetadata plugin for TTF/OTF/WOFF font information. + * + * Uses the 'PHP Font Lib' library (PhenX/php-font-lib). + * + * @FileMetadata( + * id = "font", + * title = @Translation("Font"), + * help = @Translation("File metadata plugin for TTF/OTF/WOFF font information, using the PHP Font Lib."), + * ) + */ +class Font extends FileMetadataPluginBase { + + /** + * {@inheritdoc} + */ + public function getSupportedKeys($options = NULL) { + return array_merge(['FontType', 'FontWeight'], array_values(name::$nameIdCodes)); + } + + /** + * {@inheritdoc} + */ + protected function doGetMetadataFromFile() { + $font = LibFont::load($this->getLocalTempPath()); + // @todo ::parse raises 'Undefined offset' notices in phenx/php-font-lib + // 0.5, suppress errors while upstream is fixed. + @$font->parse(); + $keys = $this->getSupportedKeys(); + $metadata = []; + foreach ($keys as $key) { + $l_key = strtolower($key); + switch ($l_key) { + case 'fonttype': + $metadata[$l_key] = $font->getFontType(); + break; + + case 'fontweight': + $metadata[$l_key] = $font->getFontWeight(); + break; + + default: + $code = array_search($l_key, array_map('strtolower', name::$nameIdCodes), TRUE); + if ($value = $font->getNameTableString($code)) { + $metadata[$l_key] = $value; + } + break; + + } + } + return $metadata; + } + + /** + * Validates a file metadata key. + * + * @return bool + * TRUE if the key is valid. + * + * @throws \Drupal\file_mdm\FileMetadataException + * In case the key is invalid. + */ + protected function validateKey($key, $method) { + if (!is_string($key)) { + throw new FileMetadataException("Invalid metadata key specified", $this->getPluginId(), $method); + } + if (!in_array(strtolower($key), array_map('strtolower', $this->getSupportedKeys()), TRUE)) { + throw new FileMetadataException("Invalid metadata key '{$key}' specified", $this->getPluginId(), $method); + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function doGetMetadata($key = NULL) { + if ($key === NULL) { + return $this->metadata; + } + else { + $this->validateKey($key, __FUNCTION__); + $l_key = strtolower($key); + if (in_array($l_key, array_map('strtolower', $this->getSupportedKeys()), TRUE)) { + return isset($this->metadata[$l_key]) ? $this->metadata[$l_key] : NULL; + } + return NULL; + } + } + + /** + * {@inheritdoc} + */ + protected function doSetMetadata($key, $value) { + throw new FileMetadataException('Changing font metadata is not supported', $this->getPluginId()); + } + + /** + * {@inheritdoc} + */ + protected function doRemoveMetadata($key) { + throw new FileMetadataException('Deleting font metadata is not supported', $this->getPluginId()); + } + +} diff --git a/web/modules/contrib/file_mdm/file_mdm_font/tests/src/Kernel/FileMetadataFontTest.php b/web/modules/contrib/file_mdm/file_mdm_font/tests/src/Kernel/FileMetadataFontTest.php new file mode 100644 index 000000000..1cb24647e --- /dev/null +++ b/web/modules/contrib/file_mdm/file_mdm_font/tests/src/Kernel/FileMetadataFontTest.php @@ -0,0 +1,114 @@ +<?php + +namespace Drupal\Tests\file_mdm_font\Kernel; + +use Drupal\file_mdm\FileMetadataInterface; +use Drupal\Tests\file_mdm\Kernel\FileMetadataManagerTestBase; + +/** + * Tests that the file metadata 'font' plugin works properly. + * + * @group File Metadata + */ +class FileMetadataFontTest extends FileMetadataManagerTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'system', + 'simpletest', + 'file_mdm', + 'file_mdm_font', + 'file_test', + 'image_effects', + ]; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->installConfig(['file_mdm_font']); + } + + /** + * Test 'font' plugin. + */ + public function testFontPlugin() { + // The font files that will be tested. + $font_files = [ + [ + 'uri' => drupal_get_path('module', 'image_effects') . '/tests/fonts/LinLibertineTTF_5.3.0_2012_07_02/LinLibertine_Rah.ttf', + 'count_keys' => 15, + 'test_keys' => [ + ['Version', 'Version 5.3.0 ; ttfautohint (v0.9)'], + ['version', 'Version 5.3.0 ; ttfautohint (v0.9)'], + ['VeRsIoN', 'Version 5.3.0 ; ttfautohint (v0.9)'], + ['FontWeight', 400], + ], + ], + [ + 'uri' => drupal_get_path('module', 'image_effects') . '/tests/fonts/LinLibertineTTF_5.3.0_2012_07_02/LinBiolinum_Kah.ttf', + 'count_keys' => 15, + 'test_keys' => [ + ['FullName', 'Linux Biolinum Keyboard'], + ['fullname', 'Linux Biolinum Keyboard'], + ['fUlLnAmE', 'Linux Biolinum Keyboard'], + ], + ], + ]; + + $fmdm = $this->container->get('file_metadata_manager'); + + // Walk through test files. + foreach ($font_files as $font_file) { + $file_metadata = $fmdm->uri($font_file['uri']); + if (!$file_metadata) { + $this->fail("File not found: {$font_file['uri']}"); + continue; + } + $this->assertEqual($font_file['count_keys'], $this->countMetadataKeys($file_metadata, 'font')); + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $file_metadata->isMetadataLoaded('font')); + foreach ($font_file['test_keys'] as $test) { + $this->assertEqual($test[1], $file_metadata->getMetadata('font', $test[0])); + } + } + } + + /** + * Test 'font' plugin supported keys. + */ + public function testSupportedKeys() { + $expected_keys = [ + 'FontType', + 'FontWeight', + 'Copyright', + 'FontName', + 'FontSubfamily', + 'UniqueID', + 'FullName', + 'Version', + 'PostScriptName', + 'Trademark', + 'Manufacturer', + 'Designer', + 'Description', + 'FontVendorURL', + 'FontDesignerURL', + 'LicenseDescription', + 'LicenseURL', + 'PreferredFamily', + 'PreferredSubfamily', + 'CompatibleFullName', + 'SampleText', + ]; + + $fmdm = $this->container->get('file_metadata_manager'); + $file_md = $fmdm->uri(drupal_get_path('module', 'image_effects') . '/tests/fonts/LinLibertineTTF_5.3.0_2012_07_02/LinLibertine_Rah.ttf'); + $this->assertEqual($expected_keys, $file_md->getSupportedKeys('font')); + } + +} diff --git a/web/modules/contrib/file_mdm/phpcs.xml.dist b/web/modules/contrib/file_mdm/phpcs.xml.dist new file mode 100644 index 000000000..20e1cb4cd --- /dev/null +++ b/web/modules/contrib/file_mdm/phpcs.xml.dist @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ruleset name="file_mdm"> + <description>PHP CodeSniffer configuration for file_mdm module.</description> + <file>.</file> + <arg name="extensions" value="inc,install,module,php,profile,test,theme"/> + + <!--Exclude README.md.--> + <exclude-pattern>./README.md</exclude-pattern> + + <!--Use Drupal code rule.--> + <rule ref="Drupal"></rule> + +</ruleset> diff --git a/web/modules/contrib/file_mdm/src/Element/FileMetadataCaching.php b/web/modules/contrib/file_mdm/src/Element/FileMetadataCaching.php new file mode 100644 index 000000000..12fb974df --- /dev/null +++ b/web/modules/contrib/file_mdm/src/Element/FileMetadataCaching.php @@ -0,0 +1,112 @@ +<?php + +namespace Drupal\file_mdm\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\FormElement; + +/** + * Implements a form element to enable capturing cache information for file_mdm. + * + * @FormElement("file_mdm_caching") + */ +class FileMetadataCaching extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return [ + '#input' => TRUE, + '#process' => [[$class, 'processCaching']], + '#element_validate' => [[$class, 'validateCaching']], + ]; + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input !== FALSE && $input !== NULL) { + $disallowed_paths = $input['disallowed_paths']; + if (!empty($disallowed_paths)) { + $disallowed_paths = preg_replace('/\r/', '', $disallowed_paths); + $disallowed_paths = explode("\n", $disallowed_paths); + while (empty($disallowed_paths[count($disallowed_paths) - 1])) { + array_pop($disallowed_paths); + } + $input['disallowed_paths'] = $disallowed_paths ?: []; + } + else { + $input['disallowed_paths'] = []; + } + return $input; + } + return NULL; + } + + /** + * Processes a 'file_mdm_caching' form element. + * + * @param array $element + * The form element to process. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The processed element. + */ + public static function processCaching(array &$element, FormStateInterface $form_state, array &$complete_form) { + $element['enabled'] = [ + '#type' => 'checkbox', + '#title' => t('Cache metadata'), + '#default_value' => $element['#default_value']['enabled'], + '#description' => t("If selected, metadata retrieved from files will be cached for further access."), + ]; + $options = [86400, 172800, 604800, 1209600, 3024000, 7862400]; + $options = array_map([\Drupal::service('date.formatter'), 'formatInterval'], array_combine($options, $options)); + $options = [-1 => t('Never')] + $options; + $element['expiration'] = [ + '#type' => 'select', + '#title' => t('Cache expires'), + '#default_value' => $element['#default_value']['expiration'], + '#options' => $options, + '#description' => t("Specify the required lifetime of cached entries. Longer times may lead to increased cache sizes."), + '#states' => [ + 'visible' => [ + ':input[name="' . $element['#name'] . '[enabled]"]' => array('checked' => TRUE), + ], + ], + ]; + $element['disallowed_paths'] = [ + '#type' => 'textarea', + '#title' => t('Excluded paths'), + '#rows' => 3, + '#default_value' => implode("\n", $element['#default_value']['disallowed_paths']), + '#description' => t("Only files prefixed by a valid URI scheme will be cached, like for example <kbd>public://</kbd>. Files in the <kbd>temporary://</kbd> scheme will never be cached. Specify here if there are any paths to be additionally <strong>excluded</strong> from caching, one per line. Use wildcard patterns when entering the path. For example, <kbd>public://styles/*</kbd>."), + '#states' => [ + 'visible' => [ + ':input[name="' . $element['#name'] . '[enabled]"]' => array('checked' => TRUE), + ], + ], + ]; + + return $element; + } + + /** + * Form element validation handler. + */ + public static function validateCaching(&$element, FormStateInterface $form_state, &$complete_form) { + // Validate cache exclusion paths. + foreach ($element['#value']['disallowed_paths'] as $path) { + if (!file_valid_uri($path)) { + $form_state->setError($element['disallowed_paths'], t("'@path' is an invalid URI path", ['@path' => $path])); + } + } + } + +} diff --git a/web/modules/contrib/file_mdm/src/FileMetadata.php b/web/modules/contrib/file_mdm/src/FileMetadata.php new file mode 100644 index 000000000..4f2444f51 --- /dev/null +++ b/web/modules/contrib/file_mdm/src/FileMetadata.php @@ -0,0 +1,276 @@ +<?php + +namespace Drupal\file_mdm; + +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\File\FileSystemInterface; +use Drupal\file_mdm\Plugin\FileMetadataPluginManager; +use Psr\Log\LoggerInterface; + +/** + * A file metadata object. + */ +class FileMetadata implements FileMetadataInterface { + + /** + * The FileMetadata plugin manager. + * + * @var \Drupal\file_mdm\Plugin\FileMetadataPluginManager + */ + protected $pluginManager; + + /** + * The file_mdm logger. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * The file system service. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $fileSystem; + + /** + * The URI of the file. + * + * @var string + */ + protected $uri = ''; + + /** + * The hash used to reference the URI. + * + * @var string + */ + protected $hash; + + /** + * The local filesystem path to the file. + * + * This is used to allow accessing local copies of files stored remotely, to + * minimise remote calls and allow functions that cannot access remote stream + * wrappers to operate locally. + * + * @var string + */ + protected $localTempPath; + + /** + * The array of FileMetadata plugins for this URI. + * + * @var \Drupal\file_mdm\Plugin\FileMetadataPluginInterface[] + */ + protected $plugins = []; + + /** + * Constructs a FileMetadata object. + * + * @param \Drupal\file_mdm\Plugin\FileMetadataPluginManager $plugin_manager + * The file metadata plugin manager. + * @param \Psr\Log\LoggerInterface $logger + * The logger service. + * @param \Drupal\Core\File\FileSystemInterface $file_system + * The file system service. + * @param string $uri + * The URI of the file. + * @param string $hash + * The hash used to reference the URI by file_mdm. + */ + public function __construct(FileMetadataPluginManager $plugin_manager, LoggerInterface $logger, FileSystemInterface $file_system, $uri, $hash) { + $this->pluginManager = $plugin_manager; + $this->logger = $logger; + $this->fileSystem = $file_system; + $this->uri = $uri; + $this->hash = $hash; + } + + /** + * {@inheritdoc} + */ + public function getUri() { + return $this->uri; + } + + /** + * {@inheritdoc} + */ + public function getLocalTempPath() { + return $this->localTempPath; + } + + /** + * {@inheritdoc} + */ + public function setLocalTempPath($temp_uri) { + $this->localTempPath = $temp_uri; + foreach ($this->plugins as $plugin) { + $plugin->setLocalTempPath($this->localTempPath); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function copyUriToTemp($temp_uri = NULL) { + if ($temp_uri === NULL) { + $temp_uri = $this->fileSystem->tempnam('temporary://', 'file_mdm_'); + $this->fileSystem->unlink($temp_uri); + $temp_uri .= '.' . pathinfo($this->getUri(), PATHINFO_EXTENSION); + } + if ($temp_path = file_unmanaged_copy($this->getUri(), $this->fileSystem->realpath($temp_uri), FILE_EXISTS_REPLACE)) { + $this->setLocalTempPath($temp_path); + } + return (bool) $temp_path; + } + + /** + * {@inheritdoc} + */ + public function copyTempToUri() { + if (($temp_path = $this->getLocalTempPath()) === NULL) { + return FALSE; + } + return (bool) file_unmanaged_copy($temp_path, $this->getUri(), FILE_EXISTS_REPLACE); + } + + /** + * {@inheritdoc} + */ + public function getFileMetadataPlugin($metadata_id) { + if (!isset($this->plugins[$metadata_id])) { + try { + $this->plugins[$metadata_id] = $this->pluginManager->createInstance($metadata_id); + $this->plugins[$metadata_id]->setUri($this->uri); + $this->plugins[$metadata_id]->setLocalTempPath($this->localTempPath ?: $this->uri); + $this->plugins[$metadata_id]->setHash($this->hash); + } + catch (PluginNotFoundException $e) { + return NULL; + } + } + return $this->plugins[$metadata_id]; + } + + /** + * {@inheritdoc} + */ + public function getSupportedKeys($metadata_id, $options = NULL) { + try { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + $keys = $plugin->getSupportedKeys($options); + } + else { + $keys = NULL; + } + } + catch (\Exception $e) { + $this->logger->error($e->getMessage()); + $keys = NULL; + } + return $keys; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($metadata_id, $key = NULL) { + try { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + $metadata = $plugin->getMetadata($key); + } + else { + $metadata = NULL; + } + } + catch (\Exception $e) { + $this->logger->error($e->getMessage()); + $metadata = NULL; + } + return $metadata; + } + + /** + * {@inheritdoc} + */ + public function removeMetadata($metadata_id, $key) { + try { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + return $plugin->removeMetadata($key); + } + } + catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function setMetadata($metadata_id, $key, $value) { + try { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + return $plugin->setMetadata($key, $value); + } + } + catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function isMetadataLoaded($metadata_id) { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + return $plugin->isMetadataLoaded(); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function loadMetadata($metadata_id, $metadata) { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + return $plugin->loadMetadata($metadata); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function loadMetadataFromCache($metadata_id) { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + return $plugin->loadMetadataFromCache(); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function saveMetadataToCache($metadata_id, array $tags = []) { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + return $plugin->saveMetadataToCache($tags); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function saveMetadataToFile($metadata_id) { + if ($plugin = $this->getFileMetadataPlugin($metadata_id)) { + return $plugin->saveMetadataToFile(); + } + return FALSE; + } + +} diff --git a/web/modules/contrib/file_mdm/src/FileMetadataException.php b/web/modules/contrib/file_mdm/src/FileMetadataException.php new file mode 100644 index 000000000..ac3c2b16c --- /dev/null +++ b/web/modules/contrib/file_mdm/src/FileMetadataException.php @@ -0,0 +1,20 @@ +<?php + +namespace Drupal\file_mdm; + +/** + * Exception thrown by file_mdm and plugins on failures. + */ +class FileMetadataException extends \Exception { + + /** + * Constructs a FileMetadataException object. + */ + public function __construct($message, $plugin_id = NULL, $method = NULL, \Exception $previous = NULL) { + $msg = $message; + $msg .= $plugin_id ? " (plugin: {$plugin_id})" : ""; + $msg .= $method ? " (method: {$method})" : ""; + parent::__construct($msg, 0, $previous); + } + +} diff --git a/web/modules/contrib/file_mdm/src/FileMetadataInterface.php b/web/modules/contrib/file_mdm/src/FileMetadataInterface.php new file mode 100644 index 000000000..28a28017d --- /dev/null +++ b/web/modules/contrib/file_mdm/src/FileMetadataInterface.php @@ -0,0 +1,208 @@ +<?php + +namespace Drupal\file_mdm; + +/** + * Provides an interface for file metadata objects. + */ +interface FileMetadataInterface { + + /** + * Metadata not loaded. + */ + const NOT_LOADED = 0; + + /** + * Metadata loaded by code. + */ + const LOADED_BY_CODE = 1; + + /** + * Metadata loaded from cache. + */ + const LOADED_FROM_CACHE = 2; + + /** + * Metadata loaded from file. + */ + const LOADED_FROM_FILE = 3; + + /** + * Gets the URI of the file. + * + * @return string|null + * The URI of the file, or a local path. + */ + public function getUri(); + + /** + * Gets the local filesystem URI to the temporary file. + * + * @return string|null + * The URI, or a local path, of the temporary file. + */ + public function getLocalTempPath(); + + /** + * Sets the local filesystem URI to the temporary file. + * + * @param string $temp_uri + * A URI to a temporary file. + * + * @return $this + */ + public function setLocalTempPath($temp_uri); + + /** + * Copies the file at URI to a local temporary file. + * + * @param string $temp_uri + * (optional) a URI to a temporary file. If NULL, a temp URI will be + * defined by the operation. Defaults to NULL. + * + * @return bool + * TRUE if the file was copied successfully, FALSE + * otherwise. + */ + public function copyUriToTemp($temp_uri = NULL); + + /** + * Copies the local temporary file to the destination URI. + * + * @return bool + * TRUE if the file was copied successfully, FALSE + * otherwise. + */ + public function copyTempToUri(); + + /** + * Gets a FileMetadata plugin instance. + * + * @param string $metadata_id + * The id of the plugin whose instance is to be returned. If it is does + * not exist, an instance is created. + * + * @return \Drupal\file_mdm\Plugin\FileMetadataPluginInterface|null + * The FileMetadata plugin instance. NULL if no plugin is found. + */ + public function getFileMetadataPlugin($metadata_id); + + /** + * Returns a list of supported metadata keys. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * @param mixed $options + * (optional) Allows specifying additional options to control the list of + * metadata keys returned. + * + * @return array + * A simple array of metadata keys supported. + */ + public function getSupportedKeys($metadata_id, $options = NULL); + + /** + * Gets a metadata element. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * @param mixed|null $key + * A key to determine the metadata element to be returned. If NULL, the + * entire metadata will be returned. + * + * @return mixed + * The value of the element specified by $key. If $key is NULL, the entire + * metadata. + */ + public function getMetadata($metadata_id, $key = NULL); + + /** + * Removes a metadata element. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * @param mixed $key + * A key to determine the metadata element to be removed. + * + * @return bool + * TRUE if metadata was removed successfully, FALSE otherwise. + */ + public function removeMetadata($metadata_id, $key); + + /** + * Sets a metadata element. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * @param mixed $key + * A key to determine the metadata element to be changed. + * @param mixed $value + * The value to change the metadata element to. + * + * @return bool + * TRUE if metadata was changed successfully, FALSE otherwise. + */ + public function setMetadata($metadata_id, $key, $value); + + /** + * Checks if file metadata has been already loaded. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * + * @return bool + * TRUE if metadata is loaded, FALSE otherwise. + */ + public function isMetadataLoaded($metadata_id); + + /** + * Loads file metadata. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * @param mixed $metadata + * The file metadata associated to the file at URI. + * + * @return bool + * TRUE if metadata was loaded successfully, FALSE otherwise. + */ + public function loadMetadata($metadata_id, $metadata); + + /** + * Loads file metadata from a cache entry. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * + * @return bool + * TRUE if metadata was loaded successfully, FALSE otherwise. + */ + public function loadMetadataFromCache($metadata_id); + + /** + * Caches metadata for file at URI. + * + * Uses the 'file_mdm' cache bin. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * @param array $tags + * (optional) An array of cache tags to save to cache. + * + * @return bool + * TRUE if metadata was saved successfully, FALSE otherwise. + */ + public function saveMetadataToCache($metadata_id, array $tags = []); + + /** + * Saves metadata to file at URI. + * + * @param string $metadata_id + * The id of the FileMetadata plugin. + * + * @return bool + * TRUE if metadata was saved successfully, FALSE otherwise. + */ + public function saveMetadataToFile($metadata_id); + +} diff --git a/web/modules/contrib/file_mdm/src/FileMetadataManager.php b/web/modules/contrib/file_mdm/src/FileMetadataManager.php new file mode 100644 index 000000000..97d8a6b4d --- /dev/null +++ b/web/modules/contrib/file_mdm/src/FileMetadataManager.php @@ -0,0 +1,159 @@ +<?php + +namespace Drupal\file_mdm; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\file_mdm\Plugin\FileMetadataPluginManager; +use Psr\Log\LoggerInterface; + +/** + * A service class to provide file metadata. + */ +class FileMetadataManager implements FileMetadataManagerInterface { + + use StringTranslationTrait; + + /** + * The FileMetadata plugin manager. + * + * @var \Drupal\file_mdm\Plugin\FileMetadataPluginManager + */ + protected $pluginManager; + + /** + * The file_mdm logger. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The file system service. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $fileSystem; + + /** + * The cache service. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * The array of FileMetadata objects currently in use. + * + * @var \Drupal\file_mdm\FileMetadataInterface[] + */ + protected $files = []; + + /** + * Constructs a FileMetadataManager object. + * + * @param \Drupal\file_mdm\Plugin\FileMetadataPluginManager $plugin_manager + * The FileMetadata plugin manager. + * @param \Psr\Log\LoggerInterface $logger + * The file_mdm logger. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\File\FileSystemInterface $file_system + * The file system service. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_service + * The cache service. + */ + public function __construct(FileMetadataPluginManager $plugin_manager, LoggerInterface $logger, ConfigFactoryInterface $config_factory, FileSystemInterface $file_system, CacheBackendInterface $cache_service) { + $this->pluginManager = $plugin_manager; + $this->logger = $logger; + $this->configFactory = $config_factory; + $this->fileSystem = $file_system; + $this->cache = $cache_service; + } + + /** + * Returns an hash for the URI, used internally by the manager. + * + * @param string $uri + * The URI to a file. + * + * @return string + * An hash string. + */ + protected function calculateHash($uri) { + // Sanitize URI removing duplicate slashes, if any. + // @see http://stackoverflow.com/questions/12494515/remove-unnecessary-slashes-from-path + $uri = preg_replace('/([^:])(\/{2,})/', '$1/', $uri); + // If URI is invalid and no local file path exists, return NULL. + if (!file_valid_uri($uri) && !$this->fileSystem->realpath($uri)) { + return NULL; + } + // Return a hash of the URI. + return hash('sha256', $uri); + } + + /** + * {@inheritdoc} + */ + public function has($uri) { + $hash = $this->calculateHash($uri); + return $hash ? isset($this->files[$hash]) : NULL; + } + + /** + * {@inheritdoc} + */ + public function uri($uri) { + if (!$hash = $this->calculateHash($uri)) { + return NULL; + } + if (!isset($this->files[$hash])) { + $this->files[$hash] = new FileMetadata($this->pluginManager, $this->logger, $this->fileSystem, $uri, $hash); + } + return $this->files[$hash]; + } + + /** + * {@inheritdoc} + */ + public function deleteCachedMetadata($uri) { + if (!$hash = $this->calculateHash($uri)) { + return FALSE; + } + foreach (array_keys($this->pluginManager->getDefinitions()) as $plugin_id) { + $this->cache->delete("hash:{$plugin_id}:{$hash}"); + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function release($uri) { + if (!$hash = $this->calculateHash($uri)) { + return FALSE; + } + if (isset($this->files[$hash])) { + unset($this->files[$hash]); + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function count() { + return count($this->files); + } + +} diff --git a/web/modules/contrib/file_mdm/src/FileMetadataManagerInterface.php b/web/modules/contrib/file_mdm/src/FileMetadataManagerInterface.php new file mode 100644 index 000000000..62a0b51b2 --- /dev/null +++ b/web/modules/contrib/file_mdm/src/FileMetadataManagerInterface.php @@ -0,0 +1,63 @@ +<?php + +namespace Drupal\file_mdm; + +/** + * Provides an interface for file metadata manager objects. + */ +interface FileMetadataManagerInterface { + + /** + * Determines if the URI is currently in use by the manager. + * + * @param string $uri + * The URI to a file. + * + * @return bool + * TRUE if the URI is in use, FALSE otherwise. + */ + public function has($uri); + + /** + * Returns a FileMetadata object for the URI, creating it if necessary. + * + * @param string $uri + * The URI to a file. + * + * @return \Drupal\file_mdm\FileMetadataInterface|null + * The FileMetadata object for the specified URI. + */ + public function uri($uri); + + /** + * Deletes the all the cached metadata for the URI. + * + * @param string $uri + * The URI to a file. + * + * @return bool + * TRUE if the cached metadata was removed, FALSE in case of error. + */ + public function deleteCachedMetadata($uri); + + /** + * Releases the FileMetadata object for the URI. + * + * @param string $uri + * The URI to a file. + * + * @return bool + * TRUE if the FileMetadata for the URI was removed from the manager, + * FALSE otherwise. + */ + public function release($uri); + + /** + * Returns the count of FileMetadata objects currently in use. + * + * @return int + * The number of FileMetadata objects currently in use. + */ + public function count(); + +} diff --git a/web/modules/contrib/file_mdm/src/Form/SettingsForm.php b/web/modules/contrib/file_mdm/src/Form/SettingsForm.php new file mode 100644 index 000000000..a91c170b3 --- /dev/null +++ b/web/modules/contrib/file_mdm/src/Form/SettingsForm.php @@ -0,0 +1,136 @@ +<?php + +namespace Drupal\file_mdm\Form; + +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\file_mdm\Plugin\FileMetadataPluginManager; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Configures file_mdm settings for this site. + */ +class SettingsForm extends ConfigFormBase { + + /** + * An array containing the available metadata plugins. + * + * @var \Drupal\file_mdm\Plugin\FileMetadataPluginInterface[] + */ + protected $metadataPlugins = []; + + /** + * Constructs a SettingsForm object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\file_mdm\Plugin\FileMetadataPluginManager $manager + * The file metadata plugin manager. + */ + public function __construct(ConfigFactoryInterface $config_factory, FileMetadataPluginManager $manager) { + parent::__construct($config_factory); + foreach ($manager->getDefinitions() as $id => $definition) { + $this->metadataPlugins[$id] = $manager->createInstance($id); + } + uasort($this->metadataPlugins, function ($a, $b) { + return Unicode::strcasecmp((string) $a->getPluginDefinition()['title'], (string) $b->getPluginDefinition()['title']); + }); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('plugin.manager.file_metadata') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'file_mdm_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['file_mdm.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Cache metadata. + $form['metadata_cache'] = [ + '#type' => 'details', + '#open' => TRUE, + '#collapsible' => FALSE, + '#title' => $this->t('Metadata caching'), + '#tree' => TRUE, + ]; + $form['metadata_cache']['settings'] = [ + '#type' => 'file_mdm_caching', + '#default_value' => $this->config('file_mdm.settings')->get('metadata_cache'), + ]; + + // Settings tabs. + $form['plugins'] = array( + '#type' => 'vertical_tabs', + '#tree' => FALSE, + ); + + // Load subforms from each plugin. + foreach ($this->metadataPlugins as $id => $plugin) { + $definition = $plugin->getPluginDefinition(); + $form['file_mdm_plugin_settings'][$id] = array( + '#type' => 'details', + '#title' => $definition['title'], + '#description' => $definition['help'], + '#open' => FALSE, + '#tree' => TRUE, + '#group' => 'plugins', + ); + $form['file_mdm_plugin_settings'][$id] += $plugin->buildConfigurationForm(array(), $form_state); + } + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + // Call the form validation handler for each of the plugins. + foreach ($this->metadataPlugins as $plugin) { + $plugin->validateConfigurationForm($form, $form_state); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Call the form submit handler for each of the plugins. + foreach ($this->metadataPlugins as $plugin) { + $plugin->submitConfigurationForm($form, $form_state); + } + + $this->config('file_mdm.settings')->set('metadata_cache', $form_state->getValue(['metadata_cache', 'settings'])); + + // Only save settings if they have changed to prevent unnecessary cache + // invalidations. + if ($this->config('file_mdm.settings')->getOriginal() != $this->config('file_mdm.settings')->get()) { + $this->config('file_mdm.settings')->save(); + } + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/contrib/file_mdm/src/Plugin/Annotation/FileMetadata.php b/web/modules/contrib/file_mdm/src/Plugin/Annotation/FileMetadata.php new file mode 100644 index 000000000..7632ce004 --- /dev/null +++ b/web/modules/contrib/file_mdm/src/Plugin/Annotation/FileMetadata.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\file_mdm\Plugin\Annotation; + +use Drupal\Component\Annotation\Plugin; + +/** + * Defines a Plugin annotation object for FileMetadata plugins. + * + * @Annotation + */ +class FileMetadata extends Plugin { + + /** + * The plugin ID. + * + * @var string + */ + public $id; + + /** + * The title of the plugin. + * + * The string should be wrapped in a @Translation(). + * + * @var \Drupal\Core\Annotation\Translation + * + * @ingroup plugin_translatable + */ + public $title; + + /** + * An informative description of the plugin. + * + * The string should be wrapped in a @Translation(). + * + * @var \Drupal\Core\Annotation\Translation + * + * @ingroup plugin_translatable + */ + public $help; + +} diff --git a/web/modules/contrib/file_mdm/src/Plugin/FileMetadata/FileMetadataPluginBase.php b/web/modules/contrib/file_mdm/src/Plugin/FileMetadata/FileMetadataPluginBase.php new file mode 100644 index 000000000..fd2a0ddea --- /dev/null +++ b/web/modules/contrib/file_mdm/src/Plugin/FileMetadata/FileMetadataPluginBase.php @@ -0,0 +1,538 @@ +<?php + +namespace Drupal\file_mdm\Plugin\FileMetadata; + +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\PluginBase; +use Drupal\file_mdm\FileMetadataException; +use Drupal\file_mdm\FileMetadataInterface; +use Drupal\file_mdm\Plugin\FileMetadataPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Abstract implementation of a base File Metadata plugin. + */ +abstract class FileMetadataPluginBase extends PluginBase implements FileMetadataPluginInterface { + + /** + * The cache service. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The URI of the file. + * + * @var string + */ + protected $uri; + + /** + * The local filesystem path to the file. + * + * This is used to allow accessing local copies of files stored remotely, to + * minimise remote calls and allow functions that cannot access remote stream + * wrappers to operate locally. + * + * @var string + */ + protected $localTempPath; + + /** + * The hash used to reference the URI. + * + * @var string + */ + protected $hash; + + /** + * The metadata of the file. + * + * @var mixed + */ + protected $metadata = NULL; + + /** + * The metadata loading status. + * + * @var int + */ + protected $isMetadataLoaded = FileMetadataInterface::NOT_LOADED; + + /** + * Track if metadata has been changed from version on file. + * + * @var bool + */ + protected $hasMetadataChangedFromFileVersion = FALSE; + + /** + * Track if file metadata on cache needs update. + * + * @var bool + */ + protected $hasMetadataChangedFromCacheVersion = FALSE; + + /** + * Constructs a FileMetadataPluginBase plugin. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_service + * The cache service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, CacheBackendInterface $cache_service, ConfigFactoryInterface $config_factory) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->cache = $cache_service; + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('cache.file_mdm'), + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public static function defaultConfiguration() { + return [ + 'cache' => [ + 'override' => FALSE, + 'settings' => [ + 'enabled' => TRUE, + 'expiration' => 172800, + 'disallowed_paths' => [], + ], + ], + ]; + } + + /** + * Gets the configuration object for this plugin. + * + * @param bool $editable + * If TRUE returns the editable configuration object. + * + * @return \Drupal\Core\Config\ImmutableConfig|\Drupal\Core\Config\Config + * The ImmutableConfig of the Config object for this plugin. + */ + protected function getConfigObject($editable = FALSE) { + $plugin_definition = $this->getPluginDefinition(); + $config_name = $plugin_definition['provider'] . '.file_metadata_plugin.' . $plugin_definition['id']; + return $editable ? $this->configFactory->getEditable($config_name) : $this->configFactory->get($config_name); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['override'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Override main caching settings'), + '#default_value' => $this->configuration['cache']['override'], + ]; + $form['cache_details'] = [ + '#type' => 'details', + '#open' => TRUE, + '#collapsible' => FALSE, + '#title' => $this->t('Metadata caching'), + '#tree' => TRUE, + '#states' => [ + 'visible' => [ + ':input[name="' . $this->getPluginId() . '[override]"]' => ['checked' => TRUE], + ], + ], + ]; + $form['cache_details']['settings'] = [ + '#type' => 'file_mdm_caching', + '#default_value' => $this->configuration['cache']['settings'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + // @codingStandardsIgnoreStart + $this->configuration['cache']['override'] = (bool) $form_state->getValue([$this->getPluginId(), 'override']); + $this->configuration['cache']['settings'] = $form_state->getValue([$this->getPluginId(), 'cache_details', 'settings']); + // @codingStandardsIgnoreEnd + + $config = $this->getConfigObject(TRUE); + $config->set('configuration', $this->configuration); + if ($config->getOriginal('configuration') != $config->get('configuration')) { + $config->save(); + } + } + + /** + * {@inheritdoc} + */ + public function setUri($uri) { + if (!$uri) { + throw new FileMetadataException('Missing $uri argument', $this->getPluginId(), __FUNCTION__); + } + $this->uri = $uri; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getUri() { + return $this->uri; + } + + /** + * {@inheritdoc} + */ + public function setLocalTempPath($temp_path) { + $this->localTempPath = $temp_path; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLocalTempPath() { + return $this->localTempPath; + } + + /** + * {@inheritdoc} + */ + public function setHash($hash) { + if (!$hash) { + throw new FileMetadataException('Missing $hash argument', $this->getPluginId(), __FUNCTION__); + } + $this->hash = $hash; + return $this; + } + + /** + * {@inheritdoc} + */ + public function isMetadataLoaded() { + return $this->isMetadataLoaded; + } + + /** + * {@inheritdoc} + */ + public function loadMetadata($metadata) { + $this->metadata = $metadata; + $this->hasMetadataChangedFromFileVersion = TRUE; + $this->hasMetadataChangedFromCacheVersion = TRUE; + $this->deleteCachedMetadata(); + if ($this->metadata === NULL) { + $this->isMetadataLoaded = FileMetadataInterface::NOT_LOADED; + } + else { + $this->isMetadataLoaded = FileMetadataInterface::LOADED_BY_CODE; + $this->saveMetadataToCache(); + } + return (bool) $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function loadMetadataFromFile() { + if (!file_exists($this->getLocalTempPath())) { + // File does not exists. + throw new FileMetadataException("File at '{$this->getLocalTempPath()}' does not exist", $this->getPluginId(), __FUNCTION__); + } + $this->hasMetadataChangedFromFileVersion = FALSE; + if (($this->metadata = $this->doGetMetadataFromFile()) === NULL) { + $this->isMetadataLoaded = FileMetadataInterface::NOT_LOADED; + $this->deleteCachedMetadata(); + } + else { + $this->isMetadataLoaded = FileMetadataInterface::LOADED_FROM_FILE; + $this->saveMetadataToCache(); + } + return (bool) $this->metadata; + } + + /** + * Gets file metadata from the file at URI/local path. + * + * @return mixed + * The metadata retrieved from the file. + * + * @throws \Drupal\file_mdm\FileMetadataException + * In case there were significant errors reading from file. + */ + abstract protected function doGetMetadataFromFile(); + + /** + * {@inheritdoc} + */ + public function loadMetadataFromCache() { + $plugin_id = $this->getPluginId(); + $this->hasMetadataChangedFromFileVersion = FALSE; + $this->hasMetadataChangedFromCacheVersion = FALSE; + if ($this->isUriFileMetadataCacheable() !== FALSE && ($cache = $this->cache->get("hash:{$plugin_id}:{$this->hash}"))) { + $this->metadata = $cache->data; + $this->isMetadataLoaded = FileMetadataInterface::LOADED_FROM_CACHE; + } + else { + $this->metadata = NULL; + $this->isMetadataLoaded = FileMetadataInterface::NOT_LOADED; + } + return (bool) $this->metadata; + } + + /** + * Checks if file metadata should be cached. + * + * @return array|bool + * The caching settings array retrieved from configuration if file metadata + * is cacheable, FALSE otherwise. + */ + protected function isUriFileMetadataCacheable() { + // Check plugin settings first, if they override general settings. + if ($this->configuration['cache']['override']) { + $settings = $this->configuration['cache']['settings']; + if (!$settings['enabled']) { + return FALSE; + } + } + + // Use general settings if they are not overridden by plugin. + if (!isset($settings)) { + $settings = $this->configFactory->get('file_mdm.settings')->get('metadata_cache'); + if (!$settings['enabled']) { + return FALSE; + } + } + + // URIs without valid scheme, and temporary:// URIs are not cached. + if (!file_valid_uri($this->getUri()) || file_uri_scheme($this->getUri()) === 'temporary') { + return FALSE; + } + + // URIs falling into disallowed paths are not cached. + foreach ($settings['disallowed_paths'] as $pattern) { + $p = "#^" . strtr(preg_quote($pattern, '#'), ['\*' => '.*', '\?' => '.']) . "$#i"; + if (preg_match($p, $this->getUri())) { + return FALSE; + } + } + + return $settings; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = NULL) { + if (!$this->getUri()) { + throw new FileMetadataException("No URI specified", $this->getPluginId(), __FUNCTION__); + } + if (!$this->hash) { + throw new FileMetadataException("No hash specified", $this->getPluginId(), __FUNCTION__); + } + if ($this->metadata === NULL) { + // Metadata has not been loaded yet. Try loading it from cache first. + $this->loadMetadataFromCache(); + } + if ($this->metadata === NULL && $this->isMetadataLoaded !== FileMetadataInterface::LOADED_FROM_FILE) { + // Metadata has not been loaded yet. Try loading it from file if URI is + // defined and a read attempt was not made yet. + $this->loadMetadataFromFile(); + } + return $this->doGetMetadata($key); + } + + /** + * Gets a metadata element. + * + * @param mixed|null $key + * A key to determine the metadata element to be returned. If NULL, the + * entire metadata will be returned. + * + * @return mixed|null + * The value of the element specified by $key. If $key is NULL, the entire + * metadata. If no metadata is available, return NULL. + */ + abstract protected function doGetMetadata($key = NULL); + + /** + * {@inheritdoc} + */ + public function setMetadata($key, $value) { + if ($key === NULL) { + throw new FileMetadataException("No metadata key specified for file at '{$this->getUri()}'", $this->getPluginId(), __FUNCTION__); + } + if (!$this->metadata && !$this->getMetadata()) { + throw new FileMetadataException("No metadata loaded for file at '{$this->getUri()}'", $this->getPluginId(), __FUNCTION__); + } + if ($this->doSetMetadata($key, $value)) { + $this->hasMetadataChangedFromFileVersion = TRUE; + if ($this->isMetadataLoaded === FileMetadataInterface::LOADED_FROM_CACHE) { + $this->hasMetadataChangedFromCacheVersion = TRUE; + } + return TRUE; + } + return FALSE; + } + + /** + * Sets a metadata element. + * + * @param mixed $key + * A key to determine the metadata element to be changed. + * @param mixed $value + * The value to change the metadata element to. + * + * @return bool + * TRUE if metadata was changed successfully, FALSE otherwise. + */ + abstract protected function doSetMetadata($key, $value); + + /** + * {@inheritdoc} + */ + public function removeMetadata($key) { + if ($key === NULL) { + throw new FileMetadataException("No metadata key specified for file at '{$this->getUri()}'", $this->getPluginId(), __FUNCTION__); + } + if (!$this->metadata && !$this->getMetadata()) { + throw new FileMetadataException("No metadata loaded for file at '{$this->getUri()}'", $this->getPluginId(), __FUNCTION__); + } + if ($this->doRemoveMetadata($key)) { + $this->hasMetadataChangedFromFileVersion = TRUE; + if ($this->isMetadataLoaded === FileMetadataInterface::LOADED_FROM_CACHE) { + $this->hasMetadataChangedFromCacheVersion = TRUE; + } + return TRUE; + } + return FALSE; + } + + /** + * Removes a metadata element. + * + * @param mixed $key + * A key to determine the metadata element to be removed. + * + * @return bool + * TRUE if metadata was removed successfully, FALSE otherwise. + */ + abstract protected function doRemoveMetadata($key); + + /** + * {@inheritdoc} + */ + public function isSaveToFileSupported() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function saveMetadataToFile() { + if (!$this->isSaveToFileSupported()) { + throw new FileMetadataException('Write metadata to file is not supported', $this->getPluginId(), __FUNCTION__); + } + if ($this->metadata === NULL) { + return FALSE; + } + if ($this->hasMetadataChangedFromFileVersion) { + // Clears cache so that next time metadata will be fetched from file. + $this->deleteCachedMetadata(); + return $this->doSaveMetadataToFile(); + } + return FALSE; + } + + /** + * Saves metadata to file at URI. + * + * @return bool + * TRUE if metadata was saved successfully, FALSE otherwise. + */ + protected function doSaveMetadataToFile() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function saveMetadataToCache(array $tags = []) { + if ($this->metadata === NULL) { + return FALSE; + } + if (($cache_settings = $this->isUriFileMetadataCacheable()) === FALSE) { + return FALSE; + } + if ($this->isMetadataLoaded !== FileMetadataInterface::LOADED_FROM_CACHE || ($this->isMetadataLoaded === FileMetadataInterface::LOADED_FROM_CACHE && $this->hasMetadataChangedFromCacheVersion)) { + $tags = Cache::mergeTags($tags, $this->getConfigObject()->getCacheTags()); + $tags = Cache::mergeTags($tags, $this->configFactory->get('file_mdm.settings')->getCacheTags()); + $expire = $cache_settings['expiration'] === -1 ? Cache::PERMANENT : time() + $cache_settings['expiration']; + $this->cache->set("hash:{$this->getPluginId()}:{$this->hash}", $this->getMetadataToCache(), $expire, $tags); + $this->hasMetadataChangedFromCacheVersion = FALSE; + return TRUE; + } + return FALSE; + } + + /** + * Gets metadata to save to cache. + * + * @return mixed + * The metadata to be cached. + */ + protected function getMetadataToCache() { + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function deleteCachedMetadata() { + if ($this->isUriFileMetadataCacheable() === FALSE) { + return FALSE; + } + $plugin_id = $this->getPluginId(); + $this->cache->delete("hash:{$plugin_id}:{$this->hash}"); + $this->hasMetadataChangedFromCacheVersion = FALSE; + return TRUE; + } + +} diff --git a/web/modules/contrib/file_mdm/src/Plugin/FileMetadata/GetImageSize.php b/web/modules/contrib/file_mdm/src/Plugin/FileMetadata/GetImageSize.php new file mode 100644 index 000000000..4922edf09 --- /dev/null +++ b/web/modules/contrib/file_mdm/src/Plugin/FileMetadata/GetImageSize.php @@ -0,0 +1,89 @@ +<?php + +namespace Drupal\file_mdm\Plugin\FileMetadata; + +use Drupal\file_mdm\FileMetadataException; + +/** + * FileMetadata plugin for getimagesize. + * + * @FileMetadata( + * id = "getimagesize", + * title = @Translation("Getimagesize"), + * help = @Translation("File metadata plugin for PHP getimagesize()."), + * ) + */ +class GetImageSize extends FileMetadataPluginBase { + + /** + * {@inheritdoc} + */ + public function getSupportedKeys($options = NULL) { + return [0, 1, 2, 3, 'mime', 'channels', 'bits']; + } + + /** + * {@inheritdoc} + */ + protected function doGetMetadataFromFile() { + if ($data = @getimagesize($this->getLocalTempPath())) { + return $data; + } + else { + return NULL; + } + } + + /** + * Validates a file metadata key. + * + * @return bool + * TRUE if the key is valid. + * + * @throws \Drupal\file_mdm\FileMetadataException + * In case the key is invalid. + */ + protected function validateKey($key, $method) { + if (!is_int($key) && !is_string($key)) { + throw new FileMetadataException("Invalid metadata key specified", $this->getPluginId(), $method); + } + if (!in_array($key, $this->getSupportedKeys(), TRUE)) { + throw new FileMetadataException("Invalid metadata key '{$key}' specified", $this->getPluginId(), $method); + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function doGetMetadata($key = NULL) { + if ($key === NULL) { + return $this->metadata; + } + else { + $this->validateKey($key, __FUNCTION__); + return isset($this->metadata[$key]) ? $this->metadata[$key] : NULL; + } + } + + /** + * {@inheritdoc} + */ + protected function doSetMetadata($key, $value) { + $this->validateKey($key, __FUNCTION__); + $this->metadata[$key] = $value; + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function doRemoveMetadata($key) { + $this->validateKey($key, __FUNCTION__); + if (isset($this->metadata[$key])) { + unset($this->metadata[$key]); + } + return TRUE; + } + +} diff --git a/web/modules/contrib/file_mdm/src/Plugin/FileMetadataPluginInterface.php b/web/modules/contrib/file_mdm/src/Plugin/FileMetadataPluginInterface.php new file mode 100644 index 000000000..b44a9522d --- /dev/null +++ b/web/modules/contrib/file_mdm/src/Plugin/FileMetadataPluginInterface.php @@ -0,0 +1,211 @@ +<?php + +namespace Drupal\file_mdm\Plugin; + +use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Plugin\PluginFormInterface; + +/** + * Provides an interface defining a FileMetadata plugin. + */ +interface FileMetadataPluginInterface extends ContainerFactoryPluginInterface, PluginInspectionInterface, PluginFormInterface { + + /** + * Gets default configuration for this plugin. + * + * @return array + * An associative array with the default configuration. + */ + public static function defaultConfiguration(); + + /** + * Sets the URI of the file. + * + * @param string $uri + * A URI. + * + * @return $this + * + * @throws \Drupal\file_mdm\FileMetadataException + * If no URI is specified. + */ + public function setUri($uri); + + /** + * Gets the URI of the file. + * + * @return string + * The URI of the file. + */ + public function getUri(); + + /** + * Sets the local filesystem path to the file. + * + * This is used to allow accessing local copies of files stored remotely, to + * minimise remote calls and allow functions that cannot access remote stream + * wrappers to operate locally. + * + * @param string $temp_path + * A filesystem path. + * + * @return $this + */ + public function setLocalTempPath($temp_path); + + /** + * Gets the local filesystem path to the file. + * + * This is used to allow accessing local copies of files stored remotely, to + * minimise remote calls and allow functions that cannot access remote stream + * wrappers to operate locally. + * + * @return string + * The local filesystem path to the file. + */ + public function getLocalTempPath(); + + /** + * Sets the hash used to reference the URI by the metadata manager. + * + * @param string $hash + * A hash string. + * + * @return $this + * + * @throws \Drupal\file_mdm\FileMetadataException + * If no hash is specified. + */ + public function setHash($hash); + + /** + * Returns a list of metadata keys supported by the plugin. + * + * @param mixed $options + * (optional) Allows specifying additional options to control the list of + * metadata keys returned. + * + * @return array + * A simple array of metadata keys supported. + */ + public function getSupportedKeys($options = NULL); + + /** + * Checks if file metadata has been already loaded. + * + * @return bool + * TRUE if metadata is loaded, FALSE otherwise. + */ + public function isMetadataLoaded(); + + /** + * Loads file metadata from an in-memory object/array. + * + * @param mixed $metadata + * The file metadata associated to the file at URI. + * + * @return bool + * TRUE if metadata was loaded successfully, FALSE otherwise. + */ + public function loadMetadata($metadata); + + /** + * Loads file metadata from the file at URI/local path. + * + * @return bool + * TRUE if metadata was loaded successfully, FALSE otherwise. + * + * @throws \Drupal\file_mdm\FileMetadataException + * In case there were significant errors reading from file. + */ + public function loadMetadataFromFile(); + + /** + * Loads file metadata from a cache entry. + * + * @return bool + * TRUE if metadata was loaded successfully, FALSE otherwise. + * + * @throws \Drupal\file_mdm\FileMetadataException + * In case of significant errors. + */ + public function loadMetadataFromCache(); + + /** + * Gets a metadata element. + * + * @param mixed|null $key + * A key to determine the metadata element to be returned. If NULL, the + * entire metadata will be returned. + * + * @return mixed + * The value of the element specified by $key. If $key is NULL, the entire + * metadata. + */ + public function getMetadata($key = NULL); + + /** + * Sets a metadata element. + * + * @param mixed $key + * A key to determine the metadata element to be changed. + * @param mixed $value + * The value to change the metadata element to. + * + * @return bool + * TRUE if metadata was changed successfully, FALSE otherwise. + */ + public function setMetadata($key, $value); + + /** + * Removes a metadata element. + * + * @param mixed $key + * A key to determine the metadata element to be removed. + * + * @return bool + * TRUE if metadata was removed successfully, FALSE otherwise. + */ + public function removeMetadata($key); + + /** + * Determines if plugin is capable of writing metadata to files. + * + * @return bool + * TRUE if plugin can save data to files, FALSE otherwise. + */ + public function isSaveToFileSupported(); + + /** + * Saves metadata to file at URI. + * + * @return bool + * TRUE if metadata was saved successfully, FALSE otherwise. + */ + public function saveMetadataToFile(); + + /** + * Caches metadata for file at URI. + * + * Uses the 'file_mdm' cache bin. + * + * @param array $tags + * (optional) An array of cache tags to save to cache. + * + * @return bool + * TRUE if metadata was saved successfully, FALSE otherwise. + */ + public function saveMetadataToCache(array $tags = []); + + /** + * Removes cached metadata for file at URI. + * + * Uses the 'file_mdm' cache bin. + * + * @return bool + * TRUE if metadata was removed, FALSE otherwise. + */ + public function deleteCachedMetadata(); + +} diff --git a/web/modules/contrib/file_mdm/src/Plugin/FileMetadataPluginManager.php b/web/modules/contrib/file_mdm/src/Plugin/FileMetadataPluginManager.php new file mode 100644 index 000000000..b451640f0 --- /dev/null +++ b/web/modules/contrib/file_mdm/src/Plugin/FileMetadataPluginManager.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\file_mdm\Plugin; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; + +/** + * Plugin manager for FileMetadata plugins. + */ +class FileMetadataPluginManager extends DefaultPluginManager { + + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * {@inheritdoc} + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory) { + parent::__construct('Plugin/FileMetadata', $namespaces, $module_handler, 'Drupal\file_mdm\Plugin\FileMetadataPluginInterface', 'Drupal\file_mdm\Plugin\Annotation\FileMetadata'); + $this->alterInfo('file_metadata_plugin_info'); + $this->setCacheBackend($cache_backend, 'file_metadata_plugins'); + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function createInstance($plugin_id, array $configuration = array()) { + $plugin_definition = $this->getDefinition($plugin_id); + $default_config = call_user_func($plugin_definition['class'] . '::defaultConfiguration'); + $configuration = $this->configFactory->get($plugin_definition['provider'] . '.file_metadata_plugin.' . $plugin_id)->get('configuration') ?: []; + return parent::createInstance($plugin_id, NestedArray::mergeDeep($default_config, $configuration)); + } + +} diff --git a/web/modules/contrib/file_mdm/tests/files/1024-2006_1011_093752.jpg b/web/modules/contrib/file_mdm/tests/files/1024-2006_1011_093752.jpg new file mode 100644 index 0000000000000000000000000000000000000000..925ed043fe6873b4883f3c6e0a6acaa4f7b0a6a8 GIT binary patch literal 209855 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ⅅ5M!5^mRhPuf!5j=NHm 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 Y!ourm1tFJ#In;!o;7wbS=t@lg*_~m$tN;K2 literal 0 HcmV?d00001 diff --git a/web/modules/contrib/file_mdm/tests/files/sample-1.tiff b/web/modules/contrib/file_mdm/tests/files/sample-1.tiff new file mode 100644 index 0000000000000000000000000000000000000000..2efe348db93d1b9d66c3188b33609a1408fec0a3 GIT binary patch literal 6925 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 literal 0 HcmV?d00001 diff --git a/web/modules/contrib/file_mdm/tests/files/test-exif.jpeg b/web/modules/contrib/file_mdm/tests/files/test-exif.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..999354ca2ed8a710edfde222b0fc244f00894ff3 GIT binary patch literal 15287 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 D@^VkR literal 0 HcmV?d00001 diff --git a/web/modules/contrib/file_mdm/tests/src/Kernel/FileMetadataManagerTest.php b/web/modules/contrib/file_mdm/tests/src/Kernel/FileMetadataManagerTest.php new file mode 100644 index 000000000..89556e59c --- /dev/null +++ b/web/modules/contrib/file_mdm/tests/src/Kernel/FileMetadataManagerTest.php @@ -0,0 +1,424 @@ +<?php + +namespace Drupal\Tests\file_mdm\Kernel; + +use Drupal\file_mdm\FileMetadataInterface; + +/** + * Tests that File Metadata Manager works properly. + * + * @group File Metadata + */ +class FileMetadataManagerTest extends FileMetadataManagerTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['system', 'simpletest', 'file_mdm', 'file_test']; + + /** + * Tests using the 'getimagesize' plugin. + */ + public function testFileMetadata() { + // Prepare a copy of test files. + file_unmanaged_copy(drupal_get_path('module', 'simpletest') . '/files/image-test.png', 'public://', FILE_EXISTS_REPLACE); + file_unmanaged_copy(drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', 'public://', FILE_EXISTS_REPLACE); + // The image files that will be tested. + $image_files = [ + [ + // Pass a path instead of the URI. + 'uri' => drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', + 'count_keys' => 7, + 'test_keys' => [ + [0, 100], + [1, 75], + [2, IMAGETYPE_JPEG], + ['bits', 8], + ['channels', 3], + ['mime', 'image/jpeg'], + ], + ], + [ + // Pass a URI. + 'uri' => 'public://test-exif.jpeg', + 'count_keys' => 7, + 'test_keys' => [ + [0, 100], + [1, 75], + [2, IMAGETYPE_JPEG], + ['bits', 8], + ['channels', 3], + ['mime', 'image/jpeg'], + ], + ], + [ + // PHP getimagesize works on remote stream wrappers. + 'uri' => 'dummy-remote://test-exif.jpeg', + 'count_keys' => 7, + 'test_keys' => [ + [0, 100], + [1, 75], + [2, IMAGETYPE_JPEG], + ['bits', 8], + ['channels', 3], + ['mime', 'image/jpeg'], + ], + ], + [ + // JPEG Image with GPS data. + 'uri' => drupal_get_path('module', 'file_mdm') . '/tests/files/1024-2006_1011_093752.jpg', + 'count_keys' => 7, + 'test_keys' => [ + [0, 1024], + [1, 768], + [2, IMAGETYPE_JPEG], + ['bits', 8], + ['channels', 3], + ['mime', 'image/jpeg'], + ], + ], + [ + // TIFF image. + 'uri' => drupal_get_path('module', 'file_mdm') . '/tests/files/sample-1.tiff', + 'count_keys' => 5, + 'test_keys' => [ + [0, 174], + [1, 38], + [2, IMAGETYPE_TIFF_MM], + ['mime', 'image/tiff'], + ], + ], + [ + // PNG image. + 'uri' => 'public://image-test.png', + 'count_keys' => 6, + 'test_keys' => [ + [0, 40], + [1, 20], + [2, IMAGETYPE_PNG], + ['bits', 8], + ['mime', 'image/png'], + ], + ], + ]; + + // Get the file metadata manager service. + $fmdm = $this->container->get('file_metadata_manager'); + + // Walk through test files. + foreach ($image_files as $image_file) { + $file_metadata = $fmdm->uri($image_file['uri']); + $this->assertNotNull($file_metadata->getMetadata('getimagesize')); + // Read from file. + $this->assertEqual($image_file['count_keys'], $this->countMetadataKeys($file_metadata, 'getimagesize')); + foreach ($image_file['test_keys'] as $test) { + $entry = $file_metadata->getMetadata('getimagesize', $test[0]); + $this->assertEqual($test[1], $entry); + } + // Try getting an unsupported key. + $this->assertNull($file_metadata->getMetadata('getimagesize', 'baz')); + // Try getting an invalid key. + $this->assertNull($file_metadata->getMetadata('getimagesize', ['qux' => 'laa'])); + // Change MIME type. + $this->assertTrue($file_metadata->setMetadata('getimagesize', 'mime', 'foo/bar')); + $this->assertEqual('foo/bar', $file_metadata->getMetadata('getimagesize', 'mime')); + // Try adding an unsupported key. + $this->assertFalse($file_metadata->setMetadata('getimagesize', 'baz', 'qux')); + $this->assertNull($file_metadata->getMetadata('getimagesize', 'baz')); + // Try adding an invalid key. + $this->assertFalse($file_metadata->setMetadata('getimagesize', ['qux' => 'laa'], 'hoz')); + // Remove MIME type. + $this->assertTrue($file_metadata->removeMetadata('getimagesize', 'mime')); + $this->assertEqual($image_file['count_keys'] - 1, $this->countMetadataKeys($file_metadata, 'getimagesize')); + $this->assertNull($file_metadata->getMetadata('getimagesize', 'mime')); + // Try removing an unsupported key. + $this->assertFalse($file_metadata->removeMetadata('getimagesize', 'baz')); + // Try removing an invalid key. + $this->assertFalse($file_metadata->removeMetadata('getimagesize', ['qux' => 'laa'])); + // Try getting/setting/removing metadata for a non-existing plugin. + $this->assertNull($file_metadata->getMetadata('laila', 'han')); + $this->assertFalse($file_metadata->setMetadata('laila', 'han', 'solo')); + $this->assertFalse($file_metadata->removeMetadata('laila', 'han')); + } + + // Test releasing URI. + $this->assertEqual(6, $fmdm->count()); + $this->assertTrue($fmdm->has($image_files[0]['uri'])); + $this->assertTrue($fmdm->release($image_files[0]['uri'])); + $this->assertEqual(5, $fmdm->count()); + $this->assertFalse($fmdm->has($image_files[0]['uri'])); + $this->assertFalse($fmdm->release($image_files[0]['uri'])); + + // Test loading metadata from an in-memory object. + $file_metadata_from = $fmdm->uri($image_files[0]['uri']); + $this->assertEqual(6, $fmdm->count()); + $metadata = $file_metadata_from->getMetadata('getimagesize'); + $new_file_metadata = $fmdm->uri('public://test-output.jpeg'); + $this->assertEqual(7, $fmdm->count()); + $new_file_metadata->loadMetadata('getimagesize', $metadata); + $this->assertEqual($image_files[0]['count_keys'], $this->countMetadataKeys($new_file_metadata, 'getimagesize')); + foreach ($image_files[0]['test_keys'] as $test) { + $entry = $file_metadata->getMetadata('getimagesize', $test[0]); + $this->assertEqual($test[1], $new_file_metadata->getMetadata('getimagesize', $test[0])); + } + } + + /** + * Test caching. + */ + public function testFileMetadataCaching() { + // Prepare a copy of test files. + file_unmanaged_copy(drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', 'public://', FILE_EXISTS_REPLACE); + file_unmanaged_copy(drupal_get_path('module', 'simpletest') . '/files/image-test.gif', 'public://', FILE_EXISTS_REPLACE); + file_unmanaged_copy(drupal_get_path('module', 'simpletest') . '/files/image-test.png', 'public://', FILE_EXISTS_REPLACE); + + // The image files that will be tested. + $image_files = [ + [ + // Pass a URI. + 'uri' => 'public://image-test.gif', + 'cache' => TRUE, + 'delete' => TRUE, + 'count_keys' => 7, + 'test_keys' => [ + [0, 40], + [1, 20], + [2, IMAGETYPE_GIF], + ['mime', 'image/gif'], + ], + ], + [ + // Pass a path instead of the URI. + 'uri' => drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', + 'cache' => FALSE, + 'delete' => FALSE, + 'count_keys' => 7, + 'test_keys' => [ + [0, 100], + [1, 75], + [2, IMAGETYPE_JPEG], + ['mime', 'image/jpeg'], + ], + ], + [ + // PHP getimagesize works on remote stream wrappers. + 'uri' => 'dummy-remote://image-test.png', + 'cache' => TRUE, + 'delete' => TRUE, + 'count_keys' => 6, + 'test_keys' => [ + [0, 40], + [1, 20], + [2, IMAGETYPE_PNG], + ['mime', 'image/png'], + ], + ], + ]; + + // Get the file metadata manager service. + $fmdm = $this->container->get('file_metadata_manager'); + + // Walk through test files. + foreach ($image_files as $image_file) { + // Read from file. + $file_metadata = $fmdm->uri($image_file['uri']); + $this->assertNotNull($file_metadata->getMetadata('getimagesize')); + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $file_metadata->isMetadataLoaded('getimagesize')); + + // Release URI. + $file_metadata = NULL; + $this->assertTrue($fmdm->release($image_file['uri'])); + $this->assertEqual(0, $fmdm->count()); + + if ($image_file['delete']) { + // Delete file. + file_unmanaged_delete($image_file['uri']); + // No file to be found at URI. + $this->assertFalse(file_exists($image_file['uri'])); + } + + // Read from cache if possible. + $file_metadata = $fmdm->uri($image_file['uri']); + $this->assertNotNull($file_metadata->getMetadata('getimagesize')); + if ($image_file['cache']) { + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $file_metadata->isMetadataLoaded('getimagesize')); + } + else { + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $file_metadata->isMetadataLoaded('getimagesize')); + } + $this->assertEqual($image_file['count_keys'], $this->countMetadataKeys($file_metadata, 'getimagesize')); + foreach ($image_file['test_keys'] as $test) { + $entry = $file_metadata->getMetadata('getimagesize', $test[0]); + $this->assertEqual($test[1], $entry); + } + + // Change MIME type and remove 0, 1, 2, 3. + $this->assertTrue($file_metadata->setMetadata('getimagesize', 'mime', 'foo/bar')); + $this->assertTrue($file_metadata->removeMetadata('getimagesize', 0)); + $this->assertTrue($file_metadata->removeMetadata('getimagesize', 1)); + $this->assertTrue($file_metadata->removeMetadata('getimagesize', 2)); + $this->assertTrue($file_metadata->removeMetadata('getimagesize', 3)); + + // Save again to cache. + if ($image_file['cache']) { + $this->assertTrue($file_metadata->saveMetadataToCache('getimagesize')); + } + else { + $this->assertFalse($file_metadata->saveMetadataToCache('getimagesize')); + } + + if ($image_file['cache']) { + // Release URI. + $file_metadata = NULL; + $this->assertTrue($fmdm->release($image_file['uri'])); + $this->assertIdentical(0, $fmdm->count()); + + // Read from cache. + $file_metadata = $fmdm->uri($image_file['uri']); + $this->assertIdentical($image_file['count_keys'] - 4, $this->countMetadataKeys($file_metadata, 'getimagesize')); + $this->assertIdentical('foo/bar', $file_metadata->getMetadata('getimagesize', 'mime')); + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $file_metadata->isMetadataLoaded('getimagesize')); + } + + $file_metadata = NULL; + $this->assertTrue($fmdm->release($image_file['uri'])); + $this->assertEqual(0, $fmdm->count()); + } + } + + /** + * Tests remote files, setting local temp path explicitly. + */ + public function testRemoteFileSetLocalPath() { + // The image files that will be tested. + $image_files = [ + [ + // Remote storage file. Pass the path to a local copy of the file. + 'uri' => 'dummy-remote://test-exif.jpeg', + 'local_path' => $this->container->get('file_system')->realpath('temporary://test-exif.jpeg'), + 'count_keys' => 7, + 'test_keys' => [ + [0, 100], + [1, 75], + [2, IMAGETYPE_JPEG], + ['bits', 8], + ['channels', 3], + ['mime', 'image/jpeg'], + ], + ], + ]; + + // Get the file metadata manager service. + $fmdm = $this->container->get('file_metadata_manager'); + + // Copy the test file to a temp location. + file_unmanaged_copy(drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', 'temporary://', FILE_EXISTS_REPLACE); + + // Test setting local temp path explicitly. The files should be parsed + // even if not available on the URI. + foreach ($image_files as $image_file) { + $file_metadata = $fmdm->uri($image_file['uri']); + $file_metadata->setLocalTempPath($image_file['local_path']); + // No file to be found at URI. + $this->assertFalse(file_exists($image_file['uri'])); + // File to be found at local temp path. + $this->assertTrue(file_exists($file_metadata->getLocalTempPath())); + $this->assertEqual($image_file['count_keys'], $this->countMetadataKeys($file_metadata, 'getimagesize')); + foreach ($image_file['test_keys'] as $test) { + $entry = $file_metadata->getMetadata('getimagesize', $test[0]); + $this->assertEqual($test[1], $entry); + } + // Copies temp to destination URI. + $this->assertTrue($file_metadata->copyTempToUri()); + $this->assertTrue(file_exists($image_file['uri'])); + + // Release URI and check metadata was cached. + $file_metadata = NULL; + $this->assertTrue($fmdm->release($image_file['uri'])); + $this->assertEqual(0, $fmdm->count()); + $file_metadata = $fmdm->uri($image_file['uri']); + $this->assertNotNull($file_metadata->getMetadata('getimagesize')); + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $file_metadata->isMetadataLoaded('getimagesize')); + } + } + + /** + * Tests remote files, letting file_mdm manage setting local temp path. + */ + public function testRemoteFileCopy() { + // The image files that will be tested. + $image_files = [ + [ + // Remote storage file. Pass the path to a local copy of the file. + 'uri' => 'dummy-remote://test-exif.jpeg', + 'count_keys' => 7, + 'test_keys' => [ + [0, 100], + [1, 75], + [2, IMAGETYPE_JPEG], + ['bits', 8], + ['channels', 3], + ['mime', 'image/jpeg'], + ], + ], + ]; + + // Get the file metadata manager service. + $fmdm = $this->container->get('file_metadata_manager'); + $file_system = $this->container->get('file_system'); + + // Copy the test file to dummy-remote wrapper. + file_unmanaged_copy(drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', 'dummy-remote://', FILE_EXISTS_REPLACE); + + foreach ($image_files as $image_file) { + $file_metadata = $fmdm->uri($image_file['uri']); + $file_metadata->copyUriToTemp(); + // File to be found at destination URI. + $this->assertTrue(file_exists($image_file['uri'])); + // File to be found at local temp URI. + $this->assertIdentical(0, strpos($file_system->basename($file_metadata->getLocalTempPath()), 'file_mdm_')); + $this->assertTrue(file_exists($file_metadata->getLocalTempPath())); + $this->assertEqual($image_file['count_keys'], $this->countMetadataKeys($file_metadata, 'getimagesize')); + foreach ($image_file['test_keys'] as $test) { + $entry = $file_metadata->getMetadata('getimagesize', $test[0]); + $this->assertEqual($test[1], $entry); + } + + // Release URI and check metadata was cached. + $file_metadata = NULL; + $this->assertTrue($fmdm->release($image_file['uri'])); + $this->assertEqual(0, $fmdm->count()); + $file_metadata = $fmdm->uri($image_file['uri']); + $this->assertNotNull($file_metadata->getMetadata('getimagesize')); + $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $file_metadata->isMetadataLoaded('getimagesize')); + } + } + + /** + * Tests URI sanitization. + */ + public function testSanitizedUri() { + // Get the file metadata manager service. + $fmdm = $this->container->get('file_metadata_manager'); + $file_system = $this->container->get('file_system'); + + // Copy a test file to test directory. + $test_directory = 'public://test-images/'; + file_prepare_directory($test_directory, FILE_CREATE_DIRECTORY); + file_unmanaged_copy(drupal_get_path('module', 'file_mdm') . '/tests/files/test-exif.jpeg', $test_directory, FILE_EXISTS_REPLACE); + + // Get file metadata object. + $file_metadata = $fmdm->uri('public://test-images/test-exif.jpeg'); + $this->assertEqual(7, $this->countMetadataKeys($file_metadata, 'getimagesize')); + + // Check that the file metadata manager has the URI in different forms. + $this->assertTrue($fmdm->has('public://test-images/test-exif.jpeg')); + $this->assertTrue($fmdm->has('public:///test-images/test-exif.jpeg')); + $this->assertTrue($fmdm->has('public://test-images//test-exif.jpeg')); + $this->assertTrue($fmdm->has('public://////test-images////test-exif.jpeg')); + $this->assertFalse($fmdm->has('public:/test-images/test-exif.jpeg')); + } + +} diff --git a/web/modules/contrib/file_mdm/tests/src/Kernel/FileMetadataManagerTestBase.php b/web/modules/contrib/file_mdm/tests/src/Kernel/FileMetadataManagerTestBase.php new file mode 100644 index 000000000..72de71946 --- /dev/null +++ b/web/modules/contrib/file_mdm/tests/src/Kernel/FileMetadataManagerTestBase.php @@ -0,0 +1,46 @@ +<?php + +namespace Drupal\Tests\file_mdm\Kernel; + +use Drupal\file_mdm\FileMetadataInterface; +use Drupal\KernelTests\KernelTestBase; + +/** + * Base test class for File Metadata Manager. + */ +abstract class FileMetadataManagerTestBase extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->installConfig(['file_mdm']); + } + + /** + * Returns the count of metadata keys found in the file. + * + * @param \Drupal\file_mdm\FileMetadataInterface $file_md + * The FileMetadata object. + * @param string $metadata_id + * The file metadata plugin id. + * @param mixed $options + * (optional) Allows specifying additional options to control the list of + * metadata keys returned. + * + * @return int + * The count of metadata keys found in the file. + */ + protected function countMetadataKeys(FileMetadataInterface $file_md, $metadata_id, $options = NULL) { + $supported_keys = $file_md->getSupportedKeys($metadata_id, $options); + $count = 0; + foreach ($supported_keys as $key) { + if ($file_md->getMetadata($metadata_id, $key)) { + $count++; + } + } + return $count; + } + +} diff --git a/web/modules/contrib/htmlawed/htmlawed.info.yml b/web/modules/contrib/htmlawed/htmlawed.info.yml index c820170e1..e898bbfd6 100644 --- a/web/modules/contrib/htmlawed/htmlawed.info.yml +++ b/web/modules/contrib/htmlawed/htmlawed.info.yml @@ -3,8 +3,9 @@ description: Use htmLawed to restrict and correct HTML for compliance with admin type: module package: filter # core: 8.x -# Information added by Drupal.org packaging script on 2017-12-07 -version: '8.x-3.3' + +# Information added by Drupal.org packaging script on 2018-07-05 +version: '8.x-3.5' core: '8.x' project: 'htmlawed' -datestamp: 1512623891 +datestamp: 1530751729 diff --git a/web/modules/contrib/htmlawed/htmLawed.module b/web/modules/contrib/htmlawed/htmlawed.module similarity index 100% rename from web/modules/contrib/htmlawed/htmLawed.module rename to web/modules/contrib/htmlawed/htmlawed.module diff --git a/web/modules/contrib/image_widget_crop/composer.json b/web/modules/contrib/image_widget_crop/composer.json index 076e1ff0f..5335035ec 100644 --- a/web/modules/contrib/image_widget_crop/composer.json +++ b/web/modules/contrib/image_widget_crop/composer.json @@ -26,7 +26,7 @@ } ], "require": { - "drupal/crop": "1.0 - 2.0" + "drupal/crop": "^1.0 || ^2.0" }, "require-dev": { "drupal/entity_browser": "1.x-dev", diff --git a/web/modules/contrib/image_widget_crop/config/install/image_widget_crop.settings.yml b/web/modules/contrib/image_widget_crop/config/install/image_widget_crop.settings.yml index 188659807..02ecece11 100644 --- a/web/modules/contrib/image_widget_crop/config/install/image_widget_crop.settings.yml +++ b/web/modules/contrib/image_widget_crop/config/install/image_widget_crop.settings.yml @@ -3,5 +3,6 @@ settings: css_url: '' crop_preview_image_style: 'crop_thumbnail' crop_list: [] + crop_types_required: [] warn_multiple_usages: FALSE show_default_crop: TRUE diff --git a/web/modules/contrib/image_widget_crop/config/schema/image_widget_crop.schema.yml b/web/modules/contrib/image_widget_crop/config/schema/image_widget_crop.schema.yml index 3838b64b6..87144743d 100644 --- a/web/modules/contrib/image_widget_crop/config/schema/image_widget_crop.schema.yml +++ b/web/modules/contrib/image_widget_crop/config/schema/image_widget_crop.schema.yml @@ -18,6 +18,11 @@ field.widget.settings.image_widget_crop: label: 'The preview image will be cropped' sequence: type: string + crop_types_required: + type: sequence + label: 'Required crop types' + sequence: + type: string warn_multiple_usages: type: boolean label: 'Warn user when a file have multiple usages' @@ -49,6 +54,11 @@ image_widget_crop.settings: label: 'The preview image will be cropped' sequence: type: string + crop_types_required: + type: sequence + label: 'Required crop types' + sequence: + type: string warn_multiple_usages: type: boolean label: 'Warn user when a file have multiple usages' diff --git a/web/modules/contrib/image_widget_crop/css/image_widget_crop.css b/web/modules/contrib/image_widget_crop/css/image_widget_crop.css index 8f670910c..2bd6854df 100644 --- a/web/modules/contrib/image_widget_crop/css/image_widget_crop.css +++ b/web/modules/contrib/image_widget_crop/css/image_widget_crop.css @@ -9,19 +9,54 @@ margin-top: 1em; } -.cropper--height-soft-limit-reached .cropper-point, +.cropper--width-soft-limit-reached .point-n, +.cropper--width-soft-limit-reached .point-s, +.cropper--width-soft-limit-reached .point-ne, +.cropper--width-soft-limit-reached .point-se, +.cropper--width-soft-limit-reached .point-sw, +.cropper--width-soft-limit-reached .point-nw { + background-color: #a51b00; +} + +.cropper--height-soft-limit-reached .point-w, +.cropper--height-soft-limit-reached .point-e, +.cropper--height-soft-limit-reached .point-ne, +.cropper--height-soft-limit-reached .point-se, +.cropper--height-soft-limit-reached .point-nw, +.cropper--height-soft-limit-reached .point-sw { + background-color: #a51b00; +} + .cropper--width-soft-limit-reached .cropper-point { background-color: #a51b00; } -.cropper--height-soft-limit-reached .line-w, + +.cropper--height-soft-limit-reached .line-w { + background-color: #ff0000; + opacity: 1; + width: 1px; + left: -1px; +} + .cropper--height-soft-limit-reached .line-e { background-color: #ff0000; - opacity: 0.2; + opacity: 1; + width: 1px; + right: -1px; } -.cropper--width-soft-limit-reached .line-n, + +.cropper--width-soft-limit-reached .line-n { + background-color: #ff0000; + opacity: 1; + height: 1px; + top: -1px; +} + .cropper--width-soft-limit-reached .line-s { background-color: #ff0000; - opacity: 0.2; + opacity: 1; + height: 1px; + bottom: -1px; } @media screen and (max-width: 480px) { diff --git a/web/modules/contrib/image_widget_crop/image_widget_crop.info.yml b/web/modules/contrib/image_widget_crop/image_widget_crop.info.yml index 8d6e79ed1..78744c08c 100644 --- a/web/modules/contrib/image_widget_crop/image_widget_crop.info.yml +++ b/web/modules/contrib/image_widget_crop/image_widget_crop.info.yml @@ -10,8 +10,8 @@ dependencies: test_dependencies: - file_entity -# Information added by Drupal.org packaging script on 2017-11-08 -version: '8.x-2.1' +# Information added by Drupal.org packaging script on 2018-07-04 +version: '8.x-2.2' core: '8.x' project: 'image_widget_crop' -datestamp: 1510138593 +datestamp: 1530698928 diff --git a/web/modules/contrib/image_widget_crop/image_widget_crop.module b/web/modules/contrib/image_widget_crop/image_widget_crop.module index 0078b58c0..632f51e10 100644 --- a/web/modules/contrib/image_widget_crop/image_widget_crop.module +++ b/web/modules/contrib/image_widget_crop/image_widget_crop.module @@ -5,8 +5,8 @@ * Contains image_widget_crop.module. */ -define('IMAGE_WIDGET_CROP_JS_CDN', 'https://cdnjs.cloudflare.com/ajax/libs/cropper/2.3.4/cropper.min.js'); -define('IMAGE_WIDGET_CROP_CSS_CDN', 'https://cdnjs.cloudflare.com/ajax/libs/cropper/2.3.4/cropper.min.css'); +define('IMAGE_WIDGET_CROP_JS_CDN', 'https://cdnjs.cloudflare.com/ajax/libs/cropper/3.1.3/cropper.min.js'); +define('IMAGE_WIDGET_CROP_CSS_CDN', 'https://cdnjs.cloudflare.com/ajax/libs/cropper/3.1.3/cropper.min.css'); use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; diff --git a/web/modules/contrib/image_widget_crop/js/ImageWidgetCropType.js b/web/modules/contrib/image_widget_crop/js/ImageWidgetCropType.js index 5ca293b35..3a65aa5dd 100644 --- a/web/modules/contrib/image_widget_crop/js/ImageWidgetCropType.js +++ b/web/modules/contrib/image_widget_crop/js/ImageWidgetCropType.js @@ -155,6 +155,13 @@ */ this.showDefaultCrop = true; + /** + * Flag indicating whether to show the default crop. + * + * @type {Boolean} + */ + this.isRequired = false; + /** * The soft limit of the crop. * @@ -274,7 +281,7 @@ * The "cropmove" event handler for the Cropper plugin. */ Drupal.ImageWidgetCropType.prototype.cropMove = function () { - this.updateSoftLimits(); + this.built(); }; /** @@ -375,6 +382,7 @@ // Set the default options. this.options = $.extend({}, this.defaultOptions); + this.isRequired = this.$wrapper.data('drupalIwcRequired'); // Extend this instance with data from the wrapper. var data = this.$wrapper.data(); @@ -460,7 +468,6 @@ this.options.autoCrop = true; } - this.options.data.rotate = 0; this.options.data.scaleX = 1; this.options.data.scaleY = 1; @@ -765,4 +772,32 @@ return summary.join('<br>'); }; + /** + * Override Theme function for a vertical tabs. + * + * @param {object} settings + * An object with the following keys: + * @param {string} settings.title + * The name of the tab. + * + * @return {object} + * This function has to return an object with at least these keys: + * - item: The root tab jQuery element + * - link: The anchor tag that acts as the clickable area of the tab + * (jQuery version) + * - summary: The jQuery element that contains the tab summary + */ + Drupal.theme.verticalTab = function (settings) { + var tab = {}; + this.isRequired = settings.details.data('drupalIwcRequired'); + tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>').append(tab.link = $('<a href="#"></a>').append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').text(settings.title)).append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>'))); + + // If those Crop type is required add attributes. + if (this.isRequired) { + tab.title.addClass('js-form-required form-required'); + } + + return tab; + }; + }(jQuery, Drupal)); diff --git a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/config/optional/core.entity_form_display.node.crop_responsive_example.default.yml b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/config/optional/core.entity_form_display.node.crop_responsive_example.default.yml index dbbbb8e22..1a4269d13 100644 --- a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/config/optional/core.entity_form_display.node.crop_responsive_example.default.yml +++ b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/config/optional/core.entity_form_display.node.crop_responsive_example.default.yml @@ -37,6 +37,8 @@ content: settings: show_default_crop: true warn_multiple_usages: true + crop_types_required: + - crop_16_9 preview_image_style: thumbnail crop_preview_image_style: crop_thumbnail crop_list: diff --git a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/config/optional/core.entity_form_display.node.crop_simple_example.default.yml b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/config/optional/core.entity_form_display.node.crop_simple_example.default.yml index e033fcbd2..23808694b 100644 --- a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/config/optional/core.entity_form_display.node.crop_simple_example.default.yml +++ b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/config/optional/core.entity_form_display.node.crop_simple_example.default.yml @@ -38,6 +38,8 @@ content: show_crop_area: true show_default_crop: true warn_multiple_usages: true + crop_types_required: + - crop_16_9 preview_image_style: thumbnail crop_preview_image_style: crop_thumbnail crop_list: diff --git a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/image_widget_crop_examples.info.yml b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/image_widget_crop_examples.info.yml index 4ee317807..abe2a4ca9 100644 --- a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/image_widget_crop_examples.info.yml +++ b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/image_widget_crop_examples.info.yml @@ -17,8 +17,8 @@ dependencies: theme: - bartik -# Information added by Drupal.org packaging script on 2017-11-08 -version: '8.x-2.1' +# Information added by Drupal.org packaging script on 2018-07-04 +version: '8.x-2.2' core: '8.x' project: 'image_widget_crop' -datestamp: 1510138593 +datestamp: 1530698928 diff --git a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/image_widget_crop_examples.install b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/image_widget_crop_examples.install index 70a79de00..b1ec56d4d 100644 --- a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/image_widget_crop_examples.install +++ b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/image_widget_crop_examples.install @@ -21,6 +21,9 @@ function image_widget_crop_examples_install() { ->set('settings.crop_preview_image_style', 'crop_thumbnail') ->set('settings.show_default_crop', TRUE) ->set('settings.warn_multiple_usages', FALSE) + ->set('settings.crop_types_required', [ + 'crop_16_9' => 'crop_16_9', + ]) ->set('settings.crop_list', [ 'crop_16_9' => 'crop_16_9', 'crop_4_3' => 'crop_4_3', diff --git a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/src/Tests/ImageWidgetCropExamplesTest.php b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/src/Tests/ImageWidgetCropExamplesTest.php deleted file mode 100644 index 648ec29bf..000000000 --- a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/src/Tests/ImageWidgetCropExamplesTest.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php - -namespace Drupal\image_widget_crop_examples\Tests; - -use Drupal\simpletest\WebTestBase; - -/** - * Tests image_widget_crop_examples. - * - * @group image_widget_crop_examples - * - * @ingroup media - */ -class ImageWidgetCropExamplesTest extends WebTestBase { - - /** - * Modules to install. - * - * @var array - */ - public static $modules = [ - 'menu_ui', - 'path', - 'media', - ]; - - /** - * {@inheritdoc} - */ - public function setUp() { - parent::setUp(); - // Theme needs to be set before enabling image_widget_crop_examples because - // of dependency. - \Drupal::service('theme_handler')->install(['bartik']); - $this->config('system.theme') - ->set('default', 'bartik') - ->save(); - $this->assertTrue(\Drupal::service('module_installer')->install(['image_widget_crop_examples']), 'image_widget_crop_examples installed.'); - \Drupal::service('router.builder')->rebuild(); - } - - /** - * Tests if image_widget_crop_example is correctly installed. - */ - public function testInstalled() { - $this->drupalGet(''); - $this->assertTitle('Image Widget Crop examples | Drupal'); - $this->assertText('Image Widget Crop examples'); - $this->assertText('Welcome to Image Widget Crop example.'); - $this->assertText('Image Widget Crop provides an interface for using the features of the Crop API.'); - $this->assertText('You can test the functionality with custom content types created for the demonstration of features from Image Widget Crop:'); - } - -} diff --git a/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/tests/src/Functional/ImageWidgetCropExamplesTest.php b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/tests/src/Functional/ImageWidgetCropExamplesTest.php new file mode 100644 index 000000000..344fc66b3 --- /dev/null +++ b/web/modules/contrib/image_widget_crop/modules/image_widget_crop_examples/tests/src/Functional/ImageWidgetCropExamplesTest.php @@ -0,0 +1,56 @@ +<?php + +namespace Drupal\Tests\image_widget_crop_examples\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * Tests image_widget_crop_examples. + * + * @group image_widget_crop_examples + * + * @ingroup media + */ +class ImageWidgetCropExamplesTest extends BrowserTestBase { + + /** + * Modules to install. + * + * @var array + */ + public static $modules = [ + 'menu_ui', + 'path', + 'media', + ]; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + // Theme needs to be set before enabling image_widget_crop_examples because + // of dependency. + \Drupal::service('theme_handler')->install(['bartik']); + $this->config('system.theme') + ->set('default', 'bartik') + ->save(); + + $example_module_is_installed = \Drupal::service('module_installer')->install(['image_widget_crop_examples']); + $this->assertTrue($example_module_is_installed, 'image_widget_crop_examples installed.'); + \Drupal::service('router.builder')->rebuild(); + } + + /** + * Tests if image_widget_crop_example is correctly installed. + */ + public function testInstalled() { + $this->drupalGet(''); + $this->assertSession()->titleEquals('Image Widget Crop examples | Drupal'); + $this->assertSession()->pageTextContains('Image Widget Crop examples'); + $this->assertSession()->pageTextContains('Welcome to Image Widget Crop example.'); + $this->assertSession()->pageTextContains('Image Widget Crop provides an interface for using the features of the Crop API.'); + $this->assertSession()->pageTextContains('You can test the functionality with custom content types created for the demonstration of features from Image Widget Crop:'); + } + +} diff --git a/web/modules/contrib/image_widget_crop/src/Element/ImageCrop.php b/web/modules/contrib/image_widget_crop/src/Element/ImageCrop.php index 7b33de99f..6c3852be0 100644 --- a/web/modules/contrib/image_widget_crop/src/Element/ImageCrop.php +++ b/web/modules/contrib/image_widget_crop/src/Element/ImageCrop.php @@ -31,12 +31,14 @@ class ImageCrop extends FormElement { '#file' => NULL, '#crop_preview_image_style' => 'crop_thumbnail', '#crop_type_list' => [], + '#crop_types_required' => [], '#warn_multiple_usages' => FALSE, '#show_default_crop' => TRUE, '#show_crop_area' => FALSE, '#attached' => [ 'library' => 'image_widget_crop/cropper.integration', ], + '#element_validate' => [[self::class, 'cropRequired']], '#tree' => TRUE, ]; } @@ -130,13 +132,13 @@ class ImageCrop extends FormElement { ]; /** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $crop_type_storage */ - $crop_type_storage = \Drupal::entityTypeManager()->getStorage('crop_type'); + $crop_type_storage = \Drupal::entityTypeManager() + ->getStorage('crop_type'); /** @var \Drupal\crop\Entity\CropType[] $crop_types */ if ($crop_types = $crop_type_storage->loadMultiple($crop_type_list)) { foreach ($crop_types as $type => $crop_type) { $ratio = $crop_type->getAspectRatio() ?: 'NaN'; - $element['crop_wrapper'][$type] = [ '#type' => 'details', '#title' => $crop_type->label(), @@ -145,6 +147,7 @@ class ImageCrop extends FormElement { 'data-drupal-iwc' => 'type', 'data-drupal-iwc-id' => $type, 'data-drupal-iwc-ratio' => $ratio, + 'data-drupal-iwc-required' => self::isRequiredType($element, $type), 'data-drupal-iwc-show-default-crop' => $element['#show_default_crop'] ? 'true' : 'false', 'data-drupal-iwc-soft-limit' => Json::encode($crop_type->getSoftLimit()), 'data-drupal-iwc-hard-limit' => Json::encode($crop_type->getHardLimit()), @@ -192,14 +195,17 @@ class ImageCrop extends FormElement { // Element to track whether cropping is applied or not. $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied'] = [ '#type' => 'hidden', - '#attributes' => ['data-drupal-iwc-value' => 'applied'], + '#attributes' => [ + 'data-drupal-iwc-value' => 'applied', + 'data-drupal-iwc-id' => $type, + ], '#default_value' => 0, ]; $edit = FALSE; $properties = []; $form_state_values = $form_state->getValue($element['#parents']); // Check if form state has values. - if ($form_state_values) { + if (self::hasCropValues($element, $type, $form_state)) { $form_state_properties = $form_state_values['crop_wrapper'][$type]['crop_container']['values']; // If crop is applied by the form state we keep it that way. if ($form_state_properties['crop_applied'] == '1') { @@ -245,6 +251,21 @@ class ImageCrop extends FormElement { return $element; } + /** + * Check if given $crop_type is required for current instance or not. + * + * @param array $element + * All form elements. + * @param string $crop_type_id + * The id of the current crop. + * + * @return string + * Return string "1" if given crop is required or "0". + */ + public static function isRequiredType(array $element, $crop_type_id) { + return (string) (static::hasCropRequired($element) && in_array($crop_type_id, $element['#crop_types_required']) ?: FALSE); + } + /** * Counts how many times a file has been used. * @@ -315,7 +336,7 @@ class ImageCrop extends FormElement { * @param bool $edit * Context of this form. * - * @return arraystringarraystringstring|null + * @return array|null * Populate all crop elements into the form. */ public static function getCropFormProperties(array $original_properties, $edit) { @@ -396,15 +417,98 @@ class ImageCrop extends FormElement { '@hard_limit' => $hard_limit[$element_name], '@crop_name' => $crop_type->label(), ] - )); + )); } } } + /** + * Evaluate if current element has required crops set from widget settings. + * + * @param array $element + * All form elements without crop properties. + * + * @return bool + * True if 'crop_types_required' settings is set or False. + * + * @see ImageCrop::cropRequired() + */ + public static function hasCropRequired(array $element) { + return isset($element['#crop_types_required']) || !empty($element['#crop_types_required']); + } + + /** + * Form element validation handler for crop widget elements. + * + * @param array $element + * All form elements without crop properties. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @see ImageCrop::getCropFormElement() + */ + public static function cropRequired(array $element, FormStateInterface $form_state) { + if (!static::hasCropRequired($element)) { + return; + } + + $required_crops = []; + foreach ($element['#crop_types_required'] as $crop_type_id) { + $crop_applied = $form_state->getValue($element['#parents'])['crop_wrapper'][$crop_type_id]['crop_container']['values']['crop_applied']; + $action_button = $form_state->getTriggeringElement()['#value']; + $operation = ($action_button instanceof TranslatableMarkup) ? $action_button->getUntranslatedString() : $action_button; + + if (self::fileTriggered($form_state) && self::requiredApplicable($crop_applied, $operation)) { + /** @var \Drupal\crop\Entity\CropType $crop_type */ + $crop_type = \Drupal::entityTypeManager() + ->getStorage('crop_type') + ->load($crop_type_id); + $required_crops[] = $crop_type->label(); + } + } + + if (!empty($required_crops)) { + $form_state->setError($element, \Drupal::translation() + ->formatPlural(count($required_crops), '@crop_required is required.', '@crops_required are required.', [ + "@crop_required" => current($required_crops), + "@crops_required" => implode(', ', $required_crops), + ] + )); + } + } + + /** + * Unsure we have triggered 'file_managed_file_submit' button. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return bool + * True if triggered button are 'file_managed_file_submit' or False. + */ + public static function fileTriggered(FormStateInterface $form_state) { + return !in_array('file_managed_file_submit', $form_state->getTriggeringElement()['#submit']); + } + + /** + * Evaluate if crop is applicable on current CropType. + * + * @param int $crop_applied + * Crop applied parents. + * @param string $operation + * Label current operation. + * + * @return bool + * True if current crop operation isn't "Reset crop" or False. + */ + public static function requiredApplicable($crop_applied, $operation) { + return ((int) $crop_applied === 0 && $operation !== 'Remove'); + } + /** * Set All sizes properties of the crops. * - * @return arraystringarraystringstring|null + * @return array|null * Set all possible crop zone properties. */ public static function setCoordinatesElement() { @@ -416,4 +520,23 @@ class ImageCrop extends FormElement { ]; } + /** + * Evaluate if element has crop values in form states. + * + * @param array $element + * An associative array containing the properties and children of the + * form actions container. + * @param string $type + * Id of current crop type. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return bool + * True if crop element have values or False if not. + */ + public static function hasCropValues(array $element, $type, FormStateInterface $form_state) { + $form_state_values = $form_state->getValue($element['#parents']); + return !empty($form_state_values) && isset($form_state_values['crop_wrapper'][$type]); + } + } diff --git a/web/modules/contrib/image_widget_crop/src/Form/CropWidgetForm.php b/web/modules/contrib/image_widget_crop/src/Form/CropWidgetForm.php index e9df20d6e..fc4e30dbf 100644 --- a/web/modules/contrib/image_widget_crop/src/Form/CropWidgetForm.php +++ b/web/modules/contrib/image_widget_crop/src/Form/CropWidgetForm.php @@ -181,6 +181,17 @@ class CropWidgetForm extends ConfigFormBase { '#default_value' => $this->settings->get('settings.show_default_crop'), ]; + $form['image_crop']['crop_types_required'] = [ + '#title' => $this->t('Set Crop Type as required'), + '#type' => 'select', + '#options' => $this->imageWidgetCropManager->getAvailableCropType(CropType::getCropTypeNames()), + '#empty_option' => $this->t("- Any crop selected -"), + '#default_value' => $this->settings->get('settings.crop_types_required'), + '#multiple' => TRUE, + '#description' => $this->t('Set active crop as required.'), + '#weight' => 16, + ]; + return parent::buildForm($form, $form_state); } @@ -245,7 +256,8 @@ class CropWidgetForm extends ConfigFormBase { ->set("settings.show_default_crop", $form_state->getValue('show_default_crop')) ->set("settings.show_crop_area", $form_state->getValue('show_crop_area')) ->set("settings.warn_multiple_usages", $form_state->getValue('warn_multiple_usages')) - ->set("settings.crop_list", $form_state->getValue('crop_list')); + ->set("settings.crop_list", $form_state->getValue('crop_list')) + ->set("settings.crop_types_required", $form_state->getValue('crop_types_required')); $this->settings->save(); } diff --git a/web/modules/contrib/image_widget_crop/src/Plugin/Field/FieldWidget/ImageCropWidget.php b/web/modules/contrib/image_widget_crop/src/Plugin/Field/FieldWidget/ImageCropWidget.php index 8d3eeb72b..e6538db82 100644 --- a/web/modules/contrib/image_widget_crop/src/Plugin/Field/FieldWidget/ImageCropWidget.php +++ b/web/modules/contrib/image_widget_crop/src/Plugin/Field/FieldWidget/ImageCropWidget.php @@ -2,17 +2,21 @@ namespace Drupal\image_widget_crop\Plugin\Field\FieldWidget; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\Entity\ConfigEntityStorageInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Link; use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\image\Plugin\Field\FieldWidget\ImageWidget; use Drupal\image_widget_crop\ImageWidgetCropInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\crop\Entity\CropType; +use Drupal\Core\Render\Element\Select; /** * Plugin implementation of the 'image_widget_crop' widget. @@ -91,14 +95,12 @@ class ImageCropWidget extends ImageWidget { /** * {@inheritdoc} - * - * @return arraystringstring|null|bool - * The array of settings. */ public static function defaultSettings() { return [ 'crop_preview_image_style' => 'crop_thumbnail', - 'crop_list' => NULL, + 'crop_list' => [], + 'crop_types_required' => [], 'show_crop_area' => FALSE, 'show_default_crop' => TRUE, 'warn_multiple_usages' => TRUE, @@ -126,6 +128,7 @@ class ImageCropWidget extends ImageWidget { '#show_default_crop' => $element['#show_default_crop'], '#show_crop_area' => $element['#show_crop_area'], '#warn_multiple_usages' => $element['#warn_multiple_usages'], + '#crop_types_required' => $element['#crop_types_required'], ]; } } @@ -141,7 +144,7 @@ class ImageCropWidget extends ImageWidget { * @param array $variables * An array with all existent variables for render. * - * @return arraystringarray + * @return array[] * The variables with width & height image informations. */ public static function getFileImageVariables(array $element, array &$variables) { @@ -169,8 +172,21 @@ class ImageCropWidget extends ImageWidget { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { - $element = parent::settingsForm($form, $form_state); + if (!$crop_types_options = $this->imageWidgetCropManager->getAvailableCropType(CropType::getCropTypeNames())) { + $element['message'] = [ + '#type' => 'container', + '#markup' => $this->t('No image style using the "manual crop" effect found. Please first go @link and attach the "manual crop" effect and then return to configure the field widget settings.', ['@link' => Link::createFromRoute('configure one here', 'entity.image_style.collection')->toString()]), + '#attributes' => [ + 'class' => ['messages messages--error'], + ], + ]; + + // Stop process and display error message, + // if any available Image Style is set. + return $element; + } + $element = parent::settingsForm($form, $form_state); $element['crop_preview_image_style'] = [ '#title' => $this->t('Crop preview image style'), '#type' => 'select', @@ -183,13 +199,26 @@ class ImageCropWidget extends ImageWidget { $element['crop_list'] = [ '#title' => $this->t('Crop Type'), '#type' => 'select', - '#options' => $this->imageWidgetCropManager->getAvailableCropType(CropType::getCropTypeNames()), - '#empty_option' => $this->t('<@no-preview>', ['@no-preview' => $this->t('no preview')]), + '#options' => $crop_types_options, '#default_value' => $this->getSetting('crop_list'), '#multiple' => TRUE, '#required' => TRUE, '#description' => $this->t('The type of crop to apply to your image. If your Crop Type not appear here, set an image style use your Crop Type'), '#weight' => 16, + '#ajax' => [ + 'callback' => [static::class, 'updateCropTypeRequiredOptions'], + 'event' => 'change', + ], + ]; + + $element['crop_types_required'] = [ + '#title' => $this->t('Required crop types'), + '#type' => 'select', + '#options' => $crop_types_options, + '#default_value' => $this->getSetting('crop_types_required'), + '#multiple' => TRUE, + '#description' => $this->t('Crop types that should be required.'), + '#weight' => 17, ]; $element['show_crop_area'] = [ @@ -210,9 +239,114 @@ class ImageCropWidget extends ImageWidget { '#default_value' => $this->getSetting('warn_multiple_usages'), ]; + $element['crop_types_required']['#process'] = [ + // We mandatory to re-attach 'processSelect'. + [Select::class, 'processSelect'], + [static::class, 'processCropTypesRequired'], + ]; + + return $element; + } + + /** + * Render API callback: retrieve options for current form element. + * + * @param array $element + * An associative array containing the properties and children of the + * form actions container. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The processed element. + */ + public static function processCropTypesRequired(array &$element, FormStateInterface $form_state, array &$complete_form) { + if (!$form_state->getTriggeringElement()) { + return $element; + } + + // Only display options chosen on 'crop_list', + // element in current form element. + $crop_list_form_element = self::getImageCropWidgetElement($form_state, 'crop_list'); + if (empty($crop_list_form_element)) { + return $element; + } + + $crop_list_options = $crop_list_form_element['#options']; + $crop_list_default_value = array_flip($crop_list_form_element['#default_value']); + + // Populate element options with crop_list selected options. + $element['#options'] = array_intersect_key($crop_list_options, $crop_list_default_value); + return $element; } + /** + * Ajax callback for 'crop_list' select element. + * + * This ajax callback takes care of the following things: + * - Fetching selected options on the 'crop_list' element. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * The Ajax response. + */ + public static function updateCropTypeRequiredOptions(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $triggering_element = $form_state->getTriggeringElement(); + if (isset($triggering_element['#value'])) { + $crop_type_required_form = self::getImageCropWidgetElement($form_state, 'crop_types_required'); + $crop_type_required_form['#options'] = array_intersect_key($triggering_element['#options'], $triggering_element['#value']); + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $output = $renderer->renderRoot($crop_type_required_form); + + // Transform field name onto field name class. + $field_name_class = str_replace('_', '-', $triggering_element['#parents'][1]); + + // Re-construct triggered crop required form element class. + $element_fragments = [ + 'form-item-', + 'fields-', + $field_name_class, + '-settings-edit-form-settings-', + 'crop-types-required', + ]; + + // Replace existing element with selected `crop_list` options. + $response->addCommand(new ReplaceCommand('.' . implode($element_fragments), $output)); + } + + return $response; + } + + /** + * Return a specific of ImageCropWidget form element. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param string $key + * Name of element needed. + * + * @return array + * The form element needed by $key parameter. + */ + public static function getImageCropWidgetElement(FormStateInterface $form_state, $key) { + $triggering_element = $form_state->getTriggeringElement(); + $children = $triggering_element['#parents'][0]; + $field_name = $triggering_element['#parents'][1]; + $field_element_form = $form_state->getCompleteForm()[$children][$field_name]; + + return $field_element_form['plugin']['settings_edit_form']['settings'][$key] ?: []; + } + /** * {@inheritdoc} * @@ -226,14 +360,17 @@ class ImageCropWidget extends ImageWidget { // Unset possible 'No defined styles' option. unset($image_styles['']); - // Styles could be lost because of enabled/disabled modules that defines - // their styles in code. + $crop_list = $this->getSetting('crop_list'); + if (empty($crop_list)) { + return [$this->t('No crop types selected.')]; + } + $image_style_setting = $this->getSetting('preview_image_style'); $crop_preview = $image_styles[$this->getSetting('crop_preview_image_style')]; - $crop_list = $this->getSetting('crop_list'); $crop_show_button = $this->getSetting('show_crop_area'); $show_default_crop = $this->getSetting('show_default_crop'); $warn_multiple_usages = $this->getSetting('warn_multiple_usages'); + $crop_required = $this->getSetting('crop_types_required'); $preview[] = $this->t('Always expand crop area: @bool', ['@bool' => ($crop_show_button) ? 'Yes' : 'No']); $preview[] = $this->t('Show default crop area: @bool', ['@bool' => ($show_default_crop) ? 'Yes' : 'No']); @@ -254,13 +391,17 @@ class ImageCropWidget extends ImageWidget { $preview[] = $this->t('Crop Type used: @list', ['@list' => implode(", ", $crop_list)]); } + if (!empty($crop_required)) { + $preview[] = $this->t('Required crop types : @list', ['@list' => implode(", ", $crop_required)]); + } + return $preview; } /** * {@inheritdoc} * - * @return arraystringarray + * @return array[] * The form elements for a single widget for this field. */ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { @@ -270,6 +411,7 @@ class ImageCropWidget extends ImageWidget { $element['#show_crop_area'] = $this->getSetting('show_crop_area'); $element['#show_default_crop'] = $this->getSetting('show_default_crop'); $element['#warn_multiple_usages'] = $this->getSetting('warn_multiple_usages'); + $element['#crop_types_required'] = $this->getSetting('crop_types_required'); return parent::formElement($items, $delta, $element, $form, $form_state); } diff --git a/web/modules/contrib/image_widget_crop/src/Tests/ImageWidgetCropTest.php b/web/modules/contrib/image_widget_crop/src/Tests/ImageWidgetCropTest.php index 07679bda2..983ee0b7c 100644 --- a/web/modules/contrib/image_widget_crop/src/Tests/ImageWidgetCropTest.php +++ b/web/modules/contrib/image_widget_crop/src/Tests/ImageWidgetCropTest.php @@ -2,9 +2,6 @@ namespace Drupal\image_widget_crop\Tests; -use Drupal\crop\Entity\CropType; -use Drupal\file\Entity\File; -use Drupal\image\Entity\ImageStyle; use Drupal\node\Entity\Node; use Drupal\simpletest\WebTestBase; @@ -57,7 +54,7 @@ class ImageWidgetCropTest extends WebTestBase { */ public function testCropUi() { // Test that when a crop has more than one usage we have a warning. - $this->createImageField('field_image_crop_test', 'crop_test', 'image_widget_crop', [], [], ['crop_list' => ['crop_16_9' => 'crop_16_9']]); + $this->createImageField('field_image_crop_test', 'crop_test', 'image_widget_crop', [], [], ['crop_list' => ['crop_16_9' => 'crop_16_9'], 'crop_types_required' => []]); $this->drupalGetTestFiles('image'); $this->drupalGet('node/add/crop_test'); @@ -90,7 +87,7 @@ class ImageWidgetCropTest extends WebTestBase { */ public function testImageWidgetCrop() { // Test that crop widget works properly. - $this->createImageField('field_image_crop_test', 'crop_test', 'image_widget_crop', [], [], ['crop_list' => ['crop_16_9' => 'crop_16_9']]); + $this->createImageField('field_image_crop_test', 'crop_test', 'image_widget_crop', [], [], ['crop_list' => ['crop_16_9' => 'crop_16_9'], 'crop_types_required' => []]); $this->drupalGetTestFiles('image'); $this->drupalGet('node/add/crop_test'); diff --git a/web/modules/contrib/inline_entity_form/config/schema/inline_entity_form.schema.yml b/web/modules/contrib/inline_entity_form/config/schema/inline_entity_form.schema.yml index a522ec84c..aef7d466e 100644 --- a/web/modules/contrib/inline_entity_form/config/schema/inline_entity_form.schema.yml +++ b/web/modules/contrib/inline_entity_form/config/schema/inline_entity_form.schema.yml @@ -25,6 +25,12 @@ field.widget.settings.inline_entity_form_simple: match_operator: type: string label: "Match operator" + collapsible: + type: boolean + label: "Collapsible" + collapsed: + type: boolean + label: "Collapsed by default" field.widget.settings.inline_entity_form_complex: type: mapping @@ -51,3 +57,12 @@ field.widget.settings.inline_entity_form_complex: match_operator: type: string label: "Match operator" + allow_duplicate: + type: boolean + label: "Allow duplicate" + collapsible: + type: boolean + label: "Collapsible" + collapsed: + type: boolean + label: "Collapsed by default" diff --git a/web/modules/contrib/inline_entity_form/inline_entity_form.info.yml b/web/modules/contrib/inline_entity_form/inline_entity_form.info.yml index a70d040a9..d4b6a49d2 100644 --- a/web/modules/contrib/inline_entity_form/inline_entity_form.info.yml +++ b/web/modules/contrib/inline_entity_form/inline_entity_form.info.yml @@ -6,8 +6,8 @@ package: Fields test_dependencies: - entity_reference_revisions:entity_reference_revisions -# Information added by Drupal.org packaging script on 2016-10-30 -version: '8.x-1.0-beta1' +# Information added by Drupal.org packaging script on 2018-05-22 +version: '8.x-1.0-rc1' core: '8.x' project: 'inline_entity_form' -datestamp: 1477868362 +datestamp: 1527030788 diff --git a/web/modules/contrib/inline_entity_form/inline_entity_form.module b/web/modules/contrib/inline_entity_form/inline_entity_form.module index 056ee9d44..7e1e50803 100644 --- a/web/modules/contrib/inline_entity_form/inline_entity_form.module +++ b/web/modules/contrib/inline_entity_form/inline_entity_form.module @@ -72,14 +72,21 @@ function inline_entity_form_reference_form($reference_form, &$form_state) { $ief_id = $reference_form['#ief_id']; /** @var \Drupal\field\Entity\FieldConfig $instance */ $instance = $form_state->get(['inline_entity_form', $ief_id, 'instance']); + $selection_settings = [ + 'match_operator' => $reference_form['#match_operator'], + ] + $instance->getSetting('handler_settings'); $reference_form['#title'] = t('Add existing @type_singular', ['@type_singular' => $labels['singular']]); + $reference_form['entity_id'] = [ '#type' => 'entity_autocomplete', - '#title' => t('@label', ['@label' => ucwords($labels['singular'])]), + // @todo Use bundle defined singular/plural labels as soon as + // https://www.drupal.org/node/2765065 is committed. + // @see https://www.drupal.org/node/2765065 + '#title' => t('@label', ['@label' => ucfirst($labels['singular'])]), '#target_type' => $instance->getSetting('target_type'), '#selection_handler' => $instance->getSetting('handler'), - '#selection_settings' => $instance->getSetting('handler_settings'), + '#selection_settings' => $selection_settings, '#required' => TRUE, '#maxlength' => 255, ]; @@ -125,7 +132,7 @@ function inline_entity_form_reference_form($reference_form, &$form_state) { * Validates the form for adding existing entities. * * @param array $reference_form - * The reference entity form. + * The reference entity form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state of the parent form. */ @@ -158,7 +165,7 @@ function inline_entity_form_reference_form_validate(&$reference_form, FormStateI * Adds the specified entity to the IEF form state. * * @param array $reference_form - * The reference entity form. + * The reference entity form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state of the parent form. */ @@ -334,10 +341,10 @@ function theme_inline_entity_form_entity_table($variables) { // Add header columns for each field. $first = TRUE; foreach ($fields as $field_name => $field) { - $column = ['data' => $field['label']]; + $column = ['data' => $field['label'], 'class' => ['inline-entity-form-' . $entity_type . '-' . $field_name]]; // The first column gets a special class. if ($first) { - $column['class'] = ['ief-first-column-header']; + $column['class'][] = 'ief-first-column-header'; $first = FALSE; } $header[] = $column; @@ -363,7 +370,6 @@ function theme_inline_entity_form_entity_table($variables) { } foreach ($fields as $field_name => $field) { - $data = ''; if ($field['type'] == 'label') { $data = $variables['form'][$key]['#label']; } @@ -385,6 +391,9 @@ function theme_inline_entity_form_entity_table($variables) { $data = call_user_func_array($field['callback'], $arguments); } + else { + $data = t('N/A'); + } $cells[] = ['data' => $data, 'class' => ['inline-entity-form-' . $entity_type . '-' . $field_name]]; } diff --git a/web/modules/contrib/inline_entity_form/src/Element/InlineEntityForm.php b/web/modules/contrib/inline_entity_form/src/Element/InlineEntityForm.php index b55f7812c..0fbe2335a 100644 --- a/web/modules/contrib/inline_entity_form/src/Element/InlineEntityForm.php +++ b/web/modules/contrib/inline_entity_form/src/Element/InlineEntityForm.php @@ -47,9 +47,14 @@ class InlineEntityForm extends RenderElement { '#form_mode' => 'default', // Will save entity on submit if set to TRUE. '#save_entity' => TRUE, - // 'add' or 'edit'. If NULL, determined by whether the entity is new. + // 'add', 'edit' or 'duplicate'. '#op' => NULL, '#process' => [ + // Core's #process for groups, don't remove it. + [$class, 'processGroup'], + + // InlineEntityForm's #process must run after the above ::processGroup + // in case any new elements (like groups) were added in alter hooks. [$class, 'processEntityForm'], ], '#element_validate' => [ @@ -59,9 +64,10 @@ class InlineEntityForm extends RenderElement { [$class, 'submitEntityForm'], ], '#theme_wrappers' => ['container'], - // Allow inline forms to use the #fieldset key. + '#pre_render' => [ - [$class, 'addFieldsetMarkup'], + // Core's #pre_render for groups, don't remove it. + [$class, 'preRenderGroup'], ], ]; } @@ -114,7 +120,13 @@ class InlineEntityForm extends RenderElement { $entity_form['#entity'] = $storage->create($values); } if (!isset($entity_form['#op'])) { - $entity_form['#op'] = $entity_form['#entity']->isNew() ? 'add' : 'edit'; + // When duplicating entities, the entity is new, but already has a UUID. + if ($entity_form['#entity']->isNew() && $entity_form['#entity']->uuid()) { + $entity_form['#op'] = 'duplicate'; + } + else { + $entity_form['#op'] = $entity_form['#entity']->isNew() ? 'add' : 'edit'; + } } // Prepare the entity form and the entity itself for translating. $entity_form['#entity'] = TranslationHelper::prepareEntity($entity_form['#entity'], $form_state); @@ -179,42 +191,4 @@ class InlineEntityForm extends RenderElement { return $inline_form_handler; } - /** - * Pre-render callback for the #fieldset form property. - * - * Inline forms use #tree = TRUE to keep their values in a hierarchy for - * easier storage. Moving the form elements into fieldsets during form - * building would break up that hierarchy, so it's not an option for entity - * fields. Therefore, we wait until the pre_render stage, where any changes - * we make affect presentation only and aren't reflected in $form_state. - * - * @param array $entity_form - * The entity form. - * - * @return array - * The modified entity form. - */ - public static function addFieldsetMarkup($entity_form) { - $sort = []; - foreach (Element::children($entity_form) as $key) { - $element = $entity_form[$key]; - if (isset($element['#fieldset']) && isset($entity_form[$element['#fieldset']])) { - $entity_form[$element['#fieldset']][$key] = $element; - // Remove the original element this duplicates. - unset($entity_form[$key]); - // Mark the fieldset for sorting. - if (!in_array($key, $sort)) { - $sort[] = $element['#fieldset']; - } - } - } - - // Sort all fieldsets, so that element #weight stays respected. - foreach ($sort as $key) { - uasort($entity_form[$key], '\Drupal\Component\Utility\SortArray::sortByWeightProperty'); - } - - return $entity_form; - } - } diff --git a/web/modules/contrib/inline_entity_form/src/Form/EntityInlineForm.php b/web/modules/contrib/inline_entity_form/src/Form/EntityInlineForm.php index 06cbbdc0b..1ff507379 100644 --- a/web/modules/contrib/inline_entity_form/src/Form/EntityInlineForm.php +++ b/web/modules/contrib/inline_entity_form/src/Form/EntityInlineForm.php @@ -182,9 +182,46 @@ class EntityInlineForm implements InlineFormInterface { } } } + + // Determine the children of the entity form before it has been altered. + $children_before = Element::children($entity_form); + // Allow other modules to alter the form. $this->moduleHandler->alter('inline_entity_form_entity_form', $entity_form, $form_state); + // Determine the children of the entity form after it has been altered. + $children_after = Element::children($entity_form); + + // Ensure that any new children added have #tree, #parents, #array_parents + // and handle setting the proper #group if it's referencing a local element. + // Note: the #tree, #parents and #array_parents code is a direct copy from + // \Drupal\Core\Form\FormBuilder::doBuildForm. + $children_diff = array_diff($children_after, $children_before); + foreach ($children_diff as $child) { + // Don't squash an existing tree value. + if (!isset($entity_form[$child]['#tree'])) { + $entity_form[$child]['#tree'] = $entity_form['#tree']; + } + + // Don't squash existing parents value. + if (!isset($entity_form[$child]['#parents'])) { + // Check to see if a tree of child elements is present. If so, + // continue down the tree if required. + $entity_form[$child]['#parents'] = $entity_form[$child]['#tree'] && $entity_form['#tree'] ? array_merge($entity_form['#parents'], [$child]) : [$child]; + } + + // Ensure #array_parents follows the actual form structure. + $array_parents = $entity_form['#array_parents']; + $array_parents[] = $child; + $entity_form[$child]['#array_parents'] = $array_parents; + + // Detect if there is a #group and it specifies a local element. If so, + // change it to use the proper local element's #parents group name. + if (isset($entity_form[$child]['#group']) && isset($entity_form[$entity_form[$child]['#group']])) { + $entity_form[$child]['#group'] = implode('][', $entity_form[$entity_form[$child]['#group']]['#parents']); + } + } + return $entity_form; } @@ -203,7 +240,7 @@ class EntityInlineForm implements InlineFormInterface { $form_display->validateFormValues($entity, $entity_form, $form_state); $entity->setValidationRequired(FALSE); - foreach($form_state->getErrors() as $name => $message) { + foreach ($form_state->getErrors() as $name => $message) { // $name may be unknown in $form_state and // $form_state->setErrorByName($name, $message) may suppress the error message. $form_state->setError($triggering_element, $message); diff --git a/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormBase.php b/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormBase.php index 123b58fc6..1e69c7c9f 100644 --- a/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormBase.php +++ b/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormBase.php @@ -2,7 +2,6 @@ namespace Drupal\inline_entity_form\Plugin\Field\FieldWidget; -use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; @@ -72,7 +71,7 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto * The entity type bundle info. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. - * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface + * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository * The entity display repository. */ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository) { @@ -194,6 +193,8 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto 'override_labels' => FALSE, 'label_singular' => '', 'label_plural' => '', + 'collapsible' => FALSE, + 'collapsed' => FALSE, ]; } @@ -236,6 +237,21 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto ], ], ]; + $element['collapsible'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Collapsible'), + '#default_value' => $this->getSetting('collapsible'), + ]; + $element['collapsed'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Collapsed by default'), + '#default_value' => $this->getSetting('collapsed'), + '#states' => [ + 'visible' => [ + ':input[name="' . $states_prefix . '[collapsible]"]' => ['checked' => TRUE], + ], + ], + ]; return $element; } @@ -262,6 +278,10 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto $summary[] = $this->t('Default labels are used.'); } + if ($this->getSetting('collapsible')) { + $summary[] = $this->t($this->getSetting('collapsed') ? 'Collapsible, collapsed by default' : 'Collapsible'); + } + return $summary; } @@ -291,7 +311,7 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto * - Is IEF handler loaded? * - Are we on a "real" entity form and not on default value widget? * - * @param FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state. * * @return bool @@ -404,7 +424,7 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto * @return bool * TRUE if translating is in progress, FALSE otherwise. * - * @see \Drupal\inline_entity_form\TranslationHelper::initFormLangcodes(). + * @see \Drupal\inline_entity_form\TranslationHelper::initFormLangcodes() */ protected function isTranslating(FormStateInterface $form_state) { if (TranslationHelper::isTranslating($form_state)) { @@ -453,8 +473,8 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto * still decide to cancel the parent form. * * @param $entity_form - * The form of the entity being managed inline. - * @param $form_state + * The form of the entity being managed inline. + * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state of the parent form. */ public static function submitSaveEntity($entity_form, FormStateInterface $form_state) { @@ -462,7 +482,7 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto /** @var \Drupal\Core\Entity\EntityInterface $entity */ $entity = $entity_form['#entity']; - if ($entity_form['#op'] == 'add') { + if (in_array($entity_form['#op'], ['add', 'duplicate'])) { // Determine the correct weight of the new element. $weight = 0; $entities = $form_state->get(['inline_entity_form', $ief_id, 'entities']); diff --git a/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormComplex.php b/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormComplex.php index 92a52feec..b8424be3b 100644 --- a/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormComplex.php +++ b/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormComplex.php @@ -37,7 +37,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF protected $moduleHandler; /** - * Constructs a InlineEntityFormBase object. + * Constructs a InlineEntityFormComplex object. * * @param array $plugin_id * The plugin_id for the widget. @@ -53,7 +53,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF * The entity type bundle info. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. - * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface + * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository * The entity display repository. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * Module handler service. @@ -89,6 +89,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF 'allow_new' => TRUE, 'allow_existing' => FALSE, 'match_operator' => 'CONTAINS', + 'allow_duplicate' => FALSE, ]; return $defaults; @@ -124,6 +125,11 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF ], ], ]; + $element['allow_duplicate'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Allow users to duplicate @label.', ['@label' => $labels['plural']]), + '#default_value' => $this->getSetting('allow_duplicate'), + ]; return $element; } @@ -153,6 +159,13 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF $summary[] = $this->t('Existing @label can not be referenced.', ['@label' => $labels['plural']]); } + if ($this->getSetting('allow_duplicate')) { + $summary[] = $this->t('@label can be duplicated.', ['@label' => $labels['plural']]); + } + else { + $summary[] = $this->t('@label can not be duplicated.', ['@label' => $labels['plural']]); + } + return $summary; } @@ -196,7 +209,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF $wrapper = 'inline-entity-form-' . $this->getIefId(); $element = [ - '#type' => 'fieldset', + '#type' => $this->getSetting('collapsible') ? 'details' : 'fieldset', '#tree' => TRUE, '#description' => $this->fieldDefinition->getDescription(), '#prefix' => '<div id="' . $wrapper . '">', @@ -209,12 +222,20 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF [get_class($this), 'removeTranslatabilityClue'], ], ] + $element; + if ($element['#type'] == 'details') { + $element['#open'] = !$this->getSetting('collapsed'); + } $element['#attached']['library'][] = 'inline_entity_form/widget'; $this->prepareFormState($form_state, $items, $element['#translating']); $entities = $form_state->get(['inline_entity_form', $this->getIefId(), 'entities']); + // Prepare cardinality information. + $entities_count = count($entities); + $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); + $cardinality_reached = ($cardinality > 0 && $entities_count == $cardinality); + // Build the "Multiple value" widget. // TODO - does this belong in #element_validate? $element['#element_validate'][] = [get_class($this), 'updateRowWeights']; @@ -242,7 +263,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF $this->moduleHandler->alter('inline_entity_form_table_fields', $fields, $context); $element['entities']['#table_fields'] = $fields; - $weight_delta = max(ceil(count($entities) * 1.2), 50); + $weight_delta = max(ceil($entities_count * 1.2), 50); foreach ($entities as $key => $value) { // Data used by theme_inline_entity_form_entity_table(). /** @var \Drupal\Core\Entity\EntityInterface $entity */ @@ -263,7 +284,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF ]; // Add the appropriate form. - if ($value['form'] == 'edit') { + if (in_array($value['form'], ['edit', 'duplicate'])) { $element['entities'][$key]['form'] = [ '#type' => 'container', '#attributes' => ['class' => ['ief-form', 'ief-form-row']], @@ -272,8 +293,8 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF $entity->bundle(), $parent_langcode, $key, - array_merge($parents, ['inline_entity_form', 'entities', $key, 'form']), - $entity + array_merge($parents, ['inline_entity_form', 'entities', $key, 'form']), + $value['form'] == 'edit' ? $entity : $entity->createDuplicate() ), ]; @@ -333,6 +354,23 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF ]; } + // Add the duplicate button, if allowed. + if ($settings['allow_duplicate'] && !$cardinality_reached && $entity->access('create')) { + $row['actions']['ief_entity_duplicate'] = [ + '#type' => 'submit', + '#value' => $this->t('Duplicate'), + '#name' => 'ief-' . $this->getIefId() . '-entity-duplicate-' . $key, + '#limit_validation_errors' => [array_merge($parents, ['actions'])], + '#ajax' => [ + 'callback' => 'inline_entity_form_get_element', + 'wrapper' => $wrapper, + ], + '#submit' => ['inline_entity_form_open_row_form'], + '#ief_row_delta' => $key, + '#ief_row_form' => 'duplicate', + ]; + } + // If 'allow_existing' is on, the default removal operation is unlink // and the access check for deleting happens inside the controller // removeForm() method. @@ -365,8 +403,6 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF return $element; } - $entities_count = count($entities); - $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); if ($cardinality > 1) { // Add a visual cue of cardinality count. $message = $this->t('You have added @entities_count out of @cardinality_count allowed @label.', [ @@ -379,7 +415,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF ]; } // Do not return the rest of the form if cardinality count has been reached. - if ($cardinality > 0 && $entities_count == $cardinality) { + if ($cardinality_reached) { return $element; } @@ -399,7 +435,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF // The parent entity type and bundle must not be the same as the inline // entity type and bundle, to prevent recursion. $parent_entity_type = $this->fieldDefinition->getTargetEntityTypeId(); - $parent_bundle = $this->fieldDefinition->getTargetBundle(); + $parent_bundle = $this->fieldDefinition->getTargetBundle(); if ($parent_entity_type != $target_type || $parent_bundle != $bundle) { $form_state->set(['inline_entity_form', $this->getIefId(), 'form'], 'add'); $form_state->set(['inline_entity_form', $this->getIefId(), 'form settings'], [ @@ -502,10 +538,9 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF // Used by Field API and controller methods to find the relevant // values in $form_state. '#parents' => array_merge($parents), - // Pass the current entity type. '#entity_type' => $target_type, - // Pass the widget specific labels. '#ief_labels' => $this->getEntityTypeLabels(), + '#match_operator' => $this->getSetting('match_operator'), ]; $element['form'] += inline_entity_form_reference_form($element['form'], $form_state); @@ -591,6 +626,9 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF if ($element['#op'] == 'add') { $save_label = t('Create @type_singular', ['@type_singular' => $element['#ief_labels']['singular']]); } + elseif ($element['#op'] == 'duplicate') { + $save_label = t('Duplicate @type_singular', ['@type_singular' => $element['#ief_labels']['singular']]); + } else { $delta .= '-' . $element['#ief_row_delta']; $save_label = t('Update @type_singular', ['@type_singular' => $element['#ief_labels']['singular']]); @@ -633,12 +671,12 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF ]; } else { - $element['actions']['ief_edit_save']['#ief_row_delta'] = $element['#ief_row_delta']; - $element['actions']['ief_edit_cancel']['#ief_row_delta'] = $element['#ief_row_delta']; + $element['actions']['ief_' . $element['#op'] . '_save']['#ief_row_delta'] = $element['#ief_row_delta']; + $element['actions']['ief_' . $element['#op'] . '_cancel']['#ief_row_delta'] = $element['#ief_row_delta']; - static::addSubmitCallbacks($element['actions']['ief_edit_save']); - $element['actions']['ief_edit_save']['#submit'][] = [get_called_class(), 'submitCloseRow']; - $element['actions']['ief_edit_cancel']['#submit'] = [ + static::addSubmitCallbacks($element['actions']['ief_' . $element['#op'] . '_save']); + $element['actions']['ief_' . $element['#op'] . '_save']['#submit'][] = [get_called_class(), 'submitCloseRow']; + $element['actions']['ief_' . $element['#op'] . '_cancel']['#submit'] = [ [get_called_class(), 'closeChildForms'], [get_called_class(), 'submitCloseRow'], 'inline_entity_form_cleanup_row_form_state', @@ -741,7 +779,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF * @param $form_state * The form state of the parent form. * - * @see inline_entity_form_open_row_form(). + * @see inline_entity_form_open_row_form() */ public static function submitCloseRow($form, FormStateInterface $form_state) { $element = inline_entity_form_get_element($form, $form_state); @@ -796,7 +834,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF /** * Determines bundle to be used when creating entity. * - * @param FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * Current form state. * * @return string @@ -858,7 +896,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF * @param $form_state * The form state of the parent form. * - * @see inline_entity_form_open_form(). + * @see inline_entity_form_open_form() */ public static function closeForm($form, FormStateInterface $form_state) { $element = inline_entity_form_get_element($form, $form_state); @@ -889,7 +927,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF * * @param $form * The IEF Form element. - * @param FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state of the parent form. */ public static function closeChildForms($form, FormStateInterface &$form_state) { diff --git a/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormSimple.php b/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormSimple.php index 87e779229..985e06bc6 100644 --- a/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormSimple.php +++ b/web/modules/contrib/inline_entity_form/src/Plugin/Field/FieldWidget/InlineEntityFormSimple.php @@ -36,12 +36,16 @@ class InlineEntityFormSimple extends InlineEntityFormBase { $form_state->set(['inline_entity_form', $ief_id], []); $element = [ - '#type' => 'fieldset', + '#type' => $this->getSetting('collapsible') ? 'details' : 'fieldset', '#field_title' => $this->fieldDefinition->getLabel(), '#after_build' => [ [get_class($this), 'removeTranslatabilityClue'], ], ] + $element; + if ($element['#type'] == 'details') { + $element['#open'] = !$this->getSetting('collapsed'); + } + $item = $items->get($delta); if ($item->target_id && !$item->entity) { $element['warning']['#markup'] = $this->t('Unable to load the referenced entity.'); @@ -55,7 +59,7 @@ class InlineEntityFormSimple extends InlineEntityFormBase { $delta, 'inline_entity_form' ]); - $bundle = !empty($this->getFieldSetting('handler_settings')['target_bundles']) ? reset($this->getFieldSetting('handler_settings')['target_bundles']) : NULL; + $bundle = $this->getBundle(); $element['inline_entity_form'] = $this->getInlineEntityForm($op, $bundle, $langcode, $delta, $parents, $entity); if ($op == 'edit') { @@ -196,4 +200,16 @@ class InlineEntityFormSimple extends InlineEntityFormBase { return TRUE; } + /** + * Gets the bundle for the inline entity. + * + * @return string|null + * The bundle, or NULL if not known. + */ + protected function getBundle() { + if (!empty($this->getFieldSetting('handler_settings')['target_bundles'])) { + return reset($this->getFieldSetting('handler_settings')['target_bundles']); + } + } + } diff --git a/web/modules/contrib/inline_entity_form/src/Tests/ComplexWidgetWebTest.php b/web/modules/contrib/inline_entity_form/src/Tests/ComplexWidgetWebTest.php index 974c8a05a..d606c841b 100644 --- a/web/modules/contrib/inline_entity_form/src/Tests/ComplexWidgetWebTest.php +++ b/web/modules/contrib/inline_entity_form/src/Tests/ComplexWidgetWebTest.php @@ -66,7 +66,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { */ public function testEmptyFieldIEF() { // Don't allow addition of existing nodes. - $this->setAllowExisting(FALSE); + $this->updateSetting('allow_existing', FALSE); $this->drupalGet($this->formContentAddUrl); $this->assertFieldByName('multi[form][inline_entity_form][title][0][value]', NULL, 'Title field on inline form exists.'); @@ -75,7 +75,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { $this->assertFieldByXpath('//input[@type="submit" and @value="Create node"]', NULL, 'Found "Create node" submit button'); // Allow addition of existing nodes. - $this->setAllowExisting(TRUE); + $this->updateSetting('allow_existing', TRUE); $this->drupalGet($this->formContentAddUrl); $this->assertNoFieldByName('multi[form][inline_entity_form][title][0][value]', NULL, 'Title field does not appear.'); @@ -107,7 +107,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { */ public function testEntityCreation() { // Allow addition of existing nodes. - $this->setAllowExisting(TRUE); + $this->updateSetting('allow_existing', TRUE); $this->drupalGet($this->formContentAddUrl); $this->drupalPostAjaxForm(NULL, [], $this->getButtonName('//input[@type="submit" and @value="Add new node" and @data-drupal-selector="edit-multi-actions-ief-add"]')); @@ -183,7 +183,6 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { foreach ($required_possibilities as $required) { $this->setupNestedComplexForm($required); - $nested3_title = 'nested3 title steps ' . ($required ? 'required' : 'not required'); $nested2_title = 'nested2 title steps ' . ($required ? 'required' : 'not required'); $nested1_title = 'nested1 title steps ' . ($required ? 'required' : 'not required'); @@ -213,10 +212,9 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { * Checks that nested IEF entity references can be edit and saved. * * @param \Drupal\node\Entity\Node $node - * Top level node of type ief_test_nested1 to check. + * Top level node of type ief_test_nested1 to check. * @param bool $ajax_submit - * Whether IEF form widgets should be submitted via AJax or left open. - * + * Whether IEF form widgets should be submitted via AJAX or left open. */ protected function checkNestedEntityEditing(Node $node, $ajax_submit = TRUE) { $this->drupalGet("node/{$node->id()}/edit"); @@ -225,14 +223,14 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { /** @var \Drupal\node\Entity\Node $level_2_node */ $level_2_node = $node->test_ref_nested1->entity->test_ref_nested2->entity; $level_2_node_update_title = $level_2_node->getTitle() . ' - updated'; - //edit-test-ref-nested1-entities-0-actions-ief-entity-edit + // edit-test-ref-nested1-entities-0-actions-ief-entity-edit $this->drupalPostAjaxForm(NULL, [], $this->getButtonName('//input[@type="submit" and @data-drupal-selector="edit-test-ref-nested1-entities-0-actions-ief-entity-edit"]')); - //edit-test-ref-nested1-form-inline-entity-form-entities-0-form-test-ref-nested2-entities-0-actions-ief-entity-edit + // edit-test-ref-nested1-form-inline-entity-form-entities-0-form-test-ref-nested2-entities-0-actions-ief-entity-edit $this->drupalPostAjaxForm(NULL, [], $this->getButtonName('//input[@type="submit" and @data-drupal-selector="edit-test-ref-nested1-form-inline-entity-form-entities-0-form-test-ref-nested2-entities-0-actions-ief-entity-edit"]')); $edit['test_ref_nested1[form][inline_entity_form][entities][0][form][test_ref_nested2][form][inline_entity_form][entities][0][form][title][0][value]'] = $level_2_node_update_title; if ($ajax_submit) { // Close IEF Forms with AJAX posts - //edit-test-ref-nested1-form-inline-entity-form-entities-0-form-test-ref-nested2-form-inline-entity-form-entities-0-form-actions-ief-edit-save + // edit-test-ref-nested1-form-inline-entity-form-entities-0-form-test-ref-nested2-form-inline-entity-form-entities-0-form-actions-ief-edit-save $this->drupalPostAjaxForm(NULL, $edit, $this->getButtonName('//input[@type="submit" and @data-drupal-selector="edit-test-ref-nested1-form-inline-entity-form-entities-0-form-test-ref-nested2-form-inline-entity-form-entities-0-form-actions-ief-edit-save"]')); $this->drupalPostAjaxForm(NULL, [], $this->getButtonName('//input[@type="submit" and @data-drupal-selector="edit-test-ref-nested1-form-inline-entity-form-entities-0-form-actions-ief-edit-save"]')); $this->drupalPostForm(NULL, [], t('Save')); @@ -278,7 +276,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { */ public function testEntityEditingAndRemoving() { // Allow addition of existing nodes. - $this->setAllowExisting(TRUE); + $this->updateSetting('allow_existing', TRUE); // Create three ief_reference_type entities. $referenceNodes = $this->createReferenceContent(3); @@ -291,7 +289,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { $parent_node = $this->drupalGetNodeByTitle('Some title'); // Edit the second entity. - $this->drupalGet('node/'. $parent_node->id() .'/edit'); + $this->drupalGet('node/' . $parent_node->id() . '/edit'); $cell = $this->xpath('//table[@id="ief-entity-table-edit-multi-entities"]/tbody/tr[@class="ief-row-entity draggable even"]/td[@class="inline-entity-form-node-label"]'); $title = (string) $cell[0]; @@ -315,7 +313,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { $this->assertTrue($node->last_name->value == 'Doe', 'Last name in reference node changed to Doe'); // Delete the second entity. - $this->drupalGet('node/'. $parent_node->id() .'/edit'); + $this->drupalGet('node/' . $parent_node->id() . '/edit'); $cell = $this->xpath('//table[@id="ief-entity-table-edit-multi-entities"]/tbody/tr[@class="ief-row-entity draggable even"]/td[@class="inline-entity-form-node-label"]'); $title = (string) $cell[0]; @@ -335,13 +333,13 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { $this->assertTrue(empty($deleted_node), 'The inline entity was deleted from the site.'); // Checks that entity does nor appear in IEF. - $this->drupalGet('node/'. $parent_node->id() .'/edit'); + $this->drupalGet('node/' . $parent_node->id() . '/edit'); $this->assertNoText($title, 'Deleted inline entity is not present on the page after saving parent.'); // Delete the third entity reference only, don't delete the node. The third // entity now is second referenced entity because the second one was deleted // in previous step. - $this->drupalGet('node/'. $parent_node->id() .'/edit'); + $this->drupalGet('node/' . $parent_node->id() . '/edit'); $cell = $this->xpath('//table[@id="ief-entity-table-edit-multi-entities"]/tbody/tr[@class="ief-row-entity draggable even"]/td[@class="inline-entity-form-node-label"]'); $title = (string) $cell[0]; @@ -356,7 +354,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { $this->assertResponse(200, 'Saving parent node was successful.'); // Checks that entity does nor appear in IEF. - $this->drupalGet('node/'. $parent_node->id() . '/edit'); + $this->drupalGet('node/' . $parent_node->id() . '/edit'); $this->assertNoText($title, 'Deleted inline entity is not present on the page after saving parent.'); // Checks that entity is not deleted. @@ -369,7 +367,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { */ public function testReferencingExistingEntities() { // Allow addition of existing nodes. - $this->setAllowExisting(TRUE); + $this->updateSetting('allow_existing', TRUE); // Create three ief_reference_type entities. $referenceNodes = $this->createReferenceContent(3); @@ -417,10 +415,10 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { $this->assertResponse(200, 'Saving parent for was successful.'); // Check if entities are referenced. - $this->drupalGet('node/'. $parent_node->id() .'/edit'); + $this->drupalGet('node/' . $parent_node->id() . '/edit'); for ($i = 2; $i <= 3; $i++) { $cell = $this->xpath('//table[@id="ief-entity-table-edit-multi-entities"]/tbody/tr[' . $i . ']/td[@class="inline-entity-form-node-label"]'); - $this->assertTrue($cell[0] == 'Some reference ' . $i, 'Found reference node title "Some reference ' . $i .'" in the IEF table.'); + $this->assertTrue($cell[0] == 'Some reference ' . $i, 'Found reference node title "Some reference ' . $i . '" in the IEF table.'); } // Check if all remaining nodes from all bundles are referenced. $count = 2; @@ -437,7 +435,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { * Also checks if existing entity reference form can be canceled. */ public function testReferenceExistingValidation() { - $this->setAllowExisting(TRUE); + $this->updateSetting('allow_existing', TRUE); $this->drupalGet('node/add/ief_test_complex'); $this->checkExistingValidationExpectation('', 'Node field is required.'); @@ -465,12 +463,50 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { } } + /** + * Tests if duplicating entities works. + */ + public function testDuplicatingEntities() { + $this->updateSetting('allow_duplicate', TRUE); + + $referenceNodes = $this->createReferenceContent(2); + $this->drupalCreateNode([ + 'type' => 'ief_test_complex', + 'title' => 'Some title', + 'multi' => array_values($referenceNodes), + ]); + /** @var \Drupal\node\NodeInterface $node */ + $parent_node = $this->drupalGetNodeByTitle('Some title'); + + $this->drupalGet('node/' . $parent_node->id() . '/edit'); + $this->drupalPostAjaxForm(NULL, [], $this->getButtonName('//input[@type="submit" and @id="edit-multi-entities-0-actions-ief-entity-duplicate"]')); + $this->assertResponse(200, 'Opening inline duplicate form was successful.'); + + $edit = [ + 'multi[form][inline_entity_form][entities][0][form][title][0][value]' => 'Duplicate!', + 'multi[form][inline_entity_form][entities][0][form][first_name][0][value]' => 'Bojan', + ]; + $this->drupalPostAjaxForm(NULL, $edit, $this->getButtonName('//input[@type="submit" and @data-drupal-selector="edit-multi-form-inline-entity-form-entities-0-form-actions-ief-duplicate-save"]')); + $this->assertResponse(200, 'Saving inline duplicate form was successful.'); + + $this->assertText('Some reference 1'); + $this->assertText('Some reference 2'); + $this->assertText('Duplicate!'); + $this->drupalPostForm(NULL, [], t('Save')); + $this->assertResponse(200, 'Saving parent entity was successful.'); + + // Confirm a duplicate was made. + $duplicate = Node::load(4); + $this->assertEqual($duplicate->label(), 'Duplicate!'); + $this->assertEqual($duplicate->first_name->value, 'Bojan'); + } + /** * Tests if a referenced content can be edited while the referenced content is * newer than the referencing parent node. */ public function testEditedInlineEntityValidation() { - $this->setAllowExisting(TRUE); + $this->updateSetting('allow_existing', TRUE); // Create referenced content. $referenced_nodes = $this->createReferenceContent(1); @@ -551,16 +587,18 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { } /** - * Sets allow_existing IEF setting. + * Updates an IEF setting and saves the underlying entity display. * - * @param bool $flag - * "allow_existing" flag to be set. + * @param string $name + * The name of the setting. + * @param mixed $value + * The value to set. */ - protected function setAllowExisting($flag) { + protected function updateSetting($name, $value) { /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */ $display = $this->entityFormDisplayStorage->load('node.ief_test_complex.default'); $component = $display->getComponent('multi'); - $component['settings']['allow_existing'] = $flag; + $component['settings'][$name] = $value; $display->setComponent('multi', $component)->save(); } @@ -589,7 +627,7 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { * Gets the form. * Opens the inline entity forms if they are not required. * - * @param boolean $required + * @param bool $required * Whether the fields are required. * @param array $permissions * (optional) Permissions to sign testing user in with. You may pass in an @@ -653,9 +691,9 @@ class ComplexWidgetWebTest extends InlineEntityFormTestBase { * Checks that an invalid value for an existing node will be display the expected error. * * @param $existing_node_text - * The text to enter into the existing node text field. + * The text to enter into the existing node text field. * @param $expected_error - * The error message that is expected to be shown. + * The error message that is expected to be shown. */ protected function checkExistingValidationExpectation($existing_node_text, $expected_error) { $edit = [ diff --git a/web/modules/contrib/inline_entity_form/src/Tests/ElementWebTest.php b/web/modules/contrib/inline_entity_form/src/Tests/ElementWebTest.php index 59a5a5f9f..44686e884 100644 --- a/web/modules/contrib/inline_entity_form/src/Tests/ElementWebTest.php +++ b/web/modules/contrib/inline_entity_form/src/Tests/ElementWebTest.php @@ -67,7 +67,7 @@ class ElementWebTest extends InlineEntityFormTestBase { $this->assertNodeByTitle($title, 'ief_test_custom'); if ($node = $this->getNodeByTitle($title)) { - $this->drupalGet("ief-edit-test/{$node->id()}/$form_mode_possibility"); + $this->drupalGet("ief-test/$form_mode_possibility/{$node->id()}"); $this->assertFieldByName('inline_entity_form[title][0][value]', $title, 'Node title appears in form.'); $this->checkFormDisplayFields("node.ief_test_custom.$form_mode_possibility", 'inline_entity_form'); $this->assertFieldByName('inline_entity_form[positive_int][0][value]', 11, 'Positive int field appears in form.'); diff --git a/web/modules/contrib/inline_entity_form/src/Tests/InlineEntityFormTestBase.php b/web/modules/contrib/inline_entity_form/src/Tests/InlineEntityFormTestBase.php index e3bf62d6d..d3a399f05 100644 --- a/web/modules/contrib/inline_entity_form/src/Tests/InlineEntityFormTestBase.php +++ b/web/modules/contrib/inline_entity_form/src/Tests/InlineEntityFormTestBase.php @@ -140,7 +140,7 @@ abstract class InlineEntityFormTestBase extends WebTestBase { * in inline_entity_form_test module. * * @param $form_display - * The form display to check. + * The form display to check. */ protected function checkFormDisplayFields($form_display, $prefix) { $form_display_fields = [ @@ -171,7 +171,7 @@ abstract class InlineEntityFormTestBase extends WebTestBase { ], ]; if ($fields = $form_display_fields[$form_display]) { - $this->assert('debug', 'Checking form dispaly: '. $form_display); + $this->assert('debug', 'Checking form dispaly: ' . $form_display); foreach ($fields['expected'] as $expected_field) { $this->assertFieldByName($prefix . $expected_field); } diff --git a/web/modules/contrib/inline_entity_form/src/Tests/SimpleWidgetWebTest.php b/web/modules/contrib/inline_entity_form/src/Tests/SimpleWidgetWebTest.php index 519c2611f..531c60212 100644 --- a/web/modules/contrib/inline_entity_form/src/Tests/SimpleWidgetWebTest.php +++ b/web/modules/contrib/inline_entity_form/src/Tests/SimpleWidgetWebTest.php @@ -134,8 +134,8 @@ class SimpleWidgetWebTest extends InlineEntityFormTestBase { $child_node = $this->getNodeByTitle($child_title); if ($this->assertNotNull($child_node)) { $this->assertEqual($host_node->single[0]->target_id, $child_node->id(), 'Child node is referenced'); - $this->assertEqual($child_node->positive_int[0]->value,1, 'Child node int field correct.'); - $this->assertEqual($child_node->bundle(),'ief_test_custom', 'Child node is correct bundle.'); + $this->assertEqual($child_node->positive_int[0]->value, 1, 'Child node int field correct.'); + $this->assertEqual($child_node->bundle(), 'ief_test_custom', 'Child node is correct bundle.'); } } } diff --git a/web/modules/contrib/inline_entity_form/src/Tests/TranslationTest.php b/web/modules/contrib/inline_entity_form/src/Tests/TranslationTest.php index 8d226a3bf..b58949702 100644 --- a/web/modules/contrib/inline_entity_form/src/Tests/TranslationTest.php +++ b/web/modules/contrib/inline_entity_form/src/Tests/TranslationTest.php @@ -56,7 +56,7 @@ class TranslationTest extends InlineEntityFormTestBase { /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */ $display = $form_display_storage->load('node.ief_test_complex.default'); $component = $display->getComponent('multi'); - $component['settings']['allow_existing'] = TRUe; + $component['settings']['allow_existing'] = TRUE; $display->setComponent('multi', $component)->save(); } diff --git a/web/modules/contrib/inline_entity_form/src/TranslationHelper.php b/web/modules/contrib/inline_entity_form/src/TranslationHelper.php index 6c71e5f05..6ff8670eb 100644 --- a/web/modules/contrib/inline_entity_form/src/TranslationHelper.php +++ b/web/modules/contrib/inline_entity_form/src/TranslationHelper.php @@ -21,7 +21,7 @@ class TranslationHelper { * @return \Drupal\Core\Entity\ContentEntityInterface * The prepared entity. * - * @see \Drupal\Core\Entity\ContentEntityForm::initFormLangcodes(). + * @see \Drupal\Core\Entity\ContentEntityForm::initFormLangcodes() */ public static function prepareEntity(ContentEntityInterface $entity, FormStateInterface $form_state) { $form_langcode = $form_state->get('langcode'); @@ -34,6 +34,9 @@ class TranslationHelper { // Create a translation from the source language values. $source = $form_state->get(['content_translation', 'source']); $source_langcode = $source ? $source->getId() : $entity_langcode; + if (!$entity->hasTranslation($source_langcode)) { + $entity->addTranslation($source_langcode, $entity->toArray()); + } $source_translation = $entity->getTranslation($source_langcode); $entity->addTranslation($form_langcode, $source_translation->toArray()); $translation = $entity->getTranslation($form_langcode); @@ -97,7 +100,7 @@ class TranslationHelper { * @return bool * TRUE if translating is in progress, FALSE otherwise. * - * @see \Drupal\Core\Entity\ContentEntityForm::initFormLangcodes(). + * @see \Drupal\Core\Entity\ContentEntityForm::initFormLangcodes() */ public static function isTranslating(FormStateInterface $form_state) { $form_langcode = $form_state->get('langcode'); diff --git a/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/inline_entity_form_test.info.yml b/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/inline_entity_form_test.info.yml index cc800ec7b..699134915 100644 --- a/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/inline_entity_form_test.info.yml +++ b/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/inline_entity_form_test.info.yml @@ -10,8 +10,8 @@ dependencies: - file - image -# Information added by Drupal.org packaging script on 2016-10-30 -version: '8.x-1.0-beta1' +# Information added by Drupal.org packaging script on 2018-05-22 +version: '8.x-1.0-rc1' core: '8.x' project: 'inline_entity_form' -datestamp: 1477868362 +datestamp: 1527030788 diff --git a/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/inline_entity_form_test.routing.yml b/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/inline_entity_form_test.routing.yml index aff490ae4..9cf5a0518 100644 --- a/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/inline_entity_form_test.routing.yml +++ b/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/inline_entity_form_test.routing.yml @@ -1,16 +1,9 @@ inline_entity_form_test.form: - path: '/ief-test/{form_mode}' + path: '/ief-test/{form_mode}/{node}' defaults: _form: '\Drupal\inline_entity_form_test\IefTest' _title: 'IEF test' form_mode: 'default' - requirements: - _access: 'TRUE' -inline_entity_form_test.edit_form: - path: '/ief-edit-test/{node}/{form_mode}' - defaults: - _form: '\Drupal\inline_entity_form_test\IefEditTest' - _title: 'IEF test' - form_mode: 'default' + node: NULL requirements: _access: 'TRUE' diff --git a/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/src/IefEditTest.php b/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/src/IefEditTest.php deleted file mode 100644 index 5437e8569..000000000 --- a/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/src/IefEditTest.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php - -namespace Drupal\inline_entity_form_test; - -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\node\Entity\Node; - -/** - * Tests Inline entity form element. - */ -class IefEditTest extends FormBase { - - /** - * {@inheritdoc} - */ - public function getFormID() { - return 'ief_edit_test'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, Node $node = NULL, $form_mode = 'default') { - $form['inline_entity_form'] = [ - '#type' => 'inline_entity_form', - '#entity_type' => 'node', - '#bundle' => 'ief_test_custom', - '#default_value' => $node, - '#form_mode' => $form_mode, - ]; - $form['submit'] = [ - '#type' => 'submit', - '#value' => t('Update'), - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $entity = $form['inline_entity_form']['#entity']; - drupal_set_message(t('Created @entity_type @label.', ['@entity_type' => $entity->getEntityType()->getLabel(), '@label' => $entity->label()])); - } - -} diff --git a/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/src/IefTest.php b/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/src/IefTest.php index 2df552702..344ee7c58 100644 --- a/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/src/IefTest.php +++ b/web/modules/contrib/inline_entity_form/tests/modules/inline_entity_form_test/src/IefTest.php @@ -4,6 +4,7 @@ namespace Drupal\inline_entity_form_test; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\node\Entity\Node; /** * Tests Inline entity form element. @@ -20,7 +21,7 @@ class IefTest extends FormBase { /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state, $form_mode = 'default') { + public function buildForm(array $form, FormStateInterface $form_state, $form_mode = 'default', Node $node = NULL) { $form['inline_entity_form'] = [ '#type' => 'inline_entity_form', '#entity_type' => 'node', @@ -31,6 +32,11 @@ class IefTest extends FormBase { '#type' => 'submit', '#value' => t('Save'), ]; + if (!empty($node)) { + $form['inline_entity_form']['#default_value'] = $node; + $form['submit']['#value'] = t('Update'); + } + return $form; } diff --git a/web/modules/contrib/linkchecker/drush.services.yml b/web/modules/contrib/linkchecker/drush.services.yml index 33705ca4d..eca365ef6 100644 --- a/web/modules/contrib/linkchecker/drush.services.yml +++ b/web/modules/contrib/linkchecker/drush.services.yml @@ -1,6 +1,6 @@ services: linkcker.command: - class: Drupal\linkchecker\Commands\LinkcheckerCommands + class: Drupal\linkchecker\Commands\LinkCheckerCommands arguments: - '@config.factory' - '@logger.channel.linkchecker' diff --git a/web/modules/contrib/linkchecker/linkchecker.module b/web/modules/contrib/linkchecker/linkchecker.module index ccb6ee451..0c58da289 100644 --- a/web/modules/contrib/linkchecker/linkchecker.module +++ b/web/modules/contrib/linkchecker/linkchecker.module @@ -606,7 +606,7 @@ function _linkchecker_status_handling(&$response, $link) { && preg_match('/=|\/|,/', $response->getHeader('Link')[1]) == FALSE && !in_array($response->getHeader('Link')[1], ['#top']) && in_array($response->getHeader('Content-Type'), ['text/html', 'application/xhtml+xml', 'application/xml']) - && !preg_match('/(\s[^>]*(name|id)(\s+)?=(\s+)?["\'])(' . preg_quote($response->getHeader('Link')[1], '/') . ')(["\'][^>]*>)/i', $response->getBody()) + && !preg_match('/(\s[^>]*(name|id)(\s+)?=(\s+)?["\'])(' . preg_quote(urldecode($response->getHeader('Link')[1]), '/') . ')(["\'][^>]*>)/i', $response->getBody()) ) { // Override status code 200 with status code 404 so it can be handled with // default status code 404 logic and custom error text. diff --git a/web/modules/contrib/linkchecker/migration_templates/d6_linkchecker_settings.yml b/web/modules/contrib/linkchecker/migrations/d6_linkchecker_settings.yml similarity index 100% rename from web/modules/contrib/linkchecker/migration_templates/d6_linkchecker_settings.yml rename to web/modules/contrib/linkchecker/migrations/d6_linkchecker_settings.yml diff --git a/web/modules/contrib/linkchecker/migration_templates/d7_linkchecker_settings.yml b/web/modules/contrib/linkchecker/migrations/d7_linkchecker_settings.yml similarity index 100% rename from web/modules/contrib/linkchecker/migration_templates/d7_linkchecker_settings.yml rename to web/modules/contrib/linkchecker/migrations/d7_linkchecker_settings.yml diff --git a/web/modules/contrib/linkchecker/src/Commands/LinkcheckerCommands.php b/web/modules/contrib/linkchecker/src/Commands/LinkCheckerCommands.php similarity index 97% rename from web/modules/contrib/linkchecker/src/Commands/LinkcheckerCommands.php rename to web/modules/contrib/linkchecker/src/Commands/LinkCheckerCommands.php index 30102d2ff..6957f086d 100644 --- a/web/modules/contrib/linkchecker/src/Commands/LinkcheckerCommands.php +++ b/web/modules/contrib/linkchecker/src/Commands/LinkCheckerCommands.php @@ -10,7 +10,7 @@ use Psr\Log\LoggerInterface; /** * Drush 9 commands for Linkchecker module. */ -class LinkcheckerCommands extends DrushCommands { +class LinkCheckerCommands extends DrushCommands { use StringTranslationTrait; @@ -29,7 +29,7 @@ class LinkcheckerCommands extends DrushCommands { protected $logger; /** - * LinkcheckerCommands constructor. + * LinkCheckerCommands constructor. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config * A config factory object for retrieving configuration settings. diff --git a/web/modules/contrib/linkchecker/src/Form/LinkCheckerAdminSettingsForm.php b/web/modules/contrib/linkchecker/src/Form/LinkCheckerAdminSettingsForm.php index 10b3ec63a..de4bcf1f8 100644 --- a/web/modules/contrib/linkchecker/src/Form/LinkCheckerAdminSettingsForm.php +++ b/web/modules/contrib/linkchecker/src/Form/LinkCheckerAdminSettingsForm.php @@ -186,7 +186,7 @@ class LinkCheckerAdminSettingsForm extends ConfigFormBase { 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0' => 'Windows 10 (x64), Mozilla Firefox 47.0', ], ]; - $intervals = [86400, 172800, 259200, 604800, 1209600, 2419200, 4838400]; + $intervals = [86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 7776000]; $period = array_map([\Drupal::service('date.formatter'), 'formatInterval'], array_combine($intervals, $intervals)); $form['check']['linkchecker_check_interval'] = [ '#type' => 'select', diff --git a/web/modules/contrib/linkchecker/src/Tests/LinkCheckerLinkExtractionTest.php b/web/modules/contrib/linkchecker/src/Tests/LinkCheckerLinkExtractionTest.php index de937e001..f85dc6ebf 100644 --- a/web/modules/contrib/linkchecker/src/Tests/LinkCheckerLinkExtractionTest.php +++ b/web/modules/contrib/linkchecker/src/Tests/LinkCheckerLinkExtractionTest.php @@ -87,6 +87,8 @@ class LinkCheckerLinkExtractionTest extends WebTestBase { <a href="http://example.com/foo bar/is_valid-hack.test">URL with space</a> <a href="http://example.com/ajax.html#key1=value1&key2=value2">URL with ajax query params</a> <a href="http://example.com/test.html#test">URL with standard anchor</a> +<a href="http://example.com/test.html#test%20ABC">URL with standard anchor and space</a> +<a name="test ABC">Anchor with space</a> <!-- object tag: Embed SWF files --> <object width="150" height="116" @@ -241,6 +243,7 @@ EOT; 'http://example.com/foo bar/is_valid-hack.test', 'http://example.com/ajax.html#key1=value1&key2=value2', 'http://example.com/test.html#test', + 'http://example.com/test.html#test%20ABC', ); foreach ($urls_fqdn as $org_url => $check_url) { @@ -365,6 +368,7 @@ EOT; 'http://example.com/foo bar/is_valid-hack.test', 'http://example.com/ajax.html#key1=value1&key2=value2', 'http://example.com/test.html#test', + 'http://example.com/test.html#test%20ABC', ); foreach ($documentation_urls as $documentation_url) { diff --git a/web/modules/contrib/media_entity/LICENSE.txt b/web/modules/contrib/media_entity/LICENSE.txt deleted file mode 100644 index d159169d1..000000000 --- a/web/modules/contrib/media_entity/LICENSE.txt +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - <signature of Ty Coon>, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/web/modules/contrib/media_entity/README.md b/web/modules/contrib/media_entity/README.md deleted file mode 100644 index 318a00c11..000000000 --- a/web/modules/contrib/media_entity/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Upgrade path from Media Entity to Media (core) - -This version of Media Entity is intended **only** to be used as a bridge to -move to the new "Media" module included in Drupal core (>= 8.4.0). While the -storage of media entities is the same, some aspects of the API have changed. -Because of that, if you have an existing site using Media Entity 1.x, you need -to follow the upgrade path indicated below in order to move to Media in core. - -## Upgrade instructions -1. Backup your code and your database -2. Test that you can successfully roll-back from the backup! -3. Upgrade the codebase with: - - Core: >= **8.4.x** - - Media Entity: **8.x-2.x** - - All media entity providers: **8.x-2.x** (or use patches from #2860796: Plan for - contributed modules with Media Entity API in core). Note that the modules - Media Entity Image and Media Entity Document, if present, don't need to be - updated. Their configs will be updated by the main Media Entity updates. - - All modules that depend on or interact with Media Entity: **8.x-2.x** - - The new contrib module **Media Entity Actions**: **8.x-1.x** - - Note: If your site uses media entities with the "Generic" provider, make - sure you download to your codebase the **Media Entity Generic** module as - well. -4. Clear your caches. -5. (Optional) Check that all requirements for the upgrade are met with - `drush mecu`. - **IMPORTANT**: Please note that if you are running DB updates with Drush 9 - (between 9.0.0-alpha1 and 9.0.0-beta7), you are **strongly** encouraged to - use this command prior to running the updates. Drush 9 will not run the - requirements validation and will try to run the updates even if your site has - some of the requisites misconfigured. Executing the updates in that scenario - will likely break your site. This was fixed in Drush 9.0.0-beta8. Drush 8 - users don't need to worry about this. -6. Run the DB Updates, either by visiting `/update.php`, or using `drush updb`. -7. Double-check **Media Entity** is uninstalled, and remove it from the - codebase. Remove also **Media Entity Image** / **Document**, if present. -8. Run your automated tests, if any, or manually verify that all media-related - functionality on your site works as expected. - -**Known issues concerning the upgrade path:** -- If your existing site relies on the EXIF image metadata handling, please check - https://drupal.org/node/2927481 before proceeding with the upgrade. -- Entity Browser has a 2.x branch that has new features for media in core, for - example the widget that was formerly present in the "Media Entity Image" - module. However, if you intend to upgrade Entity Browser to the 2.x branch, you - should do that only after performing the main Media Entity upgrades. There is - currently a bug preventing the Media Entity upgrade if the Entity Browser 2.x - is present in the codebase. diff --git a/web/modules/contrib/media_entity/drush.services.yml b/web/modules/contrib/media_entity/drush.services.yml deleted file mode 100644 index e17e549bb..000000000 --- a/web/modules/contrib/media_entity/drush.services.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - media_entity.drush.commands: - class: \Drupal\media_entity\Commands\DrushCommands - arguments: ['@media_entity.cli'] - tags: - - { name: drush.command } diff --git a/web/modules/contrib/media_entity/images/icons/generic.png b/web/modules/contrib/media_entity/images/icons/generic.png deleted file mode 100644 index 46125e7f6212958869bb83934c28d871929277bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1522 zcmZvcc~BBq9LFgWj}#g<O^qzCMlst~B_jz^G4a5>YH)LNHPE#T1i_<B%P_oBOVM#$ zWL(tREJ(z}qg>Vu6OYU*B{h}2Vl6F7#cFA%cDBv;nfHD(Z$5wg-Z9?{KVJ`hT@zgZ z0HE)Q#RRCU@YB>@uU-ZGD@1iMrn%#3f#e8Ud>EAoa3PSxi90+`heZ+th+%|8%0nU& z0MIP(#JB_{3@g|p-mqgv%<chyi<WkVz0`VN5cD_eOzq6{-LM(30zGqQD?b&by-=B( zWo~)(5ejP)l~Y(e5NwT6j3mU}@rbnJOq&y&Y9eW+11op=lZ%q!;o(pKCv4IRAYX%n zpl?}2lLNlhKJ9!~9eT>0C%0R*gLhHA_<a7vv`n^@+84Ny)9rNNz+dlFcJ%NxSF>Ut zQ&ZE?r%wyqU2=Zx9UDWV(fp`F=SYgzh}GM7@4z5Xfw61(Lug#%ITEQWH6n>XqXij7 zvzK}P@|l^LnEmXd+nI2jkwSUK&LE?&sO34;lT!NUv$QnLTnYCNxvPz8I*Lr@u-Uas zdg)NQ#U}0aRWnsbDm3B;^*|z5M_FYBx4<%)Y$2fus1GdZG}HsSBi@(D4*UAfH6+vN zX;Av)#DqDgdvFS0jaU4X!)9-zj59A?dLWTpc74;);bXnIonY@{t1K&|izO$lAUc$O zFc{n_A_~tPJ5~bU8czVl+AuT1t3`En@hK@}VQ_qWo%L}=oHNz25K2S~h7|Mjq%yxf z!;3E=kF5gbOAfI(7z;A#k>-bl!~6UD)1dlulA)m?vG_s{5QoEgdwZMaY9UPea=Bct z?s(sLOnMA#E=>Ll=DrXDV)o7odw}5)l*SPO11w)E)NMZ6)4*f8mg%0=K@bvCO$J3P zgS@`GOHoFf8iMBAJRNWC@K9!FXQwmFt}<?yBN^$O2@09aH-8&@xoV07(M7x1ONwF- z!B{5?67yfY?>|F7aYB8UK_Um->J9btUc095hVn|()O526XJ3rTd9V!)Bfom{1~Ua< zx%}@#`DwN~m)F)$d)?LK^cA`FIyR5Je7$_*LkD)5MCHn21w+xh49_MfN67QY9ooQ) zSy@??rWLida7acT!qQDoSs&7+;07%KOE9+WzawvjgodhHaB^}gp~xaoobL^@8h9ym zkLoQ{d}@j}9>(x0IBrnn*1tcsV_n-P{I%xrP*@GVT5zEc3!*%KN~*IaHq&&Z#&1)q z#;DsXG6M_|Co@M#A8AL}U0q!)mci?RNl3sU^DFuJ-yJz+Zcu5!ZKf7UiF013Cq#C3 zcG=JXNUo%nQ`<eQ+P6jiI*}WNT~$e?S^ipq3fpAWJ$^#oL&ANt&7_L!yO^Tr@fZ=X zxS&o!R9?&!mN{tc_=io{7a7+rqW6|mM(u>xZ?wvD^Fo?-FD?oavw&z6ibNufj*dPy zV}bdDr{<`SJ3BiyG&CH=<(5~l7;X=bx7*=f*Mw53oS&axT3YIz)kECO(caM5*a*k& zrC}==Ae&;1y#J8A(YV=NeNu)AE<k=A4pBbO;|^ejC@m`BGi}8mS@nkxY9f5z3u*Ly zP>{iKyk=?3lP6DRXJ@5SsnbUeb$U+aEf$6F#9|lu?F|-I>DoTF=b`7Jq52#8j<zl6 zoJ^9`JX48{5y?B*zL7P-L!H=)fcg;MLb00(BC~a3bhH#hj{4xWGk<7!`09);Tv1h3 zCHVD4OH0f0a((YCt#QOKI?329`Vswr$>mEeVGElr1;GLoGl3N!7bo9_l&>htlW==b y<JhUMa%!!7`bzkcert*U2H6)0zI6EfI)HB2BD(12`W1DE13cY*F+A6wQ~w5Jak_~B diff --git a/web/modules/contrib/media_entity/media_entity.drush.inc b/web/modules/contrib/media_entity/media_entity.drush.inc deleted file mode 100644 index 15704470f..000000000 --- a/web/modules/contrib/media_entity/media_entity.drush.inc +++ /dev/null @@ -1,85 +0,0 @@ -<?php - -/** - * @file - * Drush integration for media_entity. - */ - -use Drush\Log\LogLevel; - -/** - * Implements drush_hook_COMMAND_validate(). - */ -function drush_media_entity_updatedb_validate() { - // This hook exists because when running DB updates using drush, - // hook_requirements() is not enforced (see - // https://github.com/drush-ops/drush/pull/2708 for more info on that). - // Here we just re-evaluate all the checks from hook_requirements() and abort - // the "updatedb/updb" command by returning FALSE to this hook if any of them - // is not met. - drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_FULL); - - // Normally users should remove Media Entity from the codebase after the - // upgrade. However, if they don't do so, this function will be called on - // subsequent DB upgrades, even if ME is not enabled. Don't proceed on those - // circumstances. - if (!\Drupal::moduleHandler()->moduleExists('media_entity')) { - return TRUE; - } - - $checks = \Drupal::service('media_entity.cli')->validateDbUpdateRequirements(); - if (empty($checks['errors'])) { - return TRUE; - } - else { - foreach ($checks['errors'] as $error_msg) { - // We can't use drush_log() inside this hook. - drush_print($error_msg); - } - return FALSE; - } -} - -/** - * Implements hook_drush_command(). - */ -function media_entity_drush_command() { - - $items['media-entity-check-upgrade'] = [ - 'description' => 'Check upgrade requirements for Media Entity into Media in core.', - 'aliases' => ['mecu'], - 'core' => ['8+'], - 'examples' => [ - "drush mecu" => "Checks upgrade requirements for Media Entity while upgrading to Media in core.", - ], - ]; - - return $items; -} - -/** - * Callback for drush commmand "media-entity-check-upgrade" (mecu). - */ -function drush_media_entity_check_upgrade() { - // This command is useless if the DB updates have already been run. - if (drupal_get_installed_schema_version('media_entity') >= 8004) { - drush_log(dt('Your site has already run the media_entity DB updates. If you believe this is not correct, you should consider rolling back your database to a previous backup and try again.'), LogLevel::WARNING); - return; - } - - drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_FULL); - $checks = \Drupal::service('media_entity.cli')->validateDbUpdateRequirements(); - - if (empty($checks['errors'])) { - drush_log(sprintf("\033[1;32;40m\033[1m%s\033[0m", '✓') . ' ' . dt('SUCCESS: All upgrade requirements are met and you can proceed with the DB updates.'), LogLevel::OK); - } - else { - drush_log(sprintf("\033[31;40m\033[1m%s\033[0m", '✗') . ' ' . dt('ERROR: Your site did not pass all upgrade checks. You can find more information in the error messages below.'), LogLevel::ERROR); - } - foreach ($checks['passes'] as $pass_msg) { - drush_log($pass_msg, LogLevel::OK); - } - foreach ($checks['errors'] as $error_msg) { - drush_log($error_msg, LogLevel::ERROR); - } -} diff --git a/web/modules/contrib/media_entity/media_entity.info.yml b/web/modules/contrib/media_entity/media_entity.info.yml deleted file mode 100644 index 65ea9c669..000000000 --- a/web/modules/contrib/media_entity/media_entity.info.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Media entity (2.x) -description: 'Media entity API.' -type: module -package: Media -# core: 8.x -dependencies: - - drupal:image - - drupal:user - - drupal:views - - entity:entity - - drupal:system (>=8.5) - -# Information added by Drupal.org packaging script on 2018-03-09 -version: '8.x-2.0-beta2' -core: '8.x' -project: 'media_entity' -datestamp: 1520603588 diff --git a/web/modules/contrib/media_entity/media_entity.install b/web/modules/contrib/media_entity/media_entity.install deleted file mode 100644 index 6b50c9bb4..000000000 --- a/web/modules/contrib/media_entity/media_entity.install +++ /dev/null @@ -1,566 +0,0 @@ -<?php - -/** - * @file - * Install, uninstall and update hooks for Media entity module. - */ - -use Drupal\Core\Utility\UpdateException; -use Drupal\Core\Config\Entity\Query\QueryFactory; -use Drupal\Core\Database\Database; -use Drupal\Core\Url; -use Drupal\user\Entity\Role; - -/** - * Checks if required version of the Entity API is installed. - * - * @return bool - * TRUE if dependency is met and FALSE if not. - */ -function _media_entity_check_entity_version() { - if (\Drupal::moduleHandler()->moduleExists('entity')) { - $info = system_get_info('module', 'entity'); - if (version_compare($info['version'], '8.x-1.0-alpha3') >= 0) { - return TRUE; - } - } - - return FALSE; -} - -/** - * Checks if all contrib modules depending on media_entity were updated. - * - * @return array - * An empty array if all modules that depend on media_entity were updated, or - * an array of incompatible modules otherwise. This array will be keyed by - * either "providers" or "modules", depending if the incompatible module is a - * contrib module that provides a type plugin (in which case it is expected to - * have been upgraded to its 2.x branch) or just a module that depends on - * "media_entity", respectively. - */ -function _media_entity_get_incompatible_modules() { - \Drupal::service('plugin.cache_clearer')->clearCachedDefinitions(); - $incompatible_modules = []; - // Modules that provide provider plugins need to have ben updated and be - // implementing now @MediaSource instead of @MediaType plugins. - $old_plugins = \Drupal::service('plugin.manager.media_entity.type')->getDefinitions(); - // The main media_entity module defines a "generic" type. We are directly - // handling this provider's configs in the update hook. - unset($old_plugins['generic']); - foreach ($old_plugins as $definition) { - $incompatible_modules['providers'][$definition['provider']] = $definition['provider']; - } - // None of the enabled modules in the system should at this point depend on - // media_entity anymore. - /** @var \Drupal\Core\Extension\Extension[] $module_list */ - $module_list = \Drupal::moduleHandler()->getModuleList(); - foreach (array_keys($module_list) as $module_name) { - $info = system_get_info('module', $module_name); - if (!empty($info['dependencies'])) { - foreach ($info['dependencies'] as $dependency) { - if ($dependency === 'media_entity' || $dependency === 'media_entity:media_entity') { - $incompatible_modules['modules'][$module_name] = $module_name; - } - } - } - } - - // Disregard "media_entity_document" and "media_entity_image", once we will - // uninstall them ourselves as part of the update hook. - unset($incompatible_modules['providers']['media_entity_document']); - unset($incompatible_modules['modules']['media_entity_document']); - unset($incompatible_modules['providers']['media_entity_image']); - unset($incompatible_modules['modules']['media_entity_image']); - if (empty($incompatible_modules['providers'])) { - unset($incompatible_modules['providers']); - } - if (empty($incompatible_modules['modules'])) { - unset($incompatible_modules['modules']); - } - - return $incompatible_modules; -} - -/** - * Helper function to rename config dependencies. - * - * @param string $dependency_type - * The type of the dependency, such as "module" or "config". - * @param string $dependency_id - * The name of the dependency to be updated. - * @param callable $map - * A callback to be passed to array_map() to actually perform the config name - * substitution. - */ -function _media_entity_fix_dependencies($dependency_type, $dependency_id, callable $map) { - $dependents = \Drupal::service('config.manager') - ->findConfigEntityDependents($dependency_type, [$dependency_id]); - - $key = 'dependencies.' . $dependency_type; - - foreach (array_keys($dependents) as $config) { - $config = \Drupal::configFactory()->getEditable($config); - $dependencies = $config->get($key); - if (is_array($dependencies)) { - $config->set($key, array_map($map, $dependencies))->save(); - } - } -} - -/** - * Helper function to determine the name of a source field. - * - * @return string - * The source field name. If one is already stored in configuration, it is - * returned. Otherwise, a new, unused one is generated. - */ -function _media_get_source_field_name($plugin_id) { - $base_id = 'field_media_' . $plugin_id; - $tries = 0; - $storage = \Drupal::entityTypeManager()->getStorage('field_storage_config'); - - // Iterate at least once, until no field with the generated ID is found. - do { - $id = $base_id; - // If we've tried before, increment and append the suffix. - if ($tries) { - $id .= '_' . $tries; - } - $field = $storage->load('media.' . $id); - $tries++; - } while ($field); - - return $id; -} - -/** - * Gets the names of all media bundles that use a particular type plugin. - * - * @param string $plugin_id - * The type plugin ID. - * - * @return string[] - * The media bundle IDs which use the specified plugin. - */ -function _media_entity_get_bundles_by_plugin($plugin_id) { - $types = []; - foreach (\Drupal::configFactory()->listAll('media_entity.bundle') as $name) { - if (\Drupal::config($name)->get('type') == $plugin_id) { - $types[] = explode('.', $name, 3)[2]; - } - } - return $types; -} - -/** - * Checks whether this site has image types with EXIF handling enabled. - * - * @return string[] - * The media bundle IDs which have the EXIF handling enabled, or an empty - * array if none have it so. - */ -function _media_entity_get_bundles_using_exif() { - $bundles = []; - - foreach (_media_entity_get_bundles_by_plugin('image') as $bundle_name) { - $gather_exif = \Drupal::config("media_entity.bundle.$bundle_name")->get('type_configuration.gather_exif'); - if ($gather_exif) { - $bundles[] = $bundle_name; - } - } - - return $bundles; -} - -/** - * Implements hook_requirements(). - */ -function media_entity_requirements($phase) { - $requirements = []; - if ($phase == 'update' && !_media_entity_check_entity_version()) { - $requirements['entity'] = [ - 'title' => t('Media entity'), - 'value' => t('Entity API missing'), - 'description' => t( - '<a href=":url">Entity API >= 8.x-1.0-alpha3</a> module is now a dependency and needs to be installed before running updates.', - [':url' => 'https://www.drupal.org/project/entity'] - ), - 'severity' => REQUIREMENT_ERROR, - ]; - } - - // Prevent this branch from being installed on new sites. - if ($phase == 'install') { - $requirements['media_entity_update_only'] = [ - 'title' => t('Media entity'), - 'description' => t('This branch of Media Entity is intended for site upgrades only. Please use the 1.x branch or Drupal core >= 8.4.x if you are building a new site.'), - 'severity' => REQUIREMENT_ERROR, - ]; - } - - if ($phase == 'update') { - // Here we want to ensure that a series of requirements are met before - // letting the DB updates continue. However, the batch processing of - // hook_update_N() triggers this validation again during the update process. - // Because of that, we want to make sure that these requirements checks are - // only evaluated once, and we use a state variable for that. - if (!\Drupal::state()->get('media_entity_core_upgrade_started')) { - $checks = \Drupal::service('media_entity.cli')->validateDbUpdateRequirements(); - foreach ($checks['errors'] as $key => $error_msg) { - $requirements['media_entity_upgrade_' . $key] = [ - 'title' => t('Media Entity'), - 'value' => t('Please fix the error below and try again.'), - 'description' => $error_msg, - 'severity' => REQUIREMENT_ERROR, - ]; - } - } - } - - if ($phase == 'runtime') { - if (drupal_get_installed_schema_version('media_entity') < 8201) { - $requirements['media_entity_update_status'] = [ - 'title' => t('Media Entity'), - 'value' => t('DB updates for Media Entity pending.'), - 'description' => t('After updating the Media Entity code, you need to run the <a href=":update">database update script</a> as soon as possible.', [ - ':update' => Url::fromRoute('system.db_update')->toString(), - ]), - 'severity' => REQUIREMENT_WARNING, - ]; - } - else { - $requirements['media_entity_update_status'] = [ - 'title' => t('Media Entity'), - 'value' => t('DB updates for Media Entity were run.'), - 'description' => t('The Media Entity upgrade path was executed, you can now uninstall and remove the Media Entity module from the codebase.'), - 'severity' => REQUIREMENT_OK, - ]; - } - } - - return $requirements; -} - -/** - * Implements hook_install(). - */ -function media_entity_install() { - $source = drupal_get_path('module', 'media_entity') . '/images/icons'; - $destination = \Drupal::config('media_entity.settings')->get('icon_base'); - media_entity_copy_icons($source, $destination); -} - -/** - * Remove "type" base field. - */ -function media_entity_update_8001() { - $fields = \Drupal::database()->query('DESCRIBE {media_field_data}')->fetchCol(); - if (in_array('type', $fields)) { - \Drupal::database()->update('media_field_data') - ->fields(['type' => NULL]) - ->execute(); - } - - $manager = \Drupal::entityDefinitionUpdateManager(); - if ($field = $manager->getFieldStorageDefinition('type', 'media')) { - $manager->uninstallFieldStorageDefinition($field); - } -} - -/** - * Ensure media entity status value (defaulting to 1). - */ -function media_entity_update_8002() { - // Ensure status values in 'media_field_data' table. - if (\Drupal::database()->schema()->tableExists('media_field_data')) { - \Drupal::database() - ->update('media_field_data') - ->fields(['status' => 1]) - ->condition('status', NULL, 'IS NULL') - ->execute(); - } - - // Ensure status values in 'media_field_revision' table. - if (\Drupal::database()->schema()->tableExists('media_field_revision')) { - \Drupal::database() - ->update('media_field_revision') - ->fields(['status' => 1]) - ->condition('status', NULL, 'IS NULL') - ->execute(); - } - - // Flush all caches. - drupal_flush_all_caches(); -} - -/** - * Ensure Entity API is installed. - */ -function media_entity_update_8003() { - if (!_media_entity_check_entity_version()) { - throw new UpdateException('Entity API >= 8.x-1.0-alpha3 (drupal.org/project/entity) module is now a dependency and needs to be installed before running updates.'); - } -} - -/** - * Clears the module handler's hook implementation cache. - */ -function media_entity_update_8200() { - \Drupal::moduleHandler()->resetImplementations(); - \Drupal::service('plugin.cache_clearer')->clearCachedDefinitions(); -} - -/** - * Replace Media Entity with Media. - */ -function media_entity_update_8201() { - $config_factory = \Drupal::configFactory(); - - \Drupal::state()->set('media_entity_core_upgrade_started', TRUE); - - // When Media is installed, it assumes that it needs to create media bundles - // and fields. Because this is an upgrade from Media Entity, that's not the - // case. Track existing media types and fields, so that later when we delete - // the auto-created ones, we don't throw the baby out with the bathwater. - $preexisting_media_config = []; - $prefixes = [ - 'media.type.', - 'field.field.media.', - 'field.storage.media.', - 'core.entity_form_display.media.', - 'core.entity_view_display.media.', - ]; - foreach ($prefixes as $prefix) { - foreach ($config_factory->listAll($prefix) as $name) { - $preexisting_media_config[] = $name; - } - } - - $snapshots = _media_entity_snapshot_config([ - 'core.entity_form_display.media.file.default', - 'core.entity_form_display.media.image.default', - 'core.entity_view_display.media.file.default', - 'core.entity_view_display.media.image.default', - ], TRUE); - - $install = ['media']; - // Install media_entity_generic if available. It stands to reason that this - // module will only be available if you have at least one media type that uses - // the generic plugin, since it has been split out into its own module and is - // only requested if there are media bundles that use it. - // See media_entity_requirements(). - $module_data = system_rebuild_module_data(); - if (isset($module_data['media_entity_generic'])) { - $install[] = 'media_entity_generic'; - } - // Install media_entity_actions, if not enabled yet. Actions were not included - // in Media core, so we need this module to fill that gap until generic entity - // actions are part of core, likely in 8.5.x. - if (isset($module_data['media_entity_actions'])) { - $install[] = 'media_entity_actions'; - } - - // EXIF image handling was dropped from the patch that moved ME + MEI into - // core. Enable "Media Entity Image EXIF" if needed, which fills in that gap. - $bundles_with_exif = _media_entity_get_bundles_using_exif(); - if (!empty($bundles_with_exif) && isset($module_data['media_entity_image_exif'])) { - $install[] = 'media_entity_image_exif'; - } - - \Drupal::service('module_installer')->install($install); - - foreach ($snapshots as $name => $data) { - $config_factory->getEditable($name)->setData($data)->save(TRUE); - } - unset($snapshots); - - // Fix the schema. - /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_definitions */ - $field_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('media'); - $db = Database::getConnection()->schema(); - $db->changeField('media_revision', 'revision_uid', 'revision_user', $field_definitions['revision_user']->getColumns()[$field_definitions['revision_user']->getMainPropertyName()]); - $db->changeField('media_revision', 'revision_timestamp', 'revision_created', $field_definitions['revision_created']->getColumns()[$field_definitions['revision_created']->getMainPropertyName()]); - $db->changeField('media_revision', 'revision_log', 'revision_log_message', $field_definitions['revision_log_message']->getColumns()[$field_definitions['revision_log_message']->getMainPropertyName()]); - - // Delete file/image media types automatically created by core media and - // associated fields. - $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); - foreach ($prefixes as $prefix) { - foreach ($config_factory->listAll($prefix) as $name) { - if (!in_array($name, $preexisting_media_config)) { - $config_factory->getEditable($name)->delete(); - if ($prefix === 'field.storage.media.') { - $field_name = substr($name, 20); - $storage_definition = $entity_definition_update_manager->getFieldStorageDefinition($field_name, 'media'); - $entity_definition_update_manager->uninstallFieldStorageDefinition($storage_definition); - } - } - } - } - - // Move all module dependencies on existing config entities from - // "media_entity" to "media". - _media_entity_fix_dependencies('module', 'media_entity', function ($module) { - return $module === 'media_entity' ? 'media' : $module; - }); - // Move all module dependencies on existing config entities from - // "media_entity_document" to "media". - _media_entity_fix_dependencies('module', 'media_entity_document', function ($module) { - return $module === 'media_entity_document' ? 'media' : $module; - }); - // Move all module dependencies on existing config entities from - // "media_entity_image" to "media". - _media_entity_fix_dependencies('module', 'media_entity_image', function ($module) { - return $module === 'media_entity_image' ? 'media' : $module; - }); - - // Move media_entity.settings to media.settings. Note that we don't read and - // save in bulk because the key "icon_base" moved to "icon_base_uri". - $config_factory->getEditable('media.settings') - ->set('icon_base_uri', $config_factory->get('media_entity.settings')->get('icon_base')) - ->save(); - $config_factory->getEditable('media_entity.settings')->delete(); - - // Move all bundle configs to the new plugin namespace. This means moving all - // "media_entity.bundle.*" to "media.type.*". - foreach ($config_factory->listAll('media_entity.bundle.') as $original_name) { - $search = '/^media_entity\.bundle\./'; - $replace = 'media.type.'; - - $new_name = preg_replace($search, $replace, $original_name); - $config_factory->rename($original_name, $new_name); - - $config = $config_factory->getEditable($new_name); - $source_id = $config->get('type'); - $config - ->set('source_configuration', $config->get('type_configuration')) - ->clear('type_configuration') - ->set('source', $source_id == 'document' ? 'file' : $source_id) - ->clear('type') - ->save(); - - _media_entity_fix_dependencies('config', $original_name, function ($bundle_id) use ($search, $replace) { - return preg_replace($search, $replace, $bundle_id); - }); - - /** @var \Drupal\media\MediaTypeInterface $media_type */ - $media_type = \Drupal::entityTypeManager()->getStorage('media_type') - ->load($config->get('id')); - $media_source = $media_type->getSource(); - $source_field = $media_source->getSourceFieldDefinition($media_type); - if (!$source_field) { - $source_field = $media_source->createSourceField($media_type); - $source_field->getFieldStorageDefinition()->save(); - $source_field->save(); - - $media_type - ->set('source_configuration', [ - 'source_field' => $source_field->getName(), - ]); - } - $media_type->save(); - } - // Clear the old UUID map. - \Drupal::keyValue(QueryFactory::CONFIG_LOOKUP_PREFIX . 'media_bundle')->deleteAll(); - - // Update any views that use the entity:media_bundle argument validator. - _media_entity_update_views(); - - /** @var \Drupal\user\Entity\Role $role */ - foreach (Role::loadMultiple() as $role) { - if ($role->hasPermission('administer media bundles')) { - $role - ->revokePermission('administer media bundles') - ->grantPermission('administer media types') - ->save(); - } - } - - // Disable media_entity_image, media_entity_document, and media_entity. They - // are all superseded by core Media. - if (isset($module_data['media_entity_image'])) { - \Drupal::service('module_installer')->uninstall(['media_entity_image']); - } - if (isset($module_data['media_entity_document'])) { - \Drupal::service('module_installer')->uninstall(['media_entity_document']); - } - \Drupal::service('module_installer')->uninstall(['media_entity']); -} - -/** - * Updates any views that use the entity:media_bundle argument validator. - */ -function _media_entity_update_views() { - $config_factory = \Drupal::configFactory(); - - foreach ($config_factory->listAll('views.view') as $name) { - $view = $config_factory->getEditable($name); - $changed = FALSE; - - foreach ($view->get('display') as $display_id => $display) { - $key = "display.$display_id.display_options.arguments"; - - // If there are no arguments, get() will return NULL, which is [] when - // cast to an array. - $arguments = (array) $view->get($key); - - foreach ($arguments as $id => $argument) { - if ($argument['validate']['type'] == 'entity:media_bundle') { - $view->set("$key.$id.validate.type", 'entity:media_type'); - $changed = TRUE; - } - } - } - if ($changed) { - $view->save(); - } - } -} - -/** - * Collects snapshots of config objects. - * - * @param string[] $names - * The names of the config objects to snapshot. - * @param bool $delete - * (optional) Whether to delete the original config objects. Defaults to - * FALSE. - * - * @return array - * The config data, keyed by object name. - */ -function _media_entity_snapshot_config(array $names, $delete = FALSE) { - $snapshots = []; - foreach ($names as $name) { - $config = \Drupal::configFactory()->getEditable($name); - - if (!$config->isNew()) { - $snapshots[$name] = $config->get(); - - if ($delete) { - $config->delete(); - } - } - } - return $snapshots; -} - -/** - * Implements hook_update_dependencies(). - */ -function media_entity_update_dependencies() { - $dependencies = []; - - // Ensure that system_update_8402() is aware of the media entity type, which - // is declared dynamically in hook_entity_type_build(). We need to clear the - // module handler's hook implementation cache so as to guarantee that it is - // aware of media_entity_entity_type_build(). - $dependencies['system'][8402]['media_entity'] = 8200; - - // Ensure that system_update_8501() before the media update, so that the - // new revision_default field is installed in the correct table. - $dependencies['media_entity'][8201]['system'] = 8501; - - return $dependencies; -} diff --git a/web/modules/contrib/media_entity/media_entity.module b/web/modules/contrib/media_entity/media_entity.module deleted file mode 100644 index 9012a8639..000000000 --- a/web/modules/contrib/media_entity/media_entity.module +++ /dev/null @@ -1,57 +0,0 @@ -<?php - -/** - * @file - * Provides media entities. - */ - -// This empty file needs to be here so that drush commands that automatically -// include .module files on enabled modules don't complain. - -use Drupal\Core\Config\Entity\ConfigEntityType; -use Drupal\Core\Entity\ContentEntityType; -use Drupal\media_entity\Media; -use Drupal\media_entity\MediaBundle; - -/** - * Implements hook_entity_type_build(). - */ -function media_entity_entity_type_build(array &$entity_types) { - if (!\Drupal::moduleHandler()->moduleExists('media')) { - $entity_types['media'] = new ContentEntityType([ - 'id' => 'media', - 'provider' => 'media_entity', - 'class' => Media::class, - 'base_table' => 'media', - 'data_table' => 'media_field_data', - 'revision_table' => 'media_revision', - 'revision_data_table' => 'media_field_revision', - 'translatable' => TRUE, - 'entity_keys' => [ - 'id' => 'mid', - 'revision' => 'vid', - 'bundle' => 'bundle', - 'label' => 'name', - 'langcode' => 'langcode', - 'uuid' => 'uuid', - 'published' => 'status', - ], - 'revision_metadata_keys' => [ - 'revision_user' => 'revision_user', - 'revision_created' => 'revision_created', - 'revision_log_message' => 'revision_log_message', - ], - 'bundle_entity_type' => 'media_bundle', - ]); - $entity_types['media_bundle'] = new ConfigEntityType([ - 'id' => 'media_bundle', - 'provider' => 'media_entity', - 'class' => MediaBundle::class, - 'bundle_of' => 'media', - 'entity_keys' => [ - 'id' => 'id', - 'label' => 'label', - ], - ]); - } -} diff --git a/web/modules/contrib/media_entity/media_entity.services.yml b/web/modules/contrib/media_entity/media_entity.services.yml deleted file mode 100644 index 8805aecc3..000000000 --- a/web/modules/contrib/media_entity/media_entity.services.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - plugin.manager.media_entity.type: - class: Drupal\media_entity\MediaTypeManager - parent: default_plugin_manager - media_entity.cli: - class: Drupal\media_entity\CliService - arguments: ['@module_handler'] diff --git a/web/modules/contrib/media_entity/src/Annotation/MediaType.php b/web/modules/contrib/media_entity/src/Annotation/MediaType.php deleted file mode 100644 index 65a3d700c..000000000 --- a/web/modules/contrib/media_entity/src/Annotation/MediaType.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php - -namespace Drupal\media_entity\Annotation; - -use Drupal\Component\Annotation\Plugin; - -/** - * Defines an media entity type plugin annotation object. - * - * @see hook_media_entity_type_info_alter() - * - * @Annotation - */ -class MediaType extends Plugin { - - /** - * The plugin ID. - * - * @var string - */ - public $id; - - /** - * The human-readable name of the type. - * - * @ingroup plugin_translatable - * - * @var \Drupal\Core\Annotation\Translation - */ - public $label; - - /** - * A brief description of the plugin. - * - * This will be shown when adding or configuring this display. - * - * @ingroup plugin_translatable - * - * @var \Drupal\Core\Annotation\Translation (optional) - */ - public $description = ''; - -} diff --git a/web/modules/contrib/media_entity/src/CliService.php b/web/modules/contrib/media_entity/src/CliService.php deleted file mode 100644 index ece40e997..000000000 --- a/web/modules/contrib/media_entity/src/CliService.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php - -namespace Drupal\media_entity; - -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; - -/** - * Class CliService. - * - * @internal - */ -class CliService { - - use StringTranslationTrait; - - /** - * The module handler. - * - * @var \Drupal\Core\Extension\ModuleHandlerInterface - */ - protected $moduleHandler; - - /** - * CliService constructor. - * - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - */ - public function __construct(ModuleHandlerInterface $module_handler) { - $this->moduleHandler = $module_handler; - } - - /** - * Verify if the upgrade to Media in core is possible. - * - * @return array - * An associative array with two keys: - * - errors: An array of error messages, one per requirement that failed. - * An empty array here means that the site can proceed with the upgrade - * path. - * - passes: An array of success messages, one per requirement verified. - */ - public function validateDbUpdateRequirements() { - $checks = [ - 'errors' => [], - 'passes' => [], - ]; - - module_load_install('media_entity'); - - // This update only makes sense with core >= 8.4.x. - $version = explode('.', \Drupal::VERSION); - if ($version[1] < 4) { - $checks['errors'][] = $this->t('The Media Entity 2.x upgrade path only works with Drupal core >= 8.4.x'); - } - else { - $checks['passes'][] = $this->t('Drupal core is the correct version (>= 8.4.0). [@version detected]', ['@version' => \Drupal::VERSION]); - } - - // This update can't proceed if there already is an enabled module called - // "media". - if ($this->moduleHandler->moduleExists('media')) { - $checks['errors'][] = $this->t('In order to run the Media Entity 2.x upgrade, please uninstall and remove from the codebase the contributed "Media" module.'); - } - else { - $checks['passes'][] = $this->t('The contributed "Media" module is not installed.'); - } - - // Prevent the updates from running if there is a type-provider that is - // still on the 1.x branch. - $incompatible_modules = _media_entity_get_incompatible_modules(); - if (!empty($incompatible_modules)) { - $provider_modules = !empty($incompatible_modules['providers']) ? implode(", ", $incompatible_modules['providers']) : ''; - $additional_msg_providers = !empty($provider_modules) ? ' ' . $this->t('The following modules provide source plugins and need to be upgraded: @provider_modules.', [ - '@provider_modules' => $provider_modules, - ]) : ''; - $dependent_modules = !empty($incompatible_modules['modules']) ? implode(", ", $incompatible_modules['modules']) : ''; - $additional_msg_dependent = !empty($dependent_modules) ? ' ' . $this->t('The following modules depend on media entity and need to be either upgraded or uninstalled: @dependent_modules.', [ - '@dependent_modules' => $dependent_modules, - ]) : ''; - $checks['errors'][] = $this->t('Before continuing, please make sure all modules that provide plugins for Media Entity (or depend on it) have their code updated to their respective 2.x branches. Note that you will probably need to revert to the 1.x branch of the Media Entity module if you want to uninstall existing plugin modules.') . $additional_msg_providers . $additional_msg_dependent; - } - else { - $checks['passes'][] = $this->t('All provider plugins and modules depending on media_entity are up-to-date.'); - } - - $module_data = system_rebuild_module_data(); - - // Generic media types should now live in the contrib Media Entity Generic - // module, which should be available at the codebase. - $generic_types = _media_entity_get_bundles_by_plugin('generic'); - if ($generic_types) { - if (!isset($module_data['media_entity_generic'])) { - $checks['errors'][] = $this->t('One or more of your existing media types are using the Generic source, which has been moved into a separate "Media Entity Generic" module. You need to download this module to your codebase before continuing.'); - } - else { - $checks['passes'][] = $this->t('The "Media Entity Generic" module is available.'); - } - } - - // Actions now live in the contributed media_entity_actions, until generic - // entity actions are part of Drupal core (2916740). - if (!isset($module_data['media_entity_actions']) && !file_exists(\Drupal::root() . '/core/modules/media/src/Plugin/Action/PublishMedia.php')) { - $checks['errors'][] = $this->t('Media Actions (for example, for bulk operations) have been moved into a separate "Media Entity Actions" module. You need to download this module to your codebase before continuing.'); - } - else { - $checks['passes'][] = $this->t('The "Media Entity Actions" module is available.'); - } - - // EXIF image handling was dropped from the patch that moved ME + MEI into - // core. If a site was using it, we need to ensure the contrib module - // media_entity_image_exif is present, to fill in that gap. - if (!empty(_media_entity_get_bundles_using_exif()) && !isset($module_data['media_entity_image_exif'])) { - $checks['errors'][] = $this->t('Your site uses EXIF mapping on image types, and these were not initially incorporated in Media in core. In order not to lose that functionality, you need to download the module "Media Entity Image EXIF" into your codebase before continuing.'); - } - else { - $checks['passes'][] = $this->t('Site uses EXIF handling and the "Media Entity Image EXIF" module is available.'); - } - - return $checks; - } - -} diff --git a/web/modules/contrib/media_entity/src/Commands/DrushCommands.php b/web/modules/contrib/media_entity/src/Commands/DrushCommands.php deleted file mode 100644 index bf97a851e..000000000 --- a/web/modules/contrib/media_entity/src/Commands/DrushCommands.php +++ /dev/null @@ -1,64 +0,0 @@ -<?php - -namespace Drupal\media_entity\Commands; - -use Drush\Commands\DrushCommands as DrushCommandsBase; -use Drupal\media_entity\CliService; - -/** - * Add commands for Drush 9. - */ -class DrushCommands extends DrushCommandsBase { - - /** - * The cli service. - * - * @var \Drupal\media_entity\CliService - */ - protected $cliService; - - /** - * MediaEntityCommands constructor. - * - * @param \Drupal\media_entity\CliService $cli_service - * The CLI service which allows interoperability. - */ - public function __construct(CliService $cli_service) { - $this->cliService = $cli_service; - } - - /** - * Check upgrade requirements for Media Entity into Media in core. - * - * @command media_entity:check-upgrade - * @usage drush mecu - * Checks upgrade requirements for Media Entity while upgrading to Media in - * core. - * @aliases mecu,media-entity-check-upgrade - */ - public function mediaEntityCheckUpgrade() { - drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_FULL); - $logger = $this->logger(); - // This command is useless if the DB updates have already been run. - if (drupal_get_installed_schema_version('media_entity') >= 8201) { - $logger(dt('Your site has already run the media_entity DB updates. If you believe this is not correct, you should consider rolling back your database to a previous backup and try again.')); - return; - } - - $checks = $this->cliService->validateDbUpdateRequirements(); - - if (empty($checks['errors'])) { - $logger->success(sprintf("\033[1;32;40m\033[1m%s\033[0m", '✓') . ' ' . dt('All upgrade requirements are met and you can proceed with the DB updates.')); - } - else { - $logger->error(sprintf("\033[31;40m\033[1m%s\033[0m", '✗') . ' ' . dt('Your site did not pass all upgrade checks. You can find more information in the error messages below.')); - } - foreach ($checks['passes'] as $pass_msg) { - $logger->success($pass_msg); - } - foreach ($checks['errors'] as $error_msg) { - $logger->error($error_msg); - } - } - -} diff --git a/web/modules/contrib/media_entity/src/Media.php b/web/modules/contrib/media_entity/src/Media.php deleted file mode 100644 index d6747498d..000000000 --- a/web/modules/contrib/media_entity/src/Media.php +++ /dev/null @@ -1,113 +0,0 @@ -<?php - -namespace Drupal\media_entity; - -use Drupal\Core\Entity\EditorialContentEntityBase; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\BaseFieldDefinition; - -final class Media extends EditorialContentEntityBase { - - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields = parent::baseFieldDefinitions($entity_type); - - $fields['name'] = BaseFieldDefinition::create('string') - ->setLabel(t('Name')) - ->setRequired(TRUE) - ->setTranslatable(TRUE) - ->setRevisionable(TRUE) - ->setDefaultValue('') - ->setSetting('max_length', 255) - ->setDisplayOptions('form', [ - 'type' => 'string_textfield', - 'weight' => -5, - ]) - ->setDisplayConfigurable('form', TRUE) - ->setDisplayOptions('view', [ - 'label' => 'hidden', - 'type' => 'string', - 'weight' => -5, - ]); - - $fields['thumbnail'] = BaseFieldDefinition::create('image') - ->setLabel(t('Thumbnail')) - ->setDescription(t('The thumbnail of the media item.')) - ->setRevisionable(TRUE) - ->setTranslatable(TRUE) - ->setDisplayOptions('view', [ - 'type' => 'image', - 'weight' => 5, - 'label' => 'hidden', - 'settings' => [ - 'image_style' => 'thumbnail', - ], - ]) - ->setDisplayConfigurable('view', TRUE) - ->setReadOnly(TRUE); - - $fields['uid'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Authored by')) - ->setDescription(t('The user ID of the author.')) - ->setRevisionable(TRUE) - ->setDefaultValueCallback(static::class . '::getCurrentUserId') - ->setSetting('target_type', 'user') - ->setTranslatable(TRUE) - ->setDisplayOptions('form', [ - 'type' => 'entity_reference_autocomplete', - 'weight' => 5, - 'settings' => [ - 'match_operator' => 'CONTAINS', - 'size' => '60', - 'autocomplete_type' => 'tags', - 'placeholder' => '', - ], - ]) - ->setDisplayConfigurable('form', TRUE) - ->setDisplayOptions('view', [ - 'label' => 'hidden', - 'type' => 'author', - 'weight' => 0, - ]) - ->setDisplayConfigurable('view', TRUE); - - $fields['status'] - ->setDisplayOptions('form', [ - 'type' => 'boolean_checkbox', - 'settings' => [ - 'display_label' => TRUE, - ], - 'weight' => 100, - ]) - ->setDisplayConfigurable('form', TRUE); - - $fields['created'] = BaseFieldDefinition::create('created') - ->setLabel(t('Authored on')) - ->setDescription(t('The time the media item was created.')) - ->setTranslatable(TRUE) - ->setRevisionable(TRUE) - ->setDefaultValueCallback(static::class . '::getRequestTime') - ->setDisplayOptions('form', [ - 'type' => 'datetime_timestamp', - 'weight' => 10, - ]) - ->setDisplayConfigurable('form', TRUE) - ->setDisplayOptions('view', [ - 'label' => 'hidden', - 'type' => 'timestamp', - 'weight' => 0, - ]) - ->setDisplayConfigurable('view', TRUE); - - $fields['changed'] = BaseFieldDefinition::create('changed') - ->setLabel(t('Changed')) - ->setDescription(t('The time the media item was last edited.')) - ->setTranslatable(TRUE) - ->setRevisionable(TRUE); - - return $fields; - } - -} diff --git a/web/modules/contrib/media_entity/src/MediaBundle.php b/web/modules/contrib/media_entity/src/MediaBundle.php deleted file mode 100644 index a2d11f627..000000000 --- a/web/modules/contrib/media_entity/src/MediaBundle.php +++ /dev/null @@ -1,8 +0,0 @@ -<?php - -namespace Drupal\media_entity; - -use Drupal\Core\Config\Entity\ConfigEntityBase; - -final class MediaBundle extends ConfigEntityBase { -} diff --git a/web/modules/contrib/media_entity/src/MediaTypeManager.php b/web/modules/contrib/media_entity/src/MediaTypeManager.php deleted file mode 100644 index 5ec95cc03..000000000 --- a/web/modules/contrib/media_entity/src/MediaTypeManager.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -namespace Drupal\media_entity; - -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Plugin\DefaultPluginManager; - -/** - * Manages media entity type plugins. - */ -class MediaTypeManager extends DefaultPluginManager { - - /** - * Constructs a new MediaTypeManager. - * - * @param \Traversable $namespaces - * An object that implements \Traversable which contains the root paths - * keyed by the corresponding namespace to look for plugin implementations. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend - * Cache backend instance to use. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - */ - public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { - parent::__construct('Plugin/MediaEntity/Type', $namespaces, $module_handler, 'Drupal\media_entity\MediaTypeInterface', 'Drupal\media_entity\Annotation\MediaType'); - - $this->alterInfo('media_entity_type_info'); - $this->setCacheBackend($cache_backend, 'media_entity_type_plugins'); - } - -} diff --git a/web/modules/contrib/media_entity/tests/fixtures/drupal-8.4.0-media-entity.php.gz b/web/modules/contrib/media_entity/tests/fixtures/drupal-8.4.0-media-entity.php.gz deleted file mode 100644 index a99de11977142b64d31f0117bb5910cbd94d0876..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146513 zcmV)9K*hfwiwFP^_uN?m1MI!&cIrsBApF0cf-hIEl~s3DoTGU-nVq*V7>sSeOvdn4 zFIFfN0)db~5|cagIr@S6#X3R~AV6TS!FE>q$MLZ>MZ}(B?;U&i^}k2G(civ*PyGuy zx@mW7KD2e{>RzF1J1)+d7XI7!|N5`LrT#1RFN6L;eo3cv=tB*9I0ea5o^PU*J{gWu zhU2DuO#ZgJsnNt8IUe@DkvHf|^`7aa=oeBm=%u>Y#xC@+p3)X6zlT#<cQS%jO8Lg5 zbPf29V|m}m%u*h5&5=((^8Igrn|R9y|D7R|`*${)#_E?}{`R;3fgIb$$TuB3^*>X~ z8$W(@v7cGp`TIXMp9H^osE3E}@BaY5As3U0)uCqLzyGXR4jOdGgFd$XpZ}SHt_v4` z|63~cvteRO_kK?O&)?P;$z@aDzC<qiixCbk|LkGcgx1ghT;8!AKV?rWE4W*)Hm+B9 zJo7nvhu~ote`jKQX4l60);s?Q|NGg+Q_~}B>bx)&lP)N69=Y^=?F<uH4Yh9uQ|O`| zbUV;Prg;oZENplEUidQ4{k%EcWb(p--!vS{DO=-N(C!kR!IvB}K4kFY=aFj;p}R;8 z@FI!!)(=i>b3DQHxm_aNpz2OQ$(;^`U#ByL)+A|30dzM3u=$nFVY-Xo-C2}r>v)cj zoF@e?`3YUZ(FbUJXOQ8-8(STU4TyZ~c0BBdV4nDfGSmd}#y?&q7CIfx(HF-E2W?{O z7Jd&RB{y_zz=;*DaSL@iwy=+1D<AgADI)e4dxmGW#wSoS{l)7g)Mt^D=b6XlCCN%y zzDyTB{7_g>R@y6s*3<Cu{KR=^X7}fWp}fhE%G1{lwr{K4Gnzqx;lCNro1?}N&57b^ zd&Zg*w?xARMQju55u$sasEcl#!ijtKju(L%zx$5cLAXy*75NMb<6lJvzO%CU-pX6R zDTZ^*aBt;#a+iF@?&6E8|LYwi*iAY|0rV{@#SX#xF0?%hQpW5+!yqhr_7!grf3ymI zqO9E1CaD^}4c5*f0Ivp3!pE9qHm{shi~sYe>fywe%lMMdpGZgj5}1EQfw?PD{v49? z(t_$gP(;0h@ACts)N1t;vc8XXxUQxW($%|&v(=cE)4i|}eY!9?QFt5@5C`>~4H`JC z@NAOH2MCleZ{?S_^2=NK<*oeB@Kz?Mk6#YV)3*u)$#t?ZIKD@$gwZj(1G?mC>zXk3 zhyV&4(_XT|E@KAX*F)1Io1^w-F2VDW>Eg>upo2TpF^Zl&84|@C9W#rOi&1EGgd4Q5 ze#t{H;)b93eUtmfFxwLhb$+tAGw)d*fzh!*@?qDzyu>67ga$-DzXwwsDJ{`aYVckn zj&2-FamzdJAgIWL21US^l=_lVUsCEzN_|=><wMh^X8xC;`Vv%Mg6d09eNI7jY^chY zjQWyMUoz@TMtz8kiqQQfroP0~mzeqzQ=e5#nI3Yc*j;=HsxLwHC8)jx)u$Cy3mXni z)EU7p?gW><B-WS2`jS{*66>={tT6WDOG<r7sV^z@C8a*Alp11t@+G6bWYm|8`jSzf zRYtj(tmP{z;Y&(=NvSU>^;xA<fY2jE6nX9gMbtW1c}h4y50Y!WkCGKH6AJ~_g>dE! zhewvkLm!_f8qK!zRif^lOVpvGYt$`K^s4G~hlSzRr{lzMIC*n}Y%VJTqmfpV`%2pU zO4|HN+B~c9f5+s_jUm5^z@0aT@;KloJ@qakJOZxg6XlSWN<K=feJAOaC_!K5plEjQ zq&;oV)7~v&74GCV9DV`;hdu;pYeDb%YmLSnVnTPr(Px>^u^2j8<VgpIoqSS0TFwtS z6WjlMGknFy5tj_Ry!rBuaWwhvdGIv&Zr(qcfOZ~vm;L0l@}lU270(Z{v_GH_6y4w_ z-o?wgjXt`p|65g)Gfl&rXnn#lm)T8(ZO6?=ulD^m&LK3d3sqg*hHB3lVj6$=PDLJj zj^|$tj=Rpcw-sSKM*4n0^-8}-R;9lIg2x_woY~6QkGP$!<BYb%38TJmS<vGhwWk^U zE4Sszl{nFxiWr~WqnqG>rb{~XsmWf)#Bx@roT|lv8EO`t7gVW8|M=&DrES1@-!RDv z#wd=TjiGkFV^4-18_$vs8@J4bM1x`P)OQxR0ktg}+8B5(xc<4m!m8}*&wuP$N;JCz zbo;g??IHAwSrA8;&2cO%{MaWrTfMAX=jds?L$t{eVtI|2ICS1fFQ)Bb*IyCHKmG=O z3kl=zYf9M~c|C3j82VZATHsFK^V%AyYn1*BC%!YJY)r(XV}B#lrQPGa*q6h{esDkj zxA#N&@e|qmuuF#d=@;_{%P0)<i}yqP@e`4bF8=8k_=Ba-vFeYXmb?`%s?&pB?<exC z!2kHkXbik+*%xxgAl*=}9);c`_cZO*uWB{Uu9w_)O=!q9;NQ94Prv-}$Ez0?RABuU z{e-S>4muD8&W0#XbbFC3PiHUu_=z59(%JG7nXPW9lA=SPLsbBR#PUGHk^vNmMVU1W zi8loLECJ3^j?h23A3tH6{z5(xiq4;Y(SESx0?Se8jA#^?jZExOumuB?CqrmOfHZ$l z=u%*M3(v>HZ*<n*q9qW>1aAx-ePZE_H>BkY!7~IqK9NT|kAx7R06raq*%`hXd8pFX z^9(R<2DMS+S#M2{Te=a%eCIAFw-xRYIEzX#m;+gH2vGXn1XTR=OMIRi(+mD^ahG5@ z_yEC<hsl(5!lSEq$s;ptc6&Z0OD06d$aSWsj@_SrDTPmrtV!1lKEjDh2nAp01SIlf z1=!&AB`Z*nNE9TiHN5z?NGU<XRxn@r$4?Ey-l^z*YB}YN8E)XrqjVU4fhM$$EVu|R z{`iSmejQJ7$bsSCgqr+b_%{JI%l!C>9@c24MK(e@f9A*kJ}(<-F0^U8%41nbmNgZt z7{~@TfPfeUD5@d?LSPcsbQvoQd@~Di5et0?LzJ>Js9X3x%eNkDTDAe<V7_Rz7ll&G zALRV<W4_ii#=@YvP_`ICTrtEFsK+w{`J+eFsucyD!~^V#ikAb2l$=2S@+SQsdUJ#J z31Dibk5loLy_)Xn&54IQ9{D0IckZl+A%26B?dpaYyWx>npdH?NFD4<lLGEOzc}Mbz zqRJREq6ieufP_zY7HEnf15qMhf{cV%UHWtLiSyLZ_}g}sYc|ElvOW`T++Nj08E-7I z!%X+ys4k{GoR<_zO&ffo#Q0<d*Aw;)g75W<ui+OyM)k|@?By6?lVc9QyTLPr+^;ql z_)3m>A0l7%<~@qQMM=jhF9-k%vH}Eymw+bd2uOy+a8QE+7S%sDfveX}^O3uEd@1)> zb4A7%dd^+fd}i+pPpxJt4+@2cme-tLjh!t57ghuw!Ss0ImIK9c`Ob-iO*NFIIs;An zNRr<B*0%CfBzIHP&76u&@l%tu;W&QO9O7k)Rt-kuBtrqZENVc|B@Ji@GC*b-mD6;D zRRk|;9VnaDK{)vjH4X-YLhb3Ay{~h*+iRy~k27v&gmYJb8x8Zr;=DMp9tW~pK}L6r z1^5*UESGV#cqbnT-{}~p;rCu6;Dsc$em#-HHo*%oZ;SX`aObkd0P{}#(!<v1NN$0e zuJID)fr>bh*wTyyRKvhzI}l|kYfx1+;ga%SpbE1!x)|vz1;tEfYLM~n%KU)uThm6K z?P8I04RrS;H)^-(O3QwpTy?iepIwoDIKeZBe!w3g{Fx9U$4dR3$K!-epnDTdev6=2 zaQ_72o%{~14n$$D$Z>QlsT!{_L>)I|kr=RsA`=5vML=M9K@mkxmBhCz<^h3Sf+|@) zUm4*-y>RP|*umn#HlO@v+gRjo6nUz#^Z9M&#x(AQ2EN{;N{(4k<p$KwL!R6b<VhB` zG4io=y8pd6!BR{1A#(RZeMl6Q*aCM+IE4A9Az>eSgJTJpH&ls*k_b3KQHkIc44^Oy z1ROM=!b%1tqWJR?@RM3#%U4X3Z(2>?y#tvN>Po#*a|V@}VIpXcZj@2W&bG7Hd#%P8 z%+5o;!f~o~Y}~!XEA6P(iEnMm+B(!;PpHA#qN9!_PSu!e^*tRWfdO%fM_!N_okK(# z^O6h%4I!X$7y=@U1y*5ng~$3ub#7@>=ce=jP>tK2s|8ojPUU+Y34;-|+UdF8D?GG~ zRsolbJ+=hKcTS}}yV*86R|Bt+aGKH`k<hC>@m!ML90K$umHXhU8w$4=nV~kEn%y8t z^iS`BA7alVM)k4sU)B{4@|p@bkvd*fStn*HBLEgLGU9ka6EXgCJ6`T?DurA5(Xcqt zs`sA@NFEoa_Qdpj9~5U?nHl7Te#S9|gTh`9{1T`qumGWn9wB#&l6)w2Uhjur4u7my z@z9&w^j{0Gmx{0OQ#)aT!ic`<Tlh!{XIU(0h9(0ZbA-Z0g$J6ULLf;BW)#jKiVyz0 z8jkE%Rn(h3P4yyoqm*04bZf4*`+PocJrv#cs8i(IX~FNc>f3&Cc}2Zzkex@t5sy%C z=wGX0k5$h1ytF~Ggu))E({Qz(PppoQoEdhbNT?%hNkxG~;v%518X;6h#e`606=2@r zbgZx{7W6;2DDGD(jYhM?HRi53Zu>}mMz?oYnfd6hr8sVhop3h~<5sI$$rkqdz$2KR z(F4eybjA~n-_aJQpYbHUyX_W#2%kx5i&WN~jG*=DC9;QUoY%1@ne@m~5{bTH=)~UC z6a}b)h5@f5mSI^@6wt*_{Db*kjL4rM;8k60W*c4UX{s&8HGA^dy2fLvQWVB_xAUe_ zw4HPhis`*x?&TDZrg2JRggVG=LgcQOAhg-z&U=D^gFM_*&U=#HiW4>&r?y&B0x_@d zB@(k7kuXK&`;fHnIM%Tmw5oCvQ7r_3Fr{doC>fe&uz=NNMpTF%EkN#~MyI@KbOv+( zP^+_qZ)(nXC_LrHX}QB_*<n@S)ONe#^Y>M)T`W~^>qRfOC>z^io?CH1w2JdFz)3<r zW;d=Ta@giP{BE2V$nW*oH&2u6aaQ=?LmrQeY`-Fwg;PbM)kr*Gpuhq_XLJA=odF7l zn865(hAMopnnUCG=;~fa{>|ouD_6W(H}mXUY8gNEvn&~JUbrr{y;}F7*ed85aC<XX z(XPtA1J!LFSkLV%kmWrH{Iob0O=Ju~WJG`jYHS(`3lysD7&0gGf`|>CXaCg1-Kw@E z<+eG14Kt71R<k%|?Ppx*W`|6>J$q_(N8`rmKKDFJw|2Fg4Tv61*k3^`XC&tn&g3!V zc^)B^=;jIs1j*ol#<3FMHBn&6pA4^yA9^PLHcBov#B@hchqL<|!xQJZ=VH!nD<U&f zN8N^F&5Jd)RLb$wsjw}knI$DBoyhMc<JeL6sWki-I*Shh`<kP8L6kK?LWs&*qK7lA zLR1QaC>J~uBwZjh%cBeW`QAp1H#hF0{dlY4J5z1ildF72$(4Io?ctR@;PUK^bT`jU z7QoG@>D|1E1el#exB~X;9R9WBOTcrid)nXy78wwz28)Qs0EzjiN(NAMmSGfL(FLep zSjtYkC@}B>gk323R!*qh-E`;rq_FTFEBe%Qv$vfof0Li2r)GIz>gn=rt9rc~_!NWY zIS86P&Tmb@5e|BS-#Y1~ZNGJp?z39WF8PLd_;@Es2=)^X5_J`cvMdtn6*(RVA`}2L z6a(;3Q6-F#z>9xw&G+ec+??I}?0q(4!)J6|k}8ASVdlO+5UNb40~C)bm(@8g^}D;y z>rIdzZ_|C9B3^{HPd-G{3`JCBMgttjYDBq!JW!Cx0G2nfpkNtFNcgnMMY}dae8H)~ zd!2b0Trr$I?>|0V6`%cr?$z}hJWy+99*)}`v>OGo4X~qWcYhArQSqmL?E}OJ>QM~M z>0x~at8tP<)Cf*B3?OKj=oh>!08P;}gF_5d7~|8@tFABDvUjC5=l$7rL3zAqr)E|t zNa`5f-;@UvOSqbA_3`~x-P;OF6<74S3%N58?uQ2UQ4B0~VgJ$*e(&TxUMFmyPU4~g zIi3*&Aky<gftLiJNW2UT#6pQzIV=jFS0q<(`wC}Ft772mI-kc)sqNnmuFYAsoX*zU z13%ZjYt9}_oE5f6eB~ofgxyJ+#mL2-w|UksMd|`QFN~IVUSen7HJo(`-fy%T$u|vp zQ7f1Y>Q<A88;XpeBLsA;QTfd?KruuaaJ<22vZ8Xl%D<Vo=>lJIzTLH+`@`1UWbzE` zs>L?LEtvb-ZCM{RtFznr;-OQX=A3?Ru@zGqgwqiSBpwLR8>u)6E$d1ziIF~~V-LCJ zXc?y|kWZ&m+WOVN2@6fzOTln^PeEHJzth^%-^gTMjm)S@P>9A=OO0a?sUc0~BtZp4 zThxI7H5O<L<N$A|qJkiX1`<CAk=nTJm3X$!*-WF4v_)oAelXQ6XO<Z+UB0QxIs4jE z$Hgh!@;L=2b^wDNRFdgt!x2-_Bi_=Mh-!4jsxWoJ&k=~((Xtbe%n`_>Ez6mC%P$ha zl5*^|!~HF2CFFwqZ!~b>n}M;eK{QC%ez?Ta6}%=Ingm4Z7#9R;?~?5ZC^{#r8fHWY zg%3jPbTPcKy0;5Xx|$Tke7>)O!c%`%<@IVt%rL&$7p_LlTiY4WHW5qax`aX!W}8@} z6%$hNXQ<eujjKjxi0iGugurR{FFc3Ckri;x(mHqX$O^<Xc|URUAumVP<Z|0<ITLqj zVW!A>QyRv0%A<U_k{P;5CgZQZdnVEtVb^o)sOaL$q`~VP)K!K7Sc1e@gaQvBBXK}N zqR3)JfGScxh&1|+J?;8@X3!{dxOiXB&6>uIY2juwn-N<6Q)W`IGO$-4bP7A9v4ldR zlQz1Sir|)_a7(6-)x`fSB(T5sRbYLF$O$37T=gy^8J6SdD;E|8pE)#xm5RUY)X(B2 zCQ!g>nqA;nTz((hv|^mTq>EFr-;mVo6`o9=CZ)%6h85k%61?hXAMQA#nbRC~D)?}q zw`#Sr@}~M)bWYz0pm5aC;hO<1D_OQC?U@|0Q5j5FhlMf(g2Bkdc$IX3bci`whdQCW z58@wrVAaGz^U0k(Kf;GmtDQ@G(hSX&2A9jRPcmONT)*y3C)4X)(Ha3^hkpo`bJ4vN zg<2|vTC^0Z*sM`bwOQyBQdt$f%KkGo{H}j*r9kTDhxs;w-$Gl@A6m4cIX+Tp<uzS| zf(8r@>okBF0!<PnAn8~j8Y?g8vhhK9yDnEOzfpM@c9GPma4xzPp3IJgGuO9$abhy{ zDquN%QUlvz^yITEC~PB++`Fk4?^D_6-V4YgueQJt=$Anet+js~nplD5S%DZ63a<!6 zb0IsEreP5XSY-@`Wpu?*KWclL8yRcXE>xciI+_;UYXF)9)ShSajYp?kxaMm^u2gMX zxykizbjb!Nl1fgkkhOf^CGO%QjQzV#IO~U3K0oG$WRK|`hc;Q!MNJfBz{@l}fS?d) zDu;o{vJ5mhUKFH{vLxE_-6YeP6+2D6UYgz$Q23K4zN6^C14^zvsF$R>ZfTZdaB(-3 zVhx8RS8af^;_~hb2n+Fbek>i&TLO;_MQ0QsBAM`(B8mWVJO@}=BfFZ&%253%Bo*bw z*hH;CgO_^Mg*Z}mHx2VwS7xu*$g<_dq@N=fDk|T;P8NYlY_$}D_2W2-yc%%Q`B5~( z_(<(S=Ag(yL`-m+>@Gl^@DZ;PX{hTmg9sxTlKw#`n%!lErsq6%X1VT^n|2Gr5T(<( zTH)DUU{+oXp7oh)r$+;$m5d?^g?%V$M)!`{rT*P)o*zXE4Bg{Uq)3X&8IlArHbm<E zLO{`2jTlf6@jTDaM3E0dQMr!?W@YlER<*|p*HQZAo2z?OsCHZT_+k7c&qhWm-$j*Y zcaI`Lp|B4{t?1rM`w~IXWzb_0)9M6;4<alpqM-uB(wZkSqXUg$5YR9SC8)9#jvs_4 z@o@xaMfdLE`XMuL%0g@O0GWC=SLXDtS-3Ik&*rT^vxbd1+N}(+3y+<xNr|D1K9$ms zQv%7<&aDDkPEx(qM8itwi0a4bM1{~L%2ygtWJUm@#__CVunenfAA~Qbt9Z?)-qqwr zyD!f8jALG1xBK03ZPCkJ<%URWG&{3<WR$cmud+bly{e765uCg9MAG}o_+fT=i+<#v zsa&f6&(tMMsv*|R$?!<uu_R)mP|(mYjR%6ND8!zU5x^?cWXa&5%zPAk+0XZ)&TvAh z|5SMDD0QnYO>*gpD)ZLR7z}R))@=rGrs}4*lnENV6M$d`Vd@%T;S)!r?6O9%*K^z> zo&5;Xdf*&U(KU%^f3n5^Dg^;%bfUdujVOj6b!wBgp7CPgr<H<z)twje>J8V(-nKkG zlUJ2?Us_bX?A_4P+Ozb|sZ9WZBni00SMb2m=px(KIdK&oC<Mue(edB6&BM3ggy`F{ zhrq56mrXz=!zvmlV+rs)v5y5_5`khM2#|pZB4tJqHSKKSZ0oE(Zs|Oybp!VuAuiLs zf@28U58zw)GilZI=<7f-AQ^@B(S6+^HfBo`asHH^<T{m+rCl|8m8*w)yVacD_}M3> zIB46WNycm`lj80n_5l<v&l|!Mqw4^_b+Y<Zl5G6dWnN&=xpw=QQ3w`|qeH9M=i^bL z5gsVO17Jb&iSTbf8kCblFm>n;=wjHJnSQV1O@`sI@=gV)!$GNA2jt7(2G^N+^mR3& zmgw`H<#m2}Y;O_0tLIqJIe6&h4&8`;&vf<92)eYfZET3(C_5O$v<)X|i9x8B@6wXp zbddo;B~*SmX<<vS(!Lt>b%}Z73PLjOnYvDy?5!vCd!@(nw0HUcX_5nKB8Tbe4-bj> z5L9Ams?-&VG>un?Na3L-T~u-~tK->?^XjG45SUG=vAx=OL*`3KjsiopaaftwJAF8_ z9vszq7IN2ouVbPC5=FgrSAVdiZs8tpM|kn;#ciSIkkr^bvEC3Do41Y>7|V2QIPuBW z7e`3EjNu_>`6I;ZCa@FOlHZa%yFu;jt9WuLRKzz66&jEf_I!M`Tw$5Ngj}I&EhJoL z;R7OtN%AVnEXVNzO_`?U{}mn+J6n?hhlrM<8zPcL`H~`q=Ql-0Y$0I}HTvsPzEI62 zcpwV(_!jcoxA2*5dBVlyN&G89pfmPD@v>o@??Ukq=Znz`JAA(y?`WnENSj^?`<o?F zJDK0|1Y+I&_^tFFYWzpABD)mr5kjH~Zj8;dy90zGJB+XEy42@k+PaCL?<@;k`#smu zv?xvJrI~DkgbM-P>0%qZp;~rGD$-~mIK+Qi9VJG(yDM1T+D6KS2!b|1F%v13Q>kzW zWR!~y>=HSEJGAK+*^x#TURiSMIf{EUS89F}I~nyl&Ir3Pn#q()^>m?Bixz$?gb#q> za|v>D2)n0Ba!JszAk#YMnn69LEC!GWcOXMf<wZl63|_r}B<EL>TuhSW;Yvcu5&XWA z<pfRPP0wEtf$R4sL(PU}5T(LJ5%)>LKKjaDX)i|KeL>u-OL4ye>sQJ9^@#sT0-s$9 z{Ag{*N__4Fi67!&L<4g1EF7p4kVABI5hIF|H-{yCbym3f+8e(1hClP(Ktq|o(IQpo zpqiiLIWQ25Fwy6MCNUxqbi@EnBf7pW7_zGKh9={SdJeYzi5p7^#Y`kyL!nL2Sb}E) zH$(K`VVA*L$e$doK0&-P<Gb_BIA`|@rQ2T7yq!Lb`f9&vSa(zFF1xsTEIvF=XS=-$ zl8|y}7m0<WZ#xTC%RG7;QT{~RNQB>{ZG`36cjEGswvc0m)uB!0vxwv%RmmAT{@yK4 za~umpyMuZd4K!!|(>`O4rcs6xsS_R9M0rk|IDKJ5JYvo<q4Pdd=9u%$*+hfId(ByT z^OR{)Uzo7`O5KO~Xw~Q3`o@U|EblF(-b>%0`2pdN)07QaVz4Ib5U?7n00CkSD45j& zud=+vi3W3y^w~qohOnt@tk)4_Jd8}_Puyb+(Nk>9sM5`Ax(EQ)y(%ihdSQ?osnu~g z!}-N(V=^dL*tu5C7`3}QMZeZ{JH1=faR)~)=pM^LFxw8x<@FQo*Hs7V#AAxXO2r?m zKOx%l1@VRVBMO>C2I}qyn3q35b)sj--tz(Jh?AC0s!9-|yQb(TvYB^lIqzTZg#{0# zypWjb_+Da1%*1FWK|vbi6@V2^27&|`peTX?B#z+)PF1iXD(A`9f2q*bE7;$#o_cjO z62-+mS}f%DeYy9Lzst8BtzEg*2kG2n^O3*zusJF0CMIkEYdemHEVmlFb%az9X`m6& zOd?(5ESL?*^nB4XlEiA7U=T}|lMG_X3J@p+_dt?lPUJCS1kL!XN6$F!?dYaqrBP>Q zR_=?9exZAP`;^D#cwyyliVtX5s`m3f^1&yMo?*A5XQX9@W32l7QX1_X4F!kWu}{B2 zXFbx)x}132V4}ALkqA0g6&^vL>M{a?D(XNJbrA@hqA3bAFlLOudJu^$w=g@Xf(I!L z&Af44R)+RdspiV|*tZ|}2h)7Kn%&}jzF7E_K_uKVdw4qt<dZW{l)h{ayku>RWtb1r zPc|6v)zKD`tOy8;2*^f|6)E#T;S>fi5+}(lBTKv?Ui6Iaq-cv5Pv}kx$KdybW5jJK zZd;l9wBZ>?Gm;uw7|C8&<`vEcHuUFqHZr-_f$tPhXyW<Ku(S18R10Cl<T=UBvKs>; z?Qkf!+mZrm&J@2|CL~9EY)YkLPW6sP*vjMe)-yhK=^LT~t>Zfpm#k`_9Rcx8b1qs$ zY_9Q>=S@NR%Ynrv8lNj0b&o9>qCb3uF6dlmXL0Y{DL7ijNB#%tb+3>85LLsF1csxP z7&RamEcuOQhp0SOMUE3_2j;(M>_^gG^66qf{*t@NZh*;~wv&BgESl(k>2=Z$v?X!B zcAl@D=RaWQc|&B03^nS0#Xw+q1_**60+mrjfOQ3lh*c%wA|}myMwYzZw>y6HZ%XsA z{&c0zcu7r{#oH^f_SiG5@uWG<4aK>!n6~cI1*`bUB1=}RxfQ%2CD`Qb@j`k@K_MSU z2im6vWpD{6x^cGME0X^du7wSxyPkI>7mBK;sG5KPbs_+P{0cPHP=O3Zj36`w1!@;n z0GY64?#7ye<D6hYWFh-}<A?Z_dvAW_+=46Ji4~?%Q3t&)^L*XO+<^!G_C}T(_uwj< zZKJl`ex79#%GqzD?yS5@CKr7&!9#vt%4QQ;Kj76%6TFY1mvrctNb{`7unKG7$eE@~ z94&~%1KJ9YK?0OjNx!r+?EuY(Yxz@<y--5;T(4GXUyW|HmS39m#C}E^EGl)BM(j}3 zDx*5?UN^5jXSYRE0s_xU`9Xs8@~i)0#J`T-pz%l$9esu)AXXApmI!%T1{Nzi(2#<F zp<;<8mxw-gS<)Bwk$!wJE80Ig9*6r3@i+k8do{ku-!70jdaC%Ud#x#K=VpFq<0^LY zlWI>f`>6cz<Q<B~SpngU@i?(>CU8PhEXvV#+be?jUS+v$DP=F~etWF4J)Fx>@5N9f zKrpC{c6r7<&06A!t;8cn1sF+W&yrOQsJce0CdmvZi@c~n{wp+Y#fpz2TY1Rb4Z5Z3 z^_^cEUa5r%Hyfsl*Q0^|II^DyRui@#l}5|?G@)@^5*@xKxb3ga&S$o-g3W0&w8fZM z)^YJ=DuM{HM9I`JgMgr^5`eNnPjMwfGz^JD-0@YTnbYIT5@Ks!rRMoqw~CW#rJpV9 z(;I!JNF5BgsxK?BD%bP5qEWt6uKS}=ceileF6<K8zviOLCj|*zEa)u2`7#RQcx;OX zWHm!%fy82hZHcN`8kB*-LRn&YqF^b~C9xfk_;^)3v5ZTW(U42m4cLEX*jd3}%-RFX z<sA6Xy_Y<;qsqoG`*gn@PrMEoNklNcPcfW|1*9Ao(Q;W72NuZj_?0w<(Xqe-gcP1u zX~jTgp#nse5d}s<kXMXL;&){My#~40mg^VndA{G)<vfI=Td}O#VA3_YUY;w=bLQ0V z+Khiw_I8VeuHbY4u`8}T!hU-PtR{NEl8}l;)sO^L2C~GlK;U#1C^C`&uM3POL&T{D z^J$SfwOY?Ea7Ub>mWm@#v0TB+^{sJr3{+-_91%d{zLcM3+{RAn|FwT@6Q|^})mrWI zVA8|TMLinVj^{@flV@33fPw*7Mdg4XG92x1BmvA2<~4LI5q|x&C^a5)#_hC|f4-_- zgW0HhqZwHd@{fJHr(Kty@{%>J>imT06|G&(bq8Qc3|kBCr&jkr6`Eb_B^pIZc-9px zbDAaqRi&jZS&fjpDkwluF|Qy&5inLhEuN)(rd$F22d=8=+D%$)H7k-<S29Lk8@pGn zyK*hld1?vQe7m@-Q|&@*hl|&U4j)LZdiK&@Ri-_fd?IPR&wZO%&R*4!%4sT4B#dbV zT8(Je91l3mXgov`2Sx3|IUBFXdhuBuH(hqnXx%Zxel?xXdan3XlOCpin-j3zk!{@v zqeT-7yRFAJyu&*NQT-GT>Sb~2!?|NS!$rr(w29gA3bd^2gm-0HrjQ}#k|YC7lm);^ zNY)jD#RjKe616lYC7SvxJ%A4ncYUu^d6+tdY`;8dq^H)sgIZZtmWrjmrnX1j#k1JC zYF_U$^)A$s>f6?%r&9E()F?9H57|u{-@QEY4|}B(tz=H+q0AxLkB~Nh;dP)8W(I-* zc~&%3fml(WHVaE;Gqtqaxi3AS?nKH;{en;^S?pc9K7hR;UNmx7*Y$LB0hR8q(X{RF zjU$_E=b4i{!S8_9<5^kJL{U>E9mrT9tjt3qr3sRODyuxFFq{S<d*QwL^*9|?3b~Hq zwE9E)vF5795y}dgI=sE^<VIySy~ygFtT%Y}^S$DZt+awulIqnUH&d(B*AKwzoLFhJ zgXx~WaT~A{q-8W1&3hlxki!mg*WFLW$R)GthPvyciEc3DhKu1~<d}B6lP@jEZ3d~T zt1psetpASt0#=^MrE@y=NR$Y?9xa%_+@FN4>!Q)tZJ1W0>14jEPXz@}76f7@3VX{( za9@qQIaz5LP7?cGROVncWOx=hF_)KO84S@U$e%uTWA}U58pRgZGd(h9e2kToJbEE? zsG2J#iHE-~uTy_~{CY=s&?FAmRhGo^fsIjurib=qb0MKBv3kG>x}$n=EbIbtRlLMp zHa#9s%#aF}*J!+gZp0uIs<|<s29@2>Bu*3F!t?Pk_RM-4-%9FQ&DVDvOM`L9`_3ee z+{q{`9=|0|RzpzH=*2+7E};~}PR3XuigF5`UscRe0{KSUJ?qhFGZTdxt=f7oIg&i5 zZO{y|pyw@O7c_bRK8Vps)L+{Sqc+15F3Z%Fjge26ja8wL0~srO^EB72dz5GH<zm;n zpBh(%VtQ(JyuoBx%2j5AEP8vtMheewbRQrrxrVJX#jb1WIKjiSI*%YLFl{p;$yhbf zNLX|_v}<f<6faOGQ-<E?WvNCO4FhsResvswb=ot6ZshdZ*DjQdi02~$?PceI*K9|9 zy(Om`jNbqiuRk=)Grp-zXQNh816K<GI>lP9tBvm(^;YpsAP=PBf!3k{aMhqrb7$2m zG@#s9)ah3g9_BsQXJ<LrTj*A+Au7T{yIjiDjOv2P`h&K5_k5#?Z)^J*%It5^e?b~? zhqlr=T8oi*6)Auf7?#+YGHsp48UPszYcN>gbbN_|By2oqJlr9^MV&v^D>x9-v$3_l zhT_s=>tR@CrTgh^LC7+No-`5ixk}-A?22XkzMF2;`lyj_4!G`e8tVZ6V7re^b7Sw^ znq2aMVq840LqEgv;HzZ@XPV(J%gR7YKBY6r1@rOERU00{??Z39yz`Y4?$=TEih>U< zGmu;K((Z@J#O{d%I`mOGcGjJYpmiT@%uBq7R~vS?VbdYFgds#agy(zv2OyuiBKjpJ z`WSsOK169kPmY(@=@5MK5XEIq%HB0xQB7_<as1VRJQb_q8VXT14f(#11b!)cJ+&VR zD_WJ7s0zO)CeOD{$iX}GjT+g!Q81@JmQ~BFN!N@^f0i=i!onWWDK#f<KZKXp=>dO4 zhv?yn^<-o@P>&a&+vI6_0fn1(xGPLFD=bz(nJno1?1UXTsD^A({9_P&+KU)q2<M%! z#?w0ML<leIL}3jyb}!zjd1mTsEsCCg$2Ofm&L%CX6?1~HRH3FrMPPv{a3V3@kpUDz zVgbiXvLf=TgfCJmaxE<RZDF}uz$Im+D#>QQFOMWsc4uzOl3hk=R&qGg>L8A9rF%?I znSfa*^HkiGmE_a0Nq#m-DZeZ!!_Yg*fWB)EaJ(l>Z9==VBi9@SlqrPEtxdG`H8JcQ z2$lttD8{RJNEE!dO!(_bV&Ozdyvld0FHHqLk|8&aYd%w**Qy4T|Ispg13R^zGYbaw zVwCkI$<HClkEtS*5PB#l9kU^#>}sq4FlR78U?l`pBGQ4O>71-#iPxd};f0g9Wq2ZW zm(ak&twmq3=1aBHk!A?EMI%=%E4tiA-LXA3(<;|jywP0`A+czVF>;GrxgWa=i&uDx zmrYo5)vBUV$OMtE#(z~MrL738tJrsmX&s2@C<1VuvFqaTgs_1gcgp%Mv^@((PKpS_ z{2F2b>Bce_xvg6r1TC_Gp~2=*yJ@N7x~}P}%UP6{x`xyW7IIXEX0t2Wc^K+YWuYUP zN+x!tZ0HE+3~kD-@eF_%L4ZXAa=a$9x}tw%F{O39?~<Y3vy4)Gn7-?m+*`R?m6+CO zJiQ%vD&uQBYvE2!xzjn`RvxRFvDw`2#ii_)QQDkv5?k#SJWAxRql+!=9^<T{-j3xi z_Wigkd~=tD)QE$^R<`HioW#082`nQem7>R*rc0^;L<AKeD3CUx6(Qh79kED*ih(ZM zaf;byiREhE)^3X3sc*DVbYkVZ-z~S4NXuKbb`mN1m=Z_Z5xTLjy=i}7&QHOsgQ~E^ zPh3&s$#nmJ>d5~QR-^dQ`d?t<@8SPiwpb2cUY(GwvoPm=YNQ(}-9hx<d?_%fPDfwa zR`=u$_;jKpp;gz}9;C2#e_GLBu+$$bWJ#+>4p&l&)q{_S=8O!wQ-I}27Da|82OVm4 z6m*N=Gyo+I0*H7Hi3EuPyleqy_7ID1mXbNGQVhGBrt-&j{l2}$YZCc%yNbzaj2h41 z*h--<#J~61Se4~2$*Uu~o9dcIsz2(If4KV+uSTYsi#^AhY{4kFY1+v*$=(Rbs#na- zMrWw2i=7?#vQf4CO^FcXy{s?qz7_-jW<RAE>qV2|gYjb*)&=_Gmm-t1v}tgbqLadN z%cmH>oJBzG%YQynLNNwIduS^_5;YbGk|+R;WmKY0AYEfL1{p{^TddJqLW%p8h%mT* zx8BHMB_c|Bm6M-Y^)(C~K`+R<&^Na)aeUii-JgJdSj?>-+7)xh<#un$xnlf-pH0p! zy|Zz4T95E!4U9efBrL^b7#`FKVY1PiJ64Nw`70SP*3xqGudN+pz0c7G6OOx)7r$!P zK@B%MDA5%a-lCyEov;Ae3UbQE<xYY&nJgMuX}2QFxx_?re(I6Nxul3JBXc~^Ie`ZP zR#~9(IuEcc>aw9>LWIU8PcnAZn1oE*_erEJm}4gG`+BddVeNJXwOVdgsj%g$EeUxi z|IEy5?G9dKZk4T^Fpi5S(r!+DpAEkARHCGKDcyF<`hM8_-j<-CeO_A5#7#|2m&S{w z42W>baZ}MWzJ0Lgi(9(BPSJQ0wJTa4J6e?Y{TMZCzuvU1{T$G5Uaw*uq4|`~qVycr z+g?)8KDEm9LM%@}KohdRbhMxg(X!8w_?n0w`*y(8)0L}dtK^6r?wL0QSu0#W+~-Z^ zsx@v;AM|Tm&h(54^QMTNm)!hE%6Jy@h9WTv-~~wof`DZJ4ITr72zps#1s3VT1%l4G z1F6|7as7J7DXuY|{n)wZ^||Wx{ARWOJQT5J&C0j(_^z2z3)eu%7wg#^dMQ);tfvt1 zQq0Mo`8{sQ81Q1C39i#IHtj0}UEQ>y6-B-t@}bJ~{7%Pn>}?8YdSq>S(d29=WcoFZ zg;iD4hybG_h%-_18x9npp{WWd8$i-&G8}J6K$Te*Xadjh2pR%wz)RBeW~}y`3E7*q zoD303y_Qlbi<6lv<QmnHW!Q~O!x}wY38tP?n)%1fD516Hg+2dUWp&p+@-G@raEc#k zm}7xQj7&(^pl8d1LWHuysSIE>1M7^Y$QmzSl76?E3+zVH|FK=RZ`b>O7JvL^ug`eY z_KtZN>-4y(bj6NJSLJ53RoKPEH9LFa90Dyav^aMi%)6AloZb6VGV{)qe2N#GQANej zu?CQii22Bq-xR?hq>NeFpvnc0E|H;nY@dvmNL$_{?X&u@_f#O-qHb|n3F#1}zN=&O z5MR885x041s~ppRATtGS!xQuwL)Rc?3<OlnLm*&-7U`53!0Qr1SkjPkLS2s6?FY*Y zr#CAz*H;Zk9+#VAXtrUcQ_ZN}eM>Aj*}GA%#tTzfek|P7(wX67E4lW{YjyFMU-e4^ zKb}laKUP47fqA5>K-49LSd%gf6rzp;2{DXr@FJ^<m$fDn693rkVg<WJhtKc6-)aH9 z>)zcH+_fuT*V{${5r*7%91(&)pRjzb<649gt&$)DQKgy(BH~k18J70Agd$cM5kdI_ zHIFxWqNM?NyvmtY_I~o*t(EIQmNU8TP0m8Pm<5Az)tvh`ih5n^H4c<?Jhv12T@{+r zUtE`AqXfig_F)H()v3wmjXyq;kvkLfMc?Z(V!1OS($SnR^GkzEuSv|Uuy)Z@nWogL z%<rDt>F#Z1Ca1e<L%4Ol>&9&040DXtn`(vP8~v{qKCn1poq-!WXjvx+Y<W&rCykU+ zu-y%V;aN_JP9{y{*m2Zv9BFV-A@%Pe@p<3r+`iLS!mv=<U1|sixHGW{zmh!++62R) zCGK^6JRA{}FT;pWhd2X^1Lvt_!@1b;ZwhKgeZKL)ouJ&^N_VpOaN}pyIbdw9`(}tI zW`G`-mIVby<YZ0*9MMgHpld{tW-tR}0SSyCGFar;OIBBjr8K=p>^~^FK1vtvd^o$! znquv_Jj*;k+IFcmE|>7ER=<^=Y({T(U}dv^Qj|PHp_RI+7w=aI+z0biH`BM}Wky*{ zinGgW(QDE5jY>@$4+Nkcxz5zoR|l8NvJc5gDDk%H%)SO(9Nh>kXGZ9GISwb1CH)Fh zJt9$p9~7a#rGCXj^2768C-@7QJ|_QZtF)q2bcLdx{5P>ulu5~Nj+G*FbN%JtmPvl- z7X9JVA3FKb<5FhuKOg-sME{G?|5Et>hz{w}d5oY<{xgT&l;@%pq@~G%^`?S7D@ExU zgc~lP<8EA_gdJWW+eg<9e?>5mGQTaA&KUGNVL6TUQC|F>?HCS);5suuY7<GH2)>_y z-JK`if?he<==x)IhD*nZmX|P>glFC|8)ffo)_Le5<f3ZN7)8~kH|KTu>9@>K-9JNi zDP{uc9=AAlrnmpmq`9pVFb~g-W%o?(Y?A-Pd|Z}iR9S<>1kf-?1i!)1hW89m86@!n zl0;DuKe6EN;n~CRs%x8S-^$)~3jO<eW6s`qb<MQzKu@<At32y#J}$Jj1-}|qrA-w3 z2`RUmP3Sg-zOYu--cK$lpGqzq;JQtTO3)i>X9-NV_sO@+ENOTA3kd{bi<x$Zs?o<5 zuohKDkrcowstyFkFaX4o26#ghF>9!*fZu*bysFlJ61SHMcUQhNm~|!(y;6Z0`&BM$ zR}FbsD|gcseJ)p2p>|j7KWT|--UEe1D(0HpRP)w=`YDlwHd&ru77L1RV#Jz(qXgpF z_uSdFzKw99V7!T&q<(JQf6F|Wecn1bK1OG4x5?a&8a45q(P42JK5^!Go|n;FO#7=M zKhm|2<2GJ_B9A!<aEPPaCh5fR(ivbVtRhJeX~r2&FkZ)Pca8q7Qa3y8+nUwO7Zv5Y zGwj_y0A)C=R7;I3shV>Pi52gBW{2BETB2bIjy;x@NCPG}Kz4+A#ea75iWSsPYF-_( zeAtsMpO0kOKZAgKyI9%pKI2#MzENy&=g>|@^FN?9gA1?Y^_*E}^%CkiCc>|EY~{>{ zevxnLSDlVK$))8hseV7R%}xWgwILpec|qg!d8Odb({FNYMJ=LU@FnsTy*Q{uyYeg3 zu<Z@cUvTHU2ZtjIe;b9tSez8`aL2Sm>$5{h#GFFkoMJET74Rk_GKA-Kh-NqFI1@jp zj{Z8}4ZtFsyV1raV}wD??Fycrsg0rIWh^eMOlFJgmWaKa(!HLY19(AyEsOfScj2cb zm9*0Z9?=FDnD%-VI9URamwCXkoTwVA#zRs1pk3*fvzh8B_grIu)~zk(eNS?0dUt-s zx*E{x>F#(jE6qEqBow!Ir6A_t-A#w2Lh+v7Nc~+<nE(5K$d7+S0e#po2zb{#ACx-L zb(x1y27*R?rAQHhCNX3>y1{9z#3?$fuZ+U&GAT#ByS0-=t`P*S<W5{dP_`DXwxxK* z7_ye_nla%edfqjPz8T8*Gm#lRkDUyUqchFmM5r=6V6nsk0TZhOYMKtPh9Jx8vcmG{ zgXpZKmWFowrZVQ3bgj0im(tR$J@cFH?X6jZ{W9+5v;MG=(~CRWPZahy8SW68944eq zhN-`E9HBJw;}E53x^56<kPr%IsDxT*Wl5P(3*>3AF62ZEnSGRY+`DI_G_sa$N((Pt zjM7%k<BlXYtn<90%7EuFq)mK?VQU}}=&~w9h2iycbg_ONv7wAfHqx8!{j4uK!#poq z-W?L|2Th@Pr4JGE2anE3a4d1oB)G9Qn&`#pjqY0Y<;~RJS%#1t`EiKkSVXRIssa>= zV}KwFDouT5079CC1d(B6S=~o+$Gv+-l3TUk?QM1BJuAuW%dbrcx8RbX!VzBMXb)Z1 zAab7<1fVD^k#>fva0Y`F?y`*w4wUuYi1&F*Rgc(_Ev=8f8X(Ln9#8NDcUDwNCNQ&U zjkS%k{Wj6ur~>B&UtTAT((gtrGjZY4m$~Xl>jj12I;#`n?MUiBLSJqeodid3@BMfn zlln-4kU1UcEbaRzavBg=1p`IIB2X1V8-yk#5uZyAt&qEu<r`0a%&+eph2qVRT?oVB z0xplceKFffnA12FL#*(?kTnbhUElzuPEtixB$n3{?!t#9`wH-H@bTM<e(VWVx)74N zy=<HuG~R`NkC@wdZW^nwq6?D9%T>;47)eKi9$wZJ3q!SjeE%MN2n1m!-#1zBG&Qv7 zb#11lw#!UmqMG&Mcs!Fj4Lp^A`<Nzn&*-+cU_Ajmzpavf)VkrNCDT*eWo7+NSlPbg zVJkYNd7Uj>8yBJbT<Fg;d`{8Zj+(nJ&y8{RF5B_+ChKb`SAS%$(%f_7O-qdWp&bsm z5`(C{4%BJNanxl&TgEA#v@EV%e9F|&Q=@!eYZbC881?#PVa!XmZNp--)UdMi>v8)z z&z@mf@p2qT3NumBIZaXppz?@lBO-!8WhJ7faIDUVP|{gh{~&cZjhk%kI@{IQa=nty zq?`HP^Rzgyi>>snh1wIQZ}kn_;2qD|R)@*nlwKDD+1#{ZRYDR3prQt?cLGms^rZLe zcj~q^95+RK!T^uv)l<LFvXyDdG6y(CWS;lWRNxy*xr7;rIPhq70NN=bHF7-P`}R}3 zZ_7G+JwaeDA-VKs4P*HQLD{fXbxUn%`u?&I;Optf)s1gNbdCsDY^RncP!f}rg~4eY zuS+UW8I|g_j6!r7RsfLG6;VKpA)O(s^iWOjy=5x@xKtP9$#oq|Pm@tKf0xZU?wl)* zTFj_ezMe>#=X_Vsq}%QC-9YOOw;JWL;!fu2@r{lTSCgbm-z>pQ&-jBtV)!Ey0}0DV zTV^ptyBF_nLg=cG(Ynw};7@j)5hg6L{dsov`H=mhQp{nA7k#5${k~5qt54A45}D<j ze?-JiNH5eJRz)zPY0(m%<XRX8OmLzsi(UVl(DT3PY8^XE-p)8S*{*!-{s;yrc#a}5 z>|<DTV>#a*p~@rYt8UD<=;$Va<hxFIHyimROsx}?C>fi_t(srG4kvyu2-aEF{s<^& z6(dd?Gf{Q(E%tU?AjRuQ9Vv}CO=dM&6oCeL0|*k?(iC0BKs9t8L4{Fek-wxg+E*g$ zBCSFTx})7<URH5-qWdz?o^r-*KPQi}<D3Nst@&flA2r?Xz;$*T3&w$aWJ}P)g|`k$ zpmYopi4sVS+&fXC!k7H{obqRtO!#K8!@ff7{IR+59Raa51;*8agTgBudMRY1MvN~H z#jWRS67|r9fso6DuOC7Xmw+IV-9S(+_-@(lZsBNmps@0i+YMZBvD0daX34A`URPp? zI!;Ckg1794ux)@Jfi7;n{Iy>qR=Ehq56_SK4%D29|I^#txAE;{vwJR`(2<)8lAx|h ziUAmjBb$oCKmehNfuc&946!V727l=uU(3mVn>q+;6AQYFZy`+oxLB`ZN7N^?MUBnW zZ|1z;Dz|%+iOLSV5mp{6{fcML+WqwHRJHqqmwFY4Q$4W@@pk#P<wue?n>FFodqHcG zeOt|DBa&C{Gzs}j><QaV{LQlw_$r0*NV<kB5;Y#GK%u$wf}~1>q%{s`0>dI5>5QPW zpSG)VJzuwmqgL8~n)m%yF_-S7nW^Jlb4aL)#Y|;(KQ#+!@um?S(e8BMoxI!&{fIh( z352-b2p+KO1sL8#iR{}c@eQOCwoUyuS|+u3!V3w+!zLav%eo9j2t@J|2&w{s#z7V+ zP{2qRW$Bz{a?h<ftFk}!pYvs?bPP-5xXg2{-}Q%`*1*gcZ>HB*Wgv~YJN2=%ohi(Q z!*-TEXA@dE#7mlmy==F&=<V=AY1zSO&k&(Pl5`42>rTR;$yF?-^(+(-Bsrf%5ggPQ zEb|IrL`?#MA+P{qT?DEsLr#%p#GRoF(51x5fQJp(e#&Pjy#XF@^>zVc<{>>QKh&qK z_C3^}O+3&SH)%1wOA#?xB^<m^1<Tu|hnR5LJoMh7hON&hQp4L0J|e{L_m7LKh*w+h zIPSW+tGrp#GU}JRk5Ph5?RCx!b=|o{9gVk&jozi_3>F-95Y;WTl>>BW+U9UF?93cj zUlpU|S7oWXoe-3<%BW;+JvN9(5kq3@aluGR^cNrewY(<;e+kiF;xe~|{voYe&=HKR zh9!b9_Pe?KJ)mVbDESr@*d|myj6DgX%!0_Kv7Bx<P+DGhiqe}t&hObgXlLed%yWhI z%wt-fG3iZV)~sB2>=9tPym;4d4Bm9y5Oj?`KqkRlgL<evlOSjWU4PNpiJjf(&>bu9 z5%P2=?70{Cwcj$;XaqXjZxQHrC!APUh*fx&R^wuVE|rR*0FCD`P@&B7ny9ccW<Tg` zq#-_9`M%z%8IMe5*ts@rxUkD)rxjFG@=z{47nED0<rJ%hO(`X>KAW&*WtVr}0PUc? z`nS-$Tvb)nG3|WV39lZhlwtuYyux!pVi_7S!%I}95P`yQ5Oabe371Vqf9v7EBlah= z5k}s~>yL{WUBU9~ZsBqkNVl(hY@^g_mi*FUb`v%G+{iIY1l&#_p+HeaA4nyaoZLA# zSZ^l>2f4(?l1LZ9Il0$6jWA?{SxA#rksuTGf}!yjv^^SR1Xa^CRgfg+GZTg(%{mV{ z&!iWNY{4lGvO;C_P%Ltwpj3QQa3)O9Zjy~{+s4MWZQHhO+qTV(v&kFVwr%t5_a9W9 zo37E_RQF8vgTwO`_E6aKBm9Zt-b|{?Wl8z4O?cA;e*6InJwa@Ua8LI2+ucWz$imSW z+6M(-p0YsBCQ=X~c%EHVxr9P$N#ZnqX2V76V7u}S3wa2W@#yj#?a?)NUwgg|KYHea zYMjvOi#5*<4VO^5C1qV>TJSc!Dnf?E-AA<f{l7prVhAW7s`EjivjNbF=t(vf0s_QL zVk&t_5Wya5DK4@B`I(eaFJ*K~TnO8>I&Pq$W7F0_DQG1^igzO&{RNu6`kSLqpG!pz z8^2u$E|Y}jaVE_5h?w*+&-Yo7hpl_WCKOm>IJNxb&A@;msVRO@S|Sq4vXEjm)&O@z zgFld|Y4CFL*(g>Ynf?KT>F(dnobwaP+O8SjN(|^6(nEm+qtMS{tLeI~JCp#@OFE2T ziRFFLJg6OEI~vZL=|hq2mNH{RgvMn+&I}BKl-DX!G|?oCuA#r^tPs%s_!Mz8^`h+^ z{*HA?mv`UNOl1$<;M%dpN%d=`QN1cRI#&Aw-s5BZhu#bM(XAp#-x`Y|ai|b@JCa^* zg<d30Ig(AVMlgb9{VZGxT8)sU=pl?$vZ^K`KcBMd#eIR=;<tO-=d7Q^&Qh7hJ;y$& z8Z9r=LI+b{6*j_*S|AluS1FRaz#m~GnG(Ysa<XUN_gss;)U41dYV5QIV2|9wchGX+ zB8^iZX6T7K*hNm^?@v%eSX1-aE4CXi+r2X)?L3Fuv&vM<zkyVTCmlHb)zDLdbj;x6 zb*xFSeWcbVwYoXvRPgXlj_;7T>Gg^<5$`~7L$j8}n3<~;5}?qBs0N5MB81qLC06g{ zyr`BlnSN<K&0a-2VQ(9uzo`za!fDLNt?0E;k99l#NsVKzZ538|ueZR~1pkOCZKO{z zCuQWUe!RU9g%t3WoMc-xaKvJBj7k;tJDLC#B1-<oz_Vz{i2ue^6@)H#uDQ3Y?5DBL zanaVk75k@Zl&O|!R!!pS*>B%TbXm@|Qg*-PF~WdpZ#AA3$l!I@AcFcGO6+NcrijPq zORC$=ILnPZGU$iqDbZOK{f{awsYc}qDG&+{LVHM5f2NTP_Pvl4o=R5*uq4XQqioIG ze>X^?f?-wH<B5z%uzNP#7II*ly>_vrVGAKgxeDEsgQw{dC7d{kCreBR_a-35>X@G8 zbY(zt`GpfU`AbjDqSQ_j7`oaifW!sh2!ceFbZ0qMJgCyF{?zD6aq2E7i3uf_e8Jr6 z;~e;yE&ARYxI|2eLn3N>sZ|EWxIqMazbgDX7{%s8VcLH9vUm@BpV4Fb<8UK_U9Tv} zDJW1i`Ve5MnRl$kEKs^cO1P$=Av=#5YRp%EcuW0j3hB}hM}<r7oBkoef1KU<%Pfv= zG*ywE4lc01v*NtuC2<%^?lu`U_Ul(S0#lA%ZWYfANhk(z5qZR4OWH!vG)kPBe#xBa z3uu6!mXt*W`}U}JZ+>wmixomO;!#1%sFfB1LloKUV9;Gb$JDW`--a(4T`)}Cy;r`- zFK@mtK2U+0+!Y%%u4oToHPyd{qq9-G(6fj%NTSN5US%alYG2*=yU$jKFSf=?^zV6o ze@@|R$~%;=_npf09%-Dd8!(@3yATw@)_56ZL)O`-X5-FPd0dPty-kWS^Ln*$aqqS$ z4_Udv>!Co31&uPQxNNOv90UX-6d;)-IMfzB!7cOCggW2GlQkd2n!ATIqMc;3T<v}% z6kGCrC5Rq#Xm|b-NF0TJpJF^Kk4i!il}p3~2kYce2CXtyT;Q!VrY*kGNf@y4=EI(~ zkE9l*cq$GOuSs}>pb8N}f>2(V=ra|>aR?fVNOoDoa1(?c&AZyr?>f9Mavj=-y76`T zVA69ZdWlGtxbhyoI8fvHGm7#Hwn}?ZOuCPFMZw>?-w<?(AV*(pxcE}Yo;kQsOi4z% z@vJYJ!cdx%6*dsqa||U9xSFV<veE(xo3i|q9@+553<BXdEuxG=V^jF87tCB5F?rSD zJZ+;)96F_9?W($d8t7QRyukc<S*&e$M~L5MyxN2wtG#gVZ!@k)u+*Om-xlG82=^wy z3NDCkc!Gu|+JK!gRs;qwMjRuq0#^_f14j;L2_wOdE<2EyCVF1%QE$z$<kgoA&sk=u zwi}u@6<4>^?CZ38wC>TGN|$0yu)Xx^fBtJL&P(KP5>)PHNeZTz5a<R{i;Ms28!-ge zQp<iLnF9%zn^tRVaTpfnQh*r-uac{dC5%X_E9RfYOJ&9MOl;xSM&?-BeZ7_mqWerY zAG*WcD&Nd2d%0q#pkPio(ZIzR#Dhlp0A$rI(zSgiF^4KE{#lO3-wIKGcc}VCt>og? z?|yfQRd$n=Kvo0C9lf@I()e+G(Z*xoRvhpQY-b*=RB>P3<C*MbI0_?`N65eGGyHLm z9{N<^ILkLpn5-2ll&*(l{X=`#S&kZd0^x3`>+ZZHh?YwC%Ts<TQ?Y{or5WVBIk?Ks zWu_mDMbJnd>9pXzIJzg#xb|V>%t)6fFC!$|Qp(;OTz3N&1xX`QIm48Ce^_|cm1<$W z*D8jZNzk^|Lo4X5idteHAak?|=KW=GC{^`a8<zIr{G9I!UeqlGxV7*jL>r6tLgoUu zpor?iTS^2%(Z+<pb*1tmo9IqZG0#+xY2|^NOK0%he*s!xCN5UpVB06#D|TL3>lnt5 zVNbLCs67}?FLEhH|4yJLlE^cz$TIUA>8BklRiB>|Ue{J;lY;(7!F*8ZfxaGFr&kl} z<X)#IOTFxLZPEN|PRNdHTGPNDdl>&2#=t(D6)4T~aS_30ef2^E^BgJEagNB*Bbc$; zvi7<g1jq(uUD#tQo1J^1XddatNB+-OuTqF<tE20~qG?Ui;4Ok)vj;p7DFzrE%z`tw z>M%v|)V}9{7nOIGe_hguku3en6;$xY&etC0Civ>=)HLFBa89LZwyK(uUb68aj1>EV zLh}q=as8cErw-=MCjZNIrp(Q_*SRF#os^FTM^$}}==BxVRC@{Kj$=*WnYi{pqPnan zukSva{VrZ_kg=e8Hi>dYocWneKAn6FEk4nQ+q`!-!FDRgo(;|W@)APJ$7Q_3ALhIo z?ELY4nzT@9`P*U4&26^x9-JKG%d@|pT0EQTTU}~?+dY-f6HAu5qmgz;xD!#&5x&($ zxyh%;LL9!G)YKcyuAkU3vK|I6J?7F!P7d4d%%aYdv`rQHYzchn9atYa<`Y$X=?3q( z(m3R4>3XUDC19+u5x04Cq39u(v0Q)xwg(!}jE(d6`;r%+ScvKC0j2USk5O54usyjx zvpk%r5}uuOtL2AWXWz0Rr9Tv{*e|Czg57y1qTf!bGI#oUX9p=~r4b6nM})PaRzi<P z>}VgWe^ioBQ+Umd&ZywZE!L7%5^~JxdP~oFEfADo%#jz@NPfDgh>v8}wJtV@YCjKG z$s1oyu<hj19#+n-bH>z3n>E$VSo`v`TT-fPwA?7-=4y7R%>&8WD0YY!zBsfoE~11U zwt-YEbqyfl3eFV36x5Jlm{eHKiqDfVxYc<>UAr%f6p_s0WqLPGQy#15J#KYWOm6(P z!?Ac*)We@`D9#vz1dIB3kGCZh1O8njCF0WMb_;}{r>Ite<yll|8CYY~fn8E=*!(4f z`2ZQKT|9tv<SuByns`?fU8x=nw5o(TKu1MWP8F%#S4I05LR_6}UFsA!n5!byBKMTt zUSc-eV^m6QA<l6k`+PLl;=+seaS3MTizJgTQ~cok{Dvqo++ZZXL=V<1PKiZby*pD4 zR@}h>Nv^EK3aFSW*k7ZHx%=r1wWfde3|I+h9+`sJ--In=Fe_G3Gw03QN4HC$x=6Kt zpDyY&YB4xVRjSseMAfX$PF&$^bLcIbPCnfSD#Ywg)i{!;Md2rb7JmT8LWl36j-u`{ zBpa&IT!&<$;N7%DgoJru3L=oB8VZHOr#$;Z2m>mi)k3}Y9HAq?#<#8CpU;v;t)i_- z-rZ8x#V2ZQN^IH&2He_;OCWR-X5_<7sFpiIIC4|hMBv5fz5z@Ops%5gqD%HS5)AYt z&A4#hshpHRg2!M2hXueQx~8tGvskj96}eZ`%AgD4&u<;jH}+CBMez$ua%M>kFR3oq zZd&;h>h5adW5t~7iQ&MY$W^|J=_@D+gVl3w;`;DRk25L8h@w<ruDO>s>%?fS)t#Wn zVj{Ywn>iJGl8V4$ppjo#400?{>H)>nggFedl4hC<2%g(&u{=CZcp<zD$9t!I*@|5q z{2(?svk%-LBsJ%Ovvs6wuF<EBO&g1+aD{>aFJC)5-`_j@pB+CRqu1Zttg$<hLytRK zJ7ceXFFiZ5ho9F|F*|V9@7(8Eebxy)7GHJR%Qqfz{^8rF1V0B|9WG+~oK56{WDgdg z>oiv%eSOJDLFmkf-it;sK=<+f{y)7fx@kS%q(bn0-~MY(W!qlua&r@XU)W!R3O`pX z1t~}NCV`bn2jxy8{B!<a_<EmD-mj3gr4sn&{?WXG-|w!E+PRb*i^Ac&Rd*~^FO~V< zXsbV!SdU6<o|^^n<&fncyhQ;hRar(a8}&QlhX;G+eGti-$aX&}uwc3~J{EfoN4Ap- z64sR!xDV6}LBmKo9li#s;b&fg3yw3RF|jgr9~!2^SiXGr&t}G>Te`e?LmaNWHv!@a zm;`KXY*q=RtYVO4Fi3R(R#>W)`s(En(;*|WE&u?Dfp*57o}|ofYqWG;AVG}Rn9nf0 zHH_(^39F1X%(FhRU$bAyp2lrxoNXGI+iC+(8x)sVRZRuSAj~kbtQu3m5Y%eq7Mz4+ zOJPkFcB)C*LL+$bqV%*444SQ!)w&0x?lMH+?h_*~JoY1@nHKI;Qx3z<wO(VKIxnvm z61J~f@&|EQe*QR;<(cMv{^c~De?T+z=x0oT#K=(Oby01}7XTuxxby2=TYH6WJ>2{p zPp{~SHbfxBWl4D)E`*lESHI^v15hp6vHG-86E(4{F(Wwx=10mfY<_nIo{RYW3fe{+ zTp%!O*N>&e5T^!%-)+_^))BAUnm*m8)t1)bYlGo@B~&;%aN<?haY`-ZoiMSDX<0kR zyJ6MmN+%w??6+o!7z!I|t|6qdU||7&cb^c09~KHnQQr1#YeBqnH!Ai+=VM-ssv(lR z*oDSMqx%d2N#kFOP;JCbLH?c)y|Q&;EP4wt{*xGaic)=Dx4-P$q@EELNgS)B?b@ne zXQk^%18ckSW~Kp?(NNILwdn!KlB?S}DZ?267!V)qtD3@7q&jwP__rlQI@!`+NzSY& zc;zkEPA+Y1a#sH%V7d^({m!nOh;Gw^i^_5Re0_rlJFIdZcbiBl&_j0%*E1I>l;JKC zrENu}r65q;jx}heFVr4<k57fJi7(9W>kZGm`!T+eO+FB7a+xEz(XS`2yVRYiY61=Q zRM2c?RTeI5Vo_F7JC1E*gxDn{K}!ZfDyr~dx%hg7?!4*KlBz#^gKX!<MT$r)Ika7( z0w>RpBj7)9--atwx33%COaDW{{_YAn{v{I+BDH}wM{&o@Kawh%%rZ;}E3xqm1a3H* z!~^NW?GS}C4=ctH%m82){Qj<duzJS)e$@7SZt}4G!tQxB^>BPXz3CfVemD_r&xw<6 zg@k&4p!-$z)F6{UbJ~hcMdD)dg!Bp7W|uotKA{=ELe&3TFVX~#rRS7<J(NsoVourO zW#HiqH})Z9FG~-054ZSA7fK<BxNR4n29BR$ISm5Y0BGXKBgR+WRrl~JM~>eoAl(wR z=WXw3rw90T<U#$2Hg9OoGiz--TK*us{|#ZLw;Nj&<Hnu&m7#3n4eAX6eP7C&Qw7|! zzHE<=XrHfm|C@J4{#QQGVCm{Egn&_LTe@au_8L%GV}6NZ{=8!!@gZO7As_J(U+EDa z$#Jj1ly79thfsxN{!rE<+UN;iqR>RY)9MKkg)i5UujBMbr{qnu=FQGTO@5yKmzyWN z{t$m?x%>{_INv*lKKboZI1h-v8RIt(_UFCZ{hF`kOJ>kq-^g*vd9diafVTk4As;i= z^G%QVi#H>|{ON}kNEH#Qn9xnJZKJ`$?mW>-M#tqfP`zdTkm<o1S!?%gx}1Y|i5lDW zZ+HuhC#NFs%=Dc-f{wDo2F&Tz_0^@9Q`lYRLIzaIdh*IQSUe)Q@<N!vzL>ULGkD$4 zXJ<x1=M@0K*<G$?PXw!gR3Uu+^;yopmU^zVBL{`*MT+{PWjn-BP5sKeQE|C2+!Amr zZ|7XQgE>K?J5q9S72UU$vw>Vh`f1BZ)_@#kfXhMkcFXD9{icW5s6}E)2xZS_Z}?zm z@{KnTf8-gwJk)8TL22?fRqhz9jKn8DTW9Q?ws48dE!!*xJww<U*#2Y#J2vvvC&)CB zo3ZLOWFbF>1)I2sm|<P6aXJc!2JJDRrO4LI@+{d4Sj{;|69t_M785NAPj`y%8$e79 z`leGtulfPVrbT~)Eu<y%1Y(gsIuPtFTBbO&JO&p);m$EIhSg(ASn&56AX@vC+$~P+ zJ*&ND!6mrx?Z9HfWc}vD)$AZUfS)xC#ug=W=7lUiM^PeY<XFQ_rccvaz2^weJ9X$7 zW6)T^Zkyiflx-b)>;O-T4X1vu3Nsj55a#xEJkQ@gXJy#-opc$UcMF`U-u{{!j<G<M zBuZbF?ty%!wy%9!@{(cmPdXf2UCPvLtwmew2Mp38-!KLWh?-)w1Du|JY&Q*Fh;Mf1 zfEDr%{((I)qUcF?Zj7YBWY0}Au684YFQ<w;Xt#6b!YYiPGze48#uwCPh}6$?34;f= zbrI3`K>HgZD<a9}3vG>anSf2=pr6T!{wJs_h#bR+1P`B<ZuU-h4M0ZdJ>--$H?z+y zEN<{kjZDlA31>>8pfr5;hMi|cG&CTUz6R0GQHic3ENk+pTM^0M4#m1hk-x|wrGjeQ zd)rds$^JZ>X2aP#f^%OrCg3j$BdNi#Hs=9Yz_{&JPYF7e<ZkRAM!BfX*4T8L=#ERM zTej3&3&y!p;%a=A?UdG<?uA4EqM(^mgY-mpF7eo&`-R%H)&GZno8BaU7f!EqT0a1c zuaPmbnUpP~87}-~l`L{w;3K#O<_v#Z{q;_L{vbqA87zJCD>YSdu}-`F2d<wYcW^s= z>Z!p&7wm7QgEQPpU-O*P`;*ghjs8)m4PBr3MHpHd6q<|$BcHy()u(9owEnJZR^K)h zpWK<Cl`i~)v&J2!CGhx}&)k4f%Z#m$UTz_VH^Rv&W+sB5^D*)#r_c9fIg6_q*GQvy z#N`<g(KXnl<Exm522&<f`nm&#_XF9~gZ6~Oek|0AF7sWxuUwm;@%6IORi`>zmS_5m z5BC^(wFLT~_;@*<T(1FHHgcVEz-KL?wxxRWewEi+`_Y&zIU<aIeF*zeC7^Gb@dk|d z*|Vn6K&jMU8GEV9<oTOarAbip1HG1fqx{I^+SXb~bK2qai%Xh{O+;Uy@E(f^2u^zc z84vLe8Us*EQp}5P5&0f$d_N(@IV6YMK1xsh;k>YZ`P=>p1VWfWs?J%IB`;qEL)fWr zN_s%H45pt_uRKifGL9O@6`wV((wdbj_%Z6WwT)4zv%as}TWwXpEZ@)XYwcqV-e2~3 zVt*pt$b$EC$@lg7>eTV>VFu&+qIx1XMxS(~7E`v|Q(8knrs^54Ve{__m@fA8`2ZfU zb6}BmIsi&(@Sja=c0Ry4|Dt?ofc)uUL#(Xtqt)XZtk-FJf9Ho!PA}K@T}LE&g~8yn z!pG6f)AIP+>3M&M-_PSqy5ob#(kSTWE{MZKy|G(*UfzhjiaIXRhH!xU{%4ngZ}$?G z{qbh!h2m?B%Ka;OA)+h{*)btpx@e9h6im*`IDw?cbnb(HRZ}`j=<5UO8zilKM4~iH zhM|=}Ae_J#><W`67s2rEM&piXpPe<E&=L`%!k{-#)?8iWMyzQIHYKEkL@^n&a7=xH zQ)zfs!ePQirbQOU_iN{F2!mmV&rhI+^l>;sEQM&8;>k*a1P4)Dh<FCk5%hQ3A65O` zlQl$JI-dcOufFhA;4>622z8$eU3hU47S?90tPDfJP6_^q&Xi<%O*Q$Ps%?-U$DwA; zJL7S$yHMq2y2=?J(}vG#O@33R3fB}TZI_J1CRPhjhdCjP-mOgfQS;IM{yv=%1$$A7 zb_33=HQyv)EwX4-dGQ^5Icld0b<_(V5j;srLWVf=u?W~<uHX&giKvH=_Rg6a_jC<c z(v@5FTa$)YM=l(Sp7Q7@BZ0u9bvcoTt!0+rnPYauT1~O0b1#!wCCDRkkprVf6Zo0N zEFOY}0#L1@wpfn{tyMW5QVo5n1yyYF$AR^N0VXmMF?AK$AFZ*_NhBf^@`2HpOyU^+ zI9bs%39EE@<1NQJUkvF>mhv=)-zq#xJR1m@WV2-Xw{G~ml6Bcz5g@;00?ScMV>d8V z<lQ4B+>_6Mu43O%qxETxTT(wiRlB99A9r^sK#7~lgiZeBjQZ?ZOzwe93}-X736L^g zZV$R<aHh5*rb|yoc@xnOKAANmB|nV83`{3DWoqnrb=&_duo<Xf1N~C=f{sVWjKQmA zK(|Q2YFQNso0TGvDjY}>iN<y|<uG5q$#%UIBI5ibWjP7+;1lR%+6>w^@=GEH!%DIv z`Trga4C4zgcq?%gXH{YSz6uk+G3tID&BA0a3S)Qjt-cYw24(IGsNVi^Rdc7)x`GXT zQA1g0bY<iNS)V854OvI=Hjrif4in5v0pFa;PfY~^%$`yfl7i56wclg<t<VKRHkThb zQoY~s!KQTgjo?ojmD@{PtApFF4c&ps2Xu7yNpS9}60>TF+{QoVH&YX56f?eWuLt*2 z_56I&{NCoxpL6jO@%hPfyAw0f82Mos1P|dSgS+nb6vh%Ln3dM5sRIuSn~jc3cdPfs zm|B(DK-IU&1FgxVK%Oqbh5ig9d&wgV#qxb$&!2$m^0N>5p?@8VJq!;2tKRp=_YVE< zdwK+Y?><@RR#+hv&`M2?6BS<p=sD+1)7nF%0vm_vU~gs)0t9I@oGmmCagFXV7bn&{ z`mSM{PDv!R!V_)9J@K@BVch3q&NLKJM<Wcq98Y$%<~Q%#qqT?hw;6sPAB*xC{@kPJ z=v_P+{>fwhAH;lwO^Tn_vjb74kGF}(CPk#)EO6T}n!Yvb+O-<mtXln+g5LxXq^#I! zWc8hDPBD0!2CmV22}%QhGfpnyk11D&1c7Z60|bPys_qB);0H$n3-&2|u;IdD>v%6a z4CWA4<61Hhn06Ne%Gj>L>(WjSb7>n=;2!D>mi-h!8zgEuB84Xq9FALXz}2qb2xmTN z<)gBVpx<VmnP29jn|Ffa_xQJe+}@2le10sRAGBo*`}{oK&+l&@+!^$zkMHWa$_S@L zga6K2$PGa}iX6L;H+fJH13IX4Mm4~WnyL)9LV<kQPS;v=B(A_~Q?P!cZruYMhV?5= z?5<CT1+u8m$_jAJyo=V|`cwyQ`AG2-A#DQ1U6V_qXuw|usTfW4N@?1DyFi;@2#VH{ z1`4qttGiQ=*FC}A(P^^PSde%WNGYj~2YwU`wP={ekRqtz1P(4HsU6A7Xzd|5j`qt7 zi-;_6l347afuo6^SioXnEL0(9LfS}*O(ZWJdE!7`_i#MgUN=_f3E{?rm6iLLUy(6c zU;DRrd%SJ>y-)CO^ZUPS6f-2={A6+N9i(IEJ#GxupILtHjih6kADDM78B*Y%qmfE? zyLk-q7-bIE_ILaRY1D?lX$PKot^KM!8ksA=&65X`e`!3+p$#{xLx3l&(m7?ZkAql- z<D=ipx!uS@7W{^)*N!W*ZNZfnIjm+P90a^m&sQWg^878~83fZgpCLzRNAM<GRe3EU zcOh+OP$#y_PB5PAjul}z;=rJq@2;TUhk9U_d&6Pbe8$Jr4XSox#P%B)5|*H-qT4$- zG#Uh&v#=DI0+Oi%HyV37qa0ajFZ{uW;t(G6jttfXx2XQz26Zb|(tG9e8u^w?!0S8X zBsGq;duC!@FQ?DA-s7-Jz<<Y2uj3c`<G6Q6^BE75ufON71NsU5b3lBkK1%R<YxvV^ zM1ekST^CoMkDt%y^;s4u5;#Yp*XtvFV<65Y>tfg^x6|$We!FJ;9NmxApl=>e(Chnt z52jz$6qd-v>-~P}nCj790Pn4}^?kRoP(Cy5kt;O7#z(KH|C&mW*W>g0vT~yk?jbLE zi*R$cK(N!Zr!PjzM?tmFQXY@r<LTozj2^&@@OT19Z(+bMlz(2SX8&BLU^Msd;r9io zBgdHgkjdxw8NKkA@PzH&@bgrO<h-E*_7e%r?RUC;?nq8Fjh{yjPA+S6h+q#iJ%+!r z<vi@>7ck6ip7@&l^_%^rpKsRG9AWJLX7hM{vUKr0rEiafo7?B{Mia>6!#A@)OozV& z_n)o3`g(ye@bzr}!XQ1v(kKz&RwS`_=K@74scHwZ%ooKb6hzsN$z>dK>~zxOu*3x7 zzofNT&;AAD4GQfFFK-bxRv?8-!HO@3j;JuG0j9ictD=l~Klz16Xtwit$kY4k!{zgS z?7rcd>ht}S?91e##zW`#_W5C9^7B3y6d$$I2b<4hTf{x5sV=J3&3eu~fLKBdP1nF& zUW}9u*H`X=nLIQERlEwuVNZ^ctQ%_<Q?4`r5O!Fr)E0Fn=#`*jmY<^n<>j&rxi!*_ z=0~~&X`u>G#nj^^Fe(oL1RL-*RC>U-!-_Bxtf8<**`Wy8X~Ca;E-LZ*$CffDl*Vbe zr21?^wfBe!kzuJ`B6%S$<wJ7(rs(+Rm*@B3QN^WV9U!mfrUOMJ$jMhgu_grcJ6Cw$ zy3#Fbs!#rbD3dH0*^X5a^aY&Z6LKUfx<up&Ve8eW3W{|^6*#k@i|Z)dwE%=)k)M7p z@78wSv-77KC7)7gN**-il`^CJGciJ*2W}8&7E#QvValwS27gMdRNF6q%7Rv(Ai{#h zz5-itCGI5wG(u`066!^!zCA;hfOG~<LZ-IFX1BUv@*KgSSuOL<mE`4axQ>7@mH!UK zCaaOzV7n=7m1jApD4|*oG_+VMrEY*2$y6pfD8_YkXvt9T3M(d@OP8=8wNC|lt<k0A ziP%D`BDv*=``4J*BkImYp|>h8(d1+bdlm595q$3r=kp6M#uv_Nw?@c!DsUuGiSFH| zs=VZd)e|Fk>-8A3O1|dfxWFKo_ZWUBdQ#e&(2&fg88w5~tN-^w-|P>EF$HAb<-33J zPhx-)G)y3s94Jy%i)?_okYVwX>^SPO!&89d%z8T3lM=Fz&vQcMb;=Gr^>RFk`US5T zNzZunUJX|hd1{M<Q!~Us894?=aA~IvYkrKQns${E7wNn+M?uWvI_uZ|8?KCl8LwbS zX%U2o4b2JNZzLh*(g;Q}FLiFg6&5YD$(i)jwmk>aI9}HFAOk)^^9Hlpb^J|_t#y7< zVv=8qXa-ovHiCO6u+t^`pdOEhJXi|8^}j_=?MF`#>Iff42zson<g#)vboC$3<yC)H z4~Sh+0W*y1U9`K5kvwTh#3eN;7BU*eN6Hl?%g**#KUI{lw_D>x6e7=CCIz%WT*K$2 z)49U;l$dbNs_ALZuxE%_NUp7Fh=;79DdtdgMIn=>mS$EcdoXXcu~bsN>692Uoz923 z+%{{89w3s)V6KY=7`FI(bW>p1DqOj7gc&w}C_?M13>1N~zXz!lS(4x=L_I$QHsvze zH(w&Vp}YMc-v|GF71GVi8T@?Q-w%v~Vp|4BuuvvudD8}ZzZ<PRMRVbG{tSHX_n2O2 z28&dZSpR}7R*np`Jr7j>-N=FQX-O+{m;paKKY3J%a#hXN$1?m(aaYX4s>@P*HzEOy z5tx0!gP0YqvLQmpz^tL&X|wN4_&}|ZMl=4`><{*vK{J&$6I4n%@LY@>idPj0A`bZV z*@+Odu?UXwXLFH3JY}|yO0pfoxxL!4QaLs=M3C`7-#Ef5z4mWT2j?(CTV;AdLlL3e zQCgaqrp3v`N{K#6Ixs$Lo2`63c!{vocJikc@PV&&m{?^`OYzLBU|@miN;-{l=lMC_ z-CxV6X4Ecv5qAj!rgMljQQaRwoQk)nd!+d^hC@7=J*oE}Hi%0L)ip;eoA$wr!k0-v zDP6y-1$5Bb6MDMq1fY(Bqimtt?@|=+21`3Z!QVZFT1`lm7r)ulehbqC8udFU@)d-c zAG^b>{YsL)%>uY-UH<)((<e2+yYiRB3=fY(>q5-6ey9_vLCXDVI5xmn#CY9dT^8qi zIt{KZ#8Gq;`3eez@xV)rx)-Z5BYkdN>(+IB3RFDB366%S)Y<`pHAkd<X+UE3Meh!v zJRwe9G6Rm|xUm-Thk=cF7k8ZP*4}UH<zS-l1Qz9*+sj2~*K-y|ndT{Y^d7+2W&}%( z877C#-I0sV$S086H%=0Krg8qF=fiw(zcP0FxxdCRz2NdH?D)jhBcrchX7@Y|oyY6P z;q$vsE94pCL-ei?%eBNsyz(s^v^RXhi3x8ZVrSbT<6cTB2|#Y(T?99W>w7Zhkd)kT z!z4;N5pt7;<7L-yp>zJKg%Jl?IK#_P(erxfybylS_Uh2tZq)YtU~>*wOHL-K3Rc|9 zp&Op|i{pJwf8G<;Ix4n$V!$GpntHPcqVr*4`0iZ(6<Gf9Ujgx|53vp+0pa}u5r<Jy z54n4(d48#3W{DV&9`N4mS;t}tvv%S^Xhu*6Q63GjQcQZvZw04RN=F_kTGApXhcKE` z`azA*XYAeu0Xaz}CixuDsQ}-d)9palQoyNKWWVK0bto|w#7p9VSj`iEVTX^whMO5m z+?U|s_yxyYGGRk*C4)~zq&=-5o@p|Hr?hlyZ;vq)x>}YV1aq9QrE(^?qxvyS@u+2N zhzhl}myc4W-Uh`5O@L%$UqvRj1+GVG88?uInmNansmM|Zna`2%B24EjE@Q+S`b$*x z35y6CEha;70j|S6K?i$wzJm(kORM3aR_~PQH6X5h?=bi_1!O4xz<B|gu7lLtkj{1b z^0E~tkWd459P8a4Z7*$>0sgmwkZakk(Pa2R{}C8h*{Jo>oM;%KI%vkkH*hxU=hrL; zv5?au24LpC)k%}3jxHe#2IC%EM$C>2BNFGA;F86mP7LYXE>&HLuw3jpP*CxlgBrYa z|8JImCS5FJq%N8IxRlkzJ1XeG9DZ{tQyB?b40KsyisQM4g^u4)@EjgKy5}KgybkPe z+e<7K?ZoQaY3e&xxBy4rctqQSBN5+%yU1gd1w~s$fDvk+FXsp)jdLrUO_BnEqPAnG zpvwDiKk&4m!lbpIQp@2~Gl)wuS=ba)-@pe;S5}jmc{=r;{%ZCFQ0sAj&V!V%@!#Qq zYUyAXCUJAe_oUDy<S7>cxX2{Dv?e@XW)IGx4=Dlup3vq_(Rx{UaTo=F{Ss33YZGm< z^2G06ImHana{bYpS&%b}cZ_*(0DZHS<I;inS8Uc#?}#Vq(TV{ZD#p^Xrj~w7`y^;@ z9k!$OGXo6c6hM^X%+8}-W&9Qi9wVE8K1jq$4$ZkLL;1)qpBk#XM=+SVgY`mK1@@#} z&TWd+MI4UXvV>K22BWp#{ximsDgO^Hw~+aIJf0?%D^8uOVDb9%t>R-7AfLZIggww_ zYHH1R8sGVOv$EM7B9DR@JM-6m_k~lx5?1QLXth+$enb@4APHgR;DadIozQ0ZucJz@ zg#)(UhS>31OPWmkg~j7{r@wLlEgG=%mRF?<#@w>T7moQK=}JXFARGi6?km0HF0PUM zAJo9*R3>)frJ{4*v5AtI4n&ztDZl<)#zq>!Ct5dXSc+Jac(0qfQCJ7;Okg)@GXatC zu(I~%QvZ__;EpM?*>1)y?`iy7wlRDQ;=@^=Omq#FG#TUHuw15?mlZ{Anqpwwtv1-U zze6|dv9FR`RZO962H|qt>;k$f4#43SF8NjiW;4>@ZriT$aTqhj*#|JzWz;P)sFL)= zg`SaScIw;|&@S4^lUx9kcv%?H!h|P?h;Qx!PyJq%)~&K18I#3>x2lrn)}Aj8S=ppt zhOBPyz;a>w1(<Ux^%>!L9@4W(j(bgGD)3dcG+B9%E%OD!tesW{D<$NW1GVC$v-ds< zvVL?gE;ykah1UFicK21LSGQzacKSUI)@WX?&|NX*3)fwGxshdM*m(3<bc<%llzZp8 z_i#E?JAbXG*jEX2|6XT_Hn9<ybg?8~k6GwJnDF*1?St7cZ;6+0{{{qjy+8%d{%C=K z86DxvrK#FfVW;M9e^|RuZ}`0?dS*TZ6B#~A3FcTs^1%(-svxC#)KIjnlsIw4ixdNu z?!e|=Q(T0GHo1#3P6SOtp;)|i{G#P=HKu6|9vnxZpda{4Pt49^(hEZbj_%zTE|Pw^ zCw{VU)5lFPqTZ1%<L~i)eNGnR@B37dQ^pT5WN%MzcFkWGls<Rb0xU#+2o!Mwg8c@7 z_EnR#O5B+TP6W$pL4R(QZ5-XNYNU5C(L6Chzj32zRtSE`OVUK9$}AN_V>_cI&ozB< zNi*WaZk3%YoY(wBJELu0c1e>=ogOH;@f@@Ndf@k7lE-}nmcp-#{ki;l_{l=efKM=I z+O`V_HQ5&$awn%5=9EyA0VAD?Z*bv!|NA->g;OG{#8mn=K{8}=)R!ccJpZ$8UN==m zv}Ax1J}F;Ge(-EIcU#)g!<!;`*j6^P`R+sdd1%9zW8@=fJxE=gaY!obq|}{cPs!w4 zo1@6dU~*K-Cg0|?2@p?<$Ves684OAGnc5?LR!5QBHxk8h29tE;uWcy2g%_}*+@1-x z(r-QuI!{;TY)=>$<5!n5Bj7>GVK_SN2dmjF4^usZ`TpzlTf3~-&{L7uxfVfZ&bhPf z9~hPnCn+U)l4vS7_PJw~pg2$&`Vx;K=Sv<r8TNsFZ5{O1I}WOtcxH%0QJ`X+B>Iv& zT8#PrG<OM-ua70JcQH7wsl36&#&3S^ZZFm1KDlQEk3q7Y&7&x%8#jiswQ)zB*{(i- zJPQPkM;nN!)ws7u-0c&xKvoiJQ|+Nz{5OMnn36G^(-=8uFjfQR*Jdx1EIH~WFWI)G zaRsN*YN5ems_qO*U~xT<g33*XQ&q+H(o{V96O1^AzCv<+8ChKd9}A(?aRifijG}Q2 zwsVfLI7&W=x1$vk&~a*N6Vli0<6H~Sj?42L<|vNycgDj=4*$oBjUD3D@Tug7!v&-` zfM_pJ4b%}!htOFj(|8<LIlLb%2}@pGfKX^~HAE!Y3N2Eny&Uq8mcMCpdxOxUGnQt9 zksYuZL(>%u8<#m<FjQ?YQ0$gcT%B{Jn*#?I!I@9?eGmEoo|ocakJB9g`0hAcGVAPb ztuPbpJ?-Vd*~Q$x`A5rb+kn?L`l&?^F0C!9Gn{E)3bht1!(!kp_x3t;VF#0*6iN8> zmc_!s$>e7`yJJlwdHCpztvw-(!X?%tjfv=NWqvPY-hb9%=QevwVdap$SB(B|SUDiX z#lu6Vem?1?ANmkIyF`{es)Z(ME{wn@_ETV7{a6CcXDO7Qj5+@HK#PV{Mf+K+KiNtG z3VMC<*eK{eupXQmU4Q@A!Y%k>p*ie;4Zly}noja?Y6n|IqnMQj5i8|lQ4<t!Ts;)_ z1!Ku?BILolYZKCli1EAAyET9}-lH#Fp(+e0`2<o10){$TDWN4<pF3t>x%}4B60~aN zzHCD`oTE2h@J81z_rp+&Pz6eWSJ&R7T{xsPSJ+G_Q$MT>jSiomEs$9HZdIGH*8rWJ zy}6s482l^SE1pl?<qSUi1wA@p7vV=^P-<0d*3r<T)8At!jyJ+0^|?3sdQfo*wKQ4o zc6gff1od41O2oyh$v)VQ%`K<SYtDR1@-qBOzcK4sL#xzMa}hdJ>Uj{pe`H=G-8uqS z#?xSu%|1JEm!})VBI^Ng!U0TIArd#t$=iT_EyxJDEyGAx0!!fSLD0`AezE{PI}mkT zCsNsCc4yAqZSw_xeZuxwq=-~wVG?f60TN!zQDk_>tDsFJ=XRk(Qj|iM;by1`FN8_6 zhcgO8Id9mFj}oCh>yws2xKRZ<D3_*D`!{dugcJ5&N6hbI1I|6=zNzUs-%{I)g5KV2 zmSxRm_s!=X6dG@c<aoE6&2qTFIkjvHQGHh&Y9zK49u#Z$l;$72qj|j6oSJRrovPqj zHh7@}LYDs<5E=g)T^<wq7~+P65nw_TIwE5K7yL@$2e3JMQLC|3&4RpgqL8|jGC3&@ zXu8(z%o{dkp$o>6m^6QbPxLUSb<q@{;*h!rAKH1;@AZI_4H9<z?<qWnb311Lwg4ga z|Ljg<IuB!l4gcG99r~Xm&Vv~w|4mu{3#9J<Ck?^JwoWDg1;UQ(|I+S%MmXwihIYfL zwh5cuq2g;|aeTx{UBGbbG|yvCPwqQE6Zt=%49k7UN_0P<$IcE<<^E?+y}}_;!Oe;= zJ5S%w^*uMU!Kc)EWbfIBBYC?&c@7eAB0gB}>3osqiF{DE0Tn|oW7DPx=DD11DJOZ> zg5^MIHzIMVLJY0*gi-gCZks^lS%Y{&ywEHHc<FfnEV-IY6Jg*nqCp>C{rN-U`1%6M zrP(E72opUzDbZ%wSa`bY?yyOc3X+_9S&E@s9|vG5CzDqTPJfWKOjas_FI2kvGXFl0 zuZ8I9m*5kIZ64fM{T_Y2ZXDFUPq%NQx7TwjT>&UDvCP@9<50hDB6m_2bi)iM@Micj zG8>=$lAr)i^Luz4{F4c)Boc5u*z%=jBO58T%uQzS{V@7rihNIH7N)B8DWeOTM2|ze zC~*D87cN+QmR~bS%r#C>qKkN4c&;ORr;LQcO=&3=)QsHS{F?lO?wm#lUD4SE`AFzV zia2Z2`_jl#2vc_A@kscE-mnZ@g#p0cArrW;I8hYIl{a7}cVSreK*HEl+G3h=BMqEV z>GkMi8vw(RU@T)Hc<ERad4{;*j`kuP6<dnJpar0VCl(wStYmW9Jd>}7V6>iaxy7<a z<>X<3_IxLJ1<#P#YXZl8OGr*Ayxbw~Pz)KX)2y(&B6JsH<fN~bW{P9+`Kck1%B(d@ zP%=M(^F1h<YcaXTG9lH6l|rxDY=DU5U)Rn6AU2fVBORO<a7CTNr~LwCobc&CFf8QZ z_t~@ZS7m-yIA4!AEAsABgq9xN**W(f`1XMP7^vxtUD)Vkjhu>TkDzF8py~2jJ3E_g zP5OVP@u9Kq<9$fUV-$?%y1TQq(_Cf}fhhTpgA|(FaJb$zTlzQs8yhvWr)&YjDCCwG zRrgmx*j3@gx?l+v!CxzdHdX?`ZlZQ3$+G_cB%(@0t2KGq0aZlYfJ*9h*<#_59Nt3V zi!wPX)qd3y8+c0x%)j~y2!4Lf8^L{kKXxfq7PTae0QwVW2i@#?4!*RExa_NhxNJE4 zA>8_u?KT-v5@F?PkCs)su9Y)~Ylqx8*HUh_%7sFzhq+3Z5h=(jHU5b5yI_y-yWp00 zb`$-uqtn1mrjg-<7nXg4&?OE7geta!^DFdhCa)WaZ@m{{2tM(LqI`APyLa6{Zn^vs zOy`q@H?*6jX2VFzefb+E-Ji_j!g#U8`O_9Geb0<{by<*)d-KFR_eWz+J>-u}98E`b zvowORSrEGf%}RX^rb%BQryC%@8fXg1TP@BmuUd5TyK0x`2YzInZqNJK=ItKdpON&> zgYfvdzjVh0n`;<9R}XtP@rLg@&st-f(vc?q8as(TDn*!!E{Ywe&&hkFH^7fS#*Bh6 zJI1y)=T-9)30nM1Chq*I(L0pu`8$v_ZmByFlM?ptP*l7T9`V=wPTF7M=aj^Cd#MvH zJcEQf(L*WrffRtbqBw-V&^}*48|aNbJ$xomzvwFIf-RP7qD8FmCEC?br5Jju29`&I zSwup{q-yB$$2Wf|m_0!_ObTQ=MiIX-O)VoBFv09o)UA9>M;%$TB;L>}@VxTy7_x=u zhb?G*yygGA)^F47VQ-sGjtAxW1%6+C&G>)1lOaw?`(5P!ta%Uk8@Yp5eA>>;=J)u2 z>!Z^(j%$_=(nPadUN0$!r!t7#KN#Z0i({TKeh@~g2#5-jiK(bz)cmrbHndS4g%6%K zi;)gYT+l;1(!aCM#1t7)h<4MVX`e?}liGk34d-0{SG;My%!tG=)$NzaSP=dZQmWal zAEL7UD#Ry)$c<*Sc6u5u%7A9>y@|9fNFhmp#4)mp-vv~!%Y<eO!Xj<mqNpr7Emy8v z#{6N7&)B04Q_dMht&mK!`x1U`EDF)y5{)A$x+wAu)*Y0eJ_b;=-D`^A_Nvu5{&7_@ z{kPKdNcF;XJ~)1P_^cSLF|4w0`KP@a+PtTSH<k0-_Ufdy3Y$_+Hr!us)y!!#ZUO&K zF?hYmm3eltiDxS`w2o7fu)S5W>PB=nr&KN*|JjpO-^x^HGB!AU4`SQEY8@nSTuwp} zv=t5qVT!tF&mmpkH!J<vab7Gs)GOnjYLmc{2=~GnEuNb8Vq$!P>6zMda-KmTyCWhw z{J^0mHU~ByZmjLDMW;DgEm!R!;iFtemK?jxl<#r79aPMlt3)6jM03{kA7Qa9D`AJC zxY7W2SRKW*JDHX{U)Kpr+EiTAVi;(El^WVUno|LaAIO~c<P@*QWO@#eo+Q{M%F`G~ z%y<;r_CICH%?eJqhz;UQF}8Qkw1DcxS>@fmnuC&iUXdN5ym_As2^fymo^dzauJL*d zG&uM0DT`P+6W0`egK?)2fCZ%7Q^?3R2L{0~=VKk3ZN$bVE2^gG%JIahMv8k%sA!)w zdw){ja%~AMb=kccY$8m{^kT<M`0DP0QQSqQ^ZpWB_--0U->yz_YydU;eqx7_Kq9f7 zBk0AgxrvpQZqJaGZp0A$w4;yJpx_*|yeH@9eV(KB%;MxLUqw>M7FQr^jrRe_90k9C ze<(Mu3j?MrwZB5$EwjH>*^!;CRT`-ga|8{Hb17^&^KZQ)Y2R@$oV50e{W|Tvi)+|+ zT^J{oKwrOK%Iy~%R>J+WI!XMlw2uLHA?a`<fMvm$;ZtHk$C-fI*%ma}UX>B)cU;>6 z0x}s>F)}y8o_UK+`;`V;KU>~vhUzI_W14Im1#r?bX@8}&TT_;gPGejs;?kpn)W=!c zC>B0XytAxZDWHj_vXhiT-EF*NaKp>5*JRBL&9W`XehJMNKo91-q8u6ExXA-AG#){H zFg4tu<DoSXeRdS!>%sA+5T)%ggP5~5kGvB2Q!D;v9SE@)_~R-U9Ak%$)Z%1}KmZ!k zB>7BAL;&mENJ%i;-LMzj;O$jfE8hTTDVLm~-$i_+dfQ>p#Z_4PVZ(Rr+#B5k;?PvX zS5Z>XXnJ1QQ;3aidc5G*OV$z^h!`o@IWT&F3_rtw&QiXJt`WN1ZBPz>9?UUZfQ8G` z?>8_vj5jp=nWi!l3(@doc7L|sc1`N?gVx1xewc&*2sLMhgU2E1$vn9=3ZFbnqeSCk zGjjhx*dmZNsHWv%nKzsWG0&Y`t>f42^U2J?9UjW>54p|5P?P_FQU3PPA094G(Ca)M zwSv*lK=Q|jeFil|w+|V79mvg>h|E8G)0C?G2fl=M2i`(=Ws9VxuD(oyow;}`ZPMat zBgQsrZT`B`#_|bAP;?G}%y3`u$sx4p&PgD(2XT6B#4&~r62>0KC-7cE>8JQjR?q;M zh1*k(zZpKhDRz@HZB|^}HfZ~CA-%~~FoGu-&n}VYr25n6jrsE<2StNJp*3_RDBX!j zCM%Fc4pc_dZDTDGi%Z@tDKKgnlLj8Dh*P;hw|r-isl1V<yu~8*<Po4vJ}@hSiT$`* z-x{Ed5us^T!rGeUHXe1ZbE|?7U;R+X(a)4a$&U7-h{s05Q};~IL~z0Dv(;UV_Zj$y zwj1y&ph|Fh{?)xK_&OCLs8Eml|Dy^AfmXI+FQe-&o|j$^bk`Yl^tO&DFAfpBeQXVe zvb!PJV7m^GPDGJ&UEb$=e0L)_WZ;;wgx?L}i+yOX$8aH6Y$HQppq#2MR7{CE>t{mU zi+Euhy#U6|q@=?Bxo!6XiSz_SQ=u0ml%|t6Z48Ab&cGF4U7VPCWb5YfG1=arcmUH~ zL}}_uq`olq30ZrRkye%VW>tXLZ4T$6kr^XVugTdYSvn0yHsZj_){}2RR#TAlhW5$) zbD!3wYq)X!4aDQ8wMO;VAOT?yei|MC-iI3@Xc8Qq5@h8bz|xGI(4KaJ&LomPz~Tgt z>lV<}mEI8imaWg1{|)eZe+SI;m+zecR_x32w|zV?hrSQ0!;1&yy9svS{9Jk>KeXVv zAuTARWXq0G{B+@%3VN1DU^}KVi0;2+N#JzWCj<OQ?BSRxxx%qL{&WY~H;I$Fl|wTV zCij7aZ;9aG;!}Lnz_S0P0--|!qH)+yb@<o0fTJnCG3o>&xmT);RB?6(P481vDX-|E zg>u&%bq7~sTvqm}55Io$h#)reAIJGu1kR!UAB>lf0^?(Hp)WXZa1{6OAC9MD^px<v z-F!XvpvygRh9(*1_YcS87I=Ra40tpy14a`*@`yaM92C}l;vWI1@=F&@v-;JE&DBfi zvHG_XT^e8W>8kAoU)p0m=wY8;fw`m|zdeJLO2hblbVjcEf~D|<y*Y5Acga%Sp}Tq0 z?0#xPJTat;cDqu(yMtcpSETo2uG8uGUGZ^KZ*~o==<2iXp<$!si7hEzx<6@?M>$*s zFeJ$q3v#)9TRHTz#j|bVE;KZ0onG~Z3}^oiUvXfGPTC>=knnthL=ESA0f~X*_^5?v zwlQ=hpPUV7c86z{!8|GaXN3)QWH9iBkA{q6Pwj74z|GK3XAu1m)aMoQ03y5iP(DMD z9feQMwuOuBem$!&Fb!pmgf%L-O8DimkWpeDym{IIhI3etFK)wUT#E&7!BMam;GXcy zDKeuuHkgtB-EoQf$_)%s7w{Sv2;XVyi2-<pJz$QxxuDTm)Qf}M@{G>Pna(u!b&Vy7 zu#+Kl=DtaPVyi}9nr01s7^i=b)u{hGW}==kfL&j+fWF6vD`dxAbua(~-c+4-$QCl- znoRf%kJLQ2My*8IG$0Wd7`D-^sp4f^DJHT@$U?6d?X`t%VB1-@U)k=p3F(S5&5t#! zZZa_qG#|IH9kfg&4Uy0!7%yqlwR%T)8Wm5PNUnagb4L%Q@=6ctRqI3rGpUQ4)#()w zzatAz`Tiz(`FSl44_oc(WrJO1Gynv+Ln_=tX5$qsjfNdH$T>@nLf=^n<Co_bOTz|X z$6kq?Oo*@a)%&O~HU(r{bc1vMMWNWjT*P$t7|y|8EB%@2L!uY=PszT-k;O|NbAmkq z$Uxu>J}?d$FrCDKMN={2@@;)V&?7`LIIa}-a!jB&9)3_x50pEpI--*m0#Kt_hKSu` z!RDKTH*jEFaMl8`lUvyR2wPUh5kgSu0SK`Cs~nVOT?2`K{xbM{CfQoaj?}k$l~66Z znP&j=Q-M3}O<jGZTEnDM72te?jC7vun`zMaF_}7`eu&i=o!QOlHE%snY2CqixO%6$ z)&XlS#nB9*TtHaX2LDkJK7a!KgRNCz4OY^F0j{lXBBd`P1@jox_aqXiz6J_Ptx}yx zrnPaD<zxcdK(?e3TEQ`fZ<SD6A&|Ccyuhwxejp{virFb)E3f3n&fnhq?Q8E|$NCCq ztLYw2chP3Axuq4cv7`$;OFDp2Ti|Xe-Ze*UKlr0oovc=)x5)rM@{%IcwhwNU-P|Pm zzW`@In7_6b#H_$WdBS)lXNhNvEwPfytScN!)iVqyrt)l%g!Q_4{f^!@vXEnth3*Kl zkZeG&i_`8>x0P{62?fFDi2h6%XB`6AX`&+f%a3g0Pu0DBoG|UjtYVG{TQOc!<2AiQ zUelJUR~i+#!CS6{bCTDjtQc7fLfdNoCFTnrC&HQXEt6hsn3rK*caC|j21Hjej$PH) zQ3D8f=TOpX$KX*hhGIP*V({MK<h)lmZdjFJRfbi4Ev)M3p|$(RX?a(|Ue%=GPKG-f z?qp(T^|7<|n%G&iY7PsU-DkAyN~^NY60bTw6(^5kySr^(9$}+$k^&j`3?%r&i(Y{_ z1N)5_He{Xy=EV_X><Fi;EQ(~(25=&R+=_F_10b&Tk2}iaeke|T4~KqW%0PWVJ)HOc z_@h^?!%3Yk;X1;|*nM$w5OvV`c74r*YO#vAUTo2u=mwg1%MXaP5!X{)F^OTki{B+y z--MM2QCMVsK-ZR-rYFG-n#l7u^ITP+@;rpcW;GNv;KB~w3pnm)S`5&ZM$c4E2PysD z5yo4!a7|fhIYS%2xEdg$80KIhFp8K^%8=$RYMm&YjB#)Z7>=4Wcujc}|I9$C^g#(J zokM_5n~OO)rK>X-eWndsFRpN>b_HVnwhIxocrf(W<fRu-qHFzCRlwI42xxVo+*S;J z!8m$X@b@K)$(_Kmzs>0Dln@$d>)K7U9&_Do$6RlzjP%U}sO>LM#jgTYI)tD&QV5FM z8iL~O7=p5=I&^b1PE8#;Pg94^^Pz30u&{tl>d?X50$J=^aaLqmT%<_|cCSO{GUy*# zJ5H)YhYLyL;%O&rpWp!u5NCp<(BDb19F3|-wusixJi#P&0_Nx#96^owS155u%WY~( z+({+q@O0@)(DCc)0ait(wx6hVB{M;RQ>x7AtCwmE%}qKNnsaY?p}A;(h34>(SfRNp zs?1cF+k1sM^v>yQHo7te=Jp!~_TsN5Y-@Gc*4iMski^KL|0&UN<obWp*f>?0Qw!RN zyl$lwR_l`wcok<2e@ZBmAV(NiImsE;2o&R?m5ik#DJ4?90b)e;CRMK`Ti3>QsFj8_ z&VaAA5RZc2;_wZri<?;o!EzWAU(?%S?B$zQ@7c7#g`AgZs#{dmEt+AqT+pu*PZQO; zjKMOtU};iXq{jR&;Erz2@uZLIlVs&YQcZ}kP$W*sNtKO5QK!JU^^+mLStZ3fO!lu- zBl*|$Hd7;`U8(uXDPq#((@J(B?}>nq>Udr8QP+>Z@v>M6@X_P5hue{X{8@FlKE?4m zG;^=gmu)T8H4fw|s}xL@BQR&N*(ALcX+^a)TguK7G}{gHJnO*uhP|n>b8Auvcd*;Y zW)q9q#gCA4)NbOz=N2*X5IV(EzbGHj9K1DYNJq~TL~p&)XB(|Z4kT=R<&sHp8MO`N zTVBci*zV3p)3QWzuf}}mYs`0fltvzi3o8wMX!(AcS)d3*EBA8Ok6qVJVb<AvXLmN= zIeeW+9`IF(biosx%y}{fm{_+m$ssBlQ0o?#yp!6PPn#ZGfRa$0k4SysxbHt)I`4n{ z;me@32fqxSJ$wIf{==);aQfnG@<aF~%bn4?)7Q)NId$&}P&j;+HTX|I3b!P~pLlpQ z?S3xxgL6yN_r3J0H$u7uqMU}^Nwlr)*(``U7B`K8UW-w1YlB}GYvQIx!9CdCR@u$= zVV7&Qy~Py#1N;s?3y#{`Y2!3@!ke=bdf~R|qQ+8q6icCGq4QJ=ol??zPZqixFe53R zuD84^CYaX*^O|7ZI}^;igK3|#OpYC^R7|YyD143SLgi}Hl<=y^tWTGV3l=e~2LN9V z^L6Y{wnpK;RPZtTv#1-EXIP$Ld3TBBQ6oaD0lR|8-L{BKH;c6NvJ~(43@OgN<&2w; zcdsCUeUek=kUB*Dh+{b~4XZV**09>IMY@!V$XvH1RZ05vVIp3%X;89pQcZ$#c*;RD zf9d+b=|`Ad-fJfMIE`|do9F8|s<pc!nkv?Snc+EuL${Wg7Vl}8AAW6mSsQ>Cb1*4! z_t1)9v%Eelrx87Ih{U%Jd1`!5?U3YELFC<70@O|#-?}`3CCw9v)jWaaa2tuJU7?dQ z;`dJzAw{yD&RfwgcggLNQ>7FOp_1xYtHQ#cvZScg-IAG<@-dzs)ig`C<Q!?!S`@{9 z!&emvdZV|myCt;kEql(PzFlUm5TPSASwXECb4sp`y?w5Z&p7&Uf}|rTjZK?<q&>$) zlySQGn1oQ)Niy3;bu{t8<>)Scdexp}U6~Ql&%7beP~N`5`T~?EqEGzgFMoM<`r^lT zr$0U(s9^}`-o3wed~9vC0=sjy0*wuSJx+eN@&p}F_4iqiz}ci8fhjGpr_usK(9Cvm za?&>4Z&qG-v+&U?!-0aG@ul%(g2e*O1Q9zOO>Kn66Dz=^Ppb#pV={<6MS89~==APk z8FgCKDmOZ3=<>Tf>Zlq2-1Mv4T)L3Jt64sV0v>$}6;na-xRZ)0_QfzDj|Gm|5S3HC zD&@7uNDv?lK}^z%lxR=%{=c5le-EPn-enaxu2@;A&4*QgNg=et4|c2#Cl_!&tmmo@ zlr(4FmSs@3i(XzE6x}_%+27Z>CO90Fpi-e`O3>60Czw@^)Z?KUd@9#KQ46NvHb7M` z1@T4&&&W{d@gNs4vI^)Jv+d_z<|cMw`Mw7%ANaYIq)uWvS!x3}%0jnD4gk4O#cbp0 z9FEbPoDeJ6$>L%;PA7zmtAX1de<xtCR6EP1HS9r^6G@kJr-*K=_<8o~hiAo)Z(f1$ z{i`R#A1>if+3%yXmv28JneH;k(l7D*_aFap`c#hS4p?V2p>5Ck^#g%8gnS-4glmrt zibFHP8B-&Ra3&Ke)Uz^R4kk!@_aJSWey^V*tpXAlFo#2jX!|=>JYvf2yyUn3`JcTf z{3i~M0VNmpN$kx(|1U3*{c#DZ11tH~k^Xo&KaHD8NbXgBv^%gwr(gY-nno>TTBA-? z!pC4h16r8c+<s5S3Y3R%dm#daDZIU%l)ZbK_tK|icj#``zUteF1E@eC+}71@-(q(L zU{U5i9o)4}ySrA+{-M}5T(v;5(C9-giYZom-BXQ%#-fT+hg#}iJwo1(3Cc7<ncqrK zrv23fWn%vzF-b{E{AuM)!aD#dq<B9Ms(mf*a+bAET+~Q;qn|5fPqJcx7r9rXT%3-B ztvOy4sIygqpqhZBv$lu~b=9|yQy%4QZBH}o)v#9+^L~feYa29N!f9`y!|r0H@kaWN z%OkkR0|RDepkEW21PL`Fk5H-;d^j4oIbpfK6{NYfhmy`Otj=<K#*-Wq>qT9YVb`5S z!mC^KUr`wh=;lp<Zc6EVHuEX<gn(`X$!Q=tZwVx)Vrf)(nPkY_l$%K}A7QJZ?eJ<o z{WN3-CL>83N!m!#U$Z3b)<O`XVmODBOmK_p<OC)rNNcJd1!{vvZeAmIN_>8$&|EuW zyr@goV24Ade)9+?jD$*K&KGmOnDfP)FD>VbW6qa7oiAOWzEmH2Q;(^`NVd>p>X(mm z%A8g0nN!A`GUk-I^QTOUT}m!Qq671i66Zn=-Nl!Ll}TrKUW@sXbLv;ibTlk4Se#3u zK3RZKS(%yNO=k7e*Tz1!4~3|`_Kb99$pLiAp|t{FuEVLNggPDm*g_4yBan5uIk(KY zWzMay`P|wPh7yO)U!Vqd!J#y#(~hT;d@!s$o^Y=z+)D+rAS2qV4|I%SZM=0PN9;?f zQFBC@Bg!06cL`oJhd6sug6r5c_%x~>7dcR~GNs?BOzDJo%cP`0b)o0});KHc+L|kn z#+(MynB5&|3=QNhQ%f6JW-qz}jU{}-(1UugEon*>+%e~<xMNzFF{dNU7$#7ryd!C< zCs4@-z+>uM+zt3KQuWQzp=7ECjn1TfjZp?PA8eRCx_;AUb$>RlD7%5pK#j?1GM?KV zlhXw+u>z3Xi0`x^9435Jj61A|3fdX+P;INZ`$(R0-Qu1x5WP)!5rr;83wkx?S<{4u z*~57GhDZhyh~759iCY6F?mKouESM}s94j1QPZ7sQ;RXyT2n;KAv0|^a^spCUB%(vp zu!Lqz?#^66Zmdbs!aUzSeqgr$p|-z37AtT7uXjOF?!W|Y4~+7D#R7bZ4@@r^JX3IO zcj5t>=Cs0rIM+rvFx&xj!cAU-%Y3ciu{*r@z#P)m26%X?dy(JV&b=tTErNPHdVKcq z-uBk%`n&3oed<y;bj_(1AnRHJt`;6lmLo79F0LvHM8Zy9t&Ru^ECS7Kh`vwx9fngw zGevBk10EK@_gHPA*FM&;(^u$rlAXTg-NRYyC5s<+08Kx@$?yvlA&oe8BsE&TnDRo` zJ@5$(M|eypTFak9jrP?zTI-uss7w;(Guz+WV`Mh5B^A`n;AT-XTR|~(z~og9KZPP2 zmg_JF1>jW;42tlT7UV&3$83n)x^m4VHx0I<6WYAb=K6lcs|0}7SO+VZYH+`+4!0j~ z#Qie^s7oThQLtGx*sf7UI^wzAiuih6VH1*B(Dmh8A>RGqw=Tfv>0~-a1d7`0h~rvB zsa^Omwu&$eEkBQ9D{)|E6?vKk0QgRx28YCP^*jnBdcw|JJdnW{wL)-mPZkhRB-S=e z*9um9Sg~g|3wuFx{qZVz1)k-_lZ(ZZvo{}q{`}s0^uyaH!Ra*n`}vDg_hI?)#f!)3 zr_X*Sqq#a>-4n7N4SgRy@RmK$;}1yquF_bv`_)Gue8>(O^x5g;9GmAC(;nz$aE|R6 zy@lAsNds_H#8z$M!s;snXklR|4Om#z-M9$~>$QNwZVmX_1x<DXP}q29SnYtTJFS4J z`v7EZ1zp__pe>$N254I`$sLhw4I1p;&|t9=+?=AY9Szpm0u6T9R5T18>?%B1SAz!| z5;$1&KLrSuT>ozZ1ADD%$!_K4r@*NV^+~v==*Yfs=nT;b{8{Vc7hcsV$VV=9xRZ{1 z-LNM(UF`{L=c<!6;cGGmXe$a~u1?cqT~AGqT@1IF*806e6_U|bD3U9?G;0)bcf9KP z=~9-4o8zwAsvIefe$@6KfAp%kK0))*yT0bugM-yVsxVV`C?D&ydQ?OJRD##AaDb+F zA)bUSrI8e<XuDaV$>WiZZ3wOS5S}cFsd-u;AqvTzxp-{tq2}<a`avXqjp!^78gl+N z1%-U28bK9ZScQbKFVnIDiut%wz^QLmmAiB`iilYuAIQkKUS;|pu#Hd`3ZnM)LB1rC z=t7PFm8@E^$^!;u$2SAoTvLqG6yr3-I88B5sTijk<LY;eaqTAk-jxVfdzT2;Z({qJ z-MZCo6~jW;)edPO)vFTAVGEtFE>m9fnFpM?XslWZ6`6=pnWu$ttFl^WoGOoY&Aw#G zj#o^PFrW30c5`N~djDn|nvR+{gA*)}vN8vk1D(R6&aP0a2orUBHA8DWsD;{=>Y^MU z^SW!9_Z3c3sQ^L;4=Fu<a54^2e<T)3E?4I3Ms3Z`#RqFoR0kiEBl(!zRv(kMuB@Qn zy{w?+v{t?>Ak14TU-Db+l`o$_#=qMfum!9_-EUb%=W;r3tLWU_%Bob@B*5-$r{Bjr z{r>;HFU&xr=xbYH*M^^j$3d-P=6K7LWphY%W^9`~xXI$TVAnCz*0g<ad5P#1EKlV% z;uP$+g1f)lc3rtCy}fM8+g6U&J3u*Fl0GVLmV0|GT6{;j@_wQPaPtEyBh26w$&wu- z1_IXwi5uEh=0L~tizKi>;sFaLd71(T`bEBb!~n0)UVHo<!Q;QZKmYRW&u?CQDWAQ6 z3nvdxpS^tXV)VW^9}i|fyc~U8W@oRK@1Hz-`6vjp4~joOqRmP(rUA)%Ro#$$`BkU2 zj5a`=_6gEhdjS3a7);RW_h<_}LW0<eJvr}P4i^`_tLbvyLt7GV>da>pS9bB{FTa)| zuWqovBCjO!kOd&ad5=tH(<|<@c*RXESFQIx#FRhcu^bxry<>{GZdJ^+Q7Qa}%9WY` zg4F>8`z+^b-$XgrX6077K5NX^oE)EWfk5rm)>^JD6<D#xQMsh&I={dCOE?XbADDsU zh&@f}<Ob<CW{>%NI;+{2dPT15+B$T$8xL6+^KO)ry}boS@sqRpaEfCktP$2|uWJIk zeYHik-IYUQZe1x{ZJ#As?QV2gW)lb8+uGJu7IdfT-qu`MQ}@={weGE!^D1FX`ZGu| z-ZNQW$wbnhX-7Z}Lr?vH+UzZ7Jl4#>h8fr}0~>Z%V8a>#Q+iIK4Wn4{d>BE%qgIYC z&>K1n&~c$Rlw|jJCFU1n!^8{|GfeDGGckpY2#~UbwXLWjDjLUE;gP5+qq_n4;X1}= z>S-9pXBeMheEVa3hbg0htG#6lZVza$p@CZeVKmJ?@bVcOb6pwx5pQY|r8yAFL^YAO zbx$e0n0dwrJm^!=sr6qt0z<~0B;F&#j}1RI{P?c$V=*ZUF_+QG!UYZ0+h+WTv7?67 z+zmGXs9GmwDkjfkReHyv=DosH2sOOT@U}b4+X$izuWvH|=hN02Hy#c#SuE#b{Teh^ z|3+fTK7q=IFazac%_CC6IF5R}UM>G_E@z4?u>oJwQ1EMC{T2#-{qm8VG$anDZq=|} z!+H(t{pMIN-YFGh?Oou4%`F@lzg|Dd+Bpsz7TDunYHaB}=BCD$;lqXx8$NuP8e0O3 z>sn%q!;y|OrEaS`Te;Est4H8)A~00N)*Qpu3|ss9*qZ&VR<|WEPB^<}z$<VxL{OHQ zYiiM!?buYiNy!yoZ$epxytct~oDL^66)b3&j`_`Q@DTDFUt29C++j96O(7JV+4BO7 zVQ)I`K|JX_v0J;>MQJpyt&hZs!PH0kO2KiYsBxZZrAa>$B)XajXt8yw9ece)t=8KR zu5>$UweAbaYPVvocXh*et?(*^qkYR&U8VS%olClkQ8)*$=DLr79z|riYXLn?AWr(n z0n53?jqz!O8J`9h?MHHHZBZ-1%%>w$)a~0U>b4P9Z!`V{U#ekgbX-AsU^9l$c=_w$ z1)eyxmKv|G<gxF}4RmcbNx3=eh94MypyLNj`536cRMdDw`>+D8TmRVR3QpU13~H}I z?fq6uv#t^F+qTR~{9p2OiA2abxYXM$tD9Sey9#d~?VSH#e^bwSk5w`m4=1wUXbe8f zdr{TI_P5{p5ob2M+3+1<x+lvqoKrltmC4|HF0*`FqS$rymJU<>XmuT@-C|m=8wRFV zB6y@JpsW^9wn+hHhJU$5{p6Uk%rQZ(tcw}(YXdO4BoNE>o~~|je>?ND-&24kn}CU| zE$}-&N0@io<4)hj0%RAoxDv-ms~w@`#q^SaiDGZQh`L6nIlU4%(dO{vWU{idY&zFH z?zf~&N-D%79x9lzy?DevTR-Yh+i87&ztGFKwPtU#*l;Ic=I({3H$rai4$K^K6jKhW z9Px6}bcfxqDqOb^r7ahl4mH4~?9l6JDo&#(pYaCv34E(ol@8db8ilDXr&*d>JQPr$ z2P({}j4?IA@wp>)U=km1sm=SGjtJ8eP6NQT(aP_8M2z~L3|Szu%|>N2=tws6rF4Mk zSJCX!eWGVyf<<<bPtQAoX4^rKhZ&5m7}$yB$1bqqH21BzDB?6u(<p<^0YI}|zD$@m zq@(FMUyz+Zw7u8AC1Cbp^!IT5I{M+uG#`Kd^78Y?mzHI@KRkT3c=_zH_aT9=?Wd&- zqK!t!=S>3z^owvy0?!sD8O=_^y?p~STm&19us4QfNg;4Wcx*vika4k<K55~wFTWNz zY&2E-gTqd$6496|_vUa$6_9mxq6ML{tt^Lu$-d1n+4e21a=i^q_6t+v`j|DYw*a+v zJA+!|y0(A((Zk>DU0?UmpCyh!MXEU~R<#ef>kOzCC@ltdyTM9>jvbLAu@@|=SN>HW zKlNWE?w5{C^HbhMLO5-e!lwI;B`11vHh;B-#L>ug;ZF_zwK$wxh?^V;kh)`-50_(( z^nAH3((_GV5KA<0N8qU|>b!;M-z{Vz@0z;;P%ZA}+X@eTs}bwl$f4qU#|8avKxx-U zg9b+1-VsJyX5g-?dJ=@gs$)`v@W#Ha&R7J?s-UFRDRNyie;tz|XHw+$k|Kv?BSxKH zOPZX0xHP$zTCtU{N0ibOwp24jktH?lPZ}%F5fXM$W4}CoWO8;5Lo!eVcNzILCRTY& zGPzC(Q!0%pYI;Y;q`Y+;RQ@xeMZ=;Di!v<g&cF(|n0&GeoYzdX>6SR}#&fbkMv<pl zOtmZcmI7UHV4j6O_qN8}Ht0@<Um1S&Rftro!Br`Bq<S$rGAH%5<8Y`)rdXTdP=-U@ zMf^Fma|N8vfW+9%7kn03@eSpvdhcOg^M&n~df)2f%^U6-w-LH8cxF5DJNcy}aJvY@ z&WqmUgE2fuW_TVCnY_>9k?xbYxBEz;oh<JE@kj4+n6qern`Vl5Z=w#dg<+k#Ei5t! zZyAqtWFF~s=aG)#k;eeD<D29Ym=7^aDqE{vdMp@qQ1K+$``g6fJWx4HkK`xrQNnx2 zl7ftcXCyo$;eD-8oN7r+1%}h%5u}uErURNDC>=!xptJh!_Vqwlp?k`~IZbOOk6i3- z&E#(w$a-_2m;=QeD4W!G54prEfzLNEgypDso=SGBoiI)Gjp5|5C|eO7lOAR7t(29z z1EPl9iWXUqPt;c68jeBIy893vWXNP+V_GP6F(AqozRfWp$KT5M5+9gmKP!PKyJO`w zn`@G0Gq=WU?r(N;4<`amX<>Hi2vh~4<yJ<Z?h{dGwdYnry;+f?jpZl^a;nMDMIwdK zrwj2?11ZO+C^njzukLiqM-U#r9S}H74irrSI3e`KRGK;k8Aqxm&yv`O*?<cD*IIuQ zRX|AAvL}yQBJU+7?^vbo<)+lVJPdUUW>*cfUQ4Bz#m>){SW^%UEvt|+4uqkj;`LNx zwHS26YJ`p^^u-;)?9kxoRQNa>qv#0N_J^TER(1_Zh!8u8)(83REa09XRxYN4axj6H z3dYL48e^qTKvwz;Wu<@5b_*M3erRRT_ANinfdx`GvH}pgwr9Ip8pVCO_WcTIrB4Z{ ze-BRjbf@+i*h*iBs_aW}D}4cPrO%*B`qj>+#+g2utv){t`xIoQFQTmUbucS^hO^SY zHyy;q*)Ko+_VWGH^eX@D^x==?*+1;$(PH`Y#m5hC#xGue$=?5t#`*ebpCPUE@3Hy7 zAFpPxPx)w{p5oR0UoNKKl|6o68i_Oj!i)N%07yTG`*?OSy+nsm55q|HrbQ1)u=tA* z{gQBxdeaHHM-ZIoilMg=-SnvtQcXplodSIUsHM-WDE)@VPkEZ$sJ_l=0}qAGJ-rd1 ze6E|Nd%7>cw}^eIIZ$6=LE4TBE6^B|Cwzi1q-xqJmP<vMoGtYQIG4W4m?$AdoCYZr zOJ6V}^@)X|ngG0OtA0t};Ez{Nkijx+7sgZsGA8V+r6F}q#da!qmp%ngY54>pH38#A z`>mkfKuw&Neyu|EE2NixEo${S)=OVdEc!CqOP|Vb{d+Zm)rc>B1@fgYVZOACE1)zw z%(MgI&@X)%`=xsGbT%8&sUYo%FfV-}fxE8(fT_MgAYl6JgzD=+V5+V$7??hD-}IY6 zFnvMM?JH0)6)BvSbhLzi6*!oRFfkyEKwbnAW(6IW(=pUep<C@!85M=>qJZr*>I!2U zi#JJNN#wmyHHl{mOVI<&;QB1<u#a=8TVBAe&<yH6)5eA0!VCD!>x&_+72^=@(*AYf zfdOwu|A~6aE273Dt7PMrZ%ETnIX$a~iS4^zsRv}6)GnxgErj=Lmy@(Rxr?b^`!xE* zsUdGo$?GD0fcstgf>EO{_)4S?<8|zdMv=Z??w7w%8~pnMP)=XcT<V!dx=YO`YVD<( zCvvIgZh{6QFA3dg#Lp);EBa~_*5nDdiSD4>kA?fS&qjGDX*{I+l;$byyXgzgo4(e2 zBY($rG>>W8bH$-4UYvgI#$k)4^fly*EBI&ng2%isdYv@_k6S;X-B#3l)93D+ngdDV zBY2QdUA|A1dHQYi%h_RbOD#y8L%9=HjJsw%G2xn2L#=va_ZifAO{zoY$x?X)9y5cF zuL*jKsg$~U-H>5;SqW)q)%{&&PWZ?XcC>c7|F!xICkloif(d@iB+Kd(<RbViUsNax zUw4wowrmIeugSiN^a=={)vj}W#OM9m3@B3%2e$8vW_*c+T8|p7`cU6>>FN!HiK;og zG<&i{R@<GMlg<r6(zm!lS-QxL#C^d}-&bt+eG&G)FB|G>HmqL0=BG?;dzVj{+WOuX z&F|aH;kx<Yw)lQ+jPEmRe7zruJ-&LSF)`XHq-u?5cCW?15gmi;s*enWZwsdRzG$28 zOU8MX4+#{#8xyMdK_v}hdAKhC>GwIBdtZdom-M{)fo)ln`jsQ_Cj2ay<(Vk{`0Sw| zh^*!Zt+ZONWrepdkQheXZ;?OuvZnGx@<pxnZN1@xx*+<Z52DYU5Pi`LQ41S_Em&Tt zvM)ju_G|NUU$QUvo7Tj>jK$nnEQNgmjk#Z02Km*>G}vEZ8|(|l!Tw6?V4s@@D*_hn zetnjx(&r}2KE>MR=Z>l*6R81L!W=m3Z7KnOSZ7hGztU<^X|8a?sN$?Ls`#RDf!tNu zF1dMH*}PCQ#Mo8V`ZDdN`d+hVWDe-V_|!O`GJ^-c{H`MOZ;D#3ukY>6dOV#Vl@oS1 zAjiNC3lC&g5~Q)^yLM{DAaN~F_%Mj0#D;!)hYZML`_uT>@x}bh^Ecty;ORsA-RST3 z!;$qd7(M><EVdq}pVKsZwD{op4FfXivN($x0got`l%VM|y?WnnY)(#Ub22s*6YDWv z+LD`<$5XW3=ERFUy6OQ^rkCzJil&dBA>h8f$r6hw_&xC{V|KpXW@lnPU}k5s`m^~I zsn@VH#%E)EzRSjE$L?l*cGp^;n+=Ejgy;C!iu76GTq{H+u!({A49~O_IziO)QO!)6 zCcA`<+p4-r_Nv<KwK1L><GC@O8{@e#o*U!&HpX+@<iGxH@78nN3Cjt3=w-xC%O(St z&h7&k(35Ivk(jO1v+zsLkhI<t7eJ>(MG>(~5b=3PqBlykkLNJw3K|j;k)nidA>MP| z0|nZDSaG`QT>zvf;f&87mVeO;fRr#f+0;be9E;92tq{|JEcV`}7JJrx9&@jFH|Q&3 z+`Y^MjJf~roBLH(>K%cVt}w(OO>VlAwtnq`);13El4zdNnft3%N6IU=y(F4NuWqZ0 z9&nRE%R&n;9?7XAi{ueE|1!gZkDq;efewWpK>wdD=W}XN`!{MlBlOEA?4)J&q;Xw$ zbC>Y?l>J8e;l7x~Q5zsz9^GXA+rM%2rEnNW<*6U7>*ED2>tlCX8qz0uwAaQ>)y+*s z^gYq$J>d52ZCzIFUu?C^>bcftE-a5_EVMbI^lhy4dHI3l^P;4<xBv@<N=MW4b0j?u zCnw*u?+Si~8B!+x^yk0+%fI};s818>x3d1VL<1dzKmFPISL&sHiy7IUzGLzN63LJx zO5}!tmt|?;#9nTH_kFK+f>C=I@nlsO(dBeG$wyGVjBa4w#oS|Ho?U=Bwv`Tt#3@D| zm`oQvba;=*LvP=_dtBY2_!snL$BpY(m~<z2;d;G4(|^8WV_4Y}H2GcHFA<-lFD!<W ztCr?%rF|wP4ENjvS_Zm)XNfec^9dVg{SNO1E|7=`=%)L@os@?o4A1RM7t8^BQ*%N0 z*l>)uKgEE#>D3W?1lYW-S)`W!JUM-KreB}HalczJ5BS_UNV|^h`YRen$|!qs^U0SR z-OKD>klN}u*dOwr?`HEMt|Z#~0IwPxzGiF+r<FFCy6@|657E*48E$ZLup65&Hu3Iu zn@|5-W1_}a*y)N16J*#%T|&lnmWv`Dr#{I*-2Uv1oG1oG;AgQpd(GKv&fcBpf`v8= zsN3SOU1=xLubRmog)tT&NFBcbzEMgHV>FC$UqUFhBisg^<lB%!*Uq-~u_LnDY5>Lt zWZ11?x5fr!Y(T~aq&<C`N1ZVX9rj2zrXh1An<LqnhVD4;JsQ6>79+zi4Zk!NqpwY7 zy_H6!5gd%c1PRG`7iL?mA)FG<haw#ufS7Uq{V}-x@fb-%)n0DBJ@=;LqW*D^gng^{ zZV;&Boey_tsdZkP4dHQ(hvVJXa+A@KjC8&d`&-g<c;0zE4<RTEl1U|(Qxu&;(r(q% z1=^PIkf&J3v#MY$nm)Ol3_mU*eo)6dM-2F3Fu+V~Fd!LanjsJt*XTvt5hi^Xe|FUo zv(%hT!t1(?DE(l&_51p#lQ`ASKevCnl?2(1_BY#?n%~*a5jmWo(`%uei8wNso`ruB z4@cT&y->2qQO$DANa<xZb2T5Tei0%hzl8ayHWd9)jaH9!!_VgL9HDcRRYZ9hIt8$T z$c2_4xUL2K5LiV9;s^wJkVLjJ4DueF)WZ?KEFBNu{Wcw^L-=%7F2^T}&x?Af<nC;l zj)vKT`}bYji7dyyuYFu%%-`YQ?fbJ47*3Q4qMe9BTbe4dYa$E!&I#-&4BXI8<QbH1 zI^Oyjb=4USvK%IU>RMS4+LmwInUzE!wc^N5{Uir=!Q_aoW;CB>B(vgVHaTyb&N0k~ zz#8Iw&HLwYg5<93e)BzfUWqaEzI0QVR?qe~n^rP&yG*O6PHT~){{vxe<%ykGK1eeQ z<auUgULHkZQWPNex1ZJpEYZ~D#X1U=pOdE$%m@|><{0;-N9u|65<A{(B7<nfL<Y(F ziS*Tp^b;q_vc$Fm*9MlKI?zhIII;3D_X7~bKJ>%wC-SG^4C4UNJ&cI%^ST*rd7*u3 zKTYzJ=?qRjk4LmhO=(SwVwk~-#4$+4Y(B-Qe8b84cyz+==$0gKnX=w>NutncUy>;7 zK-+=3Bv~8<j_2i8VS~)_L-ZGL?81rz+s(qn@v>-#C1EUJQ;yMiZ4-up&XLU~HE~x= zD!I94Qo|<02+`{>v;ZP+%P#`oN<zoBLg4sC5<{3JJ1~s=VYGy!zpt89PT`G5H<-vI zPF!cRBQ5q<9BFaTVJahaDnkf-HxC@k&5)$v0|2eq2A&mUPMqXvWP7f+{Zx*@=Q%8y zne62E+1d$hc|kX=zt5m{UcE4KHapVdWW~0RlMdTHR;M+I?KDk6V%c64BH7E1t=RV+ zE6j7-kJ21?DcpWq!30eK&ahidr<P^gc3@fMXu7zt7GU(jvPM`HI9XgQ$LXNUtZ(<d zO%^5a0&he4EpdbPRY~0FyR9?mkf6WB#nJX*`Z}}`mqH^R&I%RS-OSlla|h`~=y&VI zA#H%IeNa6vq)^cfxF2^muotx*o%FSyRGHY;8y`r`<Mzd<`l~TJ<b$^PtGicCa`WZZ z{UE(93}nw10?Ig6+XsZ!>-pewQH`vdIxmcubU^qjJz=qUS2ZB<)M}c^m2SH3?QhOK zZgbu?&3VTioqycX1zR`gAGf*SxXp#zG#75&T-0c;yZpaJbMZFK#alO*v^Te@z;}TJ z+v)tOi?p{qw6wQUu$bIP>+5CE1rqX+&_15s-k7V}!9iwmy#%A5>&bB6ZUs(!T8W`@ z81Qz5sczd_s%AJ*e#?u+^d--6{LR1U?){5i$1iTW#Suv`w_1lyu2cuO+3>Nh{`LcR zqs=E-_ahxm)6MpG?W*p$iG_s`XJ=14v8EC2UQ`;dn`U#<wbleJuy8_ynNDz{m;t~O zpnjWn9Bq{cHUPfs1(_9QDYX1Na;!LT94iW(Jj(MR26-xWh1!7X5)hvM;9hxEF5sB8 zazYN&0kQLx7Xy0n-e0c|IS2=U0>l6fA}UhL&jJTqXVGvXJM)|%%u^q%97L=Rg2($V zCr2HK>-$*}c%J1rj)%JM+g6grfdyPAcG57lW7l3e5KkS*WSYa{E`}TCNghOzm4;Db z`MAn;0*cIXfCs!ZhiT+{E60#%$3XDKk+85BW*@GOG!o#1L4jS@VGQv|aw~;lYK4WJ z#98d-XjN8@Bx)bYF~{MhzHg@>N1GydEI+heE6%de3ejj>C&)cJTfN5v?KqCSH~hSS zMG<+H7sY6AWMOQ@PU2cFA~@Ie(GrElHhW_}9ZwhVIAaJLH%SxEwrr3gMg@}y(U#*Q z=9_y(7=}p{2P?<m=(%kPJ{@TUksE*_w`~j9n8Bu5ZpB5BTS@LEcH#SB9%ZXW5Hv<W zst(NIc#4LAVUgK5P9zZHLyyV}0vLFK<z-onc1Q*+5aofD6(G-CKedB!<qq*<?WjPW z-`tQg=ztWRiJ6bRK(>!7MCBP;H$O?!H1Wf5^-&VGory=?^>JiJkq2VSgIGR6vbu#j zkC;H>`VmONFo4l!yS_^0Ioh5{JkQHwL|vH&bIW&A2c7B;8jVx9p__YoY&$E*U~4(o z5eJaO=%|2&<>fYB1UI!3JQQfhj_cVVcZ+B<TB2<0$R|jWLg*&3YekOlTYlnV?wbND zOM@beGRG-^yBRIfVbH{Y<BS31HgIeww~7b^mJiT*2vRS!(!|cQIE~Xh^EMN838z|4 zC<rTMTgMm(nsiXa=;Q!}7a{(X1y&5)*ecMT_K`dvCTYHMAg*>(9CIy@4CQ$+Kz+7j zbZP`iVI_ebS)rSyw(CNW=5RClkW7CDCK((ZaVc^q^NJ`*EYxKi@uw)Z(#%2fVG$rP zH%y?D*xSj6$CRz04`MI&Tq|_Y8u%XC6fy7-RoO0pt{;cK>ufd(eo`EhreKr=K?;4e z4{RhMrVy=#9om)$9L#1NKXrr6SS_KWgY0hdQFRoH3pj=kQZQc(Kj=E@iyN4H;e`E{ zyfQ$4gAwaw$pMn_3E2AV0+b(04v_37+9zifh6H}lqs51G4_?603_s%IM*{65CB$Yi z{7^tKp0y25FL6l|{8WgainLFSVTpaZr1w$##~5BTekyLCjdTu?v^rmoQ}Vr}z1egQ zCi!r3UKM`BdWGgWkP#mU>gHrgo^{%1g(B;dr(6nYtsO8JPU_E*b?~(hE~e8_d0-+f z;U^yRB++Ohnj*x$krxKEW(1gDsr2VCp5A<gSst|n8*5)^(5t#c^tr}v@o8}r&7+*M zbwo>_5UDZLtTcT}AjU|u;pjsOEykJ+#|LmeJRCEk)<FmQX^}=qg>dmP9eY+<B)|&8 z7zwa$fzF1N$8-mla?tJ<p6}oQ$)s>H%P){FlqMk(HFC$c(TSJ)AX(Y{o0@kw36N9~ zdX^1*$MSO<$$EBfTQLOCh8YYB9x2;x-ZhkU$nJYc5_JI5HxYd#mLK{B5+!0_x!Bh1 zgarugaGln+?#w!5|NS6J{V>arrr@If+fIyT9?RQa=Hr-r-|@0_wx+cV=a2&k!W<?Z z($s(-;Ye$&<@kAq#L^InbSdy*u$u7K9Hs~DJ5nV*8}&W#vDv~4eJc);o>OFqq`bt1 zf$y&~f_FZ74mpr8Mp~KYXIAXsYU6eS5%Y`^3wni{!`MlnpKq}Um3VZ}z9*@d+Ma`U zKu{n8iX$sd62wDo&yRz^O~N2tr(&*`P!BnRD34v>rARXi@D@m-9BF2@Z#kJAWoTK_ z0Ioi|)`^{m?0g)A5b;mc`65RQC`CGQf_H<LXO8VSLEvXAy>e^B?}K(di6FECq&&Jf zGa*AXl*WE!;SuG)4l^g;hW8)s>`D?hMAD9LWe`ujhvc3#EgZ|qBRhx@_4unTT{j|> z)1DW0>V`#Z<xzn2-#kna#RJ=l96NE-JcG$rM8ma{Lv|gcdFqAGvy#jWu$wJJB3l8` z@fAcMN<osO{w9K&q>&!7<7r?QeqaYy=3@c5h#hoTp?!-4hsg5-Sa_bZ$($>C!a=)U z#5k2WwOrRh<btz@;w-Q&$15Nb>mv|mtLJ>J_I1eK=SdbsQGhmQ;Ce_i0Z7&<;==Mk zoP-c&z85%~9ImPwcgVg&fV~=7g4lWNB5E&OD|MaF@=_#OqMx>xuQXJxQc@4u@4`X+ zB}Dp@i#8`Vup!0Di+sf25r=d$8^m_8&S=|F!9VCQ{K(GT49PZL7-2tbhNd5l1&KDk z6We+0!F&_KZ`x-L+WRO<6NqHVsDNk@GMu88#DN8oRt3{Mg0T&@kZWqY)<HX;Bh4_* zft7>AN0je)I4lHdhX4kVQxu_B_?swJT4URu2Ee)Spw0zRH5CxpNUO@M9Hw|bct}An zg2-|`bP`8#;(CeKNmSrXo#3X>pvqykk6~~leCq(xEREdIPb|dubDY7CWHlSO7Si8? zI0FzA*=k0FWHr_Z?K=ChpUzJfNKqKzjB>q^A$LKZ*tP{jfX*-AdlnKeT`SCjz=5$3 zl5Ay&k?TCT#~L}ojo`9?B`&Ibl-?H+;`T)vBDol`f8WRF7t(sIICrBYMFKPc{zkno zAk0&ceb`6uapm3tP370y)qKZukZ5MbUW~S}k7hkhVSuA@?80^s3oVj$GJd8sbaJs6 zk8lwZh{KGJF$x!*S~f(q<A4ZhDuo@P<1EarEC<kya-^-KaGmVd7)1d_qhl=tj-vA% zH@Cbf4>A8CkS4wrq^{?L1@?8P>uE?C04^MVjL!QWEaDuUUO|RL2y9fafa|%I6QyWj z0w)4Vuug!})6lW#C-Bl7$(^ZHxHvY&PJoq$L1=|J^bzm({5&eQ8v(v>j1fdG2wadM zhL26OKFE<Q=Ot)c=SbPj3UrQz`F10?gzzX^Ad2$<NzD=30@%iexRRA7F0=y2u>l%D z;D^CFX-GeUF=DP4#~4B!B1xeLLkq$pz`HoJ;uL9QVFog!C)n8Dv)v+GK`=kY2tWp- zI4F=9;b0SCgoAxC$WjU&5Q4<P84T;f2iiw4LOR*eb_CkM5OF5#F-nnInZz-U#m+34 zfHXoUHHg+o*Pdo5IMiu5K1TO5fTe2}ZHF{OOf^bv3(vm=?HuhCq#oF@w@$Ncbicfq z&i5kHp=m(>>Ni0egdP&uEXTJUY^^1M`-PPiv4ak2-$guiohVH|&sw2U^Z--QfxC_< zBL}hXSc#8qS4D=@8Kl`DEg(d@!o${xa2?6WT`!iS(f)c);d${|4p^aXeKv~|r!EHP z@jN8X`Cb?xWhMh?1t3m5u;U<IDNF>MbTkdh3Kd<N6PDL>%rW|r<GP-Y-Ky9&jTC*v zs8Xb`WoYnufh3V&WiY1itr#N-XgtArvNtib&`+TsVGC+nU>@v13!u?j5pWYPDDtrI zR*K}lzjkN@D&{7}7KJvVz#Ldk>Yzg&N#|A!5m)mP2hnO!*lRq3{wmTW72(N$ZeU~} z4P1zISP!6`=O;)MORyZ7rKuOA-ROZdTo*jwIx?=A^M7w(d`0GD=+r6DA!;Mum}fRp z6w$8B{US<ouSlcSywML<j1Najo?rt|cDOA!u24c%LBWr+6nL=((K=av2y82kkq{la zw&%M^4C5f~B=e}PQCghC^I^Fsw+n0G(-2;+Tr4L*XMj@>%OYMZbPOPk%(Ji;!_IsV zC9Aaom4%26%8Rvtpgiu_T(n+ZW+xWR(81<MNopl|iZ$oRcB8-xV&HAULZq=Bl7*}t zneS$gqTd$1V{#@NptXI7EG&_S2tY_&DJ<H!&bnSt!MBg<ie77fYbIg-HrBtRnz za<qDGoW?OIAYAQkP?@7NvcogSwc`ttG)_`Kvm6_z8KFPX8X_$!0s+hbDEt_#&YW_( z?zeea9(Q0x5&E`^y+ep33nYdlSPMx!m;|ot<EXH$8OdQ~k+owBLnlrB0-b1XSRf|p z1Ej43&kDoH4Lq3Hep+lICQD;Gj+nfDj8W`oFv!?J03*+e-PpG5&<!I@tX>gpA|`9r z??7Vm+Mz`t#V0q!flRjL`+k4~F6=$a<1EOWB=pw|E$F)E4m7w{fy8l~$OlLQapJ_q z(3LLs<%CG}hrU<jFih-VUD#ZvYmE+zh7Jt~F3Zuzgm_|>xM^a=NQuVQnAA$4>sc<e zQx9aJ3xafA*4c_N9Yzdk9ZeABc>=u%3;T%n(lA2jS>#)8m_>P9IIwV6pVW5SXcpk} zbTS=Z4Uoh*Cgy|V4XOZ6S_F<2IuHXTV?iVZkySV#3Xyu|ByqS71VcY4elj0*IALnr zIodE*mf=i!kV7kRKx_qQ_xN_0A&I|O=b&CeaYr3XoCZOPWOgfpKB70IOIm2xL{<_d z9%AQi3V^fSScpV;)UhOPg1YWz*i(%?j$UrX5NCN6z;kdkJR-X7$n22PZToOCkYx@! zTvKSHU4vH10#Shv+^`6c@Z@GL+>RB<BE(VGD34*}qT>|_d)WBwq=5wzAIIK8FV6hf zhcMo5P~r(RhvhMkRfvSy&`%;O3S9@O9=T)1Q5ITW?%QbuGB+vgPOfhKp!Ou0)8Z@i zuHL$fV?!etTVar59FqX)8V;`75NCO6r*^g`(gWv4l16A#vZzB_CxYEk1{1~sl0uOj zkh_Re`w*QuiI;m8jDkEWQV3F5tYZe^U}`OwWO>ep*<v||M;Qu|rIQF_OXN28g5wY( zfRsNAk}QZca`JesOdt+LRcm<&P&&=8jxwOENF(A#$TMQ`b5jc-In4r@=Vn0)GJkbk z3$}}i0}|Cep-A*m#*$~TQ>2b>LAO8$s1s&5Km^G)Y3@cT4844H`k1<Dbn4yF7N&q{ znukeY1sRfO{1{*#X8<fO$TKHPL0n{O(LkCDL-aV>HvV&eBf)>;P~ufQEeT?z*rZmL zXNV1W=&#sE3*|s3D!eFlyfxTN4j&O~tQyk+$gIP79OxBsXvdc4he)zPdN&4mfL4Kc z8yb!8WVydC=SCinLU4N}wGIOcK!~-P07=hyfzTO)u{IoRD**vS5_JZm%@*i@OuEB( zK#?Y~8x>ZVV*$r2LL^H&Na684+si=VI|b-=?(p#(mQ8mUQdShu%ksnu@(^(aA7dC| zn3pg^dQjm7P7?U*#2a}?RL?ntM9^V4NOQ~V2rUi`?T*ntiroaUgtUOUoyJk>MVk#r zWC=&-a7CU1Xrpa|BxryHodSDgA{z;xVV1!#&EnO<r|W4~XMa>S!f>9e0!2MLi7=8@ znnD*z1Bg4vDX^`?&O*=6!XWU2^#j1IobdR{ea1$tAAlEwz=LjRA&m^j_=PMHyNI1Q z3_+F_YgQmp2Y~dK#T+p&m>+Q%VHzZMjCQyiCwLe|fy7L-IlLkWpi?A?55c<d8hIF` z;NJk({yWYHfNuvbM$d8s9P65=Xe-#zwjw|CZ9lMKuo|MKIfDHL2oZ4s{e3lRF~;PG zHDSdOVn2`XMo8oLBF}Oi7ipMD>Vee>wNCWl9+iSg8z-c;&oMfl!rb#>qyT$3EDkBa zh;)#$Xys87f!Ox)HC`$lWGr@^lp~se;ZbHiE!+SkwufZG5UITo?ErwpU<;-haGX4f z*Cg`)|LlEhliNm?<^B{#ed+EAmqfhyD8)vsEZNhVk;@)E?DZ}-7$6B&VN4P<L8_#7 z)PLW+`A#75Bmja{*w|^SKp>IG$H|j7@4d&EsFknJlaHi7lXS}y&}o{EZ^CJ=v4WgT zwtYRBAhH2UX=pp*Pt1AllqD~r?<VhhQP(1t`vGHfxmX%t4|dSBwT+2?J#l>JdOAVh zg1%wo`13s1T-^sy6}(oyVQx-+&!1S=t1(XT44EJSK@mq98P<Ga=B;0j{u^rBTtLK= z-r>Qt?1?pZ(EiQ<O-VRLAEAW{U7Q-`bh<F-i#)uc$*|xZB>!fkH69Pgh6#((*z~}6 z)v~WqP0@aJIW?A38_~Dy!=8AJYm2x}t+6*+de&$H&gY{A33d0J#RBZ@Q*S<8S_><4 zbQmMG#t=c+XoVNcxjjUSJ%U5<6<OM7N{$e@nU7YUVO!(jB0J*6n5Y%5FEl*0%KmaP zf!uW0-V{*8^H$gMr8m9y#?yu2`QBunonF?M#>O!T6|AGqI8$GGaNuNV&PS-Nr`7_Y zwgVH;K=g5L8p|Qt8840TGH*DOQ%VS|qo&q6s=Z&XW*cv}3$}MDu2N%TK6aezxo54e z?GdW;WI6V)rz<p|(TYDEJGoJf=p8TUI}}rK8};{$g@fqS6jJs;boF$E;2P0IH1v%H zT9StL(#Rt$a{al@p^eHfM&5EhnykRr0yX|*=v{l>6iw6VXg>D*v16{RybY{edG%!Y z;N3-Se8tL~n`n<Z1!q=ZEW-&}^$i<sUY8TcUJO^GRc?&3{1q`XNgba+Pm^|7Pg0w} zem?Za*5rEV%+Y>hWg~3&mx#R})@x42<As@@I*h#I#g197YRtcqSN-{Nyc~NDh{!{( z+v&tZz~dQ^*3(}so$)9yvNu+Jk~O)$>O)A$;&{MPAo^4T6UzW&?hpr6OjeWYxn&{{ z^2UgFITK^-8+nn*>?NqNYW45qbLvaD@a9Lf6HD<mnxehw683mgh)tWqLTh>8+`X7C z9S3b_ja43yvUj}@@2YJUAHriIqGcZ*4}SFG-(KK<%;1L``XHQK;s_Py?<55`r_%*^ z34RlPKDs^zXRUg-s!U8w_S~`Y*q$3$%6WP{A6kp+1!UCl=M#H6AD^enL|!0F*&CVD z)p#{REPcGP(FS$t!xtb5Y>dZ~>2PID7S<|{nGs)i^oAGSHr#&OgvY4)iaRUR<V#}+ z+yKqXG1{1-eV%u{vZpH((PPJ3j`Ofls(9VP1lg6J+Rn=HS981trL+=lrLUKk?JXDf zVmV!vsr>P94;5H|jB_28hvBC`tOJ|H!X8~OK-C7dKO#1Bb9{}MusO7b=3+jc<N-P! z+hj67+=sJhw)G#Ht+eATM+<*~mQCy{VS=;o(gxWM$QM5cp@_U8E?3$+Ebf=#U88k2 ztm(vFpq8C1U{g98`G|Ho3q*g>n(8kRzAUXgIFagHV=AiDDx02V`4*%rgPanm%8=3q zEt%kJJwdyr`6}CS2o_4I%F3x{qZPMoe>j}`t7~uQz*fUwUVDoP8Xt?*Vli3Gr=x5S zJ1_u96<_VchAV6j5%E~SrecLFJYOJ2H8rm-bA@(Gj)^uIr78?!@y%8k?Kw@fnt;n6 zEROaPmdWY0v4BK1;|1C!O-od`wiC(J3Ogp+7?`8s^}_J6u@;=*P}TkEaz6JA-(Kb9 z_EM)G)xtqb%xvd}yLxuesg)lO(Qes7i_6G>DD0&(LgN{&k@J-`U(P3E%bQzy=FTK` z%=Z4G>6wTCdbF}UG@Kpa+U5`~kT9^lmF+u|@pS0h^Sos-vAl%ab+B!=#XUro(7tSV zy_%!OwtXOfBRHb-tmVp>j~ve)l|XCQp9NnUt}+BljHX}?$Cl+G$SgS1>*ds#tVYgo zH8w}37MR-h^r=-|j^`kR1YxK)s<(|`&x4E`OKWMORUM5Me^Fu`zgve<gmz+$*4a0P z<K@yauYChz)s3-nJ@>6Sgba+Ai=kmUer^iqWHU!?OvcrCK1B0wat*p{AdES=o<elM zYB^s(UMpiccS@}1iVdqw#c7Sv)*mr+oN}0+zs#tAinoi`U*Qh=Iz~5;whg;bcsC~f z8;rD9)M^Wd;<Ja~a6gNVn++Z((G6tX!|#sZF<cC!{v@g1<bOXtHu`~d9Hf1kVe*V_ zv$SuV(oZCrqO_kj=B3aZ#4K<2^CU@f<>T--7|gqedh#ScjW`MTDPJZ1%K!QJh{|Pt zYRqXcS+GOTkpN*pp1;$F0{>b3yWzIlhG`m0@+VQsq#5c6V5n33-e389f4lJ0hXX$V zQ2%Q96M;Tu4RzI(uynWCqy#!3=o!IjUleSkgNN=~#y^Eh644eH$4=6R{fvXT-|mk) zZ|%N85RLu^pxgZcKSci`e|&;3gb?DtNKgKcfBPQ<=Vv?Ao59zsSGUl|4*msI3H8@( z;Vte_Q_RVlXyc#OVfe>!H$$reYZmI5j@kpQ1*kpZ<@;ZEIG>P*T9*MnYUU-AfkCfa zdvkxyR9<*@5HS~2)^2^o3LHeT#lS&+Q1o%{QGF(Iii6&{@%MNBl72XU@$lxaNB(hW z;4ZOr82vbdF8Jst$Os8(7!Vk$Ka+2#eZjtc0&Ph@Mf!*M9`)ad2JQVXL4<1@p*me% zk<TNeOu9CKr?z(sG+#A;LhDlN_N>Tta7kk?o*nkyHd^Cmn|Z61zrc2&f;aI#R=b-t zE4hy9z(lk7I0*Lv>OOD%X6-@r3pwtgqm~@ivH|;AJW9Ir-h-yuM|`g0@)tfS2ZD3u z(N9Yu|9OpXLnGm^57+-B<<MzOog;Ncq)(ua?4d_KT-ZKszX>ANt-yAfiO}B<xby+p zERiurZ=c<vb_NqaDxH0^G~H>}!<-AYe?-za9+Rsuu+TVV`6q8*@mWA&QYj2iZ&Us` zTq%>DUZvypmd-Tz!HfLa!JDsXDW*y15gjf|@8HS5)B1_wB>hB6Oguqqz{VZ<Va};I zAkuyKFykeHxtshv5lwOR>Svq+LhjCwhkdZU`y|G*`$>AoPu?D3+0~5&Ba6NVKR`#a zG#tq%d;7a1X4O(>49OVZ(S9b^#TcQz#1{f-nk)Snq1VD#kbifi-w_7xy|w(St(1>_ z9AZd!+y}AoL7MAsAEM9JLn$v9-!yBYW#O%F5^pWh!h&*ci9;%BOEOOfFx&_96QLvM za3!4K7|if+#%D%!IJ4Divq3|PQyVls(Nc0?1C5rlM$?+C4Kw<7xu%o&J>=*vwizZZ zenX&P26q!svDvB#hYNw)1{MXJC8iB95ZF)Tos06=y4#0ef+Yb(?wE8%P4B$Jq#lOm zYKH6JE+HKhe)0EvRIL_?51Ff)I$Sl*h|s9lQ6jN%fi_~qD6u|pg2WgEAxJWza_DnQ zKU(aA9qC3QS%>+^pT~eoFOKOXR6&^N(ZTRz)B<+{7fm!{KS+2(^%B8_%s(8^P>Q%N zqWC`YAw7TV3V+L*2`OwYJ{5n2C)rH=9+ibJ{4AH0LhfSJOj~ZEz>neyg&aCgp~v+! zF^l8f_FV4LxE#C~dVYXM^*|ZbJ+teXT^-DhWKY4&P6MEI)4a-{E3-bDBjrq-4-2|< z+`Pr<8F~;4Av1<ngxpQi%w4=BlPp@0{FCr9%I6;Z;O_pw_#Enb(QAa#DMqnTl4Xr4 z$pVa>2Wz}2b&6!ch6DA&F-rXLENB)}fy=a#2?B%f<+GbVn50oo!bWCIVx{26(~M() z)i`6#{1~h$O29WKOZXMt^}V<W9>nOF``~+Ym-HYUtb}mPCpaK?-6hzZEaFG}0RGUW zzxEHf{~%w5ax2_L?thDKB2PUwZvh-D7C>B?x(t9eJF6kzeZ8VB0U0!3;+MWe5Prnx z8!z#jUv9iDs`Sgh@L!KFMT1RA8hTy^sI?U;gEqatCETve+r=}`&q2gXa0=I-J=Bxu zmdG=zl7O6^E%$7>7CY!zux0FmQMVbi;NmIMVR=zL6CdWed$M^kB%0D3e7zwL%Z%6} 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 X5Is#!fyJk;0`LC=TWA8oj2#&OfnE5O diff --git a/web/modules/contrib/media_entity/tests/src/Functional/CoreMediaUpdatePathTest.php b/web/modules/contrib/media_entity/tests/src/Functional/CoreMediaUpdatePathTest.php deleted file mode 100644 index 2ab9adfd0..000000000 --- a/web/modules/contrib/media_entity/tests/src/Functional/CoreMediaUpdatePathTest.php +++ /dev/null @@ -1,149 +0,0 @@ -<?php - -namespace Drupal\Tests\media_entity\Functional; - -use Drupal\Core\Config\Entity\Query\QueryFactory; -use Drupal\FunctionalTests\Update\UpdatePathTestBase; - -/** - * @group media_entity - */ -class CoreMediaUpdatePathTest extends UpdatePathTestBase { - - /** - * {@inheritdoc} - */ - protected function setDatabaseDumpFiles() { - $this->databaseDumpFiles = [ - __DIR__ . '/../../fixtures/drupal-8.4.0-media-entity.php.gz', - ]; - } - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // All this can be removed when #2877383 lands. - $this->config('system.action.media_delete_action')->delete(); - $this->config('system.action.media_publish_action')->delete(); - $this->config('system.action.media_save_action')->delete(); - $this->config('system.action.media_unpublish_action')->delete(); - - $this->config('views.view.media') - ->clear('display.default.display_options.fields.media_bulk_form') - ->save(); - } - - public function testUpdatePath() { - $icon_base_uri = $this->config('media_entity.settings')->get('icon_base'); - - $this->runUpdates(); - $assert = $this->assertSession(); - - // As with all translatable, versionable content entity types, media - // entities should have the revision_translation_affected base field. - // This may have been created during the update path by system_update_8402, - // so we should check for it here. - /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */ - $field_manager = $this->container->get('entity_field.manager'); - $this->assertArrayHasKey('revision_translation_affected', $field_manager->getBaseFieldDefinitions('media')); - $field_manager->clearCachedFieldDefinitions(); - - $this->drupalLogin($this->rootUser); - $this->drupalGet('/admin/modules'); - $assert->checkboxNotChecked('modules[media_entity_document][enable]'); - $assert->checkboxNotChecked('modules[media_entity_image][enable]'); - $assert->checkboxNotChecked('modules[media_entity][enable]'); - $assert->checkboxChecked('modules[media_entity_generic][enable]'); - // Media is not currently displayed on the Modules page. - $this->assertArrayHasKey('media', $this->config('core.extension')->get('module')); - - $this->drupalGet('/admin/structure/media/manage/file'); - $assert->statusCodeEquals(200); - $assert->fieldValueEquals('source', 'file'); - $assert->pageTextContains('File field is used to store the essential information'); - - $this->drupalGet('/admin/structure/media/manage/image'); - $assert->statusCodeEquals(200); - $assert->fieldValueEquals('source', 'image'); - $assert->pageTextContains('Image field is used to store the essential information'); - - $this->drupalGet('/admin/structure/media/manage/generic'); - $assert->statusCodeEquals(200); - $assert->fieldValueEquals('source', 'generic'); - $assert->pageTextContains('Generic media field is used to store the essential information'); - - $this->assertFrontPageMedia('Image 3', 'main img'); - $this->assertFrontPageMedia('Generic 1', 'main img[src *= "/media-icons/generic/generic.png"]'); - $this->assertFrontPageMedia('File 2', 'main img[src *= "/media-icons/generic/document.png"]'); - $this->assertFrontPageMedia('File 3', 'main img[src *= "/media-icons/generic/document.png"]'); - $this->assertFrontPageMedia('Image 1', 'main img'); - $this->assertFrontPageMedia('Generic 3', 'main img[src *= "/media-icons/generic/generic.png"]'); - - // Assert that Media Entity's config is migrated. - $this->assertTrue($this->config('media_entity.settings')->isNew()); - $this->assertEquals($icon_base_uri, $this->config('media.settings')->get('icon_base_uri')); - $this->assertEmpty( - $this->container->get('config.factory')->listAll('media_entity.bundle') - ); - - $this->activateModule(); - /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ - $storage = $this->container - ->get('entity_type.manager') - ->getStorage('media_type'); - - foreach (['file', 'image', 'generic'] as $type) { - $config = $this->config("media.type.$type"); - $this->assertFalse($config->isNew()); - $this->assertNull($config->get('type')); - $this->assertNull($config->get('type_configuration')); - $this->assertInternalType('string', $config->get('source')); - $this->assertInternalType('array', $config->get('source_configuration')); - $this->assertInternalType('string', $config->get('source_configuration.source_field')); - - // Ensure that the media type can be queried by UUID. - $uuid = $config->get('uuid'); - $this->assertNotEmpty($uuid); - $result = $storage->getQuery()->condition('uuid', $uuid)->execute(); - $this->assertEquals($result[$type], $type); - } - - // The UUID map for legacy media bundles should be cleared out. - $old_uuid_map = $this->container - ->get('keyvalue') - ->get(QueryFactory::CONFIG_LOOKUP_PREFIX . 'media_bundle') - ->getAll(); - $this->assertEmpty($old_uuid_map); - } - - protected function assertFrontPageMedia($link, $assert_selectors) { - $this->drupalGet('<front>'); - $this->clickLink($link); - - $assert = $this->assertSession(); - foreach ((array) $assert_selectors as $selector) { - $assert->elementExists('css', $selector); - } - } - - /** - * Activates the Media module in PHPUnit's memory space. - */ - protected function activateModule() { - $this->container - ->get('module_handler') - ->addModule('media', 'core/modules/media'); - - /** @var \ArrayObject $namespaces */ - $namespaces = $this->container->get('container.namespaces'); - $namespaces['Drupal\\media'] = 'core/modules/media/src'; - - $this->container - ->get('entity_type.manager') - ->clearCachedDefinitions(); - } - -} diff --git a/web/modules/contrib/media_entity_instagram/media_entity_instagram.info.yml b/web/modules/contrib/media_entity_instagram/media_entity_instagram.info.yml index dc692e99f..65482a410 100644 --- a/web/modules/contrib/media_entity_instagram/media_entity_instagram.info.yml +++ b/web/modules/contrib/media_entity_instagram/media_entity_instagram.info.yml @@ -6,8 +6,8 @@ package: Media dependencies: - drupal:media (>= 8.4.0) -# Information added by Drupal.org packaging script on 2017-09-29 -version: '8.x-2.0-alpha1' +# Information added by Drupal.org packaging script on 2018-09-18 +version: '8.x-2.0-alpha2' core: '8.x' project: 'media_entity_instagram' -datestamp: 1506671648 +datestamp: 1537264396 diff --git a/web/modules/contrib/media_entity_instagram/src/Plugin/Validation/Constraint/InstagramEmbedCodeConstraint.php b/web/modules/contrib/media_entity_instagram/src/Plugin/Validation/Constraint/InstagramEmbedCodeConstraint.php index 52642b86d..afbf469b5 100644 --- a/web/modules/contrib/media_entity_instagram/src/Plugin/Validation/Constraint/InstagramEmbedCodeConstraint.php +++ b/web/modules/contrib/media_entity_instagram/src/Plugin/Validation/Constraint/InstagramEmbedCodeConstraint.php @@ -7,7 +7,7 @@ use Symfony\Component\Validator\Constraint; /** * Check if a value is a valid Instagram embed code/URL. * - * @constraint( + * @Constraint( * id = "InstagramEmbedCode", * label = @Translation("Instagram embed code", context = "Validation"), * type = { "link", "string", "string_long" } diff --git a/web/modules/contrib/media_entity_instagram/tests/src/Functional/InstagramEmbedFormatterTest.php b/web/modules/contrib/media_entity_instagram/tests/src/Functional/InstagramEmbedFormatterTest.php index 9e2d99b3d..abf642635 100644 --- a/web/modules/contrib/media_entity_instagram/tests/src/Functional/InstagramEmbedFormatterTest.php +++ b/web/modules/contrib/media_entity_instagram/tests/src/Functional/InstagramEmbedFormatterTest.php @@ -2,9 +2,8 @@ namespace Drupal\Tests\media_entity_instagram\Functional; -use Drupal\media_entity\Entity\MediaBundle; -use Drupal\media_entity\Tests\MediaTestTrait; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\media\Functional\MediaFunctionalTestCreateMediaTypeTrait; /** * Tests for Instagram embed formatter. @@ -13,40 +12,24 @@ use Drupal\Tests\BrowserTestBase; */ class InstagramEmbedFormatterTest extends BrowserTestBase { + use MediaFunctionalTestCreateMediaTypeTrait; + /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ - public static $modules = [ + protected static $modules = [ 'media_entity_instagram', - 'media_entity', + 'media', 'node', 'field_ui', 'views_ui', 'block', ]; - use MediaTestTrait; - - /** - * The test user. - * - * @var \Drupal\User\UserInterface - */ - protected $adminUser; - - /** - * Media entity machine id. - * - * @var string - */ - protected $mediaId = 'instagram'; - /** - * The test media bundle. + * The test media type. * - * @var \Drupal\media_entity\MediaBundleInterface + * @var \Drupal\media\MediaTypeInterface */ protected $testBundle; @@ -56,12 +39,11 @@ class InstagramEmbedFormatterTest extends BrowserTestBase { protected function setUp() { parent::setUp(); - $bundle['bundle'] = $this->mediaId; - $this->testBundle = $this->drupalCreateMediaBundle($bundle, 'instagram'); + $this->testBundle = $this->createMediaType(['bundle' => 'instagram'], 'instagram'); $this->drupalPlaceBlock('local_actions_block'); - $this->adminUser = $this->drupalCreateUser([ + $account = $this->drupalCreateUser([ 'administer media', - 'administer media bundles', + 'administer media types', 'administer media fields', 'administer media form display', 'administer media display', @@ -75,135 +57,56 @@ class InstagramEmbedFormatterTest extends BrowserTestBase { // Other permissions. 'administer views', ]); - $this->drupalLogin($this->adminUser); + $this->drupalLogin($account); } /** * Tests adding and editing an instagram embed formatter. */ - public function testManageFieldFormatter() { + public function testFieldFormatter() { // Test and create one media bundle. $bundle = $this->testBundle; + $assert = $this->assertSession(); + // Assert that the media bundle has the expected values before proceeding. $this->drupalGet('admin/structure/media/manage/' . $bundle->id()); - $this->assertSession()->fieldValueEquals('label', $bundle->label()); - $this->assertSession()->fieldValueEquals('type', 'instagram'); - - // Add and save field settings (Embed code). - $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/fields/add-field'); - $edit_conf = [ - 'new_storage_type' => 'string_long', - 'label' => 'Embed code', - 'field_name' => 'embed_code', - ]; - $this->drupalPostForm(NULL, $edit_conf, t('Save and continue')); - $this->assertSession()->pageTextContains('These settings apply to the ' . $edit_conf['label'] . ' field everywhere it is used.'); - $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => '1', - ]; - $this->drupalPostForm(NULL, $edit, t('Save field settings')); - $this->assertSession()->pageTextContains('Updated field ' . $edit_conf['label'] . ' field settings.'); - - // Set the new field as required. - $edit = [ - 'required' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save settings')); - $this->assertSession()->pageTextContains('Saved ' . $edit_conf['label'] . ' configuration.'); - - // Assert that the new field configuration has been successfully saved. - $this->assertEquals('Embed code', $this->xpath('//*[@id="field-embed-code"]/td[1]')[0]->getText()); - $this->assertEquals('field_embed_code', $this->xpath('//*[@id="field-embed-code"]/td[2]')[0]->getText()); - $this->assertEquals('Text (plain, long)', $this->xpath('//*[@id="field-embed-code"]/td[3]')[0]->getText()); - - // Test if edit worked and if new field values have been saved as - // expected. - $this->drupalGet('admin/structure/media/manage/' . $bundle->id()); - $this->assertSession()->fieldValueEquals('label', $bundle->label()); - $this->assertSession()->fieldValueEquals('type', 'instagram'); - $this->assertSession()->fieldValueEquals('type_configuration[instagram][source_field]', 'field_embed_code'); - $this->drupalPostForm(NULL, NULL, t('Save media bundle')); - $this->assertSession()->pageTextContains('The media bundle ' . $bundle->label() . ' has been updated.'); - $this->assertSession()->pageTextContains($bundle->label()); - - $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/display'); + $assert->fieldValueEquals('label', $bundle->label()); + $assert->fieldValueEquals('source', 'instagram'); + $assert->pageTextContains('Instagram field is used to store the essential information about the media item.'); + $assert->buttonExists('Save')->press(); + $assert->pageTextContains('The media type ' . $bundle->label() . ' has been updated.'); + + entity_get_display('media', $bundle->id(), 'default') + ->setComponent('field_media_instagram', [ + 'label' => 'above', + 'type' => 'instagram_embed', + 'settings' => [ + 'hidecaption' => FALSE, + ], + ]) + ->save(); // Set and save the settings of the new field. - $edit = [ - 'fields[field_embed_code][label]' => 'above', - 'fields[field_embed_code][type]' => 'instagram_embed', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertSession()->pageTextContains('Your settings have been saved.'); - - // First set absolute size of the embed. - $this->submitForm([], 'field_embed_code_settings_edit'); - $edit = [ - 'fields[field_embed_code][settings_edit_form][settings][hidecaption]' => FALSE, - ]; - $this->submitForm($edit, 'field_embed_code_plugin_settings_update'); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertSession()->pageTextContains('Your settings have been saved.'); - $this->assertSession()->pageTextContains('Caption: Visible'); + $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/display'); + $assert->pageTextContains('Caption: Visible'); // Create and save the media with an instagram media code. $this->drupalGet('media/add/' . $bundle->id()); - // Example instagram from https://www.instagram.com/developer/embedding/ - $instagram = 'https://www.instagram.com/p/bNd86MSFv6/'; - - $edit = [ - 'name[0][value]' => 'My test instagram', - 'field_embed_code[0][value]' => $instagram, - ]; - $this->drupalPostForm(NULL, $edit, t('Save and publish')); + $assert->fieldExists('Name')->setValue('My test instagram'); + // Example instagram from https://www.instagram.com/developer/embedding + $assert->fieldExists('Instagram')->setValue('https://www.instagram.com/p/bNd86MSFv6/'); + $assert->buttonExists('Save')->press(); // Assert that the media has been successfully saved. - $this->assertSession()->pageTextContains('My test instagram'); - $this->assertSession()->pageTextContains('Embed code'); + $assert->pageTextContains('My test instagram'); + $assert->pageTextContains('Instagram'); // Assert that the formatter exists on this page and that it has absolute // size. - $this->assertFieldByXPath('//blockquote'); - $this->assertSession()->responseContains('platform.instagram.com/en_US/embeds.js'); - } - - /** - * Creates media bundle. - * - * @param array $values - * The media bundle values. - * @param string $type_name - * (optional) The media type provider plugin that is responsible for - * additional logic related to this media). - * - * @return \Drupal\Core\Entity\EntityInterface - * Returns newly created media bundle. - */ - protected function drupalCreateMediaBundle(array $values = [], $type_name = 'generic') { - if (!isset($values['bundle'])) { - $id = strtolower($this->randomMachineName()); - } - else { - $id = $values['bundle']; - } - $values += [ - 'id' => $id, - 'label' => $id, - 'type' => $type_name, - 'type_configuration' => [], - 'field_map' => [], - 'new_revision' => FALSE, - ]; - - $bundle = MediaBundle::create($values); - $status = $bundle->save(); - - $this->assertEquals(SAVED_NEW, $status, t('Created media bundle %bundle.', ['%bundle' => $bundle->id()])); - - return $bundle; + $assert->elementExists('css', 'blockquote'); + $assert->responseContains('platform.instagram.com/en_US/embeds.js'); } } diff --git a/web/modules/contrib/memcache/README.txt b/web/modules/contrib/memcache/README.txt index 6705434e0..de8b2ae6a 100644 --- a/web/modules/contrib/memcache/README.txt +++ b/web/modules/contrib/memcache/README.txt @@ -1,6 +1,6 @@ ## IMPORTANT NOTE ## -This file contains installation instructions for the 8.x-1.x version of the +This file contains installation instructions for the 8.x-2.x version of the Drupal Memcache module. Configuration differs between 8.x and 7.x versions of the module, so be sure to follow the 7.x instructions if you are configuring the 7.x-1.x version of this module! @@ -83,32 +83,6 @@ The bin/cluster/server model can be described as follows: - If a bin can not be found it will map to 'default'. -### Stampede Protection ### - -Memcache includes stampede protection for rebuilding expired and invalid cache -items. To enable stampede protection, add the following config in settings.php: - -$settings['memcache']['stampede_protection'] = TRUE; - -To avoid lock stampedes, it is important that you enable the memcache lock -implementation when enabling stampede protection -- enabling stampede protection -without enabling the Memcache lock implementation can cause worse performance. - -Only change the following values if you're sure you know what you're doing, -which requires reading the memcachie.inc code. - -The value passed to Drupal\Core\Lock\LockBackendInterface::wait(), defaults to 5: - $settings['memcache']['stampede_wait_time'] = 5; - -The maximum number of calls to Drupal\Core\Lock\LockBackendInterface::wait() due -to stampede protection during a single request, defaults to 3: - $settings['memcache']['stampede_wait_limit'] = 3; - -When adjusting these variables, be aware that: - - wait_time * wait_limit is designed to default to a number less than - standard web server timeouts (i.e. 15 seconds vs. apache's default of - 30 seconds). - ### Prefixing ### If you want to have multiple Drupal installations share memcached instances, @@ -117,7 +91,7 @@ config in settings.php: $settings['memcache']['key_prefix'] = 'something_unique'; -### Key Hash Algorithm +### Key Hash Algorithm ### Note: if the length of your prefix + key + bin combine to be more than 250 characters, they will be automatically hashed. Memcache only supports key @@ -155,20 +129,17 @@ the default in this case but could be set using: Memcache locks can be enabled through the services.yml file. - services: - # Replaces the default lock backend with a memcache implementation. - lock: - class: Drupal\Core\Lock\LockBackendInterface - factory: memcache.lock.factory:get - - # Replaces the default persistent lock backend with a memcache implementation. - lock.persistent: - class: Drupal\Core\Lock\LockBackendInterface - factory: memcache.lock.factory:getPersistent +services: + # Replaces the default lock backend with a memcache implementation. + lock: + class: Drupal\Core\Lock\LockBackendInterface + factory: memcache.lock.factory:get -## Cache Container on bootstrap ## +## Cache Container on bootstrap (with cache tags on database) ## By default Drupal starts the cache_container on the database, in order to override that you can use the following code on your settings.php file. Make sure that the $class_load->addPsr4 is poiting to the right location of memcache (on this case modules/contrib/memcache/src) +In this mode, the database is still bootstrapped so that cache tag invalidation can be handled. If you want to avoid database bootstrap, see the container definition in the next section instead. + $memcache_exists = class_exists('Memcache', FALSE); $memcached_exists = class_exists('Memcached', FALSE); if ($memcache_exists || $memcached_exists) { @@ -187,30 +158,89 @@ if ($memcache_exists || $memcached_exists) { 'class' => 'Drupal\Core\Site\Settings', 'factory' => 'Drupal\Core\Site\Settings::getInstance', ], - 'memcache.config' => [ - 'class' => 'Drupal\memcache\DrupalMemcacheConfig', + 'memcache.settings' => [ + 'class' => 'Drupal\memcache\MemcacheSettings', 'arguments' => ['@settings'], ], - 'memcache.backend.cache.factory' => [ - 'class' => 'Drupal\memcache\DrupalMemcacheFactory', - 'arguments' => ['@memcache.config'] + 'memcache.factory' => [ + 'class' => 'Drupal\memcache\Driver\MemcacheDriverFactory', + 'arguments' => ['@memcache.settings'], + ], + 'memcache.timestamp.invalidator.bin' => [ + 'class' => 'Drupal\memcache\Invalidator\MemcacheTimestampInvalidator', + # Adjust tolerance factor as appropriate when not running memcache on localhost. + 'arguments' => ['@memcache.factory', 'memcache_bin_timestamps', 0.001], ], 'memcache.backend.cache.container' => [ - 'class' => 'Drupal\memcache\DrupalMemcacheFactory', - 'factory' => ['@memcache.backend.cache.factory', 'get'], + 'class' => 'Drupal\memcache\DrupalMemcacheInterface', + 'factory' => ['@memcache.factory', 'get'], 'arguments' => ['container'], ], - 'lock.container' => [ - 'class' => 'Drupal\memcache\Lock\MemcacheLockBackend', - 'arguments' => ['container', '@memcache.backend.cache.container'], - ], 'cache_tags_provider.container' => [ 'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum', 'arguments' => ['@database'], ], 'cache.container' => [ 'class' => 'Drupal\memcache\MemcacheBackend', - 'arguments' => ['container', '@memcache.backend.cache.container', '@lock.container', '@memcache.config', '@cache_tags_provider.container'], + 'arguments' => ['container', '@memcache.backend.cache.container', '@cache_tags_provider.container', '@memcache.timestamp.invalidator.bin'], + ], + ], + ]; +} + +## Cache Container on bootstrap (pure memcache) ## +By default Drupal starts the cache_container on the database, in order to override that you can use the following code on your settings.php file. Make sure that the $class_load->addPsr4 is poiting to the right location of memcache (on this case modules/contrib/memcache/src) + +For this mode to work correctly, you must be using the overridden cache_tags.invalidator.checksum service. +See example.services.yml for the corresponding configuration. + +$memcache_exists = class_exists('Memcache', FALSE); +$memcached_exists = class_exists('Memcached', FALSE); +if ($memcache_exists || $memcached_exists) { + $class_loader->addPsr4('Drupal\\memcache\\', 'modules/contrib/memcache/src'); + + // Define custom bootstrap container definition to use Memcache for cache.container. + $settings['bootstrap_container_definition'] = [ + 'parameters' => [], + 'services' => [ + # Dependencies. + 'settings' => [ + 'class' => 'Drupal\Core\Site\Settings', + 'factory' => 'Drupal\Core\Site\Settings::getInstance', + ], + 'memcache.settings' => [ + 'class' => 'Drupal\memcache\MemcacheSettings', + 'arguments' => ['@settings'], + ], + 'memcache.factory' => [ + 'class' => 'Drupal\memcache\Driver\MemcacheDriverFactory', + 'arguments' => ['@memcache.settings'], + ], + 'memcache.timestamp.invalidator.bin' => [ + 'class' => 'Drupal\memcache\Invalidator\MemcacheTimestampInvalidator', + # Adjust tolerance factor as appropriate when not running memcache on localhost. + 'arguments' => ['@memcache.factory', 'memcache_bin_timestamps', 0.001], + ], + 'memcache.timestamp.invalidator.tag' => [ + 'class' => 'Drupal\memcache\Invalidator\MemcacheTimestampInvalidator', + # Remember to update your main service definition in sync with this! + # Adjust tolerance factor as appropriate when not running memcache on localhost. + 'arguments' => ['@memcache.factory', 'memcache_tag_timestamps', 0.001], + ], + 'memcache.backend.cache.container' => [ + 'class' => 'Drupal\memcache\DrupalMemcacheInterface', + 'factory' => ['@memcache.factory', 'get'], + # Actual cache bin to use for the container cache. + 'arguments' => ['container'], + ], + # Define a custom cache tags invalidator for the bootstrap container. + 'cache_tags_provider.container' => [ + 'class' => 'Drupal\memcache\Cache\TimestampCacheTagsChecksum', + 'arguments' => ['@memcache.timestamp.invalidator.tag'], + ], + 'cache.container' => [ + 'class' => 'Drupal\memcache\MemcacheBackend', + 'arguments' => ['container', '@memcache.backend.cache.container', '@cache_tags_provider.container', '@memcache.timestamp.invalidator.bin'], ], ], ]; @@ -254,14 +284,14 @@ default options (selected through performance testing). These options will be set unless overridden in settings.php. $settings['memcache']['options'] = [ - Memcached::OPT_COMPRESSION => FALSE, + Memcached::OPT_COMPRESSION => TRUE, Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT, ]; These are as follows: - * Turn off compression, as this takes more CPU cycles than it's worth for most - users + * Turn on compression, as this allows more data to be stored and in turn + should result in less evictions. * Turn on consistent distribution, which allows you to add/remove servers easily diff --git a/web/modules/contrib/memcache/example.services.yml b/web/modules/contrib/memcache/example.services.yml new file mode 100644 index 000000000..50e0636ab --- /dev/null +++ b/web/modules/contrib/memcache/example.services.yml @@ -0,0 +1,29 @@ +# This file contains example services overrides. +# +# Enable with this line in settings.php +# $settings['container_yamls'][] = 'modules/memcache/example.services.yml'; +# +# Or copy & paste the desired services into sites/default/services.yml. +# +# Note that the memcache module must be enabled for this to work. + +services: + # Timestamp invalidation service used for invalidation logic. + memcache.timestamp.invalidator.tag: + class: Drupal\memcache\Invalidator\MemcacheTimestampInvalidator + # Remember to use the same bin as the bootstrap container if you are using it! + # Adjust tolerance factor as appropriate when not running memcache on localhost. + arguments: ['@memcache.factory', 'memcache_tag_timestamps', 0.001] + + # Cache tag checksum backend. Used by memcache and most other cache backends + # to deal with cache tag invalidations. + cache_tags.invalidator.checksum: + class: Drupal\memcache\Cache\TimestampCacheTagsChecksum + arguments: ['@memcache.timestamp.invalidator.tag'] + tags: + - { name: cache_tags_invalidator } + + # Replaces the default lock backend with a memcache implementation. + lock: + class: Drupal\Core\Lock\LockBackendInterface + factory: ['@memcache.lock.factory', get] diff --git a/web/modules/contrib/memcache/memcache.info.yml b/web/modules/contrib/memcache/memcache.info.yml index c83a060f9..e35f56865 100644 --- a/web/modules/contrib/memcache/memcache.info.yml +++ b/web/modules/contrib/memcache/memcache.info.yml @@ -4,8 +4,8 @@ type: module package: 'Performance and scalability' # core: 8.x -# Information added by Drupal.org packaging script on 2017-10-18 -version: '8.x-2.0-alpha5' +# Information added by Drupal.org packaging script on 2018-10-26 +version: '8.x-2.0' core: '8.x' project: 'memcache' -datestamp: 1508351354 +datestamp: 1540546685 diff --git a/web/modules/contrib/memcache/memcache.install b/web/modules/contrib/memcache/memcache.install index 61d780d81..fd7158999 100644 --- a/web/modules/contrib/memcache/memcache.install +++ b/web/modules/contrib/memcache/memcache.install @@ -1,5 +1,10 @@ <?php +/** + * @file + * Install, update and uninstall hooks for the Memcache module. + */ + /** * Implements hook_requirements(). */ diff --git a/web/modules/contrib/memcache/memcache.services.yml b/web/modules/contrib/memcache/memcache.services.yml index 1e85ebde5..d523f10d7 100644 --- a/web/modules/contrib/memcache/memcache.services.yml +++ b/web/modules/contrib/memcache/memcache.services.yml @@ -1,13 +1,17 @@ services: - memcache.config: - class: Drupal\memcache\DrupalMemcacheConfig + memcache.settings: + class: Drupal\memcache\MemcacheSettings arguments: ['@settings'] memcache.factory: - class: Drupal\memcache\DrupalMemcacheFactory - arguments: ['@memcache.config'] + class: Drupal\memcache\Driver\MemcacheDriverFactory + arguments: ['@memcache.settings'] + memcache.timestamp.invalidator.bin: + class: Drupal\memcache\Invalidator\MemcacheTimestampInvalidator + # Override this service and adjust tolerance if necessary. + arguments: ['@memcache.factory', 'memcache_bin_timestamps', 0.001] cache.backend.memcache: class: Drupal\memcache\MemcacheBackendFactory - arguments: ['@lock', '@memcache.config', '@memcache.factory', '@cache_tags.invalidator.checksum'] + arguments: ['@memcache.factory', '@cache_tags.invalidator.checksum', '@memcache.timestamp.invalidator.bin'] memcache.lock.factory: class: Drupal\memcache\Lock\MemcacheLockFactory arguments: ['@memcache.factory'] diff --git a/web/modules/contrib/memcache/memcache_admin/config/install/memcache_admin.settings.yml b/web/modules/contrib/memcache/memcache_admin/config/install/memcache_admin.settings.yml new file mode 100644 index 000000000..ede8614b6 --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/config/install/memcache_admin.settings.yml @@ -0,0 +1 @@ +show_memcache_statistics: 0 diff --git a/web/modules/contrib/memcache/memcache_admin/memcache_admin.info.yml b/web/modules/contrib/memcache/memcache_admin/memcache_admin.info.yml new file mode 100644 index 000000000..5fe3f12bf --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/memcache_admin.info.yml @@ -0,0 +1,17 @@ +name: Memcache Admin +description: Adds a User Interface to monitor the Memcache for this site. +package: Performance and scalability + +type: module +# core: 8.x + +configure: memcache_admin.settings + +dependancies: + - memcache + +# Information added by Drupal.org packaging script on 2018-10-26 +version: '8.x-2.0' +core: '8.x' +project: 'memcache' +datestamp: 1540546685 diff --git a/web/modules/contrib/memcache/memcache_admin/memcache_admin.install b/web/modules/contrib/memcache/memcache_admin/memcache_admin.install new file mode 100644 index 000000000..2b412bbc8 --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/memcache_admin.install @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Update functions for memcache_admin. + */ + +/** + * Implements hook_uninstall(). + */ +function memcache_admin_uninstall() { + Drupal::configFactory()->getEditable('memcache_admin.settings')->clear('show_memcache_statistics')->save(); +} diff --git a/web/modules/contrib/memcache/memcache_admin/memcache_admin.links.menu.yml b/web/modules/contrib/memcache/memcache_admin/memcache_admin.links.menu.yml new file mode 100644 index 000000000..2967e043d --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/memcache_admin.links.menu.yml @@ -0,0 +1,11 @@ +memcache_admin.settings: + title: Memcache + description: Show memcache statistics at the bottom of each page. + parent: system.admin_config_system + route_name: memcache_admin.settings + +memcache_admin.reports: + title: Memcache statistics + description: View statistics for all configured memcache servers. + parent: system.admin_reports + route_name: memcache_admin.reports diff --git a/web/modules/contrib/memcache/memcache_admin/memcache_admin.permissions.yml b/web/modules/contrib/memcache/memcache_admin/memcache_admin.permissions.yml new file mode 100644 index 000000000..4913427e3 --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/memcache_admin.permissions.yml @@ -0,0 +1,6 @@ +access memcache statistics: + title: Access memcache statistics + +access slab cachedump: + title: Access cachedump of memcache slab + restrict access: true diff --git a/web/modules/contrib/memcache/memcache_admin/memcache_admin.routing.yml b/web/modules/contrib/memcache/memcache_admin/memcache_admin.routing.yml new file mode 100644 index 000000000..4711bb06f --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/memcache_admin.routing.yml @@ -0,0 +1,47 @@ +memcache_admin.settings: + path: /admin/config/system/memcache + defaults: + _form: Drupal\memcache_admin\Form\MemcacheAdminSettingsForm + _title: Memcache Admin Settings + requirements: + _permission: administer site configuration + +memcache_admin.reports: + path: /admin/reports/memcache + defaults: + _controller: Drupal\memcache_admin\Controller\MemcacheStatisticsController::statsTable + _title: Memcache Statistics + requirements: + _permission: access memcache statistics + +memcache_admin.reports_cluster: + path: '/admin/reports/memcache/{cluster}' + defaults: + _controller: Drupal\memcache_admin\Controller\MemcacheStatisticsController::statsTable + _title: Memcache Statistics - Bin + requirements: + _permission: access memcache statistics + +memcache_admin.reports_server: + path: '/admin/reports/memcache/{cluster}/{server}' + defaults: + _controller: Drupal\memcache_admin\Controller\MemcacheStatisticsController::statsTableRaw + _title: Memcache Statistics - Server + requirements: + _permission: access memcache statistics + +memcache_admin.reports_type: + path: '/admin/reports/memcache/{cluster}/{server}/{type}' + defaults: + _controller: Drupal\memcache_admin\Controller\MemcacheStatisticsController::statsTableRaw + _title: Memcache Statistics - Type + requirements: + _permission: access memcache statistics + +memcache_admin.reports_slab_dump: + path: '/admin/reports/memcache/{cluster}/{server}/{type}/cachedump/{slab}' + defaults: + _controller: Drupal\memcache_admin\Controller\MemcacheStatisticsController::statsTableRaw + _title: Memcache Statistics - Slab Cache + requirements: + _permission: access slab cachedump diff --git a/web/modules/contrib/memcache/memcache_admin/memcache_admin.services.yml b/web/modules/contrib/memcache/memcache_admin/memcache_admin.services.yml new file mode 100644 index 000000000..41de36ccf --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/memcache_admin.services.yml @@ -0,0 +1,5 @@ +services: + memcache_admin.display_statistics: + class: Drupal\memcache_admin\EventSubscriber\MemcacheAdminSubscriber + tags: + - { name: event_subscriber } diff --git a/web/modules/contrib/memcache/memcache_admin/src/Controller/MemcacheStatisticsController.php b/web/modules/contrib/memcache/memcache_admin/src/Controller/MemcacheStatisticsController.php new file mode 100644 index 000000000..08a6af7a1 --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/src/Controller/MemcacheStatisticsController.php @@ -0,0 +1,563 @@ +<?php + +namespace Drupal\memcache_admin\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Datetime\DateFormatter; +use Drupal\Core\Link; +use Drupal\Core\Url; +use Drupal\Component\Render\HtmlEscapedText; + +/** + * Memcache Statistics. + */ +class MemcacheStatisticsController extends ControllerBase { + + /** + * Callback for the Memcache Stats page. + * + * @param string $bin + * The bin name. + * + * @return string + * The page output. + */ + public function statsTable($bin = 'default') { + $output = []; + $servers = []; + + // Get the statistics. + $bin = $this->binMapping($bin); + /** @var $memcache \Drupal\memcache\DrupalMemcacheInterface */ + $memcache = \Drupal::service('memcache.factory')->get($bin, TRUE); + $stats = $memcache->stats($bin, 'default', TRUE); + + if (empty($stats[$bin])) { + + // Break this out to make drupal_set_message easier to read. + $additional_message = $this->t( + '@enable the memcache module', + [ + '@enable' => Link::fromTextAndUrl(t('enable'), Url::fromUri('base:/admin/modules', ['fragment' => 'edit-modules-performance-and-scalability'])), + ] + ); + if (\Drupal::moduleHandler()->moduleExists('memcache')) { + $additional_message = $this->t( + 'visit the Drupal admin @status page', + [ + '@status' => Link::fromTextAndUrl(t('status report'), Url::fromUri('base:/admin/reports/status')), + ] + ); + } + + // Failed to load statistics. Provide a useful error about where to get + // more information and help. + drupal_set_message( + t( + 'There may be a problem with your Memcache configuration. Please review @readme and :more for more information.', + [ + '@readme' => 'README.txt', + ':more' => $additional_message, + ] + ), + 'error' + ); + } + else { + if (count($stats[$bin])) { + $stats = $stats[$bin]; + $aggregate = array_pop($stats); + + if ($memcache->getMemcache() instanceof \Memcached) { + $version = t('Memcached v@version', ['@version' => phpversion('Memcached')]); + } + elseif ($memcache->getMemcache() instanceof \Memcache) { + $version = t('Memcache v@version', ['@version' => phpversion('Memcache')]); + } + else { + $version = t('Unknown'); + drupal_set_message(t('Failed to detect the memcache PECL extension.'), 'error'); + } + + foreach ($stats as $server => $statistics) { + if (empty($statistics['uptime'])) { + drupal_set_message(t('Failed to connect to server at :address.', [':address' => $server]), 'error'); + } + else { + $servers[] = $server; + + $data['server_overview'][$server] = t('v@version running @uptime', ['@version' => $statistics['version'], '@uptime' => \Drupal::service('date.formatter')->formatInterval($statistics['uptime'])]); + $data['server_pecl'][$server] = t('n/a'); + $data['server_time'][$server] = \Drupal::service('date.formatter')->format($statistics['time']); + $data['server_connections'][$server] = $this->statsConnections($statistics); + $data['cache_sets'][$server] = $this->statsSets($statistics); + $data['cache_gets'][$server] = $this->statsGets($statistics); + $data['cache_counters'][$server] = $this->statsCounters($statistics); + $data['cache_transfer'][$server] = $this->statsTransfer($statistics); + $data['cache_average'][$server] = $this->statsAverage($statistics); + $data['memory_available'][$server] = $this->statsMemory($statistics); + $data['memory_evictions'][$server] = number_format($statistics['evictions']); + } + } + } + + // Build a custom report array. + $report = [ + 'uptime' => [ + 'uptime' => [ + 'label' => t('Uptime'), + 'servers' => $data['server_overview'], + ], + 'extension' => [ + 'label' => t('PECL extension'), + 'servers' => [$servers[0] => $version], + ], + 'time' => [ + 'label' => t('Time'), + 'servers' => $data['server_time'], + ], + 'connections' => [ + 'label' => t('Connections'), + 'servers' => $data['server_connections'], + ], + ], + 'stats' => [], + 'memory' => [ + 'memory' => [ + 'label' => t('Available memory'), + 'servers' => $data['memory_available'], + ], + 'evictions' => [ + 'label' => t('Evictions'), + 'servers' => $data['memory_evictions'], + ], + ], + ]; + + // Don't display aggregate totals if there's only one server. + if (count($servers) > 1) { + $report['uptime']['uptime']['total'] = t('n/a'); + $report['uptime']['extension']['servers'] = $data['server_pecl']; + $report['uptime']['extension']['total'] = $version; + $report['uptime']['time']['total'] = t('n/a'); + $report['uptime']['connections']['total'] = $this->statsConnections($aggregate); + $report['memory']['memory']['total'] = $this->statsMemory($aggregate); + $report['memory']['evictions']['total'] = number_format($aggregate['evictions']); + } + + // Report on stats. + $stats = [ + 'sets' => t('Sets'), + 'gets' => t('Gets'), + 'counters' => t('Counters'), + 'transfer' => t('Transferred'), + 'average' => t('Per-connection average'), + ]; + + foreach ($stats as $type => $label) { + $report['stats'][$type] = [ + 'label' => $label, + 'servers' => $data["cache_{$type}"], + ]; + + if (count($servers) > 1) { + $func = 'stats' . ucfirst($type); + $report['stats'][$type]['total'] = $this->{$func}($aggregate); + } + } + + $output = $this->statsTablesOutput($bin, $servers, $report); + } + + return $output; + } + + /** + * Callback for the Memcache Stats page. + * + * @param string $cluster + * The Memcache cluster name. + * @param string $server + * The Memcache server name. + * @param string $type + * The type of statistics to retrieve when using the Memcache extension. + * + * @return string + * The page output. + */ + public function statsTableRaw($cluster, $server, $type = 'default') { + $cluster = $this->binMapping($cluster); + $server = str_replace('!', '/', $server); + + $slab = \Drupal::routeMatch()->getParameter('slab'); + $memcache = \Drupal::service('memcache.factory')->get($cluster, TRUE); + if ($type == 'slabs' && !empty($slab)) { + $stats = $memcache->stats($cluster, $slab, FALSE); + } + else { + $stats = $memcache->stats($cluster, $type, FALSE); + } + + // @codingStandardsIgnoreStart + // @todo - breadcrumb + // $breadcrumbs = [ + // l(t('Home'), NULL), + // l(t('Administer'), 'admin'), + // l(t('Reports'), 'admin/reports'), + // l(t('Memcache'), 'admin/reports/memcache'), + // l(t($bin), "admin/reports/memcache/$bin"), + // ]; + // if ($type == 'slabs' && arg(6) == 'cachedump' && user_access('access slab cachedump')) { + // $breadcrumbs[] = l($server, "admin/reports/memcache/$bin/$server"); + // $breadcrumbs[] = l(t('slabs'), "admin/reports/memcache/$bin/$server/$type"); + // } + // drupal_set_breadcrumb($breadcrumbs); + // @codingStandardsIgnoreEnd + if (isset($stats[$cluster][$server]) && is_array($stats[$cluster][$server]) && count($stats[$cluster][$server])) { + $output = $this->statsTablesRawOutput($cluster, $server, $stats[$cluster][$server], $type); + } + elseif ($type == 'slabs' && is_array($stats[$cluster]) && count($stats[$cluster])) { + $output = $this->statsTablesRawOutput($cluster, $server, $stats[$cluster], $type); + } + else { + $output = $this->statsTablesRawOutput($cluster, $server, [], $type); + drupal_set_message(t('No @type statistics for this bin.', ['@type' => $type])); + } + + return $output; + } + + /** + * Helper function, reverse map the memcache_bins variable. + */ + private function binMapping($bin = 'cache') { + $memcache = \Drupal::service('memcache.factory')->get(NULL, TRUE); + $memcache_bins = $memcache->getBins(); + + $bins = array_flip($memcache_bins); + if (isset($bins[$bin])) { + return $bins[$bin]; + } + else { + return $this->defaultBin($bin); + } + } + + /** + * Helper function. Returns the bin name. + */ + private function defaultBin($bin) { + if ($bin == 'default') { + return 'cache'; + } + + return $bin; + } + + /** + * Statistics report: format total and open connections. + */ + private function statsConnections($stats) { + return $this->t( + '@current open of @total total', + [ + '@current' => number_format($stats['curr_connections']), + '@total' => number_format($stats['total_connections']), + ] + ); + } + + /** + * Statistics report: calculate # of set cmds and total cmds. + */ + private function statsSets($stats) { + if (($stats['cmd_set'] + $stats['cmd_get']) == 0) { + $sets = 0; + } + else { + $sets = $stats['cmd_set'] / ($stats['cmd_set'] + $stats['cmd_get']) * 100; + } + if (empty($stats['uptime'])) { + $average = 0; + } + else { + $average = $sets / $stats['uptime']; + } + return $this->t( + '@average/s; @set sets (@sets%) of @total commands', + [ + '@average' => number_format($average, 2), + '@sets' => number_format($sets, 2), + '@set' => number_format($stats['cmd_set']), + '@total' => number_format($stats['cmd_set'] + $stats['cmd_get']), + ] + ); + } + + /** + * Statistics report: calculate # of get cmds, broken down by hits and misses. + */ + private function statsGets($stats) { + if (($stats['cmd_set'] + $stats['cmd_get']) == 0) { + $gets = 0; + } + else { + $gets = $stats['cmd_get'] / ($stats['cmd_set'] + $stats['cmd_get']) * 100; + } + if (empty($stats['uptime'])) { + $average = 0; + } + else { + $average = $stats['cmd_get'] / $stats['uptime']; + } + return $this->t( + '@average/s; @total gets (@gets%); @hit hits (@percent_hit%) @miss misses (@percent_miss%)', + [ + '@average' => number_format($average, 2), + '@gets' => number_format($gets, 2), + '@hit' => number_format($stats['get_hits']), + '@percent_hit' => ($stats['cmd_get'] > 0 ? number_format($stats['get_hits'] / $stats['cmd_get'] * 100, 2) : '0.00'), + '@miss' => number_format($stats['get_misses']), + '@percent_miss' => ($stats['cmd_get'] > 0 ? number_format($stats['get_misses'] / $stats['cmd_get'] * 100, 2) : '0.00'), + '@total' => number_format($stats['cmd_get']), + ] + ); + } + + /** + * Statistics report: calculate # of increments and decrements. + */ + private function statsCounters($stats) { + if (!is_array($stats)) { + $stats = []; + } + + $stats += [ + 'incr_hits' => 0, + 'incr_misses' => 0, + 'decr_hits' => 0, + 'decr_misses' => 0, + ]; + + return $this->t( + '@incr increments, @decr decrements', + [ + '@incr' => number_format($stats['incr_hits'] + $stats['incr_misses']), + '@decr' => number_format($stats['decr_hits'] + $stats['decr_misses']), + ] + ); + } + + /** + * Statistics report: calculate bytes transferred. + */ + private function statsTransfer($stats) { + if ($stats['bytes_written'] == 0) { + $written = 0; + } + else { + $written = $stats['bytes_read'] / $stats['bytes_written'] * 100; + } + return $this->t( + '@to:@from (@written% to cache)', + [ + '@to' => format_size((int) $stats['bytes_read']), + '@from' => format_size((int) $stats['bytes_written']), + '@written' => number_format($written, 2), + ] + ); + } + + /** + * Statistics report: calculate per-connection averages. + */ + private function statsAverage($stats) { + if ($stats['total_connections'] == 0) { + $get = 0; + $set = 0; + $read = 0; + $write = 0; + } + else { + $get = $stats['cmd_get'] / $stats['total_connections']; + $set = $stats['cmd_set'] / $stats['total_connections']; + $read = $stats['bytes_written'] / $stats['total_connections']; + $write = $stats['bytes_read'] / $stats['total_connections']; + } + return $this->t( + '@read in @get gets; @write in @set sets', + [ + '@get' => number_format($get, 2), + '@set' => number_format($set, 2), + '@read' => format_size(number_format($read, 2)), + '@write' => format_size(number_format($write, 2)), + ] + ); + } + + /** + * Statistics report: calculate available memory. + */ + private function statsMemory($stats) { + if ($stats['limit_maxbytes'] == 0) { + $percent = 0; + } + else { + $percent = 100 - $stats['bytes'] / $stats['limit_maxbytes'] * 100; + } + return $this->t( + '@available (@percent%) of @total', + [ + '@available' => format_size($stats['limit_maxbytes'] - $stats['bytes']), + '@percent' => number_format($percent, 2), + '@total' => format_size($stats['limit_maxbytes']), + ] + ); + } + + /** + * Generates render array for output. + */ + private function statsTablesOutput($bin, $servers, $stats) { + $memcache = \Drupal::service('memcache.factory')->get(NULL, TRUE); + $memcache_bins = $memcache->getBins(); + + $links = []; + foreach ($servers as $server) { + + // Convert socket file path so it works with an argument, this should + // have no impact on non-socket configurations. Convert / to !. + $links[] = Link::fromTextandUrl($server, Url::fromUri('base:/admin/reports/memcache/' . $memcache_bins[$bin] . '/' . str_replace('/', '!', $server)))->toString(); + } + + if (count($servers) > 1) { + $headers = array_merge(['', t('Totals')], $links); + } + else { + $headers = array_merge([''], $links); + } + + $output = []; + foreach ($stats as $table => $data) { + $rows = []; + foreach ($data as $data_row) { + $row = []; + $row[] = $data_row['label']; + if (isset($data_row['total'])) { + $row[] = $data_row['total']; + } + foreach ($data_row['servers'] as $server) { + $row[] = $server; + } + $rows[] = $row; + } + $output[$table] = [ + '#theme' => 'table', + '#header' => $headers, + '#rows' => $rows, + + ]; + } + + return $output; + } + + /** + * Generates render array for output. + */ + private function statsTablesRawOutput($cluster, $server, $stats, $type) { + $user = \Drupal::currentUser(); + $current_type = isset($type) ? $type : 'default'; + $memcache = \Drupal::service('memcache.factory')->get(NULL, TRUE); + $memcache_bins = $memcache->getBins(); + $bin = isset($memcache_bins[$cluster]) ? $memcache_bins[$cluster] : 'default'; + $slab = \Drupal::routeMatch()->getParameter('slab'); + + // Provide navigation for the various memcache stats types. + $links = []; + if (count($memcache->statsTypes())) { + foreach ($memcache->statsTypes() as $type) { + // @todo render array + $link = Link::fromTextandUrl($type, Url::fromUri('base:/admin/reports/memcache/' . $bin . '/' . str_replace('/', '!', $server) . '/' . ($type == 'default' ? '' : $type)))->toString(); + if ($current_type == $type) { + $links[] = '<strong>' . $link . '</strong>'; + } + else { + $links[] = $link; + } + } + } + $build = [ + 'links' => [ + '#markup' => !empty($links) ? implode($links, ' | ') : '', + ], + ]; + + $build['table'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Property'), + $this->t('Value'), + ], + ]; + + $row = 0; + + // Items are returned as an array within an array within an array. We step + // in one level to properly display the contained statistics. + if ($current_type == 'items' && isset($stats['items'])) { + $stats = $stats['items']; + } + + foreach ($stats as $key => $value) { + + // Add navigation for getting a cachedump of individual slabs. + if (($current_type == 'slabs' || $current_type == 'items') && is_int($key) && $user->hasPermission('access slab cachedump')) { + $build['table'][$row]['key'] = [ + '#type' => 'link', + '#title' => $this->t('Slab @slab', ['@slab' => $key]), + '#url' => Url::fromUri('base:/admin/reports/memcache/' . $bin . '/' . str_replace('/', '!', $server) . '/slabs/cachedump/' . $key), + ]; + } + else { + $build['table'][$row]['key'] = ['#plain_text' => $key]; + } + + if (is_array($value)) { + $subrow = 0; + $build['table'][$row]['value'] = ['#type' => 'table']; + foreach ($value as $k => $v) { + + // Format timestamp when viewing cachedump of individual slabs. + if ($current_type == 'slabs' && $user->hasPermission('access slab cachedump') && !empty($slab) && $k == 0) { + $k = $this->t('Size'); + $v = format_size($v); + } + elseif ($current_type == 'slabs' && $user->hasPermission('access slab cachedump') && !empty($slab) && $k == 1) { + $k = $this->t('Expire'); + $full_stats = $memcache->stats($cluster); + $infinite = $full_stats[$cluster][$server]['time'] - $full_stats[$cluster][$server]['uptime']; + if ($v == $infinite) { + $v = $this->t('infinite'); + } + else { + $v = $this->t('in @time', ['@time' => \Drupal::service('date.formatter')->formatInterval($v - \Drupal::time()->getRequestTime())]); + } + } + $build['table'][$row]['value'][$subrow] = [ + 'key' => ['#plain_text' => $k], + 'value' => ['#plain_text' => $v], + ]; + $subrow++; + } + } + else { + $build['table'][$row]['value'] = ['#plain_text' => $value]; + } + $row++; + } + + return $build; + } + +} diff --git a/web/modules/contrib/memcache/memcache_admin/src/EventSubscriber/MemcacheAdminSubscriber.php b/web/modules/contrib/memcache/memcache_admin/src/EventSubscriber/MemcacheAdminSubscriber.php new file mode 100644 index 000000000..2dec7e6c9 --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/src/EventSubscriber/MemcacheAdminSubscriber.php @@ -0,0 +1,143 @@ +<?php + +namespace Drupal\memcache_admin\EventSubscriber; + +use Drupal\Core\Render\Element\HtmlTag; +use Drupal\Core\Render\Markup; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Drupal\Component\Render\HtmlEscapedText; +use Drupal\Core\Render\HtmlResponse; + +/** + * Memcache Admin Subscriber. + */ +class MemcacheAdminSubscriber implements EventSubscriberInterface { + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::RESPONSE][] = ['displayStatistics']; + return $events; + } + + /** + * Display statistics on page. + */ + public function displayStatistics(FilterResponseEvent $event) { + $user = \Drupal::currentUser(); + + // Removed exclusion criteria, untested. Will likely need to add some of + // these back in. + // @codingStandardsIgnoreStart + // strstr($_SERVER['PHP_SELF'], '/update.php') + // substr($_GET['q'], 0, strlen('batch')) == 'batch' + // strstr($_GET['q'], 'autocomplete') + // substr($_GET['q'], 0, strlen('system/files')) == 'system/files' + // in_array($_GET['q'], ['upload/js', 'admin/content/node-settings/rebuild']) + // @codingStandardsIgnoreEnd + // @todo validate these checks + if ($user->id() == 0) { + // Suppress for the above criteria. + } + else { + $response = $event->getResponse(); + + // Don't call theme() during shutdown if the registry has been rebuilt + // (such as when enabling/disabling modules on admin/build/modules) as + // things break. + // Instead, simply exit without displaying admin statistics for this page + // load. See http://drupal.org/node/616282 for discussion. + // @todo make sure this is not still a requirement. + // @codingStandardsIgnoreStart + // if (!function_exists('theme_get_registry') || !theme_get_registry()) { + // return; + // }. + // @codingStandardsIgnoreEnd + // Try not to break non-HTML pages. + if ($response instanceof HTMLResponse) { + + // This should only apply to page content. + if (stripos($response->headers->get('content-type'), 'text/html') !== FALSE) { + $show_stats = \Drupal::config('memcache_admin.settings')->get('show_memcache_statistics'); + if ($show_stats && $user->hasPermission('access memcache statistics')) { + $output = ''; + + $memcache = \Drupal::service('memcache.factory')->get(NULL, TRUE); + $memcache_stats = $memcache->requestStats(); + if (!empty($memcache_stats['ops'])) { + foreach ($memcache_stats['ops'] as $row => $stats) { + $memcache_stats['ops'][$row][0] = new HtmlEscapedText($stats[0]); + $memcache_stats['ops'][$row][1] = number_format($stats[1], 2); + $hits = number_format($this->statsPercent($stats[2], $stats[3]), 1); + $misses = number_format($this->statsPercent($stats[3], $stats[2]), 1); + $memcache_stats['ops'][$row][2] = number_format($stats[2]) . " ($hits%)"; + $memcache_stats['ops'][$row][3] = number_format($stats[3]) . " ($misses%)"; + } + + $build = [ + '#theme' => 'table', + '#header' => [ + t('operation'), + t('total ms'), + t('total hits'), + t('total misses'), + ], + '#rows' => $memcache_stats['ops'], + ]; + $output .= \Drupal::service('renderer')->renderRoot($build); + } + + if (!empty($memcache_stats['all'])) { + $build = [ + '#type' => 'table', + '#header' => [ + t('ms'), + t('operation'), + t('bin'), + t('key'), + t('status'), + ], + ]; + foreach ($memcache_stats['all'] as $row => $stats) { + $build[$row]['ms'] = ['#plain_text' => $stats[0]]; + $build[$row]['operation'] = ['#plain_text' => $stats[1]]; + $build[$row]['bin'] = ['#plain_text' => $stats[2]]; + $build[$row]['key'] = [ + '#separator' => ' | ', + ]; + foreach (explode('\n', $stats[3]) as $akey) { + $build[$row]['key']['child'][]['#plain_text'] = $akey; + } + $build[$row]['status'] = ['#plain_text' => $stats[4]]; + } + $output .= \Drupal::service('renderer')->renderRoot($build); + } + + if (!empty($output)) { + $response->setContent($response->getContent() . '<div id="memcache-devel"><h2>' . t('Memcache statistics') . '</h2>' . $output . '</div>'); + } + } + } + } + } + } + + /** + * Helper function. Calculate a percentage. + */ + private function statsPercent($a, $b) { + if ($a == 0) { + return 0; + } + elseif ($b == 0) { + return 100; + } + else { + return $a / ($a + $b) * 100; + } + } + +} diff --git a/web/modules/contrib/memcache/memcache_admin/src/Form/MemcacheAdminSettingsForm.php b/web/modules/contrib/memcache/memcache_admin/src/Form/MemcacheAdminSettingsForm.php new file mode 100644 index 000000000..e33a2bd78 --- /dev/null +++ b/web/modules/contrib/memcache/memcache_admin/src/Form/MemcacheAdminSettingsForm.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\memcache_admin\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Memcache admin settings form. + */ +class MemcacheAdminSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'memcache_admin_admin_settings'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['show_memcache_statistics'] = [ + '#type' => 'checkbox', + '#title' => t('Show memcache statistics at the bottom of each page'), + '#default_value' => \Drupal::config('memcache_admin.settings')->get('show_memcache_statistics'), + '#description' => t("These statistics will be visible to users with the 'access memcache statistics' permission."), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function getEditableConfigNames() { + return ['memcache_admin.settings']; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('memcache_admin.settings') + ->set('show_memcache_statistics', $form_state->getValue('show_memcache_statistics')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/contrib/memcache/src/Cache/TimestampCacheTagsChecksum.php b/web/modules/contrib/memcache/src/Cache/TimestampCacheTagsChecksum.php new file mode 100644 index 000000000..2b2f81854 --- /dev/null +++ b/web/modules/contrib/memcache/src/Cache/TimestampCacheTagsChecksum.php @@ -0,0 +1,147 @@ +<?php + +namespace Drupal\memcache\Cache; + +use Drupal\Core\Cache\CacheTagsChecksumInterface; +use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Drupal\memcache\Invalidator\TimestampInvalidatorInterface; + +/** + * Cache tags invalidations checksum implementation by timestamp invalidation. + */ +class TimestampCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface { + + /** + * The timestamp invalidator object. + * + * @var \Drupal\memcache\Invalidator\TimestampInvalidatorInterface + */ + protected $invalidator; + + /** + * Contains already loaded cache invalidations from the backend. + * + * @var array + */ + protected $tagCache = []; + + /** + * A list of tags that have already been invalidated in this request. + * + * Used to prevent the invalidation of the same cache tag multiple times. + * + * @var array + */ + protected $invalidatedTags = []; + + /** + * Constructs a TimestampCacheTagsChecksum object. + * + * @param \Drupal\memcache\Invalidator\TimestampInvalidatorInterface $invalidator + * The timestamp invalidator object. + */ + public function __construct(TimestampInvalidatorInterface $invalidator) { + $this->invalidator = $invalidator; + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) { + foreach ($tags as $tag) { + // @todo Revisit this behavior and determine a better way to handle. + // Only invalidate tags once per request unless they are written again. + if (isset($this->invalidatedTags[$tag])) { + continue; + } + $this->invalidatedTags[$tag] = TRUE; + $this->tagCache[$tag] = $this->invalidator->invalidateTimestamp($tag); + } + } + + /** + * {@inheritdoc} + */ + public function getCurrentChecksum(array $tags) { + // @todo Revisit the invalidatedTags hack. + // Remove tags that were already invalidated during this request from the + // static caches so that another invalidation can occur later in the same + // request. Without that, written cache items would not be invalidated + // correctly. + foreach ($tags as $tag) { + unset($this->invalidatedTags[$tag]); + } + // Taking the minimum of the current timestamp and the checksum is used to + // ensure that items that are not valid yet are identified properly as not + // valid. The checksum will change continuously until the item is valid, + // at which point the checksum will match and freeze at that value. + return min($this->invalidator->getCurrentTimestamp(), $this->calculateChecksum($tags)); + } + + /** + * {@inheritdoc} + */ + public function isValid($checksum, array $tags) { + if (empty($tags)) { + // If there weren't any tags, the checksum should always be 0 or FALSE. + return $checksum == 0; + } + return $checksum == $this->calculateChecksum($tags); + } + + /** + * Calculates the current checksum for a given set of tags. + * + * @param array $tags + * The array of tags to calculate the checksum for. + * + * @return int + * The calculated checksum. + */ + protected function calculateChecksum(array $tags) { + + $query_tags = array_diff($tags, array_keys($this->tagCache)); + if ($query_tags) { + $backend_tags = $this->invalidator->getLastInvalidationTimestamps($query_tags); + $this->tagCache += $backend_tags; + $invalid = array_diff($query_tags, array_keys($backend_tags)); + if (!empty($invalid)) { + // Invalidate any missing tags now. This is necessary because we cannot + // zero-optimize our tag list -- we can't tell the difference between + // a tag that has never been invalidated and a tag that was + // garbage-collected by the backend! + // + // This behavioral difference is the main change that allows us to use + // an unreliable backend to track cache tag invalidation. + // + // Invalidating the tag will cause it to start being tracked, so it can + // be matched against the checksums stored on items. + // All items cached after that point with the tag will end up with + // a valid checksum, and all items cached before that point with the tag + // will have an invalid checksum, because missing invalidations will + // keep moving forward in time as they get garbage collected and are + // re-invalidated. + // + // The main effect of all this is that a tag going missing + // will automatically cause the cache items tagged with it to no longer + // have the correct checksum. + foreach ($invalid as $invalid_tag) { + $this->invalidator->invalidateTimestamp($invalid_tag); + } + } + } + + // The checksum is equal to the *most recent* invalidation of an applicable + // tag. If the item is untagged, the checksum is always 0. + return max([0] + array_intersect_key($this->tagCache, array_flip($tags))); + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->tagCache = []; + $this->invalidatedTags = []; + } + +} diff --git a/web/modules/contrib/memcache/src/Connection/MemcacheConnection.php b/web/modules/contrib/memcache/src/Connection/MemcacheConnection.php new file mode 100644 index 000000000..83e507591 --- /dev/null +++ b/web/modules/contrib/memcache/src/Connection/MemcacheConnection.php @@ -0,0 +1,81 @@ +<?php + +namespace Drupal\memcache\Connection; + +/** + * Class MemcacheConnection. + */ +class MemcacheConnection implements MemcacheConnectionInterface { + + /** + * The memcache object. + * + * @var \Memcache + */ + protected $memcache; + + /** + * Constructs a MemcacheConnection object. + */ + public function __construct() { + $this->memcache = new \Memcache(); + } + + /** + * {@inheritdoc} + */ + public function addServer($server_path, $persistent = FALSE) { + list($host, $port) = explode(':', $server_path); + + // Support unix sockets in the format 'unix:///path/to/socket'. + if ($host == 'unix') { + // When using unix sockets with Memcache use the full path for $host. + $host = $server_path; + // Port is always 0 for unix sockets. + $port = 0; + } + + // When using the PECL memcache extension, we must use ->(p)connect + // for the first connection. + return $this->connect($host, $port, $persistent); + } + + /** + * {@inheritdoc} + */ + public function getMemcache() { + return $this->memcache; + } + + /** + * {@inheritdoc} + */ + public function close() { + $this->memcache->close(); + } + + /** + * Connects to a memcache server. + * + * @param string $host + * The server path without port. + * @param int $port + * The server port. + * @param bool $persistent + * Whether this server connection is persistent or not. + * + * @return \Memcache|bool + * A Memcache object for a successful persistent connection. TRUE for a + * successful non-persistent connection. FALSE when the server fails to + * connect. + */ + protected function connect($host, $port, $persistent) { + if ($persistent) { + return @$this->memcache->pconnect($host, $port); + } + else { + return @$this->memcache->connect($host, $port); + } + } + +} diff --git a/web/modules/contrib/memcache/src/Connection/MemcacheConnectionInterface.php b/web/modules/contrib/memcache/src/Connection/MemcacheConnectionInterface.php new file mode 100644 index 000000000..1c79361d1 --- /dev/null +++ b/web/modules/contrib/memcache/src/Connection/MemcacheConnectionInterface.php @@ -0,0 +1,33 @@ +<?php + +namespace Drupal\memcache\Connection; + +/** + * Defines the Memcache connection interface. + */ +interface MemcacheConnectionInterface { + + /** + * Adds a memcache server. + * + * @param string $server_path + * The server path including port. + * @param bool $persistent + * Whether this server connection is persistent or not. + */ + public function addServer($server_path, $persistent = FALSE); + + /** + * Returns the internal memcache object. + * + * @return object + * e.g. \Memcache|\Memcached + */ + public function getMemcache(); + + /** + * Closes the memcache instance connection. + */ + public function close(); + +} diff --git a/web/modules/contrib/memcache/src/Connection/MemcachedConnection.php b/web/modules/contrib/memcache/src/Connection/MemcachedConnection.php new file mode 100755 index 000000000..5413f3704 --- /dev/null +++ b/web/modules/contrib/memcache/src/Connection/MemcachedConnection.php @@ -0,0 +1,78 @@ +<?php + +namespace Drupal\memcache\Connection; + +use Drupal\memcache\MemcacheSettings; + +/** + * Class MemcachedConnection. + */ +class MemcachedConnection implements MemcacheConnectionInterface { + + /** + * The memcache object. + * + * @var \Memcached + */ + protected $memcache; + + /** + * Constructs a MemcachedConnection object. + * + * @param \Drupal\memcache\MemcacheSettings $settings + * The memcache config object. + */ + public function __construct(MemcacheSettings $settings) { + $this->memcache = new \Memcached(); + + $default_opts = [ + \Memcached::OPT_COMPRESSION => TRUE, + \Memcached::OPT_DISTRIBUTION => \Memcached::DISTRIBUTION_CONSISTENT, + ]; + foreach ($default_opts as $key => $value) { + $this->memcache->setOption($key, $value); + } + // See README.txt for setting custom Memcache options when using the + // memcached PECL extension. + foreach ($settings->get('options', []) as $key => $value) { + $this->memcache->setOption($key, $value); + } + + // SASL configuration to authenticate with Memcached. + // Note: this only affects the Memcached PECL extension. + if ($sasl_config = $settings->get('sasl', [])) { + $this->memcache->setSaslAuthData($sasl_config['username'], $sasl_config['password']); + } + } + + /** + * {@inheritdoc} + */ + public function addServer($server_path, $persistent = FALSE) { + list($host, $port) = explode(':', $server_path); + + if ($host == 'unix') { + // Memcached expects just the path to the socket without the protocol. + $host = substr($server_path, 7); + // Port is always 0 for unix sockets. + $port = 0; + } + + return $this->memcache->addServer($host, $port, $persistent); + } + + /** + * {@inheritdoc} + */ + public function getMemcache() { + return $this->memcache; + } + + /** + * {@inheritdoc} + */ + public function close() { + $this->memcache->quit(); + } + +} diff --git a/web/modules/contrib/memcache/src/Driver/DriverBase.php b/web/modules/contrib/memcache/src/Driver/DriverBase.php new file mode 100644 index 000000000..ab69927e6 --- /dev/null +++ b/web/modules/contrib/memcache/src/Driver/DriverBase.php @@ -0,0 +1,355 @@ +<?php + +namespace Drupal\memcache\Driver; + +use Drupal\Component\Utility\Timer; +use Drupal\memcache\MemcacheSettings; +use Drupal\memcache\DrupalMemcacheInterface; + +/** + * Class DriverBase. + */ +abstract class DriverBase implements DrupalMemcacheInterface { + + /** + * The memcache config object. + * + * @var \Drupal\memcache\MemcacheSettings + */ + protected $settings; + + /** + * The memcache object. + * + * @var \Memcache|\Memcached + * E.g. \Memcache|\Memcached + */ + protected $memcache; + + /** + * The hash algorithm to pass to hash(). Defaults to 'sha1'. + * + * @var string + */ + protected $hashAlgorithm; + + /** + * The prefix memcache key for all keys. + * + * @var string + */ + protected $prefix; + + /** + * Stats for the entire request. + * + * @var array + */ + protected static $stats = [ + 'all' => [], + 'ops' => [], + ]; + + /** + * Constructs a DriverBase object. + * + * @param \Drupal\memcache\MemcacheSettings $settings + * The memcache config object. + * @param \Memcached|\Memcache $memcache + * An existing memcache connection object. + * @param string $bin + * The class instance specific cache bin to use. + */ + public function __construct(MemcacheSettings $settings, $memcache, $bin = NULL) { + $this->settings = $settings; + $this->memcache = $memcache; + + $this->hashAlgorithm = $this->settings->get('key_hash_algorithm', 'sha1'); + + $prefix = $this->settings->get('key_prefix', ''); + if ($prefix) { + $this->prefix = $prefix . ':'; + } + + if ($bin) { + $this->prefix .= $bin . ':'; + } + } + + /** + * {@inheritdoc} + */ + public function get($key) { + $collect_stats = $this->statsInit(); + + $full_key = $this->key($key); + $result = $this->memcache->get($full_key); + + if ($collect_stats) { + $this->statsWrite('get', 'cache', [$full_key => (bool) $result]); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function key($key) { + $full_key = urlencode($this->prefix . '-' . $key); + + // Memcache only supports key lengths up to 250 bytes. If we have generated + // a longer key, we shrink it to an acceptable length with a configurable + // hashing algorithm. Sha1 was selected as the default as it performs + // quickly with minimal collisions. + if (strlen($full_key) > 250) { + $full_key = urlencode($this->prefix . '-' . hash($this->hashAlgorithm, $key)); + $full_key .= '-' . substr(urlencode($key), 0, (250 - 1) - strlen($full_key) - 1); + } + + return $full_key; + } + + /** + * {@inheritdoc} + */ + public function delete($key) { + $collect_stats = $this->statsInit(); + + $full_key = $this->key($key); + $result = $this->memcache->delete($full_key, 0); + + if ($collect_stats) { + $this->statsWrite('delete', 'cache', [$full_key => $result]); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function flush() { + $collect_stats = $this->statsInit(); + + $result = $this->memcache->flush(); + + if ($collect_stats) { + $this->statsWrite('flush', 'cache', ['' => $result]); + } + } + + /** + * Retrieves statistics recorded during memcache operations. + * + * @param string $stats_bin + * The bin to retrieve statistics for. + * @param string $stats_type + * The type of statistics to retrieve when using the Memcache extension. + * @param bool $aggregate + * Whether to aggregate statistics. + */ + public function stats($stats_bin = 'cache', $stats_type = 'default', $aggregate = FALSE) { + + // The stats_type can be over-loaded with an integer slab id, if doing a + // cachedump. We know we're doing a cachedump if $slab is non-zero. + $slab = (int) $stats_type; + $stats = []; + + foreach ($this->getBins() as $bin => $target) { + if ($stats_bin == $bin) { + if (isset($this->memcache)) { + if ($this->memcache instanceof \Memcached) { + $stats[$bin] = $this->memcache->getStats(); + } + + // The PHP Memcache extension 3.x version throws an error if the stats + // type is NULL or not in {reset, malloc, slabs, cachedump, items, + // sizes}. If $stats_type is 'default', then no parameter should be + // passed to the Memcache memcache_get_extended_stats() function. + elseif ($this->memcache instanceof \Memcache) { + if ($stats_type == 'default' || $stats_type == '') { + $stats[$bin] = $this->memcache->getExtendedStats(); + } + + // If $slab isn't zero, then we are dumping the contents of a + // specific cache slab. + elseif (!empty($slab)) { + $stats[$bin] = $this->memcache->getStats('cachedump', $slab); + } + else { + $stats[$bin] = $this->memcache->getExtendedStats($stats_type); + } + } + } + } + } + + // Optionally calculate a sum-total for all servers in the current bin. + if ($aggregate) { + + // Some variables don't logically aggregate. + $no_aggregate = [ + 'pid', + 'time', + 'version', + 'pointer_size', + 'accepting_conns', + 'listen_disabled_num', + ]; + + foreach ($stats as $bin => $servers) { + if (is_array($servers)) { + foreach ($servers as $server) { + if (is_array($server)) { + foreach ($server as $key => $value) { + if (!in_array($key, $no_aggregate)) { + if (isset($stats[$bin]['total'][$key])) { + $stats[$bin]['total'][$key] += $value; + } + else { + $stats[$bin]['total'][$key] = $value; + } + } + } + } + } + } + } + } + + return $stats; + } + + /** + * Helper function to get the bins. + */ + public function getBins() { + $memcache_bins = \Drupal::configFactory()->getEditable('memcache.settings')->get('memcache_bins'); + if (!isset($memcache_bins)) { + $memcache_bins = ['cache' => 'default']; + } + + return $memcache_bins; + } + + /** + * Helper function to get the servers. + */ + public function getServers() { + $memcache_servers = \Drupal::configFactory()->getEditable('memcache.settings')->get('memcache_servers'); + if (!isset($memcache_servers)) { + $memcache_servers = ['127.0.0.1:11211' => 'default']; + } + + return $memcache_servers; + } + + /** + * Helper function to get memcache. + */ + public function getMemcache() { + return $this->memcache; + } + + /** + * Helper function to get request stats. + */ + public function requestStats() { + return self::$stats; + } + + /** + * Returns an array of available statistics types. + */ + public function statsTypes() { + if ($this->memcache instanceof \Memcache) { + // TODO: Determine which versions of the PECL memcache extension have + // these other stats types: 'malloc', 'maps', optionally detect this + // version and expose them. These stats are "subject to change without + // warning" unfortunately. + return ['default', 'slabs', 'items', 'sizes']; + } + else { + // The Memcached PECL extension only offers the default statistics. + return ['default']; + } + } + + /** + * Helper function to initialize the stats for a memcache operation. + */ + protected function statsInit() { + static $drupal_static_fast; + + if (!isset($drupal_static_fast)) { + $drupal_static_fast = &drupal_static(__FUNCTION__, ['variable_checked' => NULL, 'user_access_checked' => NULL]); + } + $variable_checked = &$drupal_static_fast['variable_checked']; + $user_access_checked = &$drupal_static_fast['user_access_checked']; + + // Confirm DRUPAL_BOOTSTRAP_VARIABLES has been reached. We don't use + // drupal_get_bootstrap_phase() as it's buggy. We can use variable_get() + // here because _drupal_bootstrap_variables() includes module.inc + // immediately after it calls variable_initialize(). + // @codingStandardsIgnoreStart + // if (!isset($variable_checked) && function_exists('module_list')) { + // $variable_checked = variable_get('show_memcache_statistics', FALSE); + // } + // If statistics are enabled we need to check user access. + // if (!empty($variable_checked) && !isset($user_access_checked) && !empty($GLOBALS['user']) && function_exists('user_access')) { + // // Statistics are enabled and the $user object has been populated, so check + // // that the user has access to view them. + // $user_access_checked = user_access('access memcache statistics'); + // } + // @codingStandardsIgnoreEnd + // Return whether or not statistics are enabled and the user can access + // them. + if ((!isset($variable_checked) || $variable_checked) && (!isset($user_access_checked) || $user_access_checked)) { + Timer::start('dmemcache'); + return TRUE; + } + else { + return FALSE; + } + } + + /** + * Memcache statistics to be displayed at end of page generation. + * + * @param string $action + * The action being performed (get, set, etc...). + * @param string $bin + * The memcache bin the action is being performed in. + * @param array $keys + * Keyed array in the form (string)$cid => (bool)$success. The keys the + * action is being performed on, and whether or not it was a success. + */ + protected function statsWrite($action, $bin, array $keys) { + + // Determine how much time elapsed to execute this action. + $time = Timer::read('dmemcache'); + + // Build the 'all' and 'ops' arrays displayed by memcache_admin.module. + foreach ($keys as $key => $success) { + self::$stats['all'][] = [ + number_format($time, 2), + $action, + $bin, + $key, + $success ? 'hit' : 'miss', + ]; + if (!isset(self::$stats['ops'][$action])) { + self::$stats['ops'][$action] = [$action, 0, 0, 0]; + } + self::$stats['ops'][$action][1] += $time; + if ($success) { + self::$stats['ops'][$action][2]++; + } + else { + self::$stats['ops'][$action][3]++; + } + } + } + +} diff --git a/web/modules/contrib/memcache/src/Driver/MemcacheDriver.php b/web/modules/contrib/memcache/src/Driver/MemcacheDriver.php new file mode 100644 index 000000000..3f16e9e2f --- /dev/null +++ b/web/modules/contrib/memcache/src/Driver/MemcacheDriver.php @@ -0,0 +1,90 @@ +<?php + +namespace Drupal\memcache\Driver; + +/** + * Class MemcacheDriver. + */ +class MemcacheDriver extends DriverBase { + + /** + * {@inheritdoc} + */ + public function set($key, $value, $exp = 0, $flag = FALSE) { + $collect_stats = $this->statsInit(); + + $full_key = $this->key($key); + $result = $this->memcache->set($full_key, $value, $flag, $exp); + + if ($collect_stats) { + $this->statsWrite('set', 'cache', [$full_key => (int) $result]); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function add($key, $value, $expire = 0) { + $collect_stats = $this->statsInit(); + + $full_key = $this->key($key); + $result = $this->memcache->add($full_key, $value, FALSE, $expire); + + if ($collect_stats) { + $this->statsWrite('add', 'cache', [$full_key => (int) $result]); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getMulti(array $keys) { + $collect_stats = $this->statsInit(); + $multi_stats = []; + + $full_keys = []; + + foreach ($keys as $key => $cid) { + $full_key = $this->key($cid); + $full_keys[$cid] = $full_key; + + if ($collect_stats) { + $multi_stats[$full_key] = FALSE; + } + } + + $results = $this->memcache->get($full_keys); + + // If $results is FALSE, convert it to an empty array. + if (!$results) { + $results = []; + } + + if ($collect_stats) { + foreach ($multi_stats as $key => $value) { + $multi_stats[$key] = isset($results[$key]) ? TRUE : FALSE; + } + } + + // Convert the full keys back to the cid. + $cid_results = []; + + // Order isn't guaranteed, so ensure the return order matches that + // requested. So base the results on the order of the full_keys, as they + // reflect the order of the $cids passed in. + foreach (array_intersect($full_keys, array_keys($results)) as $cid => $full_key) { + $cid_results[$cid] = $results[$full_key]; + } + + if ($collect_stats) { + $this->statsWrite('getMulti', 'cache', $multi_stats); + } + + return $cid_results; + } + +} diff --git a/web/modules/contrib/memcache/src/Driver/MemcacheDriverFactory.php b/web/modules/contrib/memcache/src/Driver/MemcacheDriverFactory.php new file mode 100644 index 000000000..88544336c --- /dev/null +++ b/web/modules/contrib/memcache/src/Driver/MemcacheDriverFactory.php @@ -0,0 +1,206 @@ +<?php + +namespace Drupal\memcache\Driver; + +use Drupal\memcache\Connection\MemcacheConnection; +use Drupal\memcache\Connection\MemcachedConnection; +use Drupal\memcache\MemcacheSettings; +use Drupal\memcache\MemcacheException; + +/** + * Factory class for creation of Memcache objects. + */ +class MemcacheDriverFactory { + + /** + * The settings object. + * + * @var \Drupal\memcache\MemcacheSettings + */ + protected $settings; + + /** + * The connection class reference. + * + * @var string + */ + protected $connectionClass; + + /** + * The driver class reference. + * + * @var string + */ + protected $driverClass; + + /** + * Whether to connect to memcache using a persistent connection. + * + * @var bool + */ + protected $persistent; + + /** + * An array of Memcache connections keyed by bin. + * + * @var \Drupal\memcache\Connection\MemcacheConnectionInterface[] + */ + protected $connections = []; + + /** + * An array of configured servers. + * + * @var array + */ + protected $servers = []; + + /** + * An array of configured bins. + * + * @var string[] + */ + protected $bins = []; + + /** + * An array of failed connections to configured servers keyed by server name. + * + * @var bool[] + */ + protected $failedConnectionCache = []; + + /** + * Constructs a MemcacheDriverFactory object. + * + * @param \Drupal\memcache\MemcacheSettings $settings + * The settings object. + */ + public function __construct(MemcacheSettings $settings) { + $this->settings = $settings; + + $this->initialize(); + } + + /** + * Returns a Memcache object based on settings and the bin requested. + * + * @param string $bin + * The bin which is to be used. + * @param bool $flush + * Rebuild the bin/server/cache mapping. + * + * @return \Drupal\memcache\DrupalMemcacheInterface|bool + * A Memcache object. + */ + public function get($bin = NULL, $flush = FALSE) { + if ($flush) { + $this->flush(); + } + + if (empty($this->connections) || empty($this->connections[$bin])) { + // If there is no cluster for this bin in $bins, cluster is + // 'default'. + $cluster = empty($this->bins[$bin]) ? 'default' : $this->bins[$bin]; + + // If this bin isn't in our $bins configuration array, and the + // 'default' cluster is already initialized, map the bin to 'default' + // because we always map the 'default' bin to the 'default' cluster. + if (empty($this->bins[$bin]) && !empty($this->connections['default'])) { + $this->connections[$bin] = &$this->connections['default']; + } + else { + // Create a new Memcache object. Each cluster gets its own Memcache + // object. + /** @var \Drupal\memcache\Connection\MemcacheConnectionInterface $memcache */ + $memcache = new $this->connectionClass($this->settings); + + // A variable to track whether we've connected to the first server. + $init = FALSE; + + // Link all the servers to this cluster. + foreach ($this->servers as $s => $c) { + if ($c == $cluster && !isset($this->failedConnectionCache[$s])) { + if ($memcache->addServer($s, $this->persistent) && !$init) { + $init = TRUE; + } + + if (!$init) { + $this->failedConnectionCache[$s] = FALSE; + } + } + } + + if ($init) { + // Map the current bin with the new Memcache object. + $this->connections[$bin] = $memcache; + + // Now that all the servers have been mapped to this cluster, look for + // other bins that belong to the cluster and map them too. + foreach ($this->bins as $b => $c) { + if (($c == $cluster) && ($b != $bin)) { + // Map this bin and cluster by reference. + $this->connections[$b] = &$this->connections[$bin]; + } + } + } + else { + throw new MemcacheException('Memcache instance could not be initialized. Check memcache is running and reachable'); + } + } + } + + return empty($this->connections[$bin]) ? FALSE : new $this->driverClass($this->settings, $this->connections[$bin]->getMemcache(), $bin); + } + + /** + * Initializes memcache settings. + */ + protected function initialize() { + // If an extension is specified in settings.php, use that when available. + $preferred = $this->settings->get('extension', NULL); + + if (isset($preferred) && class_exists($preferred)) { + $extension = $preferred; + } + // If no extension is set, default to Memcached. + elseif (class_exists('Memcached')) { + $extension = \Memcached::class; + } + elseif (class_exists('Memcache')) { + $extension = \Memcache::class; + } + else { + throw new MemcacheException('No Memcache extension found'); + } + + // @todo Make driver class configurable? + $this->connectionClass = MemcachedConnection::class; + $this->driverClass = MemcachedDriver::class; + + if ($extension === \Memcache::class) { + $this->connectionClass = MemcacheConnection::class; + $this->driverClass = MemcacheDriver::class; + } + + // Values from settings.php. + $this->servers = $this->settings->get('servers', ['127.0.0.1:11211' => 'default']); + $this->bins = $this->settings->get('bins', ['default' => 'default']); + + // Indicate whether to connect to memcache using a persistent connection. + // Note: this only affects the Memcache PECL extension, and does not affect + // the Memcached PECL extension. For a detailed explanation see: + // http://drupal.org/node/822316#comment-4427676 + $this->persistent = $this->settings->get('persistent', FALSE); + } + + /** + * Flushes the memcache bin/server/cache mappings and closes connections. + */ + protected function flush() { + foreach ($this->connections as $cluster) { + $cluster->close(); + } + + $this->connections = []; + } + +} diff --git a/web/modules/contrib/memcache/src/Driver/MemcachedDriver.php b/web/modules/contrib/memcache/src/Driver/MemcachedDriver.php new file mode 100755 index 000000000..0e8e67d66 --- /dev/null +++ b/web/modules/contrib/memcache/src/Driver/MemcachedDriver.php @@ -0,0 +1,94 @@ +<?php + +namespace Drupal\memcache\Driver; + +/** + * Class MemcachedDriver. + */ +class MemcachedDriver extends DriverBase { + + /** + * {@inheritdoc} + */ + public function set($key, $value, $exp = 0, $flag = FALSE) { + $collect_stats = $this->statsInit(); + + $full_key = $this->key($key); + $result = $this->memcache->set($full_key, $value, $exp); + + if ($collect_stats) { + $this->statsWrite('set', 'cache', [$full_key => (int) $result]); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function add($key, $value, $expire = 0) { + $collect_stats = $this->statsInit(); + + $full_key = $this->key($key); + $result = $this->memcache->add($full_key, $value, $expire); + + if ($collect_stats) { + $this->statsWrite('add', 'cache', [$full_key => (int) $result]); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getMulti(array $keys) { + $collect_stats = $this->statsInit(); + $multi_stats = []; + + $full_keys = []; + + foreach ($keys as $key => $cid) { + $full_key = $this->key($cid); + $full_keys[$cid] = $full_key; + + if ($collect_stats) { + $multi_stats[$full_key] = FALSE; + } + } + + if (PHP_MAJOR_VERSION === 7) { + $results = $this->memcache->getMulti($full_keys, \Memcached::GET_PRESERVE_ORDER); + } + else { + $cas_tokens = NULL; + $results = $this->memcache->getMulti($full_keys, $cas_tokens, \Memcached::GET_PRESERVE_ORDER); + } + + // If $results is FALSE, convert it to an empty array. + if (!$results) { + $results = []; + } + + if ($collect_stats) { + foreach ($multi_stats as $key => $value) { + $multi_stats[$key] = isset($results[$key]) ? TRUE : FALSE; + } + } + + // Convert the full keys back to the cid. + $cid_results = []; + $cid_lookup = array_flip($full_keys); + + foreach (array_filter($results) as $key => $value) { + $cid_results[$cid_lookup[$key]] = $value; + } + + if ($collect_stats) { + $this->statsWrite('getMulti', 'cache', $multi_stats); + } + + return $cid_results; + } + +} diff --git a/web/modules/contrib/memcache/src/DrupalMemcache.php b/web/modules/contrib/memcache/src/DrupalMemcache.php deleted file mode 100644 index dd1027f6c..000000000 --- a/web/modules/contrib/memcache/src/DrupalMemcache.php +++ /dev/null @@ -1,109 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\memcache\DrupalMemcache. - */ - -namespace Drupal\memcache; - -use Psr\Log\LogLevel; - -/** - * Class DrupalMemcache. - */ -class DrupalMemcache extends DrupalMemcacheBase { - - /** - * {@inheritdoc} - */ - public function __construct(DrupalMemcacheConfig $settings) { - parent::__construct($settings); - - $this->memcache = new \Memcache(); - } - - /** - * @{@inheritdoc} - */ - public function addServer($server_path, $persistent = FALSE) { - list($host, $port) = explode(':', $server_path); - - // Support unix sockets in the format 'unix:///path/to/socket'. - if ($host == 'unix') { - // When using unix sockets with Memcache use the full path for $host. - $host = $server_path; - // Port is always 0 for unix sockets. - $port = 0; - } - - // When using the PECL memcache extension, we must use ->(p)connect - // for the first connection. - return $this->connect($host, $port, $persistent); - } - - /** - * {@inheritdoc} - */ - public function close() { - $this->memcache->close(); - } - - /** - * Connects to a memcache server. - * - * @param string $host - * @param int $port - * @param bool $persistent - * - * @return bool|mixed - */ - protected function connect($host, $port, $persistent) { - if ($persistent) { - return @$this->memcache->pconnect($host, $port); - } - else { - return @$this->memcache->connect($host, $port); - } - } - - /** - * {@inheritdoc} - */ - public function set($key, $value, $exp = 0, $flag = FALSE) { - $full_key = $this->key($key); - return $this->memcache->set($full_key, $value, $flag, $exp); - } - - /** - * {@inheritdoc} - */ - public function getMulti(array $keys) { - $full_keys = array(); - - foreach ($keys as $cid) { - $full_key = $this->key($cid); - $full_keys[$cid] = $full_key; - } - - $results = $this->memcache->get($full_keys); - - // If $results is FALSE, convert it to an empty array. - if (!$results) { - $results = array(); - } - - // Convert the full keys back to the cid. - $cid_results = array(); - - // Order isn't guaranteed, so ensure the return order matches that - // requested. So base the results on the order of the full_keys, as they - // reflect the order of the $cids passed in. - foreach (array_intersect($full_keys, array_keys($results)) as $cid => $full_key) { - $cid_results[$cid] = $results[$full_key]; - } - - return $cid_results; - } - -} diff --git a/web/modules/contrib/memcache/src/DrupalMemcacheBase.php b/web/modules/contrib/memcache/src/DrupalMemcacheBase.php deleted file mode 100644 index 7c2bf39cd..000000000 --- a/web/modules/contrib/memcache/src/DrupalMemcacheBase.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\memcache\DrupalMemcacheBase. - */ - -namespace Drupal\memcache; - -use Psr\Log\LogLevel; - -/** - * Class DrupalMemcacheBase. - */ -abstract class DrupalMemcacheBase implements DrupalMemcacheInterface { - - /** - * The memcache config object. - * - * @var \Drupal\memcache\DrupalMemcacheConfig - */ - protected $settings; - - /** - * The memcache object. - * - * @var mixed - * E.g. \Memcache|\Memcached - */ - protected $memcache; - - /** - * The hash algorithm to pass to hash(). Defaults to 'sha1' - * - * @var string - */ - protected $hashAlgorithm; - - /** - * The prefix memcache key for all keys. - * - * @var string - */ - protected $prefix; - - /** - * Constructs a DrupalMemcacheBase object. - * - * @param \Drupal\memcache\DrupalMemcacheConfig - * The memcache config object. - */ - public function __construct(DrupalMemcacheConfig $settings) { - $this->settings = $settings; - - $this->hashAlgorithm = $this->settings->get('key_hash_algorithm', 'sha1'); - $this->prefix = $this->settings->get('key_prefix', ''); - } - - /** - * {@inheritdoc} - */ - public function get($key) { - $full_key = $this->key($key); - return $this->memcache->get($full_key); - } - - /** - * {@inheritdoc} - */ - public function key($key) { - $full_key = urlencode($this->prefix . '-' . $key); - - // Memcache only supports key lengths up to 250 bytes. If we have generated - // a longer key, we shrink it to an acceptable length with a configurable - // hashing algorithm. Sha1 was selected as the default as it performs - // quickly with minimal collisions. - if (strlen($full_key) > 250) { - $full_key = urlencode(hash($this->hashAlgorithm, $this->prefix . '-' . $key)); - } - - return $full_key; - } - - /** - * {@inheritdoc} - */ - public function delete($key) { - $full_key = $this->key($key); - return $this->memcache->delete($full_key, 0); - } - - /** - * {@inheritdoc} - */ - public function flush() { - $this->memcache->flush(); - } - -} diff --git a/web/modules/contrib/memcache/src/DrupalMemcacheFactory.php b/web/modules/contrib/memcache/src/DrupalMemcacheFactory.php deleted file mode 100644 index 30e9ccee8..000000000 --- a/web/modules/contrib/memcache/src/DrupalMemcacheFactory.php +++ /dev/null @@ -1,187 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\memcache\DrupalMemcacheFactory. - */ - -namespace Drupal\memcache; - -use Psr\Log\LogLevel; - -/** - * Factory class for creation of Memcache objects. - */ -class DrupalMemcacheFactory { - - /** - * The settings object. - * - * @var \Drupal\memcache\DrupalMemcacheConfig - */ - protected $settings; - - /** - * @var string - */ - protected $extension; - - /** - * @var bool - */ - protected $memcachePersistent; - - /** - * @var \Drupal\memcache\DrupalMemcacheInterface[] - */ - protected $memcacheCache = array(); - - /** - * @var array - */ - protected $memcacheServers = array(); - - /** - * @var array - */ - protected $memcacheBins = array(); - - /** - * @var array - */ - protected $failedConnectionCache = array(); - - /** - * Constructs a DrupalMemcacheFactory object. - * - * @param \Drupal\memcache\DrupalMemcacheConfig $settings - */ - public function __construct(DrupalMemcacheConfig $settings) { - $this->settings = $settings; - - $this->initialize(); - } - - /** - * Returns a Memcache object based on settings and the bin requested. - * - * @param string $bin - * The bin which is to be used. - * - * @param bool $flush - * Rebuild the bin/server/cache mapping. - * - * @return \Drupal\memcache\DrupalMemcacheInterface - * A Memcache object. - */ - public function get($bin = NULL, $flush = FALSE) { - if ($flush) { - $this->flush(); - } - - if (empty($this->memcacheCache) || empty($this->memcacheCache[$bin])) { - // If there is no cluster for this bin in $memcache_bins, cluster is - // 'default'. - $cluster = empty($this->memcacheBins[$bin]) ? 'default' : $this->memcacheBins[$bin]; - - // If this bin isn't in our $memcacheBins configuration array, and the - // 'default' cluster is already initialized, map the bin to 'default' - // because we always map the 'default' bin to the 'default' cluster. - if (empty($this->memcacheBins[$bin]) && !empty($this->memcacheCache['default'])) { - $this->memcacheCache[$bin] = &$this->memcacheCache['default']; - } - else { - // Create a new Memcache object. Each cluster gets its own Memcache - // object. - // @todo Can't add a custom memcache class here yet. - if ($this->extension == 'Memcached') { - $memcache = new DrupalMemcached($this->settings); - } - elseif ($this->extension == 'Memcache') { - $memcache = new DrupalMemcache($this->settings); - } - - // A variable to track whether we've connected to the first server. - $init = FALSE; - - // Link all the servers to this cluster. - foreach ($this->memcacheServers as $s => $c) { - if ($c == $cluster && !isset($this->failedConnectionCache[$s])) { - if ($memcache->addServer($s, $this->memcachePersistent) && !$init) { - $init = TRUE; - } - - if (!$init) { - $this->failedConnectionCache[$s] = FALSE; - } - } - } - - if ($init) { - // Map the current bin with the new Memcache object. - $this->memcacheCache[$bin] = $memcache; - - // Now that all the servers have been mapped to this cluster, look for - // other bins that belong to the cluster and map them too. - foreach ($this->memcacheBins as $b => $c) { - if (($c == $cluster) && ($b != $bin)) { - // Map this bin and cluster by reference. - $this->memcacheCache[$b] = &$this->memcacheCache[$bin]; - } - } - } - else { - throw new MemcacheException('Memcache instance could not be initialized. Check memcache is running and reachable'); - } - } - } - - return empty($this->memcacheCache[$bin]) ? FALSE : $this->memcacheCache[$bin]; - } - - /** - * Initializes memcache settings. - */ - protected function initialize() { - // If an extension is specified in settings.php, use that when available. - $preferred = $this->settings->get('extension', NULL); - if (isset($preferred) && class_exists($preferred)) { - $this->extension = $preferred; - } - // If no extension is set, default to Memcache. The Memcached extension has - // some features that the older extension lacks but also an unfixed bug that - // affects cache clears. - // @see http://pecl.php.net/bugs/bug.php?id=16829 - elseif (class_exists('Memcache')) { - $this->extension = 'Memcache'; - } - elseif (class_exists('Memcached')) { - $this->extension = 'Memcached'; - } - else { - throw new MemcacheException('No Memcache extension found'); - } - - // Values from settings.php - $this->memcacheServers = $this->settings->get('servers', ['127.0.0.1:11211' => 'default']); - $this->memcacheBins = $this->settings->get('bins', ['default' => 'default']); - - // Indicate whether to connect to memcache using a persistent connection. - // Note: this only affects the Memcache PECL extension, and does not affect - // the Memcached PECL extension. For a detailed explanation see: - // http://drupal.org/node/822316#comment-4427676 - $this->memcachePersistent = $this->settings->get('persistent', FALSE); - } - - /** - * Flushes the memcache bin/server/cache mappings and closes connections. - */ - protected function flush() { - foreach ($this->memcacheCache as $cluster) { - $cluster->close(); - } - - $this->memcacheCache = array(); - } - -} diff --git a/web/modules/contrib/memcache/src/DrupalMemcacheInterface.php b/web/modules/contrib/memcache/src/DrupalMemcacheInterface.php index 530ba6b01..68e16cd24 100644 --- a/web/modules/contrib/memcache/src/DrupalMemcacheInterface.php +++ b/web/modules/contrib/memcache/src/DrupalMemcacheInterface.php @@ -1,10 +1,5 @@ <?php -/** - * @file - * Contains \Drupal\memcache\DrupalMemcacheInterface. - */ - namespace Drupal\memcache; /** @@ -68,6 +63,21 @@ interface DrupalMemcacheInterface { */ public function delete($key); + /** + * Add an item to Memcache if it doesn't exist already. + * + * @param string $key + * The key to add. + * @param mixed $value + * The value to add. + * @param int $expire + * The expiration time in seconds. + * + * @return bool + * TRUE on success or FALSE on failure. + */ + public function add($key, $value, $expire = 0); + /** * Prepares the memcache key. * @@ -82,7 +92,7 @@ interface DrupalMemcacheInterface { /** * Immediately invalidates all existing items. * - * flush doesn't actually free any resources, it only marks all the + * Flush doesn't actually free any resources, it only marks all the * items as expired, so occupied memory will be overwritten by new items. * * @return bool @@ -90,18 +100,4 @@ interface DrupalMemcacheInterface { */ public function flush(); - /** - * Closes the memacache instance connection. - */ - public function close(); - - /** - * Adds a memcache server. - * - * @param string $server_path - * The server path including port. - * @param bool $persistent - * Whether this server connection is persistent or not. - */ - public function addServer($server_path, $persistent = FALSE); } diff --git a/web/modules/contrib/memcache/src/DrupalMemcached.php b/web/modules/contrib/memcache/src/DrupalMemcached.php deleted file mode 100755 index 3f636bc0c..000000000 --- a/web/modules/contrib/memcache/src/DrupalMemcached.php +++ /dev/null @@ -1,108 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\memcache\DrupalMemcached. - */ - -namespace Drupal\memcache; - -/** - * Class DrupalMemcached. - */ -class DrupalMemcached extends DrupalMemcacheBase { - - /** - * {@inheritdoc} - */ - public function __construct(DrupalMemcacheConfig $settings) { - parent::__construct($settings); - - $this->memcache = new \Memcached(); - - $default_opts = array( - \Memcached::OPT_COMPRESSION => FALSE, - \Memcached::OPT_DISTRIBUTION => \Memcached::DISTRIBUTION_CONSISTENT, - ); - foreach ($default_opts as $key => $value) { - $this->memcache->setOption($key, $value); - } - // See README.txt for setting custom Memcache options when using the - // memcached PECL extension. - foreach ($this->settings->get('options', []) as $key => $value) { - $this->memcache->setOption($key, $value); - } - - // SASL configuration to authenticate with Memcached. - // Note: this only affects the Memcached PECL extension. - if ($sasl_config = $this->settings->get('sasl', [])) { - $this->memcache->setSaslAuthData($sasl_config['username'], $sasl_config['password']); - } - } - - /** - * {@inheritdoc} - */ - public function addServer($server_path, $persistent = FALSE) { - list($host, $port) = explode(':', $server_path); - - if ($host == 'unix') { - // Memcached expects just the path to the socket without the protocol - $host = substr($server_path, 7); - // Port is always 0 for unix sockets. - $port = 0; - } - - return $this->memcache->addServer($host, $port, $persistent); - } - - /** - * {@inheritdoc} - */ - public function close() { - $this->memcache->quit(); - } - - /** - * {@inheritdoc} - */ - public function set($key, $value, $exp = 0, $flag = FALSE) { - $full_key = $this->key($key); - return $this->memcache->set($full_key, $value, $exp); - } - - /** - * {@inheritdoc} - */ - public function getMulti(array $keys) { - $full_keys = array(); - - foreach ($keys as $cid) { - $full_key = $this->key($cid); - $full_keys[$cid] = $full_key; - } - - if (PHP_MAJOR_VERSION === 7) { - $results = $this->memcache->getMulti($full_keys, \Memcached::GET_PRESERVE_ORDER); - } else { - $cas_tokens = NULL; - $results = $this->memcache->getMulti($full_keys, $cas_tokens, \Memcached::GET_PRESERVE_ORDER); - } - - // If $results is FALSE, convert it to an empty array. - if (!$results) { - $results = array(); - } - - // Convert the full keys back to the cid. - $cid_results = array(); - $cid_lookup = array_flip($full_keys); - - foreach (array_filter($results) as $key => $value) { - $cid_results[$cid_lookup[$key]] = $value; - } - - return $cid_results; - } - -} diff --git a/web/modules/contrib/memcache/src/Invalidator/MemcacheTimestampInvalidator.php b/web/modules/contrib/memcache/src/Invalidator/MemcacheTimestampInvalidator.php new file mode 100644 index 000000000..56e8f1cfa --- /dev/null +++ b/web/modules/contrib/memcache/src/Invalidator/MemcacheTimestampInvalidator.php @@ -0,0 +1,62 @@ +<?php + +namespace Drupal\memcache\Invalidator; + +use Drupal\memcache\Driver\MemcacheDriverFactory; + +/** + * Class MemcacheTimestampInvalidator. + */ +class MemcacheTimestampInvalidator extends TimestampInvalidatorBase { + + /** + * A Memcache object. + * + * @var \Drupal\memcache\DrupalMemcacheInterface + */ + protected $memcache; + + /** + * MemcacheTimestampInvalidator constructor. + * + * @param \Drupal\memcache\Driver\MemcacheDriverFactory $memcache_factory + * Factory class for creation of Memcache objects. + * @param string $bin + * Memcache bin to store the timestamps in. + * @param float $tolerance + * Allowed clock skew between servers, in decimal seconds. + */ + public function __construct(MemcacheDriverFactory $memcache_factory, $bin, $tolerance = 0.001) { + parent::__construct($tolerance); + $this->memcache = $memcache_factory->get($bin); + } + + /** + * {@inheritdoc} + */ + public function invalidateTimestamp($tag) { + return $this->markAsOutdated($tag); + } + + /** + * {@inheritdoc} + */ + public function getLastInvalidationTimestamp($tag) { + return $this->memcache->get($tag); + } + + /** + * {@inheritdoc} + */ + public function getLastInvalidationTimestamps(array $tags) { + return $this->memcache->getMulti($tags); + } + + /** + * {@inheritdoc} + */ + protected function writeTimestamp($tag, $timestamp) { + return $this->memcache->set($tag, $timestamp); + } + +} diff --git a/web/modules/contrib/memcache/src/Invalidator/TimestampInvalidatorBase.php b/web/modules/contrib/memcache/src/Invalidator/TimestampInvalidatorBase.php new file mode 100644 index 000000000..5dbab7cc7 --- /dev/null +++ b/web/modules/contrib/memcache/src/Invalidator/TimestampInvalidatorBase.php @@ -0,0 +1,85 @@ +<?php + +namespace Drupal\memcache\Invalidator; + +/** + * Class TimestampInvalidatorBase. + * + * Base class for timestamp-based tag invalidation. + * + * @package Drupal\memcache\Invalidator + */ +abstract class TimestampInvalidatorBase implements TimestampInvalidatorInterface { + + /** + * Allowed timestamp slop. + * + * @var float + */ + protected $tolerance; + + /** + * TimestampInvalidatorBase constructor. + * + * @param float $tolerance + * Allowed clock skew between servers, in decimal seconds. + */ + public function __construct($tolerance = 0.001) { + $this->tolerance = $tolerance; + } + + /** + * Mark a tag as outdated. + * + * @param string $tag + * Tag to mark as outdated. + * + * @return float + * New timestamp for tag. + */ + protected function markAsOutdated($tag) { + $now = $this->getCurrentTimestamp($this->tolerance); + $current = $this->getLastInvalidationTimestamp($tag); + if ($now > $current) { + $this->writeTimestamp($tag, $now); + return $now; + } + else { + return $current; + } + } + + /** + * {@inheritdoc} + */ + public function getCurrentTimestamp($offset = 0.0) { + // @todo Eventually we might want to use a time service instead of microtime(). + // Unfortunately, TimeInterface needs a request object and we don't have + // that in the bootstrap container. + return round(microtime(TRUE) + $offset, 3); + } + + /** + * {@inheritdoc} + */ + abstract public function invalidateTimestamp($tag); + + /** + * {@inheritdoc} + */ + abstract public function getLastInvalidationTimestamps(array $tags); + + /** + * Write an updated timestamp for a tag to the backend. + * + * @param string $tag + * Tag to write. + * @param float $timestamp + * New timestamp to write. + * + * @return bool + * Success or failure from backend. + */ + abstract protected function writeTimestamp($tag, $timestamp); + +} diff --git a/web/modules/contrib/memcache/src/Invalidator/TimestampInvalidatorInterface.php b/web/modules/contrib/memcache/src/Invalidator/TimestampInvalidatorInterface.php new file mode 100644 index 000000000..2621609cb --- /dev/null +++ b/web/modules/contrib/memcache/src/Invalidator/TimestampInvalidatorInterface.php @@ -0,0 +1,61 @@ +<?php + +namespace Drupal\memcache\Invalidator; + +/** + * Interface TimestampInvalidatorInterface. + * + * Defines an interface for timestamp-based tag invalidation. + * + * @package Drupal\memcache\Invalidator + */ +interface TimestampInvalidatorInterface { + + /** + * Invalidate the timestamp of a tag. + * + * @param string $tag + * Tag to invalidate. + * + * @return float + * New timestamp of tag. + */ + public function invalidateTimestamp($tag); + + /** + * Get the last invalidation timestamp of a tag. + * + * @param string $tag + * Tag to check. + * + * @return float + * The last invalidation timestamp of the tag. + */ + public function getLastInvalidationTimestamp($tag); + + /** + * Get the last invalidation timestamps of a set of tags. + * + * @param array $tags + * Array of tags to check (keys are ignored.) + * + * @return array|bool + * The last invalidation timestamps on file, or FALSE on failure. + */ + public function getLastInvalidationTimestamps(array $tags); + + /** + * Get the current timestamp, optionally offset by a number. + * + * The standard granularity of the resulting timestamp is three decimal + * places, (1 millisecond). + * + * @param float $offset + * Offset to apply to timestamp before rounding. + * + * @return float + * Current timestamp in decimal seconds. + */ + public function getCurrentTimestamp($offset = 0.0); + +} diff --git a/web/modules/contrib/memcache/src/Lock/MemcacheLockBackend.php b/web/modules/contrib/memcache/src/Lock/MemcacheLockBackend.php index a2a7130f0..1490acfea 100644 --- a/web/modules/contrib/memcache/src/Lock/MemcacheLockBackend.php +++ b/web/modules/contrib/memcache/src/Lock/MemcacheLockBackend.php @@ -1,10 +1,5 @@ <?php -/** - * @file - * Contains \Drupal\memcache\MemcacheLockBackend. - */ - namespace Drupal\memcache\Lock; use Drupal\Core\Lock\LockBackendAbstract; @@ -20,7 +15,7 @@ class MemcacheLockBackend extends LockBackendAbstract { * * @var array */ - protected $locks = array(); + protected $locks = []; /** * The bin name for this lock. @@ -38,6 +33,11 @@ class MemcacheLockBackend extends LockBackendAbstract { /** * Constructs a new MemcacheLockBackend. + * + * @param string $bin + * The bin name for this lock. + * @param \Drupal\memcache\DrupalMemcacheInterface $memcache + * The memcache wrapper object. */ public function __construct($bin, DrupalMemcacheInterface $memcache) { $this->bin = $bin; @@ -57,11 +57,10 @@ class MemcacheLockBackend extends LockBackendAbstract { $timeout = (int) max($timeout, 1); $lock_id = $this->getLockId(); - $key = $this->getKey($name); if (isset($this->locks[$name])) { // Try to extend the expiration of a lock we already acquired. - $success = !$this->lockMayBeAvailable($name) && $this->memcache->set($key, $lock_id, $timeout); + $success = !$this->lockMayBeAvailable($name) && $this->memcache->set($name, $lock_id, $timeout); if (!$success) { // The lock was broken. @@ -72,7 +71,7 @@ class MemcacheLockBackend extends LockBackendAbstract { } else { if ($this->lockMayBeAvailable($name)) { - $success = $this->memcache->set($key, $lock_id, $timeout); + $success = $this->memcache->add($name, $lock_id, $timeout); if (!$success) { return FALSE; @@ -93,14 +92,14 @@ class MemcacheLockBackend extends LockBackendAbstract { * {@inheritdoc} */ public function lockMayBeAvailable($name) { - return !$this->memcache->get($this->getKey($name)); + return !$this->memcache->get($name); } /** * {@inheritdoc} */ public function release($name) { - $this->memcache->delete($this->getKey($name)); + $this->memcache->delete($name); // We unset unconditionally since caller assumes lock is released anyway. unset($this->locks[$name]); } @@ -114,22 +113,14 @@ class MemcacheLockBackend extends LockBackendAbstract { } foreach ($this->locks as $name => $id) { - $key = $this->getKey($name); - $value = $this->memcache->get($key); + $value = $this->memcache->get($name); if ($value == $lock_id) { - $this->memcache->delete($key); + $this->memcache->delete($name); } } $this->locks = []; } - /** - * Gets a storage key based on the lock name. - */ - protected function getKey($name) { - return 'lock:' . $this->bin . ':' . $name; - } - } diff --git a/web/modules/contrib/memcache/src/Lock/MemcacheLockFactory.php b/web/modules/contrib/memcache/src/Lock/MemcacheLockFactory.php index d3abda95b..936a9a893 100644 --- a/web/modules/contrib/memcache/src/Lock/MemcacheLockFactory.php +++ b/web/modules/contrib/memcache/src/Lock/MemcacheLockFactory.php @@ -1,13 +1,8 @@ <?php -/** - * @file - * Contains \Drupal\memcache\Lock\MemcacheLockFactory. - */ - namespace Drupal\memcache\Lock; -use Drupal\memcache\DrupalMemcacheFactory; +use Drupal\memcache\Driver\MemcacheDriverFactory; /** * THe memcache lock factory. @@ -24,16 +19,17 @@ class MemcacheLockFactory { /** * The memcache factory. * - * @var \Drupal\memcache\DrupalMemcacheFactory + * @var \Drupal\memcache\Driver\MemcacheDriverFactory */ protected $factory; /** - * Constructs a new MemcacheLockBackend. + * Constructs a new MemcacheLockFactory. * - * @param \Drupal\memcache\DrupalMemcacheFactory $memcache_factory + * @param \Drupal\memcache\Driver\MemcacheDriverFactory $memcache_factory + * The memcache factory. */ - public function __construct(DrupalMemcacheFactory $memcache_factory) { + public function __construct(MemcacheDriverFactory $memcache_factory) { $this->factory = $memcache_factory; } @@ -41,18 +37,10 @@ class MemcacheLockFactory { * Gets a lock backend instance. * * @return \Drupal\Core\Lock\LockBackendInterface + * A locked Memcache backend instance. */ public function get() { return new MemcacheLockBackend($this->bin, $this->factory->get($this->bin)); } - /** - * Gets a persistent lock backend instance. - * - * @return \Drupal\Core\Lock\LockBackendInterface - */ - public function getPersistent() { - return new PersistentMemcacheLockBackend($this->bin, $this->factory->get($this->bin)); - } - } diff --git a/web/modules/contrib/memcache/src/Lock/PersistentMemcacheLockBackend.php b/web/modules/contrib/memcache/src/Lock/PersistentMemcacheLockBackend.php deleted file mode 100644 index 5b00a6d62..000000000 --- a/web/modules/contrib/memcache/src/Lock/PersistentMemcacheLockBackend.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\memcache\PersistentMemcacheLockBackend. - */ - -namespace Drupal\memcache\Lock; - -use Drupal\memcache\DrupalMemcacheInterface; - -class PersistentMemcacheLockBackend extends MemcacheLockBackend { - - /** - * Constructs a new MemcacheLockBackend. - * - * @param \Drupal\Memcache\DrupalMemcacheInterface $memcache - */ - public function __construct($bin, DrupalMemcacheInterface $memcache) { - $this->bin = $bin; - - // Do not call the parent constructor to avoid registering a shutdwon - // function that will release all locks at the end of the request. - $this->memcache = $memcache; - // Set the lockId to a fixed string to make the lock ID the same across - // multiple requests. The lock ID is used as a page token to relate all the - // locks set during a request to each other. - // @see \Drupal\Core\Lock\LockBackendInterface::getLockId() - $this->lockId = 'persistent'; - } - -} diff --git a/web/modules/contrib/memcache/src/MemcacheBackend.php b/web/modules/contrib/memcache/src/MemcacheBackend.php index 13b6da731..d85de8e75 100644 --- a/web/modules/contrib/memcache/src/MemcacheBackend.php +++ b/web/modules/contrib/memcache/src/MemcacheBackend.php @@ -1,16 +1,11 @@ <?php -/** - * @file - * Contains \Drupal\memcache\MemcacheBackend. - */ - namespace Drupal\memcache; -use Drupal\Core\Cache\Cache; +use Drupal\Component\Assertion\Inspector; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsChecksumInterface; -use Drupal\Core\Lock\LockBackendInterface; +use Drupal\memcache\Invalidator\TimestampInvalidatorInterface; /** * Defines a Memcache cache backend. @@ -25,11 +20,11 @@ class MemcacheBackend implements CacheBackendInterface { protected $bin; /** - * The lock count. + * The (micro)time the bin was last deleted. * - * @var int + * @var float */ - protected $lockCount = 0; + protected $lastBinDeletionTime; /** * The memcache wrapper object. @@ -39,53 +34,45 @@ class MemcacheBackend implements CacheBackendInterface { protected $memcache; /** - * The lock backend that should be used. - * - * @var \Drupal\Core\Lock\LockBackendInterface - */ - protected $lock; - - /** - * The Settings instance. + * The cache tags checksum provider. * - * @var \Drupal\memcache\DrupalMemcacheConfig + * @var \Drupal\Core\Cache\CacheTagsChecksumInterface|\Drupal\Core\Cache\CacheTagsInvalidatorInterface */ - protected $settings; + protected $checksumProvider; /** - * The cache tags checksum provider. + * The timestamp invalidation provider. * - * @var \Drupal\Core\Cache\CacheTagsChecksumInterface + * @var \Drupal\memcache\Invalidator\TimestampInvalidatorInterface */ - protected $checksumProvider; + protected $timestampInvalidator; /** * Constructs a MemcacheBackend object. - *\Drupal\Core\Site\Settings + * * @param string $bin * The bin name. * @param \Drupal\memcache\DrupalMemcacheInterface $memcache * The memcache object. - * @param \Drupal\Core\Lock\LockBackendInterface $lock - * The lock backend. - * @param \Drupal\memcache\DrupalMemcacheConfig $settings - * The settings instance. * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider * The cache tags checksum service. + * @param \Drupal\memcache\Invalidator\TimestampInvalidatorInterface $timestamp_invalidator + * The timestamp invalidation provider. */ - public function __construct($bin, DrupalMemcacheInterface $memcache, LockBackendInterface $lock, DrupalMemcacheConfig $settings, CacheTagsChecksumInterface $checksum_provider) { + public function __construct($bin, DrupalMemcacheInterface $memcache, CacheTagsChecksumInterface $checksum_provider, TimestampInvalidatorInterface $timestamp_invalidator) { $this->bin = $bin; $this->memcache = $memcache; - $this->lock = $lock; - $this->settings = $settings; $this->checksumProvider = $checksum_provider; + $this->timestampInvalidator = $timestamp_invalidator; + + $this->ensureBinDeletionTimeIsSet(); } /** * {@inheritdoc} */ public function get($cid, $allow_invalid = FALSE) { - $cids = array($cid); + $cids = [$cid]; $cache = $this->getMultiple($cids, $allow_invalid); return reset($cache); } @@ -94,14 +81,14 @@ class MemcacheBackend implements CacheBackendInterface { * {@inheritdoc} */ public function getMultiple(&$cids, $allow_invalid = FALSE) { - $keys = array_map(function($cid) { - return $this->key($cid); - }, $cids); - - $cache = $this->memcache->getMulti($keys); + $cache = $this->memcache->getMulti($cids); $fetched = []; - foreach ($cache as $key => $result) { + foreach ($cache as $result) { + if (!$this->timeIsGreaterThanBinDeletionTime($result->created)) { + continue; + } + if ($this->valid($result->cid, $result) || $allow_invalid) { // Add it to the fetched items to diff later. $fetched[$result->cid] = $result; @@ -126,51 +113,14 @@ class MemcacheBackend implements CacheBackendInterface { * The cache item. * * @return bool + * TRUE if valid, FALSE otherwise. */ protected function valid($cid, \stdClass $cache) { - $lock_key = "memcache_$cid:$this->bin"; - $cache->valid = FALSE; - - if ($cache) { - // Items that have expired are invalid. - if (isset($cache->expire) && ($cache->expire != CacheBackendInterface::CACHE_PERMANENT) && ($cache->expire <= REQUEST_TIME)) { - // If the memcache_stampede_protection variable is set, allow one - // process to rebuild the cache entry while serving expired content to - // the rest. - if ($this->settings->get('stampede_protection', FALSE)) { - // The process that acquires the lock will get a cache miss, all - // others will get a cache hit. - if (!$this->lock->acquire($lock_key, $this->settings->get('stampede_semaphore', 15))) { - $cache->valid = TRUE; - } - } - } - else { - $cache->valid = TRUE; - } - } - // On cache misses, attempt to avoid stampedes when the - // memcache_stampede_protection variable is enabled. - else { - if ($this->settings->get('stampede_protection', FALSE) && !$this->lock->acquire($lock_key, $this->settings->get('stampede_semaphore', 15))) { - // Prevent any single request from waiting more than three times due to - // stampede protection. By default this is a maximum total wait of 15 - // seconds. This accounts for two possibilities - a cache and lock miss - // more than once for the same item. Or a cache and lock miss for - // different items during the same request. - // @todo: it would be better to base this on time waited rather than - // number of waits, but the lock API does not currently provide this - // information. Currently the limit will kick in for three waits of 25ms - // or three waits of 5000ms. - $this->lockCount++; - if ($this->lockCount <= $this->settings->get('stampede_wait_limit', 3)) { - // The memcache_stampede_semaphore variable was used in previous - // releases of memcache, but the max_wait variable was not, so by - // default divide the semaphore value by 3 (5 seconds). - $this->lock->wait($lock_key, $this->settings->get('stampede_wait_time', 5)); - $cache = $this->get($cid); - } - } + $cache->valid = TRUE; + + // Items that have expired are invalid. + if ($cache->expire != CacheBackendInterface::CACHE_PERMANENT && $cache->expire <= REQUEST_TIME) { + $cache->valid = FALSE; } // Check if invalidateTags() has been called with any of the items's tags. @@ -178,14 +128,16 @@ class MemcacheBackend implements CacheBackendInterface { $cache->valid = FALSE; } - return (bool) $cache->valid; + return $cache->valid; } /** * {@inheritdoc} */ - public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array()) { - assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)'); + public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = []) { + assert(Inspector::assertAllStrings($tags)); + + $tags[] = "memcache:$this->bin"; $tags = array_unique($tags); // Sort the cache tags so that they are stored consistently. sort($tags); @@ -193,14 +145,14 @@ class MemcacheBackend implements CacheBackendInterface { // Create new cache object. $cache = new \stdClass(); $cache->cid = $cid; - $cache->data = is_object($data) ? clone $data : $data; + $cache->data = $data; $cache->created = round(microtime(TRUE), 3); $cache->expire = $expire; $cache->tags = $tags; $cache->checksum = $this->checksumProvider->getCurrentChecksum($tags); // Cache all items permanently. We handle expiration in our own logic. - return $this->memcache->set($this->key($cid), $cache); + return $this->memcache->set($cid, $cache); } /** @@ -208,10 +160,10 @@ class MemcacheBackend implements CacheBackendInterface { */ public function setMultiple(array $items) { foreach ($items as $cid => $item) { - $item += array( + $item += [ 'expire' => CacheBackendInterface::CACHE_PERMANENT, - 'tags' => array(), - ); + 'tags' => [], + ]; $this->set($cid, $item['data'], $item['expire'], $item['tags']); } @@ -221,7 +173,7 @@ class MemcacheBackend implements CacheBackendInterface { * {@inheritdoc} */ public function delete($cid) { - $this->memcache->delete($this->key($cid)); + $this->memcache->delete($cid); } /** @@ -229,7 +181,7 @@ class MemcacheBackend implements CacheBackendInterface { */ public function deleteMultiple(array $cids) { foreach ($cids as $cid) { - $this->memcache->delete($this->key($cid)); + $this->memcache->delete($cid); } } @@ -237,15 +189,14 @@ class MemcacheBackend implements CacheBackendInterface { * {@inheritdoc} */ public function deleteAll() { - // Invalidate all keys, as we can't actually delete all? - $this->invalidateAll(); + $this->lastBinDeletionTime = $this->timestampInvalidator->invalidateTimestamp($this->bin); } /** * {@inheritdoc} */ public function invalidate($cid) { - $this->invalidateMultiple((array) $cid); + $this->invalidateMultiple([$cid]); } /** @@ -266,7 +217,7 @@ class MemcacheBackend implements CacheBackendInterface { foreach ($cids as $cid) { if ($item = $this->get($cid)) { $item->expire = REQUEST_TIME - 1; - $this->memcache->set($this->key($cid), $item); + $this->memcache->set($cid, $item); } } } @@ -275,7 +226,7 @@ class MemcacheBackend implements CacheBackendInterface { * {@inheritdoc} */ public function invalidateAll() { - $this->memcache->flush(); + $this->invalidateTags(["memcache:$this->bin"]); } /** @@ -289,7 +240,7 @@ class MemcacheBackend implements CacheBackendInterface { * {@inheritdoc} */ public function removeBin() { - // Do nothing here too? + $this->lastBinDeletionTime = $this->timestampInvalidator->invalidateTimestamp($this->bin); } /** @@ -301,7 +252,7 @@ class MemcacheBackend implements CacheBackendInterface { } /** - * (@inheritdoc) + * {@inheritdoc} */ public function isEmpty() { // We do not know so err on the safe side? Not sure if we can know this? @@ -309,14 +260,54 @@ class MemcacheBackend implements CacheBackendInterface { } /** - * Returns a cache key prefixed with the current bin. + * Determines if a (micro)time is greater than the last bin deletion time. * - * @param string $cid + * @param float $item_microtime + * A given (micro)time. + * + * @internal + * + * @return bool + * TRUE if the (micro)time is greater than the last bin deletion time, FALSE + * otherwise. + */ + protected function timeIsGreaterThanBinDeletionTime($item_microtime) { + $last_bin_deletion = $this->getBinLastDeletionTime(); + + // If there is time, assume FALSE as there is no previous deletion time + // to compare with. + if (!$last_bin_deletion) { + return FALSE; + } + + return $item_microtime > $last_bin_deletion; + } + + /** + * Gets the last invalidation time for the bin. + * + * @internal * - * @return string + * @return float + * The last invalidation timestamp of the tag. */ - protected function key($cid) { - return $this->bin . '-' . $cid; + protected function getBinLastDeletionTime() { + if (!isset($this->lastBinDeletionTime)) { + $this->lastBinDeletionTime = $this->timestampInvalidator->getLastInvalidationTimestamp($this->bin); + } + + return $this->lastBinDeletionTime; + } + + /** + * Ensures a last bin deletion time has been set. + * + * @internal + */ + protected function ensureBinDeletionTimeIsSet() { + if (!$this->getBinLastDeletionTime()) { + $this->lastBinDeletionTime = $this->timestampInvalidator->invalidateTimestamp($this->bin); + } } } diff --git a/web/modules/contrib/memcache/src/MemcacheBackendFactory.php b/web/modules/contrib/memcache/src/MemcacheBackendFactory.php index 04f0c6f8a..ea9581786 100644 --- a/web/modules/contrib/memcache/src/MemcacheBackendFactory.php +++ b/web/modules/contrib/memcache/src/MemcacheBackendFactory.php @@ -1,38 +1,21 @@ <?php -/** - * @file - * Contains \Drupal\memcache\MemcacheBackendFactory. - */ - namespace Drupal\memcache; -use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\Cache\CacheFactoryInterface; use Drupal\Core\Cache\CacheTagsChecksumInterface; +use Drupal\memcache\Driver\MemcacheDriverFactory; +use Drupal\memcache\Invalidator\TimestampInvalidatorInterface; /** - * Class DatabaseBackendFactory. + * Class MemcacheBackendFactory. */ -class MemcacheBackendFactory { - - /** - * The lock backend that should be used. - * - * @var \Drupal\Core\Lock\LockBackendInterface - */ - protected $lock; - - /** - * The settings object. - * - * @var \Drupal\memcache\DrupalMemcacheConfig - */ - protected $settings; +class MemcacheBackendFactory implements CacheFactoryInterface { /** * The memcache factory object. * - * @var \Drupal\memcache\DrupalMemcacheFactory + * @var \Drupal\memcache\Driver\MemcacheDriverFactory */ protected $memcacheFactory; @@ -44,23 +27,32 @@ class MemcacheBackendFactory { protected $checksumProvider; /** - * Constructs the DatabaseBackendFactory object. + * The timestamp invalidation provider. + * + * @var \Drupal\memcache\Invalidator\TimestampInvalidatorInterface + */ + protected $timestampInvalidator; + + /** + * Constructs the MemcacheBackendFactory object. * - * @param \Drupal\Core\Lock\LockBackendInterface $lock - * @param \Drupal\memcache\DrupalMemcacheConfig $settings - * @param \Drupal\memcache\DrupalMemcacheFactory $memcache_factory + * @param \Drupal\memcache\Driver\MemcacheDriverFactory $memcache_factory + * The memcache factory object. + * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider + * The cache tags checksum provider. + * @param \Drupal\memcache\Invalidator\TimestampInvalidatorInterface $timestamp_invalidator + * The timestamp invalidation provider. */ - function __construct(LockBackendInterface $lock, DrupalMemcacheConfig $settings, DrupalMemcacheFactory $memcache_factory, CacheTagsChecksumInterface $checksum_provider) { - $this->lock = $lock; - $this->settings = $settings; + public function __construct(MemcacheDriverFactory $memcache_factory, CacheTagsChecksumInterface $checksum_provider, TimestampInvalidatorInterface $timestamp_invalidator) { $this->memcacheFactory = $memcache_factory; $this->checksumProvider = $checksum_provider; + $this->timestampInvalidator = $timestamp_invalidator; } /** * Gets MemcacheBackend for the specified cache bin. * - * @param $bin + * @param string $bin * The cache bin for which the object is created. * * @return \Drupal\memcache\MemcacheBackend @@ -70,9 +62,8 @@ class MemcacheBackendFactory { return new MemcacheBackend( $bin, $this->memcacheFactory->get($bin), - $this->lock, - $this->settings, - $this->checksumProvider + $this->checksumProvider, + $this->timestampInvalidator ); } diff --git a/web/modules/contrib/memcache/src/MemcacheException.php b/web/modules/contrib/memcache/src/MemcacheException.php index 9cce2b24d..94e41e668 100644 --- a/web/modules/contrib/memcache/src/MemcacheException.php +++ b/web/modules/contrib/memcache/src/MemcacheException.php @@ -1,10 +1,5 @@ <?php -/** - * @file - * Contains \Drupal\memcache\MemcacheException. - */ - namespace Drupal\memcache; /** diff --git a/web/modules/contrib/memcache/src/DrupalMemcacheConfig.php b/web/modules/contrib/memcache/src/MemcacheSettings.php similarity index 89% rename from web/modules/contrib/memcache/src/DrupalMemcacheConfig.php rename to web/modules/contrib/memcache/src/MemcacheSettings.php index c9d9fc0a3..7f26aa465 100644 --- a/web/modules/contrib/memcache/src/DrupalMemcacheConfig.php +++ b/web/modules/contrib/memcache/src/MemcacheSettings.php @@ -1,18 +1,13 @@ <?php -/** - * @file - * Contains \Drupal\memcache\DrupalMemcacheConfig. - */ - namespace Drupal\memcache; use Drupal\Core\Site\Settings; /** - * Class for holding Memcache related config + * Class for holding Memcache related config. */ -class DrupalMemcacheConfig { +class MemcacheSettings { /** * Array with the settings. @@ -60,4 +55,5 @@ class DrupalMemcacheConfig { public function getAll() { return $this->settings; } + } diff --git a/web/modules/contrib/memcache/src/Tests/MemcacheLockFunctionalTest.php b/web/modules/contrib/memcache/src/Tests/MemcacheLockFunctionalTest.php index 7eeed701b..2e69078d3 100644 --- a/web/modules/contrib/memcache/src/Tests/MemcacheLockFunctionalTest.php +++ b/web/modules/contrib/memcache/src/Tests/MemcacheLockFunctionalTest.php @@ -1,10 +1,5 @@ <?php -/** - * @file - * Contains \Drupal\memcache\Tests\MemcacheLockFunctionalTest. - */ - namespace Drupal\memcache\Tests; use Drupal\Tests\system\Functional\Lock\LockFunctionalTest; @@ -24,4 +19,3 @@ class MemcacheLockFunctionalTest extends LockFunctionalTest { public static $modules = ['system_test', 'memcache', 'memcache_test']; } - diff --git a/web/modules/contrib/memcache/tests/modules/memcache_test/memcache_test.info.yml b/web/modules/contrib/memcache/tests/modules/memcache_test/memcache_test.info.yml index 8bea12f4d..0e32e75c0 100644 --- a/web/modules/contrib/memcache/tests/modules/memcache_test/memcache_test.info.yml +++ b/web/modules/contrib/memcache/tests/modules/memcache_test/memcache_test.info.yml @@ -2,13 +2,12 @@ name: 'Memcache test' type: module description: 'Support module for memcache testing.' package: Testing -# version: VERSION # core: 8.x dependencies: - memcache -# Information added by Drupal.org packaging script on 2017-10-18 -version: '8.x-2.0-alpha5' +# Information added by Drupal.org packaging script on 2018-10-26 +version: '8.x-2.0' core: '8.x' project: 'memcache' -datestamp: 1508351354 +datestamp: 1540546685 diff --git a/web/modules/contrib/memcache/tests/modules/memcache_test/src/MemcacheTestServiceProvider.php b/web/modules/contrib/memcache/tests/modules/memcache_test/src/MemcacheTestServiceProvider.php index ebd3033ec..77f00e3be 100644 --- a/web/modules/contrib/memcache/tests/modules/memcache_test/src/MemcacheTestServiceProvider.php +++ b/web/modules/contrib/memcache/tests/modules/memcache_test/src/MemcacheTestServiceProvider.php @@ -20,11 +20,6 @@ class MemcacheTestServiceProvider implements ServiceModifierInterface { $definition->setFactory([new Reference('memcache.lock.factory'), 'get']); $container->setDefinition('lock', $definition); - - $definition = new Definition('Drupal\Core\Lock\LockBackendInterface'); - $definition->setFactory([new Reference('memcache.lock.factory'), 'getPersistent']); - - $container->setDefinition('lock.persistent', $definition); } } diff --git a/web/modules/contrib/memcache/src/Tests/MemcacheBackendUnitTest.php b/web/modules/contrib/memcache/tests/src/Kernel/MemcacheBackendTest.php similarity index 56% rename from web/modules/contrib/memcache/src/Tests/MemcacheBackendUnitTest.php rename to web/modules/contrib/memcache/tests/src/Kernel/MemcacheBackendTest.php index 816867be8..63459147b 100644 --- a/web/modules/contrib/memcache/src/Tests/MemcacheBackendUnitTest.php +++ b/web/modules/contrib/memcache/tests/src/Kernel/MemcacheBackendTest.php @@ -1,21 +1,16 @@ <?php -/** - * @file - * Contains \Drupal\memcache\Tests\MemcacheBackendUnitTest. - */ - -namespace Drupal\memcache\Tests; +namespace Drupal\Tests\memcache\Kernel; +use Drupal\KernelTests\Core\Cache\GenericCacheBackendUnitTestBase; use Drupal\memcache\MemcacheBackendFactory; -use Drupal\system\Tests\Cache\GenericCacheBackendUnitTestBase; /** * Tests the MemcacheBackend. * * @group memcache */ -class MemcacheBackendUnitTest extends GenericCacheBackendUnitTestBase { +class MemcacheBackendTest extends GenericCacheBackendUnitTestBase { /** * Modules to enable. @@ -31,7 +26,7 @@ class MemcacheBackendUnitTest extends GenericCacheBackendUnitTestBase { * A new MemcacheBackend object. */ protected function createCacheBackend($bin) { - $factory = new MemcacheBackendFactory($this->container->get('lock'), $this->container->get('memcache.config'), $this->container->get('memcache.factory'), $this->container->get('cache_tags.invalidator.checksum')); + $factory = new MemcacheBackendFactory($this->container->get('memcache.factory'), $this->container->get('cache_tags.invalidator.checksum')); return $factory->get($bin); } diff --git a/web/modules/contrib/memcache/tests/src/Unit/DrupalMemcacheConfigTest.php b/web/modules/contrib/memcache/tests/src/Unit/MemcacheSettingsTest.php similarity index 76% rename from web/modules/contrib/memcache/tests/src/Unit/DrupalMemcacheConfigTest.php rename to web/modules/contrib/memcache/tests/src/Unit/MemcacheSettingsTest.php index f05d27002..29421a1ab 100644 --- a/web/modules/contrib/memcache/tests/src/Unit/DrupalMemcacheConfigTest.php +++ b/web/modules/contrib/memcache/tests/src/Unit/MemcacheSettingsTest.php @@ -1,21 +1,16 @@ <?php -/** - * @file - * Contains \Drupal\memcache\Tests\DrupalMemcacheConfigTest. - */ - namespace Drupal\Tests\memcache\Unit; -use Drupal\memcache\DrupalMemcacheConfig; +use Drupal\memcache\MemcacheSettings; use Drupal\Core\Site\Settings; use Drupal\Tests\UnitTestCase; /** - * @coversDefaultClass \Drupal\memcache\DrupalMemcacheConfig + * @coversDefaultClass \Drupal\memcache\MemcacheSettings * @group memcache */ -class DrupalMemcacheConfigTest extends UnitTestCase { +class MemcacheSettingsTest extends UnitTestCase { /** * Simple settings array to test against. @@ -27,23 +22,23 @@ class DrupalMemcacheConfigTest extends UnitTestCase { /** * The class under test. * - * @var \Drupal\memcache\DrupalMemcacheConfig + * @var \Drupal\memcache\MemcacheSettings */ protected $settings; /** * @covers ::__construct */ - protected function setUp(){ + protected function setUp() { $this->config = [ 'memcache' => [ 'servers' => ['127.0.0.2:12345' => 'default'], - 'bin' => ['default' => 'default'] + 'bin' => ['default' => 'default'], ], 'hash_salt' => $this->randomMachineName(), ]; $settings = new Settings($this->config); - $this->settings = new DrupalMemcacheConfig($settings); + $this->settings = new MemcacheSettings($settings); } /** @@ -65,4 +60,5 @@ class DrupalMemcacheConfigTest extends UnitTestCase { public function testGetAll() { $this->assertEquals($this->config['memcache'], $this->settings->getAll()); } + } diff --git a/web/modules/contrib/metatag/CHANGELOG.txt b/web/modules/contrib/metatag/CHANGELOG.txt index 70faf127e..512dc5927 100644 --- a/web/modules/contrib/metatag/CHANGELOG.txt +++ b/web/modules/contrib/metatag/CHANGELOG.txt @@ -1,3 +1,64 @@ +Metatag 8.x-1.7, 2018-08-31 +--------------------------- +#2994979 by DamienMcKenna, dspachos, ynotpeanutbutter, oxy86, IT-Cru, kdeds, + zenimagine: Fixed backwards compatibility break when support for multiple- + value tags was added. +#2990923 by th_tushar, DamienMcKenna: Fixed coding standards. + + +Metatag 8.x-1.6, 2018-08-21 +--------------------------- +#2961777 by thejimbirch, Baysaa: "Geographical position" (geo.position) should + use semi-colon instead of comma. +#2964626 by idebr: og:image:secure_url allows for multiple values. +#2865267 by DamienMcKenna, okonvicka, aldibier: <title> tags empty after + installing Metatag. +#2962426 by dragonwize: Image URL double encoded. +#2973277 by mvantuch, DamienMcKenna: Support for og:image:alt. +#2977002 by DamienMcKenna, maxoid: Enable testing on the two og product meta + tags. +#2977545 by pmelab, bappa.sarkar, DamienMcKenna: GraphQL throws an error due to + Metatag's use of a custom data structure. +#2925714 by acbramley, vakulrai, Berdir, nkoporec: Replace deprecated + BaseFieldDefinition ::setQueryable. +#2957411 by pmelab: Field item empty check on empty arrays. +#2958743 by DamienMcKenna, AdamEvertsson, StepanISK, trong.nguyen.tcec, + chiefme, quixxel, rjg, CProfessionals: Ignore update.php requests when + loading entities; error message when updating from 8.x-1.0 to 8.x-1.5. +#2961918 by thejimbirch, Michelle: Add the site.webmanifest meta tag. +#2895577 by plopesc, drupallogic: Error loading + /admin/config/search/metatag?page=1. +#2943954 by thejimbirch, DamienMcKenna: Document an approach to simplify + overriding meta tags on a per-entity basis. +#2968902 by DamienMcKenna, Berdir: Default user page title token should use + [user:display-name]. +#2977197 by justin., DamienMcKenna: Allow modules to override the entity used + for token replacements. +#2691313 by KarenS, carstenG, DamienMcKenna, Michelle, marysmech: New option to + control which meta tag groups are used on each form. +#2563657 by plopesc, scotthooker, dawehner, ryanissamson, KevinVb, dobrzyns, + PieterDC, peter.keppert, piggito, DamienMcKenna, Grayle, kunal.kursija, + Citizen Dan, al0a, kaushashah, yohanaraujo07@gmail.com, markandrewsutton, + fuzzyjared, igalafate, PascoS, rybchynski: Panels / Page Manager + integration. +#2989295 by JKerschner: Coding style improvements. +#2987852 by Chris Burge: Allow meta tags to be altered before page attachment. +#2989543 by JKerschner: Remove unused variables and imports. +#2753595 by sinn, thejimbirch: W3C validation error on xmlns attribute. +#2988072 by juanolalla: Allow contex entity to be overriden in alter hook. +#2628934 by nikunjkotecha, smaz, e.escribano, idebr, DamienMcKenna, + ZapevalovAnton, caspervoogt: Full support for meta tags that allow multiple + values. +#2987904 by thejimbirch: Remove groups redundantly stored in the main module. +#2987107 by ziomizar: Add support for bubbeable metadata in the MetatagToken + service. +#2978106 by Rolf van de Krol: Translated metatags not showing in normalized + entity representations (i.e. REST). +#2799861 by kalpaitch, DamienMcKenna, dmitry.yankouski, ckaotik: Canonical links + repeated when using Panels pages. +#2532596 by alzz, DamienMcKenna, thejimbirch: Add new meta tag: google. + + Metatag 8.x-1.5, 2018-03-29 --------------------------- #2932596 by yo30, Punk_UnDeaD: Wrong method in abstract class LinkSizesBase. diff --git a/web/modules/contrib/metatag/README.txt b/web/modules/contrib/metatag/README.txt index 4f7da5132..da936bf71 100644 --- a/web/modules/contrib/metatag/README.txt +++ b/web/modules/contrib/metatag/README.txt @@ -115,6 +115,28 @@ Standard usage scenario Drupal's translation system. +Simplify the content administration experience +-------------------------------------------------------------------------------- +This module and its submodules gives a site's content team the ability to add +every meta tag ever. The standard meta tag form added by the Metatag field on +content entities can be overwhelming to content creators and editors who just +need to manage a few options. + +The easiest way of simplifying this for content teams is to add new fields to +the content type for the meta data fields that are needed and skip adding the +Metatag field entirely, then use tokens for those fields in the defaults +(/admin/config/search/metatag). These fields can be used in the entity's +display, or just left hidden. + + +Alternative option to simplify the content administration experience +-------------------------------------------------------------------------------- +On the settings page (/admin/config/search/metatag/settings) are options to +control which meta tag groups are available for each entity bundle. This allows +e.g. the Favicon meta tags to be available for global configurations but to hide +them on entity forms. + + Programmatically assign meta tags to an entity -------------------------------------------------------------------------------- There are two ways to assign an entity's meta tags in custom module. Both diff --git a/web/modules/contrib/metatag/composer.json b/web/modules/contrib/metatag/composer.json index 63bf22789..0e7921859 100644 --- a/web/modules/contrib/metatag/composer.json +++ b/web/modules/contrib/metatag/composer.json @@ -25,6 +25,8 @@ "require-dev": { "drupal/devel": "^1.0", "drupal/redirect": "^1.0", - "drupal/restui": "^1.0" + "drupal/restui": "^1.0", + "drupal/page_manager": "^4.0", + "drupal/schema_metatag": "^1.0" } } diff --git a/web/modules/contrib/metatag/config/install/metatag.metatag_defaults.user.yml b/web/modules/contrib/metatag/config/install/metatag.metatag_defaults.user.yml index 5ce0b3fa0..06dfbe3d1 100644 --- a/web/modules/contrib/metatag/config/install/metatag.metatag_defaults.user.yml +++ b/web/modules/contrib/metatag/config/install/metatag.metatag_defaults.user.yml @@ -6,4 +6,4 @@ label: User tags: canonical_url: '[user:url]' description: '[site:name]' - title: '[user:name] | [site:name]' + title: '[user:display-name] | [site:name]' diff --git a/web/modules/contrib/metatag/config/schema/metatag.metatag_tag.schema.yml b/web/modules/contrib/metatag/config/schema/metatag.metatag_tag.schema.yml index 5f6f30c5c..19c84e429 100644 --- a/web/modules/contrib/metatag/config/schema/metatag.metatag_tag.schema.yml +++ b/web/modules/contrib/metatag/config/schema/metatag.metatag_tag.schema.yml @@ -62,3 +62,6 @@ metatag.metatag_tag.geo_position: metatag.metatag_tag.set_cookie: type: label label: 'Set Cookie' +metatag.metatag_tag.google: + type: label + label: 'Google' diff --git a/web/modules/contrib/metatag/config/schema/metatag.settings.schema.yml b/web/modules/contrib/metatag/config/schema/metatag.settings.schema.yml new file mode 100644 index 000000000..e7dd55550 --- /dev/null +++ b/web/modules/contrib/metatag/config/schema/metatag.settings.schema.yml @@ -0,0 +1,7 @@ +metatag.settings: + type: mapping + label: 'Metatag settings' + mapping: + entity_type_groups: + type: mapping + label: 'Metatag groups that apply to each entity type' diff --git a/web/modules/contrib/metatag/metatag.api.php b/web/modules/contrib/metatag/metatag.api.php index cbdee9ed7..cd2ad01d8 100644 --- a/web/modules/contrib/metatag/metatag.api.php +++ b/web/modules/contrib/metatag/metatag.api.php @@ -31,11 +31,29 @@ function hook_metatag_route_entity(\Drupal\Core\Routing\RouteMatchInterface $rou * @param array $metatags * The special meta tags to be added to the page. * @param array $context - * The context, containing the entity used for token replacements. + * The context for the current meta tags being generated. Will contain the + * following: + * 'entity' - The entity being processed; passed by reference. */ -function hook_metatags_alter(array &$metatags, array $context) { +function hook_metatags_alter(array &$metatags, array &$context) { // Exclude meta tags on frontpage. if (\Drupal::service('path.matcher')->isFrontPage()) { $metatags = NULL; } } + +/** + * Alter the meta tags for any page prior to page attachment. + * + * @param array $metatag_attachments + * An array of metatag objects to be attached to the current page. + */ +function hook_metatags_attachments_alter(array &$metatag_attachments) { + if (\Drupal::service('path.matcher')->isFrontPage() && \Drupal::currentUser()->isAnonymous()) { + foreach ($metatag_attachments['#attached']['html_head'] as $id => $attachment) { + if ($attachment[1] == 'title') { + $metatag_attachments['#attached']['html_head'][$id][0]['#attributes']['content'] = 'Front Page Title for Anonymous Users'; + } + } + } +} diff --git a/web/modules/contrib/metatag/metatag.info.yml b/web/modules/contrib/metatag/metatag.info.yml index a7bec9f56..37efe692b 100644 --- a/web/modules/contrib/metatag/metatag.info.yml +++ b/web/modules/contrib/metatag/metatag.info.yml @@ -11,9 +11,10 @@ test_dependencies: - devel:devel - redirect:redirect - restui:restui + - schema_metatag:schema_web_page -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag.install b/web/modules/contrib/metatag/metatag.install index 5fe86ac5d..2f96a4941 100644 --- a/web/modules/contrib/metatag/metatag.install +++ b/web/modules/contrib/metatag/metatag.install @@ -59,7 +59,7 @@ function metatag_update_8101() { ->getStorage('field_storage_config') ->loadByProperties(['type' => 'metatag']); - foreach ($field_storage_configs as $key => $field_storage) { + foreach ($field_storage_configs as $field_storage) { $field_name = $field_storage->getName(); // Get the individual fields (field instances) associated with bundles. @@ -75,7 +75,6 @@ function metatag_update_8101() { // Get the default value for this field on this bundle. $field_default_tags_value = $field->getDefaultValueLiteral(); $field_default_tags_value = $field_default_tags_value[0]['value']; - $field_default_tags = unserialize($field_default_tags_value); // Determine the table and "value" field names. $field_table = "node__" . $field_name; @@ -112,7 +111,7 @@ function metatag_update_8102(&$sandbox) { ->getStorage('field_storage_config') ->loadByProperties(['type' => 'metatag']); - foreach ($field_storage_configs as $key => $field_storage) { + foreach ($field_storage_configs as $field_storage) { $field_name = $field_storage->getName(); // Get the individual fields (field instances) associated with bundles. @@ -286,7 +285,7 @@ function metatag_update_8103() { ->getStorage('field_storage_config') ->loadByProperties(['type' => 'metatag']); - foreach ($field_storage_configs as $key => $field_storage) { + foreach ($field_storage_configs as $field_storage) { $field_name = $field_storage->getName(); // Get the individual fields (field instances) associated with bundles. diff --git a/web/modules/contrib/metatag/metatag.links.menu.yml b/web/modules/contrib/metatag/metatag.links.menu.yml index 438984e4e..f1fef7187 100644 --- a/web/modules/contrib/metatag/metatag.links.menu.yml +++ b/web/modules/contrib/metatag/metatag.links.menu.yml @@ -4,3 +4,8 @@ entity.metatag_defaults.collection: route_name: entity.metatag_defaults.collection description: 'Configure Metatag defaults.' parent: system.admin_config_search +metatag.settings: + title: 'Settings' + route_name: metatag.settings + description: 'Configure Metatag defaults.' + parent: entity.metatag_defaults.collection diff --git a/web/modules/contrib/metatag/metatag.links.task.yml b/web/modules/contrib/metatag/metatag.links.task.yml new file mode 100644 index 000000000..637e9beb5 --- /dev/null +++ b/web/modules/contrib/metatag/metatag.links.task.yml @@ -0,0 +1,10 @@ +metatag_defaults: + route_name: 'entity.metatag_defaults.collection' + base_route: 'entity.metatag_defaults.collection' + title: 'Metatag defaults' + weight: 0 +metatag.settings: + route_name: 'metatag.settings' + base_route: 'entity.metatag_defaults.collection' + title: 'Settings' + weight: 10 diff --git a/web/modules/contrib/metatag/metatag.module b/web/modules/contrib/metatag/metatag.module index 2732fcc06..b9fad84a6 100644 --- a/web/modules/contrib/metatag/metatag.module +++ b/web/modules/contrib/metatag/metatag.module @@ -80,9 +80,9 @@ function metatag_form_field_config_edit_form_alter(&$form, FormStateInterface $f // Step through the default value structure and erase any '#default_value' // items that are found. - foreach ($form['default_value']['widget'][0] as $key => &$outer) { + foreach ($form['default_value']['widget'][0] as &$outer) { if (is_array($outer)) { - foreach ($outer as $key => &$inner) { + foreach ($outer as &$inner) { if (is_array($inner) && isset($inner['#default_value'])) { if (is_array($inner['#default_value'])) { $inner['#default_value'] = []; @@ -117,6 +117,10 @@ function metatag_page_attachments(array &$attachments) { return NULL; } + // Trigger hook_metatags_attachments_alter(). + // Allow modules to rendered metatags prior to attaching. + \Drupal::service('module_handler')->alter('metatags_attachments', $metatag_attachments); + // If any Metatag items were found, append them. if (!empty($metatag_attachments['#attached']['html_head'])) { if (empty($attachments['#attached'])) { @@ -193,9 +197,32 @@ function metatag_entity_view_alter(array &$build, EntityInterface $entity, Entit return; } + // Panelized entities still use the original entity's controller, but with + // custom built entities. In those cases hook_entity_view_alter might be + // called too early, where meta links are not yet set. + // @see \Drupal\node\Controller\NodeController::view + if ($display->getThirdPartySetting('panelizer', 'enable', FALSE)) { + $build['#pre_render'][] = '_metatag_panelizer_pre_render'; + return; + } + _metatag_remove_duplicate_entity_tags($build); } +/** + * Pre render callback for entities processed by Panelizer. + * + * @param array $element + * The render array being processed. + * + * @return array + * The filtered render array. + */ +function _metatag_panelizer_pre_render(array $element) { + _metatag_remove_duplicate_entity_tags($element); + return $element; +} + /** * Remove duplicate entity tags from a build. * @@ -377,18 +404,10 @@ function metatag_preprocess_html(&$variables) { if (!empty($attachments['#attached']['html_head'])) { foreach ($attachments['#attached']['html_head'] as $key => $attachment) { if (!empty($attachment[1]) && $attachment[1] == 'title') { - // It's safe to access the value directly because it was already - // processed in MetatagManager::generateElements(). - $variables['head_title_array'] = []; // Empty head_title to avoid the site name and slogan to be appended to // the meta title. $variables['head_title'] = []; $variables['head_title']['title'] = html_entity_decode($attachment[0]['#attributes']['content'], ENT_QUOTES); - // Original: - // $variables['head_title_array']['title'] = - // $attachment[0]['#attributes']['content']; - // $variables['head_title'] = implode(' | ', - // $variables['head_title_array']); break; } } @@ -430,10 +449,15 @@ function metatag_get_tags_from_route($entity = NULL) { // Trigger hook_metatags_alter(). // Allow modules to override tags or the entity used for token replacements. $context = [ - 'entity' => $entity, + 'entity' => &$entity, ]; \Drupal::service('module_handler')->alter('metatags', $metatags, $context); + // If the entity was changed above, use that for generating the meta tags. + if (isset($context['entity'])) { + $entity = $context['entity']; + } + return $metatag_manager->generateElements($metatags, $entity); } @@ -503,8 +527,8 @@ function metatag_entity_base_field_info(EntityTypeInterface $entity_type) { ->setLabel(t('Metatags')) ->setDescription(t('The meta tags for the entity.')) ->setClass('\Drupal\metatag\Plugin\Field\MetatagEntityFieldItemList') - ->setQueryable(FALSE) ->setComputed(TRUE) + ->setTranslatable(TRUE) ->setTargetEntityTypeId($entity_type->id()); } diff --git a/web/modules/contrib/metatag/metatag.routing.yml b/web/modules/contrib/metatag/metatag.routing.yml index 7d839b812..d809e4dbc 100644 --- a/web/modules/contrib/metatag/metatag.routing.yml +++ b/web/modules/contrib/metatag/metatag.routing.yml @@ -48,3 +48,13 @@ entity.metatag_defaults.revert_form: _permission: 'administer meta tags' options: _admin_route: TRUE + +metatag.settings: + path: '/admin/config/search/metatag/settings' + defaults: + _form: '\Drupal\metatag\Form\MetatagSettingsForm' + _title: 'Configure the Metatag module' + requirements: + _permission: 'administer meta tags' + options: + _admin_route: TRUE diff --git a/web/modules/contrib/metatag/metatag_app_links/metatag_app_links.info.yml b/web/modules/contrib/metatag/metatag_app_links/metatag_app_links.info.yml index ed3dcb3ab..0c438e827 100644 --- a/web/modules/contrib/metatag/metatag_app_links/metatag_app_links.info.yml +++ b/web/modules/contrib/metatag/metatag_app_links/metatag_app_links.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_dc/metatag_dc.info.yml b/web/modules/contrib/metatag/metatag_dc/metatag_dc.info.yml index b466bb4c9..3123b511b 100644 --- a/web/modules/contrib/metatag/metatag_dc/metatag_dc.info.yml +++ b/web/modules/contrib/metatag/metatag_dc/metatag_dc.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_dc_advanced/metatag_dc_advanced.info.yml b/web/modules/contrib/metatag/metatag_dc_advanced/metatag_dc_advanced.info.yml index ed824a2cb..e9fc95e9e 100644 --- a/web/modules/contrib/metatag/metatag_dc_advanced/metatag_dc_advanced.info.yml +++ b/web/modules/contrib/metatag/metatag_dc_advanced/metatag_dc_advanced.info.yml @@ -7,8 +7,8 @@ dependencies: - metatag:metatag - metatag:metatag_dc -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_facebook/metatag_facebook.info.yml b/web/modules/contrib/metatag/metatag_facebook/metatag_facebook.info.yml index d6ef96650..ed308edda 100644 --- a/web/modules/contrib/metatag/metatag_facebook/metatag_facebook.info.yml +++ b/web/modules/contrib/metatag/metatag_facebook/metatag_facebook.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_favicons/metatag_favicons.info.yml b/web/modules/contrib/metatag/metatag_favicons/metatag_favicons.info.yml index 38da6f313..af4b6caa6 100644 --- a/web/modules/contrib/metatag/metatag_favicons/metatag_favicons.info.yml +++ b/web/modules/contrib/metatag/metatag_favicons/metatag_favicons.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_favicons/metatag_favicons.module b/web/modules/contrib/metatag/metatag_favicons/metatag_favicons.module index fe5c71dc3..39a0b19ee 100644 --- a/web/modules/contrib/metatag/metatag_favicons/metatag_favicons.module +++ b/web/modules/contrib/metatag/metatag_favicons/metatag_favicons.module @@ -33,7 +33,7 @@ function metatag_favicons_page_attachments_alter(array &$attachments) { } // Remove the default shortcut icon if one was set by Metatag. - foreach ($attachments['#attached']['html_head'] as $position => $element) { + foreach ($attachments['#attached']['html_head'] as $element) { if (isset($element[1]) && $element[1] == 'shortcut_icon') { foreach ($attachments['#attached']['html_head_link'] as $key => $value) { if (isset($value[0]['rel']) && $value[0]['rel'] == 'shortcut icon') { diff --git a/web/modules/contrib/metatag/metatag_favicons/src/Tests/MetatagFaviconsTagsTest.php b/web/modules/contrib/metatag/metatag_favicons/src/Tests/MetatagFaviconsTagsTest.php index 02e1ba231..04f588505 100644 --- a/web/modules/contrib/metatag/metatag_favicons/src/Tests/MetatagFaviconsTagsTest.php +++ b/web/modules/contrib/metatag/metatag_favicons/src/Tests/MetatagFaviconsTagsTest.php @@ -105,56 +105,63 @@ class MetatagFaviconsTagsTest extends MetatagTagsTestBase { } /** - * Implements {tag_name}TestOutputXpath() for - * 'apple_touch_icon_precomposed_114x114'. + * Implements {tag_name}TestOutputXpath(). + * + * For 'apple_touch_icon_precomposed_114x114'. */ private function appleTouchIconPrecomposed114x114TestOutputXpath() { return "//link[@rel='apple-touch-icon-precomposed' and @sizes='114x114']"; } /** - * Implements {tag_name}TestOutputXpath() for - * 'apple_touch_icon_precomposed_120x120'. + * Implements {tag_name}TestOutputXpath(). + * + * For 'apple_touch_icon_precomposed_120x120'. */ private function appleTouchIconPrecomposed120x120TestOutputXpath() { return "//link[@rel='apple-touch-icon-precomposed' and @sizes='120x120']"; } /** - * Implements {tag_name}TestOutputXpath() for - * 'apple_touch_icon_precomposed_144x144'. + * Implements {tag_name}TestOutputXpath(). + * + * For 'apple_touch_icon_precomposed_144x144'. */ private function appleTouchIconPrecomposed144x144TestOutputXpath() { return "//link[@rel='apple-touch-icon-precomposed' and @sizes='144x144']"; } /** - * Implements {tag_name}TestOutputXpath() for - * 'apple_touch_icon_precomposed_152x152'. + * Implements {tag_name}TestOutputXpath(). + * + * For 'apple_touch_icon_precomposed_152x152'. */ private function appleTouchIconPrecomposed152x152TestOutputXpath() { return "//link[@rel='apple-touch-icon-precomposed' and @sizes='152x152']"; } /** - * Implements {tag_name}TestOutputXpath() for - * 'apple_touch_icon_precomposed_180x180'. + * Implements {tag_name}TestOutputXpath(). + * + * For 'apple_touch_icon_precomposed_180x180'. */ private function appleTouchIconPrecomposed180x180TestOutputXpath() { return "//link[@rel='apple-touch-icon-precomposed' and @sizes='180x180']"; } /** - * Implements {tag_name}TestOutputXpath() for - * 'apple_touch_icon_precomposed_72x72'. + * Implements {tag_name}TestOutputXpath(). + * + * For 'apple_touch_icon_precomposed_72x72'. */ private function appleTouchIconPrecomposed72x72TestOutputXpath() { return "//link[@rel='apple-touch-icon-precomposed' and @sizes='72x72']"; } /** - * Implements {tag_name}TestOutputXpath() for - * 'apple_touch_icon_precomposed_76x76'. + * Implements {tag_name}TestOutputXpath(). + * + * For 'apple_touch_icon_precomposed_76x76'. */ private function appleTouchIconPrecomposed76x76TestOutputXpath() { return "//link[@rel='apple-touch-icon-precomposed' and @sizes='76x76']"; diff --git a/web/modules/contrib/metatag/metatag_google_cse/metatag_google_cse.info.yml b/web/modules/contrib/metatag/metatag_google_cse/metatag_google_cse.info.yml index ef0d568f1..7ae07312a 100644 --- a/web/modules/contrib/metatag/metatag_google_cse/metatag_google_cse.info.yml +++ b/web/modules/contrib/metatag/metatag_google_cse/metatag_google_cse.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_google_plus/metatag_google_plus.info.yml b/web/modules/contrib/metatag/metatag_google_plus/metatag_google_plus.info.yml index c9d19ac04..6a2dd43f1 100644 --- a/web/modules/contrib/metatag/metatag_google_plus/metatag_google_plus.info.yml +++ b/web/modules/contrib/metatag/metatag_google_plus/metatag_google_plus.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_hreflang/metatag_hreflang.info.yml b/web/modules/contrib/metatag/metatag_hreflang/metatag_hreflang.info.yml index f0eefd780..fde674e1f 100644 --- a/web/modules/contrib/metatag/metatag_hreflang/metatag_hreflang.info.yml +++ b/web/modules/contrib/metatag/metatag_hreflang/metatag_hreflang.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_hreflang/src/Plugin/Derivative/HreflangDeriver.php b/web/modules/contrib/metatag/metatag_hreflang/src/Plugin/Derivative/HreflangDeriver.php index 46e274261..0eeef79b3 100644 --- a/web/modules/contrib/metatag/metatag_hreflang/src/Plugin/Derivative/HreflangDeriver.php +++ b/web/modules/contrib/metatag/metatag_hreflang/src/Plugin/Derivative/HreflangDeriver.php @@ -5,8 +5,6 @@ namespace Drupal\metatag_hreflang\Plugin\Derivative; use Drupal\Component\Plugin\Derivative\DeriverBase; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Create a new hreflang tag plugin for each enabled language. diff --git a/web/modules/contrib/metatag/metatag_mobile/config/schema/metatag_mobile.metatag_tag.schema.yml b/web/modules/contrib/metatag/metatag_mobile/config/schema/metatag_mobile.metatag_tag.schema.yml index 7f704b258..6431aaa2b 100644 --- a/web/modules/contrib/metatag/metatag_mobile/config/schema/metatag_mobile.metatag_tag.schema.yml +++ b/web/modules/contrib/metatag/metatag_mobile/config/schema/metatag_mobile.metatag_tag.schema.yml @@ -95,6 +95,9 @@ metatag.metatag_tag.theme_color: metatag.metatag_tag.viewport: type: label label: 'Mobile: Viewport' +metatag.metatag_tag.web_manifest: + type: label + label: 'Mobile: Web Manifest' metatag.metatag_tag.x_ua_compatible: type: label label: 'X-UA-Compatible' diff --git a/web/modules/contrib/metatag/metatag_mobile/metatag_mobile.info.yml b/web/modules/contrib/metatag/metatag_mobile/metatag_mobile.info.yml index 46227b1c5..fb1c833d5 100644 --- a/web/modules/contrib/metatag/metatag_mobile/metatag_mobile.info.yml +++ b/web/modules/contrib/metatag/metatag_mobile/metatag_mobile.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_mobile/metatag_mobile.module b/web/modules/contrib/metatag/metatag_mobile/metatag_mobile.module index a1778779a..dfe0ff94c 100644 --- a/web/modules/contrib/metatag/metatag_mobile/metatag_mobile.module +++ b/web/modules/contrib/metatag/metatag_mobile/metatag_mobile.module @@ -104,15 +104,3 @@ function theme_metatag_mobile_ios_app($variables) { return theme('metatag_link_rel', $variables); } - -/** - * theme-color - * MobileOptimized - * HandheldFriendly - * viewport - * cleartype - * apple-mobile-web-app-capable - * apple-mobile-web-app-status-bar-style - * format-detection - * android-app - */ diff --git a/web/modules/contrib/metatag/metatag_mobile/src/Plugin/metatag/Tag/WebManifest.php b/web/modules/contrib/metatag/metatag_mobile/src/Plugin/metatag/Tag/WebManifest.php new file mode 100644 index 000000000..5028cae01 --- /dev/null +++ b/web/modules/contrib/metatag/metatag_mobile/src/Plugin/metatag/Tag/WebManifest.php @@ -0,0 +1,24 @@ +<?php + +namespace Drupal\metatag_mobile\Plugin\metatag\Tag; + +use Drupal\metatag\Plugin\metatag\Tag\LinkRelBase; + +/** + * The Web Manifest for Progressive Web Apps. + * + * @MetatagTag( + * id = "web_manifest", + * label = @Translation("Web Manifest"), + * description = @Translation("A URL to a manifest.json file that describes the application. The <a href='https://developer.mozilla.org/en-US/docs/Web/Manifest'>JSON-based manifest</a> provides developers with a centralized place to put metadata associated with a web application."), + * name = "manifest", + * group = "mobile", + * weight = 92, + * type = "uri", + * secure = FALSE, + * multiple = FALSE + * ) + */ +class WebManifest extends LinkRelBase { + // Nothing here yet. Just a placeholder class for a plugin. +} diff --git a/web/modules/contrib/metatag/metatag_mobile/src/Tests/MetatagMobileTagsTest.php b/web/modules/contrib/metatag/metatag_mobile/src/Tests/MetatagMobileTagsTest.php index 0baaeeb60..428744962 100644 --- a/web/modules/contrib/metatag/metatag_mobile/src/Tests/MetatagMobileTagsTest.php +++ b/web/modules/contrib/metatag/metatag_mobile/src/Tests/MetatagMobileTagsTest.php @@ -46,6 +46,7 @@ class MetatagMobileTagsTest extends MetatagTagsTestBase { 'msapplication_window', 'theme_color', 'viewport', + 'web_manifest', 'x_ua_compatible', ]; @@ -114,8 +115,9 @@ class MetatagMobileTagsTest extends MetatagTagsTestBase { } /** - * Implements {tag_name}TestValueAttribute() for - * 'android-app-link-alternative'. + * Implements {tag_name}TestValueAttribute(). + * + * For 'android-app-link-alternative'. */ private function androidAppLinkAlternativeTestValueAttribute() { return 'href'; @@ -212,6 +214,20 @@ class MetatagMobileTagsTest extends MetatagTagsTestBase { return $this->randomImageUrl(); } + /** + * Implements {tag_name}TestOutputXpath() for 'web_manifest'. + */ + private function webManifestTestOutputXpath() { + return "//link[@rel='manifest']"; + } + + /** + * Implements {tag_name}TestValueAttribute() for 'web_manifest'. + */ + private function webManifestTestValueAttribute() { + return 'href'; + } + /** * Implements {tag_name}TestNameAttribute() for 'x-ua-compatible'. */ diff --git a/web/modules/contrib/metatag/metatag_open_graph/config/schema/metatag_open_graph.metatag_tag.schema.yml b/web/modules/contrib/metatag/metatag_open_graph/config/schema/metatag_open_graph.metatag_tag.schema.yml index 02066c150..593df480f 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/config/schema/metatag_open_graph.metatag_tag.schema.yml +++ b/web/modules/contrib/metatag/metatag_open_graph/config/schema/metatag_open_graph.metatag_tag.schema.yml @@ -20,6 +20,9 @@ metatag.metatag_tag.og_fax_number: metatag.metatag_tag.og_image: type: label label: 'Open Graph: Image' +metatag.metatag_tag.og_image_alt: + type: label + label: 'Open Graph: Image alt' metatag.metatag_tag.og_image_height: type: label label: 'Open Graph: Image height' diff --git a/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.info.yml b/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.info.yml index 9ee1bd53f..7755b893b 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.info.yml +++ b/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.install b/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.install index 81df412bb..28baa7a4e 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.install +++ b/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.install @@ -9,7 +9,7 @@ use Drupal\Core\Entity\ContentEntityBase; use Drupal\metatag\Entity\MetatagDefaults; /** - * Implementsations of hook_update_N(). + * Implementations of hook_update_N(). */ /** diff --git a/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.module b/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.module index 46621e46b..fa5ffa707 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.module +++ b/web/modules/contrib/metatag/metatag_open_graph/metatag_open_graph.module @@ -21,18 +21,10 @@ function metatag_open_graph_preprocess_html(&$variables) { $namespaces = []; if (!\Drupal::moduleHandler()->moduleExists('rdf')) { $namespaces = [ - 'xmlns:dc' => 'http://purl.org/dc/terms/', - 'xmlns:og' => 'http://ogp.me/ns#', + 'prefix' => 'og: http://ogp.me/ns#', ]; } - // Namespaces for OpenGraph. - $namespaces['xmlns:article'] = "http://ogp.me/ns/article#"; - $namespaces['xmlns:book'] = "http://ogp.me/ns/book#"; - $namespaces['xmlns:product'] = "http://ogp.me/ns/product#"; - $namespaces['xmlns:profile'] = "http://ogp.me/ns/profile#"; - $namespaces['xmlns:video'] = "http://ogp.me/ns/video#"; - // Namespaces for Google+. if (isset($variables['itemtype'])) { $namespaces['itemscope'] = ''; diff --git a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookAuthor.php b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookAuthor.php index 982e915da..da02560b5 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookAuthor.php +++ b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookAuthor.php @@ -2,7 +2,7 @@ namespace Drupal\metatag_open_graph\Plugin\metatag\Tag; -use \Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; +use Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; /** * The Open Graph "Book author" meta tag. diff --git a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookISBN.php b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookISBN.php index 8545a3ba8..434525832 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookISBN.php +++ b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookISBN.php @@ -2,7 +2,7 @@ namespace Drupal\metatag_open_graph\Plugin\metatag\Tag; -use \Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; +use Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; /** * Provides a plugin for the 'book:isbn' meta tag. diff --git a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookReleaseDate.php b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookReleaseDate.php index 1dc3eed29..f2ee39db1 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookReleaseDate.php +++ b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookReleaseDate.php @@ -2,7 +2,7 @@ namespace Drupal\metatag_open_graph\Plugin\metatag\Tag; -use \Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; +use Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; /** * Provides a plugin for the 'book:release_date' meta tag. diff --git a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookTag.php b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookTag.php index 1a60cdb2f..0495d299f 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookTag.php +++ b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/BookTag.php @@ -2,7 +2,7 @@ namespace Drupal\metatag_open_graph\Plugin\metatag\Tag; -use \Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; +use Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; /** * The Open Graph "Book tag" meta tag. diff --git a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/OgImageAlt.php b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/OgImageAlt.php new file mode 100644 index 000000000..bc239bd30 --- /dev/null +++ b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/OgImageAlt.php @@ -0,0 +1,25 @@ +<?php + +namespace Drupal\metatag_open_graph\Plugin\metatag\Tag; + +use Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; + +/** + * Provides a plugin for the 'og:image:alt' meta tag. + * + * @MetatagTag( + * id = "og_image_alt", + * label = @Translation("Image 'alt'"), + * description = @Translation("A description of what is in the image, not a caption. If the page specifies an og:image it should specify og:image:alt."), + * name = "og:image:alt", + * group = "open_graph", + * weight = 15, + * type = "string", + * secure = FALSE, + * multiple = FALSE, + * absolute_url = FALSE + * ) + */ +class OgImageAlt extends MetaPropertyBase { + // Nothing here yet. Just a placeholder class for a plugin. +} diff --git a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/OgImageSecureUrl.php b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/OgImageSecureUrl.php index fbf7a152a..b0af05e8a 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/OgImageSecureUrl.php +++ b/web/modules/contrib/metatag/metatag_open_graph/src/Plugin/metatag/Tag/OgImageSecureUrl.php @@ -16,7 +16,7 @@ use Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; * weight = 11, * type = "image", * secure = TRUE, - * multiple = FALSE, + * multiple = TRUE, * absolute_url = TRUE * ) */ diff --git a/web/modules/contrib/metatag/metatag_open_graph/src/Tests/MetatagOpenGraphTagsTest.php b/web/modules/contrib/metatag/metatag_open_graph/src/Tests/MetatagOpenGraphTagsTest.php index 236515527..af726e8e0 100644 --- a/web/modules/contrib/metatag/metatag_open_graph/src/Tests/MetatagOpenGraphTagsTest.php +++ b/web/modules/contrib/metatag/metatag_open_graph/src/Tests/MetatagOpenGraphTagsTest.php @@ -32,6 +32,7 @@ class MetatagOpenGraphTagsTest extends MetatagTagsTestBase { 'og_email', 'og_fax_number', 'og_image', + 'og_image_alt', 'og_image_height', 'og_image_secure_url', 'og_image_type', diff --git a/web/modules/contrib/metatag/metatag_open_graph_products/metatag_open_graph_products.info.yml b/web/modules/contrib/metatag/metatag_open_graph_products/metatag_open_graph_products.info.yml index 87e062c60..39090f3ba 100644 --- a/web/modules/contrib/metatag/metatag_open_graph_products/metatag_open_graph_products.info.yml +++ b/web/modules/contrib/metatag/metatag_open_graph_products/metatag_open_graph_products.info.yml @@ -7,8 +7,8 @@ dependencies: - metatag:metatag - metatag:metatag_open_graph -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_open_graph_products/src/Plugin/metatag/Tag/ProductPriceAmount.php b/web/modules/contrib/metatag/metatag_open_graph_products/src/Plugin/metatag/Tag/ProductPriceAmount.php index fe0b9ad0a..0d73285ef 100644 --- a/web/modules/contrib/metatag/metatag_open_graph_products/src/Plugin/metatag/Tag/ProductPriceAmount.php +++ b/web/modules/contrib/metatag/metatag_open_graph_products/src/Plugin/metatag/Tag/ProductPriceAmount.php @@ -2,7 +2,7 @@ namespace Drupal\metatag_open_graph_products\Plugin\metatag\Tag; -use \Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; +use Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; /** * Provides a plugin for the 'product:price:amount' meta tag. diff --git a/web/modules/contrib/metatag/metatag_open_graph_products/src/Plugin/metatag/Tag/ProductPriceCurrency.php b/web/modules/contrib/metatag/metatag_open_graph_products/src/Plugin/metatag/Tag/ProductPriceCurrency.php index ed567f5c0..74536ae7f 100644 --- a/web/modules/contrib/metatag/metatag_open_graph_products/src/Plugin/metatag/Tag/ProductPriceCurrency.php +++ b/web/modules/contrib/metatag/metatag_open_graph_products/src/Plugin/metatag/Tag/ProductPriceCurrency.php @@ -2,7 +2,7 @@ namespace Drupal\metatag_open_graph_products\Plugin\metatag\Tag; -use \Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; +use Drupal\metatag\Plugin\metatag\Tag\MetaPropertyBase; /** * Provides a plugin for the 'product:price:currency' meta tag. diff --git a/web/modules/contrib/metatag/metatag_open_graph_products/src/Tests/MetatagOpenGraphProductsTagsTest.php b/web/modules/contrib/metatag/metatag_open_graph_products/src/Tests/MetatagOpenGraphProductsTagsTest.php index 3b302b7dc..3af3a2ac6 100644 --- a/web/modules/contrib/metatag/metatag_open_graph_products/src/Tests/MetatagOpenGraphProductsTagsTest.php +++ b/web/modules/contrib/metatag/metatag_open_graph_products/src/Tests/MetatagOpenGraphProductsTagsTest.php @@ -5,7 +5,7 @@ namespace Drupal\metatag_open_graph_products\Tests; use Drupal\metatag\Tests\MetatagTagsTestBase; /** - * Tests that each of the Metatag Open Graph tags work correctly. + * Tests that each of the Metatag Open Graph Product tags work correctly. * * @group metatag */ @@ -14,7 +14,10 @@ class MetatagOpenGraphProductsTagsTest extends MetatagTagsTestBase { /** * {@inheritdoc} */ - private $tags = []; + private $tags = [ + 'product_price_amount', + 'product_price_currency', + ]; /** * {@inheritdoc} @@ -38,18 +41,8 @@ class MetatagOpenGraphProductsTagsTest extends MetatagTagsTestBase { * Each of these meta tags has a different tag name vs its internal name. */ private function getTestTagName($tag_name) { - // Replace the first underline with a colon. - $tag_name = str_replace('og_', 'og:', $tag_name); - $tag_name = str_replace('article_', 'article:', $tag_name); - - // Some tags have an additional underline that turns into a colon. - $tag_name = str_replace('og:image_', 'og:image:', $tag_name); - $tag_name = str_replace('og:video_', 'og:video:', $tag_name); - - // Additional fixes. - if ($tag_name == 'og:locale_alternative') { - $tag_name = 'og:locale:alternate'; - } + // Replace the underlines with a colon. + $tag_name = str_replace('_', ':', $tag_name); return $tag_name; } diff --git a/web/modules/contrib/metatag/metatag_page_manager/metatag_page_manager.info.yml b/web/modules/contrib/metatag/metatag_page_manager/metatag_page_manager.info.yml new file mode 100644 index 000000000..c25fa9338 --- /dev/null +++ b/web/modules/contrib/metatag/metatag_page_manager/metatag_page_manager.info.yml @@ -0,0 +1,14 @@ +name: Metatag Page Manager +type: module +description: Provides metatag support for Page Manager variants. +# core: 8.x +package: SEO +dependencies: + - page_manager:page_manager + - metatag:metatag + +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' +core: '8.x' +project: 'metatag' +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_page_manager/metatag_page_manager.module b/web/modules/contrib/metatag/metatag_page_manager/metatag_page_manager.module new file mode 100644 index 000000000..68e0a5ba8 --- /dev/null +++ b/web/modules/contrib/metatag/metatag_page_manager/metatag_page_manager.module @@ -0,0 +1,106 @@ +<?php + +/** + * @file + * Contains metatag_page_manager.module. + */ + +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\page_manager\Entity\PageVariant; +use Drupal\metatag\Entity\MetatagDefaults; + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function metatag_page_manager_form_metatag_defaults_add_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $variants_options['Page Variants'] = _metatag_page_manager_get_variants(); + $form['id']['#options'] = array_merge($form['id']['#options'], $variants_options); +} + +/** + * Implements hook_page_attachments(). + */ +function metatag_page_manager_page_attachments(array &$attachments) { + // Fetch entity from request. + $entity = \Drupal::request()->attributes->get('_entity'); + if ($entity) { + + $key = $entity->getEntityType()->id() . '__' . $entity->id(); + // Get default metatags. + $metatag_defaults = metatag_get_default_tags(); + // Load page variant metatags. + $metatag_variant = MetatagDefaults::load($key); + if ($metatag_variant) { + // Overwrite the metatag defaults with the tags of the page variant. + $metatag_defaults = array_merge($metatag_defaults, $metatag_variant->get('tags')); + + // Set the metatag in the static metatag attachments parameter so the + // metatag module wouldn't overwrite them. + $metatag_attachments = &drupal_static('metatag_attachments'); + + $metatag_manager = \Drupal::service('metatag.manager'); + $metatag_attachments = $metatag_manager->generateElements($metatag_defaults, $entity); + + // If any Metatag items were found, append them. + if (!empty($metatag_attachments['#attached']['html_head'])) { + if (empty($attachments['#attached'])) { + $attachments['#attached'] = []; + } + if (empty($attachments['#attached']['html_head'])) { + $attachments['#attached']['html_head'] = []; + } + foreach ($metatag_attachments['#attached']['html_head'] as $item) { + $attachments['#attached']['html_head'][] = $item; + } + } + } + } +} + +/** + * Returns all available page variants. + * + * @return string[] + * A list of page variants keyed by label. + */ +function _metatag_page_manager_get_variants() { + /** @var \Drupal\page_manager\Entity\PageVariant[] $variants */ + $variants = PageVariant::loadMultiple(); + $variant_options = []; + // Load all metatag defaults so we can filter the variants which already have + // a metatag default configured. + $metatag_defaults = MetatagDefaults::loadMultiple(); + foreach ($variants as $key => $variant) { + $id = $variant->getEntityType()->id() . '__' . $key; + if (!isset($metatag_defaults[$id])) { + $label = $variant->getPage()->label() . ' : ' . $variant->label(); + $variant_options[$id] = $label; + } + } + return $variant_options; +} + +/** + * Implements hook_metatag_alter(). + */ +function metatag_page_manager_metatags_alter(array &$metatags, array &$context) { + if (!$context['entity'] instanceof PageVariant) { + return; + } + + $key = $context['entity']->getEntityType()->id() . '__' . $context['entity']->id(); + $metatag_variant = MetatagDefaults::load($key); + if ($metatag_variant) { + $metatags = array_merge($metatags, $metatag_variant->get('tags')); + } +} + +/** + * Implements hook_metatag_route_entity(). + */ +function metatag_page_manager_metatag_route_entity(RouteMatchInterface $route_match) { + if ($variant = $route_match->getParameter('page_manager_page_variant')) { + return $variant; + } +} diff --git a/web/modules/contrib/metatag/metatag_page_manager/src/Tests/Functional/MetatagPageManagerTest.php b/web/modules/contrib/metatag/metatag_page_manager/src/Tests/Functional/MetatagPageManagerTest.php new file mode 100644 index 000000000..14c228966 --- /dev/null +++ b/web/modules/contrib/metatag/metatag_page_manager/src/Tests/Functional/MetatagPageManagerTest.php @@ -0,0 +1,177 @@ +<?php + +namespace Drupal\metatag_page_manager\Tests\Functional; + +use Drupal\page_manager\Entity\Page; +use Drupal\page_manager\Entity\PageVariant; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\metatag\Functional\MetatagHelperTrait; + +/** + * Confirm the Page Manager integration works. + * + * @group metatag + */ +class MetatagPageManagerTest extends BrowserTestBase { + + use MetatagHelperTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + // This module. + 'metatag_page_manager', + ]; + + /** + * The assert session object. + * + * @var \Drupal\Tests\WebAssert + */ + public $assertSession; + + /** + * {@inheritdoc} + */ + public function setUp() { + // TODO: Change the autogenerated stub. + parent::setUp(); + + $this->assertSession = $this->assertSession(); + + Page::create([ + 'id' => 'metatag_page_manager_test', + 'label' => 'Metatag Page', + 'path' => '/metatag-test', + ])->save(); + PageVariant::create([ + 'id' => 'metatag_page_manager_variant_test', + 'variant' => 'block_display', + 'label' => 'Metatag Variant', + 'page' => 'metatag_page_manager_test', + 'weight' => 10, + ])->save(); + + \Drupal::service("router.builder")->rebuild(); + + // Log in as user 1. + $this->loginUser1(); + } + + /** + * Tests a single variant page. + */ + public function testSingleVariantPage() { + $this->drupalGet('/metatag-test'); + $this->assertSession->statusCodeEquals(200); + + // Confirm what the page title looks like by default. + $this->assertSession->titleEquals('Metatag Page | Drupal'); + + // Create the Metatag object through the UI to check the custom label. + $edit = [ + 'id' => 'page_variant__metatag_page_manager_variant_test', + 'title' => 'My title', + ]; + + $this->drupalPostForm('/admin/config/search/metatag/add', $edit, 'Save'); + $this->assertSession->pageTextContains('Page Variant: Metatag Page: Metatag Variant'); + + // Clear caches to load the right metatags. + drupal_flush_all_caches(); + + $this->drupalGet('/metatag-test'); + $this->assertSession->statusCodeEquals(200); + + // Confirm what the page title is overridden. + $this->assertSession->titleEquals('My title'); + } + + /** + * Tests a single variant page. + */ + public function testMultipleVariantPage() { + // Add a new variant. + $new_variant = PageVariant::create([ + 'id' => 'metatag_page_manager_multiple_variant_test', + 'variant' => 'block_display', + 'label' => 'Metatag Multiple Variant', + 'page' => 'metatag_page_manager_test', + 'weight' => 0, + ]); + $anonymous_selection = [ + 'id' => 'user_role', + 'roles' => [ + 'anonymous' => 'anonymous', + ], + 'negate' => FALSE, + 'context_mapping' => [ + 'user' => 'current_user', + ], + ]; + $new_variant->set('selection_criteria', [$anonymous_selection]); + $new_variant->save(); + + // Clear caches to load the right metatags. + drupal_flush_all_caches(); + + $this->drupalGet('/metatag-test'); + $this->assertSession->statusCodeEquals(200); + + // Confirm what the page title looks like by default. + $this->assertSession->titleEquals('Metatag Page | Drupal'); + + // Create the Metatag object through the UI to check the custom label. + $edit = [ + 'id' => 'page_variant__metatag_page_manager_variant_test', + 'title' => 'My title', + ]; + + $this->drupalPostForm('/admin/config/search/metatag/add', $edit, 'Save'); + $this->assertSession->pageTextContains('Page Variant: Metatag Page: Metatag Variant'); + + // Clear caches to load the right metatags. + drupal_flush_all_caches(); + + $this->drupalGet('/metatag-test'); + $this->assertSession->statusCodeEquals(200); + + // Confirm what the page title is overridden. + $this->assertSession->titleEquals('My title'); + + // Visiting page as anon user, should get the default title. + $this->drupalLogout(); + $this->drupalGet('/metatag-test'); + $this->assertSession->statusCodeEquals(200); + + // Confirm what the page title looks like by default. + $this->assertSession->titleEquals('Metatag Page | Drupal'); + + // Login and add custom metatag for anonymous user variant. + $this->loginUser1(); + // Create the Metatag object through the UI to check the custom label. + $edit = [ + 'id' => 'page_variant__metatag_page_manager_multiple_variant_test', + 'title' => 'My title anonymous', + ]; + + $this->drupalPostForm('/admin/config/search/metatag/add', $edit, 'Save'); + $this->assertSession->pageTextContains('Page Variant: Metatag Page: Metatag Multiple Variant'); + + // Clear caches to load the right metatags. + drupal_flush_all_caches(); + + // Visit page as logged in user and confirm the right title. + $this->drupalGet('/metatag-test'); + $this->assertSession->statusCodeEquals(200); + $this->assertSession->titleEquals('My title'); + + // Visit page as anonymous user and confirm the right title. + $this->drupalLogout(); + $this->drupalGet('/metatag-test'); + $this->assertSession->statusCodeEquals(200); + $this->assertSession->titleEquals('My title anonymous'); + } + +} diff --git a/web/modules/contrib/metatag/metatag_pinterest/metatag_pinterest.info.yml b/web/modules/contrib/metatag/metatag_pinterest/metatag_pinterest.info.yml index ffc7f866c..836418679 100644 --- a/web/modules/contrib/metatag/metatag_pinterest/metatag_pinterest.info.yml +++ b/web/modules/contrib/metatag/metatag_pinterest/metatag_pinterest.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_twitter_cards/metatag_twitter_cards.info.yml b/web/modules/contrib/metatag/metatag_twitter_cards/metatag_twitter_cards.info.yml index ff6ff9106..122e885ca 100644 --- a/web/modules/contrib/metatag/metatag_twitter_cards/metatag_twitter_cards.info.yml +++ b/web/modules/contrib/metatag/metatag_twitter_cards/metatag_twitter_cards.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_verification/metatag_verification.info.yml b/web/modules/contrib/metatag/metatag_verification/metatag_verification.info.yml index f88204a47..336c45f08 100644 --- a/web/modules/contrib/metatag/metatag_verification/metatag_verification.info.yml +++ b/web/modules/contrib/metatag/metatag_verification/metatag_verification.info.yml @@ -6,8 +6,8 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_views/metatag_views.info.yml b/web/modules/contrib/metatag/metatag_views/metatag_views.info.yml index b01e4c9cb..db451743c 100644 --- a/web/modules/contrib/metatag/metatag_views/metatag_views.info.yml +++ b/web/modules/contrib/metatag/metatag_views/metatag_views.info.yml @@ -4,11 +4,11 @@ description: Provides views integration for metatags. # core: 8.x package: SEO dependencies: - - metatag - - views + - metatag:metatag + - drupal:views -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/metatag_views/metatag_views.module b/web/modules/contrib/metatag/metatag_views/metatag_views.module index 463fb0f63..02b9e9cd8 100644 --- a/web/modules/contrib/metatag/metatag_views/metatag_views.module +++ b/web/modules/contrib/metatag/metatag_views/metatag_views.module @@ -51,7 +51,7 @@ function metatag_get_view_tags($view, $display_id = NULL) { /** * Implements hook_metatags_alter(). */ -function metatag_views_metatags_alter(&$metatags, $context) { +function metatag_views_metatags_alter(array &$metatags, array &$context) { if (!$context['entity'] instanceof ViewEntityInterface) { return; } diff --git a/web/modules/contrib/metatag/metatag_views/src/Controller/MetatagViewsController.php b/web/modules/contrib/metatag/metatag_views/src/Controller/MetatagViewsController.php index f43b742dd..2ddf0b824 100644 --- a/web/modules/contrib/metatag/metatag_views/src/Controller/MetatagViewsController.php +++ b/web/modules/contrib/metatag/metatag_views/src/Controller/MetatagViewsController.php @@ -17,11 +17,15 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class MetatagViewsController extends ControllerBase { /** + * The Views storage interface. + * * @var \Drupal\Core\Entity\EntityStorageInterface */ protected $viewStorage; /** + * The Metatag manager interface. + * * @var \Drupal\metatag\MetatagManagerInterface */ protected $metatagManager; @@ -77,13 +81,13 @@ class MetatagViewsController extends ControllerBase { * Generates the renderable array for views meta tags UI. * * @return array - * Thelist of details. + * The list of details. */ public function listViews() { $elements = []; $elements['header'] = [ - '#markup' => '<p>' . t("To view a list of displays with meta tags set up, click on a view name. To view a summary of meta tags configuration for a particular display, click on the display name. If you need to set meta tags for a specific view, choose Add views meta tags. Reverting the meta tags removes the specific configuration and falls back to defaults.") . '</p>', + '#markup' => '<p>' . $this->t("To view a list of displays with meta tags set up, click on a view name. To view a summary of meta tags configuration for a particular display, click on the display name. If you need to set meta tags for a specific view, choose Add views meta tags. Reverting the meta tags removes the specific configuration and falls back to defaults.") . '</p>', ]; // Iterate over the values and build the whole UI. @@ -144,15 +148,15 @@ class MetatagViewsController extends ControllerBase { '#type' => 'operations', '#links' => [ 'edit' => [ - 'title' => t('Edit'), + 'title' => $this->t('Edit'), 'url' => Url::fromRoute('metatag_views.metatags.edit', $params), ], 'translate' => [ - 'title' => t('Translate'), + 'title' => $this->t('Translate'), 'url' => Url::fromRoute('metatag_views.metatags.translate_overview', $params), ], 'revert' => [ - 'title' => t('Revert'), + 'title' => $this->t('Revert'), 'url' => Url::fromRoute('metatag_views.metatags.revert', $params), ], ], diff --git a/web/modules/contrib/metatag/metatag_views/src/Controller/MetatagViewsTranslationController.php b/web/modules/contrib/metatag/metatag_views/src/Controller/MetatagViewsTranslationController.php index 59c82e04f..eec9afc6e 100644 --- a/web/modules/contrib/metatag/metatag_views/src/Controller/MetatagViewsTranslationController.php +++ b/web/modules/contrib/metatag/metatag_views/src/Controller/MetatagViewsTranslationController.php @@ -4,7 +4,6 @@ namespace Drupal\metatag_views\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Url; use Drupal\metatag\MetatagManagerInterface; @@ -16,11 +15,15 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class MetatagViewsTranslationController extends ControllerBase { /** + * The View storage interface. + * * @var \Drupal\Core\Entity\EntityStorageInterface */ protected $viewStorage; /** + * The Metatag manager. + * * @var \Drupal\metatag\MetatagManagerInterface */ protected $metatagManager; @@ -68,7 +71,6 @@ class MetatagViewsTranslationController extends ControllerBase { $config_name = $view->getConfigDependencyName(); $config_path = 'display.' . $display_id . '.display_options.display_extenders.metatag_display_extender.metatags'; - $access_manager = \Drupal::service('access_manager'); $configuration = \Drupal::service('config.factory')->get($config_name); $config_source = $configuration->getOriginal($config_path, FALSE); diff --git a/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsEditForm.php b/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsEditForm.php index 96e970b9b..0e395e842 100644 --- a/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsEditForm.php +++ b/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsEditForm.php @@ -26,6 +26,8 @@ class MetatagViewsEditForm extends FormBase { protected $metatagManager; /** + * The Views manager. + * * @var \Drupal\Core\Entity\EntityStorageInterface */ protected $viewsManager; @@ -84,7 +86,7 @@ class MetatagViewsEditForm extends FormBase { } $form['metatags'] = $this->metatagManager->form($metatags, $form, ['view']); - $form['metatags']['#title'] = t('Metatags'); + $form['metatags']['#title'] = $this->t('Metatags'); $form['metatags']['#type'] = 'fieldset'; // Need to create that AFTER the $form['metatags'] as the whole form is @@ -99,7 +101,7 @@ class MetatagViewsEditForm extends FormBase { $form['actions']['submit'] = [ '#type' => 'submit', - '#value' => t('Submit'), + '#value' => $this->t('Submit'), ]; return $form; diff --git a/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsRevertForm.php b/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsRevertForm.php index 049f1f956..1bd585929 100644 --- a/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsRevertForm.php +++ b/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsRevertForm.php @@ -9,7 +9,7 @@ use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Defines a confirmation form for deleting mymodule data. + * Defines a form for reverting views metatags. */ class MetatagViewsRevertForm extends ConfirmFormBase { @@ -116,7 +116,7 @@ class MetatagViewsRevertForm extends ConfirmFormBase { $config_name = $this->view->getConfigDependencyName(); $config_path = 'display.' . $this->displayId . '.display_options.display_extenders.metatag_display_extender.metatags'; - $configuration = $this->configFactory()->getEditable($config_name) + $this->configFactory()->getEditable($config_name) ->clear($config_path) ->save(); diff --git a/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsTranslationForm.php b/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsTranslationForm.php index 73a1df6a2..88875c0a6 100644 --- a/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsTranslationForm.php +++ b/web/modules/contrib/metatag/metatag_views/src/Form/MetatagViewsTranslationForm.php @@ -36,33 +36,43 @@ class MetatagViewsTranslationForm extends FormBase { protected $languageManager; /** + * The Views manager. + * * @var \Drupal\Core\Entity\EntityStorageInterface */ protected $viewsManager; /** + * The Metatag token service. + * * @var \Drupal\metatag\MetatagToken */ protected $tokenService; /** + * The Metatag tag plugin manager. + * * @var \Drupal\metatag\MetatagTagPluginManager */ protected $tagPluginManager; /** - * View entity object. + * The View entity object. * * @var \Drupal\views\ViewEntityInterface */ protected $view; /** + * View ID. + * * @var string */ protected $viewId; /** + * View display ID. + * * @var string */ protected $displayId = 'default'; diff --git a/web/modules/contrib/metatag/metatag_views/src/Plugin/views/display_extender/MetatagDisplayExtender.php b/web/modules/contrib/metatag/metatag_views/src/Plugin/views/display_extender/MetatagDisplayExtender.php index b14946e71..c31a4e825 100644 --- a/web/modules/contrib/metatag/metatag_views/src/Plugin/views/display_extender/MetatagDisplayExtender.php +++ b/web/modules/contrib/metatag/metatag_views/src/Plugin/views/display_extender/MetatagDisplayExtender.php @@ -45,9 +45,9 @@ class MetatagDisplayExtender extends DisplayExtenderPluginBase { * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. - * @param Drupal\metatag\MetatagTagPluginManager $metatag_plugin_manager + * @param \Drupal\metatag\MetatagTagPluginManager $metatag_plugin_manager * The plugin manager for metatag tags. - * @param Drupal\metatag\MetatagManagerInterface $metatag_manager + * @param \Drupal\metatag\MetatagManagerInterface $metatag_manager * The metatag manager. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, MetatagTagPluginManager $metatag_plugin_manager, MetatagManagerInterface $metatag_manager) { diff --git a/web/modules/contrib/metatag/src/Command/GenerateGroupCommand.php b/web/modules/contrib/metatag/src/Command/GenerateGroupCommand.php index 3a9d47b8e..af11ec50a 100644 --- a/web/modules/contrib/metatag/src/Command/GenerateGroupCommand.php +++ b/web/modules/contrib/metatag/src/Command/GenerateGroupCommand.php @@ -30,28 +30,34 @@ class GenerateGroupCommand extends Command { use ConfirmationTrait; /** + * The metatag group generator. + * * @var \Drupal\metatag\Generator\MetatagGroupGenerator */ protected $generator; /** + * The console extension manager. + * * @var \Drupal\Console\Extension\Manager */ protected $extensionManager; /** + * The console chain queue. + * * @var \Drupal\Console\Core\Utils\ChainQueue */ protected $chainQueue; /** - * GenerateTagCommand constructor. + * The GenerateTagCommand constructor. * - * @param Drupal\metatag\Generator\MetatagGroupGenerator $generator + * @param \Drupal\metatag\Generator\MetatagGroupGenerator $generator * The generator object. - * @param Drupal\Console\Extension\Manager $extensionManager + * @param \Drupal\Console\Extension\Manager $extensionManager * The extension manager object. - * @param Drupal\Console\Core\Utils\ChainQueue $chainQueue + * @param \Drupal\Console\Core\Utils\ChainQueue $chainQueue * The chain queue object. */ public function __construct( diff --git a/web/modules/contrib/metatag/src/Command/GenerateTagCommand.php b/web/modules/contrib/metatag/src/Command/GenerateTagCommand.php index b692c113d..f5a829aed 100644 --- a/web/modules/contrib/metatag/src/Command/GenerateTagCommand.php +++ b/web/modules/contrib/metatag/src/Command/GenerateTagCommand.php @@ -32,42 +32,52 @@ class GenerateTagCommand extends Command { use ConfirmationTrait; /** + * The Metatag manager. + * * @var \Drupal\metatag\MetatagManager */ protected $metatagManager; /** + * The Metatag tag generator. + * * @var \Drupal\metatag\Generator\MetatagTagGenerator */ protected $generator; /** + * An extension manager. + * * @var \Drupal\Console\Extension\Manager */ protected $extensionManager; /** + * The string converter. + * * @var \Drupal\Console\Core\Utils\StringConverter */ protected $stringConverter; /** + * The console chain queue. + * * @var \Drupal\Console\Core\Utils\ChainQueue */ protected $chainQueue; /** - * GenerateTagCommand constructor. + * The GenerateTagCommand constructor. * - * @param Drupal\metatag\MetatagManager $metatagManager + * @param \Drupal\metatag\MetatagManager $metatagManager * The metatag manager object. - * @param Drupal\metatag\Generator\MetatagTagGenerator $generator + * @param \Drupal\metatag\Generator\MetatagTagGenerator $generator * The tag generator object. - * @param Drupal\Console\Extension\Manager $extensionManager + * @param \Drupal\Console\Extension\Manager $extensionManager * The extension manager object. - * @param Drupal\Console\Core\Utils\StringConverter $stringConverter + * @param \Drupal\Console\Core\Utils\StringConverter $stringConverter * The string converter object. - * @param Drupal\Console\Core\Utils\ChainQueue $chainQueue + * @param \Drupal\Console\Core\Utils\ChainQueue $chainQueue * The chain queue object. */ public function __construct( diff --git a/web/modules/contrib/metatag/src/Entity/MetatagDefaults.php b/web/modules/contrib/metatag/src/Entity/MetatagDefaults.php index 17c3ad496..da9a8efbb 100644 --- a/web/modules/contrib/metatag/src/Entity/MetatagDefaults.php +++ b/web/modules/contrib/metatag/src/Entity/MetatagDefaults.php @@ -3,6 +3,7 @@ namespace Drupal\metatag\Entity; use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Config\FileStorage; use Drupal\Core\Config\InstallStorage; use Drupal\Core\Config\StorageInterface; @@ -120,4 +121,26 @@ class MetatagDefaults extends ConfigEntityBase implements MetatagDefaultsInterfa } } + /** + * {@inheritdoc} + */ + public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) { + // Put always Global in 1st place and front page later if available. + if ($a->id() == 'global') { + return -1; + } + elseif ($b->id() == 'global') { + return 1; + } + elseif ($a->id() == 'front') { + return -1; + } + elseif ($b->id() == 'front') { + return 1; + } + + // Use the default sort function. + return parent::sort($a, $b); + } + } diff --git a/web/modules/contrib/metatag/src/Form/MetatagDefaultsForm.php b/web/modules/contrib/metatag/src/Form/MetatagDefaultsForm.php index 54e032f06..ee2997237 100644 --- a/web/modules/contrib/metatag/src/Form/MetatagDefaultsForm.php +++ b/web/modules/contrib/metatag/src/Form/MetatagDefaultsForm.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\metatag\MetatagManager; +use Drupal\page_manager\Entity\PageVariant; /** * Class MetatagDefaultsForm. @@ -49,8 +50,8 @@ class MetatagDefaultsForm extends EntityForm { $options = $this->getAvailableBundles(); $form['id'] = [ '#type' => 'select', - '#title' => t('Type'), - '#description' => t('Select the type of default meta tags you would like to add.'), + '#title' => $this->t('Type'), + '#description' => $this->t('Select the type of default meta tags you would like to add.'), '#options' => $options, '#required' => TRUE, '#default_value' => $default_type, @@ -75,8 +76,26 @@ class MetatagDefaultsForm extends EntityForm { $values = $metatag_defaults->get('tags'); } - // Add metatag form fields. - $form = $metatag_manager->form($values, $form); + // Retrieve configuration settings. + $settings = $this->config('metatag.settings'); + $entity_type_groups = $settings->get('entity_type_groups'); + + // Find the current entity type and bundle. + $metatag_defaults_id = $metatag_defaults->id(); + $type_parts = explode('__', $metatag_defaults_id); + $entity_type = $type_parts[0]; + $entity_bundle = isset($type_parts[1]) ? $type_parts[1] : NULL; + + // See if there are requested groups for this entity type and bundle. + $groups = !empty($entity_type_groups[$entity_type]) && !empty($entity_type_groups[$entity_type][$entity_bundle]) ? $entity_type_groups[$entity_type][$entity_bundle] : []; + // Limit the form to requested groups, if any. + if (!empty($groups)) { + $form = $metatag_manager->form($values, $form, [$entity_type], $groups); + } + // Otherwise, display all groups. + else { + $form = $metatag_manager->form($values, $form); + } return $form; } @@ -143,7 +162,21 @@ class MetatagDefaultsForm extends EntityForm { // Get the bundle label. $bundle_manager = \Drupal::service('entity_type.bundle.info'); $bundle_info = $bundle_manager->getBundleInfo($entity_type); - $entity_label .= ': ' . $bundle_info[$entity_bundle]['label']; + if ($entity_type === 'page_variant') { + // Check if page manager is enabled and try to load the page variant + // so the label of the variant can be used. + $moduleHandler = \Drupal::service('module_handler'); + if ($moduleHandler->moduleExists('metatag_page_manager')) { + $page_variant = PageVariant::load($entity_bundle); + $page = $page_variant->getPage(); + if ($page_variant) { + $entity_label .= ': ' . $page->label() . ': ' . $page_variant->label(); + } + } + } + else { + $entity_label .= ': ' . $bundle_info[$entity_bundle]['label']; + } } // Set the label to the config entity. @@ -192,7 +225,7 @@ class MetatagDefaultsForm extends EntityForm { */ protected function getAvailableBundles() { $options = []; - $entity_types = $this->getSupportedEntityTypes(); + $entity_types = static::getSupportedEntityTypes(); /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager */ $entity_manager = \Drupal::service('entity_type.manager'); /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */ @@ -221,7 +254,7 @@ class MetatagDefaultsForm extends EntityForm { * @return array * A list of available entity types as $machine_name => $label. */ - protected function getSupportedEntityTypes() { + public static function getSupportedEntityTypes() { $entity_types = []; /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager */ @@ -255,7 +288,7 @@ class MetatagDefaultsForm extends EntityForm { // viewable. $links = $definition->get('links'); if (!empty($links)) { - $entity_types[$entity_name] = $this->getEntityTypeLabel($definition); + $entity_types[$entity_name] = static::getEntityTypeLabel($definition); } } } @@ -266,13 +299,13 @@ class MetatagDefaultsForm extends EntityForm { /** * Returns the text label for the entity type specified. * - * @param Drupal\Core\Entity\EntityTypeInterface $entityType + * @param \Drupal\Core\Entity\EntityTypeInterface $entityType * The entity type to process. * * @return string * A label. */ - protected function getEntityTypeLabel(EntityTypeInterface $entityType) { + public static function getEntityTypeLabel(EntityTypeInterface $entityType) { $label = $entityType->getLabel(); if (is_a($label, 'Drupal\Core\StringTranslation\TranslatableMarkup')) { diff --git a/web/modules/contrib/metatag/src/Form/MetatagSettingsForm.php b/web/modules/contrib/metatag/src/Form/MetatagSettingsForm.php new file mode 100644 index 000000000..a77e24603 --- /dev/null +++ b/web/modules/contrib/metatag/src/Form/MetatagSettingsForm.php @@ -0,0 +1,104 @@ +<?php + +namespace Drupal\metatag\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Defines the configuration export form. + */ +class MetatagSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'metatag_admin_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['metatag.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $settings = $this->config('metatag.settings')->get('entity_type_groups'); + + $form['entity_type_groups'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Entity type / Group Mapping'), + '#description' => $this->t('Identify which metatag groups should be available on which entity type / bundle combination. Unselected groups will not appear on the configuration form for that entity type, reducing the size of the form and increasing performance. If no groups are selected for a type, all groups will appear.'), + '#tree' => TRUE, + ]; + + $metatag_manager = \Drupal::service('metatag.manager'); + $bundle_manager = \Drupal::service('entity_type.bundle.info'); + $metatag_groups = $metatag_manager->sortedGroups(); + $entity_types = MetatagDefaultsForm::getSupportedEntityTypes(); + foreach ($entity_types as $entity_type => $entity_label) { + $bundles = $bundle_manager->getBundleInfo($entity_type); + foreach ($bundles as $bundle_id => $bundle_info) { + // Create an option list for each bundle. + $options = []; + foreach ($metatag_groups as $group_name => $group_info) { + $options[$group_name] = $group_info['label']; + } + // Format a collapsible fieldset for each group for easier readability. + $form['entity_type_groups'][$entity_type][$bundle_id] = [ + '#type' => 'details', + '#title' => $entity_label . ': ' . $bundle_info['label'], + ]; + $form['entity_type_groups'][$entity_type][$bundle_id][] = [ + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => isset($settings[$entity_type]) && isset($settings[$entity_type][$bundle_id]) ? $settings[$entity_type][$bundle_id] : [], + ]; + } + } + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $settings = $this->config('metatag.settings'); + $value = $form_state->getValue('entity_type_groups'); + $value = static::arrayFilterRecursive($value); + // Remove the extra layer created by collapsible fieldsets. + foreach ($value as $entity_type => $bundle) { + foreach ($bundle as $bundle_id => $groups) { + $value[$entity_type][$bundle_id] = $groups[0]; + } + } + $settings->set('entity_type_groups', $value)->save(); + parent::submitForm($form, $form_state); + } + + /** + * Recursively filter results. + * + * @param array $input + * The array to filter. + * + * @return array + * The filtered array. + */ + public static function arrayFilterRecursive(array $input) { + foreach ($input as &$value) { + if (is_array($value)) { + $value = static::arrayFilterRecursive($value); + } + } + return array_filter($input); + } + +} diff --git a/web/modules/contrib/metatag/src/Generator/MetatagGroupGenerator.php b/web/modules/contrib/metatag/src/Generator/MetatagGroupGenerator.php index 10cee2830..9b737e302 100644 --- a/web/modules/contrib/metatag/src/Generator/MetatagGroupGenerator.php +++ b/web/modules/contrib/metatag/src/Generator/MetatagGroupGenerator.php @@ -12,11 +12,15 @@ use Drupal\Console\Core\Utils\TwigRenderer; class MetatagGroupGenerator extends Generator { /** + * The console manager. + * * @var \Drupal\Console\Extension\Manager */ protected $extensionManager; /** + * The twig renderer. + * * @var \Drupal\Console\Core\Utils\TwigRenderer */ protected $renderer; @@ -24,8 +28,10 @@ class MetatagGroupGenerator extends Generator { /** * MetatagGroupGenerator constructor. * - * @param Drupal\Console\Extension\Manager $extensionManager - * @param Drupal\Console\Core\Utils\TwigRenderer $renderer + * @param \Drupal\Console\Extension\Manager $extensionManager + * An extension manager. + * @param \Drupal\Console\Core\Utils\TwigRenderer $renderer + * Twig renderer. */ public function __construct(Manager $extensionManager, TwigRenderer $renderer) { $this->extensionManager = $extensionManager; @@ -38,12 +44,19 @@ class MetatagGroupGenerator extends Generator { * Generator plugin. * * @param string $base_class + * Base class. * @param string $module + * Module name. * @param string $label + * Group label. * @param string $description + * Group description. * @param string $plugin_id + * Plugin ID. * @param string $class_name + * Class name. * @param string $weight + * Group weight. */ public function generate($base_class, $module, $label, $description, $plugin_id, $class_name, $weight) { $parameters = [ diff --git a/web/modules/contrib/metatag/src/Generator/MetatagTagGenerator.php b/web/modules/contrib/metatag/src/Generator/MetatagTagGenerator.php index a5b60da39..ba089b64a 100644 --- a/web/modules/contrib/metatag/src/Generator/MetatagTagGenerator.php +++ b/web/modules/contrib/metatag/src/Generator/MetatagTagGenerator.php @@ -12,11 +12,15 @@ use Drupal\Console\Core\Utils\TwigRenderer; class MetatagTagGenerator extends Generator { /** + * An extension manager. + * * @var \Drupal\Console\Extension\Manager */ protected $extensionManager; /** + * The twig renderer. + * * @var \Drupal\Console\Core\Utils\TwigRenderer */ protected $render; @@ -24,8 +28,10 @@ class MetatagTagGenerator extends Generator { /** * MetatagTagGenerator constructor. * - * @param Drupal\Console\Extension\Manager $extensionManager - * @param Drupal\Console\Core\Utils\TwigRenderer $render + * @param \Drupal\Console\Extension\Manager $extensionManager + * An extension manager. + * @param \Drupal\Console\Core\Utils\TwigRenderer $render + * Twig renderer. */ public function __construct(Manager $extensionManager, TwigRenderer $render) { $this->extensionManager = $extensionManager; @@ -38,17 +44,29 @@ class MetatagTagGenerator extends Generator { * Generator plugin. * * @param string $base_class + * Base class. * @param string $module + * Module name. * @param string $name + * Tag name. * @param string $label + * Tag label. * @param string $description + * Tag description. * @param string $plugin_id + * Plugin ID. * @param string $class_name + * Class name. * @param string $group + * Tag group. * @param string $weight + * Tag weight. * @param string $type + * Tag type. * @param bool $secure + * Is secure. * @param bool $multiple + * Is multiple. */ public function generate($base_class, $module, $name, $label, $description, $plugin_id, $class_name, $group, $weight, $type, $secure, $multiple) { $parameters = [ diff --git a/web/modules/contrib/metatag/src/MetatagDefaultsListBuilder.php b/web/modules/contrib/metatag/src/MetatagDefaultsListBuilder.php index 3811665f0..0b4945009 100644 --- a/web/modules/contrib/metatag/src/MetatagDefaultsListBuilder.php +++ b/web/modules/contrib/metatag/src/MetatagDefaultsListBuilder.php @@ -13,17 +13,42 @@ class MetatagDefaultsListBuilder extends ConfigEntityListBuilder { /** * {@inheritdoc} */ - public function load() { - $entities = parent::load(); + protected function getEntityIds() { + $query = $this->getStorage()->getQuery() + ->condition('id', 'global', '<>'); - // Move the Global defaults to the top. Don't assume that the global config - // exists, it might have been removed. - if (isset($entities['global'])) { - return ['global' => $entities['global']] + $entities; + // Only add the pager if a limit is specified. + if ($this->limit) { + $query->pager($this->limit); } - else { - return $entities; + + $entity_ids = $query->execute(); + + // Load global entity always. + return $entity_ids + $this->getParentIds($entity_ids); + } + + /** + * Gets the parent entity ids for the list of entities to load. + * + * @param array $entity_ids + * The metatag entity ids. + * + * @return array + * The list of parents to load + */ + protected function getParentIds(array $entity_ids) { + $parents = ['global' => 'global']; + foreach ($entity_ids as $entity_id) { + if (strpos($entity_id, '__') !== FALSE) { + $entity_id_array = explode('__', $entity_id); + $parent = reset($entity_id_array); + $parents[$parent] = $parent; + } } + $parents_query = $this->getStorage()->getQuery() + ->condition('id', $parents, 'IN'); + return $parents_query->execute(); } /** @@ -64,7 +89,7 @@ class MetatagDefaultsListBuilder extends ConfigEntityListBuilder { /** * Renders the Metatag defaults label plus its configuration. * - * @param Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\EntityInterface $entity * The Metatag defaults entity. * * @return array diff --git a/web/modules/contrib/metatag/src/MetatagManager.php b/web/modules/contrib/metatag/src/MetatagManager.php index 5f04a0880..57c4ca281 100644 --- a/web/modules/contrib/metatag/src/MetatagManager.php +++ b/web/modules/contrib/metatag/src/MetatagManager.php @@ -7,7 +7,6 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; -use Drupal\metatag\Entity\MetatagDefaults; use Drupal\views\ViewEntityInterface; /** @@ -18,27 +17,35 @@ use Drupal\views\ViewEntityInterface; class MetatagManager implements MetatagManagerInterface { /** + * The group plugin manager. + * * @var \Drupal\metatag\MetatagGroupPluginManager */ protected $groupPluginManager; /** + * The tag plugin manager. + * * @var \Drupal\metatag\MetatagTagPluginManager */ protected $tagPluginManager; /** + * The Metatag defaults. + * * @var array */ protected $metatagDefaults; /** + * The Metatag token. + * * @var \Drupal\metatag\MetatagToken */ protected $tokenService; /** - * Metatag logging channel. + * The Metatag logging channel. * * @var \Drupal\Core\Logger\LoggerChannelInterface */ @@ -47,15 +54,15 @@ class MetatagManager implements MetatagManagerInterface { /** * Constructor for MetatagManager. * - * @param Drupal\metatag\MetatagGroupPluginManager $groupPluginManager + * @param \Drupal\metatag\MetatagGroupPluginManager $groupPluginManager * The MetatagGroupPluginManager object. - * @param Drupal\metatag\MetatagTagPluginManager $tagPluginManager + * @param \Drupal\metatag\MetatagTagPluginManager $tagPluginManager * The MetatagTagPluginMπanager object. - * @param Drupal\metatag\MetatagToken $token + * @param \Drupal\metatag\MetatagToken $token * The MetatagToken object. - * @param Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory * The LoggerChannelFactoryInterface object. - * @param Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * The EntityTypeManagerInterface object. */ public function __construct(MetatagGroupPluginManager $groupPluginManager, @@ -246,7 +253,6 @@ class MetatagManager implements MetatagManagerInterface { $groups_and_tags = $this->sortedGroupsWithTags(); - $first = TRUE; foreach ($groups_and_tags as $group_name => $group) { // Only act on groups that have tags and are in the list of included // groups (unless that list is null). @@ -255,8 +261,7 @@ class MetatagManager implements MetatagManagerInterface { $element[$group_name]['#type'] = 'details'; $element[$group_name]['#title'] = $group['label']; $element[$group_name]['#description'] = $group['description']; - $element[$group_name]['#open'] = $first; - $first = FALSE; + $element[$group_name]['#open'] = FALSE; foreach ($group['tags'] as $tag_name => $tag) { // Only act on tags in the included tags list, unless that is null. @@ -268,6 +273,11 @@ class MetatagManager implements MetatagManagerInterface { $tag_value = isset($values[$tag_name]) ? $values[$tag_name] : NULL; $tag->setValue($tag_value); + // Open any groups that have non-empty values. + if (!empty($tag_value)) { + $element[$group_name]['#open'] = TRUE; + } + // Create the bit of form for this tag. $element[$group_name][$tag_name] = $tag->form($element); } @@ -279,7 +289,13 @@ class MetatagManager implements MetatagManagerInterface { } /** - * Returns a list of the metatag fields on an entity. + * Returns a list of the Metatag fields on an entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity to examine. + * + * @return array + * The fields from the entity which are Metatag fields. */ protected function getFields(ContentEntityInterface $entity) { $field_list = []; @@ -309,10 +325,13 @@ class MetatagManager implements MetatagManagerInterface { /** * Returns a list of the meta tags with values from a field. * - * @param Drupal\Core\Entity\ContentEntityInterface $entity + * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The ContentEntityInterface object. * @param string $field_name * The name of the field to work on. + * + * @return array + * Array of field tags. */ protected function getFieldTags(ContentEntityInterface $entity, $field_name) { $tags = []; @@ -328,10 +347,13 @@ class MetatagManager implements MetatagManagerInterface { } /** + * Returns default meta tags for an entity. * - * - * @param Drupal\Core\Entity\ContentEntityInterface $entity + * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity to work on. + * + * @return array + * The default meta tags appropriate for this entity. */ public function getDefaultMetatags(ContentEntityInterface $entity = NULL) { // Get general global metatags. @@ -369,7 +391,7 @@ class MetatagManager implements MetatagManagerInterface { } /** - * + * Returns global meta tags. * * @return array * The global meta tags. @@ -379,7 +401,7 @@ class MetatagManager implements MetatagManagerInterface { } /** - * + * Returns special meta tags. * * @return array * The defaults for this page, if it's a special page. @@ -401,9 +423,9 @@ class MetatagManager implements MetatagManagerInterface { } /** + * Returns default meta tags for an entity. * - * - * @param Drupal\Core\Entity\ContentEntityInterface $entity + * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity to work with. * * @return array @@ -466,6 +488,12 @@ class MetatagManager implements MetatagManagerInterface { * Render array with tag elements. */ public function generateRawElements(array $tags, $entity = NULL) { + // Ignore the update.php path. + $request = \Drupal::request(); + if ($request->getBaseUrl() == '/update.php') { + return []; + } + $rawTags = []; $metatag_tags = $this->tagPluginManager->getDefinitions(); @@ -507,12 +535,7 @@ class MetatagManager implements MetatagManagerInterface { $tag->setValue($value); $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); - if ($tag->type() === 'image') { - $processed_value = $this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode]); - } - else { - $processed_value = PlainTextOutput::renderFromHtml(htmlspecialchars_decode($this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode]))); - } + $processed_value = PlainTextOutput::renderFromHtml(htmlspecialchars_decode($this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode]))); // Now store the value with processed tokens back into the plugin. $tag->setValue($processed_value); @@ -521,7 +544,18 @@ class MetatagManager implements MetatagManagerInterface { $output = $tag->output(); if (!empty($output)) { - $rawTags[$tag_name] = $output; + $output = $tag->multiple() ? $output : [$output]; + + // Backwards compatibility for modules which don't support this logic. + if (isset($output['#tag'])) { + $output = [$output]; + } + + foreach ($output as $index => $element) { + // Add index to tag name as suffix to avoid having same key. + $index_tag_name = $tag->multiple() ? $tag_name . '_' . $index : $tag_name; + $rawTags[$index_tag_name] = $element; + } } } } diff --git a/web/modules/contrib/metatag/src/MetatagToken.php b/web/modules/contrib/metatag/src/MetatagToken.php index a1190275a..e240fc019 100644 --- a/web/modules/contrib/metatag/src/MetatagToken.php +++ b/web/modules/contrib/metatag/src/MetatagToken.php @@ -3,6 +3,7 @@ namespace Drupal\metatag; use Drupal\Core\Utility\Token; +use Drupal\Core\Render\BubbleableMetadata; /** * Token handling service. Uses core token service or contributed Token. @@ -35,17 +36,20 @@ class MetatagToken { * Arguments for token->replace(). * @param array $options * Any additional options necessary. + * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata + * (optional) An object to which static::generate() and the hooks and + * functions that it invokes will add their required bubbleable metadata. * * @return mixed|string * The processed string. */ - public function replace($string, array $data = [], array $options = []) { + public function replace($string, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL) { // Set default requirements for metatag unless options specify otherwise. $options = $options + [ 'clear' => TRUE, ]; - $replaced = $this->token->replace($string, $data, $options); + $replaced = $this->token->replace($string, $data, $options, $bubbleable_metadata); // Ensure that there are no double-slash sequences due to empty token // values. diff --git a/web/modules/contrib/metatag/src/Plugin/Field/FieldType/MetatagFieldItem.php b/web/modules/contrib/metatag/src/Plugin/Field/FieldType/MetatagFieldItem.php index 930fa4b49..1a6726a80 100644 --- a/web/modules/contrib/metatag/src/Plugin/Field/FieldType/MetatagFieldItem.php +++ b/web/modules/contrib/metatag/src/Plugin/Field/FieldType/MetatagFieldItem.php @@ -50,7 +50,7 @@ class MetatagFieldItem extends FieldItemBase { */ public function isEmpty() { $value = $this->get('value')->getValue(); - return $value === NULL || $value === ''; + return $value === NULL || $value === '' || $value === serialize([]); } /** diff --git a/web/modules/contrib/metatag/src/Plugin/Field/FieldWidget/MetatagFirehose.php b/web/modules/contrib/metatag/src/Plugin/Field/FieldWidget/MetatagFirehose.php index 0acbb2b71..6797f59ae 100644 --- a/web/modules/contrib/metatag/src/Plugin/Field/FieldWidget/MetatagFirehose.php +++ b/web/modules/contrib/metatag/src/Plugin/Field/FieldWidget/MetatagFirehose.php @@ -74,8 +74,24 @@ class MetatagFirehose extends WidgetBase implements ContainerFactoryPluginInterf } } - // Create the form element. - $element = $this->metatagManager->form($values, $element, [$item->getEntity()->getentityTypeId()]); + // Retrieve configuration settings. + $settings = \Drupal::config('metatag.settings'); + $entity_type_groups = $settings->get('entity_type_groups'); + + // Find the current entity type and bundle. + $entity_type = $item->getEntity()->getentityTypeId(); + $entity_bundle = $item->getEntity()->bundle(); + + // See if there are requested groups for this entity type and bundle. + $groups = !empty($entity_type_groups[$entity_type]) && !empty($entity_type_groups[$entity_type][$entity_bundle]) ? $entity_type_groups[$entity_type][$entity_bundle] : []; + // Limit the form to requested groups, if any. + if (!empty($groups)) { + $element = $this->metatagManager->form($values, $element, [$entity_type], $groups); + } + // Otherwise, display all groups. + else { + $element = $this->metatagManager->form($values, $element, [$entity_type]); + } // Put the form element into the form's "advanced" group. $element['#group'] = 'advanced'; @@ -90,7 +106,6 @@ class MetatagFirehose extends WidgetBase implements ContainerFactoryPluginInterf // Flatten the values array to remove the groups and then serialize all the // meta tags into one value for storage. $tag_manager = \Drupal::service('plugin.manager.metatag.tag'); - $tags = $tag_manager->getDefinitions(); foreach ($values as &$value) { $flattened_value = []; foreach ($value as $group) { diff --git a/web/modules/contrib/metatag/src/Plugin/Field/MetatagEntityFieldItemList.php b/web/modules/contrib/metatag/src/Plugin/Field/MetatagEntityFieldItemList.php index 8df202918..69c11265e 100644 --- a/web/modules/contrib/metatag/src/Plugin/Field/MetatagEntityFieldItemList.php +++ b/web/modules/contrib/metatag/src/Plugin/Field/MetatagEntityFieldItemList.php @@ -5,7 +5,7 @@ namespace Drupal\metatag\Plugin\Field; use Drupal\Core\Field\FieldItemList; /** - * Defines a metatag list class for better normalization targetting. + * Defines a metatag list class for better normalization targeting. */ class MetatagEntityFieldItemList extends FieldItemList { } diff --git a/web/modules/contrib/metatag/src/Plugin/GraphQL/Scalars/MetatagScalar.php b/web/modules/contrib/metatag/src/Plugin/GraphQL/Scalars/MetatagScalar.php new file mode 100644 index 000000000..105ef5366 --- /dev/null +++ b/web/modules/contrib/metatag/src/Plugin/GraphQL/Scalars/MetatagScalar.php @@ -0,0 +1,20 @@ +<?php + +namespace Drupal\metatag\Plugin\GraphQL\Scalars; + +use Drupal\graphql\Plugin\GraphQL\Scalars\Internal\StringScalar; + +/** + * Metatag module dummy type. + * + * Metatag module defines a custom data type that essentially is a string, but + * not called "string", which causes the GraphQL type system chokes. + * + * @GraphQLScalar( + * id = "metatag", + * name = "metatag", + * type = "string" + * ) + */ +class MetatagScalar extends StringScalar { +} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Group/AppLinks.php b/web/modules/contrib/metatag/src/Plugin/metatag/Group/AppLinks.php deleted file mode 100644 index 2ebbbd1b4..000000000 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Group/AppLinks.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\metatag\Plugin\metatag\Group; - -/** - * The App Links group. - * - * @MetatagGroup( - * id = "app_links", - * label = @Translation("App Links"), - * description = @Translation("Meta tags used to expose App Links for app deep linking. See <a href='http://applinks.org/'>applinks.org</a> for details and documentation.."), - * weight = 9 - * ) - */ -class AppLinks extends GroupBase { - // Inherits everything from Base. -} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Group/DublinCoreAdvanced.php b/web/modules/contrib/metatag/src/Plugin/metatag/Group/DublinCoreAdvanced.php deleted file mode 100644 index 8f29fb0a2..000000000 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Group/DublinCoreAdvanced.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\metatag\Plugin\metatag\Group; - -/** - * The Dublin Core Advanced group. - * - * @MetatagGroup( - * id = "dublin_core_advanced", - * label = @Translation("Dublin Core - Advanced tags"), - * description = @Translation("These tags are not part of the Metadata Element Set but may be useful for certain scenarios."), - * weight = 8 - * ) - */ -class DublinCoreAdvanced extends GroupBase { - // Inherits everything from Base. -} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Group/DublinCoreBasic.php b/web/modules/contrib/metatag/src/Plugin/metatag/Group/DublinCoreBasic.php deleted file mode 100644 index bb9b0bd28..000000000 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Group/DublinCoreBasic.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\metatag\Plugin\metatag\Group; - -/** - * The Dublin Core Basic group. - * - * @MetatagGroup( - * id = "dublin_core_basic", - * label = @Translation("Dublin Core - Basic tags"), - * description = @Translation("The Dublin Core Metadata Element Set, aka 'Dublin Core meta tags', are a set of internationally standardized metadata tags used to describe content to make identification and classification of content easier; the standards are controlled by the <a href='http://dublincore.org/'>Dublin Core Metadata Initiative (DCMI)</a>."), - * weight = 7 - * ) - */ -class DublinCoreBasic extends GroupBase { - // Inherits everything from Base. -} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Group/Facebook.php b/web/modules/contrib/metatag/src/Plugin/metatag/Group/Facebook.php deleted file mode 100644 index 953fb9b4c..000000000 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Group/Facebook.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\metatag\Plugin\metatag\Group; - -/** - * The basic group. - * - * @MetatagGroup( - * id = "facebook", - * label = @Translation("Facebook"), - * description = @Translation("Meta tags used to integrate with Facebook's APIs. Most sites do not need to use these, they are primarily of benefit for sites using either the Facebook widgets, the Facebook Connect single-signon system, or are using Facebook's APIs in a custom way. Sites that do need these meta tags usually will only need to set them globally."), - * weight = 6 - * ) - */ -class Facebook extends GroupBase { - // Inherits everything from Base. -} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Group/GooglePlus.php b/web/modules/contrib/metatag/src/Plugin/metatag/Group/GooglePlus.php deleted file mode 100644 index 6fd3c0748..000000000 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Group/GooglePlus.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\metatag\Plugin\metatag\Group; - -/** - * The open graph group. - * - * @MetatagGroup( - * id = "google_plus", - * label = @Translation("Google Plus"), - * description = @Translation("A set of meta tags specially for controlling the summaries displayed when content is shared on <a href='https://plus.google.com/'>Google+</a>."), - * weight = 5 - * ) - */ -class GooglePlus extends GroupBase { - // Inherits everything from Base. -} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Group/SiteValidation.php b/web/modules/contrib/metatag/src/Plugin/metatag/Group/SiteValidation.php deleted file mode 100644 index 9f57208c2..000000000 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Group/SiteValidation.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\metatag\Plugin\metatag\Group; - -/** - * The Site Validation group. - * - * @MetatagGroup( - * id = "site_validation", - * label = @Translation("Site Validation"), - * description = @Translation("These meta tags are used to confirm site ownership with search engines and other services."), - * weight = 10 - * ) - */ -class SiteValidation extends GroupBase { - // Inherits everything from Base. -} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Group/TwitterCards.php b/web/modules/contrib/metatag/src/Plugin/metatag/Group/TwitterCards.php deleted file mode 100644 index 8cd680f11..000000000 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Group/TwitterCards.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\metatag\Plugin\metatag\Group; - -/** - * The TwitterCards group. - * - * @MetatagGroup( - * id = "twitter_cards", - * label = @Translation("Twitter Cards"), - * description = @Translation("A set of meta tags specially for controlling the summaries displayed when content is shared on <a href='https://twitter.com/'>Twitter</a>."), - * weight = 4 - * ) - */ -class TwitterCards extends GroupBase { - // Inherits everything from Base. -} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Tag/GeoPosition.php b/web/modules/contrib/metatag/src/Plugin/metatag/Tag/GeoPosition.php index cc670b9e6..c50d7c6b2 100644 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Tag/GeoPosition.php +++ b/web/modules/contrib/metatag/src/Plugin/metatag/Tag/GeoPosition.php @@ -8,7 +8,7 @@ namespace Drupal\metatag\Plugin\metatag\Tag; * @MetatagTag( * id = "geo_position", * label = @Translation("Geographical position"), - * description = @Translation("Geo-spatial information in 'latitude, longitude' format, e.g. '50.167958, -97.133185'; <a href='https://en.wikipedia.org/wiki/Geographic_coordinate_system'>see Wikipedia for details</a>."), + * description = @Translation("Geo-spatial information in 'latitude; longitude' format, e.g. '50.167958; -97.133185'; <a href='https://en.wikipedia.org/wiki/Geographic_coordinate_system'>see Wikipedia for details</a>."), * name = "geo.position", * group = "advanced", * weight = 0, diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Tag/Google.php b/web/modules/contrib/metatag/src/Plugin/metatag/Tag/Google.php new file mode 100644 index 000000000..0f81d955d --- /dev/null +++ b/web/modules/contrib/metatag/src/Plugin/metatag/Tag/Google.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\metatag\Plugin\metatag\Tag; + +/** + * Provides a plugin for the 'google' meta tag. + * + * @MetatagTag( + * id = "google", + * label = @Translation("Google"), + * description = @Translation("This meta tag communicates with Google. There are currently two directives supported: 'nositelinkssearchbox' to not to show the sitelinks search box, and 'notranslate' to ask Google not to offer a translation of the page. Both options may be added, just separate them with a comma. See <a href='https://support.google.com/webmasters/answer/79812?hl=en'>meta tags that Google understands</a> for further details."), + * name = "google", + * group = "advanced", + * weight = 5, + * type = "label", + * secure = FALSE, + * multiple = FALSE + * ) + */ +class Google extends MetaNameBase { + // Nothing here yet. Just a placeholder class for a plugin. +} diff --git a/web/modules/contrib/metatag/src/Plugin/metatag/Tag/MetaNameBase.php b/web/modules/contrib/metatag/src/Plugin/metatag/Tag/MetaNameBase.php index 5eafb805a..9212571ef 100644 --- a/web/modules/contrib/metatag/src/Plugin/metatag/Tag/MetaNameBase.php +++ b/web/modules/contrib/metatag/src/Plugin/metatag/Tag/MetaNameBase.php @@ -314,15 +314,17 @@ abstract class MetaNameBase extends PluginBase { * A render array or an empty string. */ public function output() { - // Parse out the image URL, if needed. - $value = $this->parseImageUrl(); - $value = $this->tidy($value); - - if (empty($value)) { + if (empty($this->value)) { // If there is no value, we don't want a tag output. - $element = ''; + return $this->multiple() ? [] : ''; } - else { + + // Parse out the image URL, if needed. + $value = $this->parseImageUrl(); + $values = $this->multiple() ? explode(',', $value) : [$value]; + $elements = []; + foreach ($values as $value) { + $value = $this->tidy($value); if ($this->requiresAbsoluteUrl()) { // Relative URL. if (parse_url($value, PHP_URL_HOST) == NULL) { @@ -339,7 +341,7 @@ abstract class MetaNameBase extends PluginBase { $value = str_replace('http://', 'https://', $value); } - $element = [ + $elements[] = [ '#tag' => 'meta', '#attributes' => [ $this->nameAttribute => $this->name, @@ -348,7 +350,7 @@ abstract class MetaNameBase extends PluginBase { ]; } - return $element; + return $this->multiple() ? $elements : reset($elements); } /** diff --git a/web/modules/contrib/metatag/src/Tests/MetatagAdminTest.php b/web/modules/contrib/metatag/src/Tests/MetatagAdminTest.php index fe662796b..3072b62e0 100644 --- a/web/modules/contrib/metatag/src/Tests/MetatagAdminTest.php +++ b/web/modules/contrib/metatag/src/Tests/MetatagAdminTest.php @@ -3,7 +3,9 @@ namespace Drupal\metatag\Tests; use Drupal\metatag\MetatagManager; +use Drupal\metatag\Entity\MetatagDefaults; use Drupal\simpletest\WebTestBase; +use Drupal\Tests\metatag\Functional\MetatagHelperTrait; /** * Tests the Metatag administration. @@ -12,6 +14,8 @@ use Drupal\simpletest\WebTestBase; */ class MetatagAdminTest extends WebTestBase { + use MetatagHelperTrait; + /** * {@inheritdoc} */ @@ -441,4 +445,53 @@ class MetatagAdminTest extends WebTestBase { $this->assertLink('Delete'); } + /** + * Test that metatag list page pager works as expected. + */ + public function testListPager() { + $this->loginUser1(); + + $this->drupalGet('admin/config/search/metatag'); + $this->assertLinkByHref('/admin/config/search/metatag/global'); + $this->assertLinkByHref('/admin/config/search/metatag/front'); + $this->assertLinkByHref('/admin/config/search/metatag/403'); + $this->assertLinkByHref('/admin/config/search/metatag/404'); + $this->assertLinkByHref('/admin/config/search/metatag/node'); + $this->assertLinkByHref('/admin/config/search/metatag/taxonomy_term'); + $this->assertLinkByHref('/admin/config/search/metatag/user'); + + // Create 50 vocabularies and generate metatag defaults for all of them. + for ($i = 0; $i < 50; $i++) { + $vocabulary = $this->createVocabulary(); + MetatagDefaults::create([ + 'id' => 'taxonomy_term__' . $vocabulary->id(), + 'label' => 'Taxonomy term: ' . $vocabulary->label(), + ])->save(); + } + + // Reload the page. + $this->drupalGet('admin/config/search/metatag'); + $this->assertLinkByHref('/admin/config/search/metatag/global'); + $this->assertLinkByHref('/admin/config/search/metatag/front'); + $this->assertLinkByHref('/admin/config/search/metatag/403'); + $this->assertLinkByHref('/admin/config/search/metatag/404'); + $this->assertLinkByHref('/admin/config/search/metatag/node'); + $this->assertLinkByHref('/admin/config/search/metatag/taxonomy_term'); + // User entity not visible because it has been pushed to the next page. + $this->assertNoLinkByHref('/admin/config/search/metatag/user'); + $this->clickLinkPartialName('Next'); + + // Go to next page and confirm that parents are loaded and user us present. + $this->assertLinkByHref('/admin/config/search/metatag/global'); + $this->assertLinkByHref('/admin/config/search/metatag/taxonomy_term'); + // Main links not visible in the 2nd page. + $this->assertNoLinkByHref('/admin/config/search/metatag/front'); + $this->assertNoLinkByHref('/admin/config/search/metatag/403'); + $this->assertNoLinkByHref('/admin/config/search/metatag/404'); + $this->assertNoLinkByHref('/admin/config/search/metatag/node'); + // User is present because was pushed to page 2. + $this->assertLinkByHref('/admin/config/search/metatag/user'); + + } + } diff --git a/web/modules/contrib/metatag/src/Tests/MetatagHelperTrait.php b/web/modules/contrib/metatag/src/Tests/MetatagHelperTrait.php index 1fff4e48e..fc9538abf 100644 --- a/web/modules/contrib/metatag/src/Tests/MetatagHelperTrait.php +++ b/web/modules/contrib/metatag/src/Tests/MetatagHelperTrait.php @@ -93,7 +93,7 @@ trait MetatagHelperTrait { * be automatically generated. If the 'name' item is not present the 'vid' * will be used. * - * @return Drupal\taxonomy\Entity\Vocabulary + * @return \Drupal\taxonomy\Entity\Vocabulary * A fully formatted vocabulary object. */ private function createVocabulary(array $values = []) { diff --git a/web/modules/contrib/metatag/src/Tests/MetatagNodeTranslationTest.php b/web/modules/contrib/metatag/src/Tests/MetatagNodeTranslationTest.php index 07e0199ed..669726a34 100644 --- a/web/modules/contrib/metatag/src/Tests/MetatagNodeTranslationTest.php +++ b/web/modules/contrib/metatag/src/Tests/MetatagNodeTranslationTest.php @@ -45,6 +45,9 @@ class MetatagNodeTranslationTest extends WebTestBase { */ protected $adminUser; + /** + * Setup basic environment. + */ protected function setUp() { parent::setUp(); diff --git a/web/modules/contrib/metatag/src/Tests/MetatagTagTypesTest.php b/web/modules/contrib/metatag/src/Tests/MetatagTagTypesTest.php index c6647741a..c1404835b 100644 --- a/web/modules/contrib/metatag/src/Tests/MetatagTagTypesTest.php +++ b/web/modules/contrib/metatag/src/Tests/MetatagTagTypesTest.php @@ -117,8 +117,8 @@ class MetatagTagTypesTest extends WebTestBase { /** * Tests the 'secure' meta tag attribute. * - * Tests insecure values in og:image:secure_url (a tag with secure attribue - * set to TRUE) and in og:image (a tag with secure attribue set to FALSE). To + * Tests insecure values in og:image:secure_url (a tag with secure attribute + * set to TRUE) and in og:image (a tag with secure attribute set to FALSE). To * To pass og:image_secure should be changed to https:// and og:image * unchanged. */ diff --git a/web/modules/contrib/metatag/src/Tests/MetatagTagsTest.php b/web/modules/contrib/metatag/src/Tests/MetatagTagsTest.php index 46f28c819..5cdb3a501 100644 --- a/web/modules/contrib/metatag/src/Tests/MetatagTagsTest.php +++ b/web/modules/contrib/metatag/src/Tests/MetatagTagsTest.php @@ -21,6 +21,7 @@ class MetatagTagsTest extends MetatagTagsTestBase { 'geo_placename', 'geo_position', 'geo_region', + 'google', 'icbm', 'image_src', 'keywords', diff --git a/web/modules/contrib/metatag/src/Tests/MetatagTagsTestBase.php b/web/modules/contrib/metatag/src/Tests/MetatagTagsTestBase.php index 2f9b8e548..88fc3129b 100644 --- a/web/modules/contrib/metatag/src/Tests/MetatagTagsTestBase.php +++ b/web/modules/contrib/metatag/src/Tests/MetatagTagsTestBase.php @@ -3,7 +3,6 @@ namespace Drupal\metatag\Tests; use Drupal\Component\Render\FormattableMarkup; -use Drupal\Component\Utility\Html; use Drupal\simpletest\WebTestBase; use Symfony\Component\DependencyInjection\Container; @@ -112,7 +111,7 @@ abstract class MetatagTagsTestBase extends WebTestBase { public function testTagsInputOutput() { // Create a content type to test with. $this->createContentType(['type' => 'page']); - $node = $this->drupalCreateNode([ + $this->drupalCreateNode([ 'title' => t('Hello, world!'), 'type' => 'page', ]); @@ -304,7 +303,7 @@ abstract class MetatagTagsTestBase extends WebTestBase { * Generate a URL for an image. * * @return string - * An absolute URL to a non-existant image. + * An absolute URL to a non-existent image. */ private function randomImageUrl() { return 'http://www.example.com/images/' . $this->randomMachineName() . '.png'; diff --git a/web/modules/contrib/metatag/tests/modules/metatag_test_custom_route/metatag_test_custom_route.info.yml b/web/modules/contrib/metatag/tests/modules/metatag_test_custom_route/metatag_test_custom_route.info.yml index de7d3e089..dad9f064a 100644 --- a/web/modules/contrib/metatag/tests/modules/metatag_test_custom_route/metatag_test_custom_route.info.yml +++ b/web/modules/contrib/metatag/tests/modules/metatag_test_custom_route/metatag_test_custom_route.info.yml @@ -6,8 +6,8 @@ package: Testing dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/tests/modules/metatag_test_custom_route/src/Controller/MetatagTestCustomRouteController.php b/web/modules/contrib/metatag/tests/modules/metatag_test_custom_route/src/Controller/MetatagTestCustomRouteController.php index ca3e50087..24a537cd1 100644 --- a/web/modules/contrib/metatag/tests/modules/metatag_test_custom_route/src/Controller/MetatagTestCustomRouteController.php +++ b/web/modules/contrib/metatag/tests/modules/metatag_test_custom_route/src/Controller/MetatagTestCustomRouteController.php @@ -14,7 +14,7 @@ class MetatagTestCustomRouteController extends ControllerBase { */ public function test() { $render = [ - '#markup' => t('<p>Hello world!</p>'), + '#markup' => $this->t('<p>Hello world!</p>'), ]; return $render; diff --git a/web/modules/contrib/metatag/tests/modules/metatag_test_tag/metatag_test_tag.info.yml b/web/modules/contrib/metatag/tests/modules/metatag_test_tag/metatag_test_tag.info.yml index 0d031063f..04a5a51c8 100644 --- a/web/modules/contrib/metatag/tests/modules/metatag_test_tag/metatag_test_tag.info.yml +++ b/web/modules/contrib/metatag/tests/modules/metatag_test_tag/metatag_test_tag.info.yml @@ -6,8 +6,8 @@ package: Testing dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2018-03-29 -version: '8.x-1.5' +# Information added by Drupal.org packaging script on 2018-08-31 +version: '8.x-1.7' core: '8.x' project: 'metatag' -datestamp: 1522344488 +datestamp: 1535726412 diff --git a/web/modules/contrib/metatag/tests/src/Functional/MetatagHelperTrait.php b/web/modules/contrib/metatag/tests/src/Functional/MetatagHelperTrait.php index e88535510..659b5ef63 100644 --- a/web/modules/contrib/metatag/tests/src/Functional/MetatagHelperTrait.php +++ b/web/modules/contrib/metatag/tests/src/Functional/MetatagHelperTrait.php @@ -91,7 +91,7 @@ trait MetatagHelperTrait { * be automatically generated. If the 'name' item is not present the 'vid' * will be used. * - * @return Drupal\taxonomy\Entity\Vocabulary + * @return \Drupal\taxonomy\Entity\Vocabulary * A fully formatted vocabulary object. */ private function createVocabulary(array $values = []) { diff --git a/web/modules/contrib/metatag/tests/src/Functional/SchemaMetatagTest.php b/web/modules/contrib/metatag/tests/src/Functional/SchemaMetatagTest.php new file mode 100644 index 000000000..bc4330c6d --- /dev/null +++ b/web/modules/contrib/metatag/tests/src/Functional/SchemaMetatagTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\metatag\Functional; + +use Drupal\Tests\schema_web_page\Functional\SchemaWebPageTest; + +/** + * Wrapper to trigger one of the Schema.org Metatag module's tests. + * + * This will help avoid making changes to Metatag that trigger problems for + * separate submodules. + * + * @see https://www.drupal.org/project/metatag/issues/2994979 + * + * @group metatag + */ +class SchemaMetatagTest extends SchemaWebPageTest { + // Just run the tests as-is. +} diff --git a/web/modules/contrib/metatag/tests/src/Kernel/MetatagManagerTest.php b/web/modules/contrib/metatag/tests/src/Kernel/MetatagManagerTest.php index 4dcccdbb4..f086bef46 100644 --- a/web/modules/contrib/metatag/tests/src/Kernel/MetatagManagerTest.php +++ b/web/modules/contrib/metatag/tests/src/Kernel/MetatagManagerTest.php @@ -43,7 +43,69 @@ class MetatagManagerTest extends KernelTestBase { 'content' => 'http://www.example.com/example/foo.png', ], ], - 'og_image_url', + 'og_image_url_0', + ], + [ + [ + '#tag' => 'meta', + '#attributes' => [ + 'property' => 'og:image:width', + 'content' => 100, + ], + ], + 'og_image_width', + ], + [ + [ + '#tag' => 'meta', + '#attributes' => [ + 'property' => 'og:image:height', + 'content' => 100, + ], + ], + 'og_image_height', + ], + ], + ], + ]; + $this->assertEquals($expected, $tags); + } + + /** + * Tests metatags with multiple values return multiple metatags. + */ + public function testMetatagMultiple() { + /** @var \Drupal\metatag\MetatagManager $metatag_manager */ + $metatag_manager = \Drupal::service('metatag.manager'); + + $tags = $metatag_manager->generateElements([ + 'og_image_width' => 100, + 'og_image_height' => 100, + 'og_image_url' => 'http://www.example.com/example/foo.png, http://www.example.com/example/foo2.png', + ]); + + $expected = [ + '#attached' => [ + 'html_head' => [ + [ + [ + '#tag' => 'meta', + '#attributes' => [ + 'property' => 'og:image:url', + 'content' => 'http://www.example.com/example/foo.png', + ], + ], + 'og_image_url_0', + ], + [ + [ + '#tag' => 'meta', + '#attributes' => [ + 'property' => 'og:image:url', + 'content' => 'http://www.example.com/example/foo2.png', + ], + ], + 'og_image_url_1', ], [ [ diff --git a/web/modules/contrib/migrate_plus/config/schema/migrate_plus.data_types.schema.yml b/web/modules/contrib/migrate_plus/config/schema/migrate_plus.data_types.schema.yml index 27c8006f4..92e8288ec 100644 --- a/web/modules/contrib/migrate_plus/config/schema/migrate_plus.data_types.schema.yml +++ b/web/modules/contrib/migrate_plus/config/schema/migrate_plus.data_types.schema.yml @@ -6,7 +6,6 @@ migrate_plugin: plugin: type: string label: 'Plugin' - migrate_destination: type: migrate_plugin label: 'Destination' @@ -25,6 +24,35 @@ migrate_source: constants: type: ignore label: 'Constants' + ids: + type: ignore + label: 'Source IDs schema definition for migrate mapping table' + urls: + type: sequence + label: 'URLs from which to fetch' + sequence: + type: string + data_fetcher_plugin: + type: string + label: 'Fetcher plugin' + data_parser_plugin: + type: string + label: 'Parser plugin' + fields: + type: ignore + label: Mapping of field names to selectors + function: + type: string + label: 'Function to call on the service' + parameters: + type: ignore + label: 'Parameters to pass to function on the service' + response_type: + type: string + label: 'Type of response; XML string, object or array' + item_selector: + type: string + label: 'XPath selector' migrate_process: type: migrate_plugin diff --git a/web/modules/contrib/migrate_plus/config/schema/migrate_plus.process.schema.yml b/web/modules/contrib/migrate_plus/config/schema/migrate_plus.process.schema.yml index b1f123bc3..7b90a3d4e 100644 --- a/web/modules/contrib/migrate_plus/config/schema/migrate_plus.process.schema.yml +++ b/web/modules/contrib/migrate_plus/config/schema/migrate_plus.process.schema.yml @@ -71,9 +71,9 @@ migrate_plus.process.get: type: string label: 'Source key' -migrate_plus.process.iterator: +migrate_plus.process.sub_process: type: migrate_process - label: 'Iterator process' + label: 'Sub process' mapping: process: type: ignore diff --git a/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_node.yml b/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_node.yml index 321c576df..7494d1f40 100644 --- a/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_node.yml +++ b/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_node.yml @@ -2,6 +2,8 @@ id: beer_node label: Beers of the world migration_group: beer +migration_tags: + - example source: plugin: beer_node destination: diff --git a/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_term.yml b/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_term.yml index a8ab579b3..c091b63d7 100644 --- a/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_term.yml +++ b/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_term.yml @@ -12,6 +12,10 @@ label: Migrate style categories from the source database to taxonomy terms # configuration to be merged with our own configuration here). migration_group: beer +# The category or tag for the migration. +migration_tags: + - example + # Every migration must have a source plugin, which controls the delivery of our # source data. In this case, our source plugin has the name "beer_term", which # Drupal resolves to the PHP class defined in diff --git a/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_user.yml b/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_user.yml index 4bd5b7f23..d31b7253e 100644 --- a/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_user.yml +++ b/web/modules/contrib/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_user.yml @@ -4,6 +4,8 @@ id: beer_user label: Beer Drinkers of the world migration_group: beer +migration_tags: + - example source: plugin: beer_user destination: diff --git a/web/modules/contrib/migrate_plus/migrate_example/migrate_example.info.yml b/web/modules/contrib/migrate_plus/migrate_example/migrate_example.info.yml index bd446988f..5ff995788 100644 --- a/web/modules/contrib/migrate_plus/migrate_example/migrate_example.info.yml +++ b/web/modules/contrib/migrate_plus/migrate_example/migrate_example.info.yml @@ -10,8 +10,8 @@ dependencies: - drupal:menu_ui - drupal:path -# Information added by Drupal.org packaging script on 2018-02-23 -version: '8.x-4.0-beta3' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1519400598 +datestamp: 1536264189 diff --git a/web/modules/contrib/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.info.yml b/web/modules/contrib/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.info.yml index ab36c1e3d..b9dd2672c 100644 --- a/web/modules/contrib/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.info.yml +++ b/web/modules/contrib/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.info.yml @@ -11,8 +11,8 @@ dependencies: - drupal:options - drupal:taxonomy -# Information added by Drupal.org packaging script on 2018-02-23 -version: '8.x-4.0-beta3' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1519400598 +datestamp: 1536264189 diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.weather_soap.yml b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.weather_soap.yml index ae2ad03ab..328bcca84 100755 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.weather_soap.yml +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.weather_soap.yml @@ -2,13 +2,16 @@ id: weather_soap label: SOAP service providing weather. migration_group: wine +migration_tags: + - advanced example source: # We use the SOAP parser source plugin. plugin: url data_fetcher_plugin: http # Ignored - SoapClient does the fetching. data_parser_plugin: soap # URL of a WSDL endpoint. - urls: http://www.webservicex.net/globalweather.asmx?WSDL + urls: + - http://www.webservicex.net/globalweather.asmx?WSDL # The function to call on the service, and the parameters to pass. See # http://www.webservicex.net/New/Home/ServiceDetail/56 for the XML structure # of this feed - how CountryName is passed within the GetCitiesByCountry @@ -48,3 +51,6 @@ process: name: City destination: plugin: entity:taxonomy_term +migration_dependencies: + required: {} + optional: {} diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_json.yml b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_json.yml index e8991a699..d7d3953af 100755 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_json.yml +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_json.yml @@ -2,6 +2,8 @@ id: wine_role_json label: JSON feed of roles (positions) migration_group: wine +migration_tags: + - advanced example source: # We use the JSON source plugin. plugin: url @@ -17,7 +19,8 @@ source: # Normally, this is one or more fully-qualified URLs or file paths. Because # we can't hardcode your local URL, we provide a relative path here which # hook_install() will rewrite to a full URL for the current site. - urls: /migrate_example_advanced_position?_format=json + urls: + - /migrate_example_advanced_position?_format=json # An xpath-like selector corresponding to the items to be imported. item_selector: position # Under 'fields', we list the data items to be imported. The first level keys diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_xml.yml b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_xml.yml index 6b7234c24..b5032a4f1 100755 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_xml.yml +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_xml.yml @@ -2,6 +2,8 @@ id: wine_role_xml label: XML feed of roles (positions) migration_group: wine +migration_tags: + - advanced example source: # We use the XML data parser plugin. plugin: url @@ -10,7 +12,8 @@ source: # Normally, this is one or more fully-qualified URLs or file paths. Because # we can't hardcode your local URL, we provide a relative path here which # hook_install() will rewrite to a full URL for the current site. - urls: /migrate_example_advanced_position?_format=xml + urls: + - /migrate_example_advanced_position?_format=xml # Visit the URL above (relative to your site root) and look at it. You can see # that <response> is the outer element, and each item we want to import is a # <position> element. The item_xpath value is the xpath to use to query the diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_terms.yml b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_terms.yml index 9f7e8047d..56eb27386 100644 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_terms.yml +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_terms.yml @@ -1,6 +1,8 @@ id: wine_terms label: Migrate all categories into Drupal taxonomy terms migration_group: wine +migration_tags: + - advanced example source: plugin: wine_term destination: diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_list.yml.txt b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_list.yml.txt index f8bd712d5..ff88862b2 100755 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_list.yml.txt +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_list.yml.txt @@ -3,13 +3,16 @@ id: wine_variety_list label: XML feed of varieties migration_group: wine +migration_tags: + - advanced example source: # We use the XML source plugin. plugin: xml # Normally, this is one or more fully-qualified URLs or file paths. Because # we can't hardcode your local URL, we provide a relative path here which # hook_install() will rewrite to a full URL for the current site. - urls: /migrate_example_advanced_variety_list?_format=xml + urls: + - /migrate_example_advanced_variety_list?_format=xml item_url: /migrate_example_advanced_variety_list/:id?_format=xml id_selector: /response/items # Visit the URL above (relative to your site root) and look at it. You can see diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_multi_xml.yml b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_multi_xml.yml index 318b3c1b1..a501a7e75 100755 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_multi_xml.yml +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_multi_xml.yml @@ -2,6 +2,8 @@ id: wine_variety_multi_xml label: XML feed of varieties migration_group: wine +migration_tags: + - advanced example source: # We use the XML source plugin. plugin: url diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced.info.yml b/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced.info.yml index b0226fb3e..1d396e34b 100644 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced.info.yml +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced.info.yml @@ -8,8 +8,8 @@ dependencies: - migrate_plus:migrate_example_advanced_setup - migrate_plus:migrate_plus -# Information added by Drupal.org packaging script on 2018-02-23 -version: '8.x-4.0-beta3' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1519400598 +datestamp: 1536264189 diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced.install b/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced.install index fa931770c..2023bdee1 100644 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced.install +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced.install @@ -21,7 +21,11 @@ function migrate_example_advanced_install() { if ($wine_role_xml_migration) { $source = $wine_role_xml_migration->get('source'); $request = \Drupal::request(); - $source['urls'] = 'http://' . $request->getHttpHost() . $source['urls']; + $urls = []; + foreach ($source['urls'] as $url) { + $urls[] = 'http://' . $request->getHttpHost() . $url; + } + $source['urls'] = $urls; $wine_role_xml_migration->set('source', $source); $wine_role_xml_migration->save(); } @@ -30,7 +34,11 @@ function migrate_example_advanced_install() { if ($wine_role_json_migration) { $source = $wine_role_json_migration->get('source'); $request = \Drupal::request(); - $source['urls'] = 'http://' . $request->getHttpHost() . $source['urls']; + $urls = []; + foreach ($source['urls'] as $url) { + $urls[] = 'http://' . $request->getHttpHost() . $url; + } + $source['urls'] = $urls; $wine_role_json_migration->set('source', $source); $wine_role_json_migration->save(); } diff --git a/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.info.yml b/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.info.yml index 8913482e0..870586568 100644 --- a/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.info.yml +++ b/web/modules/contrib/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.info.yml @@ -11,8 +11,8 @@ dependencies: - drupal:taxonomy - drupal:rest -# Information added by Drupal.org packaging script on 2018-02-23 -version: '8.x-4.0-beta3' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1519400598 +datestamp: 1536264189 diff --git a/web/modules/contrib/migrate_plus/migrate_plus.info.yml b/web/modules/contrib/migrate_plus/migrate_plus.info.yml index e63da44a9..c5c0e29c6 100644 --- a/web/modules/contrib/migrate_plus/migrate_plus.info.yml +++ b/web/modules/contrib/migrate_plus/migrate_plus.info.yml @@ -6,8 +6,8 @@ package: Migration dependencies: - drupal:migrate (>=8.3) -# Information added by Drupal.org packaging script on 2018-02-23 -version: '8.x-4.0-beta3' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1519400598 +datestamp: 1536264189 diff --git a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php index 7361f7327..a749a6388 100644 --- a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php +++ b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php @@ -2,8 +2,13 @@ namespace Drupal\migrate_plus\Plugin\migrate\process; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigratePluginManager; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * This plugin generates entities within the process plugin. @@ -16,10 +21,10 @@ use Drupal\migrate\Row; * * All the configuration from the lookup plugin applies here. In its most * simple form, this plugin needs no configuration. If there are fields on the - * generated entity that are required or need some default value, that can be - * provided via a default_values configuration option. + * generated entity that are required or need some value, their values can be + * provided via values and/or default_values configuration options. * - * Example usage with default_values configuration: + * Example usage with values and default_values configuration: * @code * destination: * plugin: 'entity:node' @@ -27,20 +32,86 @@ use Drupal\migrate\Row; * type: * plugin: default_value * default_value: page + * foo: bar * field_tags: * plugin: entity_generate * source: tags * default_values: * description: Default description - * field_long_description: Default long description + * values: + * field_long_description: some_source_field + * field_foo: '@foo' * @endcode */ class EntityGenerate extends EntityLookup { + /** + * The row from the source to process. + * + * @var \Drupal\migrate\Row + */ + protected $row; + + /** + * The MigrateExecutable instance. + * + * @var \Drupal\migrate\MigrateExecutable + */ + protected $migrateExecutable; + + /** + * The get process plugin instance. + * + * @var \Drupal\migrate\Plugin\migrate\process\Get + */ + protected $getProcessPlugin; + + /** + * EntityGenerate constructor. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $pluginId + * The plugin_id for the plugin instance. + * @param mixed $pluginDefinition + * The plugin implementation definition. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration. + * @param \Drupal\Core\Entity\EntityManagerInterface $entityManager + * The $entityManager instance. + * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selectionPluginManager + * The $selectionPluginManager instance. + * @param \Drupal\migrate\Plugin\MigratePluginManager $migratePluginManager + * The MigratePluginManager instance. + */ + public function __construct(array $configuration, $pluginId, $pluginDefinition, MigrationInterface $migration, EntityManagerInterface $entityManager, SelectionPluginManagerInterface $selectionPluginManager, MigratePluginManager $migratePluginManager) { + parent::__construct($configuration, $pluginId, $pluginDefinition, $migration, $entityManager, $selectionPluginManager); + if (isset($configuration['values'])) { + $this->getProcessPlugin = $migratePluginManager->createInstance('get', ['source' => $configuration['values']]); + } + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $pluginId, + $pluginDefinition, + $migration, + $container->get('entity.manager'), + $container->get('plugin.manager.entity_reference_selection'), + $container->get('plugin.manager.migrate.process') + ); + } + /** * {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrateExecutable, Row $row, $destinationProperty) { + $this->row = $row; + $this->migrateExecutable = $migrateExecutable; // Creates an entity if the lookup determines it doesn't exist. if (!($result = parent::transform($value, $migrateExecutable, $row, $destinationProperty))) { $result = $this->generateEntity($value); @@ -94,6 +165,13 @@ class EntityGenerate extends EntityLookup { $entity_values[$key] = $value; } } + // Gather any additional properties/fields. + if (isset($this->configuration['values']) && is_array($this->configuration['values'])) { + foreach ($this->configuration['values'] as $key => $property) { + $source_value = $this->getProcessPlugin->transform(NULL, $this->migrateExecutable, $this->row, $property); + $entity_values[$key] = $source_value; + } + } return $entity_values; } diff --git a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/EntityLookup.php b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/EntityLookup.php index d02bb9124..2ae0d07c1 100644 --- a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/EntityLookup.php +++ b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/EntityLookup.php @@ -2,6 +2,7 @@ namespace Drupal\migrate_plus\Plugin\migrate\process; +use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -152,6 +153,11 @@ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginIn * {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrateExecutable, Row $row, $destinationProperty) { + // If the source data is an empty array, return the same. + if (gettype($value) === 'array' && count($value) === 0) { + return []; + } + // In case of subfields ('field_reference/target_id'), extract the field // name only. $parts = explode('/', $destinationProperty); @@ -270,7 +276,8 @@ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginIn if (!$ignoreCase) { // Returns the entity's identifier. foreach ($results as $k => $identifier) { - $result_value = $this->entityManager->getStorage($this->lookupEntityType)->load($identifier)->{$this->lookupValueKey}->value; + $entity = $this->entityManager->getStorage($this->lookupEntityType)->load($identifier); + $result_value = $entity instanceof ConfigEntityInterface ? $entity->get($this->lookupValueKey) : $entity->get($this->lookupValueKey)->value; if (($multiple && !in_array($result_value, $value, TRUE)) || (!$multiple && $result_value !== $value)) { unset($results[$k]); } diff --git a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/Merge.php b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/Merge.php index 1e0467917..463595e9d 100644 --- a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/Merge.php +++ b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/Merge.php @@ -41,8 +41,8 @@ use Drupal\migrate\Row; * paragraphs_field: * plugin: merge * source: - * - @temp_body - * - @temp_images + * - '@temp_body' + * - '@temp_images' * destination: * plugin: 'entity:node' */ @@ -53,12 +53,12 @@ class Merge extends ProcessPluginBase { */ public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { if (!is_array($value)) { - throw new MigrateException('Input should be an array.'); + throw new MigrateException(sprintf('Merge process failed for destination property (%s): input is not an array.', $destination_property)); } $new_value = []; - foreach ($value as $item) { + foreach($value as $i => $item) { if (!is_array($item)) { - throw new MigrateException('One of the items is not an array that can be merged.'); + throw new MigrateException(sprintf('Merge process failed for destination property (%s): index (%s) in the source value is not an array that can be merged.', $destination_property, $i)); } $new_value = array_merge($new_value, $item); } diff --git a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/MultipleValues.php b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/MultipleValues.php new file mode 100644 index 000000000..be6779a12 --- /dev/null +++ b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/MultipleValues.php @@ -0,0 +1,58 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Treat an array of values as a separate / individual values. + * + * @code + * process: + * field_authors: + * - + * plugin: explode + * delimiter: ', ' + * source: authors + * - + * plugin: single_value + * - + * plugin: callback + * callable: custom_sort_authors + * - + * plugin: multiple_values + * @endcode + * + * Assume the "authors" field contains comma separated author names. + * + * We split the names into multiple values and then use the "single_value" + * plugin to treat them as a single array of author names. After that, we + * pass the values through a custom sort. Callback multiple setting is false. To + * convert from a single value to multiple, use the "multiple_values" plugin. It + * will make the next plugin treat the values individually instead of an array + * of values. + * + * @MigrateProcessPlugin( + * id = "multiple_values", + * handle_multiples = TRUE + * ) + */ +class MultipleValues extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + return $value; + } + + /** + * {@inheritdoc} + */ + public function multiple() { + return TRUE; + } + +} diff --git a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/SingleValue.php b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/SingleValue.php new file mode 100644 index 000000000..2583a9026 --- /dev/null +++ b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/SingleValue.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Treat an array of values as a single value. + * + * @code + * process: + * field_authors: + * - + * plugin: explode + * delimiter: ', ' + * source: authors + * - + * plugin: single_value + * @endcode + * + * Assume the "authors" field contains comma separated author names. + * + * After the explode, we end up with each author name as an individual value. + * But if we want to perform a sort on all values using a callback, we will + * need to send all the values to a callable together as an array of author + * names. Calling the "single_value" plugin in such a case will combine all the + * values into a single array for the next plugin. + * + * @MigrateProcessPlugin( + * id = "single_value", + * handle_multiples = TRUE + * ) + */ +class SingleValue extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + return $value; + } + +} diff --git a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php index f25b1a47f..1a0c8f545 100644 --- a/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php +++ b/web/modules/contrib/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php @@ -15,6 +15,44 @@ use Drupal\migrate\Row; * @MigrateProcessPlugin( * id = "skip_on_value" * ) + * + * Available configuration keys: + * - value: An single value or array of values against which the source value + * should be compared. + * - not_equals: (optional) If set, skipping occurs when values are not equal. + * - method: What to do if the input value is empty. Possible values: + * - row: Skips the entire row when an empty value is encountered. + * - process: Prevents further processing of the input property when the value + * is empty. + * + * Examples: + * + * Example usage with minimal configuration: + * @code + * type: + * plugin: skip_on_value + * source: content_type + * method: row + * value: blog + * @endcode + * + * The above example will skip processing the input property if the content_type + * source field equals "blog". + * + * Example usage with full configuration: + * @code + * type: + * plugin: skip_on_value + * not_equals: true + * source: content_type + * method: row + * value: + * - article + * - testimonial + * @endcode + * + * The above example will skip processing any row for which the source row's + * content type field is not "article" or "testimonial". */ class SkipOnValue extends ProcessPluginBase { @@ -27,10 +65,15 @@ class SkipOnValue extends ProcessPluginBase { } if (is_array($this->configuration['value'])) { + $value_in_array = FALSE; + $not_equals = isset($this->configuration['not_equals']); + foreach ($this->configuration['value'] as $skipValue) { - if ($this->compareValue($value, $skipValue, !isset($this->configuration['not_equals']))) { - throw new MigrateSkipRowException(); - } + $value_in_array |= $this->compareValue($value, $skipValue); + } + + if (($not_equals && !$value_in_array) || (!$not_equals && $value_in_array)) { + throw new MigrateSkipRowException(); } } elseif ($this->compareValue($value, $this->configuration['value'], !isset($this->configuration['not_equals']))) { @@ -49,10 +92,15 @@ class SkipOnValue extends ProcessPluginBase { } if (is_array($this->configuration['value'])) { + $value_in_array = FALSE; + $not_equals = isset($this->configuration['not_equals']); + foreach ($this->configuration['value'] as $skipValue) { - if ($this->compareValue($value, $skipValue, !isset($this->configuration['not_equals']))) { - throw new MigrateSkipProcessException(); - } + $value_in_array |= $this->compareValue($value, $skipValue); + } + + if (($not_equals && !$value_in_array) || (!$not_equals && $value_in_array)) { + throw new MigrateSkipProcessException(); } } elseif ($this->compareValue($value, $this->configuration['value'], !isset($this->configuration['not_equals']))) { diff --git a/web/modules/contrib/migrate_plus/src/Plugin/migrate_plus/data_parser/Json.php b/web/modules/contrib/migrate_plus/src/Plugin/migrate_plus/data_parser/Json.php index df9b3fca8..c1220bc5f 100755 --- a/web/modules/contrib/migrate_plus/src/Plugin/migrate_plus/data_parser/Json.php +++ b/web/modules/contrib/migrate_plus/src/Plugin/migrate_plus/data_parser/Json.php @@ -15,13 +15,6 @@ use Drupal\migrate_plus\DataParserPluginBase; */ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterface { - /** - * The request headers passed to the data fetcher. - * - * @var array - */ - protected $headers = []; - /** * Iterator over the JSON data. * @@ -119,7 +112,12 @@ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterfa $field_data = $current; $field_selectors = explode('/', trim($selector, '/')); foreach ($field_selectors as $field_selector) { - $field_data = $field_data[$field_selector]; + if (is_array($field_data) && array_key_exists($field_selector, $field_data)) { + $field_data = $field_data[$field_selector]; + } + else { + $field_data = ''; + } } $this->currentItem[$field_name] = $field_data; } diff --git a/web/modules/contrib/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php b/web/modules/contrib/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php index a33b6a086..8a58d8b07 100644 --- a/web/modules/contrib/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php +++ b/web/modules/contrib/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php @@ -77,7 +77,7 @@ class SimpleXml extends DataParserPluginBase { } // Reduce single-value results to scalars. foreach ($this->currentItem as $field_name => $values) { - if (count($values) == 1) { + if (is_array($values) && count($values) == 1) { $this->currentItem[$field_name] = reset($values); } } diff --git a/web/modules/contrib/migrate_plus/tests/data/missing_properties.json b/web/modules/contrib/migrate_plus/tests/data/missing_properties.json new file mode 100644 index 000000000..59b7d6a6e --- /dev/null +++ b/web/modules/contrib/migrate_plus/tests/data/missing_properties.json @@ -0,0 +1,21 @@ +[ + { + "id": "1", + "title": "Title", + "video": { + "title": "Video title", + "url": "https://localhost/" + } + }, + { + "id": "2", + "video": { + "title": "Video title", + "url": "https://localhost/" + } + }, + { + "id": "3", + "title": "Title 3" + } +] diff --git a/web/modules/contrib/migrate_plus/tests/data/simple_xml_reduce_single_value.xml b/web/modules/contrib/migrate_plus/tests/data/simple_xml_reduce_single_value.xml new file mode 100644 index 000000000..0cffd2890 --- /dev/null +++ b/web/modules/contrib/migrate_plus/tests/data/simple_xml_reduce_single_value.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<items> + <item id="1"> + <values title="Values"> + <value>Value 1</value> + <value>Value 2</value> + </values> + </item> + <item id="2"> + <values title="Values"> + <value>Value 1 (single)</value> + </values> + </item> +</items> diff --git a/web/modules/contrib/migrate_plus/tests/src/Functional/LoadTest.php b/web/modules/contrib/migrate_plus/tests/src/Functional/LoadTest.php new file mode 100644 index 000000000..c9aceda72 --- /dev/null +++ b/web/modules/contrib/migrate_plus/tests/src/Functional/LoadTest.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Functional; + +use Drupal\Core\Url; +use Drupal\Tests\BrowserTestBase; + +/** + * Simple test to ensure that main page loads with module enabled. + * + * @group migrate_plus + */ +class LoadTest extends BrowserTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'migrate_plus', + 'migrate_example', + 'migrate_example_advanced', + ]; + + /** + * A user with permission to administer site configuration. + * + * @var \Drupal\user\UserInterface + */ + protected $user; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->user = $this->drupalCreateUser(['administer site configuration']); + $this->drupalLogin($this->user); + } + + /** + * Tests that the home page loads with a 200 response. + */ + public function testLoad() { + $this->drupalGet(Url::fromRoute('<front>')); + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php b/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php index a4c0f27f6..3d46496de 100644 --- a/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php +++ b/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate\process; +use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; @@ -119,12 +120,16 @@ class EntityGenerateTest extends KernelTestBase implements MigrateMessageInterfa public function testTransform(array $definition, array $expected, array $preSeed = []) { // Pre seed some test data. foreach ($preSeed as $storageName => $values) { - /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ - $storage = $this->container - ->get('entity_type.manager') - ->getStorage($storageName); - $entity = $storage->create($values); - $entity->save(); + // If the first element of $values is a non-empty array, create multiple + // entities. Otherwise, create just one entity. + if (isset($values[0])) { + foreach ($values as $itemValues) { + $this->createTestData($storageName, $itemValues); + } + } + else { + $this->createTestData($storageName, $values); + } } /** @var \Drupal\migrate\Plugin\Migration $migration */ @@ -139,15 +144,54 @@ class EntityGenerateTest extends KernelTestBase implements MigrateMessageInterfa $properties = array_diff_key($row, array_flip(['id'])); foreach ($properties as $property => $value) { if (is_array($value)) { - foreach ($value as $key => $expectedValue) { - if (empty($expectedValue)) { - $this->assertEmpty($entity->{$property}->getValue(), "Expected value is empty but field $property is not empty."); - } - elseif ($entity->{$property}->getValue()) { - $this->assertEquals($expectedValue, $entity->{$property}[0]->entity->$key->value); + if (empty($value)) { + $this->assertEmpty($entity->{$property}->getValue(), "Expected value is 'unset' but field $property is set."); + } + else { + // Check if we're testing multiple values in one field. If so, loop + // through them one-by-one and check that they're present in the + // $entity. + if (isset($value[0])) { + foreach ($value as $valueID => $valueToCheck) { + foreach ($valueToCheck as $key => $expectedValue) { + if (empty($expectedValue)) { + if (!$entity->{$property}->isEmpty()) { + $this->assertTrue($entity->{$property}[0]->entity->$key->isEmpty(), "Expected value is empty but field $property.$key is not empty."); + } + else { + $this->assertTrue($entity->{$property}->isEmpty(), "FOOBAR Expected value is empty but field $property is not empty."); + } + } + elseif ($entity->{$property}->getValue()) { + $this->assertEquals($expectedValue, $entity->{$property}[$valueID]->entity->$key->value); + } + else { + $this->fail("Expected value: $expectedValue does not exist in $property."); + } + } + } } + // If we get to this point, we're only checking a + // single field value. else { - $this->fail("Expected value: $expectedValue does not exist in $property."); + foreach ($value as $key => $expectedValue) { + if (empty($expectedValue)) { + if (!$entity->{$property}->isEmpty()) { + $this->assertTrue($entity->{$property}[0]->entity->$key->isEmpty(), "Expected value is empty but field $property.$key is not empty."); + } + else { + $this->assertTrue($entity->{$property}->isEmpty(), "BINBAZ Expected value is empty but field $property is not empty."); + } + } + elseif ($entity->{$property}->getValue()) { + $referenced_entity = $entity->{$property}[0]->entity; + $result_value = $referenced_entity instanceof ConfigEntityInterface ? $referenced_entity->get($key) : $referenced_entity->get($key)->value; + $this->assertEquals($expectedValue, $result_value); + } + else { + $this->fail("Expected value: $expectedValue does not exist in $property."); + } + } } } } @@ -314,6 +358,406 @@ class EntityGenerateTest extends KernelTestBase implements MigrateMessageInterfa ], ], ], + 'provide values' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => 'Apples', + ], + [ + 'id' => 2, + 'title' => 'content item 2', + 'term' => 'Bananas', + ], + [ + 'id' => 3, + 'title' => 'content item 3', + 'term' => 'Grapes', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + 'term_upper' => [ + 'plugin' => 'callback', + 'source' => 'term', + 'callable' => 'strtoupper', + ], + $this->fieldName => [ + 'plugin' => 'entity_generate', + 'source' => 'term', + 'values' => [ + 'description' => '@term_upper', + ], + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + 'tid' => 2, + 'name' => 'Apples', + 'description' => 'APPLES', + ], + ], + 'row 2' => [ + 'id' => 2, + 'title' => 'content item 2', + $this->fieldName => [ + 'tid' => 3, + 'name' => 'Bananas', + 'description' => 'BANANAS', + ], + ], + 'row 3' => [ + 'id' => 3, + 'title' => 'content item 3', + $this->fieldName => [ + 'tid' => 1, + 'name' => 'Grapes', + 'description' => NULL, + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'lookup single existing term returns correct term' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => 'Grapes', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + 'tid' => 1, + 'name' => 'Grapes', + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'lookup single missing term returns null value' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => 'Apple', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'lookup multiple existing terms returns correct terms' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => [ + 'Grapes', + 'Apples', + ], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'title' => 'title', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + [ + 'tid' => 1, + 'name' => 'Grapes', + ], + [ + 'tid' => 2, + 'name' => 'Apples', + ], + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + [ + 'name' => 'Apples', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + ], + 'lookup multiple mixed terms returns correct terms' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => [ + 'Grapes', + 'Pears', + ], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'title' => 'title', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => '1', + 'title' => 'content item 1', + $this->fieldName => [ + [ + 'tid' => 1, + 'name' => 'Grapes', + ], + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + [ + 'name' => 'Apples', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + ], + 'lookup with empty term value returns no terms' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => [], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'title' => 'title', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'lookup config entity' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'name' => 'user 1', + 'mail' => 'user1@user1.com', + 'roles' => ['role_1'], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'name' => 'name', + 'roles' => [ + 'plugin' => 'entity_lookup', + 'entity_type' => 'user_role', + 'value_key' => 'id', + 'source' => 'roles', + ], + ], + 'destination' => [ + 'plugin' => 'entity:user', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'name' => 'user 1', + 'roles' => [ + 'id' => 'role_1', + 'label' => 'Role 1', + ], + ], + ], + 'pre seed' => [ + 'user_role' => [ + 'id' => 'role_1', + 'label' => 'Role 1', + ], + ], + ], ]; } @@ -324,4 +768,21 @@ class EntityGenerateTest extends KernelTestBase implements MigrateMessageInterfa $this->assertTrue($type == 'status', $message); } + /** + * Create pre-seed test data. + * + * @param string $storageName + * The storage manager to create. + * @param array $values + * The values to use when creating the entity. + */ + private function createTestData($storageName, array $values) { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->container + ->get('entity_type.manager') + ->getStorage($storageName); + $entity = $storage->create($values); + $entity->save(); + } + } diff --git a/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php b/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php new file mode 100644 index 000000000..2fb4f2458 --- /dev/null +++ b/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php @@ -0,0 +1,112 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate_plus\data_parser; + +use Drupal\KernelTests\KernelTestBase; + +/** + * Test of the data_parser Json migrate_plus plugin. + * + * @group migrate_plus + */ +class JsonTest extends KernelTestBase { + + public static $modules = ['migrate', 'migrate_plus']; + + /** + * Tests missing properties in json file. + * + * @param string $file + * File name in tests/data/ directory of this module. + * @param array $ids + * Array of ids to pass to the plugin. + * @param array $fields + * Array of fields to pass to the plugin. + * @param array $expected + * Expected array from json decoded file. + * + * @dataProvider jsonBaseDataProvider + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * @throws \Exception + */ + public function testMissingProperties($file, array $ids, array $fields, array $expected) { + $path = $this->container + ->get('module_handler') + ->getModule('migrate_plus') + ->getPath(); + $url = $path . '/tests/data/' . $file; + + /** @var \Drupal\migrate_plus\DataParserPluginManager $plugin_manager */ + $plugin_manager = $this->container + ->get('plugin.manager.migrate_plus.data_parser'); + $conf = [ + 'plugin' => 'url', + 'data_fetcher_plugin' => 'file', + 'data_parser_plugin' => 'json', + 'destination' => 'node', + 'urls' => [$url], + 'ids' => $ids, + 'fields' => $fields, + 'item_selector' => NULL, + ]; + $json_parser = $plugin_manager->createInstance('json', $conf); + + $data = []; + foreach ($json_parser as $item) { + $data[] = $item; + } + + $this->assertEquals($expected, $data); + } + + /** + * Provides multiple test cases for the testMissingProperty method. + * + * @return array + * The test cases. + */ + public function jsonBaseDataProvider() { + return [ + 'missing properties' => [ + 'file' => 'missing_properties.json', + 'ids' => ['id' => ['type' => 'integer']], + 'fields' => [ + [ + 'name' => 'id', + 'label' => 'Id', + 'selector' => '/id', + ], + [ + 'name' => 'title', + 'label' => 'Title', + 'selector' => '/title', + ], + [ + 'name' => 'video_url', + 'label' => 'Video url', + 'selector' => '/video/url', + ], + ], + 'expected' => [ + [ + 'id' => '1', + 'title' => 'Title', + 'video_url' => 'https://localhost/', + ], + [ + 'id' => '2', + 'title' => '', + 'video_url' => 'https://localhost/', + ], + [ + 'id' => '3', + 'title' => 'Title 3', + 'video_url' => '', + ], + ], + ], + ]; + } + +} diff --git a/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php b/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php new file mode 100644 index 000000000..fe6b345e1 --- /dev/null +++ b/web/modules/contrib/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php @@ -0,0 +1,77 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate_plus\data_parser; + +use Drupal\KernelTests\KernelTestBase; + +/** + * Test of the data_parser SimpleXml migrate_plus plugin. + * + * @group migrate_plus + */ +class SimpleXmlTest extends KernelTestBase { + + public static $modules = ['migrate', 'migrate_plus']; + + /** + * Tests reducing single values. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * @throws \Exception + */ + public function testReduceSingleValue() { + $path = $this->container + ->get('module_handler') + ->getModule('migrate_plus') + ->getPath(); + $url = $path . '/tests/data/simple_xml_reduce_single_value.xml'; + + /** @var \Drupal\migrate_plus\DataParserPluginManager $plugin_manager */ + $plugin_manager = $this->container + ->get('plugin.manager.migrate_plus.data_parser'); + $conf = [ + 'plugin' => 'url', + 'data_fetcher_plugin' => 'file', + 'data_parser_plugin' => 'simple_xml', + 'destination' => 'node', + 'urls' => [$url], + 'ids' => ['id' => ['type' => 'integer']], + 'fields' => [ + [ + 'name' => 'id', + 'label' => 'Id', + 'selector' => '@id', + ], + [ + 'name' => 'values', + 'label' => 'Values', + 'selector' => 'values', + ], + ], + 'item_selector' => '/items/item', + ]; + $parser = $plugin_manager->createInstance('simple_xml', $conf); + + $data = []; + foreach ($parser as $item) { + $values = []; + foreach ($item['values'] as $value) { + $values[] = (string) $value; + } + $data[] = $values; + } + + $expected = [ + [ + 'Value 1', + 'Value 2', + ], + [ + 'Value 1 (single)', + ], + ]; + + $this->assertEquals($expected, $data); + } + +} diff --git a/web/modules/contrib/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php b/web/modules/contrib/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php new file mode 100644 index 000000000..60a520c0d --- /dev/null +++ b/web/modules/contrib/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; +use Drupal\migrate_plus\Plugin\migrate\process\MultipleValues; + +/** + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\MultipleValues + * @group migrate + */ +class MultipleValuesTest extends MigrateProcessTestCase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->plugin = new MultipleValues([], 'multiple_values', []); + parent::setUp(); + } + + /** + * Test input treated as multiple value output. + */ + public function testTreatAsMultiple() { + $value = ['v1', 'v2', 'v3']; + $output = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($output, $value); + $this->assertTrue($this->plugin->multiple()); + } + +} diff --git a/web/modules/contrib/migrate_plus/tests/src/Unit/process/SingleValueTest.php b/web/modules/contrib/migrate_plus/tests/src/Unit/process/SingleValueTest.php new file mode 100644 index 000000000..203b1c5cb --- /dev/null +++ b/web/modules/contrib/migrate_plus/tests/src/Unit/process/SingleValueTest.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; +use Drupal\migrate_plus\Plugin\migrate\process\SingleValue; + +/** + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\SingleValue + * @group migrate + */ +class SingleValueTest extends MigrateProcessTestCase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->plugin = new SingleValue([], 'single_value', []); + parent::setUp(); + } + + /** + * Test input treated as single value output. + */ + public function testTreatAsSingle() { + $value = ['v1', 'v2', 'v3']; + $output = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($output, $value); + $this->assertFalse($this->plugin->multiple()); + } + +} diff --git a/web/modules/contrib/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php b/web/modules/contrib/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php index c1bdfac23..968e6f0fb 100644 --- a/web/modules/contrib/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php +++ b/web/modules/contrib/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php @@ -65,6 +65,36 @@ class SkipOnValueTest extends MigrateProcessTestCase { $this->assertEquals($value, '4'); } + /** + * @covers ::process + */ + public function testProcessBypassesOnMultipleNonValue() { + $configuration['method'] = 'process'; + $configuration['value'] = [1, 1, 2, 3, 5, 8]; + $configuration['not_equals'] = TRUE; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(5, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '5'); + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(1, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '1'); + } + + /** + * @covers ::row + */ + public function testRowBypassesOnMultipleNonValue() { + $configuration['method'] = 'row'; + $configuration['value'] = [1, 1, 2, 3, 5, 8]; + $configuration['not_equals'] = TRUE; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(5, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '5'); + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(1, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '1'); + } + /** * @covers ::row */ diff --git a/web/modules/contrib/migrate_tools/composer.json b/web/modules/contrib/migrate_tools/composer.json new file mode 100644 index 000000000..d5c8fca8f --- /dev/null +++ b/web/modules/contrib/migrate_tools/composer.json @@ -0,0 +1,27 @@ +{ + "name": "drupal/migrate_tools", + "description": "Tools to assist in developing and running migrations.", + "type": "drupal-module", + "homepage": "http://drupal.org/project/migrate_tools", + "support": { + "issues": "http://drupal.org/project/migrate_tools", + "irc": "irc://irc.freenode.org/drupal-migrate", + "source": "http://cgit.drupalcode.org/migrate_tools" + }, + "license": "GPL-2.0+", + "require": { + "drupal/migrate_plus": "^4" + }, + "require-dev": { + "drupal/coder": "^8", + "drupal/migrate_source_csv": "^2.2" + }, + "minimum-stability": "dev", + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + } +} diff --git a/web/modules/contrib/migrate_tools/drush.services.yml b/web/modules/contrib/migrate_tools/drush.services.yml new file mode 100644 index 000000000..ed0a3917b --- /dev/null +++ b/web/modules/contrib/migrate_tools/drush.services.yml @@ -0,0 +1,6 @@ +services: + migrate_tools.commands: + class: \Drupal\migrate_tools\Commands\MigrateToolsCommands + arguments: ['@plugin.manager.migration', '@date.formatter', '@entity_type.manager', '@keyvalue'] + tags: + - { name: drush.command } diff --git a/web/modules/contrib/migrate_tools/migrate_tools.drush.inc b/web/modules/contrib/migrate_tools/migrate_tools.drush.inc index 30ddf5934..73ddec530 100644 --- a/web/modules/contrib/migrate_tools/migrate_tools.drush.inc +++ b/web/modules/contrib/migrate_tools/migrate_tools.drush.inc @@ -6,11 +6,12 @@ */ use Drupal\Component\Utility\Unicode; +use Drupal\migrate\Exception\RequirementsException; use Drupal\migrate\Plugin\MigrationInterface; -use Drupal\migrate_tools\MigrateExecutable; -use Drupal\migrate_tools\DrushLogMigrateMessage; -use Drupal\Core\Datetime\DateFormatter; +use Drupal\migrate\Plugin\RequirementsInterface; use Drupal\migrate_plus\Entity\MigrationGroup; +use Drupal\migrate_tools\DrushLogMigrateMessage; +use Drupal\migrate_tools\MigrateExecutable; /** * Implements hook_drush_command(). @@ -63,30 +64,30 @@ function migrate_tools_drush_command() { 'migrate-import beer_user --idlist=5' => 'Import the user record with source ID 5', ], 'drupal dependencies' => ['migrate_tools'], - 'aliases' => ['mi'], + 'aliases' => ['mi', 'mim'], ]; - $items['migrate-rollback'] = array( + $items['migrate-rollback'] = [ 'description' => 'Rollback one or more migrations.', - 'options' => array( + 'options' => [ 'all' => 'Process all migrations.', 'group' => 'A comma-separated list of migration groups to rollback', 'tag' => 'ID of the migration tag to rollback', 'feedback' => 'Frequency of progress messages, in items processed', - ), - 'arguments' => array( + ], + 'arguments' => [ 'migration' => 'Name of migration(s) to rollback. Delimit multiple using commas.', - ), - 'examples' => array( + ], + 'examples' => [ 'migrate-rollback --all' => 'Perform all migrations', 'migrate-rollback --group=beer' => 'Rollback all migrations in the beer group', 'migrate-rollback --tag=user' => 'Rollback all migrations with the user tag', 'migrate-rollback --group=beer --tag=user' => 'Rollback all migrations in the beer group and with the user tag', 'migrate-rollback beer_term,beer_node' => 'Rollback imported terms and nodes', - ), - 'drupal dependencies' => array('migrate_tools'), - 'aliases' => array('mr'), - ); + ], + 'drupal dependencies' => ['migrate_tools'], + 'aliases' => ['mr'], + ]; $items['migrate-stop'] = [ 'description' => 'Stop an active migration operation.', @@ -112,7 +113,7 @@ function migrate_tools_drush_command() { 'migration' => 'ID of the migration', ], 'options' => [ - 'csv' => 'Export messages as a CSV' + 'csv' => 'Export messages as a CSV', ], 'examples' => [ 'migrate-messages MyNode' => 'Show all messages for the MyNode migration', @@ -137,7 +138,10 @@ function migrate_tools_drush_command() { } /** + * Display migration status. + * * @param string $migration_names + * The migration names. */ function drush_migrate_tools_migrate_status($migration_names = '') { $names_only = drush_get_option('names-only'); @@ -151,12 +155,12 @@ function drush_migrate_tools_migrate_status($migration_names = '') { $group_name = !empty($group) ? "{$group->label()} ({$group->id()})" : $group_id; if ($names_only) { $table[] = [ - dt('Group: @name', array('@name' => $group_name)) + dt('Group: @name', ['@name' => $group_name]), ]; } else { $table[] = [ - dt('Group: @name', array('@name' => $group_name)), + dt('Group: @name', ['@name' => $group_name]), dt('Status'), dt('Total'), dt('Imported'), @@ -175,34 +179,34 @@ function drush_migrate_tools_migrate_status($migration_names = '') { ['@migration' => $migration_id, '@message' => $e->getMessage()])); continue; } - try { - $source_rows = $source_plugin->count(); - // -1 indicates uncountable sources. - if ($source_rows == -1) { - $source_rows = dt('N/A'); - $unprocessed = dt('N/A'); - } - else { - $unprocessed = $source_rows - $map->processedCount(); - } - } - catch (Exception $e) { - drush_print($e->getMessage()); - drush_log(dt('Could not retrieve source count from @migration: @message', - ['@migration' => $migration_id, '@message' => $e->getMessage()])); - $source_rows = dt('N/A'); - $unprocessed = dt('N/A'); - } - if ($names_only) { $table[] = [$migration_id]; } else { + try { + $source_rows = $source_plugin->count(); + // -1 indicates uncountable sources. + if ($source_rows == -1) { + $source_rows = dt('N/A'); + $unprocessed = dt('N/A'); + } + else { + $unprocessed = $source_rows - $map->processedCount(); + } + } + catch (Exception $e) { + drush_print($e->getMessage()); + drush_log(dt('Could not retrieve source count from @migration: @message', + ['@migration' => $migration_id, '@message' => $e->getMessage()])); + $source_rows = dt('N/A'); + $unprocessed = dt('N/A'); + } + $status = $migration->getStatusLabel(); $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); $last_imported = $migrate_last_imported_store->get($migration->id(), FALSE); if ($last_imported) { - /** @var DateFormatter $date_formatter */ + /** @var \Drupal\Core\Datetime\DateFormatter $date_formatter */ $date_formatter = \Drupal::service('date.formatter'); $last_imported = $date_formatter->format($last_imported / 1000, 'custom', 'Y-m-d H:i:s'); @@ -210,7 +214,14 @@ function drush_migrate_tools_migrate_status($migration_names = '') { else { $last_imported = ''; } - $table[] = [$migration_id, $status, $source_rows, $imported, $unprocessed, $last_imported]; + $table[] = [ + $migration_id, + $status, + $source_rows, + $imported, + $unprocessed, + $last_imported, + ]; } } } @@ -218,12 +229,22 @@ function drush_migrate_tools_migrate_status($migration_names = '') { } /** + * Import a migration. + * * @param string $migration_names + * The migration names. */ function drush_migrate_tools_migrate_import($migration_names = '') { $group_names = drush_get_option('group'); $tag_names = drush_get_option('tag'); $all = drush_get_option('all'); + + // Display a depreciation message if "mi" alias is used. + $args = drush_get_arguments(); + if ($args[0] === 'mi') { + drush_log('The \'mi\' alias is deprecated and will no longer work with Drush 9. Consider the use of \'mim\' alias instead.', 'warning'); + } + $options = []; if (!$all && !$group_names && !$migration_names && !$tag_names) { drush_set_error('MIGRATE_ERROR', dt('You must specify --all, --group, --tag or one or more migration names separated by commas')); @@ -248,22 +269,24 @@ function drush_migrate_tools_migrate_import($migration_names = '') { } /** - * Executes a single migration. If the --execute-dependencies option was given, - * the migration's dependencies will also be executed first. + * Executes a single migration. + * + * If the --execute-dependencies option was given, the migration's dependencies + * will also be executed first. * * @param \Drupal\migrate\Plugin\MigrationInterface $migration - * The migration to execute. + * The migration to execute. * @param string $migration_id - * The migration ID (not used, just an artifact of array_walk()). + * The migration ID (not used, just an artifact of array_walk()). * @param array $options - * Additional options for the migration. + * Additional options for the migration. */ -function _drush_migrate_tools_execute_migration(MigrationInterface $migration, $migration_id, array $options = []) { +function _drush_migrate_tools_execute_migration(MigrationInterface $migration, $migration_id, array $options = []) { $log = new DrushLogMigrateMessage(); if (drush_get_option('execute-dependencies')) { if ($required_IDS = $migration->get('requirements')) { - $manager = \Drupal::service('plugin.manager.config_entity_migration'); + $manager = \Drupal::service('plugin.manager.migration'); $required_migrations = $manager->createInstances($required_IDS); $dependency_options = array_merge($options, ['is_dependency' => TRUE]); array_walk($required_migrations, __FUNCTION__, $dependency_options); @@ -276,12 +299,22 @@ function _drush_migrate_tools_execute_migration(MigrationInterface $migration, $ $migration->getIdMap()->prepareUpdate(); } $executable = new MigrateExecutable($migration, $log, $options); - // drush_op() provides --simulate support - drush_op(array($executable, 'import')); + // Function drush_op() provides --simulate support. + drush_op([$executable, 'import']); + if ($count = $executable->getFailedCount()) { + // Nudge Drush to use a non-zero exit code. + drush_set_error('MIGRATE_ERROR', dt('!name Migration - !count failed.', [ + '!name' => $migration_id, + '!count' => $count, + ])); + } } /** + * Rollback migrations. + * * @param string $migration_names + * The migration names. */ function drush_migrate_tools_migrate_rollback($migration_names = '') { $group_names = drush_get_option('group'); @@ -311,17 +344,21 @@ function drush_migrate_tools_migrate_rollback($migration_names = '') { foreach ($migration_list as $migration_id => $migration) { $executable = new MigrateExecutable($migration, $log, $options); // drush_op() provides --simulate support. - drush_op(array($executable, 'rollback')); + drush_op([$executable, 'rollback']); } } } /** + * Stop a migration. + * * @param string $migration_id + * The migration id. */ function drush_migrate_tools_migrate_stop($migration_id = '') { - /** @var MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = \Drupal::service('plugin.manager.migration') + ->createInstance($migration_id); if ($migration) { $status = $migration->getStatus(); switch ($status) { @@ -346,11 +383,15 @@ function drush_migrate_tools_migrate_stop($migration_id = '') { } /** + * Reset status. + * * @param string $migration_id + * The migration id. */ function drush_migrate_tools_migrate_reset_status($migration_id = '') { - /** @var MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = \Drupal::service('plugin.manager.migration') + ->createInstance($migration_id); if ($migration) { $status = $migration->getStatus(); if ($status == MigrationInterface::STATUS_IDLE) { @@ -367,11 +408,15 @@ function drush_migrate_tools_migrate_reset_status($migration_id = '') { } /** + * Print messages. + * * @param string $migration_id + * The migration id. */ function drush_migrate_tools_migrate_messages($migration_id) { - /** @var MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = \Drupal::service('plugin.manager.migration') + ->createInstance($migration_id); if ($migration) { $map = $migration->getIdMap(); $first = TRUE; @@ -386,7 +431,7 @@ function drush_migrate_tools_migrate_messages($migration_id) { } $first = FALSE; } - $table[] = (array)$row; + $table[] = (array) $row; } if (empty($table)) { drush_log(dt('No messages for this migration'), 'status'); @@ -412,11 +457,15 @@ function drush_migrate_tools_migrate_messages($migration_id) { } /** + * Print source fields. + * * @param string $migration_id + * The migration id. */ function drush_migrate_tools_migrate_fields_source($migration_id) { - /** @var MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = \Drupal::service('plugin.manager.migration') + ->createInstance($migration_id); if ($migration) { $source = $migration->getSourcePlugin(); $table = []; @@ -434,9 +483,10 @@ function drush_migrate_tools_migrate_fields_source($migration_id) { * Retrieve a list of active migrations. * * @param string $migration_ids - * Comma-separated list of migrations - if present, return only these migrations. + * Comma-separated list of migrations - if present, return only these + * migrations. * - * @return MigrationInterface[][] + * @return \Drupal\migrate\Plugin\MigrationInterface[][] * An array keyed by migration group, each value containing an array of * migrations or an empty array if no migrations match the input criteria. */ @@ -445,20 +495,33 @@ function drush_migrate_tools_migration_list($migration_ids = '') { $filter['migration_group'] = drush_get_option('group') ? explode(',', drush_get_option('group')) : []; $filter['migration_tags'] = drush_get_option('tag') ? explode(',', drush_get_option('tag')) : []; - $manager = \Drupal::service('plugin.manager.config_entity_migration'); + $manager = \Drupal::service('plugin.manager.migration'); $plugins = $manager->createInstances([]); $matched_migrations = []; // Get the set of migrations that may be filtered. if (empty($migration_ids)) { - $matched_migrations = $plugins; + $matched_migrations = $plugins; } else { // Get the requested migrations. $migration_ids = explode(',', Unicode::strtolower($migration_ids)); foreach ($plugins as $id => $migration) { if (in_array(Unicode::strtolower($id), $migration_ids)) { - $matched_migrations [$id] = $migration; + $matched_migrations[$id] = $migration; + } + } + } + + // Do not return any migrations which fail to meet requirements. + /** @var \Drupal\migrate\Plugin\Migration $migration */ + foreach ($matched_migrations as $id => $migration) { + if ($migration->getSourcePlugin() instanceof RequirementsInterface) { + try { + $migration->getSourcePlugin()->checkRequirements(); + } + catch (RequirementsException $e) { + unset($matched_migrations[$id]); } } } diff --git a/web/modules/contrib/migrate_tools/migrate_tools.info.yml b/web/modules/contrib/migrate_tools/migrate_tools.info.yml index ebf06eb43..e3cb18da2 100644 --- a/web/modules/contrib/migrate_tools/migrate_tools.info.yml +++ b/web/modules/contrib/migrate_tools/migrate_tools.info.yml @@ -4,11 +4,13 @@ description: 'Tools to assist in developing and running migrations.' package: Migration # core: 8.x dependencies: - - drupal:migrate (>=8.2) + - drupal:migrate (>=8.3) - migrate_plus:migrate_plus +test_dependencies: + - migrate_source_csv:migrate_source_csv (>=8.x-2.2) -# Information added by Drupal.org packaging script on 2016-10-12 -version: '8.x-3.0-beta1' +# Information added by Drupal.org packaging script on 2018-08-27 +version: '8.x-4.0' core: '8.x' project: 'migrate_tools' -datestamp: 1476313443 +datestamp: 1535380087 diff --git a/web/modules/contrib/migrate_tools/migrate_tools.links.task.yml b/web/modules/contrib/migrate_tools/migrate_tools.links.task.yml index 415d1780a..da508a0ff 100644 --- a/web/modules/contrib/migrate_tools/migrate_tools.links.task.yml +++ b/web/modules/contrib/migrate_tools/migrate_tools.links.task.yml @@ -39,3 +39,15 @@ entity.migration.delete_form: base_route: entity.migration.overview title: Delete weight: 10 + + +entity.migration.overview_general_edit: + title: Overview + route_name: entity.migration.edit_form + parent_id: entity.migration.edit_form + +entity.migration.overview_source_edit: + title: Source + route_name: migrate_tools.source_csv + parent_id: entity.migration.edit_form + weight: 1 diff --git a/web/modules/contrib/migrate_tools/migrate_tools.module b/web/modules/contrib/migrate_tools/migrate_tools.module index 4adb55506..b8113fe3d 100644 --- a/web/modules/contrib/migrate_tools/migrate_tools.module +++ b/web/modules/contrib/migrate_tools/migrate_tools.module @@ -6,20 +6,18 @@ */ /** - * Implements hook_entity_type_alter(). + * Implements hook_entity_type_build(). */ -function migrate_tools_entity_type_alter(array &$entity_types) { - // Inject our UI into the general migration and migration group config entities. +function migrate_tools_entity_type_build(array &$entity_types) { + // Inject our UI into the general migration and migration group config + // entities. /** @var \Drupal\Core\Config\Entity\ConfigEntityType[] $entity_types */ $entity_types['migration'] ->set('admin_permission', 'administer migrations') ->setHandlerClass('list_builder', 'Drupal\migrate_tools\Controller\MigrationListBuilder') -// ->setFormClass('add', 'Drupal\migrate_tools\Form\MigrationAddForm') ->setFormClass('edit', 'Drupal\migrate_tools\Form\MigrationEditForm') ->setFormClass('delete', 'Drupal\migrate_tools\Form\MigrationDeleteForm') -// ->setLinkTemplate('edit-form', '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}') - ->setLinkTemplate('list-form', '/admin/structure/migrate/manage/{migration_group}/migrations') -/* ->setLinkTemplate('delete-form', '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/delete')*/; + ->setLinkTemplate('list-form', '/admin/structure/migrate/manage/{migration_group}/migrations'); $entity_types['migration_group'] ->set('admin_permission', 'administer migrations') @@ -28,6 +26,33 @@ function migrate_tools_entity_type_alter(array &$entity_types) { ->setFormClass('edit', 'Drupal\migrate_tools\Form\MigrationGroupEditForm') ->setFormClass('delete', 'Drupal\migrate_tools\Form\MigrationGroupDeleteForm') ->setLinkTemplate('edit-form', '/admin/structure/migrate/manage/{migration_group}') -/* ->setLinkTemplate('list-form', '/admin/structure/migrate/manage/{migration_group}/migrations')*/ ->setLinkTemplate('delete-form', '/admin/structure/migrate/manage/{migration_group}/delete'); } + +/** + * Implements hook_migration_plugins_alter(). + */ +function migrate_tools_migration_plugins_alter(array &$migrations) { + /** @var \Drupal\Core\TempStore\PrivateTempStoreFactory $store */ + $tempStoreFactory = \Drupal::service('tempstore.private'); + $store = $tempStoreFactory->get('migrate_tools'); + // TODO: remove work-around after + // https://www.drupal.org/project/drupal/issues/2860341 is fixed. + if (!\Drupal::request()->hasSession()) { + $session = \Drupal::service('session'); + \Drupal::request()->setSession($session); + $session->start(); + } + // Get the list of changed migrations. + $migrationsChanged = $store->get('migrations_changed'); + if (isset($store) && (is_array($migrationsChanged))) { + // Alter the source column names for each changed migration. + foreach ($migrationsChanged as $id) { + $data = $store->get($id); + if (isset($data['changed'])) { + $migrations[$id]['source']['column_names'] = $data['changed']; + } + } + } + +} diff --git a/web/modules/contrib/migrate_tools/migrate_tools.routing.yml b/web/modules/contrib/migrate_tools/migrate_tools.routing.yml index b12735a28..08f43e5b5 100644 --- a/web/modules/contrib/migrate_tools/migrate_tools.routing.yml +++ b/web/modules/contrib/migrate_tools/migrate_tools.routing.yml @@ -58,52 +58,140 @@ entity.migration.overview: defaults: _controller: '\Drupal\migrate_tools\Controller\MigrationController::overview' _title: 'Migration overview' + _migrate_group: true requirements: _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group entity.migration.source: path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/source' defaults: _controller: '\Drupal\migrate_tools\Controller\MigrationController::source' _title: 'Source' + _migrate_group: true requirements: _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group entity.migration.process: path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/process' defaults: _controller: '\Drupal\migrate_tools\Controller\MigrationController::process' _title: 'Process' + _migrate_group: true requirements: _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +entity.migration.process.run: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/process/run' + defaults: + _controller: '\Drupal\migrate_tools\Controller\MigrationController::run' + _title: 'Run' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group entity.migration.destination: path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/destination' defaults: _controller: '\Drupal\migrate_tools\Controller\MigrationController::destination' _title: 'Destination' + _migrate_group: true requirements: _permission: 'administer migrations' - + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group # This is the router item for editing our migration entity. entity.migration.edit_form: path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/edit' defaults: _title: 'Edit migration' _entity_form: migration.edit + _migrate_group: true requirements: _entity_access: migration.update - + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group # This is the router item for deleting an instance of our migration entity. entity.migration.delete_form: path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/delete' defaults: _title: 'Delete migration' _entity_form: migration.delete + _migrate_group: true requirements: _entity_access: migration.delete - + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group migrate_tools.messages: path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/messages' defaults: _controller: '\Drupal\migrate_tools\Controller\MessageController::overview' _title: 'Messages' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +migrate_tools.execute: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/execute' + defaults: + _form: '\Drupal\migrate_tools\Form\MigrationExecuteForm' + _title: 'Execute migration' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +migrate_tools.source_csv: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/source/edit' + defaults: + _form: 'Drupal\migrate_tools\Form\SourceCsvForm' + _migrate_group: true requirements: _permission: 'administer migrations' + _custom_access: 'Drupal\migrate_tools\Form\SourceCsvForm::access' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group diff --git a/web/modules/contrib/migrate_tools/migrate_tools.services.yml b/web/modules/contrib/migrate_tools/migrate_tools.services.yml new file mode 100644 index 000000000..fa61e007f --- /dev/null +++ b/web/modules/contrib/migrate_tools/migrate_tools.services.yml @@ -0,0 +1,9 @@ +services: + logger.channel.migrate_tools: + class: Drupal\Core\Logger\LoggerChannel + factory: logger.factory:get + arguments: ['migrate_tools'] + route_processor.migrate_group: + class: Drupal\migrate_tools\Routing\RouteProcessor + tags: + - { name: route_processor_outbound } diff --git a/web/modules/contrib/migrate_tools/phpcs.xml b/web/modules/contrib/migrate_tools/phpcs.xml new file mode 100644 index 000000000..a18054efb --- /dev/null +++ b/web/modules/contrib/migrate_tools/phpcs.xml @@ -0,0 +1,207 @@ +<?xml version="1.0"?> +<ruleset name="Drupal coding standards"> + <description>Drupal 8 coding standards</description> + + <file>.</file> + <arg name="extensions" value="inc,install,module,php,profile,test,theme"/> + + <!--Exclude third party code.--> + <exclude-pattern>./vendor/*</exclude-pattern> + <!--Run Drupal standards.--> + <rule ref="Drupal.Array"/> + <rule ref="Drupal.Classes"/> + <rule ref="Drupal.Commenting"> + <!-- TagsNotGrouped and ParamGroup have false-positives. + @see https://www.drupal.org/node/2060925 --> + <exclude name="Drupal.Commenting.DocComment.TagsNotGrouped"/> + <exclude name="Drupal.Commenting.DocComment.ParamGroup"/> + </rule> + <rule ref="Drupal.ControlStructures"/> + <rule ref="Drupal.CSS"/> + <rule ref="Drupal.Files"/> + <rule ref="Drupal.Formatting"/> + <rule ref="Drupal.Functions"/> + <rule ref="Drupal.InfoFiles"/> + <rule ref="Drupal.Methods"/> + <rule ref="Drupal.NamingConventions"/> + <rule ref="Drupal.Scope"/> + <rule ref="Drupal.Semantics"/> + <rule ref="Drupal.Strings"/> + <rule ref="Drupal.WhiteSpace"/> + + <!-- Drupal Practice sniffs --> + <rule ref="DrupalPractice.Commenting"/> + + <!-- Generic sniffs --> + <rule ref="Generic.Arrays.DisallowLongArraySyntax"/> + <rule ref="Generic.Files.ByteOrderMark"/> + <rule ref="Generic.Files.LineEndings"/> + <rule ref="Generic.Formatting.SpaceAfterCast"/> + <rule ref="Generic.Functions.FunctionCallArgumentSpacing"/> + <rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie"> + <properties> + <property name="checkClosures" value="true"/> + </properties> + </rule> + <rule ref="Generic.NamingConventions.ConstructorName"/> + <rule ref="Generic.NamingConventions.UpperCaseConstantName"/> + <rule ref="Generic.PHP.DeprecatedFunctions"/> + <rule ref="Generic.PHP.DisallowShortOpenTag"/> + <rule ref="Generic.PHP.LowerCaseKeyword"/> + <rule ref="Generic.PHP.UpperCaseConstant"/> + <rule ref="Generic.WhiteSpace.DisallowTabIndent"/> + + <!-- MySource sniffs --> + <rule ref="MySource.Debug.DebugCode"/> + + <!-- PEAR sniffs --> + <rule ref="PEAR.Files.IncludingFile"/> + <!-- Disable some error messages that we do not want. --> + <rule ref="PEAR.Files.IncludingFile.UseIncludeOnce"> + <severity>0</severity> + </rule> + <rule ref="PEAR.Files.IncludingFile.UseInclude"> + <severity>0</severity> + </rule> + <rule ref="PEAR.Files.IncludingFile.UseRequireOnce"> + <severity>0</severity> + </rule> + <rule ref="PEAR.Files.IncludingFile.UseRequire"> + <severity>0</severity> + </rule> + <rule ref="PEAR.Functions.ValidDefaultValue"/> + + <!-- PEAR sniffs --> + <rule ref="PEAR.Functions.FunctionCallSignature"/> + <!-- The sniffs inside PEAR.Functions.FunctionCallSignature silenced below are + also silenced in Drupal CS' ruleset.xml. The code below is a 1-on-1 copy + from that file. --> + <!-- Disable some error messages that we already cover. --> + <rule ref="PEAR.Functions.FunctionCallSignature.SpaceAfterOpenBracket"> + <severity>0</severity> + </rule> + <rule ref="PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket"> + <severity>0</severity> + </rule> + <!-- Disable some error messages that we do not want. --> + <rule ref="PEAR.Functions.FunctionCallSignature.Indent"> + <severity>0</severity> + </rule> + <rule ref="PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket"> + <severity>0</severity> + </rule> + <rule ref="PEAR.Functions.FunctionCallSignature.CloseBracketLine"> + <severity>0</severity> + </rule> + <rule ref="PEAR.Functions.FunctionCallSignature.EmptyLine"> + <severity>0</severity> + </rule> + + <!-- PSR-2 sniffs --> + <rule ref="PSR2.Classes.PropertyDeclaration"> + <exclude name="PSR2.Classes.PropertyDeclaration.Underscore"/> + </rule> + <rule ref="PSR2.Namespaces.NamespaceDeclaration"/> + <rule ref="PSR2.Namespaces.UseDeclaration"> + <exclude name="PSR2.Namespaces.UseDeclaration.UseAfterNamespace"/> + </rule> + + <!-- Squiz sniffs --> + <rule ref="Squiz.Arrays.ArrayBracketSpacing"/> + <rule ref="Squiz.Arrays.ArrayDeclaration"> + <exclude name="Squiz.Arrays.ArrayDeclaration.NoKeySpecified"/> + <exclude name="Squiz.Arrays.ArrayDeclaration.KeySpecified"/> + </rule> + <!-- Disable some error messages that we do not want. --> + <rule ref="Squiz.Arrays.ArrayDeclaration.CloseBraceNotAligned"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.DoubleArrowNotAligned"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.FirstValueNoNewline"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.KeyNotAligned"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.MultiLineNotAllowed"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.NoComma"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.NotLowerCase"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.SingleLineNotAllowed"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.ValueNotAligned"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Arrays.ArrayDeclaration.ValueNoNewline"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration"/> + <!-- Disable some error messages that we already cover. --> + <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.AsNotLower"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceAfterOpen"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceBeforeClose"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.ForLoopDeclaration"/> + <!-- Disable some error messages that we already cover. --> + <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingAfterOpen"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingBeforeClose"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Functions.MultiLineFunctionDeclaration"/> + <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.ContentAfterBrace"> + <severity>0</severity> + </rule> + <!-- Standard yet to be finalized on this (https://www.drupal.org/node/1539712). --> + <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.FirstParamSpacing"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.Indent"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.CloseBracketLine"> + <severity>0</severity> + </rule> + <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing"> + <properties> + <property name="equalsSpacing" value="1"/> + </properties> + </rule> + <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.NoSpaceBeforeArg"> + <severity>0</severity> + </rule> + <rule ref="Squiz.PHP.LowercasePHPFunctions"/> + <rule ref="Squiz.Strings.ConcatenationSpacing"> + <properties> + <property name="spacing" value="1"/> + <property name="ignoreNewlines" value="true"/> + </properties> + </rule> + <rule ref="Squiz.WhiteSpace.LanguageConstructSpacing" /> + <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/> + <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/> + + <!-- Zend sniffs --> + <rule ref="Zend.Files.ClosingTag"/> + +</ruleset> diff --git a/web/modules/contrib/migrate_tools/src/Commands/MigrateToolsCommands.php b/web/modules/contrib/migrate_tools/src/Commands/MigrateToolsCommands.php new file mode 100644 index 000000000..976b3c684 --- /dev/null +++ b/web/modules/contrib/migrate_tools/src/Commands/MigrateToolsCommands.php @@ -0,0 +1,719 @@ +<?php + +namespace Drupal\migrate_tools\Commands; + +use Consolidation\OutputFormatters\StructuredData\RowsOfFields; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Datetime\DateFormatter; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\migrate\Exception\RequirementsException; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Plugin\MigrationPluginManager; +use Drupal\migrate\Plugin\RequirementsInterface; +use Drupal\migrate_tools\Drush9LogMigrateMessage; +use Drupal\migrate_tools\MigrateExecutable; +use Drush\Commands\DrushCommands; + +/** + * Migrate Tools drush commands. + */ +class MigrateToolsCommands extends DrushCommands { + + /** + * Migration plugin manager service. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManager + */ + protected $migrationPluginManager; + + /** + * Date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatter + */ + protected $dateFormatter; + + /** + * Entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Key-value store service. + * + * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface + */ + protected $keyValue; + + /** + * Migrate message logger. + * + * @var \Drupal\migrate_tools\Drush9LogMigrateMessage + */ + protected $migrateMessage; + + /** + * MigrateToolsCommands constructor. + * + * @param \Drupal\migrate\Plugin\MigrationPluginManager $migrationPluginManager + * Migration Plugin Manager service. + * @param \Drupal\Core\Datetime\DateFormatter $dateFormatter + * Date formatter service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * Entity type manager service. + * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $keyValue + * Key-value store service. + */ + public function __construct(MigrationPluginManager $migrationPluginManager, DateFormatter $dateFormatter, EntityTypeManagerInterface $entityTypeManager, KeyValueFactoryInterface $keyValue) { + parent::__construct(); + $this->migrationPluginManager = $migrationPluginManager; + $this->dateFormatter = $dateFormatter; + $this->entityTypeManager = $entityTypeManager; + $this->keyValue = $keyValue; + } + + /** + * List all migrations with current status. + * + * @param string $migration_names + * Restrict to a comma-separated list of migrations (Optional). + * @param array $options + * Additional options for the command. + * + * @command migrate:status + * + * @option group A comma-separated list of migration groups to list + * @option tag Name of the migration tag to list + * @option names-only Only return names, not all the details (faster) + * + * @usage migrate:status + * Retrieve status for all migrations + * @usage migrate:status --group=beer + * Retrieve status for all migrations in a given group + * @usage migrate:status --tag=user + * Retrieve status for all migrations with a given tag + * @usage migrate:status --group=beer --tag=user + * Retrieve status for all migrations in the beer group + * and with the user tag. + * @usage migrate:status beer_term,beer_node + * Retrieve status for specific migrations + * + * @validate-module-enabled migrate_tools + * + * @aliases ms, migrate-status + * + * @field-labels + * group: Group + * id: Migration ID + * status: Status + * total: Total + * imported: Imported + * unprocessed: Unprocessed + * last_imported: Last Imported + * @default-fields group,id,status,total,imported,unprocessed,last_imported + * + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields + * Migrations status formatted as table. + */ + public function status($migration_names = '', array $options = ['group' => NULL, 'tag' => NULL, 'names-only' => NULL]) { + $names_only = $options['names-only']; + + $migrations = $this->migrationsList($migration_names, $options); + + $table = []; + // Take it one group at a time, listing the migrations within each group. + foreach ($migrations as $group_id => $migration_list) { + /** @var \Drupal\migrate_plus\Entity\MigrationGroup $group */ + $group = $this->entityTypeManager->getStorage('migration_group')->load($group_id); + $group_name = !empty($group) ? "{$group->label()} ({$group->id()})" : $group_id; + + foreach ($migration_list as $migration_id => $migration) { + if ($names_only) { + $table[] = [ + 'group' => dt('Group: @name', ['@name' => $group_name]), + 'id' => $migration_id, + ]; + } + else { + try { + $map = $migration->getIdMap(); + $imported = $map->importedCount(); + $source_plugin = $migration->getSourcePlugin(); + } + catch (\Exception $e) { + $this->logger()->error( + dt( + 'Failure retrieving information on @migration: @message', + ['@migration' => $migration_id, '@message' => $e->getMessage()] + ) + ); + continue; + } + + try { + $source_rows = $source_plugin->count(); + // -1 indicates uncountable sources. + if ($source_rows == -1) { + $source_rows = dt('N/A'); + $unprocessed = dt('N/A'); + } + else { + $unprocessed = $source_rows - $map->processedCount(); + } + } + catch (\Exception $e) { + $this->logger()->error( + dt( + 'Could not retrieve source count from @migration: @message', + ['@migration' => $migration_id, '@message' => $e->getMessage()] + ) + ); + $source_rows = dt('N/A'); + $unprocessed = dt('N/A'); + } + + $status = $migration->getStatusLabel(); + $migrate_last_imported_store = $this->keyValue->get( + 'migrate_last_imported' + ); + $last_imported = $migrate_last_imported_store->get( + $migration->id(), + FALSE + ); + if ($last_imported) { + $last_imported = $this->dateFormatter->format( + $last_imported / 1000, + 'custom', + 'Y-m-d H:i:s' + ); + } + else { + $last_imported = ''; + } + $table[] = [ + 'group' => $group_name, + 'id' => $migration_id, + 'status' => $status, + 'total' => $source_rows, + 'imported' => $imported, + 'unprocessed' => $unprocessed, + 'last_imported' => $last_imported, + ]; + } + } + + // Add empty row to separate groups, for readability. + end($migrations); + if ($group_id !== key($migrations)) { + $table[] = []; + } + } + + return new RowsOfFields($table); + } + + /** + * Perform one or more migration processes. + * + * @param string $migration_names + * ID of migration(s) to import. Delimit multiple using commas. + * @param array $options + * Additional options for the command. + * + * @command migrate:import + * + * @option all Process all migrations. + * @option group A comma-separated list of migration groups to import + * @option tag Name of the migration tag to import + * @option limit Limit on the number of items to process in each migration + * @option feedback Frequency of progress messages, in items processed + * @option idlist Comma-separated list of IDs to import + * @option update In addition to processing unprocessed items from the + * source, update previously-imported items with the current data + * @option force Force an operation to run, even if all dependencies are not + * satisfied + * @option execute-dependencies Execute all dependent migrations first. + * + * @usage migrate:import --all + * Perform all migrations + * @usage migrate:import --group=beer + * Import all migrations in the beer group + * @usage migrate:import --tag=user + * Import all migrations with the user tag + * @usage migrate:import --group=beer --tag=user + * Import all migrations in the beer group and with the user tag + * @usage migrate:import beer_term,beer_node + * Import new terms and nodes + * @usage migrate:import beer_user --limit=2 + * Import no more than 2 users + * @usage migrate:import beer_user --idlist=5 + * Import the user record with source ID 5 + * + * @validate-module-enabled migrate_tools + * + * @aliases mim, migrate-import + * + * @throws \Exception + * If there are not enough parameters to the command. + */ + public function import($migration_names = '', array $options = ['all' => NULL, 'group' => NULL, 'tag' => NULL, 'limit' => NULL, 'feedback' => NULL, 'idlist' => NULL, 'update' => NULL, 'force' => NULL, 'execute-dependencies' => NULL]) { + $group_names = $options['group']; + $tag_names = $options['tag']; + $all = $options['all']; + $additional_options = []; + if (!$all && !$group_names && !$migration_names && !$tag_names) { + throw new \Exception(dt('You must specify --all, --group, --tag or one or more migration names separated by commas')); + } + + foreach (['limit', 'feedback', 'idlist', 'update', 'force', 'execute-dependencies'] as $option) { + if ($options[$option]) { + $additional_options[$option] = $options[$option]; + } + } + + $migrations = $this->migrationsList($migration_names, $options); + if (empty($migrations)) { + $this->logger->error(dt('No migrations found.')); + } + + // Take it one group at a time, importing the migrations within each group. + foreach ($migrations as $group_id => $migration_list) { + array_walk( + $migration_list, + [$this, 'executeMigration'], + $additional_options + ); + } + } + + /** + * Rollback one or more migrations. + * + * @param string $migration_names + * Name of migration(s) to rollback. Delimit multiple using commas. + * @param array $options + * Additional options for the command. + * + * @command migrate:rollback + * + * @option all Process all migrations. + * @option group A comma-separated list of migration groups to rollback + * @option tag ID of the migration tag to rollback + * @option feedback Frequency of progress messages, in items processed + * + * @usage migrate:rollback --all + * Perform all migrations + * @usage migrate:rollback --group=beer + * Rollback all migrations in the beer group + * @usage migrate:rollback --tag=user + * Rollback all migrations with the user tag + * @usage migrate:rollback --group=beer --tag=user + * Rollback all migrations in the beer group and with the user tag + * @usage migrate:rollback beer_term,beer_node + * Rollback imported terms and nodes + * @validate-module-enabled migrate_tools + * + * @aliases mr, migrate-rollback + * + * @throws \Exception + * If there are not enough parameters to the command. + */ + public function rollback($migration_names = '', array $options = ['all' => NULL, 'group' => NULL, 'tag' => NULL, 'feedback' => NULL]) { + $group_names = $options['group']; + $tag_names = $options['tag']; + $all = $options['all']; + $additional_options = []; + if (!$all && !$group_names && !$migration_names && !$tag_names) { + throw new \Exception(dt('You must specify --all, --group, --tag, or one or more migration names separated by commas')); + } + + if ($options['feedback']) { + $additional_options['feedback'] = $options['feedback']; + } + + $migrations = $this->migrationsList($migration_names, $options); + if (empty($migrations)) { + $this->logger()->error(dt('No migrations found.')); + } + + // Take it one group at a time, + // rolling back the migrations within each group. + foreach ($migrations as $group_id => $migration_list) { + // Roll back in reverse order. + $migration_list = array_reverse($migration_list); + foreach ($migration_list as $migration_id => $migration) { + $executable = new MigrateExecutable( + $migration, + $this->getMigrateMessage(), + $additional_options + ); + // drush_op() provides --simulate support. + drush_op([$executable, 'rollback']); + } + } + } + + /** + * Stop an active migration operation. + * + * @param string $migration_id + * ID of migration to stop. + * + * @command migrate:stop + * + * @validate-module-enabled migrate_tools + * @aliases mst, migrate-stop + */ + public function stop($migration_id = '') { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance( + $migration_id + ); + if ($migration) { + $status = $migration->getStatus(); + switch ($status) { + case MigrationInterface::STATUS_IDLE: + $this->logger()->warning( + dt('Migration @id is idle', ['@id' => $migration_id]) + ); + break; + + case MigrationInterface::STATUS_DISABLED: + $this->logger()->warning( + dt('Migration @id is disabled', ['@id' => $migration_id]) + ); + break; + + case MigrationInterface::STATUS_STOPPING: + $this->logger()->warning( + dt('Migration @id is already stopping', ['@id' => $migration_id]) + ); + break; + + default: + $migration->interruptMigration(MigrationInterface::RESULT_STOPPED); + $this->logger()->notice( + dt('Migration @id requested to stop', ['@id' => $migration_id]) + ); + break; + } + } + else { + $this->logger()->error( + dt('Migration @id does not exist', ['@id' => $migration_id]) + ); + } + } + + /** + * Reset a active migration's status to idle. + * + * @param string $migration_id + * ID of migration to reset. + * + * @command migrate:reset-status + * + * @validate-module-enabled migrate_tools + * @aliases mrs, migrate-reset-status + */ + public function resetStatus($migration_id = '') { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance( + $migration_id + ); + if ($migration) { + $status = $migration->getStatus(); + if ($status == MigrationInterface::STATUS_IDLE) { + $this->logger()->warning( + dt('Migration @id is already Idle', ['@id' => $migration_id]) + ); + } + else { + $migration->setStatus(MigrationInterface::STATUS_IDLE); + $this->logger()->notice( + dt('Migration @id reset to Idle', ['@id' => $migration_id]) + ); + } + } + else { + $this->logger()->error( + dt('Migration @id does not exist', ['@id' => $migration_id]) + ); + } + } + + /** + * View any messages associated with a migration. + * + * @param string $migration_id + * ID of the migration. + * @param array $options + * Additional options for the command. + * + * @command migrate:messages + * + * @option csv Export messages as a CSV + * + * @usage migrate:messages MyNode + * Show all messages for the MyNode migration + * + * @validate-module-enabled migrate_tools + * + * @aliases mmsg,migrate-messages + * + * @field-labels + * source_ids_hash: Source IDs Hash + * level: Level + * message: Message + * @default-fields source_ids_hash,level,message + * + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields + * Source fields of the given migration formatted as a table. + */ + public function messages($migration_id, array $options = ['csv' => NULL]) { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance( + $migration_id + ); + if (!$migration) { + $this->logger()->error( + dt('Migration @id does not exist', ['@id' => $migration_id]) + ); + return NULL; + } + + $map = $migration->getIdMap(); + $table = []; + foreach ($map->getMessageIterator() as $row) { + unset($row->msgid); + $table[] = (array) $row; + } + if (empty($table)) { + $this->logger()->notice(dt('No messages for this migration')); + return NULL; + } + + if ($options['csv']) { + fputcsv(STDOUT, array_keys($table[0])); + foreach ($table as $row) { + fputcsv(STDOUT, $row); + } + return NULL; + } + return new RowsOfFields($table); + } + + /** + * List the fields available for mapping in a source. + * + * @param string $migration_id + * ID of the migration. + * + * @command migrate:fields-source + * + * @usage migrate:fields-source my_node + * List fields for the source in the my_node migration + * + * @validate-module-enabled migrate_tools + * + * @aliases mfs, migrate-fields-source + * + * @field-labels + * machine_name: Machine Name + * description: Description + * @default-fields machine_name,description + * + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields + * Source fields of the given migration formatted as a table. + */ + public function fieldsSource($migration_id) { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance( + $migration_id + ); + if ($migration) { + $source = $migration->getSourcePlugin(); + $table = []; + foreach ($source->fields() as $machine_name => $description) { + $table[] = [ + 'machine_name' => $machine_name, + 'description' => strip_tags($description), + ]; + } + return new RowsOfFields($table); + } + else { + $this->logger()->error( + dt('Migration @id does not exist', ['@id' => $migration_id]) + ); + } + } + + /** + * Retrieve a list of active migrations. + * + * @param string $migration_ids + * Comma-separated list of migrations - + * if present, return only these migrations. + * @param array $options + * Command options. + * + * @return \Drupal\migrate\Plugin\MigrationInterface[][] + * An array keyed by migration group, each value containing an array of + * migrations or an empty array if no migrations match the input criteria. + */ + protected function migrationsList($migration_ids = '', array $options = []) { + // Filter keys must match the migration configuration property name. + $filter['migration_group'] = $options['group'] ? explode( + ',', + $options['group'] + ) : []; + $filter['migration_tags'] = $options['tag'] ? explode( + ',', + $options['tag'] + ) : []; + + $manager = $this->migrationPluginManager; + $plugins = $manager->createInstances([]); + $matched_migrations = []; + + // Get the set of migrations that may be filtered. + if (empty($migration_ids)) { + $matched_migrations = $plugins; + } + else { + // Get the requested migrations. + $migration_ids = explode(',', Unicode::strtolower($migration_ids)); + foreach ($plugins as $id => $migration) { + if (in_array(Unicode::strtolower($id), $migration_ids)) { + $matched_migrations[$id] = $migration; + } + } + } + + // Do not return any migrations which fail to meet requirements. + /** @var \Drupal\migrate\Plugin\Migration $migration */ + foreach ($matched_migrations as $id => $migration) { + if ($migration->getSourcePlugin() instanceof RequirementsInterface) { + try { + $migration->getSourcePlugin()->checkRequirements(); + } + catch (RequirementsException $e) { + unset($matched_migrations[$id]); + } + } + } + + // Filters the matched migrations if a group or a tag has been input. + if (!empty($filter['migration_group']) || !empty($filter['migration_tags'])) { + // Get migrations in any of the specified groups and with any of the + // specified tags. + foreach ($filter as $property => $values) { + if (!empty($values)) { + $filtered_migrations = []; + foreach ($values as $search_value) { + foreach ($matched_migrations as $id => $migration) { + // Cast to array because migration_tags can be an array. + $configured_values = (array) $migration->get($property); + $configured_id = (in_array( + $search_value, + $configured_values + )) ? $search_value : 'default'; + if (empty($search_value) || $search_value == $configured_id) { + if (empty($migration_ids) || in_array( + Unicode::strtolower($id), + $migration_ids + )) { + $filtered_migrations[$id] = $migration; + } + } + } + } + $matched_migrations = $filtered_migrations; + } + } + } + + // Sort the matched migrations by group. + if (!empty($matched_migrations)) { + foreach ($matched_migrations as $id => $migration) { + $configured_group_id = empty($migration->get('migration_group')) ? 'default' : $migration->get('migration_group'); + $migrations[$configured_group_id][$id] = $migration; + } + } + return isset($migrations) ? $migrations : []; + } + + /** + * Executes a single migration. + * + * If the --execute-dependencies option was given, + * the migration's dependencies will also be executed first. + * + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration to execute. + * @param string $migration_id + * The migration ID (not used, just an artifact of array_walk()). + * @param array $options + * Additional options of the command. + * + * @throws \Exception + * If some migrations failed during execution. + */ + protected function executeMigration(MigrationInterface $migration, $migration_id, array $options = []) { + // Keep track of all migrations run during this command so the same + // migration is not run multiple times. + static $executed_migrations = []; + + if (isset($options['execute-dependencies'])) { + $required_migrations = $migration->get('requirements'); + $required_migrations = array_filter($required_migrations, function ($value) use ($executed_migrations) { + return !isset($executed_migrations[$value]); + }); + + if (!empty($required_migrations)) { + $manager = $this->migrationPluginManager; + $required_migrations = $manager->createInstances($required_migrations); + $dependency_options = array_merge($options, ['is_dependency' => TRUE]); + array_walk($required_migrations, [$this, __FUNCTION__], $dependency_options); + $executed_migrations += $required_migrations; + } + } + if (!empty($options['force'])) { + $migration->set('requirements', []); + } + if (!empty($options['update'])) { + $migration->getIdMap()->prepareUpdate(); + } + $executable = new MigrateExecutable($migration, $this->getMigrateMessage(), $options); + // drush_op() provides --simulate support. + drush_op([$executable, 'import']); + $executed_migrations += [$migration_id => $migration_id]; + if ($count = $executable->getFailedCount()) { + // Nudge Drush to use a non-zero exit code. + throw new \Exception( + dt( + '!name Migration - !count failed.', + ['!name' => $migration_id, '!count' => $count] + ) + ); + } + } + + /** + * Gets the migrate message logger. + * + * @return \Drupal\migrate\MigrateMessageInterface + * The migrate message service. + */ + protected function getMigrateMessage() { + if (!isset($this->migrateMessage)) { + $this->migrateMessage = new Drush9LogMigrateMessage($this->logger()); + } + return $this->migrateMessage; + } + +} diff --git a/web/modules/contrib/migrate_tools/src/Controller/MessageController.php b/web/modules/contrib/migrate_tools/src/Controller/MessageController.php index 54a194fb8..fbd347c96 100644 --- a/web/modules/contrib/migrate_tools/src/Controller/MessageController.php +++ b/web/modules/contrib/migrate_tools/src/Controller/MessageController.php @@ -6,7 +6,9 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Database\Connection; use Drupal\migrate\Plugin\MigrationInterface; -use Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; +use Drupal\migrate_plus\Entity\MigrationGroupInterface; +use Drupal\migrate_plus\Entity\MigrationInterface as MigratePlusMigrationInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -24,9 +26,9 @@ class MessageController extends ControllerBase { /** * Plugin manager for migration plugins. * - * @var \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface */ - protected $migrationConfigEntityPluginManager; + protected $migrationPluginManager; /** * {@inheritdoc} @@ -34,7 +36,7 @@ class MessageController extends ControllerBase { public static function create(ContainerInterface $container) { return new static( $container->get('database'), - $container->get('plugin.manager.config_entity_migration') + $container->get('plugin.manager.migration') ); } @@ -43,12 +45,12 @@ class MessageController extends ControllerBase { * * @param \Drupal\Core\Database\Connection $database * A database connection. - * @param \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager - * The plugin manager for config entity-based migrations. + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager + * The migration plugin manager. */ - public function __construct(Connection $database, MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager) { + public function __construct(Connection $database, MigrationPluginManagerInterface $migration_plugin_manager) { $this->database = $database; - $this->migrationConfigEntityPluginManager = $migration_config_entity_plugin_manager; + $this->migrationPluginManager = $migration_plugin_manager; } /** @@ -71,21 +73,19 @@ class MessageController extends ControllerBase { * * Messages are truncated at 56 chars. * - * @param string $migration_group - * Machine name of the migration's group. - * - * @param string $migration - * Machine name of the migration. + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. * * @return array * A render array as expected by drupal_render(). */ - public function overview($migration_group, $migration) { + public function overview(MigrationGroupInterface $migration_group, MigratePlusMigrationInterface $migration) { $rows = []; $classes = static::getLogLevelClassMap(); - /** @var MigrationInterface $migration */ - $migration = $this->migrationConfigEntityPluginManager->createInstance($migration); - $source_id_field_names = array_keys($migration->getSourcePlugin()->getIds()); + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $source_id_field_names = array_keys($migration_plugin->getSourcePlugin()->getIds()); $column_number = 1; foreach ($source_id_field_names as $source_id_field_name) { $header[] = [ @@ -104,8 +104,8 @@ class MessageController extends ControllerBase { 'field' => 'message', ]; - $message_table = $migration->getIdMap()->messageTableName(); - $map_table = $migration->getIdMap()->mapTableName(); + $message_table = $migration_plugin->getIdMap()->messageTableName(); + $map_table = $migration_plugin->getIdMap()->mapTableName(); $query = $this->database->select($message_table, 'msg') ->extend('\Drupal\Core\Database\Query\PagerSelectExtender') ->extend('\Drupal\Core\Database\Query\TableSortExtender'); diff --git a/web/modules/contrib/migrate_tools/src/Controller/MigrationController.php b/web/modules/contrib/migrate_tools/src/Controller/MigrationController.php index 09ae154ad..15c90795c 100644 --- a/web/modules/contrib/migrate_tools/src/Controller/MigrationController.php +++ b/web/modules/contrib/migrate_tools/src/Controller/MigrationController.php @@ -3,12 +3,17 @@ namespace Drupal\migrate_tools\Controller; use Drupal\Core\Controller\ControllerBase; -use Drupal\migrate\Plugin\MigrationInterface; use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Html; -use Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager; +use Drupal\Core\Routing\CurrentRouteMatch; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\migrate_plus\Entity\MigrationGroupInterface; +use Drupal\migrate_plus\Entity\MigrationInterface; +use Drupal\migrate_tools\MigrateBatchExecutable; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Url; +use Drupal\migrate\MigrateMessage; /** * Returns responses for migrate_tools migration view routes. @@ -18,18 +23,28 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn /** * Plugin manager for migration plugins. * - * @var \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface */ - protected $migrationConfigEntityPluginManager; + protected $migrationPluginManager; + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\CurrentRouteMatch + */ + protected $currentRouteMatch; /** * Constructs a new MigrationController object. * - * @param \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager * The plugin manager for config entity-based migrations. + * @param \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatch + * The current route match. */ - public function __construct(MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager) { - $this->migrationConfigEntityPluginManager = $migration_config_entity_plugin_manager; + public function __construct(MigrationPluginManagerInterface $migration_plugin_manager, CurrentRouteMatch $currentRouteMatch) { + $this->migrationPluginManager = $migration_plugin_manager; + $this->currentRouteMatch = $currentRouteMatch; } /** @@ -37,26 +52,23 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn */ public static function create(ContainerInterface $container) { return new static( - $container->get('plugin.manager.config_entity_migration') + $container->get('plugin.manager.migration'), + $container->get('current_route_match') ); } /** * Displays an overview of a migration entity. * - * @param string $migration_group - * Machine name of the migration's group. - * @param string $migration - * Machine name of the migration. + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. * * @return array * A render array as expected by drupal_render(). */ - public function overview($migration_group, $migration) { - - /** @var MigrationInterface $migration */ - $migration = $this->migrationConfigEntityPluginManager->createInstance($migration); - + public function overview(MigrationGroupInterface $migration_group, MigrationInterface $migration) { $build['overview'] = [ '#type' => 'fieldset', '#title' => $this->t('Overview'), @@ -64,7 +76,7 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn $build['overview']['group'] = [ '#title' => $this->t('Group:'), - '#markup' => Xss::filterAdmin($migration_group), + '#markup' => Xss::filterAdmin($migration_group->label()), '#type' => 'item', ]; @@ -73,8 +85,8 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn '#markup' => Xss::filterAdmin($migration->label()), '#type' => 'item', ]; - - $migration_dependencies = $migration->getMigrationDependencies(); + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $migration_dependencies = $migration_plugin->getMigrationDependencies(); if (!empty($migration_dependencies['required'])) { $build['overview']['dependencies'] = [ '#title' => $this->t('Migration Dependencies') , @@ -96,19 +108,15 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn /** * Display source information of a migration entity. * - * @param string $migration_group - * Machine name of the migration's group. - * @param string $migration - * Machine name of the migration. + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. * * @return array * A render array as expected by drupal_render(). */ - public function source($migration_group, $migration) { - - /** @var MigrationInterface $migration */ - $migration = $this->migrationConfigEntityPluginManager->createInstance($migration); - + public function source(MigrationGroupInterface $migration_group, MigrationInterface $migration) { // Source field information. $build['source'] = [ '#type' => 'fieldset', @@ -119,8 +127,8 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn 'id' => 'migration-detail-source', ], ]; - - $source = $migration->getSourcePlugin(); + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $source = $migration_plugin->getSourcePlugin(); $build['source']['query'] = [ '#type' => 'item', '#title' => $this->t('Query'), @@ -128,7 +136,7 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn ]; $header = [$this->t('Machine name'), $this->t('Description')]; $rows = []; - foreach ($source->fields($migration) as $machine_name => $description) { + foreach ($source->fields($migration_plugin) as $machine_name => $description) { $rows[] = [ ['data' => Html::escape($machine_name)], ['data' => Xss::filterAdmin($description)], @@ -146,20 +154,45 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn } /** - * Display process information of a migration entity. + * Run a migration. * - * @param string $migration_group - * Machine name of the migration's group. - * @param string $migration - * Machine name of the migration. + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. * * @return array * A render array as expected by drupal_render(). */ - public function process($migration_group, $migration) { + public function run(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + $migrateMessage = new MigrateMessage(); + $options = []; + + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $executable = new MigrateBatchExecutable($migration_plugin, $migrateMessage, $options); + $executable->batchImport(); - /** @var MigrationInterface $migration */ - $migration = $this->migrationConfigEntityPluginManager->createInstance($migration); + $migration_group = $this->currentRouteMatch->getParameter('migration_group'); + $route_parameters = [ + 'migration_group' => $migration_group, + 'migration' => $migration->id(), + ]; + return batch_process(Url::fromRoute('entity.migration.process', $route_parameters)); + } + + /** + * Display process information of a migration entity. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function process(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); // Process information. $build['process'] = [ @@ -174,7 +207,7 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn $this->t('Default'), ]; $rows = []; - foreach ($migration->getProcess() as $destination_id => $process_line) { + foreach ($migration_plugin->getProcess() as $destination_id => $process_line) { $row = []; $row[] = ['data' => Html::escape($destination_id)]; if (isset($process_line[0]['source'])) { @@ -205,23 +238,28 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn '#empty' => $this->t('No process defined.'), ]; + $build['process']['run'] = [ + '#type' => 'link', + '#title' => $this->t('Run'), + '#url' => Url::fromRoute('entity.migration.process.run', ['migration_group' => $migration_group->id(), 'migration' => $migration->id()]), + ]; + return $build; } /** * Displays destination information of a migration entity. * - * @param string $migration_group - * Machine name of the migration's group. - * @param string $migration - * Machine name of the migration. + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. * * @return array * A render array as expected by drupal_render(). */ - public function destination($migration_group, $migration) { - /** @var MigrationInterface $migration */ - $migration = $this->migrationConfigEntityPluginManager->createInstance($migration); + public function destination(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); // Destination field information. $build['destination'] = [ @@ -233,7 +271,7 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn 'id' => 'migration-detail-destination', ], ]; - $destination = $migration->getDestinationPlugin(); + $destination = $migration_plugin->getDestinationPlugin(); $build['destination']['type'] = [ '#type' => 'item', '#title' => $this->t('Type'), diff --git a/web/modules/contrib/migrate_tools/src/Controller/MigrationGroupListBuilder.php b/web/modules/contrib/migrate_tools/src/Controller/MigrationGroupListBuilder.php index 6757c22b1..8ab2b87fb 100644 --- a/web/modules/contrib/migrate_tools/src/Controller/MigrationGroupListBuilder.php +++ b/web/modules/contrib/migrate_tools/src/Controller/MigrationGroupListBuilder.php @@ -34,13 +34,13 @@ class MigrationGroupListBuilder extends ConfigEntityListBuilder { /** * Builds a row for an entity in the entity listing. * - * @param EntityInterface $entity + * @param \Drupal\Core\Entity\EntityInterface $entity * The entity for which to build the row. * * @return array * A render array of the table row for displaying the entity. * - * @see Drupal\Core\Entity\EntityListController::render() + * @see \Drupal\Core\Entity\EntityListController::render() */ public function buildRow(EntityInterface $entity) { $row['label'] = $entity->label(); diff --git a/web/modules/contrib/migrate_tools/src/Controller/MigrationListBuilder.php b/web/modules/contrib/migrate_tools/src/Controller/MigrationListBuilder.php index 16ee84f32..d078764ce 100644 --- a/web/modules/contrib/migrate_tools/src/Controller/MigrationListBuilder.php +++ b/web/modules/contrib/migrate_tools/src/Controller/MigrationListBuilder.php @@ -2,17 +2,18 @@ namespace Drupal\migrate_tools\Controller; +use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Routing\CurrentRouteMatch; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate_plus\Entity\MigrationGroup; -use Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Datetime\DateFormatter; /** * Provides a listing of migration entities in a given group. @@ -33,9 +34,16 @@ class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHand /** * Plugin manager for migration plugins. * - * @var \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface */ - protected $migrationConfigEntityPluginManager; + protected $migrationPluginManager; + + /** + * The logger service. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; /** * Constructs a new EntityListBuilder object. @@ -46,13 +54,16 @@ class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHand * The entity storage class. * @param \Drupal\Core\Routing\CurrentRouteMatch $current_route_match * The current route match service. - * @param \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager * The plugin manager for config entity-based migrations. + * @param \Drupal\Core\Logger\LoggerChannelInterface $logger + * The logger service. */ - public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, CurrentRouteMatch $current_route_match, MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager) { + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, CurrentRouteMatch $current_route_match, MigrationPluginManagerInterface $migration_plugin_manager, LoggerChannelInterface $logger) { parent::__construct($entity_type, $storage); $this->currentRouteMatch = $current_route_match; - $this->migrationConfigEntityPluginManager = $migration_config_entity_plugin_manager; + $this->migrationPluginManager = $migration_plugin_manager; + $this->logger = $logger; } /** @@ -63,7 +74,8 @@ class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHand $entity_type, $container->get('entity.manager')->getStorage($entity_type->id()), $container->get('current_route_match'), - $container->get('plugin.manager.config_entity_migration') + $container->get('plugin.manager.migration'), + $container->get('logger.channel.migrate_tools') ); } @@ -100,7 +112,7 @@ class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHand * @return array * A render array structure of header strings. * - * @see Drupal\Core\Entity\EntityListController::render() + * @see \Drupal\Core\Entity\EntityListController::render() */ public function buildHeader() { $header['label'] = $this->t('Migration'); @@ -111,7 +123,8 @@ class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHand $header['unprocessed'] = $this->t('Unprocessed'); $header['messages'] = $this->t('Messages'); $header['last_imported'] = $this->t('Last Imported'); - return $header; // + parent::buildHeader(); + $header['operations'] = $this->t('Operations'); + return $header; } /** @@ -120,84 +133,104 @@ class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHand * @param \Drupal\Core\Entity\EntityInterface $migration_entity * The migration plugin for which to build the row. * - * @return array + * @return array|null * A render array of the table row for displaying the plugin information. * - * @see Drupal\Core\Entity\EntityListController::render() + * @see \Drupal\Core\Entity\EntityListController::render() */ public function buildRow(EntityInterface $migration_entity) { - $migration = $this->migrationConfigEntityPluginManager->createInstance($migration_entity->id()); - $migration_group = $migration->get('migration_group'); - if (!$migration_group) { - $migration_group = 'default'; + try { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance($migration_entity->id()); + $migration_group = $migration->get('migration_group'); + if (!$migration_group) { + $migration_group = 'default'; + } + $route_parameters = [ + 'migration_group' => $migration_group, + 'migration' => $migration->id(), + ]; + $row['label'] = [ + 'data' => [ + '#type' => 'link', + '#title' => $migration->label(), + '#url' => Url::fromRoute("entity.migration.overview", $route_parameters), + ], + ]; + $row['machine_name'] = $migration->id(); + $row['status'] = $migration->getStatusLabel(); } - $route_parameters = array( - 'migration_group' => $migration_group, - 'migration' => $migration->id(), - ); - $row['label'] = array( - 'data' => array( - '#type' => 'link', - '#title' => $migration->label(), - '#url' => Url::fromRoute("entity.migration.overview", $route_parameters), - ), - ); - $row['machine_name'] = $migration->id(); - $row['status'] = $migration->getStatusLabel(); - - // Derive the stats. - $source_plugin = $migration->getSourcePlugin(); - $row['total'] = $source_plugin->count(); - $map = $migration->getIdMap(); - $row['imported'] = $map->importedCount(); - // -1 indicates uncountable sources. - if ($row['total'] == -1) { - $row['total'] = $this->t('N/A'); - $row['unprocessed'] = $this->t('N/A'); + catch (PluginException $e) { + $this->logger->warning('Migration entity id %id is malformed: %orig', ['%id' => $migration_entity->id(), '%orig' => $e->getMessage()]); + return NULL; } - else { - $row['unprocessed'] = $row['total'] - $map->processedCount(); - } - $row['messages'] = array( - 'data' => array( - '#type' => 'link', - '#title' => $map->messageCount(), - '#url' => Url::fromRoute("migrate_tools.messages", $route_parameters), - ), - ); - $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); - $last_imported = $migrate_last_imported_store->get($migration->id(), FALSE); - if ($last_imported) { - /** @var DateFormatter $date_formatter */ - $date_formatter = \Drupal::service('date.formatter'); - $row['last_imported'] = $date_formatter->format($last_imported / 1000, - 'custom', 'Y-m-d H:i:s'); + + try { + // Derive the stats. + $source_plugin = $migration->getSourcePlugin(); + $row['total'] = $source_plugin->count(); + $map = $migration->getIdMap(); + $row['imported'] = $map->importedCount(); + // -1 indicates uncountable sources. + if ($row['total'] == -1) { + $row['total'] = $this->t('N/A'); + $row['unprocessed'] = $this->t('N/A'); + } + else { + $row['unprocessed'] = $row['total'] - $map->processedCount(); + } + $row['messages'] = [ + 'data' => [ + '#type' => 'link', + '#title' => $map->messageCount(), + '#url' => Url::fromRoute("migrate_tools.messages", $route_parameters), + ], + ]; + $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); + $last_imported = $migrate_last_imported_store->get($migration->id(), FALSE); + if ($last_imported) { + /** @var \Drupal\Core\Datetime\DateFormatter $date_formatter */ + $date_formatter = \Drupal::service('date.formatter'); + $row['last_imported'] = $date_formatter->format($last_imported / 1000, + 'custom', 'Y-m-d H:i:s'); + } + else { + $row['last_imported'] = ''; + } + + $row['operations']['data'] = [ + '#type' => 'dropbutton', + '#links' => [ + 'simple_form' => [ + 'title' => $this->t('Execute'), + 'url' => Url::fromRoute('migrate_tools.execute', [ + 'migration_group' => $migration_group, + 'migration' => $migration->id(), + ]), + ], + ], + ]; } - else { - $row['last_imported'] = ''; + catch (PluginException $e) { + // Derive the stats. + $row['status'] = $this->t('No data found'); + $row['total'] = $this->t('N/A'); + $row['imported'] = $this->t('N/A'); + $row['unprocessed'] = $this->t('N/A'); + $row['messages'] = $this->t('N/A'); + $row['last_imported'] = $this->t('N/A'); + $row['operations'] = $this->t('N/A'); } - return $row; // + parent::buildRow($migration_entity); - } - /** - * {@inheritdoc} - */ - public function getDefaultOperations(EntityInterface $entity) { - $operations = parent::getDefaultOperations($entity); - $migration_group = $entity->get('migration_group'); - if (!$migration_group) { - $migration_group = 'default'; - } -// $this->addGroupParameter($operations['edit']['url'], $migration_group); -// $this->addGroupParameter($operations['delete']['url'], $migration_group); - return $operations; + return $row; } /** + * Add group route parameter. + * * @param \Drupal\Core\Url $url * The URL associated with an operation. - * - * @param $migration_group + * @param string $migration_group * The migration's parent group. */ protected function addGroupParameter(Url $url, $migration_group) { diff --git a/web/modules/contrib/migrate_tools/src/Drush9LogMigrateMessage.php b/web/modules/contrib/migrate_tools/src/Drush9LogMigrateMessage.php new file mode 100644 index 000000000..0217a58e1 --- /dev/null +++ b/web/modules/contrib/migrate_tools/src/Drush9LogMigrateMessage.php @@ -0,0 +1,48 @@ +<?php + +namespace Drupal\migrate_tools; + +use Drupal\migrate\MigrateMessageInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; + +/** + * Print message in drush from migrate message. Drush 9 version. + * + * @package Drupal\migrate_tools + */ +class Drush9LogMigrateMessage implements MigrateMessageInterface, LoggerAwareInterface { + + use LoggerAwareTrait; + + /** + * The map between migrate status and drush log levels. + * + * @var array + */ + protected $map = [ + 'status' => 'notice', + ]; + + /** + * DrushLogMigrateMessage constructor. + */ + public function __construct(LoggerInterface $logger) { + $this->setLogger($logger); + } + + /** + * Output a message from the migration. + * + * @param string $message + * The message to display. + * @param string $type + * The type of message to display. + */ + public function display($message, $type = 'status') { + $type = isset($this->map[$type]) ? $this->map[$type] : $type; + $this->logger->log($type, $message); + } + +} diff --git a/web/modules/contrib/migrate_tools/src/DrushLogMigrateMessage.php b/web/modules/contrib/migrate_tools/src/DrushLogMigrateMessage.php index 9ed1d9fb0..4f7e92494 100644 --- a/web/modules/contrib/migrate_tools/src/DrushLogMigrateMessage.php +++ b/web/modules/contrib/migrate_tools/src/DrushLogMigrateMessage.php @@ -4,6 +4,11 @@ namespace Drupal\migrate_tools; use Drupal\migrate\MigrateMessageInterface; +/** + * Class DrushLogMigrateMessage. + * + * @package Drupal\migrate_tools + */ class DrushLogMigrateMessage implements MigrateMessageInterface { /** diff --git a/web/modules/contrib/migrate_tools/src/Form/MigrationAddForm.php b/web/modules/contrib/migrate_tools/src/Form/MigrationAddForm.php index 77cf0dd11..63c1fc5c3 100644 --- a/web/modules/contrib/migrate_tools/src/Form/MigrationAddForm.php +++ b/web/modules/contrib/migrate_tools/src/Form/MigrationAddForm.php @@ -30,7 +30,6 @@ class MigrationAddForm extends MigrationFormBase { */ protected function actions(array $form, FormStateInterface $form_state) { $actions = parent::actions($form, $form_state); -// $actions['submit']['#value'] = $this->t('Create Migration'); unset($actions['submit']); return $actions; } diff --git a/web/modules/contrib/migrate_tools/src/Form/MigrationDeleteForm.php b/web/modules/contrib/migrate_tools/src/Form/MigrationDeleteForm.php index b28fa9be3..593046dba 100644 --- a/web/modules/contrib/migrate_tools/src/Form/MigrationDeleteForm.php +++ b/web/modules/contrib/migrate_tools/src/Form/MigrationDeleteForm.php @@ -7,7 +7,7 @@ use Drupal\Core\Url; use Drupal\Core\Form\FormStateInterface; /** - * Class MigrationDeleteForm. + * Provides the delete form for our Migration entity. * * @package Drupal\migrate_tools\Form * @@ -22,9 +22,9 @@ class MigrationDeleteForm extends EntityConfirmFormBase { * Translated string. */ public function getQuestion() { - return $this->t('Are you sure you want to delete migration %label?', array( - '%label' => $this->entity->label(), - )); + return $this->t('Are you sure you want to delete migration %label?', [ + '%label' => $this->entity->label(), + ]); } /** @@ -60,9 +60,9 @@ class MigrationDeleteForm extends EntityConfirmFormBase { $this->entity->delete(); // Set a message that the entity was deleted. - drupal_set_message(t('Migration %label was deleted.', array( + drupal_set_message(t('Migration %label was deleted.', [ '%label' => $this->entity->label(), - ))); + ])); // Redirect the user to the list controller when complete. $form_state->setRedirectUrl($this->getCancelUrl()); diff --git a/web/modules/contrib/migrate_tools/src/Form/MigrationEditForm.php b/web/modules/contrib/migrate_tools/src/Form/MigrationEditForm.php index 144c74739..214d0ae35 100644 --- a/web/modules/contrib/migrate_tools/src/Form/MigrationEditForm.php +++ b/web/modules/contrib/migrate_tools/src/Form/MigrationEditForm.php @@ -6,8 +6,6 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; /** - * Class MigrationEditForm - * * Provides the edit form for our Migration entity. * * @package Drupal\migrate_tools\Form @@ -37,14 +35,15 @@ class MigrationEditForm extends MigrationFormBase { } /** + * Add group route parameter. + * * @param \Drupal\Core\Url $url * The URL associated with an operation. - * - * @param $migration_group + * @param string $migration_group * The migration's parent group. */ protected function addGroupParameter(Url $url, $migration_group) { - $route_parameters = $url->getRouteParameters() + array('migration_group' => $migration_group); + $route_parameters = $url->getRouteParameters() + ['migration_group' => $migration_group]; $url->setRouteParameters($route_parameters); } diff --git a/web/modules/contrib/migrate_tools/src/Form/MigrationExecuteForm.php b/web/modules/contrib/migrate_tools/src/Form/MigrationExecuteForm.php new file mode 100644 index 000000000..d05b736eb --- /dev/null +++ b/web/modules/contrib/migrate_tools/src/Form/MigrationExecuteForm.php @@ -0,0 +1,212 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\FormBase; +use Drupal\migrate\MigrateMessage; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; +use Drupal\migrate_tools\MigrateBatchExecutable; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * This form is specifically for configuring process pipelines. + */ +class MigrationExecuteForm extends FormBase { + + /** + * Plugin manager for migration plugins. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * Constructs a new MigrationExecuteForm object. + * + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager + * The plugin manager for config entity-based migrations. + */ + public function __construct(MigrationPluginManagerInterface $migration_plugin_manager) { + $this->migrationPluginManager = $migration_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.migration') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'migration_execute_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $form = []; + + $form['operations'] = $this->migrateMigrateOperations(); + + return $form; + } + + /** + * Get Operations. + */ + private function migrateMigrateOperations() { + // Build the 'Update options' form. + $form = [ + '#type' => 'fieldset', + '#title' => t('Operations'), + ]; + $options = [ + 'import' => t('Import'), + 'rollback' => t('Rollback'), + 'stop' => t('Stop'), + 'reset' => t('Reset'), + ]; + $form['operation'] = [ + '#type' => 'select', + '#title' => t('Choose an operation to run'), + '#options' => $options, + '#default_value' => 'import', + '#required' => TRUE, + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => t('Execute'), + ]; + $definitions = []; + $definitions[] = $this->t('Import: Imports all previously unprocessed records from the source, plus any records marked for update, into destination Drupal objects.'); + $definitions[] = $this->t('Rollback: Deletes all Drupal objects created by the import.'); + $definitions[] = $this->t('Stop: Cleanly interrupts any import or rollback processes that may currently be running.'); + $definitions[] = $this->t('Reset: Sometimes a process may fail to stop cleanly, and be left stuck in an Importing or Rolling Back status. Choose Reset to clear the status and permit other operations to proceed.'); + $form['definitions'] = [ + '#theme' => 'item_list', + '#title' => $this->t('Definitions'), + '#list_type' => 'ul', + '#items' => $definitions, + ]; + + $form['options'] = [ + '#type' => 'fieldset', + '#title' => t('Options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ]; + $form['options']['update'] = [ + '#type' => 'checkbox', + '#title' => t('Update'), + '#description' => t('Check this box to update all previously-imported content + in addition to importing new content. Leave unchecked to only import + new content'), + ]; + $form['options']['force'] = [ + '#type' => 'checkbox', + '#title' => t('Ignore dependencies'), + '#description' => t('Check this box to ignore dependencies when running imports + - all tasks will run whether or not their dependent tasks have + completed.'), + ]; + // @TODO: Limit is not working. Perhaps because of batch? See + // https://www.drupal.org/project/migrate_tools/issues/2924298. + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if (empty($form_state->getValue('operation'))) { + $form_state->setErrorByName('operation', $this->t('Please select an operation.')); + return; + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $operation = $form_state->getValue('operation'); + + if ($form_state->getValue('limit')) { + $limit = $form_state->getValue('limit'); + } + else { + $limit = 0; + } + + if ($form_state->getValue('update')) { + $update = $form_state->getValue('update'); + } + else { + $update = 0; + } + if ($form_state->getValue('force')) { + $force = $form_state->getValue('force'); + } + else { + $force = 0; + } + + $migration = \Drupal::routeMatch()->getParameter('migration'); + if ($migration) { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration_plugin */ + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $migrateMessage = new MigrateMessage(); + + switch ($operation) { + case 'import': + + $options = [ + 'limit' => $limit, + 'update' => $update, + 'force' => $force, + ]; + + $executable = new MigrateBatchExecutable($migration_plugin, $migrateMessage, $options); + $executable->batchImport(); + + break; + + case 'rollback': + + $options = [ + 'limit' => $limit, + 'update' => $update, + 'force' => $force, + ]; + + $executable = new MigrateBatchExecutable($migration_plugin, $migrateMessage, $options); + $executable->rollback(); + + break; + + case 'stop': + + $migration_plugin->interruptMigration(MigrationInterface::RESULT_STOPPED); + + break; + + case 'reset': + + $migration_plugin->setStatus(MigrationInterface::STATUS_IDLE); + + break; + + } + } + } + +} diff --git a/web/modules/contrib/migrate_tools/src/Form/MigrationFormBase.php b/web/modules/contrib/migrate_tools/src/Form/MigrationFormBase.php index 8eb8841cb..3ba6a0f55 100644 --- a/web/modules/contrib/migrate_tools/src/Form/MigrationFormBase.php +++ b/web/modules/contrib/migrate_tools/src/Form/MigrationFormBase.php @@ -7,7 +7,6 @@ use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Form\FormStateInterface; use Drupal\migrate_plus\Entity\MigrationGroup; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\migrate\Plugin\MigrationInterface; /** * Class MigrationFormBase. @@ -19,6 +18,8 @@ use Drupal\migrate\Plugin\MigrationInterface; class MigrationFormBase extends EntityForm { /** + * The entity query factory. + * * @var \Drupal\Core\Entity\Query\QueryFactory */ protected $entityQueryFactory; @@ -26,8 +27,8 @@ class MigrationFormBase extends EntityForm { /** * Construct the MigrationGroupFormBase. * - * For simple entity forms, there's no need for a constructor. Our migration form - * base, however, requires an entity query factory to be injected into it + * For simple entity forms, there's no need for a constructor. Our migration + * form base, however, requires an entity query factory to be injected into it * from the container. We later use this query factory to build an entity * query for the exists() method. * @@ -39,12 +40,7 @@ class MigrationFormBase extends EntityForm { } /** - * Factory method for MigrationFormBase. - * - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * A container interface service. - * - * @return \Drupal\migrate_tools\Form\MigrationGroupFormBase + * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static($container->get('entity.query')); @@ -67,34 +63,34 @@ class MigrationFormBase extends EntityForm { // Get anything we need from the base class. $form = parent::buildForm($form, $form_state); - /** @var MigrationInterface $migration */ + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ $migration = $this->entity; $form['warning'] = [ '#markup' => $this->t('Creating migrations is not yet supported. See <a href=":url">:url</a>', [ ':url' => 'https://www.drupal.org/node/2573241', - ]) + ]), ]; // Build the form. - $form['label'] = array( + $form['label'] = [ '#type' => 'textfield', '#title' => $this->t('Label'), '#maxlength' => 255, '#default_value' => $migration->label(), '#required' => TRUE, - ); - $form['id'] = array( + ]; + $form['id'] = [ '#type' => 'machine_name', '#title' => $this->t('Machine name'), '#default_value' => $migration->id(), - '#machine_name' => array( - 'exists' => array($this, 'exists'), + '#machine_name' => [ + 'exists' => [$this, 'exists'], 'replace_pattern' => '([^a-z0-9_]+)|(^custom$)', 'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".', - ), + ], '#disabled' => !$migration->isNew(), - ); + ]; $groups = MigrationGroup::loadMultiple(); $group_options = []; @@ -105,14 +101,14 @@ class MigrationFormBase extends EntityForm { $migration->set('migration_group', 'default'); } - $form['migration_group'] = array( + $form['migration_group'] = [ '#type' => 'select', '#title' => $this->t('Migration Group'), '#empty_value' => '', '#default_value' => $migration->get('migration_group'), '#options' => $group_options, '#description' => $this->t('Assign this migration to an existing group.'), - ); + ]; return $form; } @@ -124,7 +120,7 @@ class MigrationFormBase extends EntityForm { * The entity ID. * @param array $element * The form element. - * @param FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. * * @return bool @@ -165,14 +161,7 @@ class MigrationFormBase extends EntityForm { } /** - * Overrides Drupal\Core\Entity\EntityFormController::save(). - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * An associative array containing the current state of the form. - * - * @return $this + * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { $migration = $this->getEntity(); @@ -180,16 +169,16 @@ class MigrationFormBase extends EntityForm { if ($status == SAVED_UPDATED) { // If we edited an existing entity... - drupal_set_message($this->t('Migration %label has been updated.', array('%label' => $migration->label()))); + drupal_set_message($this->t('Migration %label has been updated.', ['%label' => $migration->label()])); } else { // If we created a new entity... - drupal_set_message($this->t('Migration %label has been added.', array('%label' => $migration->label()))); + drupal_set_message($this->t('Migration %label has been added.', ['%label' => $migration->label()])); } // Redirect the user back to the listing route after the save operation. $form_state->setRedirect('entity.migration.list', - array('migration_group' => $migration->get('migration_group'))); + ['migration_group' => $migration->get('migration_group')]); } } diff --git a/web/modules/contrib/migrate_tools/src/Form/MigrationGroupDeleteForm.php b/web/modules/contrib/migrate_tools/src/Form/MigrationGroupDeleteForm.php index ac047f83d..98d1baf70 100644 --- a/web/modules/contrib/migrate_tools/src/Form/MigrationGroupDeleteForm.php +++ b/web/modules/contrib/migrate_tools/src/Form/MigrationGroupDeleteForm.php @@ -7,7 +7,7 @@ use Drupal\Core\Url; use Drupal\Core\Form\FormStateInterface; /** - * Class MigrationGroupDeleteForm. + * Provides the delete form for our Migration Group entity. * * @package Drupal\migrate_tools\Form * @@ -22,9 +22,9 @@ class MigrationGroupDeleteForm extends EntityConfirmFormBase { * Translated string. */ public function getQuestion() { - return $this->t('Are you sure you want to delete migration group %label?', array( - '%label' => $this->entity->label(), - )); + return $this->t('Are you sure you want to delete migration group %label?', [ + '%label' => $this->entity->label(), + ]); } /** @@ -60,9 +60,9 @@ class MigrationGroupDeleteForm extends EntityConfirmFormBase { $this->entity->delete(); // Set a message that the entity was deleted. - drupal_set_message(t('Migration group %label was deleted.', array( + drupal_set_message(t('Migration group %label was deleted.', [ '%label' => $this->entity->label(), - ))); + ])); // Redirect the user to the list controller when complete. $form_state->setRedirectUrl($this->getCancelUrl()); diff --git a/web/modules/contrib/migrate_tools/src/Form/MigrationGroupEditForm.php b/web/modules/contrib/migrate_tools/src/Form/MigrationGroupEditForm.php index ee52359cd..1eedc2d26 100644 --- a/web/modules/contrib/migrate_tools/src/Form/MigrationGroupEditForm.php +++ b/web/modules/contrib/migrate_tools/src/Form/MigrationGroupEditForm.php @@ -5,8 +5,6 @@ namespace Drupal\migrate_tools\Form; use Drupal\Core\Form\FormStateInterface; /** - * Class MigrationGroupEditForm - * * Provides the edit form for our Migration Group entity. * * @package Drupal\migrate_tools\Form diff --git a/web/modules/contrib/migrate_tools/src/Form/MigrationGroupFormBase.php b/web/modules/contrib/migrate_tools/src/Form/MigrationGroupFormBase.php index 30229fe27..d60944a75 100644 --- a/web/modules/contrib/migrate_tools/src/Form/MigrationGroupFormBase.php +++ b/web/modules/contrib/migrate_tools/src/Form/MigrationGroupFormBase.php @@ -6,7 +6,6 @@ use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Form\FormStateInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\migrate_plus\Entity\MigrationGroupInterface; /** * Class MigrationGroupFormBase. @@ -18,6 +17,8 @@ use Drupal\migrate_plus\Entity\MigrationGroupInterface; class MigrationGroupFormBase extends EntityForm { /** + * The query factory service. + * * @var \Drupal\Core\Entity\Query\QueryFactory */ protected $entityQueryFactory; @@ -25,10 +26,10 @@ class MigrationGroupFormBase extends EntityForm { /** * Construct the MigrationGroupFormBase. * - * For simple entity forms, there's no need for a constructor. Our migration group form - * base, however, requires an entity query factory to be injected into it - * from the container. We later use this query factory to build an entity - * query for the exists() method. + * For simple entity forms, there's no need for a constructor. Our migration + * group form base, however, requires an entity query factory to be injected + * into it from the container. We later use this query factory to build an + * entity query for the exists() method. * * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory * An entity query factory for the migration group entity type. @@ -38,13 +39,7 @@ class MigrationGroupFormBase extends EntityForm { } /** - * Factory method for MigrationGroupFormBase. - * - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * A container interface service. - * - * @return \Drupal\migrate_tools\Form\MigrationFormBase - * + * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static($container->get('entity.query')); @@ -67,41 +62,41 @@ class MigrationGroupFormBase extends EntityForm { // Get anything we need from the base class. $form = parent::buildForm($form, $form_state); - /** @var MigrationGroupInterface $migration_group */ + /** @var \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group */ $migration_group = $this->entity; // Build the form. - $form['label'] = array( + $form['label'] = [ '#type' => 'textfield', '#title' => $this->t('Label'), '#maxlength' => 255, '#default_value' => $migration_group->label(), '#required' => TRUE, - ); - $form['id'] = array( + ]; + $form['id'] = [ '#type' => 'machine_name', '#title' => $this->t('Machine name'), '#default_value' => $migration_group->id(), - '#machine_name' => array( - 'exists' => array($this, 'exists'), + '#machine_name' => [ + 'exists' => [$this, 'exists'], 'replace_pattern' => '([^a-z0-9_]+)|(^custom$)', 'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".', - ), + ], '#disabled' => !$migration_group->isNew(), - ); - $form['description'] = array( + ]; + $form['description'] = [ '#type' => 'textfield', '#title' => $this->t('Description'), '#maxlength' => 255, '#default_value' => $migration_group->get('description'), - ); - $form['source_type'] = array( + ]; + $form['source_type'] = [ '#type' => 'textfield', '#title' => $this->t('Source type'), '#description' => $this->t('Type of source system the group is migrating from, for example "Drupal 6" or "WordPress 4".'), '#maxlength' => 255, '#default_value' => $migration_group->get('source_type'), - ); + ]; // Return the form. return $form; @@ -114,7 +109,7 @@ class MigrationGroupFormBase extends EntityForm { * The entity ID. * @param array $element * The form element. - * @param FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. * * @return bool @@ -155,14 +150,7 @@ class MigrationGroupFormBase extends EntityForm { } /** - * Overrides Drupal\Core\Entity\EntityFormController::save(). - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * An associative array containing the current state of the form. - * - * @return $this + * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { $migration_group = $this->getEntity(); @@ -170,11 +158,11 @@ class MigrationGroupFormBase extends EntityForm { if ($status == SAVED_UPDATED) { // If we edited an existing entity... - drupal_set_message($this->t('Migration group %label has been updated.', array('%label' => $migration_group->label()))); + drupal_set_message($this->t('Migration group %label has been updated.', ['%label' => $migration_group->label()])); } else { // If we created a new entity... - drupal_set_message($this->t('Migration group %label has been added.', array('%label' => $migration_group->label()))); + drupal_set_message($this->t('Migration group %label has been added.', ['%label' => $migration_group->label()])); } // Redirect the user back to the listing route after the save operation. diff --git a/web/modules/contrib/migrate_tools/src/Form/SourceCsvForm.php b/web/modules/contrib/migrate_tools/src/Form/SourceCsvForm.php new file mode 100644 index 000000000..40b5dc3ba --- /dev/null +++ b/web/modules/contrib/migrate_tools/src/Form/SourceCsvForm.php @@ -0,0 +1,453 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\TempStore\PrivateTempStoreFactory; +use Drupal\Core\TempStore\TempStoreException; +use Drupal\Core\Url; +use Drupal\migrate_plus\Entity\MigrationInterface; +use Drupal\migrate_source_csv\Plugin\migrate\source\CSV; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; + +/** + * Provides an edit form for CSV source plugin column_names configuration. + * + * This means you can tell the migration which columns your data is in and no + * longer edit the CSV to fit the column order set in the migration or edit the + * migration yml itself. + * + * Changes made to the column configuration, or aliases, are stored in the + * private migrate_toools private store keyed by the migration plugin id. The + * data stored for each migrations consists of two arrays, the 'original' column + * aliases and the 'updated' column aliases. + * + * An addtional list of all changed migration id is kept in the store, in the + * key 'migrations_changed' + * + * Private Store Usage: + * migrations_changed: An array of the ids of the migrations that have been + * changed: + * [migration_id]: The original and changed values for this column assignments + * + * Format of the source configuration saved in the store. + * @code + * migration_id + * original + * column_index1 + * property 1 => label 1 + * column_index2 + * property 2 => label 2 + * updated + * column_index1 + * property 2 => label 2 + * column_index2 + * property 1 => label 1 + * @endcode + * + * Example source configuration. + * @code + * custom_migration + * original + * 2 + * title => title + * 3 + * body => foo + * updated + * 8 + * title => new_title + * 9 + * body => new_body + * @endcode + */ +class SourceCsvForm extends FormBase { + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * Plugin manager for migration plugins. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * The messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * Temporary store for column assignment changes. + * + * @var \Drupal\Core\TempStore\PrivateTempStoreFactory + */ + protected $store; + + /** + * The file object that reads the CSV file. + * + * @var \SplFileObject + */ + protected $file = NULL; + + /** + * The migration being examined. + * + * @var \Drupal\migrate\Plugin\MigrationInterface + */ + protected $migration; + + /** + * The migration plugin id. + * + * @var string + */ + protected $id; + + /** + * The array of columns names from the CSV source plugin. + * + * @var array + */ + protected $columnNames; + + /** + * An array of options for the column select form field.. + * + * @var array + */ + protected $options; + + /** + * An array of modified and original column_name source plugin configuration. + * + * @var array + */ + protected $sourceConfiguration; + + /** + * Constructs new SourceCsvForm object. + * + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager + * The plugin manager for config entity-based migrations. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. + * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $private_store + * The private store. + */ + public function __construct(Connection $connection, MigrationPluginManagerInterface $migration_plugin_manager, MessengerInterface $messenger, PrivateTempStoreFactory $private_store) { + $this->connection = $connection; + $this->migrationPluginManager = $migration_plugin_manager; + $this->messenger = $messenger; + $this->store = $private_store->get('migrate_tools'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('database'), + $container->get('plugin.manager.migration'), + $container->get('messenger'), + $container->get('tempstore.private') + ); + } + + /** + * A custom access check. + * + * @param \Drupal\Core\Session\AccountInterface $account + * Run access checks for this account. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return \Drupal\Core\Access\AccessResult + * Allowed or forbidden, neutral if tempstore is empty. + */ + public function access(AccountInterface $account, MigrationInterface $migration) { + try { + $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + } + catch (PluginException $e) { + return AccessResult::forbidden(); + } + + if ($this->migration) { + if ($source = $this->migration->getSourcePlugin()) { + if (is_a($source, CSV::class)) { + return AccessResult::allowed(); + } + } + } + return AccessResult::forbidden(); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, MigrationInterface $migration = NULL) { + try { + // @TODO: remove this horrible config work around after + // https://www.drupal.org/project/drupal/issues/2986665 is fixed. + $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + /** @var \Drupal\migrate_source_csv\Plugin\migrate\source\CSV $source */ + $source = $this->migration->getSourcePlugin(); + $source->setConfiguration($migration->toArray()['source']); + } + catch (PluginException $e) { + return AccessResult::forbidden(); + } + + // Get the source file after the properties are initialized. + $source->initializeIterator(); + $this->file = $source->getFile(); + + // Set the input field options to the header row values or, if there are + // no such values, use an indexed array. + if ($this->file->getHeaderRowCount() > 0) { + $this->options = $this->getHeaderColumnNames(); + } + else { + for ($i = 0; $i < $this->getFileColumnCount(); $i++) { + $this->options[$i] = $i; + } + } + + // Set the store key to the migration id. + $this->id = $this->migration->getPluginId(); + + // Get the column names from the file or from the store, if updated + // values are in the store. + $this->sourceConfiguration = $this->store->get($this->id); + if (isset($this->sourceConfiguration['changed'])) { + if ($config = $this->sourceConfiguration['changed']) { + $this->columnNames = $config; + } + } + else { + // Get the calculated column names. This is either the header rows or + // the configuration column_name value. + $this->columnNames = $this->file->getColumnNames(); + if (!isset($this->sourceConfiguration['original'])) { + // Save as the original values. + $this->sourceConfiguration['original'] = $this->columnNames; + $this->store->set($this->id, $this->sourceConfiguration); + } + } + $form['#title'] = $this->t('Column Aliases'); + + $form['heading'] = [ + '#type' => 'item', + '#title' => $this->t(':label', [':label' => $this->migration->label()]), + '#description' => '<p>' . $this->t('You can change the columns to be used by this migration for each source property.') . '</p>', + ]; + // Create a form field for each column in this migration. + foreach ($this->columnNames as $index => $data) { + $property_name = key($data); + $default_value = $index; + $label = $this->getLabel($this->sourceConfiguration['original'], $property_name); + + $description = $this->t('Select the column where the data for <em>:label</em>, property <em>:property</em>, will be found.', [ + ':label' => $label, + ':property' => $property_name, + ]); + $form['aliases'][$property_name] = [ + '#type' => 'select', + '#title' => $label, + '#description' => $description, + '#options' => $this->options, + '#default_value' => $default_value, + ]; + } + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#button_type' => 'primary', + '#value' => $this->t('Submit'), + ]; + $form['actions']['cancel'] = [ + '#type' => 'submit', + '#value' => $this->t('Cancel'), + '#submit' => ['::cancel'], + '#limit_validation_errors' => [], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // Display an error message if two properties have the same source column. + $values = []; + foreach ($this->columnNames as $index => $data) { + $property_name = key($data); + $value = $form_state->getValue($property_name); + if (in_array($value, $values)) { + $form_state->setErrorByName($property_name, $this->t('Source properties can not share the same source column.')); + } + $values[] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Create a new column_names configuration. + $new_column_names = []; + foreach ($this->columnNames as $index => $data) { + // Keep the property name as it is used in the process pipeline. + $property_name = key($data); + // Get the new column number from the form alias field for this property. + $new_index = $form_state->getValue($property_name); + // Get the new label from the options array. + $new_label = $this->options[$new_index]; + // Save using the new column number and new label. + $new_column_names[$new_index] = [$property_name => $new_label]; + } + // Update the file columns. + $this->file->setColumnNames($new_column_names); + // Save as updated in the store. + $this->sourceConfiguration['changed'] = $new_column_names; + $this->store->set($this->id, $this->sourceConfiguration); + + $changed = ($this->store->get('migrations_changed')) ? $this->store->get('migrations_changed') : []; + if (!in_array($this->id, $changed)) { + $changed[] = $this->id; + $this->store->set('migrations_changed', $changed); + } + } + + /** + * Form submission handler for the 'cancel' action. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function cancel(array $form, FormStateInterface $form_state) { + // Restore the file columns to the original settings. + $this->file->setColumnNames($this->sourceConfiguration['original']); + // Remove this migration from the store. + try { + $this->store->delete($this->id); + } + catch (TempStoreException $e) { + $this->messenger->addError($e->getMessage()); + } + + $migrationsChanged = $this->store->get('migrations_changed'); + unset($migrationsChanged[$this->id]); + try { + $this->store->set('migrations_changed', $migrationsChanged); + } + catch (TempStoreException $e) { + $this->messenger->addError($e->getMessage()); + } + $form_state->setRedirect('entity.migration_group.list'); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'migrate_tools_source_csv'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() {} + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.migration_group.list'); + } + + /** + * Returns the header row. + * + * Use a new file handle so that CSVFileObject::current() is not executed. + * + * @return array + * The header row. + */ + public function getHeaderColumnNames() { + $row = []; + $fname = $this->file->getPathname(); + $handle = fopen($fname, 'r'); + if ($handle) { + fseek($handle, $this->file->getHeaderRowCount() - 1); + $row = fgetcsv($handle); + fclose($handle); + } + return $row; + } + + /** + * Returns the count of fields in the header row. + * + * Use a new file handle so that CSVFileObject::current() is not executed. + * + * @return int + * The number of fields in the header row. + */ + public function getFileColumnCount() { + $count = 0; + $fname = $this->file->getPathname(); + $handle = fopen($fname, 'r'); + if ($handle) { + $row = fgetcsv($handle); + $count = count($row); + fclose($handle); + } + return $count; + } + + /** + * Gets the label for a given property from a column_names array. + * + * @param array $column_names + * An array of column_names. + * @param string $property_name + * The property name to find a label for. + * + * @return string + * The label for this property. + */ + protected function getLabel(array $column_names, $property_name) { + $label = ''; + foreach ($column_names as $column) { + foreach ($column as $key => $value) { + if ($key === $property_name) { + $label = $value; + break; + } + } + } + return $label; + } + +} diff --git a/web/modules/contrib/migrate_tools/src/MigrateBatchExecutable.php b/web/modules/contrib/migrate_tools/src/MigrateBatchExecutable.php new file mode 100644 index 000000000..ee24f64d7 --- /dev/null +++ b/web/modules/contrib/migrate_tools/src/MigrateBatchExecutable.php @@ -0,0 +1,295 @@ +<?php + +namespace Drupal\migrate_tools; + +use Drupal\migrate\MigrateMessage; +use Drupal\migrate\MigrateMessageInterface; +use Drupal\migrate\Plugin\Migration; +use Drupal\migrate\Plugin\MigrationInterface; + +/** + * Defines a migrate executable class for batch migrations through UI. + */ +class MigrateBatchExecutable extends MigrateExecutable { + + /** + * Representing a batch import operation. + */ + const BATCH_IMPORT = 1; + + /** + * Indicates if we need to update existing rows or skip them. + * + * @var int + */ + protected $updateExistingRows = 0; + + /** + * Indicates if we need import dependent migrations also. + * + * @var int + */ + protected $checkDependencies = 0; + + /** + * The current batch context. + * + * @var array + */ + protected $batchContext = []; + + /** + * Plugin manager for migration plugins. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * {@inheritdoc} + */ + public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, array $options = []) { + + if (isset($options['update'])) { + $this->updateExistingRows = $options['update']; + } + + if (isset($options['force'])) { + $this->checkDependencies = $options['force']; + } + + parent::__construct($migration, $message, $options); + $this->migrationPluginManager = \Drupal::getContainer()->get('plugin.manager.migration'); + } + + /** + * Sets the current batch content so listeners can update the messages. + * + * @param array $context + * The batch context. + */ + public function setBatchContext(array &$context) { + $this->batchContext = &$context; + } + + /** + * Gets a reference to the current batch context. + * + * @return array + * The batch context. + */ + public function &getBatchContext() { + return $this->batchContext; + } + + /** + * Setup batch operations for running the migration. + */ + public function batchImport() { + // Create the batch operations for each migration that needs to be executed. + // This includes the migration for this executable, but also the dependent + // migrations. + $operations = $this->batchOperations([$this->migration], 'import', [ + 'limit' => $this->itemLimit, + 'update' => $this->updateExistingRows, + 'force' => $this->checkDependencies, + ]); + + if (count($operations) > 0) { + $batch = [ + 'operations' => $operations, + 'title' => t('Migrating %migrate', ['%migrate' => $this->migration->label()]), + 'init_message' => t('Start migrating %migrate', ['%migrate' => $this->migration->label()]), + 'progress_message' => t('Migrating %migrate', ['%migrate' => $this->migration->label()]), + 'error_message' => t('An error occurred while migrating %migrate.', ['%migrate' => $this->migration->label()]), + 'finished' => '\Drupal\migrate_tools\MigrateBatchExecutable::batchFinishedImport', + ]; + + batch_set($batch); + } + } + + /** + * Helper to generate the batch operations for importing migrations. + * + * @param \Drupal\migrate\Plugin\MigrationInterface[] $migrations + * The migrations. + * @param string $operation + * The batch operation to perform. + * @param array $options + * The migration options. + * + * @return array + * The batch operations to perform. + */ + protected function batchOperations(array $migrations, $operation, array $options = []) { + $operations = []; + foreach ($migrations as $id => $migration) { + + if (!empty($options['update'])) { + $migration->getIdMap()->prepareUpdate(); + } + + if (!empty($options['force'])) { + $migration->set('requirements', []); + } + else { + $dependencies = $migration->getMigrationDependencies(); + if (!empty($dependencies['required'])) { + $required_migrations = $this->migrationPluginManager->createInstances($dependencies['required']); + // For dependent migrations will need to be migrate all items. + $dependent_options = $options; + $dependent_options['limit'] = 0; + $operations += $this->batchOperations($required_migrations, $operation, [ + 'limit' => 0, + 'update' => $options['update'], + 'force' => $options['force'], + ]); + } + } + + $operations[] = [ + '\Drupal\migrate_tools\MigrateBatchExecutable::batchProcessImport', + [$migration->id(), $options], + ]; + } + + return $operations; + } + + /** + * Batch 'operation' callback. + * + * @param string $migration_id + * The migration id. + * @param array $options + * The batch executable options. + * @param array $context + * The sandbox context. + */ + public static function batchProcessImport($migration_id, array $options, array &$context) { + if (empty($context['sandbox'])) { + $context['finished'] = 0; + $context['sandbox'] = []; + $context['sandbox']['total'] = 0; + $context['sandbox']['counter'] = 0; + $context['sandbox']['batch_limit'] = 0; + $context['sandbox']['operation'] = MigrateBatchExecutable::BATCH_IMPORT; + } + + // Prepare the migration executable. + $message = new MigrateMessage(); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = \Drupal::getContainer()->get('plugin.manager.migration')->createInstance($migration_id); + $executable = new MigrateBatchExecutable($migration, $message, $options); + + if (empty($context['sandbox']['total'])) { + $context['sandbox']['total'] = $executable->getSource()->count(); + $context['sandbox']['batch_limit'] = $executable->calculateBatchLimit($context); + $context['results'][$migration->id()] = [ + '@numitems' => 0, + '@created' => 0, + '@updated' => 0, + '@failures' => 0, + '@ignored' => 0, + '@name' => $migration->id(), + ]; + } + + // Every iteration, we reset out batch counter. + $context['sandbox']['batch_counter'] = 0; + + // Make sure we know our batch context. + $executable->setBatchContext($context); + + // Do the import. + $result = $executable->import(); + + // Store the result; will need to combine the results of all our iterations. + $context['results'][$migration->id()] = [ + '@numitems' => $context['results'][$migration->id()]['@numitems'] + $executable->getProcessedCount(), + '@created' => $context['results'][$migration->id()]['@created'] + $executable->getCreatedCount(), + '@updated' => $context['results'][$migration->id()]['@updated'] + $executable->getUpdatedCount(), + '@failures' => $context['results'][$migration->id()]['@failures'] + $executable->getFailedCount(), + '@ignored' => $context['results'][$migration->id()]['@ignored'] + $executable->getIgnoredCount(), + '@name' => $migration->id(), + ]; + + // Do some housekeeping. + if ( + $result != MigrationInterface::RESULT_INCOMPLETE + ) { + $context['finished'] = 1; + } + else { + $context['sandbox']['counter'] = $context['results'][$migration->id()]['@numitems']; + if ($context['sandbox']['counter'] <= $context['sandbox']['total']) { + $context['finished'] = ((float) $context['sandbox']['counter'] / (float) $context['sandbox']['total']); + $context['message'] = t('Importing %migration (@percent%).', [ + '%migration' => $migration->label(), + '@percent' => (int) ($context['finished'] * 100), + ]); + } + } + + } + + /** + * Finished callback for import batches. + * + * @param bool $success + * A boolean indicating whether the batch has completed successfully. + * @param array $results + * The value set in $context['results'] by callback_batch_operation(). + * @param array $operations + * If $success is FALSE, contains the operations that remained unprocessed. + */ + public static function batchFinishedImport($success, array $results, array $operations) { + if ($success) { + foreach ($results as $migration_id => $result) { + $singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'"; + $plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'"; + drupal_set_message(\Drupal::translation()->formatPlural($result['@numitems'], + $singular_message, + $plural_message, + $result)); + } + } + } + + /** + * {@inheritdoc} + */ + public function checkStatus() { + $status = parent::checkStatus(); + + if ($status == MigrationInterface::RESULT_COMPLETED) { + // Do some batch housekeeping. + $context = $this->getBatchContext(); + + if (!empty($context['sandbox']) && $context['sandbox']['operation'] == MigrateBatchExecutable::BATCH_IMPORT) { + $context['sandbox']['batch_counter']++; + if ($context['sandbox']['batch_counter'] >= $context['sandbox']['batch_limit']) { + $status = MigrationInterface::RESULT_INCOMPLETE; + } + } + } + + return $status; + } + + /** + * Calculates how much a single batch iteration will handle. + * + * @param array $context + * The sandbox context. + * + * @return float + * The batch limit. + */ + public function calculateBatchLimit(array $context) { + // TODO Maybe we need some other more sophisticated logic here? + return ceil($context['sandbox']['total'] / 100); + } + +} diff --git a/web/modules/contrib/migrate_tools/src/MigrateExecutable.php b/web/modules/contrib/migrate_tools/src/MigrateExecutable.php index 4532a44b5..2282ab8cb 100644 --- a/web/modules/contrib/migrate_tools/src/MigrateExecutable.php +++ b/web/modules/contrib/migrate_tools/src/MigrateExecutable.php @@ -17,6 +17,9 @@ use Drupal\migrate\Event\MigrateMapDeleteEvent; use Drupal\migrate\Event\MigrateImportEvent; use Drupal\migrate_plus\Event\MigratePrepareRowEvent; +/** + * Defines a migrate executable class for drush. + */ class MigrateExecutable extends MigrateExecutableBase { /** @@ -25,12 +28,19 @@ class MigrateExecutable extends MigrateExecutableBase { * @var array * Set of counters, keyed by MigrateIdMapInterface::STATUS_* constant. */ - protected $saveCounters = array( + protected $saveCounters = [ MigrateIdMapInterface::STATUS_FAILED => 0, MigrateIdMapInterface::STATUS_IGNORED => 0, MigrateIdMapInterface::STATUS_IMPORTED => 0, MigrateIdMapInterface::STATUS_NEEDS_UPDATE => 0, - ); + ]; + + /** + * Counter of map saves, used to detect the item limit threshold. + * + * @var int + */ + protected $itemLimitCounter = 0; /** * Counter of map deletions. @@ -40,8 +50,9 @@ class MigrateExecutable extends MigrateExecutableBase { protected $deleteCounter = 0; /** - * Maximum number of items to process in this migration. 0 indicates no limit - * is to be applied. + * Maximum number of items to process in this migration. + * + * 0 indicates no limit is to be applied. * * @var int */ @@ -63,6 +74,7 @@ class MigrateExecutable extends MigrateExecutableBase { /** * Count of number of items processed so far in this migration. + * * @var int */ protected $counter = 0; @@ -93,10 +105,12 @@ class MigrateExecutable extends MigrateExecutableBase { $this->feedback = $options['feedback']; } if (isset($options['idlist'])) { - $this->idlist = explode(',', $options['idlist']); - array_walk($this->idlist , function(&$value, $key) { - $value = explode(':', $value); - }); + if (is_string($options['idlist'])) { + $this->idlist = explode(',', $options['idlist']); + array_walk($this->idlist, function (&$value, $key) { + $value = explode(':', $value); + }); + } } $this->listeners[MigrateEvents::MAP_SAVE] = [$this, 'onMapSave']; @@ -121,6 +135,7 @@ class MigrateExecutable extends MigrateExecutableBase { // Only count saves for this migration. if ($event->getMap()->getQualifiedMapTableName() == $this->migration->getIdMap()->getQualifiedMapTableName()) { $fields = $event->getFields(); + $this->itemLimitCounter++; // Distinguish between creation and update. if ($fields['source_row_status'] == MigrateIdMapInterface::STATUS_IMPORTED && $this->preExistingItem @@ -147,6 +162,7 @@ class MigrateExecutable extends MigrateExecutableBase { * Return the number of items created. * * @return int + * The number of items created. */ public function getCreatedCount() { return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED]; @@ -156,6 +172,7 @@ class MigrateExecutable extends MigrateExecutableBase { * Return the number of items updated. * * @return int + * The updated count. */ public function getUpdatedCount() { return $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]; @@ -165,6 +182,7 @@ class MigrateExecutable extends MigrateExecutableBase { * Return the number of items ignored. * * @return int + * The ignored count. */ public function getIgnoredCount() { return $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED]; @@ -174,17 +192,20 @@ class MigrateExecutable extends MigrateExecutableBase { * Return the number of items that failed. * * @return int + * The failed count. */ public function getFailedCount() { return $this->saveCounters[MigrateIdMapInterface::STATUS_FAILED]; } /** - * Return the total number of items processed. Note that STATUS_NEEDS_UPDATE - * is not counted, since this is typically set on stubs created as side - * effects, not on the primary item being imported. + * Return the total number of items processed. + * + * Note that STATUS_NEEDS_UPDATE is not counted, since this is typically set + * on stubs created as side effects, not on the primary item being imported. * * @return int + * The processed count. */ public function getProcessedCount() { return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED] + @@ -197,6 +218,7 @@ class MigrateExecutable extends MigrateExecutableBase { * Return the number of items rolled back. * * @return int + * The rollback count. */ public function getRollbackCount() { return $this->deleteCounter; @@ -235,10 +257,12 @@ class MigrateExecutable extends MigrateExecutableBase { } /** - * Emit information on what we've done since the last feedback (or the - * beginning of this migration). + * Emit information on what we've done. + * + * Either since the last feedback or the beginning of this migration. * * @param bool $done + * TRUE if this is the last items to process. Otherwise FALSE. */ protected function progressMessage($done = TRUE) { $processed = $this->getProcessedCount(); @@ -252,12 +276,15 @@ class MigrateExecutable extends MigrateExecutableBase { } $this->message->display(\Drupal::translation()->formatPlural($processed, $singular_message, $plural_message, - array('@numitems' => $processed, - '@created' => $this->getCreatedCount(), - '@updated' => $this->getUpdatedCount(), - '@failures' => $this->getFailedCount(), - '@ignored' => $this->getIgnoredCount(), - '@name' => $this->migration->id()))); + [ + '@numitems' => $processed, + '@created' => $this->getCreatedCount(), + '@updated' => $this->getUpdatedCount(), + '@failures' => $this->getFailedCount(), + '@ignored' => $this->getIgnoredCount(), + '@name' => $this->migration->id(), + ] + )); } /** @@ -272,26 +299,31 @@ class MigrateExecutable extends MigrateExecutableBase { } /** - * Emit information on what we've done since the last feedback (or the - * beginning of this migration). + * Emit information on what we've done. + * + * Either since the last feedback or the beginning of this migration. * * @param bool $done + * TRUE if this is the last items to rollback. Otherwise FALSE. */ protected function rollbackMessage($done = TRUE) { - $rolled_back = $this->getRollbackCount(); - if ($done) { - $singular_message = "Rolled back 1 item - done with '@name'"; - $plural_message = "Rolled back @numitems items - done with '@name'"; - } - else { - $singular_message = "Rolled back 1 item - continuing with '@name'"; - $plural_message = "Rolled back @numitems items - continuing with '@name'"; - } - $this->message->display(\Drupal::translation()->formatPlural($rolled_back, - $singular_message, $plural_message, - array('@numitems' => $rolled_back, - '@name' => $this->migration->id()))); - } + $rolled_back = $this->getRollbackCount(); + if ($done) { + $singular_message = "Rolled back 1 item - done with '@name'"; + $plural_message = "Rolled back @numitems items - done with '@name'"; + } + else { + $singular_message = "Rolled back 1 item - continuing with '@name'"; + $plural_message = "Rolled back @numitems items - continuing with '@name'"; + } + $this->message->display(\Drupal::translation()->formatPlural($rolled_back, + $singular_message, $plural_message, + [ + '@numitems' => $rolled_back, + '@name' => $this->migration->id(), + ] + )); + } /** * React to an item about to be imported. @@ -329,20 +361,18 @@ class MigrateExecutable extends MigrateExecutableBase { * The prepare-row event. * * @throws \Drupal\migrate\MigrateSkipRowException - * */ public function onPrepareRow(MigratePrepareRowEvent $event) { if (!empty($this->idlist)) { $row = $event->getRow(); - /** - * @TODO replace for $source_id = $row->getSourceIdValues(); when https://www.drupal.org/node/2698023 is fixed - */ + // TODO: replace for $source_id = $row->getSourceIdValues(); + // when https://www.drupal.org/node/2698023 is fixed. $migration = $event->getMigration(); $source_id = array_merge(array_flip(array_keys($migration->getSourcePlugin() ->getIds())), $row->getSourceIdValues()); $skip = TRUE; foreach ($this->idlist as $item) { - if (array_values($source_id) === $item) { + if (array_values($source_id) == $item) { $skip = FALSE; break; } @@ -356,7 +386,7 @@ class MigrateExecutable extends MigrateExecutableBase { $this->resetCounters(); } $this->counter++; - if ($this->itemLimit && ($this->getProcessedCount() + 1) >= $this->itemLimit) { + if ($this->itemLimit && ($this->itemLimitCounter + 1) >= $this->itemLimit) { $event->getMigration()->interruptMigration(MigrationInterface::RESULT_COMPLETED); } diff --git a/web/modules/contrib/migrate_tools/src/Routing/RouteProcessor.php b/web/modules/contrib/migrate_tools/src/Routing/RouteProcessor.php new file mode 100644 index 000000000..fe0c5e623 --- /dev/null +++ b/web/modules/contrib/migrate_tools/src/Routing/RouteProcessor.php @@ -0,0 +1,27 @@ +<?php + +namespace Drupal\migrate_tools\Routing; + +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface; +use Symfony\Component\Routing\Route; + +/** + * Route processor to expand migrate_group. + */ +class RouteProcessor implements OutboundRouteProcessorInterface { + + /** + * {@inheritdoc} + */ + public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { + if ($route->hasDefault('_migrate_group')) { + if ($migration = \Drupal::entityTypeManager()->getStorage('migration')->load($parameters['migration'])) { + if ($group = $migration->get('migration_group')) { + $parameters['migration_group'] = $group; + } + } + } + } + +} diff --git a/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml b/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml new file mode 100644 index 000000000..0d600a9ff --- /dev/null +++ b/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml @@ -0,0 +1,33 @@ +langcode: en +status: true +dependencies: { } +id: csv_source_test +label: Test edit of column aliases for CSV source plugin +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: { } +migration_group: csv_test +source: + plugin: csv + path: 'public://test.csv' + header_row_count: 1 + enclosure: '"' + keys: + - vid + column_names: + 0: + vid: 'Vocabulary Id' + 1: + name: 'Name' + 2: + description: 'Description' +process: + vid: vid + name: name + description: description +destination: + plugin: entity:taxonomy_vocabulary +migration_dependencies: + required: { } + optional: { } diff --git a/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml b/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml new file mode 100644 index 000000000..1c2b05931 --- /dev/null +++ b/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml @@ -0,0 +1,4 @@ +id: csv_test +label: CSV source plugin edit test +description: Test editting of source plugin via the UI +source_type: CSV diff --git a/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml b/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml new file mode 100644 index 000000000..f16e6bbd7 --- /dev/null +++ b/web/modules/contrib/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml @@ -0,0 +1,15 @@ +type: module +name: CSV Source edit test +description: 'Test editing of source plugin via the UI.' +package: Testing +# core: 8.x +dependencies: + - drupal:migrate (>=8.3) + - migrate_plus:migrate_plus + - migrate_plus:migrate_source_csv + +# Information added by Drupal.org packaging script on 2018-08-27 +version: '8.x-4.0' +core: '8.x' +project: 'migrate_tools' +datestamp: 1535380087 diff --git a/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml b/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml new file mode 100644 index 000000000..7f606d127 --- /dev/null +++ b/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml @@ -0,0 +1,32 @@ +langcode: en +status: true +dependencies: { } +id: fruit_terms +label: Fruit Terms +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: { } +migration_group: default +source: + plugin: embedded_data + data_rows: + - + name: Apple + - + name: Banana + - + name: Orange + ids: + name: + type: string + constants: + vocabulary: fruit +process: + name: name + vid: constants/vocabulary +destination: + plugin: entity:taxonomy_term +migration_dependencies: + required: { } + optional: { } diff --git a/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml b/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml new file mode 100644 index 000000000..0f35a2189 --- /dev/null +++ b/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: { } +id: default +label: Default +description: '' +source_type: '' +module: null +shared_configuration: null diff --git a/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml b/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml new file mode 100644 index 000000000..bbeb69867 --- /dev/null +++ b/web/modules/contrib/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml @@ -0,0 +1,14 @@ +type: module +name: Migrate Tools Test +description: 'Test module to test Migrate Tools.' +package: Testing +# core: 8.x +dependencies: + - drupal:migrate (>=8.3) + - migrate_plus:migrate_plus + +# Information added by Drupal.org packaging script on 2018-08-27 +version: '8.x-4.0' +core: '8.x' +project: 'migrate_tools' +datestamp: 1535380087 diff --git a/web/modules/contrib/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php b/web/modules/contrib/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php new file mode 100644 index 000000000..2f29bf5ba --- /dev/null +++ b/web/modules/contrib/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php @@ -0,0 +1,132 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Functional; + +use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\Tests\BrowserTestBase; + +/** + * Execution form test. + * + * @group migrate_tools + */ +class MigrateExecutionFormTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'user', + 'filter', + 'field', + 'node', + 'text', + 'taxonomy', + 'migrate', + 'migrate_plus', + 'migrate_tools', + 'migrate_tools_test', + ]; + + /** + * {@inheritdoc} + */ + protected $profile = 'testing'; + + /** + * The vocabulary. + * + * @var \Drupal\taxonomy\VocabularyInterface + */ + protected $vocabulary; + + /** + * The vocabulary query. + * + * @var \Drupal\Core\Entity\Query\QueryInterface + */ + protected $vocabularyQuery; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->vocabulary = $this->createVocabulary(['vid' => 'fruit', 'name' => 'Fruit']); + $this->vocabularyQuery = $this->container->get('entity_type.manager') + ->getStorage('taxonomy_term') + ->getQuery(); + // Log in as user 1. Migrations in the UI can only be performed as user 1. + $this->drupalLogin($this->rootUser); + } + + /** + * Tests execution of import and rollback of a migration. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function testExecution() { + $group = 'default'; + $migration = 'fruit_terms'; + $urlPath = "/admin/structure/migrate/manage/{$group}/migrations/{$migration}/execute"; + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 0; + $this->assertEquals($expected_count, $real_count); + $this->drupalGet($urlPath); + $this->assertSession()->responseContains('Choose an operation to run'); + $edit = [ + 'operation' => 'import', + ]; + $this->drupalPostForm($urlPath, $edit, t('Execute')); + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 3; + $this->assertEquals($expected_count, $real_count); + $edit = [ + 'operation' => 'rollback', + ]; + $this->drupalPostForm($urlPath, $edit, t('Execute')); + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 0; + $this->assertEquals($expected_count, $real_count); + $edit = [ + 'operation' => 'import', + ]; + $this->drupalPostForm($urlPath, $edit, t('Execute')); + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 3; + $this->assertEquals($expected_count, $real_count); + } + + /** + * Creates a custom vocabulary based on default settings. + * + * @param array $values + * An array of settings to change from the defaults. + * Example: 'vid' => 'foo'. + * + * @return \Drupal\taxonomy\VocabularyInterface + * Created vocabulary. + */ + protected function createVocabulary(array $values = []) { + // Find a non-existent random vocabulary name. + if (!isset($values['vid'])) { + do { + $id = strtolower($this->randomMachineName(8)); + } while (Vocabulary::load($id)); + } + else { + $id = $values['vid']; + } + $values += [ + 'id' => $id, + 'name' => $id, + ]; + $vocabulary = Vocabulary::create($values); + $status = $vocabulary->save(); + + $this->assertSame($status, SAVED_NEW); + + return $vocabulary; + } + +} diff --git a/web/modules/contrib/migrate_tools/tests/src/Functional/SourceCsvFormTest.php b/web/modules/contrib/migrate_tools/tests/src/Functional/SourceCsvFormTest.php new file mode 100644 index 000000000..767a9bfe0 --- /dev/null +++ b/web/modules/contrib/migrate_tools/tests/src/Functional/SourceCsvFormTest.php @@ -0,0 +1,241 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Functional; + +use Drupal\Core\StreamWrapper\PublicStream; +use Drupal\Core\StreamWrapper\StreamWrapperInterface; +use Drupal\Tests\BrowserTestBase; +use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\taxonomy\VocabularyInterface; + +/** + * Test the CSV column alias edit form. + * + * @requires module migrate_source_csv + * + * @group migrate_tools + */ +class SourceCsvFormTest extends BrowserTestBase { + + /** + * Temporary store for column assignment changes. + * + * @var \Drupal\Core\TempStore\PrivateTempStoreFactory + */ + protected $store; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'taxonomy', + 'migrate', + 'migrate_plus', + 'migrate_tools', + 'migrate_source_csv', + 'csv_source_test', + ]; + + /** + * {@inheritdoc} + */ + protected $profile = 'testing'; + + /** + * The migration group for the test migration. + * + * @var string + */ + protected $group; + + /** + * The test migration id. + * + * @var string + */ + protected $migration; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Log in as user 1. Migrations in the UI can only be performed as user 1. + $this->drupalLogin($this->rootUser); + + // Setup the file system so we create the source CSV. + $this->container->get('stream_wrapper_manager')->registerWrapper('public', PublicStream::class, StreamWrapperInterface::NORMAL); + $fs = \Drupal::service('file_system'); + $fs->mkdir('public://sites/default/files', NULL, TRUE); + + // The source data for this test. + $source_data = <<<'EOD' +vid,name,description,hierarchy,weight +tags,Tags,Use tags to group articles,0,0 +forums,Sujet de discussion,Forum navigation vocabulary,1,0 +test_vocabulary,Test Vocabulary,This is the vocabulary description,1,0 +genre,Genre,Genre description,1,0 +EOD; + + // Write the data to the filepath given in the test migration. + file_put_contents('public://test.csv', $source_data); + + // Get the store. + $tempStoreFactory = \Drupal::service('tempstore.private'); + $this->store = $tempStoreFactory->get('migrate_tools'); + + // Select the group and migration to test. + $this->group = 'csv_test'; + $this->migration = 'csv_source_test'; + } + + /** + * Tests the form to edit CSV column aliases. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function testSourceCsvForm() { + // Define the paths to be used. + $executeUrlPath = "/admin/structure/migrate/manage/{$this->group}/migrations/{$this->migration}/execute"; + $editUrlPath = "/admin/structure/migrate/manage/{$this->group}/migrations/{$this->migration}/source/edit"; + + // Assert the test migration is listed. + $this->drupalGet("/admin/structure/migrate/manage/{$this->group}/migrations"); + $session = $this->assertSession(); + $session->responseContains('Test edit of column aliases for CSV source plugin'); + + // Proceed to the edit page. + $this->drupalGet($editUrlPath); + $session->responseContains('You can change the columns to be used by this migration for each source property.'); + + // Test that there are 3 select fields available which match the number of + // properties in the process pipeline. + $this->assertTrue($session->optionExists('edit-vid', 'vid') + ->isSelected()); + $this->assertTrue($session->optionExists('edit-name', 'name') + ->isSelected()); + $this->assertTrue($session->optionExists('edit-description', 'description') + ->isSelected()); + $session->responseNotContains('edit-hierarchy'); + $session->responseNotContains('edit-weight'); + + // Test that all 5 columns in the CSV source are available as options on + // one of the select fields. + $this->assertTrue($session->optionExists('edit-description', 'vid')); + $this->assertTrue($session->optionExists('edit-description', 'name')); + $this->assertTrue($session->optionExists('edit-description', 'description')); + $this->assertTrue($session->optionExists('edit-description', 'hierarchy')); + $this->assertTrue($session->optionExists('edit-description', 'weight')); + + // Test that two aliases can not be the same. + $edit = [ + 'edit-vid' => 2, + 'edit-name' => 1, + 'edit-description' => 1, + ]; + $this->drupalPostForm($editUrlPath, $edit, t('Submit')); + $session->responseContains('Source properties can not share the same source column.'); + $this->assertTrue($session->optionExists('edit-vid', 'description') + ->isSelected()); + $this->assertTrue($session->optionExists('edit-name', 'name') + ->isSelected()); + $this->assertTrue($session->optionExists('edit-description', 'name') + ->isSelected()); + + // Test that changes to all the column aliases are saved. + $edit = [ + 'edit-vid' => 4, + 'edit-name' => 0, + 'edit-description' => 1, + ]; + $this->drupalPostForm($editUrlPath, $edit, t('Submit')); + $this->assertTrue($session->optionExists('edit-vid', 'weight') + ->isSelected()); + $this->assertTrue($session->optionExists('edit-name', 'vid') + ->isSelected()); + $this->assertTrue($session->optionExists('edit-description', 'name') + ->isSelected()); + + // Test that the changes are saved to store. + $columnConfiguration = $this->store->get('csv_source_test'); + $migrationsChanged = $this->store->get('migrations_changed'); + $this->assertSame(['csv_source_test'], $migrationsChanged); + $expected = + [ + 'original' => + [ + 0 => ['vid' => 'Vocabulary Id'], + 1 => ['name' => 'Name'], + 2 => ['description' => 'Description'], + ], + 'changed' => + [ + 4 => ['vid' => 'weight'], + 0 => ['name' => 'vid'], + 1 => ['description' => 'name'], + ], + ]; + $this->assertSame($expected, $columnConfiguration); + + // Test the migration with incorrect column aliases. Flush the cache to + // ensure the plugin alter is run. + drupal_flush_all_caches(); + $edit = [ + 'operation' => 'import', + ]; + $this->drupalPostForm($executeUrlPath, $edit, t('Execute')); + $session->responseContains("Processed 1 item (1 created, 0 updated, 0 failed, 0 ignored) - done with 'csv_source_test'"); + + // Rollback. + $edit = [ + 'operation' => 'rollback', + ]; + $this->drupalPostForm($executeUrlPath, $edit, t('Execute')); + + // Restore to an order that will succesfully migrate. + $edit = [ + 'edit-vid' => 0, + 'edit-name' => 1, + 'edit-description' => 2, + ]; + $this->drupalPostForm($editUrlPath, $edit, t('Submit')); + $this->assertTrue($session->optionExists('edit-vid', 'vid') + ->isSelected()); + $this->assertTrue($session->optionExists('edit-name', 'name') + ->isSelected()); + $this->assertTrue($session->optionExists('edit-description', 'description') + ->isSelected()); + + // Test the vocabulary migration. + $edit = [ + 'operation' => 'import', + ]; + drupal_flush_all_caches(); + $this->drupalPostForm($executeUrlPath, $edit, t('Execute')); + $session->responseContains("Processed 4 items (4 created, 0 updated, 0 failed, 0 ignored) - done with 'csv_source_test'"); + $this->assertEntity('tags', 'Tags', 'Use tags to group articles'); + $this->assertEntity('forums', 'Sujet de discussion', 'Forum navigation vocabulary'); + $this->assertEntity('test_vocabulary', 'Test Vocabulary', 'This is the vocabulary description'); + $this->assertEntity('genre', 'Genre', 'Genre description'); + } + + /** + * Validate a migrated vocabulary contains the expected values. + * + * @param string $id + * Entity ID to load and check. + * @param string $expected_name + * The name the migrated entity should have. + * @param string $expected_description + * The description the migrated entity should have. + */ + protected function assertEntity($id, $expected_name, $expected_description) { + /** @var \Drupal\taxonomy\VocabularyInterface $entity */ + $entity = Vocabulary::load($id); + $this->assertTrue($entity instanceof VocabularyInterface); + $this->assertSame($expected_name, $entity->label()); + $this->assertSame($expected_description, $entity->getDescription()); + } + +} diff --git a/web/modules/contrib/migrate_upgrade/drush.services.yml b/web/modules/contrib/migrate_upgrade/drush.services.yml new file mode 100644 index 000000000..29bfa55f2 --- /dev/null +++ b/web/modules/contrib/migrate_upgrade/drush.services.yml @@ -0,0 +1,6 @@ +services: + migrate_upgrade.commands: + class: \Drupal\migrate_upgrade\Commands\MigrateUpgradeCommands + arguments: ['@state'] + tags: + - { name: drush.command } diff --git a/web/modules/contrib/migrate_upgrade/migrate_upgrade.drush.inc b/web/modules/contrib/migrate_upgrade/migrate_upgrade.drush.inc index ef5404d31..f1f8da50f 100644 --- a/web/modules/contrib/migrate_upgrade/migrate_upgrade.drush.inc +++ b/web/modules/contrib/migrate_upgrade/migrate_upgrade.drush.inc @@ -53,7 +53,7 @@ function drush_migrate_upgrade() { } else { $runner->import(); - \Drupal::state()->set('migrate_drupal_ui.performed', REQUEST_TIME); + \Drupal::state()->set('migrate_drupal_ui.performed', \Drupal::time()->getRequestTime()); } // Remove the global database state. \Drupal::state()->delete('migrate.fallback_state_key'); diff --git a/web/modules/contrib/migrate_upgrade/migrate_upgrade.info.yml b/web/modules/contrib/migrate_upgrade/migrate_upgrade.info.yml index e4cf0c27e..b20bf55ab 100644 --- a/web/modules/contrib/migrate_upgrade/migrate_upgrade.info.yml +++ b/web/modules/contrib/migrate_upgrade/migrate_upgrade.info.yml @@ -9,8 +9,8 @@ dependencies: - migrate_plus:migrate_plus - drupal:dblog -# Information added by Drupal.org packaging script on 2018-02-23 -version: '8.x-3.0-rc4' +# Information added by Drupal.org packaging script on 2018-08-27 +version: '8.x-3.0-rc5' core: '8.x' project: 'migrate_upgrade' -datestamp: 1519400293 +datestamp: 1535381590 diff --git a/web/modules/contrib/migrate_upgrade/src/Commands/MigrateUpgradeCommands.php b/web/modules/contrib/migrate_upgrade/src/Commands/MigrateUpgradeCommands.php new file mode 100644 index 000000000..3c9cbfc3f --- /dev/null +++ b/web/modules/contrib/migrate_upgrade/src/Commands/MigrateUpgradeCommands.php @@ -0,0 +1,190 @@ +<?php + +namespace Drupal\migrate_upgrade\Commands; + +use Consolidation\AnnotatedCommand\AnnotationData; +use Consolidation\AnnotatedCommand\CommandData; +use Drupal\migrate_upgrade\MigrateUpgradeDrushRunner; +use Drush\Commands\DrushCommands; +use Drush\Exceptions\UserAbortException; +use Drupal\Core\State\StateInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputOption; + +/** + * Migrate Upgrade drush commands. + */ +class MigrateUpgradeCommands extends DrushCommands { + + /** + * State service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * MigrateUpgradeCommands constructor. + * + * @param \Drupal\Core\State\StateInterface $state + * State service. + */ + public function __construct(StateInterface $state) { + $this->state = $state; + } + + /** + * Perform one or more upgrade processes. + * + * @command migrate:upgrade + * + * @usage migrate-upgrade --legacy-db-url='mysql://root:pass@127.0.0.1/d6' + * Upgrade a Drupal 6 database to Drupal 8 + * @usage migrate-upgrade --legacy-db-key='drupal_7' + * Upgrade Drupal 7 database where the connection to Drupal 7 has already + * been created in settings.php ($databases['drupal_7']) + * @usage migrate-upgrade --legacy-db-url='mysql://root:pass@127.0.0.1/d7' --configure-only --migration-prefix=d7_custom_ --legacy-root=https://www.example.com + * Generate migrations for a custom migration from Drupal 7 to Drupal 8 + * + * @validate-module-enabled migrate_upgrade + * + * @aliases migrate-upgrade, mup + * + * @throws \Exception + * When an error occurs. + */ + public function upgrade(array $options = []) { + $runner = new MigrateUpgradeDrushRunner($options); + + $runner->configure(); + if ($options['configure-only']) { + $runner->export(); + } + else { + $runner->import(); + $this->state->set('migrate_drupal_ui.performed', \Drupal::time()->getRequestTime()); + } + // Remove the global database state. + $this->state->delete('migrate.fallback_state_key'); + } + + /** + * @hook validate migrate:upgrade + */ + public function validatePassword(CommandData $commandData) { + $input = $commandData->input(); + $db_url = $input->getOption('legacy-db-url'); + $db_key = $input->getOption('legacy-db-key'); + + if (!$db_url && !$db_key) { + throw new \Exception('You must provide either a --legacy-db-url or --legacy-db-key.'); + } + } + + /** + * @hook option migrate:upgrade + */ + public function legacyDatabaseUrl(Command $command, AnnotationData $annotationData) { + $command->addOption( + 'legacy-db-url', + '', + InputOption::VALUE_OPTIONAL, + 'A Drupal 6 style database URL. Required if you do not set legacy-db-key.' + ); + } + + /** + * @hook option migrate:upgrade + */ + public function legacyDatabaseKey(Command $command, AnnotationData $annotationData) { + $command->addOption( + 'legacy-db-key', + '', + InputOption::VALUE_OPTIONAL, + 'A database connection key from settings.php. Use as an alternative to legacy-db-url.' + ); + } + + /** + * @hook option migrate:upgrade + */ + public function legacyDatabasePrefix(Command $command, AnnotationData $annotationData) { + $command->addOption( + 'legacy-db-prefix', + '', + InputOption::VALUE_OPTIONAL, + 'Database prefix of the legacy Drupal installation.' + ); + } + + /** + * @hook option migrate:upgrade + */ + public function legacyRoot(Command $command, AnnotationData $annotationData) { + $command->addOption( + 'legacy-root', + '', + InputOption::VALUE_OPTIONAL, + 'For files migrations. Site web address or file system root path (if files are local) of the legacy Drupal installation.' + ); + } + + /** + * @hook option migrate:upgrade + */ + public function configureOnly(Command $command, AnnotationData $annotationData) { + $command->addOption( + 'configure-only', + '', + InputOption::VALUE_NONE, + 'Set up the appropriate upgrade processes as migrate_plus config entities but do not perform them.' + ); + } + + /** + * @hook option migrate:upgrade + */ + public function migrationPrefix(Command $command, AnnotationData $annotationData) { + $command->addOption( + 'migration-prefix', + '', + InputOption::VALUE_OPTIONAL, + 'With configure-only, a prefix to apply to generated migration ids.', + 'upgrade_' + ); + } + + /** + * Rolls back and removes upgrade migrations. + * + * @throws UserAbortException + * If user chose to not perform the rollback. + * + * @command migrate:upgrade-rollback + * @usage migrate-upgrade-rollback + * Rolls back a previously-run upgrade. It will not rollback migrations + * exported as migrate_plus config entities. + * @validate-module-enabled migrate_upgrade + * @aliases migrate-upgrade-rollback, mupr + */ + public function upgradeRollback() { + if ($date_performed = $this->state->get('migrate_drupal_ui.performed')) { + if ($this->io()->confirm(dt('All migrations will be rolled back. Are you sure?'))) { + $runner = new MigrateUpgradeDrushRunner(); + + $this->logger()->notice(dt('Rolling back the upgrades performed @date', + ['@date' => \Drupal::service('date.formatter')->format($date_performed)])); + $runner->rollback(); + $this->state->delete('migrate_drupal_ui.performed'); + $this->logger()->notice(dt('Rolled back upgrades')); + } + else { + throw new UserAbortException(); + } + } + else { + $this->logger()->warning(dt('No upgrade operation has been performed.')); + } + } + +} diff --git a/web/modules/contrib/migrate_upgrade/src/MigrateUpgradeDrushRunner.php b/web/modules/contrib/migrate_upgrade/src/MigrateUpgradeDrushRunner.php index 7196c2c4c..b219f2f1b 100644 --- a/web/modules/contrib/migrate_upgrade/src/MigrateUpgradeDrushRunner.php +++ b/web/modules/contrib/migrate_upgrade/src/MigrateUpgradeDrushRunner.php @@ -2,6 +2,7 @@ namespace Drupal\migrate_upgrade; +use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Event\MigrateEvents; use Drupal\migrate\Event\MigrateIdMapMessageEvent; @@ -11,6 +12,7 @@ use Drupal\migrate_drupal\MigrationConfigurationTrait; use Drupal\migrate_plus\Entity\Migration; use Drupal\migrate_plus\Entity\MigrationGroup; use Drupal\Core\Database\Database; +use Drush\Sql\SqlBase; class MigrateUpgradeDrushRunner { @@ -50,7 +52,21 @@ class MigrateUpgradeDrushRunner { * * @var array */ - protected $nodeMigrations = []; + protected $d6NodeMigrations = []; + + /** + * List of D6 node revision migration IDs we've seen. + * + * @var array + */ + protected $d6RevisionMigrations = []; + + /** + * Drush options parameters. + * + * @var array + */ + protected $options = []; /** * List of process plugin IDs used to lookup migrations. @@ -62,6 +78,37 @@ class MigrateUpgradeDrushRunner { 'migration_lookup', ]; + /** + * MigrateUpgradeDrushRunner constructor. + * + * @param array $options + * Drush options parameters. + */ + public function __construct(array $options = []) { + $this->setOptions($options); + } + + /** + * Set options parameters according to Drush version. + * + * @param array $options + * Drush options parameters. + */ + protected function setOptions(array $options = []) { + $this->options = $options; + // Drush <= 8. + if (empty($this->options)) { + $this->options = [ + 'legacy_db_key' => drush_get_option('legacy-db-key'), + 'legacy-db-url' => drush_get_option('legacy-db-url'), + 'legacy-db-prefix' => drush_get_option('legacy-db-prefix'), + 'legacy-root' => drush_get_option('legacy-root'), + 'debug' => drush_get_option('debug'), + 'migration-prefix' => drush_get_option('migration-prefix', 'upgrade_'), + ]; + } + } + /** * From the provided source information, instantiate the appropriate migrations * in the active configuration. @@ -69,19 +116,19 @@ class MigrateUpgradeDrushRunner { * @throws \Exception */ public function configure() { - $legacy_db_key = drush_get_option('legacy-db-key'); + $legacy_db_key = $this->options['legacy-db-key']; + $db_url = $this->options['legacy-db-url']; + $db_prefix = $this->options['legacy-db-prefix']; if (!empty($legacy_db_key)) { - $connection = Database::getConnection('default', drush_get_option('legacy-db-key')); + $connection = Database::getConnection('default', $legacy_db_key); $this->version = $this->getLegacyDrupalVersion($connection); - $database_state['key'] = drush_get_option('legacy-db-key'); + $database_state['key'] = $legacy_db_key; $database_state_key = 'migrate_drupal_' . $this->version; \Drupal::state()->set($database_state_key, $database_state); \Drupal::state()->set('migrate.fallback_state_key', $database_state_key); } else { - $db_url = drush_get_option('legacy-db-url'); - $db_spec = drush_convert_db_from_db_url($db_url); - $db_prefix = drush_get_option('legacy-db-prefix'); + $db_spec = SqlBase::dbSpecFromDbUrl($db_url); $db_spec['prefix'] = $db_prefix; $connection = $this->getConnection($db_spec); $this->version = $this->getLegacyDrupalVersion($connection); @@ -94,6 +141,7 @@ class MigrateUpgradeDrushRunner { foreach ($migrations as $migration) { $this->applyFilePath($migration); $this->expandNodeMigrations($migration); + $this->prefixFileMigration($migration); $this->migrationList[$migration->id()] = $migration; } } @@ -108,7 +156,7 @@ class MigrateUpgradeDrushRunner { $destination = $migration->getDestinationConfiguration(); if ($destination['plugin'] === 'entity:file') { // Make sure we have a single trailing slash. - $source_base_path = rtrim(drush_get_option('legacy-root'), '/') . '/'; + $source_base_path = rtrim($this->options['legacy-root'], '/') . '/'; $source = $migration->getSourceConfiguration(); $source['constants']['source_base_path'] = $source_base_path; $migration->set('source', $source); @@ -123,30 +171,59 @@ class MigrateUpgradeDrushRunner { */ protected function expandNodeMigrations(MigrationInterface $migration) { $source = $migration->getSourceConfiguration(); - // Track the node migrations as we see them. + // Track the node and node_revision migrations as we see them. if ($source['plugin'] == 'd6_node') { - $this->nodeMigrations[] = $migration->id(); + $this->d6NodeMigrations[] = $migration->id(); } - elseif ($source['plugin'] == 'd6_term_node' || $source['plugin'] == 'd6_term_node_revision') { - if ($source['plugin'] == 'd6_term_node') { - $id_property = 'nid'; - } - else { - $id_property = 'vid'; - } + elseif ($source['plugin'] == 'd6_node_revision') { + $this->d6RevisionMigrations[] = $migration->id(); + } + elseif ($source['plugin'] == 'd6_term_node') { // If the ID mapping is to the underived d6_node migration, replace // it with an expanded list of node migrations. $process = $migration->getProcess(); $new_nid_process = []; - foreach ($process[$id_property] as $delta => $plugin_configuration) { + foreach ($process['nid'] as $delta => $plugin_configuration) { if (in_array($plugin_configuration['plugin'], $this->migrationLookupPluginIds) && is_string($plugin_configuration['migration']) && substr($plugin_configuration['migration'], -7) == 'd6_node') { - $plugin_configuration['migration'] = $this->nodeMigrations; + $plugin_configuration['migration'] = $this->d6NodeMigrations; } $new_nid_process[$delta] = $plugin_configuration; } - $migration->setProcessOfProperty($id_property, $new_nid_process); + $migration->setProcessOfProperty('nid', $new_nid_process); + } + elseif ($source['plugin'] == 'd6_term_node_revision') { + // If the ID mapping is to the underived d6_node_revision migration, replace + // it with an expanded list of node migrations. + $process = $migration->getProcess(); + $new_vid_process = []; + foreach ($process['vid'] as $delta => $plugin_configuration) { + if (in_array($plugin_configuration['plugin'], $this->migrationLookupPluginIds) && + is_string($plugin_configuration['migration']) && + substr($plugin_configuration['migration'], -16) == 'd6_node_revision') { + $plugin_configuration['migration'] = $this->d6RevisionMigrations; + } + $new_vid_process[$delta] = $plugin_configuration; + } + $migration->setProcessOfProperty('vid', $new_vid_process); + } + } + + /** + * For D6 file fields, make sure the d6_file migration is prefixed. + * + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * Migration to alter. + */ + protected function prefixFileMigration(MigrationInterface $migration) { + $process = $migration->getProcess(); + foreach ($process as $destination => &$plugins) { + foreach ($plugins as &$plugin) { + if ($plugin['plugin'] == 'd6_field_file') { + $plugin['migration'] = $this->modifyId($plugin['migration']); + } + } } } @@ -155,7 +232,7 @@ class MigrateUpgradeDrushRunner { */ public function import() { static::$messages = new DrushLogMigrateMessage(); - if (drush_get_option('debug')) { + if ($this->options['debug']) { \Drupal::service('event_dispatcher')->addListener(MigrateEvents::IDMAP_MESSAGE, [get_class(), 'onIdMapMessage']); } @@ -174,7 +251,7 @@ class MigrateUpgradeDrushRunner { $db_info = \Drupal::state()->get($this->databaseStateKey); // Create a group to hold the database configuration. - $group = [ + $group_details = [ 'id' => $this->databaseStateKey, 'label' => 'Import from Drupal ' . $this->version, 'description' => 'Migrations originally generated from drush migrate-upgrade --configure-only', @@ -182,53 +259,88 @@ class MigrateUpgradeDrushRunner { 'shared_configuration' => [ 'source' => [ 'key' => 'drupal_' . $this->version, - ] - ] + ], + ], ]; // Only add the database connection info to the configuration entity // if it was passed in as a parameter. - if (!empty(drush_get_option('legacy-db-url'))) { - $group['shared_configuration']['source']['database'] = $db_info['database']; + if (!empty($this->options['legacy-db-url'])) { + $group_details['shared_configuration']['source']['database'] = $db_info['database']; } // Ditto for the key. - if (!empty(drush_get_option('legacy-db-key'))) { - $group['shared_configuration']['source']['key'] = drush_get_option('legacy-db-key'); + if (!empty($this->options['legacy-db-key'])) { + $group_details['shared_configuration']['source']['key'] = $this->options['legacy-db-key']; } - $group = MigrationGroup::create($group); + // Load existing migration group and update it with changed settings, + // or create a new one if none exists. + $group = MigrationGroup::load($group_details['id']); + if (empty($group)) { + $group = MigrationGroup::create($group_details); + } + else { + $this->setEntityProperties($group, $group_details); + } $group->save(); foreach ($this->migrationList as $migration_id => $migration) { drush_print(dt('Exporting @migration as @new_migration', ['@migration' => $migration_id, '@new_migration' => $this->modifyId($migration_id)])); - $entity_array['id'] = $migration_id; - $entity_array['class'] = $migration->get('class'); - $entity_array['cck_plugin_method'] = $migration->get('cck_plugin_method'); - $entity_array['field_plugin_method'] = $migration->get('field_plugin_method'); - $entity_array['migration_group'] = $this->databaseStateKey; - $entity_array['migration_tags'] = $migration->get('migration_tags'); - $entity_array['label'] = $migration->get('label'); - $entity_array['source'] = $migration->getSourceConfiguration(); - $entity_array['destination'] = $migration->getDestinationConfiguration(); - $entity_array['process'] = $migration->get('process'); - $entity_array['migration_dependencies'] = $migration->getMigrationDependencies(); - $migration_entity = Migration::create($this->substituteIds($entity_array)); + $migration_details['id'] = $migration_id; + $migration_details['class'] = $migration->get('class'); + $migration_details['cck_plugin_method'] = $migration->get('cck_plugin_method'); + $migration_details['field_plugin_method'] = $migration->get('field_plugin_method'); + $migration_details['migration_group'] = $this->databaseStateKey; + $migration_details['migration_tags'] = $migration->get('migration_tags'); + $migration_details['label'] = $migration->get('label'); + $migration_details['source'] = $migration->getSourceConfiguration(); + $migration_details['destination'] = $migration->getDestinationConfiguration(); + $migration_details['process'] = $migration->get('process'); + $migration_details['migration_dependencies'] = $migration->getMigrationDependencies(); + $migration_details = $this->substituteIds($migration_details); + $migration_entity = Migration::load($migration_details['id']); + if (empty($migration_entity)) { + $migration_entity = Migration::create($migration_details); + } + else { + $this->setEntityProperties($migration_entity, $migration_details); + } $migration_entity->save(); } } + /** + * Set entity properties. + * + * @param ConfigEntityInterface $entity + * The entity to update. + * @param array $properties + * The properties to update. + */ + protected function setEntityProperties(ConfigEntityInterface $entity, array $properties) { + foreach ($properties as $key => $value) { + $entity->set($key, $value); + } + foreach ($entity as $property => $value) { + // Filter out values not in updated properties. + if (!isset($properties[$property])) { + $entity->set($property, NULL); + } + } + } + /** * Rewrite any migration plugin IDs so they won't conflict with the core * IDs. * - * @param $entity_array + * @param array $entity_array * A configuration array for a migration. * * @return array * The migration configuration array modified with new IDs. */ - protected function substituteIds($entity_array) { + protected function substituteIds(array $entity_array) { $entity_array['id'] = $this->modifyId($entity_array['id']); foreach ($entity_array['migration_dependencies'] as $type => $dependencies) { foreach ($dependencies as $key => $dependency) { @@ -286,7 +398,7 @@ class MigrateUpgradeDrushRunner { * The ID modified to serve as a configuration entity ID. */ protected function modifyId($id) { - return drush_get_option('migration-prefix', 'upgrade_') . str_replace(':', '_', $id); + return $this->options['migration-prefix'] . str_replace(':', '_', $id); } /** diff --git a/web/modules/contrib/migrate_upgrade/tests/src/Unit/MigrateUpgradeDrushRunnerTest.php b/web/modules/contrib/migrate_upgrade/tests/src/Unit/MigrateUpgradeDrushRunnerTest.php index 86e906fb2..518bacc2e 100644 --- a/web/modules/contrib/migrate_upgrade/tests/src/Unit/MigrateUpgradeDrushRunnerTest.php +++ b/web/modules/contrib/migrate_upgrade/tests/src/Unit/MigrateUpgradeDrushRunnerTest.php @@ -159,14 +159,16 @@ if (!function_exists('drush_get_option')) { * Override for called function. * * @param mixed $option - * An option. + * The name of the option to get * @param mixed $default - * The default. + * Optional. The value to return if the option has not been set + * @param mixed $context + * Optional. The context to check for the option. If this is set, only this context will be searched. * * @return mixed * The default, for this override. */ - function drush_get_option($option, $default) { + function drush_get_option($option, $default = NULL, $context = NULL) { return $default; } diff --git a/web/modules/contrib/paragraphs/LICENSE.txt b/web/modules/contrib/paragraphs/LICENSE.txt deleted file mode 100644 index d159169d1..000000000 --- a/web/modules/contrib/paragraphs/LICENSE.txt +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - <signature of Ty Coon>, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/web/modules/contrib/paragraphs/README.txt b/web/modules/contrib/paragraphs/README.txt deleted file mode 100644 index ef1885f53..000000000 --- a/web/modules/contrib/paragraphs/README.txt +++ /dev/null @@ -1,79 +0,0 @@ -README.txt -========== - -Paragraph is a module to create paragraphs in your content. -You can create types(with own display and fields) as Paragraph types. - -When you use the Entity Reference Paragraphs widget + Entity Reference selection -type on your node/entity, you can select the allowed types, and when using the -widget, you can select a paragraph type from the allowed types to use -different fields/display per paragraph. - -* Different fields per paragraph type -* Using different Paragraph types in a single Paragraph field -* Displays per paragraph type - -WIDGETS -------------- - -Paragraphs currently provides two different widgets that can be used. - - * Classic: a stable UI with limited features that will not be changed or - updated. - - * Experimental: This widget provides additional features like duplicating - paragraphs and a drag & drop mode (see below) as well a improved user - experience. It is just as well tested as the classic UI but major changes - between versions are to be expected. - -Drag & drop -------------- - -The experimental widget offers a separate mode that allows to re-sort paragraphs -not just within the same level but it is also possible to change the hierarchy -and move paragraphs including their children around and into other paragraphs. - -During drag & drop mode, paragraphs are also displayed as a summary only, which -results in a very compact display that makes it easier to move them around. - -To use this, an additional library is necessary, which needs to be put in the -/libraries folder. Download from https://github.com/RubaXa/Sortable/releases, -make sure that the folder name is Sortable (with uppercase S) so that the path -to the javascript file is /libraries/Sortable/Sortable.min.js. - -Due to a known issue (https://github.com/RubaXa/Sortable/pull/1154), either -version 1.5.1 should be used or the patch from that pull request. - -If the file exists, the feature will automatically be available. - -MULTILINGUAL CONFIGURATION -------------- - * Enable the Paragraph module. - - * Add new languages for the translation in Configuration » Languages. - - * Enable any custom content type with a paragraph field to be translatable in - Configuration » Content language - and translation: - - - Under Custom language settings check Content. - - - Under Content check the content type with a paragraph field. - - - Make sure that the paragraph field is set to NOT translatable. - - - Set the fields of each paragraph type to translatable as required. - - * Check Paragraphs as the embedded reference in Configuration » Translation - Management settings. - - * Create a new content - Paragraphed article and translate it. - - -LIMITATION -------------- -For now, this module does not support switching entity reference revision field -of the paragraph itself into multilingual mode. This would raise complexity -significantly. -Check #2461695: Support translatable paragraph entity reference revision field -(https://www.drupal.org/node/2461695). diff --git a/web/modules/contrib/paragraphs/composer.json b/web/modules/contrib/paragraphs/composer.json deleted file mode 100644 index 5cb745e45..000000000 --- a/web/modules/contrib/paragraphs/composer.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "drupal/paragraphs", - "description": "Enables the creation of Paragraphs entities.", - "type": "drupal-module", - "license": "GPL-2.0", - "require": { - "drupal/core": "~8", - "drupal/entity_reference_revisions": "~1.3" - }, - "require-dev": { - "drupal/diff": "~1.0", - "drupal/replicate": "~1.0", - "drupal/inline_entity_form": "~1.0", - "drupal/field_group": "~1.0", - "drupal/block_field": "~1.0" - } -} diff --git a/web/modules/contrib/paragraphs/config/install/core.entity_view_mode.paragraph.preview.yml b/web/modules/contrib/paragraphs/config/install/core.entity_view_mode.paragraph.preview.yml deleted file mode 100644 index e32016f70..000000000 --- a/web/modules/contrib/paragraphs/config/install/core.entity_view_mode.paragraph.preview.yml +++ /dev/null @@ -1,9 +0,0 @@ -langcode: en -id: paragraph.preview -label: Preview -status: true -cache: true -targetEntityType: paragraph -dependencies: - module: - - paragraphs \ No newline at end of file diff --git a/web/modules/contrib/paragraphs/config/schema/paragraphs_type.schema.yml b/web/modules/contrib/paragraphs/config/schema/paragraphs_type.schema.yml deleted file mode 100644 index 6bcada9d0..000000000 --- a/web/modules/contrib/paragraphs/config/schema/paragraphs_type.schema.yml +++ /dev/null @@ -1,96 +0,0 @@ -paragraphs.paragraphs_type.*: - type: config_entity - label: 'Paragraphs type config' - mapping: - id: - type: string - label: 'ID' - label: - type: label - label: 'Label' - icon_uuid: - type: string - label: 'Icon uuid' - description: - type: text - label: 'Description' - behavior_plugins: - type: sequence - label: 'Plugins' - sequence: - type: paragraphs.behavior.settings.[%key] - label: 'Behavior plugins' - -paragraphs.behavior.settings_base: - type: mapping - label: 'Settings for base paragraphs behavior plugin' - mapping: - enabled: - type: boolean - label: 'Enabled' - -# The plugins which extend the base behavior plugin and don't provide -# any configuration form will validate on this entry. -paragraphs.behavior.settings.*: - type: paragraphs.behavior.settings_base - -entity_reference_selection.default:paragraph: - type: entity_reference_selection.default - mapping: - negate: - type: integer - target_bundles_drag_drop: - type: sequence - sequence: - type: mapping - mapping: - weight: - type: integer - enabled: - type: boolean - add_mode: - type: string - edit_mode: - type: string - title: - type: string - title_plural: - type: string - default_paragraph_type: - type: string - -field.widget.settings.entity_reference_paragraphs: - type: mapping - mapping: - title: - type: string - title_plural: - type: string - edit_mode: - type: string - add_mode: - type: string - form_display_mode: - type: string - default_paragraph_type: - type: string - -field.widget.settings.paragraphs: - type: mapping - mapping: - title: - type: string - title_plural: - type: string - edit_mode: - type: string - closed_mode: - type: string - autocollapse: - type: string - add_mode: - type: string - form_display_mode: - type: string - default_paragraph_type: - type: string diff --git a/web/modules/contrib/paragraphs/css/.gitignore b/web/modules/contrib/paragraphs/css/.gitignore deleted file mode 100644 index 3c3629e64..000000000 --- a/web/modules/contrib/paragraphs/css/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/web/modules/contrib/paragraphs/css/.stylelintrc.yml b/web/modules/contrib/paragraphs/css/.stylelintrc.yml deleted file mode 100644 index e3285370b..000000000 --- a/web/modules/contrib/paragraphs/css/.stylelintrc.yml +++ /dev/null @@ -1,294 +0,0 @@ -# Base taken from https://github.com/material-components/material-components-web/blob/master/.stylelintrc.yaml -# -# For more information about Drupal standards that this configuration is trying -# to follow please check -# -# - https://www.drupal.org/docs/develop/standards/css/css-formatting-guidelines -# - https://www.drupal.org/docs/develop/standards/css/csscomb-settings-for-drupal-css-formatting-and-sort-tool - -# We are using https://github.com/stylelint/stylelint-config-standard/blob/master/index.js -# as a base for our rules. -extends: stylelint-config-standard -ignoreFiles: - - node_modules/**/* -plugins: - - stylelint-selector-bem-pattern - - stylelint-scss - - stylelint-order -rules: - # Follow best practices - font-family-name-quotes: always-where-recommended - # http://stackoverflow.com/questions/2168855/is-quoting-the-value-of-url-really-necessary - function-url-quotes: never - # https://www.w3.org/TR/selectors/#attribute-selectors - # http://stackoverflow.com/q/3851091 - selector-attribute-quotes: always - # Double-quotes are our convention throughout our codebase within (S)CSS. - # They also reflect how attribute strings are normally quoted within the DOM. - string-quotes: double - # Before getting a hard attack about the fact that we require proper CSS - # ordering please read next articles: - # - https://css-tricks.com/poll-results-how-do-you-order-your-css-properties/ - # - https://www.drupal.org/docs/develop/standards/css/csscomb-settings-for-drupal-css-formatting-and-sort-tool - # Note that we do not follow exactly the same proposed standard for now like - # in csscomb ddo page. - # - # This order definition is taken from: - # - https://github.com/sasstools/sass-lint/blob/develop/lib/config/property-sort-orders/smacss.yml - order/declaration-block-properties-specified-order: - # Box - - - display - - position - - top - - right - - bottom - - left - - - flex - - flex-basis - - flex-direction - - flex-flow - - flex-grow - - flex-shrink - - flex-wrap - - align-content - - align-items - - align-self - - justify-content - - order - - - width - - min-width - - max-width - - - height - - min-height - - max-height - - - margin - - margin-top - - margin-right - - margin-bottom - - margin-left - - - padding - - padding-top - - padding-right - - padding-bottom - - padding-left - - - float - - clear - - - columns - - column-gap - - column-fill - - column-rule - - column-span - - column-count - - column-width - - - transform - - transform-box - - transform-origin - - transform-style - - - transition - - transition-delay - - transition-duration - - transition-property - - transition-timing-function - - # Border - - - border - - border-top - - border-right - - border-bottom - - border-left - - border-width - - border-top-width - - border-right-width - - border-bottom-width - - border-left-width - - - border-style - - border-top-style - - border-right-style - - border-bottom-style - - border-left-style - - - border-radius - - border-top-left-radius - - border-top-right-radius - - border-bottom-left-radius - - border-bottom-right-radius - - - border-color - - border-top-color - - border-right-color - - border-bottom-color - - border-left-color - - - outline - - outline-color - - outline-offset - - outline-style - - outline-width - - # Background - - - background - - background-attachment - - background-clip - - background-color - - background-image - - background-repeat - - background-position - - background-size - - # Text - - - color - - - font - - font-family - - font-size - - font-smoothing - - font-style - - font-variant - - font-weight - - - letter-spacing - - line-height - - list-style - - - text-align - - text-decoration - - text-indent - - text-overflow - - text-rendering - - text-shadow - - text-transform - - text-wrap - - - white-space - - word-spacing - - # Other - - - border-collapse - - border-spacing - - box-shadow - - caption-side - - content - - cursor - - empty-cells - - opacity - - overflow - - quotes - - speak - - table-layout - - vertical-align - - visibility - - z-index - declaration-property-unit-whitelist: - font-size: - - "rem" - - "px" - # The following prefix rules are enabled since we use autoprefixer. - at-rule-no-vendor-prefix: true - media-feature-name-no-vendor-prefix: true - selector-no-vendor-prefix: true - value-no-vendor-prefix: true - # Usually if you're nesting past 3 levels deep there's a problem. - max-nesting-depth: 3 - # Because we adhere to BEM we can limit the amount of necessary compound - # selectors. Most should - # just be 1 level selector. However, modifiers can introduce an additional compound selector. - # Futhermore, generic qualifying selectors (e.g. `[dir="rtl"]`) can introduce yet another level. - selector-max-compound-selectors: 4 - # For specificity: disallow IDs (0). Allow for complex combinations of classes, pseudo-classes, - # attribute modifiers based on selector-max-compound-selectors, plus an addition for - # pseudo-classes (4). Allow for pseudo-elements (1). - selector-max-specificity: 0,4,1 - # Disallow "@extend" in scss. - # http://csswizardry.com/2016/02/mixins-better-for-performance/ - # http://vanseodesign.com/css/sass-mixin-or-extend/ - # Besides performance, @extend actually *changes* the selector precedence by creating a compound - # selector, which can lead to ambiguous results. - at-rule-blacklist: - - extend - # Extremely useful for typos, and anything emergent can be ignored by this rule - property-no-unknown: - - true - - ignoreProperties: - - contain - # There is no reason that a specific ID would be needed for UI components - selector-no-id: true - # Qualifying types are not needed when using a naming system like BEM - selector-no-qualifying-type: true - # In general, we should *never* be modifying elements within our components, since we can't - # predict the use cases in which users would add a certain type of element into a component. - # The only hard exception to this are `fieldset` elements, which can be disabled and in that case - # we want our UI components within that fieldset to be disabled as well. - # Other exceptions to this may be in packages/material-components-web, in which case this rule could - # be disabled for that file, with an explanation. - selector-no-type: - - true - - ignoreTypes: - - fieldset - # Styles for components should never need the universal selector. - selector-no-universal: true - # Ensure any defined symbols are prefixed with "mdc-" - # TODO: Remove "md-" pattern. - #custom-media-pattern: ^mdc?-.+ - #custom-property-pattern: ^mdc?-.+ - #selector-class-pattern: - # - ^mdc?-.+ - # - resolveNestedSelectors: true - #selector-id-pattern: ^mdc?-.+ - # Names are more semantic than numbers - font-weight-notation: named-where-possible - # http://www.paulirish.com/2010/the-protocol-relative-url/ - function-url-no-scheme-relative: true - # @todo TODO: and FIXME: warnings are super useful because they remind us that - # we should address these within our codebase. - comment-word-blacklist: - - - - todo - - /^TODO:/ - - /^FIXME:/ - - severity: warning - # Our Drupal style guide is with leading zero. - # Leading zero presence does not matter http://hey.georgie.nu/leadingzero/. - number-leading-zero: always - - # We use Harry Roberts' BEM dialect as our preferred way to format classes. - # See: http://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/ - # See: https://github.com/postcss/postcss-bem-linter/blob/c59db3f/lib/preset-patterns.js#L39 - plugin/selector-bem-pattern: - componentName: ^[a-z]+(?:-[a-z]+)*$ - # <namespace>-<block>__<element>*--<modifier>*[<attribute>]* - componentSelectors: ^\.paragraphs?-{componentName}(?:__[a-z]+(?:-[a-z]+)*)*(?:--[a-z]+(?:-[a-z]+)*)*(?:\[.+\])*$ - ignoreSelectors: - - ^fieldset - - ^\[aria\-disabled=(?:.+)\] - - # @todo - figure how we are going to use this SCSS naming patterns? - # SCSS naming patterns, just like our CSS conventions above. - # (note for $-vars we use a leading underscore for "private" variables) - #scss/dollar-variable-pattern: - # - ^_?mdc-.+ - # - - # ignore: local - #scss/at-function-pattern: ^mdc-.+ - #scss/at-mixin-pattern: ^mdc-.+ - # Prevents unneeded nesting selectors - scss/selector-no-redundant-nesting-selector: true - # Since leading underscores are not needed, they can be omitted - scss/at-import-no-partial-leading-underscore: true - # Since mixins are explicit (`@include`) and parens are unnecessary for argumentless mixins, they - # can be omitted. - scss/at-mixin-no-argumentless-call-parentheses: true diff --git a/web/modules/contrib/paragraphs/css/README.md b/web/modules/contrib/paragraphs/css/README.md deleted file mode 100644 index a5da7f768..000000000 --- a/web/modules/contrib/paragraphs/css/README.md +++ /dev/null @@ -1,83 +0,0 @@ -## Contributing to paragraphs CSS code - -Paragraphs are currently using Gulp and SASS tools for more efficient CSS -development. For the people that wants to contribute to paragraphs CSS code you -have two options: - -1. If you want to propose CSS improvement but do not want to use our Gulp/SASS - toolchain then just change compiled CSS and create a issue with a patch from - it. When patch is accepted we will then transfer your changes to SASS and - recompile CSS files. -2. Instead of manually changing CSS files, recommended way is to reuse our - Gulp/SASS process and do changes in appropriate SASS files and then recompile - it to CSS. - - -## Preparing your development environment for Gulp/SASS toolchain - -If you want to do __step 2.__ but do not have needed Gulp/SASS experience do not -worry, process is not that difficult and is explained in next steps: - -- First thing you need to have is nodejs server on your machine. Please check - https://nodejs.org/en/download/package-manager/ and follow steps of nodejs - server installation for your operating system. - -- Then change directory to paragraphs CSS folder - - `$ cd paragraphs/css` - -- Before compiling SASS files with gulp you need to install required - dependencies with node package manager tool. In the same folder execute - - `$ npm install` - - The list of dependencies are defined in `paragraphs/css/package.json` JSON - file. - -- You are now able to compile paragraphs CSS from our SASS source files. In the - same folder execute - - `$ gulp` - -If you did not get any error your local machine is now ready and with last -command you already compiled paragraphs SASS files to CSS. - -For closer look at our Gulp configuration and tasks check -paragraphs/css/gulpfile.js. - - -## Doing changes in CSS over SASS - -Now you are ready to do necessary changes to paragraphs CSS. First locate the -CSS selector rule you want to change in CSS and then locate this rule in -appropriate SASS file. Do the change in SASS file, save it and just execute -again `$ gulp` from your console. - -When you are satisfied with result in CSS files, create Drupal paragraphs issue -and a patch in standard way. - - -## Making sure that your changes are aligned with CSS code standards - -If you are getting warning when executing `$ gulp` that are coming from -stylelint do not worry. -This warnings are coming from stylelint postcss plugin which is doing statical -checking of generated CSS files and this simply means that generated CSS code is -not compatible with paragraphs CSS coding standards. - -Generally before accepting SASS/CSS change you need to be sure that all warnings -are fixed. -But in some cases warnings can not be avoided, in that case please use turning -rules off from SASS like explained in https://github.com/stylelint/stylelint/blob/master/docs/user-guide/configuration.md#turning-rules-off-from-within-your-css. Note that you can use also `//` -comment syntax instead of `/* ... */` - -You can also just run gulp sass lint task: - -`$ gulp sass:lint` - - -## Resources - -SASS is a very powerful tool and its always good option to know your tools -better. Please check http://sass-lang.com/guide for more information on SASS -syntax and it features. diff --git a/web/modules/contrib/paragraphs/css/_variables.scss b/web/modules/contrib/paragraphs/css/_variables.scss deleted file mode 100644 index 926a1bd2c..000000000 --- a/web/modules/contrib/paragraphs/css/_variables.scss +++ /dev/null @@ -1,31 +0,0 @@ -// Colours. - -$grey-dark: #999999 !default; - - -// Layout. - -$grid-breakpoints: ( - xs: 0, - sm: 600px, - md: 768px, - lg: 992px, - xl: 1200px -) !default; - -// Gutter value for top widget section. -$gutter-top: 5px !default; - - -// Elements. - -$action-size-normal: 26px !default; -$action-size-big: 36px !default; -$action-icon-size: 24px !default; -$action-hover-bg: #f9f8f6 !default; -$action-border: #a6a6a6 !default; -$action-border-radius: 4px !default; -$action-hover-box-shadow: 0 1px 2px hsla(0, 0%, 0%, 0.125) !default; - -$info-size: 20px !default; -$info-icon-size: 16px !default; diff --git a/web/modules/contrib/paragraphs/css/gulp-options.yml b/web/modules/contrib/paragraphs/css/gulp-options.yml deleted file mode 100644 index 5bf982ac5..000000000 --- a/web/modules/contrib/paragraphs/css/gulp-options.yml +++ /dev/null @@ -1,51 +0,0 @@ -# This file holds various gulp configurations that we need in our Gulp process. - -# Locations of our SASS and CSS files. -scssSrc: "./" -cssDest: "./" - -# This are the paths that will be added to sass search paths for include. -# @todo - note that we are not using this for now really, this pattern will -# maybe make sense for more complex CSS work that we will do. If this is not -# reused in near future lets consider to remove it. -sassIncludePaths: - - "./sass/" - - "./node_modules/" - -# Automatically load any gulp plugins in package.json. -gulpLoadPlugins: - pattern: - - "gulp-*" - - "gulp.*" - - "autoprefixer" - - "postcss-*" - - "stylelint" - - "syntax_scss" - # Rename mapping to values that we will use in JS code. - rename: - postcss-reporter: "reporter" - postcss-scss: "syntax_scss" - -autoprefixer: - # Autoprefixer 7.x version by default is disabling grid support for IE but - # we still needs to support old IE versions. - # @see https://github.com/postcss/autoprefixer/releases/tag/7.0.0. - grid: true - # Browsers support is last two versions of all browsers by default, IE10+ and - # Safari 7+ because of iPhone 5c and 5s. - browsers: - - "last 2 versions" - - "ie >= 10" - - "Safari >= 7" - -# Path to stylelint config file. -stylelintOptions: - configFile: "./.stylelintrc.yml" -# Processor configuration for stylelint postcss plugin. -processorsOptions: - reporterOptions: - # Clear all messages after displaying so they are not redisplayed in other - # plugins that are coming after stylelint. - clearAllMessages: true - # Don't throw exception but continue with gulp tasks. - throwError: false diff --git a/web/modules/contrib/paragraphs/css/gulp-tasks.js b/web/modules/contrib/paragraphs/css/gulp-tasks.js deleted file mode 100644 index 05245943e..000000000 --- a/web/modules/contrib/paragraphs/css/gulp-tasks.js +++ /dev/null @@ -1,37 +0,0 @@ -// Define gulp tasks. -module.exports = function(gulp, plugins, options) { - - 'use strict'; - - // Processor for linting is assigned to options so it can be reused later. - options.processors = [ - // Options are defined in .stylelintrc.yaml file. - plugins.stylelint(options.stylelintOptions), - plugins.reporter(options.processorsOptions.reporterOptions) - ]; - - // Post CSS options. - options.postcssOptions = [ - plugins.autoprefixer(options.autoprefixer) - ]; - - // Defining gulp tasks. - - gulp.task('sass', function() { - return gulp.src(options.scssSrc + '/*.scss') - .pipe(plugins.sass({ - outputStyle: 'expanded', - includePaths: options.sassIncludePaths - })) - .pipe(plugins.postcss(options.postcssOptions)) - .pipe(gulp.dest(options.cssDest)); - }); - - gulp.task('sass:lint', function () { - return gulp.src(options.scssSrc + '/*.scss') - .pipe(plugins.postcss(options.processors, {syntax: plugins.syntax_scss})) - }); - - // Default task to run everything in correct order. - gulp.task('default', ['sass:lint', 'sass']); -}; diff --git a/web/modules/contrib/paragraphs/css/gulpfile.js b/web/modules/contrib/paragraphs/css/gulpfile.js deleted file mode 100644 index 44024f134..000000000 --- a/web/modules/contrib/paragraphs/css/gulpfile.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @file - * Provides Gulp configurations and tasks for compiling paragraphs CSS files - * from SASS files. - */ - -'use strict'; - -// Load gulp and needed lower level libs. -var gulp = require('gulp'), - yaml = require('js-yaml'), - fs = require('fs'); - -// Load gulp options. -var options = yaml.safeLoad(fs.readFileSync('./gulp-options.yml', 'utf8')); - -// Lazy load gulp plugins. -// By default gulp-load-plugins will only load "gulp-*" and "gulp.*" tasks, -// so we need to define additional patterns for other modules we are using. -var plugins = require('gulp-load-plugins')(options.gulpLoadPlugins); - -// Load gulp tasks. -require('./gulp-tasks.js')(gulp, plugins, options); diff --git a/web/modules/contrib/paragraphs/css/package.json b/web/modules/contrib/paragraphs/css/package.json deleted file mode 100644 index 5a426d2c9..000000000 --- a/web/modules/contrib/paragraphs/css/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "paragraphs", - "version": "", - "description": "", - "public": true, - "license": "GPL-2.0", - "devDependencies": { - "autoprefixer": "^7.1.3", - "gulp": "^3.9.1", - "gulp-load-plugins": "^1.5.0", - "gulp-postcss": "^7.0.0", - "gulp-sass": "^3.1.0", - "js-yaml": "^3.8.1", - "postcss-reporter": "^5.0.0", - "postcss-scss": "^1.0.2", - "stylelint": "^8.0.0", - "stylelint-config-standard": "^17.0.0", - "stylelint-order": "^0.6.0", - "stylelint-scss": "^2.1.0", - "stylelint-selector-bem-pattern": "^1.0.0" - } -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.actions.css b/web/modules/contrib/paragraphs/css/paragraphs.actions.css deleted file mode 100644 index 62c3baf81..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.actions.css +++ /dev/null @@ -1,127 +0,0 @@ -.paragraphs-actions { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; -} - -.paragraphs-actions > .button { - margin-left: 5px; - margin-right: 0; -} - -.paragraphs-actions > .button + .paragraphs-dropdown { - margin-left: 5px; -} - -.paragraphs-actions .paragraphs-icon { - margin-left: 5px; - margin-right: 5px; -} - -.paragraphs-dropdown { - position: relative; -} - -.paragraphs-dropdown-toggle { - display: block; - height: 36px; - width: 36px; - background: url("../icons/icon-actions.svg") no-repeat center; - background-size: 26px 26px; - border: 1px solid transparent; - border-radius: 4px; - -webkit-transition: all 0.1s; - transition: all 0.1s; -} - -@media (min-width: 768px) { - .paragraphs-dropdown-toggle { - height: 26px; - width: 26px; - } -} - -.paragraphs-dropdown-toggle:hover, .paragraphs-dropdown-toggle:focus { - background-color: #f9f8f6; - border: 1px solid #a6a6a6; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); - outline: none; -} - -.paragraphs-dropdown-actions { - display: none; - overflow: hidden; - position: absolute; - right: 0; - top: 36px; - z-index: 100; - background: #f9f8f6; - border: 1px solid #a6a6a6; - border-radius: 5px 0 5px 5px; - -webkit-box-shadow: 0 2px 6px 0 #aaa; - box-shadow: 0 2px 6px 0 #aaa; -} - -@media (min-width: 768px) { - .paragraphs-dropdown-actions { - top: 26px; - } -} - -.item-list .paragraphs-dropdown-actions { - list-style: none; -} - -.item-list .paragraphs-dropdown-actions li { - margin: 0; - list-style: none; -} - -.paragraphs-dropdown.open .paragraphs-dropdown-actions { - display: block; -} - -.paragraphs-dropdown-action { - width: 100%; -} - -.paragraphs-dropdown-action.button { - margin-right: 0; - margin-left: 0; - text-align: left; - background: transparent; - border: 0; - border-radius: 0; - font-weight: 600; - line-height: 18px; - white-space: nowrap; -} - -.paragraphs-dropdown-action.button:focus { - border: none; - -webkit-box-shadow: none; - box-shadow: none; - outline: none; -} - -.form-wrapper .paragraphs-dropdown-action.button { - margin-top: 0; -} - -@media (max-width: 768px) { - .form-wrapper .paragraphs-dropdown-action.button { - padding-top: 10px; - padding-bottom: 10px; - } -} - -.paragraphs-actions-item-icon { - height: 20px; - padding: 0 10px; -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.actions.scss b/web/modules/contrib/paragraphs/css/paragraphs.actions.scss deleted file mode 100644 index c5d1ce477..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.actions.scss +++ /dev/null @@ -1,126 +0,0 @@ -@import "variables"; - -.paragraphs-actions { - display: flex; - align-items: center; - - // Actions buttons (the one that are not in dropdown). - > .button { - margin-left: $gutter-top; - margin-right: 0; - - + .paragraphs-dropdown { - margin-left: $gutter-top; - } - } - - .paragraphs-icon { - margin-left: $gutter-top; - // We need right margin here so that lock icon is horizontally aligned with - // dropdown icon. - margin-right: $gutter-top; - } - - @at-root .paragraphs-dropdown { - position: relative; - - &-toggle { - // Use block display so parent can have same dimensions as toggle button. - display: block; - height: $action-size-big; - width: $action-size-big; - background: url('../icons/icon-actions.svg') no-repeat center; - background-size: $action-size-normal $action-size-normal; - border: 1px solid transparent; - border-radius: $action-border-radius; - transition: all 0.1s; - - @media (min-width: map-get($grid-breakpoints, 'md')) { - height: $action-size-normal; - width: $action-size-normal; - } - - &:hover, &:focus { - background-color: $action-hover-bg; - // Border and shadow are the same as for standard Drupal button. - border: 1px solid $action-border; - box-shadow: $action-hover-box-shadow; - outline: none; - } - } - - &-actions { - display: none; - // We are using overflow hidden to prevent button background overflowing - // on hover. - overflow: hidden; - position: absolute; - right: 0; - top: $action-size-big; - z-index: 100; - background: $action-hover-bg; - border: 1px solid $action-border; - border-radius: 5px 0 5px 5px; - box-shadow: 0 2px 6px 0 #aaa; - - @media (min-width: map-get($grid-breakpoints, 'md')) { - top: $action-size-normal; - } - - // Override seven .item-list styles - we need to do like this because we - // can't remove the class without overriding template. - .item-list & { - list-style: none; - - li { - margin: 0; - list-style: none; - } - } - } - - &.open { - .paragraphs-dropdown-actions { - display: block; - } - } - - &-action { - width: 100%; - - &.button { - margin-right: 0; - margin-left: 0; - text-align: left; - background: transparent; - border: 0; - border-radius: 0; - font-weight: 600; - line-height: 18px; - white-space: nowrap; - - &:focus { - border: none; - box-shadow: none; - outline: none; - } - - // Override seven button.css top margin. - .form-wrapper & { - margin-top: 0; - - @media (max-width: map-get($grid-breakpoints, 'md')) { - // Bigger touch area for small screens. - padding-top: 10px; - padding-bottom: 10px; - } - } - } - } - } - - &-item-icon { - height: 20px; - padding: 0 10px; - } -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.admin.css b/web/modules/contrib/paragraphs/css/paragraphs.admin.css deleted file mode 100644 index 58971ccd7..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.admin.css +++ /dev/null @@ -1,82 +0,0 @@ -.js .field--widget-entity-reference-paragraphs .dropbutton-wrapper { - display: inline-flex; - padding-right: 0em; - margin-right: 0em; - /* Override 600px breakpoint from core. */ - width: auto; -} - -.js .field--widget-entity-reference-paragraphs .dropbutton-widget { - position: relative; -} - -.js .field--widget-entity-reference-paragraphs .field-multiple-table { - margin-bottom: 10px; -} - -.js .field--widget-entity-reference-paragraphs td { - padding: 10px 0px 10px 0px; -} - -.js .field--widget-entity-reference-paragraphs .field-multiple-drag { - vertical-align: top; -} -.js .field--widget-entity-reference-paragraphs .draggable .tabledrag-handle { - padding-right: 0; - margin-left: 0; -} -.js .field--widget-entity-reference-paragraphs .tabledrag-handle .handle { - margin-left: 0; - margin-right: 0; - padding-right: 0.2em; -} - -.js .paragraph-type-top { - display: flex; - flex-wrap: nowrap; - justify-content: space-between; -} -.js .paragraphs-collapsed-description { - height: 1.538em; - padding-left: 1em; - padding-right: 1em; - position: relative; - width: 100%; - - color: #777; - overflow: hidden; - word-break: break-all; -} -.js .paragraphs-collapsed-description:after { - content: '\00a0'; - display: block; - position: absolute; - top: 0; - right: 1em; - width: 3em; - - background: #fff; - background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #fff 100%); -} -.js .draggable:hover .paragraphs-collapsed-description:after, -.js .draggable:hover .paragraphs-collapsed-description:after { - background: #f7fcff; - background: linear-gradient(to right, rgba(247, 252, 255, 0) 0%, #f7fcff 100%); -} -.js .drag-previous .paragraphs-collapsed-description:after { - background: #ffd; - background: linear-gradient(to right, rgba(255, 255, 221, 0) 0%, #ffd 100%); -} -.js .paragraph-type-title { - flex-basis: 25%; - min-width: 80px; - - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -} - -.js .field--widget-entity-reference-paragraphs .delta-order { - padding-right: 10px; - text-align: right; -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.dragdrop.css b/web/modules/contrib/paragraphs/css/paragraphs.dragdrop.css deleted file mode 100644 index c17588292..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.dragdrop.css +++ /dev/null @@ -1,98 +0,0 @@ -.paragraphs-dragdrop { - list-style-type: none; - margin: 0; - padding: 0; - min-height: 50px; -} - -.paragraphs-dragdrop:empty { - border: 1px dashed #999999; - margin-bottom: 10px; -} - -.paragraphs-dragdrop .paragraphs-dragdrop { - margin-left: 10px; - margin-right: 5px; -} - -@media all and (min-width: 780px) { - .paragraphs-dragdrop .paragraphs-dragdrop { - margin-left: 20px; - } -} - -.paragraphs-dragdrop > li { - min-height: 30px; - margin: 10px 0; - padding: 3px; - border: 1px solid #999999; - border-left-width: 10px; - -webkit-transition: border .5s ease; - transition: border .5s ease; - border-radius: 3px; - cursor: move; -} - -.paragraphs-dragdrop .paragraphs-summary-wrapper { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 5px; -} - -.paragraphs-dragdrop strong { - white-space: nowrap; -} - -.paragraphs-dragdrop .paragraphs-dragdrop-wrapper { - margin-left: 39px; - margin-top: 20px; -} - -.paragraphs-dragdrop > li .paragraphs-summary-wrapper > div { - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; - margin-left: 10px; -} - -.paragraphs-dragdrop .sortable-drag { - opacity: 0; -} - -.paragraphs-dragdrop > .sortable-ghost, -.paragraphs-dragdrop .sortable-chosen { - opacity: 1; - border-left-color: black; -} - -.paragraphs-dragdrop .tabledrag-handle { - float: left; - padding: 5px; - -webkit-align-self: flex-start; - -ms-flex-item-align: start; - align-self: flex-start; -} - -[dir="rtl"] .paragraphs-dragdrop .tabledrag-handle { - float: right; -} - -.touchevents .paragraphs-dragdrop .tabledrag-handle { - padding: 0 0 0 5px; - margin-top: -5px; -} - -.js .field--widget-paragraphs .paragraphs-dragdrop .field--widget-paragraphs { - margin-left: 5px; -} - -.field-dragdrop-mode-submit { - margin-right: 10px; -} - -.paragraphs-dragdrop-wrapper { - padding-top: 10px; -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.dragdrop.scss b/web/modules/contrib/paragraphs/css/paragraphs.dragdrop.scss deleted file mode 100644 index 8c8194f32..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.dragdrop.scss +++ /dev/null @@ -1,91 +0,0 @@ -@import "variables"; - -.paragraphs-dragdrop { - list-style-type: none; - margin: 0; - padding: 0; - min-height: 50px; - - &:empty { - border: 1px dashed $grey-dark; - margin-bottom: 10px; - } - - .paragraphs-dragdrop { - margin-left: 10px; - margin-right: 5px; - - @media all and (min-width: 780px) { - margin-left: 20px; - } - } - - > li { - min-height: 30px; - margin: 10px 0; - padding: 3px; - border: 1px solid $grey-dark; - border-left-width: 10px; - transition: border .5s ease; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - cursor: move; - } - - .paragraphs-summary-wrapper { - display: flex; - padding: 5px; - } - - strong { - white-space: nowrap; - } - - .paragraphs-dragdrop-wrapper { - margin-left: 39px; - margin-top: 20px; - } - - > li .paragraphs-summary-wrapper > div { - flex-grow: 1; - margin-left: 10px; - } - - .sortable-drag { - opacity: 0; - } - - > .sortable-ghost, - .sortable-chosen { - opacity: 1; - border-left-color: black; - } - - .tabledrag-handle { - float: left; - padding: 5px; - align-self: flex-start; - - [dir="rtl"] & { - float: right; - } - - .touchevents & { - padding: 0 0 0 5px; - margin-top: -5px; - } - } -} - -.js .field--widget-paragraphs .paragraphs-dragdrop .field--widget-paragraphs { - margin-left: 5px; -} - -.field-dragdrop-mode-submit { - margin-right: 10px; -} - -.paragraphs-dragdrop-wrapper { - padding-top: 10px; -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.list-builder.css b/web/modules/contrib/paragraphs/css/paragraphs.list-builder.css deleted file mode 100644 index 12fbd07bb..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.list-builder.css +++ /dev/null @@ -1,7 +0,0 @@ -.paragraphs-type-icon { - width: 50px; -} - -.paragraphs-type-icon img { - display: block; -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.list-builder.scss b/web/modules/contrib/paragraphs/css/paragraphs.list-builder.scss deleted file mode 100644 index 609b83622..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.list-builder.scss +++ /dev/null @@ -1,7 +0,0 @@ - -.paragraphs-type-icon { - width: 50px; - img { - display: block; - } -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.modal.css b/web/modules/contrib/paragraphs/css/paragraphs.modal.css deleted file mode 100644 index 22a368d66..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.modal.css +++ /dev/null @@ -1,23 +0,0 @@ -ul.paragraphs-add-dialog-list { - margin: 0; - padding: 0; - list-style-type: none; -} - -ul.paragraphs-add-dialog-list input.field-add-more-submit { - background-repeat: no-repeat; - background-position: 10px; - background-size: auto calc(100% - 10px); - padding-left: 40px; - text-align: left; - border-radius: 3px; -} - -.paragraphs-add-dialog-row { - padding: 5px; -} - -.paragraphs-add-dialog-row input { - width: 100%; - min-width: 200px; -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.modal.scss b/web/modules/contrib/paragraphs/css/paragraphs.modal.scss deleted file mode 100644 index 0da0fe1ad..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.modal.scss +++ /dev/null @@ -1,23 +0,0 @@ -ul.paragraphs-add-dialog-list { - margin: 0; - padding: 0; - list-style-type: none; - - input.field-add-more-submit { - background-repeat: no-repeat; - background-position: 10px; - background-size: auto calc(100% - 10px); - padding-left: 40px; - text-align: left; - border-radius: 3px; - } -} - -.paragraphs-add-dialog-row { - padding: 5px; - - input { - width: 100%; - min-width: 200px; - } -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.widget.css b/web/modules/contrib/paragraphs/css/paragraphs.widget.css deleted file mode 100644 index 12780b3d3..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.widget.css +++ /dev/null @@ -1,288 +0,0 @@ -.paragraphs-icon { - display: inline-block; - height: 20px; - width: 20px; - background: no-repeat center; - background-size: 16px 16px; -} - -.paragraphs-icon-view { - background-image: url("../icons/icon-view.svg"); -} - -.paragraphs-icon-edit-info { - background-image: url("../icons/icon-edit-info.svg"); -} - -.paragraphs-icon-edit-disabled { - background-image: url("../icons/icon-edit-disabled.svg"); -} - -.paragraphs-icon-delete { - background-image: url("../icons/icon-delete.svg"); -} - -.paragraphs-icon-delete-disabled { - background-image: url("../icons/icon-delete-disabled.svg"); -} - -.paragraphs-icon-lock { - background-image: url("../icons/icon-lock.svg"); -} - -.paragraphs-icon-changed { - background-image: url("../icons/icon-changed.svg"); -} - -.paragraphs-icon-collapse { - background-image: url("../icons/icon-collapse.svg"); -} - -.paragraphs-icon-warning { - background-image: url("../icons/icon-warning.svg"); -} - -.paragraphs-icon-error { - background-image: url("../icons/icon-error.svg"); -} - -@media (max-width: 768px) { - .button.paragraphs-icon-button { - padding: 0; - width: 36px !important; - height: 36px; - background-position: center !important; - background-repeat: no-repeat !important; - background-size: 24px 24px; - border-radius: 4px; - text-indent: -9999em; - } - .button.paragraphs-icon-button-collapse, .button.paragraphs-icon-button-collapse:active, .button.paragraphs-icon-button-collapse:hover, .button.paragraphs-icon-button-collapse:focus { - background-image: url("../icons/icon-collapse.svg"); - } - .button.paragraphs-icon-button-collapse:disabled, .button.paragraphs-icon-button-collapse:disabled:active { - background-image: url("../icons/icon-collapse-disabled.svg"); - } - .button.paragraphs-icon-button-edit, .button.paragraphs-icon-button-edit:active, .button.paragraphs-icon-button-edit:hover, .button.paragraphs-icon-button-edit:focus { - background-image: url("../icons/icon-edit.svg"); - } - .button.paragraphs-icon-button-edit:disabled, .button.paragraphs-icon-button-edit:disabled:active { - background-image: url("../icons/icon-edit-disabled.svg"); - } -} - -.paragraph-type-info { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; -} - -.paragraphs-tabs-wrapper .paragraphs-tabs { - display: none; -} - -.js .field--widget-paragraphs th .paragraphs-actions { - float: right; - margin-right: -11px; -} - -.js .field--widget-paragraphs .paragraphs-dropbutton-wrapper { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - -.js .field--widget-paragraphs .dropbutton-wrapper { - width: auto; - margin-right: 0; - padding-right: 0; -} - -.js .field--widget-paragraphs .dropbutton-widget { - position: static; -} - -.js .field--widget-paragraphs .field-multiple-table { - margin-bottom: 10px; -} - -.js .field--widget-paragraphs td { - padding: 10px 0; -} - -.js .field--widget-paragraphs .field-multiple-drag { - vertical-align: top; -} - -.js .field--widget-paragraphs .draggable .tabledrag-handle { - margin-left: 0; -} - -.js .field--widget-paragraphs .tabledrag-handle .handle { - height: 22px; -} - -.js .field--widget-paragraphs .delta-order { - padding-right: 10px; - text-align: right; -} - -.js .paragraph-type-top { - display: -ms-grid; - display: grid; - -ms-grid-columns: 100px auto 1fr auto; - grid-template-columns: 100px auto 1fr auto; - -ms-grid-rows: auto; - grid-template-rows: auto; - grid-gap: 5px 5px; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; -} - -.js .paragraph-type-top .paragraph-type-title { - grid-column: 1 / 2; - -ms-grid-row-span: 1; - -ms-grid-row: 1; - grid-row: 1 / span 1; -} - -.js .paragraph-type-top .paragraph-type-info { - grid-column: 2 / 3; - -ms-grid-column: 2; - -ms-grid-row-span: 1; - -ms-grid-row: 1; - grid-row: 1 / span 1; -} - -.js .paragraph-type-top .paragraph-type-summary { - grid-column: 1 / 5; - -ms-grid-column: 1; - -ms-grid-column-span: 5; - -ms-grid-row-span: 1; - -ms-grid-row: 2; - grid-row: 2 / span 1; -} - -@media (min-width: 600px) { - .js .paragraph-type-top .paragraph-type-summary { - grid-column: 3 / 4; - -ms-grid-column: 3; - -ms-grid-column-span: 1; - -ms-grid-row-span: 1; - -ms-grid-row: 1; - grid-row: 1 / span 1; - } -} - -.js .paragraph-type-top .paragraphs-actions { - grid-column: 3 / 5; - -ms-grid-column: 3; - -ms-grid-column-span: 2; - -ms-grid-row-span: 1; - -ms-grid-row: 1; - grid-row: 1 / span 1; - justify-self: end; - -ms-grid-column-align: end; -} - -@media (min-width: 600px) { - .js .paragraph-type-top .paragraphs-actions { - grid-column: 4 / 5; - -ms-grid-column: 4; - -ms-grid-column-span: 1; - } -} - -.js .paragraphs-subform { - margin-top: 5px; -} - -.js .paragraphs-collapsed-description { - position: relative; - height: 1.538em; - overflow: hidden; - color: #777; - word-break: break-all; -} - -.js .paragraphs-collapsed-description::after { - display: block; - position: absolute; - top: 0; - right: 0; - width: 3em; - background: -webkit-gradient(linear, left top, right top, from(rgba(255, 255, 255, 0)), to(#fff)); - background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #fff 100%); - content: "\00a0"; -} - -.draggable:hover .paragraphs-collapsed-description::after { - background: -webkit-gradient(linear, left top, right top, from(rgba(247, 252, 255, 0)), to(#f7fcff)); - background: linear-gradient(to right, rgba(247, 252, 255, 0) 0%, #f7fcff 100%); -} - -.drag-previous .paragraphs-collapsed-description::after { - background: -webkit-gradient(linear, left top, right top, from(rgba(255, 255, 221, 0)), to(#ffd)); - background: linear-gradient(to right, rgba(255, 255, 221, 0) 0%, #ffd 100%); -} - -tr:hover .paragraphs-collapsed-description::after { - background: -webkit-gradient(linear, left top, right top, from(rgba(255, 255, 221, 0)), to(#f7fcff)); - background: linear-gradient(to right, rgba(255, 255, 221, 0) 0%, #f7fcff 100%); -} - -.js .paragraph-type-title { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-basis: 100px; - -ms-flex-preferred-size: 100px; - flex-basis: 100px; - min-width: 100px; -} - -.js .paragraph-type-icon { - padding-right: 5px; - height: 16px; - width: 16px; -} - -.js .paragraph-type-label { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -@media (min-width: 600px) { - .js .paragraph-type-add-modal { - width: 100%; - padding: 10px 0; - height: 30px; - margin-top: -1.8em; - margin-bottom: -0.2em; - display: inline; - } -} - -.js .paragraph-type-add-modal-button { - display: inline-block; - margin: 0 auto; -} - -.js .paragraph-type-add-modal-button:hover { - color: #ffffff; - background: #057ec7 none; -} - -.paragraph--view-mode--preview { - padding-right: 1em; -} - -.is-horizontal .paragraphs-tabs .tabs__tab { - border-bottom: 0; -} diff --git a/web/modules/contrib/paragraphs/css/paragraphs.widget.scss b/web/modules/contrib/paragraphs/css/paragraphs.widget.scss deleted file mode 100644 index 609bef911..000000000 --- a/web/modules/contrib/paragraphs/css/paragraphs.widget.scss +++ /dev/null @@ -1,266 +0,0 @@ -// -// @file -// Experimental paragraphs widget CSS. -// - -@import "variables"; - -.paragraphs { - // Paragraphs state information icons. - &-icon { - display: inline-block; - height: $info-size; - width: $info-size; - background: no-repeat center; - background-size: $info-icon-size $info-icon-size; - - $icons: view edit-info edit-disabled delete delete-disabled lock changed collapse warning error; - @each $icon in $icons { - &-#{$icon} { - background-image: url('../icons/icon-#{$icon}.svg'); - } - } - } -} - -// Paragraphs button icon support for small screens. -@media (max-width: map-get($grid-breakpoints, 'md')) { - .button.paragraphs-icon-button { - padding: 0; - width: $action-size-big !important; - height: $action-size-big; - // We need important flag here to easily override many specific rules from - // seven buttons.css. - background-position: center !important; - background-repeat: no-repeat !important; - background-size: $action-icon-size $action-icon-size; - border-radius: $action-border-radius; - text-indent: -9999em; - - $icons: collapse edit; - @each $icon in $icons { - &-#{$icon}, - &-#{$icon}:active, - &-#{$icon}:hover, - &-#{$icon}:focus { - background-image: url('../icons/icon-#{$icon}.svg'); - } - - &-#{$icon}:disabled, - &-#{$icon}:disabled:active { - background-image: url('../icons/icon-#{$icon}-disabled.svg'); - } - } - } -} - -.paragraph-type-info { - display: flex; -} - -.paragraphs-tabs-wrapper { - .paragraphs-tabs { - display: none; - } -} - -// We are using .js prefix here mainly because we want to apply this style rules -// only for JS version of a widget. -.js { - .field--widget-paragraphs { - th .paragraphs-actions { - float: right; - // Table th padding is 12px but for some weird reason here we need to do - // -11px to return it back. - margin-right: -11px; - } - - .paragraphs-dropbutton-wrapper { - // We are using inline-flex here so 'Add type' dropdown button is inline - // and aligned 'to type' text. - display: inline-flex; - } - - .dropbutton-wrapper { - // Override 600px breakpoint from core, needed again so 'to type' is in - // the same line with add dropdown button. - width: auto; - - // Reset some CSS that are coming from core. - margin-right: 0; - padding-right: 0; - } - - // Reset some CSS that are coming from core. - .dropbutton-widget { - position: static; - } - - .field-multiple-table { - margin-bottom: 10px; - } - - td { // stylelint-disable-line selector-no-type - padding: 10px 0; - } - - .field-multiple-drag { - vertical-align: top; - } - - .draggable .tabledrag-handle { - margin-left: 0; - } - - .tabledrag-handle .handle { - height: 22px; - } - - .delta-order { - padding-right: 10px; - text-align: right; - } - } - - .paragraph-type-top { - display: grid; - grid-template-columns: 100px auto 1fr auto; - grid-template-rows: auto; - grid-gap: $gutter-top $gutter-top; - align-items: center; - - .paragraph-type-title { - grid-column: 1 / 2; - // PostCSS autoprefixer does not support -ms-grid-column and some other - // attributes that we need. This support will not be improved so we need - // to do it manually for IE. - -ms-grid-column: 1; - grid-row: 1 / span 1; - } - - .paragraph-type-info { - grid-column: 2 / 3; - -ms-grid-column: 2; - grid-row: 1 / span 1; - } - - .paragraph-type-summary { - grid-column: 1 / 5; - -ms-grid-column: 1; - -ms-grid-column-span: 5; - grid-row: 2 / span 1; - - @media (min-width: map-get($grid-breakpoints, 'sm')) { - grid-column: 3 / 4; - -ms-grid-column: 3; - -ms-grid-column-span: 1; - grid-row: 1 / span 1; - } - } - - .paragraphs-actions { - grid-column: 3 / 5; - -ms-grid-column: 3; - -ms-grid-column-span: 2; - grid-row: 1 / span 1; - justify-self: end; - -ms-grid-column-align: end; - - @media (min-width: map-get($grid-breakpoints, 'sm')) { - grid-column: 4 / 5; - -ms-grid-column: 4; - -ms-grid-column-span: 1; - } - } - } - - .paragraphs-subform { - margin-top: 5px; - } - - .paragraphs-collapsed-description { - position: relative; - height: 1.538em; - overflow: hidden; - color: #777; - word-break: break-all; - - // Fade out text element. - &::after { - display: block; - position: absolute; - top: 0; - right: 0; - width: 3em; - background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #fff 100%); - content: "\00a0"; - } - - @at-root .draggable:hover .paragraphs-collapsed-description { - &::after { - background: linear-gradient(to right, rgba(247, 252, 255, 0) 0%, #f7fcff 100%); - } - } - - @at-root .drag-previous .paragraphs-collapsed-description { - &::after { - background: linear-gradient(to right, rgba(255, 255, 221, 0) 0%, #ffd 100%); - } - } - - @at-root tr:hover .paragraphs-collapsed-description { - &::after { - background: linear-gradient(to right, rgba(255, 255, 221, 0) 0%, #f7fcff 100%); - } - } - } - - .paragraph-type { - &-title { - display: flex; - flex-basis: 100px; - min-width: 100px; - } - - &-icon { - padding-right: $gutter-top; - height: $info-icon-size; - width: $info-icon-size; - } - - &-label { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - } - - @media (min-width: map-get($grid-breakpoints, 'sm')) { - .paragraph-type-add-modal { - width: 100%; - padding: 10px 0; - height: 30px; - margin-top: -1.8em; - margin-bottom: -0.2em; - display: inline; - } - } - - .paragraph-type-add-modal-button { - display: inline-block; - margin: 0 auto; - } - - .paragraph-type-add-modal-button:hover { - color: #ffffff; - background: #057ec7 none; - } -} - -.paragraph--view-mode--preview { - padding-right: 1em; -} - -.is-horizontal .paragraphs-tabs .tabs__tab { - border-bottom: 0; -} diff --git a/web/modules/contrib/paragraphs/icons/icon-actions.svg b/web/modules/contrib/paragraphs/icons/icon-actions.svg deleted file mode 100644 index ec0e6efbe..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-actions.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> -<path class="bg" d="M0 0h24v24H0z" fill="none"/> -<path class="icon" d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" fill="#787878"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-changed.svg b/web/modules/contrib/paragraphs/icons/icon-changed.svg deleted file mode 100644 index c183c5dc8..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-changed.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> - <path class="icon" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" fill="#e29700"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-collapse-disabled.svg b/web/modules/contrib/paragraphs/icons/icon-collapse-disabled.svg deleted file mode 100644 index f9312b2d3..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-collapse-disabled.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="icon" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" fill="#afafaf"/> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> -</svg> \ No newline at end of file diff --git a/web/modules/contrib/paragraphs/icons/icon-collapse.svg b/web/modules/contrib/paragraphs/icons/icon-collapse.svg deleted file mode 100644 index 8a039b3d8..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-collapse.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="icon" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" fill="#787878"/> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> -</svg> \ No newline at end of file diff --git a/web/modules/contrib/paragraphs/icons/icon-delete-disabled.svg b/web/modules/contrib/paragraphs/icons/icon-delete-disabled.svg deleted file mode 100644 index b1b7ba425..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-delete-disabled.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="icon" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="#afafaf"/> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-delete.svg b/web/modules/contrib/paragraphs/icons/icon-delete.svg deleted file mode 100644 index 293b62613..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-delete.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="icon" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="#1d84c3"/> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-edit-disabled.svg b/web/modules/contrib/paragraphs/icons/icon-edit-disabled.svg deleted file mode 100644 index f2e871c98..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-edit-disabled.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="icon" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" fill="#afafaf"/> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-edit-info.svg b/web/modules/contrib/paragraphs/icons/icon-edit-info.svg deleted file mode 100644 index 6f55a5b7e..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-edit-info.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="icon" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" fill="#1d84c3"/> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-edit.svg b/web/modules/contrib/paragraphs/icons/icon-edit.svg deleted file mode 100644 index 0ad58517e..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-edit.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="icon" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" fill="#787878"/> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-error.svg b/web/modules/contrib/paragraphs/icons/icon-error.svg deleted file mode 100644 index be227634c..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-error.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> - <path class="icon" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" fill="#e32700"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-lock.svg b/web/modules/contrib/paragraphs/icons/icon-lock.svg deleted file mode 100644 index 535ab6919..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-lock.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path d="M0 0h24v24H0z" fill="none"/> - <path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" fill="#1d84c3"/> -</svg> \ No newline at end of file diff --git a/web/modules/contrib/paragraphs/icons/icon-view.svg b/web/modules/contrib/paragraphs/icons/icon-view.svg deleted file mode 100644 index d88023f4c..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-view.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> - <path class="icon" d="M11.5 9C10.12 9 9 10.12 9 11.5s1.12 2.5 2.5 2.5 2.5-1.12 2.5-2.5S12.88 9 11.5 9zM20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-3.21 14.21l-2.91-2.91c-.69.44-1.51.7-2.39.7C9.01 16 7 13.99 7 11.5S9.01 7 11.5 7 16 9.01 16 11.5c0 .88-.26 1.69-.7 2.39l2.91 2.9-1.42 1.42z" fill="#1d84c3"/> -</svg> diff --git a/web/modules/contrib/paragraphs/icons/icon-warning.svg b/web/modules/contrib/paragraphs/icons/icon-warning.svg deleted file mode 100644 index c183c5dc8..000000000 --- a/web/modules/contrib/paragraphs/icons/icon-warning.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path class="bg" d="M0 0h24v24H0z" fill="none"/> - <path class="icon" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" fill="#e29700"/> -</svg> diff --git a/web/modules/contrib/paragraphs/js/paragraphs.actions.js b/web/modules/contrib/paragraphs/js/paragraphs.actions.js deleted file mode 100644 index 3fe7a6cba..000000000 --- a/web/modules/contrib/paragraphs/js/paragraphs.actions.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file - * Paragraphs actions JS code for paragraphs actions button. - */ - -(function ($, Drupal) { - - 'use strict'; - - /** - * Process paragraph_actions elements. - * - * @type {Drupal~behavior} - * - * @prop {Drupal~behaviorAttach} attach - * Attaches paragraphsActions behaviors. - */ - Drupal.behaviors.paragraphsActions = { - attach: function (context, settings) { - var $actionsElement = $(context).find('.paragraphs-dropdown').once('paragraphs-dropdown'); - // Attach event handlers to toggle button. - $actionsElement.each(function () { - var $this = $(this); - var $toggle = $this.find('.paragraphs-dropdown-toggle'); - - $toggle.on('click', function (e) { - e.preventDefault(); - $this.toggleClass('open'); - }); - - $toggle.on('focusout', function (e) { - $this.removeClass('open'); - }); - }); - } - }; - -})(jQuery, Drupal); diff --git a/web/modules/contrib/paragraphs/js/paragraphs.admin.js b/web/modules/contrib/paragraphs/js/paragraphs.admin.js deleted file mode 100644 index 968d334e3..000000000 --- a/web/modules/contrib/paragraphs/js/paragraphs.admin.js +++ /dev/null @@ -1,113 +0,0 @@ -(function ($, Drupal) { - - 'use strict'; - - /** - * Set content fields to visible when tabs are created. After an action - * being performed, stay on the same perspective. - * - * @param $parWidget - * Paragraphs widget. - * @param $parTabs - * Paragraphs tabs. - * @param $parContent - * Paragraphs content tab. - * @param $parBehavior - * Paragraphs behavior tab. - * @param $mainRegion - * Main paragraph region. - */ - var setUpTab = function ($parWidget, $parTabs, $parContent, $parBehavior, $mainRegion) { - var $tabContent = $parTabs.find('#content'); - var $tabBehavior = $parTabs.find('#behavior'); - if ($tabBehavior.hasClass('is-active')) { - $parWidget.removeClass('content-active').addClass('behavior-active'); - $tabContent.removeClass('is-active'); - $tabBehavior.addClass('is-active'); - $parContent.hide(); - $parBehavior.show(); - } - else { - // Activate content tab visually if there is no previously - // activated tab. - if (!($mainRegion.hasClass('content-active')) - && !($mainRegion.hasClass('behavior-active'))) { - $tabContent.addClass('is-active'); - $mainRegion.addClass('content-active'); - } - - $parContent.show(); - $parBehavior.hide(); - - $parTabs.show(); - if ($parBehavior.length === 0) { - $parTabs.hide(); - } - } - }; - - /** - * Switching active class between tabs. - * @param $parTabs - * Paragraphs tabs. - * @param $clickedTab - * Clicked tab. - * @param $parWidget - * Paragraphs widget. - */ - var switchActiveClass = function ($parTabs, $clickedTab, $parWidget) { - $parTabs.find('li').removeClass('is-active'); - $clickedTab.parent('li').addClass('is-active'); - $parWidget.removeClass('behavior-active content-active is-active'); - $($parWidget).find($clickedTab.attr('href')).addClass('is-active'); - - if ($parWidget.find('#content').hasClass('is-active')) { - $parWidget.find('.layout-region-node-main').addClass('content-active'); - $parWidget.find('.paragraphs-content').show(); - $parWidget.find('.paragraphs-behavior').hide(); - } - - if ($parWidget.find('#behavior').hasClass('is-active')) { - $parWidget.find('.layout-region-node-main').addClass('behavior-active'); - $parWidget.find('.paragraphs-content').hide(); - $parWidget.find('.paragraphs-behavior').show(); - } - }; - - /** - * For body tag, adds tabs for selecting how the content will be displayed. - * - * @type {Drupal~behavior} - */ - Drupal.behaviors.bodyTabs = { - attach: function (context) { - var $topLevelParWidgets = $('.paragraphs-tabs-wrapper', context).filter(function() { - return $(this).parents('.paragraphs-nested').length === 0; - }); - - // Initialization. - $topLevelParWidgets.once('paragraphs-bodytabs').each(function() { - var $parWidget = $(this); - var $parTabs = $parWidget.find('.paragraphs-tabs'); - - // Create click event. - $parTabs.find('a').click(function(e) { - e.preventDefault(); - switchActiveClass($parTabs, $(this), $parWidget); - }); - }); - - if ($('.paragraphs-tabs-wrapper', context).length > 0) { - $topLevelParWidgets.each(function() { - var $parWidget = $(this); - var $parTabs = $parWidget.find('.paragraphs-tabs'); - var $parContent = $parWidget.find('.paragraphs-content'); - var $parBehavior = $parWidget.find('.paragraphs-behavior'); - var $mainRegion = $parWidget.find('.layout-region-node-main'); - setUpTab($parWidget, $parTabs, $parContent, $parBehavior, $mainRegion); - }); - } - } - }; -})(jQuery, Drupal); - diff --git a/web/modules/contrib/paragraphs/js/paragraphs.dragdrop.js b/web/modules/contrib/paragraphs/js/paragraphs.dragdrop.js deleted file mode 100644 index 8c6a8ee4a..000000000 --- a/web/modules/contrib/paragraphs/js/paragraphs.dragdrop.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * @file - * Paragraphs drag and drop handling and integration with the Sortable library. - */ - -(function ($, Drupal) { - - 'use strict'; - - /** - * jQuery plugin for Sortable - * - * Registers Sortable under a custom name to prevent a collision with jQuery - * UI. - * - * @param {Object|String} options - * @param {..*} [args] - * @returns {jQuery|*} - */ - $.fn.paragraphsSortable = function (options) { - var retVal, - args = arguments; - - this.each(function () { - var $el = $(this), - sortable = $el.data('sortable'); - - if (!sortable && (options instanceof Object || !options)) { - sortable = new Sortable(this, options); - $el.data('sortable', sortable); - } - - if (sortable) { - if (options === 'widget') { - return sortable; - } - else if (options === 'destroy') { - sortable.destroy(); - $el.removeData('sortable'); - } - else if (typeof sortable[options] === 'function') { - retVal = sortable[options].apply(sortable, [].slice.call(args, 1)); - } - else if (options in sortable.options) { - retVal = sortable.option.apply(sortable, args); - } - } - }); - - return (retVal === void 0) ? this : retVal; - }; - - - Drupal.behaviors.paragraphsDraggable = { - attach: function (context) { - - // Initialize drag and drop. - $('ul.paragraphs-dragdrop', context).each(function (i, item) { - $(item).paragraphsSortable({ - group: "paragraphs", - sort: true, - handle: ".tabledrag-handle", - onMove: isAllowed, - onEnd: handleReorder - }); - }); - - /** - * Callback to update weight and path information. - * - * @param evt - * The Sortable event. - */ - function handleReorder(evt) { - var $item = $(evt.item); - var $parent = $item.closest('.paragraphs-dragdrop'); - var $children = $parent.children('li'); - var $srcParent = $(evt.to); - var $srcChildren = $srcParent.children('li'); - - // Update both the source and target children. - updateWeightsAndPath($srcChildren); - updateWeightsAndPath($children); - } - - - /** - * Update weight and recursively update path of the provided paragraphs. - * - * @param $items - * Drag and drop items. - */ - function updateWeightsAndPath($items) { - $items.each(function (index, value) { - - // Update the weight in the weight of the current element, avoid - // matching child weights by selecting the first. - var $currentItem = $(value); - var $weight = $currentItem.find('.paragraphs-dragdrop__weight:first'); - $weight.val(index); - - // Update the path of the current element and then update all nested - // elements. - updatePaths($currentItem, $currentItem.parent()); - $currentItem.find('> div > ul').each(function () { - updateNestedPath(this, index, $currentItem); - }); - }) - } - - /** - * Update the path field based on the parent. - * - * @param $item - * A list item. - * @param $parent - * The parent of the list item. - */ - function updatePaths($item, $parent) { - // Select the first path field which is the one from the current - // element. - var $pathField = $item.find('.paragraphs-dragdrop__path:first'); - var newPath = $parent.attr('data-paragraphs-dragdrop-path'); - $pathField.val(newPath); - } - - /** - * Update nested paragraphs for a field/list. - * - * @param childList - * The paragraph field/list, parent of the children to be updated. - * @param parentIndex - * The index of the parent list item. - * @param $parentListItem - * The parent list item. - */ - function updateNestedPath(childList, parentIndex, $parentListItem) { - - var sortablePath = childList.getAttribute('data-paragraphs-dragdrop-path'); - var newParent = $parentListItem.parent().attr('data-paragraphs-dragdrop-path'); - - // Update the data attribute of the list based on the parent index and - // list item. - sortablePath = newParent + "][" + parentIndex + sortablePath.substr(sortablePath.lastIndexOf("]")); - childList.setAttribute('data-paragraphs-dragdrop-path', sortablePath); - - // Now update the children. - $(childList).children().each(function (childIndex) { - var $childListItem = $(this); - updatePaths($childListItem, $(childList), childIndex); - $(this).find('> div > ul').each(function () { - var nestedChildList = this; - updateNestedPath(nestedChildList, childIndex, $childListItem); - }); - }); - } - - - /** - * Callback to check if a paragraph item can be dropped into a position. - * - * @param evt - * The Sortable event. - * @param originalEvent - * The original Sortable event. - * - * @returns {boolean|*} - * True if the type is allowed and there is enough room. - */ - function isAllowed(evt, originalEvent) { - var dragee = evt.dragged; - var target = evt.to; - var drageeType = dragee.dataset.paragraphsDragdropBundle; - var allowedTypes = target.dataset.paragraphsDragdropAllowedTypes; - var hasSameContainer = evt.to === evt.from; - return hasSameContainer || (contains(drageeType, allowedTypes) && hasRoom(target)); - } - - /** - * Checks if the target has room. - * - * @param target - * The target list/paragraph field. - * - * @returns {boolean} - * True if the field is unlimited or limit is not reached yet. - */ - function hasRoom(target) { - - var cardinality = target.dataset.paragraphsDragdropCardinality; - var occupants = target.childNodes.length; - var isLimited = parseInt(cardinality, 10) !== -1; - var hasRoom = cardinality > occupants; - - return hasRoom || !isLimited; - } - - /** - * Checks if the paragraph type is allowed in the target type list. - * - * @param candidate - * The paragraph type. - * @param set - * Comma separated list of target types. - * - * @returns {boolean} - * TRUE if the target type is allowed. - */ - function contains(candidate, set) { - set = set.split(','); - var l = set.length; - - for(var i = 0; i < l; i++) { - if(set[i] === candidate) { - return true; - } - } - return false; - } - - // Fix for an iOS 10 bug. Binding empty event handler on the touchmove - // event. - window.addEventListener('touchmove', function () { - }) - } - } - -})(jQuery, Drupal); diff --git a/web/modules/contrib/paragraphs/js/paragraphs.modal.js b/web/modules/contrib/paragraphs/js/paragraphs.modal.js deleted file mode 100644 index 029552591..000000000 --- a/web/modules/contrib/paragraphs/js/paragraphs.modal.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @file paragraphs.modal.js - * - */ - -(function ($, Drupal, drupalSettings) { - - 'use strict'; - - /** - * Click handler for click "Add" button between paragraphs. - * - * @type {Object} - */ - Drupal.behaviors.paragraphsModalAdd = { - attach: function (context) { - $('.paragraph-type-add-modal-button', context) - .once('add-click-handler') - .on('click', function (event) { - var $button = $(this); - var $add_more_wrapper = $button.parent().siblings('.paragraphs-add-dialog'); - Drupal.paragraphsAddModal.openDialog($add_more_wrapper, $button.val()); - - // Stop default execution of click event. - event.preventDefault(); - event.stopPropagation(); - }); - } - }; - - /** - * Namespace for modal related javascript methods. - * - * @type {Object} - */ - Drupal.paragraphsAddModal = {}; - - /** - * Open modal dialog for adding new paragraph in list. - * - * @param {Object} $context - * jQuery element of form wrapper used to submit request for adding new - * paragraph to list. Wrapper also contains dialog template. - * @param {string} title - * The title of the modal form window. - */ - Drupal.paragraphsAddModal.openDialog = function ($context, title) { - - $context.dialog({ - modal: true, - resizable: false, - title: title, - width: 'auto', - close: function () { - var $dialog = $(this); - - // Destroy dialog object. - $dialog.dialog('destroy'); - } - }); - - // Close the dialog after a button was clicked. - $('.field-add-more-submit', $context) - .each(function () { - // Use mousedown event, because we are using ajax in the modal add mode - // which explicitly suppresses the click event. - $(this).on('mousedown', function () { - var $this = $(this); - $this.closest('div.ui-dialog-content').dialog('close'); - }); - }); - }; - -}(jQuery, Drupal, drupalSettings)); diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.node.paragraphed_content_demo.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.node.paragraphed_content_demo.default.yml deleted file mode 100644 index 9cf3de61f..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.node.paragraphed_content_demo.default.yml +++ /dev/null @@ -1,75 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.node.paragraphed_content_demo.field_paragraphs_demo - - node.type.paragraphed_content_demo - module: - - paragraphs -id: node.paragraphed_content_demo.default -targetEntityType: node -bundle: paragraphed_content_demo -mode: default -content: - body: - type: text_textarea_with_summary - weight: 31 - settings: - rows: 9 - summary_rows: 3 - placeholder: '' - third_party_settings: { } - created: - type: datetime_timestamp - weight: 10 - settings: { } - third_party_settings: { } - field_paragraphs_demo: - type: paragraphs - weight: 32 - settings: - title: Paragraph - title_plural: Paragraphs - edit_mode: closed - add_mode: dropdown - form_display_mode: default - default_paragraph_type: '' - third_party_settings: { } - langcode: - type: language_select - weight: 2 - settings: { } - third_party_settings: { } - path: - type: path - weight: 30 - settings: { } - third_party_settings: { } - promote: - type: boolean_checkbox - weight: 15 - settings: - display_label: true - third_party_settings: { } - sticky: - type: boolean_checkbox - weight: 16 - settings: - display_label: true - third_party_settings: { } - title: - type: string_textfield - weight: -5 - settings: - size: 60 - placeholder: '' - third_party_settings: { } - uid: - type: entity_reference_autocomplete - weight: 5 - settings: - match_operator: CONTAINS - size: 60 - placeholder: '' - third_party_settings: { } -hidden: { } diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.image_text.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.image_text.default.yml deleted file mode 100644 index 5f2de3aee..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.image_text.default.yml +++ /dev/null @@ -1,32 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.image_text.field_image_demo - - field.field.paragraph.image_text.field_text_demo - - paragraphs.paragraphs_type.image_text - module: - - image - - text -id: paragraph.image_text.default -targetEntityType: paragraph -bundle: image_text -mode: default -content: - field_image_demo: - weight: 0 - settings: - progress_indicator: throbber - preview_image_style: thumbnail - third_party_settings: { } - type: image_image - field_text_demo: - weight: 1 - settings: - rows: 5 - placeholder: '' - third_party_settings: { } - type: text_textarea -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.images.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.images.default.yml deleted file mode 100644 index 2a436cebf..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.images.default.yml +++ /dev/null @@ -1,23 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.images.field_images_demo - - paragraphs.paragraphs_type.images - module: - - image -id: paragraph.images.default -targetEntityType: paragraph -bundle: images -mode: default -content: - field_images_demo: - weight: 0 - settings: - progress_indicator: throbber - preview_image_style: thumbnail - third_party_settings: { } - type: image_image -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.nested_paragraph.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.nested_paragraph.default.yml deleted file mode 100644 index e479e0510..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.nested_paragraph.default.yml +++ /dev/null @@ -1,27 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.nested_paragraph.field_paragraphs_demo - - paragraphs.paragraphs_type.nested_paragraph - module: - - paragraphs -id: paragraph.nested_paragraph.default -targetEntityType: paragraph -bundle: nested_paragraph -mode: default -content: - field_paragraphs_demo: - weight: 0 - settings: - title: Paragraph - title_plural: Paragraphs - edit_mode: open - add_mode: dropdown - form_display_mode: default - default_paragraph_type: '' - third_party_settings: { } - type: paragraphs -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.text.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.text.default.yml deleted file mode 100644 index 9b32b29f5..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.text.default.yml +++ /dev/null @@ -1,23 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.text.field_text_demo - - paragraphs.paragraphs_type.text - module: - - text -id: paragraph.text.default -targetEntityType: paragraph -bundle: text -mode: default -content: - field_text_demo: - weight: 0 - settings: - rows: 5 - placeholder: '' - third_party_settings: { } - type: text_textarea -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.text_image.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.text_image.default.yml deleted file mode 100644 index 30e9c7825..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.text_image.default.yml +++ /dev/null @@ -1,32 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.text_image.field_image_demo - - field.field.paragraph.text_image.field_text_demo - - paragraphs.paragraphs_type.text_image - module: - - image - - text -id: paragraph.text_image.default -targetEntityType: paragraph -bundle: text_image -mode: default -content: - field_image_demo: - weight: 1 - settings: - progress_indicator: throbber - preview_image_style: thumbnail - third_party_settings: { } - type: image_image - field_text_demo: - weight: 0 - settings: - rows: 5 - placeholder: '' - third_party_settings: { } - type: text_textarea -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.user.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.user.default.yml deleted file mode 100644 index a7515b629..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_form_display.paragraph.user.default.yml +++ /dev/null @@ -1,23 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.user.field_user_demo - - paragraphs.paragraphs_type.user -id: paragraph.user.default -targetEntityType: paragraph -bundle: user -mode: default -content: - field_user_demo: - weight: 0 - settings: - match_operator: CONTAINS - size: 60 - placeholder: '' - third_party_settings: { } - type: entity_reference_autocomplete -hidden: - created: true - status: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.node.paragraphed_content_demo.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.node.paragraphed_content_demo.default.yml deleted file mode 100644 index 67ff30206..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.node.paragraphed_content_demo.default.yml +++ /dev/null @@ -1,34 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.node.paragraphed_content_demo.field_paragraphs_demo - - node.type.paragraphed_content_demo - module: - - entity_reference_revisions - - user -id: node.paragraphed_content_demo.default -targetEntityType: node -bundle: paragraphed_content_demo -mode: default -content: - body: - type: text_default - weight: 101 - settings: { } - third_party_settings: { } - label: hidden - field_paragraphs_demo: - type: entity_reference_revisions_entity_view - weight: 102 - settings: - view_mode: default - link: '' - third_party_settings: { } - label: hidden - links: - weight: 100 - settings: { } - third_party_settings: { } -hidden: - langcode: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.node.paragraphed_content_demo.teaser.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.node.paragraphed_content_demo.teaser.yml deleted file mode 100644 index 8ea77d72e..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.node.paragraphed_content_demo.teaser.yml +++ /dev/null @@ -1,25 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - core.entity_view_mode.node.teaser - - field.field.node.paragraphed_content_demo.field_paragraphs_demo - - node.type.paragraphed_content_demo - module: - - user -id: node.paragraphed_content_demo.teaser -targetEntityType: node -bundle: paragraphed_content_demo -mode: teaser -content: - body: - label: hidden - type: text_summary_or_trimmed - weight: 101 - settings: - trim_length: 600 - third_party_settings: { } - links: - weight: 100 -hidden: - field_paragraphs_demo: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.image_text.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.image_text.default.yml deleted file mode 100644 index 85e37db05..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.image_text.default.yml +++ /dev/null @@ -1,32 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.image_text.field_image_demo - - field.field.paragraph.image_text.field_text_demo - - paragraphs.paragraphs_type.image_text - module: - - image - - text -id: paragraph.image_text.default -targetEntityType: paragraph -bundle: image_text -mode: default -content: - field_image_demo: - type: image - weight: 0 - settings: - image_style: large - image_link: '' - third_party_settings: { } - label: hidden - field_text_demo: - type: text_default - weight: 1 - settings: { } - third_party_settings: { } - label: hidden -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.images.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.images.default.yml deleted file mode 100644 index 1f0bb288d..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.images.default.yml +++ /dev/null @@ -1,24 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.images.field_images_demo - - paragraphs.paragraphs_type.images - module: - - image -id: paragraph.images.default -targetEntityType: paragraph -bundle: images -mode: default -content: - field_images_demo: - type: image - weight: 0 - settings: - image_style: medium - image_link: file - third_party_settings: { } - label: hidden -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.nested_paragraph.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.nested_paragraph.default.yml deleted file mode 100644 index 7f59fd217..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.nested_paragraph.default.yml +++ /dev/null @@ -1,24 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.nested_paragraph.field_paragraphs_demo - - paragraphs.paragraphs_type.nested_paragraph - module: - - entity_reference_revisions -id: paragraph.nested_paragraph.default -targetEntityType: paragraph -bundle: nested_paragraph -mode: default -content: - field_paragraphs_demo: - weight: 0 - label: hidden - settings: - view_mode: default - link: '' - third_party_settings: { } - type: entity_reference_revisions_entity_view -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.text.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.text.default.yml deleted file mode 100644 index 352b4d85c..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.text.default.yml +++ /dev/null @@ -1,22 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.text.field_text_demo - - paragraphs.paragraphs_type.text - module: - - text -id: paragraph.text.default -targetEntityType: paragraph -bundle: text -mode: default -content: - field_text_demo: - type: text_default - weight: 0 - settings: { } - third_party_settings: { } - label: hidden -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.text_image.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.text_image.default.yml deleted file mode 100644 index ce8606143..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.text_image.default.yml +++ /dev/null @@ -1,32 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.text_image.field_image_demo - - field.field.paragraph.text_image.field_text_demo - - paragraphs.paragraphs_type.text_image - module: - - image - - text -id: paragraph.text_image.default -targetEntityType: paragraph -bundle: text_image -mode: default -content: - field_image_demo: - type: image - weight: 1 - settings: - image_style: '' - image_link: '' - third_party_settings: { } - label: hidden - field_text_demo: - type: text_default - weight: 0 - settings: { } - third_party_settings: { } - label: hidden -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.user.default.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.user.default.yml deleted file mode 100644 index 2c4ea1001..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/core.entity_view_display.paragraph.user.default.yml +++ /dev/null @@ -1,21 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.user.field_user_demo - - paragraphs.paragraphs_type.user -id: paragraph.user.default -targetEntityType: paragraph -bundle: user -mode: default -content: - field_user_demo: - weight: 0 - label: above - settings: - link: true - third_party_settings: { } - type: entity_reference_label -hidden: - created: true - uid: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.node.paragraphed_content_demo.field_paragraphs_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.node.paragraphed_content_demo.field_paragraphs_demo.yml deleted file mode 100644 index 099d91c75..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.node.paragraphed_content_demo.field_paragraphs_demo.yml +++ /dev/null @@ -1,42 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.node.field_paragraphs_demo - - node.type.paragraphed_content_demo - module: - - entity_reference_revisions -id: node.paragraphed_content_demo.field_paragraphs_demo -field_name: field_paragraphs_demo -entity_type: node -bundle: paragraphed_content_demo -label: Paragraphs -description: '' -required: false -translatable: false -default_value: { } -default_value_callback: '' -settings: - handler: 'default:paragraph' - handler_settings: - target_bundles: null - target_bundles_drag_drop: - image_text: - enabled: false - weight: -13 - images: - enabled: false - weight: -12 - nested_paragraph: - enabled: false - weight: -11 - text: - enabled: false - weight: -10 - text_image: - enabled: false - weight: -9 - user: - enabled: false - weight: -8 -field_type: entity_reference_revisions diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.image_text.field_image_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.image_text.field_image_demo.yml deleted file mode 100644 index ed345b539..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.image_text.field_image_demo.yml +++ /dev/null @@ -1,36 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_image_demo - - paragraphs.paragraphs_type.image_text - module: - - image -id: paragraph.image_text.field_image_demo -field_name: field_image_demo -entity_type: paragraph -bundle: image_text -label: Image -description: '' -required: false -translatable: true -default_value: { } -default_value_callback: '' -settings: - file_directory: '' - file_extensions: 'png gif jpg jpeg' - max_filesize: '' - max_resolution: '' - min_resolution: '' - alt_field: false - alt_field_required: false - title_field: false - title_field_required: false - default_image: - uuid: '' - alt: '' - title: '' - width: null - height: null - handler: default -field_type: image diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.image_text.field_text_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.image_text.field_text_demo.yml deleted file mode 100644 index 425f08967..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.image_text.field_text_demo.yml +++ /dev/null @@ -1,20 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_text_demo - - paragraphs.paragraphs_type.image_text - module: - - text -id: paragraph.image_text.field_text_demo -field_name: field_text_demo -entity_type: paragraph -bundle: image_text -label: Text -description: '' -required: false -translatable: true -default_value: { } -default_value_callback: '' -settings: { } -field_type: text_long diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.images.field_images_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.images.field_images_demo.yml deleted file mode 100644 index dd0080ce6..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.images.field_images_demo.yml +++ /dev/null @@ -1,36 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_images_demo - - paragraphs.paragraphs_type.images - module: - - image -id: paragraph.images.field_images_demo -field_name: field_images_demo -entity_type: paragraph -bundle: images -label: 'Images' -description: '' -required: false -translatable: false -default_value: { } -default_value_callback: '' -settings: - file_directory: '' - file_extensions: 'png gif jpg jpeg' - max_filesize: '' - max_resolution: '' - min_resolution: '' - alt_field: false - alt_field_required: false - title_field: false - title_field_required: false - default_image: - uuid: '' - alt: '' - title: '' - width: null - height: null - handler: default -field_type: image diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.nested_paragraph.field_paragraphs_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.nested_paragraph.field_paragraphs_demo.yml deleted file mode 100644 index 00e54c1e5..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.nested_paragraph.field_paragraphs_demo.yml +++ /dev/null @@ -1,42 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_paragraphs_demo - - paragraphs.paragraphs_type.nested_paragraph - module: - - entity_reference_revisions -id: paragraph.nested_paragraph.field_paragraphs_demo -field_name: field_paragraphs_demo -entity_type: paragraph -bundle: nested_paragraph -label: 'Nested Content' -description: '' -required: false -translatable: false -default_value: { } -default_value_callback: '' -settings: - handler: 'default:paragraph' - handler_settings: - target_bundles: null - target_bundles_drag_drop: - images: - weight: -13 - enabled: false - image_text: - weight: -12 - enabled: false - nested_paragraph: - weight: -11 - enabled: false - text: - weight: -10 - enabled: false - text_image: - weight: -9 - enabled: false - user: - weight: -8 - enabled: false -field_type: entity_reference_revisions diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text.field_text_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text.field_text_demo.yml deleted file mode 100644 index e4e6a3101..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text.field_text_demo.yml +++ /dev/null @@ -1,20 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_text_demo - - paragraphs.paragraphs_type.text - module: - - text -id: paragraph.text.field_text_demo -field_name: field_text_demo -entity_type: paragraph -bundle: text -label: Text -description: '' -required: true -translatable: true -default_value: { } -default_value_callback: '' -settings: { } -field_type: text_long diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text_image.field_image_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text_image.field_image_demo.yml deleted file mode 100644 index e20e71f89..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text_image.field_image_demo.yml +++ /dev/null @@ -1,44 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_image_demo - - paragraphs.paragraphs_type.text_image - module: - - content_translation - - image -third_party_settings: - content_translation: - translation_sync: - alt: alt - title: title - file: '0' -id: paragraph.text_image.field_image_demo -field_name: field_image_demo -entity_type: paragraph -bundle: text_image -label: Image -description: '' -required: false -translatable: true -default_value: { } -default_value_callback: '' -settings: - file_directory: '' - file_extensions: 'png gif jpg jpeg' - max_filesize: '' - max_resolution: '' - min_resolution: '' - alt_field: false - alt_field_required: false - title_field: false - title_field_required: false - default_image: - uuid: '' - alt: '' - title: '' - width: null - height: null - handler: default - handler_settings: { } -field_type: image diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text_image.field_text_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text_image.field_text_demo.yml deleted file mode 100644 index 89f5e03f3..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.text_image.field_text_demo.yml +++ /dev/null @@ -1,20 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_text_demo - - paragraphs.paragraphs_type.text_image - module: - - text -id: paragraph.text_image.field_text_demo -field_name: field_text_demo -entity_type: paragraph -bundle: text_image -label: Text -description: '' -required: false -translatable: true -default_value: { } -default_value_callback: '' -settings: { } -field_type: text_long diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.user.field_user_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.user.field_user_demo.yml deleted file mode 100644 index 8a965cdf0..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.field.paragraph.user.field_user_demo.yml +++ /dev/null @@ -1,25 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_user_demo - - paragraphs.paragraphs_type.user -id: paragraph.user.field_user_demo -field_name: field_user_demo -entity_type: paragraph -bundle: user -label: User -description: '' -required: false -translatable: true -default_value: { } -default_value_callback: '' -settings: - handler: 'default:user' - handler_settings: - filter: - type: _none - target_bundles: null - sort: - field: _none -field_type: entity_reference diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.node.field_paragraphs_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.node.field_paragraphs_demo.yml deleted file mode 100644 index d746d5e1c..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.node.field_paragraphs_demo.yml +++ /dev/null @@ -1,18 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - entity_reference_revisions - - node -id: node.field_paragraphs_demo -field_name: field_paragraphs_demo -entity_type: node -type: entity_reference_revisions -settings: - target_type: paragraph -module: entity_reference_revisions -locked: false -cardinality: -1 -translatable: true -indexes: { } -persist_with_no_fields: false diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_image_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_image_demo.yml deleted file mode 100644 index 3f2278e62..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_image_demo.yml +++ /dev/null @@ -1,27 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - image - - paragraphs -id: paragraph.field_image_demo -field_name: field_image_demo -entity_type: paragraph -type: image -settings: - uri_scheme: public - default_image: - uuid: '' - alt: '' - title: '' - width: null - height: null - target_type: file - display_field: false - display_default: false -module: image -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_images_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_images_demo.yml deleted file mode 100644 index 18d6b60f5..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_images_demo.yml +++ /dev/null @@ -1,27 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - image - - paragraphs -id: paragraph.field_images_demo -field_name: field_images_demo -entity_type: paragraph -type: image -settings: - uri_scheme: public - default_image: - uuid: '' - alt: '' - title: '' - width: null - height: null - target_type: file - display_field: false - display_default: false -module: image -locked: false -cardinality: -1 -translatable: true -indexes: { } -persist_with_no_fields: false diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_paragraphs_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_paragraphs_demo.yml deleted file mode 100644 index 580cae6f7..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_paragraphs_demo.yml +++ /dev/null @@ -1,18 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - entity_reference_revisions - - paragraphs -id: paragraph.field_paragraphs_demo -field_name: field_paragraphs_demo -entity_type: paragraph -type: entity_reference_revisions -settings: - target_type: paragraph -module: entity_reference_revisions -locked: false -cardinality: -1 -translatable: true -indexes: { } -persist_with_no_fields: false diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_text_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_text_demo.yml deleted file mode 100644 index a6b64c75e..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_text_demo.yml +++ /dev/null @@ -1,17 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - paragraphs - - text -id: paragraph.field_text_demo -field_name: field_text_demo -entity_type: paragraph -type: text_long -settings: { } -module: text -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_user_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_user_demo.yml deleted file mode 100644 index b94c2fe71..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/field.storage.paragraph.field_user_demo.yml +++ /dev/null @@ -1,18 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - paragraphs - - user -id: paragraph.field_user_demo -field_name: field_user_demo -entity_type: paragraph -type: entity_reference -settings: - target_type: user -module: core -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.node.paragraphed_content_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.node.paragraphed_content_demo.yml deleted file mode 100644 index 71ac888a8..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.node.paragraphed_content_demo.yml +++ /dev/null @@ -1,15 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - node.type.paragraphed_content_demo - module: - - content_translation -third_party_settings: - content_translation: - enabled: true -id: node.paragraphed_content_demo -target_entity_type_id: node -target_bundle: paragraphed_content_demo -default_langcode: site_default -language_alterable: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.image_text.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.image_text.yml deleted file mode 100644 index 02626d6d3..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.image_text.yml +++ /dev/null @@ -1,15 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - paragraphs.paragraphs_type.image_text - module: - - content_translation -third_party_settings: - content_translation: - enabled: true -id: paragraph.image_text -target_entity_type_id: paragraph -target_bundle: image_text -default_langcode: site_default -language_alterable: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.images.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.images.yml deleted file mode 100644 index 0da7eacc4..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.images.yml +++ /dev/null @@ -1,15 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - paragraphs.paragraphs_type.images - module: - - content_translation -third_party_settings: - content_translation: - enabled: true -id: paragraph.images -target_entity_type_id: paragraph -target_bundle: images -default_langcode: site_default -language_alterable: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.text.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.text.yml deleted file mode 100644 index ad58d72f2..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.text.yml +++ /dev/null @@ -1,15 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - paragraphs.paragraphs_type.text - module: - - content_translation -third_party_settings: - content_translation: - enabled: true -id: paragraph.text -target_entity_type_id: paragraph -target_bundle: text -default_langcode: site_default -language_alterable: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.text_image.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.text_image.yml deleted file mode 100644 index 43a118b7d..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.text_image.yml +++ /dev/null @@ -1,15 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - paragraphs.paragraphs_type.text_image - module: - - content_translation -third_party_settings: - content_translation: - enabled: true -id: paragraph.text_image -target_entity_type_id: paragraph -target_bundle: text_image -default_langcode: site_default -language_alterable: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.user.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.user.yml deleted file mode 100644 index 6c4308504..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/language.content_settings.paragraph.user.yml +++ /dev/null @@ -1,15 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - paragraphs.paragraphs_type.user - module: - - content_translation -third_party_settings: - content_translation: - enabled: true -id: paragraph.user -target_entity_type_id: paragraph -target_bundle: user -default_langcode: site_default -language_alterable: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/node.type.paragraphed_content_demo.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/node.type.paragraphed_content_demo.yml deleted file mode 100644 index 7ecec34d7..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/node.type.paragraphed_content_demo.yml +++ /dev/null @@ -1,11 +0,0 @@ -langcode: en -status: true -dependencies: { } -third_party_settings: { } -name: 'Paragraphed article' -type: paragraphed_content_demo -description: 'Article with Paragraphs.' -help: '' -new_revision: false -preview_mode: 1 -display_submitted: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.image_text.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.image_text.yml deleted file mode 100644 index 194e577ee..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.image_text.yml +++ /dev/null @@ -1,6 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: image_text -label: 'Image + Text' -description: 'Use <em>Image + Text</em> for adding an image on the left and a text on the right.' diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.images.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.images.yml deleted file mode 100644 index 2ea625722..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.images.yml +++ /dev/null @@ -1,6 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: images -label: Images -description: 'Use <em>Images</em> for adding one or multiple images.' \ No newline at end of file diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.nested_paragraph.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.nested_paragraph.yml deleted file mode 100644 index 7ba3e7a68..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.nested_paragraph.yml +++ /dev/null @@ -1,6 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: nested_paragraph -label: 'Nested Paragraph' -description: 'Use <em>Nested Paragraph</em> for nesting more paragraphs inside.' diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.text.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.text.yml deleted file mode 100644 index 7d448073e..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.text.yml +++ /dev/null @@ -1,6 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: text -label: Text -description: 'Use <em>Text</em> for adding a text.' diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.text_image.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.text_image.yml deleted file mode 100644 index 260d688c2..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.text_image.yml +++ /dev/null @@ -1,6 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: text_image -label: 'Text + Image' -description: 'Use <em>Test + Image</em> for adding a text on the left and an image on the right.' \ No newline at end of file diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.user.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.user.yml deleted file mode 100644 index 0883d401d..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/install/paragraphs.paragraphs_type.user.yml +++ /dev/null @@ -1,6 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: user -label: User -description: 'Use <em>User</em> for adding a reference to an existing user.' diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/language.entity.de.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/language.entity.de.yml deleted file mode 100644 index af30cc513..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/language.entity.de.yml +++ /dev/null @@ -1,8 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: de -label: German -direction: ltr -weight: 1 -locked: false diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/language.entity.fr.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/language.entity.fr.yml deleted file mode 100644 index f58516466..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/language.entity.fr.yml +++ /dev/null @@ -1,8 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: fr -label: French -direction: ltr -weight: 2 -locked: false diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/search_api.index.paragraphs_demo_index.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/search_api.index.paragraphs_demo_index.yml deleted file mode 100644 index c8cac282b..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/search_api.index.paragraphs_demo_index.yml +++ /dev/null @@ -1,61 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.node.field_paragraphs_demo - - field.storage.paragraph.field_text_demo - - field.storage.paragraph.field_paragraphs_demo - - search_api.server.paragraphs_demo_server - module: - - paragraphs - - search_api - - node -_core: - default_config_hash: B_uQ3elh-QzaQ7uk45NlfZyN4HBV3s2azPvbHDF1Xxw -id: paragraphs_demo_index -name: 'Paragraphs demo index' -description: null -read_only: false -field_settings: - field_text_demo: - label: 'Paragraphs » Paragraph » Text' - datasource_id: 'entity:node' - property_path: 'field_paragraphs_demo:entity:field_text_demo' - type: text - dependencies: - config: - - field.storage.node.field_paragraphs_demo - - field.storage.paragraph.field_text_demo - module: - - paragraphs - field_text_demo_1: - label: 'Paragraphs » Paragraph » Nested Content » Paragraph » Text' - datasource_id: 'entity:node' - property_path: 'field_paragraphs_demo:entity:field_paragraphs_demo:entity:field_text_demo' - type: text - dependencies: - config: - - field.storage.node.field_paragraphs_demo - - field.storage.paragraph.field_paragraphs_demo - - field.storage.paragraph.field_text_demo - module: - - paragraphs - - paragraphs -datasource_settings: - 'entity:node': - bundles: - default: true - selected: { } - languages: - default: true - selected: { } -processor_settings: - aggregated_field: { } - rendered_item: { } - add_url: { } -tracker_settings: - default: { } -options: - index_directly: true - cron_limit: 50 -server: paragraphs_demo_server diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/search_api.server.paragraphs_demo_server.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/search_api.server.paragraphs_demo_server.yml deleted file mode 100644 index f484c4331..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/search_api.server.paragraphs_demo_server.yml +++ /dev/null @@ -1,16 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - search_api_db -id: paragraphs_demo_server -name: 'Paragraphs demo server' -description: '' -backend: search_api_db -backend_config: - database: 'default:default' - min_chars: 1 - partial_matches: false - autocomplete: - suggest_suffix: true - suggest_words: true diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/views.view.paragraphs_search.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/views.view.paragraphs_search.yml deleted file mode 100644 index c78e09a1d..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/config/optional/views.view.paragraphs_search.yml +++ /dev/null @@ -1,226 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - search_api.index.paragraphs_demo_index - module: - - search_api - - text -id: paragraphs_search -label: 'Paragraphs search' -module: views -description: '' -tag: '' -base_table: search_api_index_paragraphs_demo_index -base_field: search_api_id -core: 8.x -display: - default: - display_plugin: default - id: default - display_title: Master - position: 0 - display_options: - access: - type: none - options: { } - cache: - type: tag - options: { } - query: - type: views_query - options: { } - exposed_form: - type: input_required - options: - submit_button: Apply - reset_button: false - reset_button_label: Reset - exposed_sorts_label: 'Sort by' - expose_sort_order: true - sort_asc_label: Asc - sort_desc_label: Desc - text_input_required: 'Add a text and click on Apply to see results.' - text_input_required_format: basic_html - pager: - type: mini - options: - items_per_page: 10 - offset: 0 - id: 0 - total_pages: null - expose: - items_per_page: false - items_per_page_label: 'Items per page' - items_per_page_options: '5, 10, 25, 50' - items_per_page_options_all: false - items_per_page_options_all_label: '- All -' - offset: false - offset_label: Offset - tags: - previous: ‹‹ - next: ›› - style: - type: default - options: - grouping: { } - row_class: '' - default_row_class: true - uses_fields: false - row: - type: search_api - options: - view_modes: - 'entity:node': - article: teaser - page: teaser - paragraphed_content_demo: teaser - fields: - text: - table: search_api_index_paragraphs_demo_index - field: text - id: text - entity_type: null - entity_field: null - plugin_id: search_api_field - relationship: none - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: true - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: value - type: text_default - settings: { } - group_column: value - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - field_rendering: true - fallback_handler: search_api - fallback_options: - link_to_item: false - multi_separator: ', ' - filters: - search_api_fulltext: - id: search_api_fulltext - table: search_api_index_paragraphs_demo_index - field: search_api_fulltext - relationship: none - group_type: group - admin_label: '' - operator: and - value: '' - group: 1 - exposed: true - expose: - operator_id: search_api_fulltext_op - label: 'Fulltext Paragraphs demo search' - description: '' - use_operator: false - operator: search_api_fulltext_op - identifier: search_api_fulltext - required: false - remember: false - multiple: false - remember_roles: - authenticated: authenticated - anonymous: '0' - administrator: '0' - is_grouped: false - group_info: - label: '' - description: '' - identifier: '' - optional: true - widget: select - multiple: false - remember: false - default_group: All - default_group_multiple: { } - group_items: { } - min_length: null - fields: - field_text_demo: field_text_demo - field_text_demo_1: field_text_demo_1 - plugin_id: search_api_fulltext - sorts: { } - header: { } - footer: { } - empty: { } - relationships: { } - arguments: { } - display_extenders: { } - cache_metadata: - max-age: 0 - contexts: - - 'languages:language_interface' - - url.query_args - tags: { } - page_1: - display_plugin: page - id: page_1 - display_title: Page - position: 1 - display_options: - display_extenders: { } - path: paragraphs_search - menu: - type: none - title: Paragraphs_demo - description: '' - expanded: false - parent: '' - weight: 0 - context: '0' - menu_name: main - cache_metadata: - max-age: 0 - contexts: - - 'languages:language_interface' - - url.query_args - tags: { } diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/css/paragraphs_demo.css b/web/modules/contrib/paragraphs/modules/paragraphs_demo/css/paragraphs_demo.css deleted file mode 100644 index a4743a4de..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/css/paragraphs_demo.css +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @file - * paragraph demo styles. - */ - -/** - * 50/50 flex grid for image/text paragraph. - */ -@media (min-width: 560px) { - .paragraph--type--image-text { - display: flex; - flex-flow: row wrap; - } - .paragraph--type--image-text .field-type-image { - flex: 1 1 0px; - margin: 0; - } - .paragraph--type--image-text .field-name-field-text-demo { - flex: 1 1 0px; - } -} - -/** - * Flex row flow for images paragraph. - */ -.paragraph--type--images .field-items { - display: flex; - flex-direction: row; - flex-wrap: wrap; -} -.paragraph--type--images .field-item { - margin: 0 1em 1em 0; -} - -/** - *50/50 flex grid for image/text paragraph. - */ -@media (min-width: 560px) { - .paragraph--type--text-image { - display: flex; - flex-flow: row wrap; - } - .paragraph--type--text-image .field-name-field-text-demo { - flex: 1 1 0px; - margin: 0 1em 0 0; - } - .paragraph--type--text-image .field-type-image { - flex: 1 1 0px; - } -} diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.info.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.info.yml deleted file mode 100644 index 66ef358d4..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.info.yml +++ /dev/null @@ -1,25 +0,0 @@ -description: 'Provides multilingual demo Paragraphs types.' -# core: 8.x -dependencies: - - paragraphs:paragraphs - - drupal:field - - drupal:image - - drupal:field_ui - - drupal:block - - drupal:language - - drupal:content_translation - - drupal:node - - search_api:search_api_db - - search_api:search_api - - drupal:views - - drupal:taxonomy -hidden: false -name: Paragraphs Demo -package: Paragraphs -type: module - -# Information added by Drupal.org packaging script on 2017-09-19 -version: '8.x-1.2' -core: '8.x' -project: 'paragraphs' -datestamp: 1505802547 diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.install b/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.install deleted file mode 100644 index a5ef6f93f..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.install +++ /dev/null @@ -1,96 +0,0 @@ -<?php -/** - * @file - * Installation hooks for paragraphs_demo module. - */ - -use Drupal\node\Entity\Node; -use Drupal\paragraphs\Entity\Paragraph; - -/** - * Implements hook_install(). - */ -function paragraphs_demo_install() { - // Ensure the translation fields are created in the database. - \Drupal::service('entity.definition_update_manager')->applyUpdates(); - - // Create three paragraphs to structure the content. - $paragraph = Paragraph::create([ - 'title' => 'Paragraph 1', - 'type' => 'text', - 'field_text_demo' => [ - 'value' => '<h2>Paragraphs is the new way of content creation!</h2> - <p>It allows you — Site Builders — to make things cleaner so that you can give more editing power to your end-users. - Instead of putting all their content in one WYSIWYG body field including images and videos, end-users can now choose on-the-fly between pre-defined Paragraph Types independent from one another. Paragraph Types can be anything you want from a simple text block or image to a complex and configurable slideshow.</p>', - 'format' => 'basic_html', - ], - ]); - $paragraph->save(); - - $paragraph2 = Paragraph::create([ - 'title' => 'Paragraph 2', - 'type' => 'text', - 'field_text_demo' => [ - 'value' => '<p>This demo creates some default Paragraph types from which you can easily create some content (Nested Paragraph, Text, Image + Text, Text + Image, Image and User). It also includes some basic styling and assures that the content is responsive on any device.</p>', - 'format' => 'basic_html', - ], - ]); - $paragraph2->save(); - - $paragraph3 = Paragraph::create([ - 'title' => 'Paragraph 3', - 'type' => 'text', - 'field_text_demo' => [ - 'value' => '<p>Apart from the included Paragraph types, you can create your own simply by going to Structure -> Paragraphs types.</p>', - 'format' => 'basic_html', - ], - ]); - $paragraph3->save(); - - $paragraph4 = Paragraph::create([ - 'title' => 'Paragraph 4', - 'type' => 'text', - 'field_text_demo' => [ - 'value' => '<p>A search api example can be found <a href="/paragraphs_search">here</a></p>', - 'format' => 'basic_html', - ], - ]); - $paragraph4->save(); - - $paragraph5 = Paragraph::create([ - 'title' => 'Paragraph 4', - 'type' => 'nested_paragraph', - 'field_paragraphs_demo' => $paragraph4, - ]); - $paragraph5->save(); - - // Add demo content with four paragraphs. - $node = Node::create(array( - 'type' => 'paragraphed_content_demo', - 'title' => 'Welcome to the Paragraphs Demo module!', - 'langcode' => 'en', - 'uid' => '0', - 'status' => 1, - 'field_paragraphs_demo' => array( - array( - 'target_id' => $paragraph->id(), - 'target_revision_id' => $paragraph->getRevisionId(), - ), - array( - 'target_id' => $paragraph2->id(), - 'target_revision_id' => $paragraph2->getRevisionId(), - ), - array( - 'target_id' => $paragraph3->id(), - 'target_revision_id' => $paragraph3->getRevisionId(), - ), - array( - 'target_id' => $paragraph5->id(), - 'target_revision_id' => $paragraph5->getRevisionId(), - ), - ), - )); - $node->save(); - // Set the node as the front page. - \Drupal::configFactory()->getEditable('system.site')->set('page.front', '/node/' . $node->id())->save(); -} diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.libraries.yml b/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.libraries.yml deleted file mode 100644 index 4dee7d641..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.libraries.yml +++ /dev/null @@ -1,5 +0,0 @@ -drupal.paragraphs_demo: - version: 1.0 - css: - theme: - css/paragraphs_demo.css: {} \ No newline at end of file diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.module b/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.module deleted file mode 100644 index 5c421db70..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/paragraphs_demo.module +++ /dev/null @@ -1,42 +0,0 @@ -<?php - -/** - * @file - * Contains paragraphs_demo.module - */ - -use Drupal\Core\Url; -use Drupal\Core\Routing\RouteMatchInterface; - -/** - * Implements hook_help(). - */ -function paragraphs_demo_help($route_name, RouteMatchInterface $route_match) { - switch ($route_name) { - // Help for the paragraphs demo module. - case 'help.page.paragraphs_demo': - $output = ''; - $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Paragraphs Demo module provides several <em>Paragraphs types</em> for the <a href=":paragraphs">Paragraphs module</a>, but no separate user interface. For more information, see the <a href=":online">online documentation for the Paragraphs module</a>.', [':online' => 'https://www.drupal.org/node/2444881', ':paragraphs' => Url::fromRoute('help.page', ['name' => 'paragraphs'])->toString()]) . '</p>'; - $output .= '<h3>' . t('Uses') . '</h3>'; - $output .= '<dt>' . t('Changing demo Paragraphs types') . '</dt>'; - $output .= '<dd>' . t('Administrators can edit the provided <em>Paragraphs types</em> on the <a href=":paragraphs">Paragraphs types page</a> if the <a href=":field_ui">Field UI</a> module is enabled. For more information on fields and entities, see the <a href=":field">Field module help page</a>.', [':paragraphs' => Url::fromRoute('entity.paragraphs_type.collection')->toString(), ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>'; - $output .= '<dt>' . t('Deleting demo Paragraphs types') . '</dt>'; - $output .= '<dd>' . t('The provided <em>demo Paragraph types</em> stay available even when the Paragraphs Demo module is uninstalled. They can be deleted individually on the <a href=":paragraphs">Paragraphs types page</a>.', [':paragraphs' => Url::fromRoute('entity.paragraphs_type.collection')->toString()]) . '</dd>'; - return $output; - break; - } -} - -/** - * Implements hook_preprocess_node() for paragraph node templates. - * - * Attach css we need for paragraph demo content. - */ -function paragraphs_demo_preprocess_node(&$variables) { - // If more general approach is needed then implement preprocessor for - // paragraph.html.twig. - if ($variables['node']->getType() === 'paragraphed_content_demo') { - $variables['#attached']['library'][] = 'paragraphs_demo/drupal.paragraphs_demo'; - } -} diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_demo/src/Tests/ParagraphsDemoTest.php b/web/modules/contrib/paragraphs/modules/paragraphs_demo/src/Tests/ParagraphsDemoTest.php deleted file mode 100644 index 16f9701a6..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_demo/src/Tests/ParagraphsDemoTest.php +++ /dev/null @@ -1,166 +0,0 @@ -<?php - -namespace Drupal\paragraphs_demo\Tests; - -use Drupal\filter\Entity\FilterFormat; -use Drupal\paragraphs\Tests\Classic\ParagraphsCoreVersionUiTestTrait; -use Drupal\simpletest\WebTestBase; - -/** - * Tests the demo module for Paragraphs. - * - * @group paragraphs - */ -class ParagraphsDemoTest extends WebTestBase { - - use ParagraphsCoreVersionUiTestTrait; - - /** - * Modules to enable. - * - * @var string[] - */ - public static $modules = array( - 'paragraphs_demo', - 'block', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->drupalPlaceBlock('local_tasks_block'); - $this->drupalPlaceBlock('local_actions_block'); - $this->drupalPlaceBlock('page_title_block'); - } - - /** - * Asserts demo paragraphs have been created. - */ - protected function testConfigurationsAndCreation() { - $basic_html_format = FilterFormat::create(array( - 'format' => 'basic_html', - 'name' => 'Basic HTML', - )); - $basic_html_format->save(); - $admin_user = $this->drupalCreateUser(array( - 'administer site configuration', - 'create paragraphed_content_demo content', - 'edit any paragraphed_content_demo content', - 'delete any paragraphed_content_demo content', - 'administer content translation', - 'create content translations', - 'administer languages', - 'administer content types', - 'administer node fields', - 'administer node display', - 'administer paragraphs types', - 'administer paragraph fields', - 'administer paragraph display', - 'administer paragraph form display', - 'administer node form display', - $basic_html_format->getPermissionName(), - )); - - $this->drupalLogin($admin_user); - - // Set edit mode to open. - $this->drupalGet('admin/structure/types/manage/paragraphed_content_demo/form-display'); - $this->drupalPostAjaxForm(NULL, [], "field_paragraphs_demo_settings_edit"); - $edit = ['fields[field_paragraphs_demo][settings_edit_form][settings][edit_mode]' => 'open']; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Check for all pre-configured paragraphs_types. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->assertText('Image + Text'); - $this->assertText('Images'); - $this->assertText('Text'); - $this->assertText('Text + Image'); - $this->assertText('User'); - - // Check for preconfigured languages. - $this->drupalGet('admin/config/regional/language'); - $this->assertText('English'); - $this->assertText('German'); - $this->assertText('French'); - - // Check for Content language translation checks. - $this->drupalGet('admin/config/regional/content-language'); - $this->assertFieldChecked('edit-entity-types-node'); - $this->assertFieldChecked('edit-entity-types-paragraph'); - $this->assertFieldChecked('edit-settings-node-paragraphed-content-demo-translatable'); - $this->assertNoFieldChecked('edit-settings-node-paragraphed-content-demo-fields-field-paragraphs-demo'); - $this->assertFieldChecked('edit-settings-paragraph-images-translatable'); - $this->assertFieldChecked('edit-settings-paragraph-image-text-translatable'); - $this->assertFieldChecked('edit-settings-paragraph-text-translatable'); - $this->assertFieldChecked('edit-settings-paragraph-text-image-translatable'); - $this->assertFieldChecked('edit-settings-paragraph-user-translatable'); - - // Check for paragraph type Image + text that has the correct fields set. - $this->drupalGet('admin/structure/paragraphs_type/image_text/fields'); - $this->assertText('Text'); - $this->assertText('Image'); - - // Check for paragraph type Text that has the correct fields set. - $this->drupalGet('admin/structure/paragraphs_type/text/fields'); - $this->assertText('Text'); - $this->assertNoText('Image'); - - // Make sure we have the paragraphed article listed as a content type. - $this->drupalGet('admin/structure/types'); - $this->assertText('Paragraphed article'); - - // Check that title and the descriptions are set. - $this->drupalGet('admin/structure/types/manage/paragraphed_content_demo'); - $this->assertText('Paragraphed article'); - $this->assertText('Article with Paragraphs.'); - - // Check that the Paragraph field is added. - $this->clickLink('Manage fields'); - $this->assertText('Paragraphs'); - - // Check that all paragraphs types are enabled (disabled). - $this->clickLink('Edit', 0); - $this->assertNoFieldChecked('edit-settings-handler-settings-target-bundles-drag-drop-image-text-enabled'); - $this->assertNoFieldChecked('edit-settings-handler-settings-target-bundles-drag-drop-images-enabled'); - $this->assertNoFieldChecked('edit-settings-handler-settings-target-bundles-drag-drop-text-image-enabled'); - $this->assertNoFieldChecked('edit-settings-handler-settings-target-bundles-drag-drop-user-enabled'); - $this->assertNoFieldChecked('edit-settings-handler-settings-target-bundles-drag-drop-text-enabled'); - - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->assertRaw('<strong data-drupal-selector="edit-field-paragraphs-demo-title">Paragraphs</strong>', 'Field name is present on the page.'); - $this->drupalPostForm(NULL, NULL, t('Add Text')); - $this->assertNoRaw('<strong data-drupal-selector="edit-field-paragraphs-demo-title">Paragraphs</strong>', 'Field name for empty field is not present on the page.'); - $this->assertRaw('<h4 class="label">Paragraphs</h4>', 'Field name appears in the table header.'); - $edit = array( - 'title[0][value]' => 'Paragraph title', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Paragraph text', - ); - $this->drupalPostForm(NULL, $edit, t('Add User')); - $edit = [ - 'field_paragraphs_demo[1][subform][field_user_demo][0][target_id]' => $admin_user->label() . ' (' . $admin_user->id() . ')', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - $this->assertText('Paragraphed article Paragraph title has been created.'); - $this->assertText('Paragraph title'); - $this->assertText('Paragraph text'); - - // Search a nested Paragraph text. - $this->drupalGet('paragraphs_search', ['query' => ['search_api_fulltext' => 'A search api example']]); - $this->assertRaw('Welcome to the Paragraphs Demo module!'); - // Search a node paragraph field text. - $this->drupalGet('paragraphs_search', ['query' => ['search_api_fulltext' => 'It allows you']]); - $this->assertRaw('Welcome to the Paragraphs Demo module!'); - // Search non existent text. - $this->drupalGet('paragraphs_search', ['query' => ['search_api_fulltext' => 'foo']]); - $this->assertNoRaw('Welcome to the Paragraphs Demo module!'); - - // Check that the dropbutton of Nested Paragraph has the Duplicate function. - // For now, this indicates that it is using the EXPERIMENTAL widget. - $this->drupalGet('node/1/edit'); - $this->assertFieldByName('field_paragraphs_demo_3_subform_field_paragraphs_demo_0_duplicate'); - } - -} diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.info.yml b/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.info.yml deleted file mode 100644 index d6e4fbbec..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.info.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Paragraphs Type Permissions -type: module -description: 'Allows users to configure permissions for individual Paragraphs types.' -# core: 8.x -package: Paragraphs - -dependencies: - - paragraphs:paragraphs - -# Information added by Drupal.org packaging script on 2017-09-19 -version: '8.x-1.2' -core: '8.x' -project: 'paragraphs' -datestamp: 1505802547 diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.module b/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.module deleted file mode 100644 index e3757416f..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.module +++ /dev/null @@ -1,95 +0,0 @@ -<?php - -/** - * @file - * Contains paragraphs_type_permissions.module - */ - -use Drupal\Core\Url; -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Session\AccountInterface; -use Drupal\paragraphs\ParagraphInterface; -use Drupal\Core\Routing\RouteMatchInterface; - -/** - * Implements hook_help(). - */ -function paragraphs_type_permissions_help($route_name, RouteMatchInterface $route_match) { - switch ($route_name) { - // Help for the Paragraphs type permissions module. - case 'help.page.paragraphs_type_permissions': - $output = ''; - $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Paragraphs Type permission module allows administrators to configure permissions individually for each <em>Paragraphs type</em>. For more information, see the <a href=":online">online documentation for the Paragraphs module</a>.', [':online' => 'https://www.drupal.org/node/2444881']) . '</p>'; - $output .= '<h3>' . t('Uses') . '</h3>'; - $output .= '<dt>' . t('Configuring permissions per Paragraphs type') . '</dt>'; - $output .= '<dd>' . t('Administrators can configure the permissions to view, create, edit, and delete each <em>Paragraphs type</em> individually on the <a href=":permissions">Permissions page</a>.', [':permissions' => Url::fromRoute('user.admin_permissions')->toString()]) . '</dd>'; - return $output; - break; - } -} - -/** - * Implements hook_ENTITY_TYPE_access() for entity type "paragraph". - */ -function paragraphs_type_permissions_paragraph_access(ParagraphInterface $entity, $operation, AccountInterface $account) { - $permissions = &drupal_static(__FUNCTION__, array()); - - if (!in_array($operation, array('view', 'update', 'delete'), TRUE)) { - // If there was no type to check against, or the $op was not one of the - // supported ones, we return access denied. - return AccessResult::neutral(); - } - - // Set static cache id to use the type machine name. - $type = $entity->getType(); - - if ($operation == 'view' && !$entity->status->value) { - return AccessResult::forbidden(); - } - - // If we've already checked access for this type, user and op, return from - // cache. - if (isset($permissions[$account->id()][$type][$operation])) { - return $permissions[$account->id()][$type][$operation]; - } - - // If the current user has access to this type/operation, return access - // allowed, forbidden otherwise. - if ($account->hasPermission('bypass paragraphs type content access') || $account->hasPermission($operation . ' paragraph content ' . $type)) { - $permissions[$account->id()][$type][$operation] = AccessResult::allowed()->cachePerPermissions(); - } - else { - $permissions[$account->id()][$type][$operation] = AccessResult::forbidden()->cachePerPermissions(); - } - - return $permissions[$account->id()][$type][$operation]; -} - -/** - * Implements hook_ENTITY_TYPE_create_access() for entity type "paragraph". - */ -function paragraphs_type_permissions_paragraph_create_access(AccountInterface $account = NULL, array $context = array(), $entity_bundle = NULL) { - $permissions = &drupal_static(__FUNCTION__, array()); - - // Set static cache id to use the type machine name. - $type = $entity_bundle; - $op = 'create'; - - // If we've already checked access for this type, user and op, return from - // cache. - if (isset($permissions[$account->id()][$type][$op])) { - return $permissions[$account->id()][$type][$op]; - } - - // If the current user has access to this type/op, return access allowed, - // forbidden otherwise. - if ($account->hasPermission('bypass paragraphs type content access') || $account->hasPermission($op . ' paragraph content ' . $type)) { - $permissions[$account->id()][$type][$op] = AccessResult::allowed()->cachePerPermissions(); - } - else { - $permissions[$account->id()][$type][$op] = AccessResult::forbidden()->cachePerPermissions(); - } - - return $permissions[$account->id()][$type][$op]; -} diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.permissions.yml b/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.permissions.yml deleted file mode 100644 index 4c4d20dd8..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/paragraphs_type_permissions.permissions.yml +++ /dev/null @@ -1,3 +0,0 @@ -permission_callbacks: - - \Drupal\paragraphs_type_permissions\ParagraphsTypePermissions::globalPermissions - - \Drupal\paragraphs_type_permissions\ParagraphsTypePermissions::paragraphTypePermissions \ No newline at end of file diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/src/ParagraphsTypePermissions.php b/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/src/ParagraphsTypePermissions.php deleted file mode 100644 index da3ba5f1f..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/src/ParagraphsTypePermissions.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php - -namespace Drupal\paragraphs_type_permissions; - -use Drupal\Core\Routing\UrlGeneratorTrait; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\paragraphs\Entity\ParagraphsType; - -/** - * Defines a class containing permission callbacks. - */ -class ParagraphsTypePermissions { - - use StringTranslationTrait; - use UrlGeneratorTrait; - - /** - * Returns an array of content permissions. - * - * @return array - */ - public function globalPermissions() { - return array( - 'bypass paragraphs type content access' => array( - 'title' => $this->t('Bypass Paragraphs type content access control'), - 'description' => $this->t('Is able to administer content for all Paragraph types'), - ), - ); - } - - /** - * Returns an array of Paragraphs type permissions. - * - * @return array - */ - public function paragraphTypePermissions() { - $perms = array(); - - // Generate paragraph permissions for all Paragraphs types. - foreach (ParagraphsType::loadMultiple() as $type) { - $perms += $this->buildPermissions($type); - } - - return $perms; - } - - /** - * Builds a standard list of node permissions for a given type. - * - * @param \Drupal\paragraphs\Entity\ParagraphsType $type - * The machine name of the node type. - * - * @return array - * An array of permission names and descriptions. - */ - protected function buildPermissions(ParagraphsType $type) { - $type_id = $type->id(); - $type_params = array('%type_name' => $type->label()); - - return array( - 'view paragraph content ' .$type_id => array( - 'title' => $this->t('%type_name: View content', $type_params), - 'description' => $this->t('Is able to view Paragraphs content of type %type_name', $type_params), - ), - 'create paragraph content ' . $type_id => array( - 'title' => $this->t('%type_name: Create content', $type_params), - 'description' => $this->t('Is able to create Paragraphs content of type %type_name', $type_params), - ), - 'update paragraph content ' . $type_id => array( - 'title' => $this->t('%type_name: Edit content', $type_params), - 'description' => $this->t('Is able to update Paragraphs content of type %type_name', $type_params), - ), - 'delete paragraph content ' . $type_id => array( - 'title' => $this->t('%type_name: Delete content', $type_params), - 'description' => $this->t('Is able to delete Paragraphs content of type %type_name', $type_params), - ), - ); - } - -} diff --git a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/src/Tests/ParagraphsTypePermissionsTest.php b/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/src/Tests/ParagraphsTypePermissionsTest.php deleted file mode 100644 index 2ba2717a0..000000000 --- a/web/modules/contrib/paragraphs/modules/paragraphs_type_permissions/src/Tests/ParagraphsTypePermissionsTest.php +++ /dev/null @@ -1,196 +0,0 @@ -<?php - -namespace Drupal\paragraphs_type_permissions\Tests; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\paragraphs\Tests\Classic\ParagraphsCoreVersionUiTestTrait; -use Drupal\simpletest\WebTestBase; -use Drupal\user\Entity\Role; - -/** - * Tests the paragraphs type permissions. - * - * @group paragraphs - */ -class ParagraphsTypePermissionsTest extends WebTestBase { - - use FieldUiTestTrait, ParagraphsCoreVersionUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'node', - 'paragraphs_demo', - 'field', - 'image', - 'field_ui', - 'paragraphs_type_permissions', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - } - - /** - * Tests paragraphs type permissions for anonymous and authenticated users. - */ - public function testAnonymousParagraphsTypePermissions() { - // Create an authenticated user without special permissions for test. - $authenticated_user = $this->drupalCreateUser(); - // Create an admin user for test. - $admin_user = $this->drupalCreateUser(array( - 'administer site configuration', - 'administer content types', - 'administer node fields', - 'administer node display', - 'administer paragraphs types', - 'administer paragraph form display', - 'create paragraphed_content_demo content', - 'edit any paragraphed_content_demo content', - 'bypass paragraphs type content access', - 'administer node form display', - )); - $this->drupalLogin($admin_user); - - // Enable the publish/unpublish checkbox fields. - $paragraph_types = [ - 'image_text', - 'images', - 'text', - ]; - foreach ($paragraph_types as $paragraph_type) { - entity_get_form_display('paragraph', $paragraph_type, 'default') - ->setComponent('status', [ - 'type' => 'boolean_checkbox' - ]) - ->save(); - } - - // Create a node with some Paragraph types. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Image + Text')); - $this->drupalPostForm(NULL, NULL, t('Add Images')); - $this->drupalPostForm(NULL, NULL, t('Add Text')); - - $image_text = $this->drupalGetTestFiles('image')[0]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_0_subform_field_image_demo_0]' => $image_text->uri, - ], t('Upload')); - $images = $this->drupalGetTestFiles('image')[1]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_1_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $edit = [ - 'title[0][value]' => 'paragraph node title', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Paragraph type Image + Text', - 'field_paragraphs_demo[2][subform][field_text_demo][0][value]' => 'Paragraph type Text', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Get the node to edit it later. - $node = $this->drupalGetNodeByTitle($edit['title[0][value]']); - - // Get the images data to check for their presence. - $image_text_tag = '/files/styles/large/public/image-test_0.png?itok='; - $images_tag = '/files/styles/medium/public/image-test_0_0.png?itok='; - - // Check that all paragraphs are shown for admin user. - $this->assertRaw($image_text_tag); - $this->assertRaw($images_tag); - $this->assertText('Paragraph type Image + Text'); - $this->assertText('Paragraph type Text'); - - // Logout, check that no paragraphs are shown for anonymous user. - $this->drupalLogout(); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw($image_text_tag); - $this->assertNoRaw($images_tag); - $this->assertNoText('Paragraph type Image + Text'); - $this->assertNoText('Paragraph type Text'); - - // Login as authenticated user, check that no paragraphs are shown for him. - $this->drupalLogin($authenticated_user); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw($image_text_tag); - $this->assertNoRaw($images_tag); - $this->assertNoText('Paragraph type Image + Text'); - $this->assertNoText('Paragraph type Text'); - - // Login as admin - $this->drupalLogout(); - $this->drupalLogin($admin_user); - - // Set edit mode to open. - $this->drupalGet('admin/structure/types/manage/paragraphed_content_demo/form-display'); - $this->drupalPostAjaxForm(NULL, [], "field_paragraphs_demo_settings_edit"); - $edit = ['fields[field_paragraphs_demo][settings_edit_form][settings][edit_mode]' => 'open']; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Unpublish the 'Image + Text' paragraph type. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertFieldChecked('edit-field-paragraphs-demo-0-subform-status-value'); - $edit = [ - 'field_paragraphs_demo[0][subform][status][value]' => FALSE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Check that 'Image + Text' paragraph is not shown anymore for admin user. - $this->assertNoRaw($image_text_tag); - $this->assertRaw($images_tag); - $this->assertNoText('Paragraph type Image + Text'); - $this->assertText('Paragraph type Text'); - - $this->drupalLogout(); - - // Add permissions to anonymous user to view only 'Image + Text' and - // 'Text' paragraph contents. - /** @var \Drupal\user\RoleInterface $anonymous_role */ - $anonymous_role = Role::load('anonymous'); - $anonymous_role->grantPermission('view paragraph content image_text'); - $anonymous_role->grantPermission('view paragraph content text'); - $anonymous_role->save(); - - // Add permissions to authenticated user to view only 'Image + Text' and - // 'Images' paragraph contents. - /** @var \Drupal\user\RoleInterface $authenticated_role */ - $authenticated_role = Role::load('authenticated'); - $authenticated_role->grantPermission('view paragraph content image_text'); - $authenticated_role->grantPermission('view paragraph content images'); - $authenticated_role->save(); - - // Check that the anonymous user can only view the 'Text' paragraph. - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw($image_text_tag); - $this->assertNoRaw($images_tag); - $this->assertNoText('Paragraph type Image + Text'); - $this->assertText('Paragraph type Text'); - - // Check that the authenticated user can only view the 'Images' paragraph. - $this->drupalLogin($authenticated_user); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw($image_text_tag); - $this->assertRaw($images_tag); - $this->assertNoText('Paragraph type Image + Text'); - $this->assertNoText('Paragraph type Text'); - - // Check the authenticated user with edit permission. - $authenticated_role->grantPermission('update paragraph content image_text'); - $authenticated_role->grantPermission('bypass node access'); - $authenticated_role->save(); - $this->drupalLogin($authenticated_user); - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertRaw('Image + Text'); - $this->assertText('Paragraph type Image + Text'); - $this->assertText('You are not allowed to remove this Paragraph.'); - $this->assertText('Published'); - $this->assertText('Images'); - $this->assertText('You are not allowed to edit or remove this Paragraph.'); - } - -} diff --git a/web/modules/contrib/paragraphs/paragraphs.api.php b/web/modules/contrib/paragraphs/paragraphs.api.php deleted file mode 100644 index 359b6eda3..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.api.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -/** - * @file - * Hooks and documentation related to paragraphs module. - */ - -/** - * @addtogroup hooks - * @{ - */ - -/** - * Alter the information provided in - * \Drupal\paragraphs\Annotation\ParagraphsBehavior. - * - * @param $paragraphs_behavior - * The array of paragraphs behavior plugins, keyed on the - * machine-readable plugin name. - */ -function hook_paragraphs_behavior_info_alter(&$paragraphs_behavior) { - // Set a new label for the my_layout plugin instead of the one - // provided in the annotation. - $paragraphs_behavior['my_layout']['label'] = t('New label'); -} - -/** - * Alter paragraphs widget. - * - * @param array $widget_actions - * Array with actions and dropdown widget actions. - * @param array $context - * An associative array containing the following key-value pairs: - * - form: The form structure to which widgets are being attached. This may be - * a full form structure, or a sub-element of a larger form. - * - widget: The widget plugin instance. - * - items: The field values, as a - * \Drupal\Core\Field\FieldItemListInterface object. - * - delta: The order of this item in the array of subelements (0, 1, 2, etc). - * - element: A form element array containing basic properties for the widget. - * - form_state: The current state of the form. - * - paragraphs_entity: the paragraphs entity for this widget. Might be - * unsaved, if we have just added a new item to the widget. - */ -function hook_paragraphs_widget_actions_alter(array &$widget_actions, array &$context) { -} - -/** - * @} End of "addtogroup hooks". - */ diff --git a/web/modules/contrib/paragraphs/paragraphs.info.yml b/web/modules/contrib/paragraphs/paragraphs.info.yml deleted file mode 100644 index b4ef23899..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.info.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Paragraphs -type: module -description: 'Enables the creation of paragraphs entities.' -# core: 8.x -package: Paragraphs -configure: entity.paragraphs_type.collection - -dependencies: - - entity_reference_revisions:entity_reference_revisions - - drupal:file -test_dependencies: - - diff:diff - - replicate:replicate - - inline_entity_form:inline_entity_form - - field_group:field_group - - block_field:block_field - -# Information added by Drupal.org packaging script on 2017-09-19 -version: '8.x-1.2' -core: '8.x' -project: 'paragraphs' -datestamp: 1505802547 diff --git a/web/modules/contrib/paragraphs/paragraphs.install b/web/modules/contrib/paragraphs/paragraphs.install deleted file mode 100644 index 3ce60702c..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.install +++ /dev/null @@ -1,215 +0,0 @@ -<?php -/** - * @file - * Installation hooks for paragraphs module. - */ - -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\paragraphs\ParagraphStorageSchema; - -/** - * Implements hook_install(). - */ -function paragraphs_install() { - // Assign a weight 1 higher than content_translation to ensure paragraphs_module_implements_alter - // runs after content_translation_module_implements_alter. - module_set_weight('paragraphs', 11); -} - -/** - * Add status field. - */ -function paragraphs_update_8001() { - $storage_definition = BaseFieldDefinition::create('boolean') - ->setLabel(t('Published')) - ->setRevisionable(TRUE) - ->setTranslatable(TRUE); - \Drupal::entityDefinitionUpdateManager() - ->installFieldStorageDefinition('status', 'paragraph', 'paragraph', $storage_definition); -} - -/** - * Add parent ID, parent type and parent field name fields. - */ -function paragraphs_update_8002() { - $storage_definition = BaseFieldDefinition::create('string') - ->setLabel(t('Parent ID')) - ->setDescription(t('The ID of the parent entity of which this entity is referenced.')) - ->setSetting('is_ascii', TRUE); - \Drupal::entityDefinitionUpdateManager() - ->installFieldStorageDefinition('parent_id', 'paragraph', 'paragraph', $storage_definition); - - $storage_definition = BaseFieldDefinition::create('string') - ->setLabel(t('Parent type')) - ->setDescription(t('The entity parent type to which this entity is referenced.')) - ->setSetting('is_ascii', TRUE) - ->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH); - \Drupal::entityDefinitionUpdateManager() - ->installFieldStorageDefinition('parent_type', 'paragraph', 'paragraph', $storage_definition); - - $storage_definition = BaseFieldDefinition::create('string') - ->setLabel(t('Parent field name')) - ->setDescription(t('The entity parent field name to which this entity is referenced.')) - ->setSetting('is_ascii', TRUE) - ->setSetting('max_length', FieldStorageConfig::NAME_MAX_LENGTH); - \Drupal::entityDefinitionUpdateManager() - ->installFieldStorageDefinition('parent_field_name', 'paragraph', 'paragraph', $storage_definition); -} - -/** - * Placeholder for the previous 8003 update. - */ -function paragraphs_update_8003() { - // The original update function was moved to be post update. - \Drupal::state()->set('paragraphs_update_8003_placeholder', TRUE); -} - -/** - * Truncate the content_translation_status columns. - */ -function paragraphs_update_8004() { - - $field_name = 'content_translation_status'; - - $tables_to_update = [ - 'paragraphs_item_field_data', - 'paragraphs_item_revision_field_data' - ]; - - $database = Drupal::database(); - $entity_definition_update_manager = Drupal::entityDefinitionUpdateManager(); - - // Ensure that the data from the content translation status field is deleted - // so that the field can safely be deleted. - foreach ($tables_to_update as $table_to_update) { - if ($database->schema()->fieldExists($table_to_update, $field_name)) { - $database->update($table_to_update) - ->fields([$field_name => NULL]) - ->execute(); - } - } - - - // Delete the storage definition if it was defined before. - $storage_definition = $entity_definition_update_manager->getFieldStorageDefinition($field_name, 'paragraph'); - if ($storage_definition) { - $entity_definition_update_manager->uninstallFieldStorageDefinition($storage_definition); - } -} - -/** - * Remove revision_timestamp, changed fields, add content_translation_changed. - */ -function paragraphs_update_8006() { - - $tables_fields = [ - 'paragraphs_item_revision' => 'revision_timestamp', - 'paragraphs_item_field_data' => 'changed', - 'paragraphs_item_revision_field_data' => 'changed', - ]; - - $database = Drupal::database(); - $entity_definition_update_manager = Drupal::entityDefinitionUpdateManager(); - - // Ensure that the data from the content translation status field is deleted - // so that the field can safely be deleted. - foreach ($tables_fields as $table => $field) { - if ($database->schema()->fieldExists($table, $field)) { - $database->update($table) - ->fields([$field => NULL]) - ->execute(); - } - } - - foreach ($tables_fields as $table => $field) { - // Delete the storage definition if it was defined before. - $storage_definition = $entity_definition_update_manager->getFieldStorageDefinition($field, 'paragraph'); - if ($storage_definition) { - $entity_definition_update_manager->uninstallFieldStorageDefinition($storage_definition); - } - } - - // Add content_translation_changed field. - $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('paragraph'); - if (isset($field_storage_definitions['content_translation_changed'])) { - $storage_definition = BaseFieldDefinition::create('changed') - ->setLabel(t('Translation changed time')) - ->setDescription(t('The Unix timestamp when the translation was most recently saved.')) - ->setRevisionable(TRUE) - ->setTranslatable(TRUE); - \Drupal::entityDefinitionUpdateManager() - ->installFieldStorageDefinition('content_translation_changed', 'paragraph', 'paragraph', $storage_definition); - } -} - -/** - * Ensure that existing paragraphs are published. - */ -function paragraphs_update_8007() { - \Drupal::database() - ->update('paragraphs_item_field_data') - ->fields(['status' => 1]) - ->isNull('status') - ->execute(); - \Drupal::database() - ->update('paragraphs_item_revision_field_data') - ->fields(['status' => 1]) - ->isNull('status') - ->execute(); -} - -/** - * Ensure that the parent indexes are added to the paragraphs entity. - */ -function paragraphs_update_8008() { - $manager = \Drupal::entityDefinitionUpdateManager(); - - // Get the current paragraph entity type definition, ensure the storage schema - // class is set. - $entity_type = $manager->getEntityType('paragraph') - ->setHandlerClass('storage_schema', ParagraphStorageSchema::class); - - // Regenerate entity type indexes. - $manager->updateEntityType($entity_type); -} - -/** - * Set the weight to 11 to override content_translation's hook_module_implements_alter implementation - */ -function paragraphs_update_8009() { - module_set_weight('paragraphs', 11); -} - -/** - * Add behavior plugins fields. - */ -function paragraphs_update_8010() { - $storage_definition = BaseFieldDefinition::create('string_long') - ->setLabel(t('Behavior settings')) - ->setDescription(t('The behavior plugin settings')); - \Drupal::entityDefinitionUpdateManager() - ->installFieldStorageDefinition('behavior_settings', 'paragraph', 'paragraph', $storage_definition); -} - -/** - * Make the behavior plugins field of Paragraphs revisionable. - */ -function paragraphs_update_8011() { - \Drupal::database()->update('paragraphs_item_field_data') - ->fields(['behavior_settings' => NULL]) - ->execute(); - - /** @var \Drupal\Core\Field\BaseFieldDefinition $storage_definition */ - $storage_definition = \Drupal::entityDefinitionUpdateManager()->getFieldStorageDefinition('behavior_settings', 'paragraph'); - $storage_definition->setRevisionable(TRUE); - \Drupal::entityDefinitionUpdateManager()->updateFieldStorageDefinition($storage_definition); -} - -/** - * Install file module. - */ -function paragraphs_update_8012() { - \Drupal::service('module_installer')->install(['file']); -} diff --git a/web/modules/contrib/paragraphs/paragraphs.libraries.yml b/web/modules/contrib/paragraphs/paragraphs.libraries.yml deleted file mode 100644 index 6ddb78287..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.libraries.yml +++ /dev/null @@ -1,77 +0,0 @@ -drupal.paragraphs.admin: - dependencies: - - core/jquery - - core/drupal - - core/drupalSettings - - core/jquery.once - - core/jquery.form - - core/drupal.ajax - - core/drupal.dropbutton - css: - theme: - css/paragraphs.admin.css: {} - -drupal.paragraphs.widget: - dependencies: - - core/jquery - - core/drupal - - core/drupalSettings - - core/jquery.once - - core/jquery.form - - core/drupal.ajax - - core/drupal.dropbutton - css: - theme: - css/paragraphs.widget.css: {} - js: - js/paragraphs.admin.js: {} - -drupal.paragraphs.actions: - css: - theme: - css/paragraphs.actions.css: {} - js: - js/paragraphs.actions.js: {} - dependencies: - - core/drupalSettings - - core/jquery.once - - core/jquery - - core/jquery.form - - core/drupal.ajax - - core/drupal - -drupal.paragraphs.list_builder: - version: VERSION - css: - theme: - css/paragraphs.list-builder.css: {} - -drupal.paragraphs.modal: - js: - js/paragraphs.modal.js: {} - css: - theme: - css/paragraphs.modal.css: {} - dependencies: - - core/drupal.dialog.ajax - - core/jquery.once - - core/jquery.ui.tabs - -sortable: - website: https://github.com/RubaXa/Sortable - license: - name: MIT - url: https://github.com/RubaXa/Sortable/blob/master/README.md - gpl-compatible: true - js: { } - -paragraphs-dragdrop: - css: - theme: - css/paragraphs.dragdrop.css: {} - js: - js/paragraphs.dragdrop.js: {} - dependencies: - - core/jquery - - core/drupal - - paragraphs/sortable diff --git a/web/modules/contrib/paragraphs/paragraphs.links.action.yml b/web/modules/contrib/paragraphs/paragraphs.links.action.yml deleted file mode 100644 index 2154a2691..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.links.action.yml +++ /dev/null @@ -1,5 +0,0 @@ -paragraphs.type_add: - route_name: 'paragraphs.type_add' - title: 'Add paragraph type' - appears_on: - - entity.paragraphs_type.collection diff --git a/web/modules/contrib/paragraphs/paragraphs.links.menu.yml b/web/modules/contrib/paragraphs/paragraphs.links.menu.yml deleted file mode 100644 index a8cd28779..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.links.menu.yml +++ /dev/null @@ -1,5 +0,0 @@ -entity.paragraphs_type.collection: - title: 'Paragraph types' - parent: system.admin_structure - description: 'Create and manage Paragraph types.' - route_name: entity.paragraphs_type.collection diff --git a/web/modules/contrib/paragraphs/paragraphs.links.task.yml b/web/modules/contrib/paragraphs/paragraphs.links.task.yml deleted file mode 100644 index 8c754f93e..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.links.task.yml +++ /dev/null @@ -1,9 +0,0 @@ -entity.paragraphs_type.edit_form: - title: 'Edit' - route_name: entity.paragraphs_type.edit_form - base_route: entity.paragraphs_type.edit_form - -entity.paragraphs_type.collection: - title: List - route_name: entity.paragraphs_type.collection - base_route: entity.paragraphs_type.collection diff --git a/web/modules/contrib/paragraphs/paragraphs.module b/web/modules/contrib/paragraphs/paragraphs.module deleted file mode 100644 index 1cd3570d0..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.module +++ /dev/null @@ -1,424 +0,0 @@ -<?php - -/** - * @file - * Contains paragraphs.module - */ - -use Drupal\Core\Field\FieldConfigInterface; -use Drupal\Core\Url; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\field\FieldStorageConfigInterface; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\Core\Render\Element; - -/** - * Implements hook_help(). - */ -function paragraphs_help($route_name, RouteMatchInterface $route_match) { - switch ($route_name) { - // Main module help for the paragraphs module. - case 'help.page.paragraphs': - $output = ''; - $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Paragraphs module provides a field type that can contain several other fields and thereby allows users to break content up on a page. Administrators can predefine <em>Paragraphs types</em> (for example a simple text block, a video, or a complex and configurable slideshow). Users can then place them on a page in any order instead of using a text editor to add and configure such elements. For more information, see the <a href=":online">online documentation for the Paragraphs module</a>.', [':online' => 'https://www.drupal.org/node/2444881']) . '</p>'; - $output .= '<h3>' . t('Uses') . '</h3>'; - $output .= '<dt>' . t('Creating Paragraphs types') . '</dt>'; - $output .= '<dd>' . t('<em>Paragraphs types</em> can be created by clicking <em>Add Paragraphs type</em> on the <a href=":paragraphs">Paragraphs types page</a>. By default a new Paragraphs type does not contain any fields.', [':paragraphs' => Url::fromRoute('entity.paragraphs_type.collection')->toString()]) . '</dd>'; - $output .= '<dt>' . t('Configuring Paragraphs types') . '</dt>'; - $output .= '<dd>' . t('Administrators can add fields to a <em>Paragraphs type</em> on the <a href=":paragraphs">Paragraphs types page</a> if the <a href=":field_ui">Field UI</a> module is enabled. The form display and the display of the Paragraphs type can also be managed on this page. For more information on fields and entities, see the <a href=":field">Field module help page</a>.', [':paragraphs' => Url::fromRoute('entity.paragraphs_type.collection')->toString(), ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>'; - $output .= '<dt>' . t('Creating content with Paragraphs') . '</dt>'; - $output .= '<dd>' . t('Administrators can add a <em>Paragraph</em> field to content types or other entities, and configure which <em>Paragraphs types</em> to include. When users create content, they can then add one or more paragraphs by choosing the appropriate type from the dropdown list. Users can also dragdrop these paragraphs. This allows users to add structure to a page or other content (for example by adding an image, a user reference, or a differently formatted block of text) more easily then including it all in one text field or by using fields in a pre-defined order.') . '</dd>'; - return $output; - break; - } -} - -function paragraphs_type_get_types() { - return ParagraphsType::loadMultiple(); -} - -function paragraphs_type_get_names() { - return array_map(function ($bundle_info) { - return $bundle_info['label']; - }, \Drupal::service('entity_type.bundle.info')->getBundleInfo('paragraphs_type')); -} - -function paragraphs_type_load($name) { - return ParagraphsType::load($name); -} - -/** - * Implements hook_theme(). - */ -function paragraphs_theme() { - return array( - 'paragraph' => array( - 'render element' => 'elements', - ), - 'paragraphs_dropbutton_wrapper' => array( - 'variables' => array('children' => NULL), - ), - 'paragraphs_info_icon' => [ - 'variables' => [ - 'message' => NULL, - 'icon' => NULL, - ], - ], - 'paragraphs_add_dialog' => [ - 'render element' => 'element', - 'template' => 'paragraphs-add-dialog', - ], - 'paragraphs_actions' => [ - 'render element' => 'element', - 'template' => 'paragraphs-actions', - ], - ); -} - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function paragraphs_theme_suggestions_paragraph(array $variables) { - $suggestions = array(); - $paragraph = $variables['elements']['#paragraph']; - $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_'); - - $suggestions[] = 'paragraph__' . $sanitized_view_mode; - $suggestions[] = 'paragraph__' . $paragraph->bundle(); - $suggestions[] = 'paragraph__' . $paragraph->bundle() . '__' . $sanitized_view_mode; - - return $suggestions; -} - -/** - * Implements hook_form_FORM_ID_alter(). - */ -function paragraphs_form_entity_form_display_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { - $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($form['#entity_type'], $form['#bundle']); - // Loop over ERR field's display options with paragraph target type. - foreach (array_keys($field_definitions) as $field_name) { - if ($field_definitions[$field_name]->getType() == 'entity_reference_revisions') { - if ($field_definitions[$field_name]->getSettings()['target_type'] == 'paragraph') { - foreach (['options_buttons', 'options_select', 'entity_reference_revisions_autocomplete'] as $option) { - unset($form['fields'][$field_name]['plugin']['type']['#options'][$option]); - } - } - } - } -} - -/** - * Implements hook_form_FORM_ID_alter(). - */ -function paragraphs_form_field_storage_config_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { - if ($form_state->getFormObject()->getEntity()->getType() == 'entity_reference') { - // Entity Reference fields are no longer supported to reference Paragraphs. - unset($form['settings']['target_type']['#options'][(string) t('Content')]['paragraph']); - } -} - -/** - * Implements hook_form_FORM_ID_alter(). - * - * Indicate unsupported multilingual paragraphs field configuration. - */ -function paragraphs_form_field_config_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { - $field = $form_state->getFormObject()->getEntity(); - - if (!\Drupal::hasService('content_translation.manager')) { - return; - } - - $bundle_is_translatable = \Drupal::service('content_translation.manager') - ->isEnabled($field->getTargetEntityTypeId(), $field->getTargetBundle()); - - if (!$bundle_is_translatable - || $field->getType() != 'entity_reference_revisions' - || $field->getSetting('target_type') != 'paragraph') { - return; - } - - // This is a translatable ERR field pointing to a paragraph. - $message_display = 'warning'; - $message_text = t('Paragraphs fields do not support translation. See the <a href=":documentation">online documentation</a>.', [ - ':documentation' => Url::fromUri('https://www.drupal.org/node/2735121') - ->toString() - ]); - - if ($form['translatable']['#default_value'] == TRUE) { - $message_display = 'error'; - } - - $form['paragraphs_message'] = array( - '#type' => 'container', - '#markup' => $message_text, - '#attributes' => array( - 'class' => array('messages messages--' . $message_display), - ), - '#weight' => 0, - ); -} - -/** - * Implements hook_module_implements_alter(). - * - * Our paragraphs_form_field_config_edit_form_alter() needs to be run after - * that of the content_translation module in order to see the current state - * of the translation field. - * - * The hook here can't be more specific, as the $hook that's passed in to this - * function is form_alter, and not form_FORM_ID_alter. - */ -function paragraphs_module_implements_alter(&$implementations, $hook) { - if ($hook == 'form_alter' && isset($implementations['paragraphs'])) { - $group = $implementations['paragraphs']; - unset($implementations['paragraphs']); - $implementations['paragraphs'] = $group; - } -} - -/** - * Implements hook_form_FORM_ID_alter(). - * - * Indicate unsupported multilingual paragraphs field configuration. - * - * Add a warning that paragraph fields can not be translated. - * Switch to error if a paragraph field is marked as translatable. - */ -function paragraphs_form_language_content_settings_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { - // Without it Paragraphs message are meaningless. - if (!\Drupal::hasService('content_translation.manager')) { - return; - } - - $content_translation_manager = \Drupal::service('content_translation.manager'); - $message_display = 'warning'; - $message_text = t('(* unsupported) Paragraphs fields do not support translation. See the <a href=":documentation">online documentation</a>.', [ - ':documentation' => Url::fromUri('https://www.drupal.org/node/2735121') - ->toString()]); - $map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('entity_reference_revisions'); - foreach ($map as $entity_type_id => $info) { - if (!$content_translation_manager->isEnabled($entity_type_id)) { - continue; - } - $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type_id); - - /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition */ - foreach ($field_storage_definitions as $name => $storage_definition) { - if ($storage_definition->getSetting('target_type') && $storage_definition->getSetting('target_type') == 'paragraph') { - - // For configurable fields, check all bundles on which the field exists, - // for base fields that are translable, check all bundles, - // untranslatable base fields do not show up at all. - $bundles = []; - if ($storage_definition instanceof FieldStorageConfigInterface) { - $bundles = $storage_definition->getBundles(); - } - elseif ($storage_definition->isTranslatable()) { - $bundles = Element::children($form['settings'][$entity_type_id]); - } - foreach($bundles as $bundle) { - if (!$content_translation_manager->isEnabled($entity_type_id, $bundle)) { - continue; - } - - // Update the label and if the paragraph field is translatable, - // display an error message instead of just a warning. - if (isset($form['settings'][$entity_type_id][$bundle]['fields'][$name]['#label'])) { - $form['settings'][$entity_type_id][$bundle]['fields'][$name]['#label'] = t('@field_label (* unsupported)', ['@field_label' => $form['settings'][$entity_type_id][$bundle]['fields'][$name]['#label']]); - } - if (!empty($form['settings'][$entity_type_id][$bundle]['fields'][$name]['#default_value'])) { - $message_display = 'error'; - } - } - } - } - } - $form['settings']['paragraphs_message'] = array( - '#type' => 'container', - '#markup' => $message_text, - '#attributes' => array( - 'class' => array('messages messages--' . $message_display), - ), - '#weight' => 0, - ); -} - -/** - * Prepares variables for paragraph templates. - * - * Default template: paragraph.html.twig. - * - * Most themes use their own copy of paragraph.html.twig. The default is located - * inside "templates/paragraph.html.twig". Look in there for the - * full list of variables. - * - * @param array $variables - * An associative array containing: - * - elements: An array of elements to display in view mode. - * - paragraph: The paragraph object. - * - view_mode: View mode; e.g., 'full', 'teaser'... - */ -function template_preprocess_paragraph(&$variables) { - $variables['view_mode'] = $variables['elements']['#view_mode']; - $variables['paragraph'] = $variables['elements']['#paragraph']; - - // Helpful $content variable for templates. - $variables += array('content' => array()); - foreach (Element::children($variables['elements']) as $key) { - $variables['content'][$key] = $variables['elements'][$key]; - } - - $paragraph_type = $variables['elements']['#paragraph']->getParagraphType(); - foreach ($paragraph_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_value) { - $plugin_value->preprocess($variables); - } - -} - -/** - * Prepares variables for modal form add widget template. - * - * Default template: paragraphs-add-dialog.html.twig - * - * @param array $variables - * An associative array containing: - * - buttons: An array of buttons to display in the modal form. - */ -function template_preprocess_paragraphs_add_dialog(&$variables) { - // Define variables for the template. - $variables += ['buttons' => []]; - foreach (Element::children($variables['element']) as $key) { - if ($key == 'add_modal_form_area') { - // $add variable for the add button. - $variables['add'] = $variables['element'][$key]; - } - else { - // Buttons for the paragraph types in the modal form. - $variables['buttons'][$key] = $variables['element'][$key]; - } - } -} - -/** - * Prepares variables for paragraphs_actions component. - * - * Default template: paragraphs-actions.html.twig. - * - * @param array $variables - * An associative array containing: - * - actions: An array of default action buttons. - * - dropdown_actions: An array of buttons for dropdown. - */ -function template_preprocess_paragraphs_actions(&$variables) { - // Define variables for the template. - $variables += ['actions' => [], 'dropdown_actions' => []]; - - $element = $variables['element']; - - if (!empty($element['actions'])) { - $variables['actions'] = $element['actions']; - } - - if (!empty($element['dropdown_actions'])) { - $variables['dropdown_actions'] = $element['dropdown_actions']; - } -} - -/** - * Implements hook_preprocess_HOOK() for field_multiple_value_form(). - */ -function paragraphs_preprocess_field_multiple_value_form(&$variables) { - if (count($variables['element']['#field_parents']) === 0 && isset($variables['table']['#rows'])) { - // Find paragraph_actions and move to header. - // @see template_preprocess_field_multiple_value_form() - if (!empty($variables['table']['#rows'][0]['data'][1]['data']['#paragraphs_header'])) { - $variables['table']['#header'][0]['data'] = [ - 'title' => $variables['table']['#header'][0]['data'], - 'button' => $variables['table']['#rows'][0]['data'][1]['data'], - ]; - unset($variables['table']['#rows'][0]); - } - } -} - -/** - * Implements hook_libraries_info(). - */ -function paragraphs_libraries_info() { - $libraries = [ - 'Sortable' => [ - 'name' => 'Sortable', - 'vendor url' => 'https://github.com/RubaXa/Sortable', - 'download url' => 'https://github.com/RubaXa/Sortable/releases', - 'files' => [ - 'js' => [ - 'Sortable.min.js' => [], - ], - ], - 'version arguments' => [ - // The version is at the end of the file, which is currently about 15k - // characters long. - 'file' => 'Sortable.min.js', - 'pattern' => '/\.version="(.*?)"/', - 'lines' => 2, - 'cols' => 20000 - ] - ], - ]; - return $libraries; -} - -/** - * Implements hook_library_info_alter(). - */ -function paragraphs_library_info_alter(&$libraries, $extension) { - if ($extension != 'paragraphs') { - return; - } - - if (\Drupal::moduleHandler()->moduleExists('libraries')) { - $info = libraries_detect('Sortable'); - } - else { - // If the library module is not installed, hardcode the path and fetch - // the required information ourself. - $library_path = 'libraries/Sortable'; - $file = 'Sortable.min.js'; - $path = DRUPAL_ROOT . '/' . $library_path . '/' . $file; - if (file_exists($path)) { - if (preg_match('/\.version="(.*?)"/', file_get_contents($path), $version)) { - $info = [ - 'installed' => TRUE, - 'version' => $version[1], - 'library path' => $library_path, - 'files' => [ - 'js' => [ - $file => [], - ], - ], - ]; - } - } - } - - if (!empty($info['installed'])) { - $libraries['sortable'] += [ - 'version' => $info['version'], - ]; - // Self hosted player, use files from library definition. - if (!empty($info['files']['js'])) { - foreach ($info['files']['js'] as $filename => $options) { - $libraries['sortable']['js']["/{$info['library path']}/{$filename}"] = $options; - } - } - } - else { - // Unset the libraries if we failed to detect them. - unset($libraries['sortable']); - unset($libraries['paragraphs-dragdrop']); - } - - return $libraries; -} diff --git a/web/modules/contrib/paragraphs/paragraphs.permissions.yml b/web/modules/contrib/paragraphs/paragraphs.permissions.yml deleted file mode 100644 index 790ec801b..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.permissions.yml +++ /dev/null @@ -1,7 +0,0 @@ -administer paragraphs types: - title: 'Administer Paragraphs types' - description: 'Allow to define the existing Paragraphs types and their Fields' - -edit behavior plugin settings: - title: 'Edit behavior plugin settings' - description: 'Users with this permission can edit behavior plugin settings on Paragraphs behavior instance' diff --git a/web/modules/contrib/paragraphs/paragraphs.post_update.php b/web/modules/contrib/paragraphs/paragraphs.post_update.php deleted file mode 100644 index bd7d3e735..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.post_update.php +++ /dev/null @@ -1,115 +0,0 @@ -<?php - -/** - * @file - * Post update functions for Paragraphs. - */ - -use Drupal\Core\Site\Settings; -use Drupal\field\Entity\FieldStorageConfig; - -/** - * Set the parent id, type and field name to the already created paragraphs. - * - * @param $sandbox - */ -function paragraphs_post_update_set_paragraphs_parent_fields(&$sandbox) { - // Don't execute the function if paragraphs_update_8003() was already executed - // which used to do the same. - - $module_schema = drupal_get_installed_schema_version('paragraphs'); - - // The state entry 'paragraphs_update_8003_placeholder' is used in order to - // indicate that the placeholder paragraphs_update_8003() function has been - // executed, so this function needs to be executed as well. If the non - // placeholder version of paragraphs_update_8003() got executed already, the - // state won't be set and we skip this update. - if ($module_schema >= 8003 && !\Drupal::state()->get('paragraphs_update_8003_placeholder', FALSE)) { - return; - } - - if (!isset($sandbox['current_paragraph_field_id'])) { - $paragraph_field_ids = []; - // Get all the entity reference revisions fields. - $map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('entity_reference_revisions'); - foreach ($map as $entity_type_id => $info) { - foreach ($info as $name => $data) { - if (FieldStorageConfig::loadByName($entity_type_id, $name)->getSetting('target_type') == 'paragraph') { - $paragraph_field_ids[] = "$entity_type_id.$name"; - } - } - } - - if (!$paragraph_field_ids) { - // There are no paragraph fields. Return before initializing the sandbox. - return; - } - - // Initialize the sandbox. - $sandbox['current_paragraph_field_id'] = 0; - $sandbox['paragraph_field_ids'] = $paragraph_field_ids; - $sandbox['max'] = count($paragraph_field_ids); - $sandbox['progress'] = 0; - } - - /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */ - $field_storage = FieldStorageConfig::load($sandbox['paragraph_field_ids'][$sandbox['current_paragraph_field_id']]); - // For revisionable entity types, we load and update all revisions. - $target_entity_type = \Drupal::entityTypeManager()->getDefinition($field_storage->getTargetEntityTypeId()); - if ($target_entity_type->isRevisionable()) { - $revision_id = $target_entity_type->getKey('revision'); - $entity_ids = \Drupal::entityQuery($field_storage->getTargetEntityTypeId()) - ->condition($field_storage->getName(), NULL, 'IS NOT NULL') - ->range($sandbox['progress'], Settings::get('paragraph_limit', 50)) - ->allRevisions() - ->sort($revision_id, 'ASC') - ->accessCheck(FALSE) - ->execute(); - } - else { - $id = $target_entity_type->getKey('id'); - $entity_ids = \Drupal::entityQuery($field_storage->getTargetEntityTypeId()) - ->condition($field_storage->getName(), NULL, 'IS NOT NULL') - ->range($sandbox['progress'], Settings::get('paragraph_limit', 50)) - ->sort($id, 'ASC') - ->accessCheck(FALSE) - ->execute(); - } - foreach ($entity_ids as $revision_id => $entity_id) { - // For revisionable entity types, we load a specific revision otherwise load - // the entity. - if ($target_entity_type->isRevisionable()) { - $host_entity = \Drupal::entityTypeManager() - ->getStorage($field_storage->getTargetEntityTypeId()) - ->loadRevision($revision_id); - } - else { - $host_entity = \Drupal::entityTypeManager() - ->getStorage($field_storage->getTargetEntityTypeId()) - ->load($entity_id); - } - foreach ($host_entity->get($field_storage->getName()) as $field_item) { - // Skip broken and already updated references (e.g. Nested paragraphs). - if ($field_item->entity && empty($field_item->entity->parent_type->value)) { - // Set the parent fields and save, ensure that no new revision is - // created. - $field_item->entity->parent_type = $field_storage->getTargetEntityTypeId(); - $field_item->entity->parent_id = $host_entity->id(); - $field_item->entity->parent_field_name = $field_storage->getName(); - $field_item->entity->setNewRevision(FALSE); - $field_item->entity->save(); - } - } - } - // Continue with the next paragraph_field_id when the loaded entities are less - // than paragraph_limit. - if (count($entity_ids) < Settings::get('paragraph_limit', 50)) { - $sandbox['current_paragraph_field_id']++; - $sandbox['progress'] = 0; - } - else { - $sandbox['progress'] += Settings::get('paragraph_limit', 50); - } - // Update #finished, 1 if the the whole update has finished. - $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['current_paragraph_field_id'] / $sandbox['max']); -} diff --git a/web/modules/contrib/paragraphs/paragraphs.routing.yml b/web/modules/contrib/paragraphs/paragraphs.routing.yml deleted file mode 100644 index 8260d9e17..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.routing.yml +++ /dev/null @@ -1,31 +0,0 @@ -# ParagraphsType routing definition -entity.paragraphs_type.collection: - path: '/admin/structure/paragraphs_type' - defaults: - _entity_list: 'paragraphs_type' - _title: 'Paragraphs types' - requirements: - _permission: 'administer paragraphs types' - -paragraphs.type_add: - path: '/admin/structure/paragraphs_type/add' - defaults: - _entity_form: 'paragraphs_type.add' - _title: 'Add Paragraphs type' - requirements: - _permission: 'administer paragraphs types' - -entity.paragraphs_type.edit_form: - path: '/admin/structure/paragraphs_type/{paragraphs_type}' - defaults: - _entity_form: 'paragraphs_type.edit' - requirements: - _permission: 'administer paragraphs types' - -entity.paragraphs_type.delete_form: - path: '/admin/structure/paragraphs_type/{paragraphs_type}/delete' - defaults: - _entity_form: 'paragraphs_type.delete' - _title: 'Delete' - requirements: - _permission: 'administer paragraphs types' diff --git a/web/modules/contrib/paragraphs/paragraphs.services.yml b/web/modules/contrib/paragraphs/paragraphs.services.yml deleted file mode 100755 index f70039287..000000000 --- a/web/modules/contrib/paragraphs/paragraphs.services.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - plugin.manager.paragraphs.behavior: - class: Drupal\paragraphs\ParagraphsBehaviorManager - parent: default_plugin_manager - arguments: ['@entity_type.manager', '@config.factory'] diff --git a/web/modules/contrib/paragraphs/src/Annotation/ParagraphsBehavior.php b/web/modules/contrib/paragraphs/src/Annotation/ParagraphsBehavior.php deleted file mode 100644 index 96d9ef28b..000000000 --- a/web/modules/contrib/paragraphs/src/Annotation/ParagraphsBehavior.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Annotation; - -use Drupal\Component\Annotation\Plugin; - -/** - * Defines a ParagraphsBehavior annotation object. - * - * Paragraphs behavior builders handle extra settings for the paragraph - * entity. - * - * @Annotation - * - */ -class ParagraphsBehavior extends Plugin { - - /** - * The plugin ID. - * - * @var string - */ - public $id; - - /** - * The human-readable name of the paragraphs behavior plugin. - * - * @ingroup plugin_translatable - * - * @var \Drupal\Core\Annotation\Translation - */ - public $label; - - /** - * The plugin description. - * - * @ingroup plugin_translatable - * - * @var string - */ - public $description; - - /** - * The plugin weight. - * - * @var int - */ - public $weight; - -} diff --git a/web/modules/contrib/paragraphs/src/Controller/ParagraphsTypeListBuilder.php b/web/modules/contrib/paragraphs/src/Controller/ParagraphsTypeListBuilder.php deleted file mode 100644 index 48fd9c092..000000000 --- a/web/modules/contrib/paragraphs/src/Controller/ParagraphsTypeListBuilder.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Controller; - -use Drupal\Core\Config\Entity\ConfigEntityListBuilder; -use Drupal\Core\Entity\EntityInterface; - -/** - * Provides a listing of ParagraphsType. - */ -class ParagraphsTypeListBuilder extends ConfigEntityListBuilder { - - /** - * {@inheritdoc} - */ - public function buildHeader() { - $header['icon_file'] = [ - 'data' => $this->t('Icon'), - ]; - $header['label'] = $this->t('Label'); - $header['id'] = $this->t('Machine name'); - $header['description'] = $this->t('Description'); - - return $header + parent::buildHeader(); - } - - /** - * {@inheritdoc} - */ - public function buildRow(EntityInterface $entity) { - $row['icon_file'] = []; - if ($icon_url = $entity->getIconUrl()) { - $row['icon_file']['class'][] = 'paragraphs-type-icon'; - $row['icon_file']['data'] = [ - '#theme' => 'image', - '#uri' => $icon_url, - '#width' => 32, - '#height' => 32, - ]; - } - $row['label'] = $entity->label(); - $row['id'] = $entity->id(); - $row['description']['data'] = ['#markup' => $entity->getDescription()]; - // You probably want a few more properties here... - return $row + parent::buildRow($entity); - } - - /** - * {@inheritdoc} - */ - public function getDefaultOperations(EntityInterface $entity) { - /** @var \Drupal\field\FieldConfigInterface $entity */ - $operations = parent::getDefaultOperations($entity); - - if (isset($operations['edit'])) { - $operations['edit']['weight'] = 30; - } - - return $operations; - } - - /** - * {@inheritdoc} - */ - public function render() { - $build = parent::render(); - $build['#attached']['library'][] = 'paragraphs/drupal.paragraphs.list_builder'; - return $build; - } - -} diff --git a/web/modules/contrib/paragraphs/src/Element/ParagraphOperations.php b/web/modules/contrib/paragraphs/src/Element/ParagraphOperations.php deleted file mode 100644 index 679c56aca..000000000 --- a/web/modules/contrib/paragraphs/src/Element/ParagraphOperations.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Element; - -use Drupal\Core\Render\Element\Operations; -use Drupal\Core\Render\Element\RenderElement; - -/** - * {@inheritdoc} - * - * @RenderElement("paragraph_operations") - */ -class ParagraphOperations extends Operations { - - /** - * {@inheritdoc} - */ - public function getInfo() { - return ['#theme' => 'links__dropbutton__operations__paragraphs'] + parent::getInfo(); - } - - /** - * {@inheritdoc} - */ - public static function preRenderDropbutton($element) { - $element = parent::preRenderDropbutton($element); - - // Attach #ajax events if title is a render array. - foreach ($element['#links'] as &$link) { - if (isset($link['title']['#ajax'])) { - $link['title'] = RenderElement::preRenderAjaxForm($link['title']); - } - } - - return $element; - } - -} diff --git a/web/modules/contrib/paragraphs/src/Element/ParagraphsActions.php b/web/modules/contrib/paragraphs/src/Element/ParagraphsActions.php deleted file mode 100644 index a199439b9..000000000 --- a/web/modules/contrib/paragraphs/src/Element/ParagraphsActions.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Element; - -use Drupal\Core\Render\Element; -use Drupal\Core\Render\Element\RenderElement; - -/** - * Provides a render element for a paragraphs actions. - * - * Paragraphs actions can have two type of actions - * - actions - this are default actions that are always visible. - * - dropdown_actions - actions that are in dropdown sub component. - * - * Usage example: - * - * @code - * $form['actions'] = [ - * '#type' => 'paragraphs_actions', - * 'actions' => $actions, - * 'dropdown_actions' => $dropdown_actions, - * ]; - * $dropdown_actions['button'] = array( - * '#type' => 'submit', - * ); - * @endcode - * - * @FormElement("paragraphs_actions") - */ -class ParagraphsActions extends RenderElement { - - /** - * {@inheritdoc} - */ - public function getInfo() { - $class = get_class($this); - - return [ - '#pre_render' => [ - [$class, 'preRenderParagraphsActions'], - ], - '#theme' => 'paragraphs_actions', - ]; - } - - /** - * Pre render callback for #type 'paragraphs_actions'. - * - * @param array $element - * Element arrar of a #type 'paragraphs_actions'. - * - * @return array - * The processed element. - */ - public static function preRenderParagraphsActions(array $element) { - $element['#attached']['library'][] = 'paragraphs/drupal.paragraphs.actions'; - - if (!empty($element['dropdown_actions'])) { - foreach (Element::children($element['dropdown_actions']) as $key) { - $dropdown_action = &$element['dropdown_actions'][$key]; - if (isset($dropdown_action['#ajax'])) { - $dropdown_action = RenderElement::preRenderAjaxForm($dropdown_action); - } - if (empty($dropdown_action['#attributes'])) { - $dropdown_action['#attributes'] = ['class' => ['paragraphs-dropdown-action']]; - } - else { - $dropdown_action['#attributes']['class'][] = 'paragraphs-dropdown-action'; - } - } - } - - return $element; - } - -} diff --git a/web/modules/contrib/paragraphs/src/Entity/Paragraph.php b/web/modules/contrib/paragraphs/src/Entity/Paragraph.php deleted file mode 100644 index 378216c09..000000000 --- a/web/modules/contrib/paragraphs/src/Entity/Paragraph.php +++ /dev/null @@ -1,672 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Entity; - -use Drupal\Component\Utility\NestedArray; -use Drupal\Component\Utility\Unicode; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Entity\ContentEntityBase; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\ChangedFieldItemList; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\TypedData\TranslatableInterface; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\entity_reference_revisions\EntityNeedsSaveInterface; -use Drupal\entity_reference_revisions\EntityNeedsSaveTrait; -use Drupal\paragraphs\ParagraphInterface; -use Drupal\user\UserInterface; - -/** - * Defines the Paragraph entity. - * - * @ingroup paragraphs - * - * @ContentEntityType( - * id = "paragraph", - * label = @Translation("Paragraph"), - * bundle_label = @Translation("Paragraph type"), - * handlers = { - * "view_builder" = "Drupal\paragraphs\ParagraphViewBuilder", - * "access" = "Drupal\paragraphs\ParagraphAccessControlHandler", - * "storage_schema" = "Drupal\paragraphs\ParagraphStorageSchema", - * "form" = { - * "default" = "Drupal\Core\Entity\ContentEntityForm", - * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", - * "edit" = "Drupal\Core\Entity\ContentEntityForm" - * }, - * "views_data" = "Drupal\views\EntityViewsData", - * }, - * base_table = "paragraphs_item", - * data_table = "paragraphs_item_field_data", - * revision_table = "paragraphs_item_revision", - * revision_data_table = "paragraphs_item_revision_field_data", - * translatable = TRUE, - * entity_revision_parent_type_field = "parent_type", - * entity_revision_parent_id_field = "parent_id", - * entity_revision_parent_field_name_field = "parent_field_name", - * entity_keys = { - * "id" = "id", - * "uuid" = "uuid", - * "bundle" = "type", - * "langcode" = "langcode", - * "revision" = "revision_id" - * }, - * bundle_entity_type = "paragraphs_type", - * field_ui_base_route = "entity.paragraphs_type.edit_form", - * common_reference_revisions_target = TRUE, - * content_translation_ui_skip = TRUE, - * render_cache = FALSE, - * default_reference_revision_settings = { - * "field_storage_config" = { - * "cardinality" = -1, - * "settings" = { - * "target_type" = "paragraph" - * } - * }, - * "field_config" = { - * "settings" = { - * "handler" = "default:paragraph" - * } - * }, - * "entity_form_display" = { - * "type" = "entity_reference_paragraphs" - * }, - * "entity_view_display" = { - * "type" = "entity_reference_revisions_entity_view" - * } - * } - * ) - */ -class Paragraph extends ContentEntityBase implements ParagraphInterface { - - use EntityNeedsSaveTrait; - - /** - * The behavior plugin data for the paragraph entity. - * - * @var array - */ - protected $unserializedBehaviorSettings; - - /** - * Number of summaries. - * - * @var int - */ - protected $summaryCount; - - /** - * {@inheritdoc} - */ - public function getParentEntity() { - if (!isset($this->get('parent_type')->value) || !isset($this->get('parent_id')->value)) { - return NULL; - } - - $parent = \Drupal::entityTypeManager()->getStorage($this->get('parent_type')->value)->load($this->get('parent_id')->value); - - // Return current translation of parent entity, if it exists. - if ($parent != NULL && ($parent instanceof TranslatableInterface) && $parent->hasTranslation($this->language()->getId())) { - return $parent->getTranslation($this->language()->getId()); - } - - return $parent; - } - - /** - * {@inheritdoc} - */ - public function label() { - $label = ''; - if ($parent = $this->getParentEntity()) { - $parent_field = $this->get('parent_field_name')->value; - $values = $parent->{$parent_field}; - foreach ($values as $key => $value) { - if ($value->entity->id() == $this->id()) { - $label = $parent->label() . ' > ' . $value->getFieldDefinition()->getLabel(); - } - } - } - return $label; - } - - /** - * {@inheritdoc} - */ - public function preSave(EntityStorageInterface $storage) { - parent::preSave($storage); - - // If no owner has been set explicitly, make the current user the owner. - if (!$this->getOwner()) { - $this->setOwnerId(\Drupal::currentUser()->id()); - } - // If no revision author has been set explicitly, make the node owner the - // revision author. - if (!$this->getRevisionAuthor()) { - $this->setRevisionAuthorId($this->getOwnerId()); - } - - // If behavior settings are not set then get them from the entity. - if ($this->unserializedBehaviorSettings !== NULL) { - $this->set('behavior_settings', serialize($this->unserializedBehaviorSettings)); - } - } - - /** - * {@inheritdoc} - */ - public function getAllBehaviorSettings() { - if ($this->unserializedBehaviorSettings === NULL) { - $this->unserializedBehaviorSettings = unserialize($this->get('behavior_settings')->value); - } - if (!is_array($this->unserializedBehaviorSettings)) { - $this->unserializedBehaviorSettings = []; - } - return $this->unserializedBehaviorSettings; - } - - /** - * {@inheritdoc} - */ - public function &getBehaviorSetting($plugin_id, $key, $default = NULL) { - $settings = $this->getAllBehaviorSettings(); - $exists = NULL; - $value = &NestedArray::getValue($settings, array_merge((array) $plugin_id, (array) $key), $exists); - if (!$exists) { - $value = $default; - } - return $value; - } - - /** - * {@inheritdoc} - */ - public function setAllBehaviorSettings(array $settings) { - // Set behavior settings fields. - $this->unserializedBehaviorSettings = $settings; - } - - /** - * {@inheritdoc} - */ - public function setBehaviorSettings($plugin_id, array $settings) { - // Set behavior settings fields. - $this->unserializedBehaviorSettings[$plugin_id] = $settings; - } - - /** - * {@inheritdoc} - */ - public function postSave(EntityStorageInterface $storage, $update = TRUE) { - $this->setNeedsSave(FALSE); - parent::postSave($storage, $update); - } - - /** - * {@inheritdoc} - */ - public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { - parent::preSaveRevision($storage, $record); - } - - /** - * {@inheritdoc} - */ - public function getCreatedTime() { - return $this->get('created')->value; - } - - /** - * {@inheritdoc} - */ - public function getOwner() { - return $this->get('uid')->entity; - } - - /** - * {@inheritdoc} - */ - public function getOwnerId() { - return $this->get('uid')->target_id; - } - - /** - * {@inheritdoc} - */ - public function setOwnerId($uid) { - $this->set('uid', $uid); - return $this; - } - - /** - * {@inheritdoc} - */ - public function setOwner(UserInterface $account) { - $this->set('uid', $account->id()); - return $this; - } - - /** - * {@inheritdoc} - */ - public function getType() { - return $this->bundle(); - } - - /** - * {@inheritdoc} - */ - public function getParagraphType() { - return $this->type->entity; - } - - /** - * {@inheritdoc} - */ - public function getRevisionAuthor() { - return $this->get('revision_uid')->entity; - } - - /** - * {@inheritdoc} - */ - public function setRevisionAuthorId($uid) { - $this->set('revision_uid', $uid); - return $this; - } - - /** - * {@inheritdoc} - */ - public function getRevisionLog() { - return ''; - } - - /** - * {@inheritdoc} - */ - public function setRevisionLog($revision_log) { - return $this; - } - - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields['id'] = BaseFieldDefinition::create('integer') - ->setLabel(t('ID')) - ->setDescription(t('The ID of the Paragraphs entity.')) - ->setReadOnly(TRUE) - ->setSetting('unsigned', TRUE); - - $fields['uuid'] = BaseFieldDefinition::create('uuid') - ->setLabel(t('UUID')) - ->setDescription(t('The UUID of the paragraphs entity.')) - ->setReadOnly(TRUE); - - $fields['revision_id'] = BaseFieldDefinition::create('integer') - ->setLabel(t('Revision ID')) - ->setDescription(t('The paragraphs entity revision ID.')) - ->setReadOnly(TRUE) - ->setSetting('unsigned', TRUE); - - $fields['type'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Type')) - ->setDescription(t('The Paragraphs type.')) - ->setSetting('target_type', 'paragraphs_type') - ->setReadOnly(TRUE); - - $fields['langcode'] = BaseFieldDefinition::create('language') - ->setLabel(t('Language code')) - ->setDescription(t('The paragraphs entity language code.')) - ->setRevisionable(TRUE); - - $fields['uid'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Authored by')) - ->setDescription(t('The user ID of the paragraphs author.')) - ->setRevisionable(TRUE) - ->setSetting('target_type', 'user') - ->setSetting('handler', 'default') - ->setDefaultValueCallback('Drupal\paragraphs\Entity\Paragraph::getCurrentUserId') - ->setTranslatable(TRUE) - ->setDisplayOptions('form', array( - 'type' => 'hidden', - 'weight' => 0, - )) - ->setDisplayConfigurable('form', TRUE); - - $fields['status'] = BaseFieldDefinition::create('boolean') - ->setLabel(t('Published')) - ->setRevisionable(TRUE) - ->setTranslatable(TRUE) - ->setDefaultValue(TRUE) - ->setDisplayConfigurable('form', TRUE); - - $fields['created'] = BaseFieldDefinition::create('created') - ->setLabel(t('Authored on')) - ->setDescription(t('The time that the Paragraph was created.')) - ->setRevisionable(TRUE) - ->setTranslatable(TRUE) - ->setDisplayOptions('form', array( - 'type' => 'hidden', - 'weight' => 0, - )) - ->setDisplayConfigurable('form', TRUE); - - $fields['revision_uid'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Revision user ID')) - ->setDescription(t('The user ID of the author of the current revision.')) - ->setSetting('target_type', 'user') - ->setQueryable(FALSE) - ->setRevisionable(TRUE); - - $fields['parent_id'] = BaseFieldDefinition::create('string') - ->setLabel(t('Parent ID')) - ->setDescription(t('The ID of the parent entity of which this entity is referenced.')) - ->setSetting('is_ascii', TRUE); - - $fields['parent_type'] = BaseFieldDefinition::create('string') - ->setLabel(t('Parent type')) - ->setDescription(t('The entity parent type to which this entity is referenced.')) - ->setSetting('is_ascii', TRUE) - ->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH); - - $fields['parent_field_name'] = BaseFieldDefinition::create('string') - ->setLabel(t('Parent field name')) - ->setDescription(t('The entity parent field name to which this entity is referenced.')) - ->setSetting('is_ascii', TRUE) - ->setSetting('max_length', FieldStorageConfig::NAME_MAX_LENGTH); - - $fields['behavior_settings'] = BaseFieldDefinition::create('string_long') - ->setLabel(t('Behavior settings')) - ->setDescription(t('The behavior plugin settings')) - ->setRevisionable(TRUE) - ->setDefaultValue(serialize([])); - - return $fields; - } - - /** - * Default value callback for 'uid' base field definition. - * - * @see ::baseFieldDefinitions() - * - * @return array - * An array of default values. - */ - public static function getCurrentUserId() { - return array(\Drupal::currentUser()->id()); - } - - /** - * {@inheritdoc} - */ - public function createDuplicate() { - $duplicate = parent::createDuplicate(); - // Loop over entity fields and duplicate nested paragraphs. - foreach ($duplicate->getFields() as $field) { - if ($field->getFieldDefinition()->getType() == 'entity_reference_revisions') { - if ($field->getFieldDefinition()->getTargetEntityTypeId() == "paragraph") { - foreach ($field as $item) { - $item->entity = $item->entity->createDuplicate(); - } - } - } - } - return $duplicate; - } - - /** - * {@inheritdoc} - */ - public function getSummary(array $options = []) { - $show_behavior_summary = isset($options['show_behavior_summary']) ? $options['show_behavior_summary'] : TRUE; - $depth_limit = isset($options['depth_limit']) ? $options['depth_limit'] : 1; - $this->summaryCount = 0; - $summary = []; - - foreach ($this->getFieldDefinitions() as $field_name => $field_definition) { - if ($field_definition->getType() == 'image' || $field_definition->getType() == 'file') { - $file_summary = $this->getFileSummary($field_name); - if ($file_summary != '') { - $summary[] = $file_summary; - } - } - - $text_summary = $this->getTextSummary($field_name, $field_definition); - if ($text_summary != '') { - $summary[] = $text_summary; - } - - if ($field_definition->getType() == 'entity_reference_revisions') { - // Decrease the depth, since we are entering a nested paragraph. - $nested_summary = $this->getNestedSummary($field_name, [ - 'show_behavior_summary' => $show_behavior_summary, - 'depth_limit' => $depth_limit - 1 - ]); - if ($nested_summary != '') { - $summary[] = $nested_summary; - } - } - - if ($field_type = $field_definition->getType() == 'entity_reference') { - if (!in_array($field_name, ['type', 'uid', 'revision_uid'])) { - if ($this->get($field_name)->entity) { - $summary[] = $this->get($field_name)->entity->label(); - } - } - } - - // Add the Block admin label referenced by block_field. - if ($field_definition->getType() == 'block_field') { - if (!empty($this->get($field_name)->first())) { - $block_admin_label = $this->get($field_name)->first()->getBlock()->getPluginDefinition()['admin_label']; - $summary[] = $block_admin_label; - } - } - } - - if ($show_behavior_summary) { - $paragraphs_type = $this->getParagraphType(); - foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) { - if ($plugin_summary = $plugin->settingsSummary($this)) { - $summary = array_merge($summary, $plugin_summary); - } - } - } - - if ($this->summaryCount) { - array_unshift($summary, (string) \Drupal::translation()->formatPlural($this->summaryCount, '1 child', '@count children')); - } - - $collapsed_summary_text = implode(', ', $summary); - return strip_tags($collapsed_summary_text); - } - - /** - * Returns an array of field names to skip in ::isChanged. - * - * @return array - * An array of field names. - */ - protected function getFieldsToSkipFromChangedCheck() { - // A list of revision fields which should be skipped from the comparision. - $fields = [ - $this->getEntityType()->getKey('revision'), - 'revision_uid', - 'revision_log', - 'revision_log_message', - ]; - - return $fields; - } - - /** - * {@inheritdoc} - */ - public function isChanged() { - if ($this->isNew()) { - return TRUE; - } - - // $this->original only exists during save. If it exists we re-use it here - // for performance reasons. - /** @var \Drupal\paragraphs\ParagraphInterface $original */ - $original = $this->original ?: NULL; - if (!$original) { - $original = $this->entityTypeManager()->getStorage($this->getEntityTypeId())->loadRevision($this->getLoadedRevisionId()); - } - - // If the current revision has just been added, we have a change. - if ($original->isNewRevision()) { - return TRUE; - } - - // The list of fields to skip from the comparision. - $skip_fields = $this->getFieldsToSkipFromChangedCheck(); - - // Compare field item current values with the original ones to determine - // whether we have changes. We skip also computed fields as comparing them - // with their original values might not be possible or be meaningless. - foreach ($this->getFieldDefinitions() as $field_name => $definition) { - if (in_array($field_name, $skip_fields, TRUE)) { - continue; - } - $field = $this->get($field_name); - // When saving entities in the user interface, the changed timestamp is - // automatically incremented by ContentEntityForm::submitForm() even if - // nothing was actually changed. Thus, the changed time needs to be - // ignored when determining whether there are any actual changes in the - // entity. - if (!($field instanceof ChangedFieldItemList) && !$definition->isComputed()) { - $items = $field->filterEmptyItems(); - $original_items = $original->get($field_name)->filterEmptyItems(); - if (!$items->equals($original_items)) { - return TRUE; - } - } - } - - return FALSE; - } - - /** - * Returns summary for file paragraph. - * - * @param string $field_name - * Field name from field definition. - * - * @return string - * Summary for image. - */ - protected function getFileSummary($field_name) { - $summary = ''; - if ($this->get($field_name)->entity) { - foreach ($this->get($field_name) as $file_key => $file_value) { - - $text = ''; - if ($file_value->description != '') { - $text = $file_value->description; - } - elseif ($file_value->title != '') { - $text = $file_value->title; - } - elseif ($file_value->alt != '') { - $text = $file_value->alt; - } - elseif ($file_value->entity->getFileName()) { - $text = $file_value->entity->getFileName(); - } - - if (strlen($text) > 150) { - $text = Unicode::truncate($text, 150); - } - - $summary = $text; - } - } - - return trim($summary); - } - - /** - * Returns summary for nested paragraphs. - * - * @param string $field_name - * Field definition id for paragraph. - * @param array $options - * (optional) An associative array of additional options. - * See \Drupal\paragraphs\ParagraphInterface::getSummary() for all of the - * available options. - * - * @return string - * Short summary for nested Paragraphs type - * or NULL if the summary is empty. - */ - protected function getNestedSummary($field_name, array $options) { - $summary = []; - if ($options['depth_limit'] >= 0) { - foreach ($this->get($field_name) as $item) { - $entity = $item->entity; - if ($entity instanceof ParagraphInterface) { - $summary[] = $entity->getSummary($options); - $this->summaryCount++; - } - } - } - - $summary = array_filter($summary); - - if (empty($summary)) { - return NULL; - } - - $paragraph_summary = implode(', ', $summary); - return $paragraph_summary; - } - - /** - * Returns summary for all text type paragraph. - * - * @param string $field_name - * Field definition id for paragraph. - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * Field definition for paragraph. - * - * @return string - * Short summary for text paragraph. - */ - public function getTextSummary($field_name, FieldDefinitionInterface $field_definition) { - $text_types = [ - 'text_with_summary', - 'text', - 'text_long', - 'list_string', - 'string', - ]; - - $excluded_text_types = [ - 'parent_id', - 'parent_type', - 'parent_field_name', - ]; - - $summary = ''; - if (in_array($field_definition->getType(), $text_types)) { - if (in_array($field_name, $excluded_text_types)) { - return $summary; - } - - $text = $this->get($field_name)->value; - if (strlen($text) > 150) { - $text = Unicode::truncate($text, 150); - } - - $summary = $text; - } - - return trim($summary); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Entity/ParagraphsType.php b/web/modules/contrib/paragraphs/src/Entity/ParagraphsType.php deleted file mode 100644 index 1ebae77b7..000000000 --- a/web/modules/contrib/paragraphs/src/Entity/ParagraphsType.php +++ /dev/null @@ -1,228 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Entity; - -use Drupal\Core\Config\Entity\ConfigEntityBundleBase; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\EntityWithPluginCollectionInterface; -use Drupal\Core\Render\Element\File; -use Drupal\paragraphs\ParagraphsBehaviorCollection; -use Drupal\paragraphs\ParagraphsBehaviorInterface; -use Drupal\paragraphs\ParagraphsTypeInterface; - -/** - * Defines the ParagraphsType entity. - * - * @ConfigEntityType( - * id = "paragraphs_type", - * label = @Translation("Paragraphs type"), - * handlers = { - * "list_builder" = "Drupal\paragraphs\Controller\ParagraphsTypeListBuilder", - * "form" = { - * "add" = "Drupal\paragraphs\Form\ParagraphsTypeForm", - * "edit" = "Drupal\paragraphs\Form\ParagraphsTypeForm", - * "delete" = "Drupal\paragraphs\Form\ParagraphsTypeDeleteConfirm" - * } - * }, - * config_prefix = "paragraphs_type", - * admin_permission = "administer paragraphs types", - * entity_keys = { - * "id" = "id", - * "label" = "label", - * }, - * config_export = { - * "id", - * "label", - * "icon_uuid", - * "description", - * "behavior_plugins", - * }, - * bundle_of = "paragraph", - * links = { - * "edit-form" = "/admin/structure/paragraphs_type/{paragraphs_type}", - * "delete-form" = "/admin/structure/paragraphs_type/{paragraphs_type}/delete", - * "collection" = "/admin/structure/paragraphs_type", - * } - * ) - */ -class ParagraphsType extends ConfigEntityBundleBase implements ParagraphsTypeInterface, EntityWithPluginCollectionInterface { - - /** - * The ParagraphsType ID. - * - * @var string - */ - public $id; - - /** - * The ParagraphsType label. - * - * @var string - */ - public $label; - - /** - * A brief description of this paragraph type. - * - * @var string - */ - public $description; - - /** - * UUID of the Paragraphs type icon file. - * - * @var string - */ - protected $icon_uuid; - - /** - * The Paragraphs type behavior plugins configuration keyed by their id. - * - * @var array - */ - public $behavior_plugins = []; - - /** - * Holds the collection of behavior plugins that are attached to this - * Paragraphs type. - * - * @var \Drupal\paragraphs\ParagraphsBehaviorCollection - */ - protected $behaviorCollection; - - /** - * {@inheritdoc} - */ - public function getIconFile() { - if ($this->icon_uuid && $icon = $this->getFileByUuid($this->icon_uuid)) { - return $icon; - } - - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function getBehaviorPlugins() { - if (!isset($this->behaviorCollection)) { - $this->behaviorCollection = new ParagraphsBehaviorCollection(\Drupal::service('plugin.manager.paragraphs.behavior'), $this->behavior_plugins); - } - return $this->behaviorCollection; - } - - /** - * {@inheritdoc} - */ - public function getIconUrl() { - if ($image = $this->getIconFile()) { - return file_create_url($image->getFileUri()); - } - - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function getBehaviorPlugin($instance_id) { - return $this->getBehaviorPlugins()->get($instance_id); - } - - /** - * {@inheritdoc} - */ - public function calculateDependencies() { - parent::calculateDependencies(); - - // Add the file icon entity as dependency if a UUID was specified. - if ($this->icon_uuid && $file_icon = $this->getIconFile()) { - $this->addDependency($file_icon->getConfigDependencyKey(), $file_icon->getConfigDependencyName()); - } - - return $this->dependencies; - } - - /** - * {@inheritdoc} - */ - public function getEnabledBehaviorPlugins() { - return $this->getBehaviorPlugins()->getEnabled(); - } - - /** - * {@inheritdoc} - */ - public function getPluginCollections() { - return ['behavior_plugins' => $this->getBehaviorPlugins()]; - } - - /** - * {@inheritdoc} - */ - public function getDescription() { - return $this->description; - } - - /** - * {@inheritdoc} - */ - public function hasEnabledBehaviorPlugin($plugin_id) { - $plugins = $this->getBehaviorPlugins(); - if ($plugins->has($plugin_id)) { - /** @var ParagraphsBehaviorInterface $plugin */ - $plugin = $plugins->get($plugin_id); - $config = $plugin->getConfiguration(); - return (array_key_exists('enabled', $config) && $config['enabled'] === TRUE); - } - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function postSave(EntityStorageInterface $storage, $update = TRUE) { - // Update the file usage for the icon files. - if (!$update || $this->icon_uuid != $this->original->icon_uuid) { - // The icon has changed. Update file usage. - /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */ - $file_usage = \Drupal::service('file.usage'); - - // Add usage of the new icon file, if it exists. It might not exist, if - // this Paragraphs type was imported as configuration, or if the icon has - // just been removed. - if ($this->icon_uuid && $new_icon = $this->getFileByUuid($this->icon_uuid)) { - $file_usage->add($new_icon, 'paragraphs', 'paragraphs_type', $this->id()); - } - if ($update) { - // Delete usage of the old icon file, if it exists. - if ($this->original->icon_uuid && $old_icon = $this->getFileByUuid($this->original->icon_uuid)) { - $file_usage->delete($old_icon, 'paragraphs', 'paragraphs_type', $this->id()); - } - } - } - - parent::postSave($storage, $update); - } - - /** - * Gets the file entity defined by the UUID. - * - * @param string $uuid - * The file entity's UUID. - * - * @return \Drupal\file\FileInterface|null - * The file entity. NULL if the UUID is invalid. - */ - protected function getFileByUuid($uuid) { - $files = $this->entityTypeManager() - ->getStorage('file') - ->loadByProperties(['uuid' => $uuid]); - if ($files) { - return current($files); - } - - return NULL; - } - -} diff --git a/web/modules/contrib/paragraphs/src/EventSubscriber/ReplicateFieldSubscriber.php b/web/modules/contrib/paragraphs/src/EventSubscriber/ReplicateFieldSubscriber.php deleted file mode 100644 index 7602c0b95..000000000 --- a/web/modules/contrib/paragraphs/src/EventSubscriber/ReplicateFieldSubscriber.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php - -namespace Drupal\paragraphs\EventSubscriber; - -use Drupal\replicate\Events\ReplicateEntityFieldEvent; -use Drupal\replicate\Events\ReplicatorEvents; -use Drupal\replicate\Replicator; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - -/** - * Event subscriber that handles cloning through the Replicate module. - */ -class ReplicateFieldSubscriber implements EventSubscriberInterface { - - /** - * The replicator service. - * - * @var \Drupal\replicate\Replicator - */ - protected $replicator; - - /** - * ReplicateFieldSubscriber constructor. - * - * @param \Drupal\replicate\Replicator $replicator - * The replicator service. - */ - public function __construct(Replicator $replicator) { - $this->replicator = $replicator; - } - - /** - * Replicates paragraphs when the parent entity is being replicated. - * - * @param \Drupal\replicate\Events\ReplicateEntityFieldEvent $event - */ - public function onClone(ReplicateEntityFieldEvent $event) { - $field_item_list = $event->getFieldItemList(); - if ($field_item_list->getItemDefinition()->getSetting('target_type') == 'paragraph') { - foreach ($field_item_list as $field_item) { - $field_item->entity = $this->replicator->replicateEntity($field_item->entity); - } - } - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() { - $events[ReplicatorEvents::replicateEntityField('entity_reference_revisions')][] = 'onClone'; - return $events; - } - -} diff --git a/web/modules/contrib/paragraphs/src/Feeds/Target/Paragraphs.php b/web/modules/contrib/paragraphs/src/Feeds/Target/Paragraphs.php deleted file mode 100644 index b71cd84ea..000000000 --- a/web/modules/contrib/paragraphs/src/Feeds/Target/Paragraphs.php +++ /dev/null @@ -1,133 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Feeds\Target; - -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\feeds\Feeds\Target\Text; -use Drupal\feeds\Plugin\Type\Target\ConfigurableTargetInterface; - -/** - * Feeds target plugin for Paragraphs fields. - * - * @FeedsTarget( - * id = "paragraphs", - * field_types = {"entity_reference_revisions"}, - * arguments = {"@entity.manager", "@current_user"} - * ) - */ -class Paragraphs extends Text implements ConfigurableTargetInterface { - - /** - * The paragraph storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $paragraphStorage; - - /** - * The paragraphs type storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $paragraphsTypeStorage; - - /** - * The field config storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $fieldConfigStorage; - - /** - * Constructs the target plugin. - */ - public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $current_user); - $this->paragraphStorage = $entity_type_manager->getStorage('paragraph'); - $this->paragraphsTypeStorage = $entity_type_manager->getStorage('paragraphs_type'); - $this->fieldConfigStorage = $entity_type_manager->getStorage('field_config'); - } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return parent::defaultConfiguration() + [ - 'paragraphs_type' => NULL, - 'paragraph_field' => NULL, - ]; - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form['paragraphs_type'] = [ - '#type' => 'select', - '#title' => $this->t('Paragraphs type'), - '#required' => TRUE, - '#options' => array_map(function(EntityInterface $paragraphs_type) { - return $paragraphs_type->label(); - }, $this->paragraphsTypeStorage->loadMultiple()), - '#default_value' => $this->configuration['paragraphs_type'], - ]; - - // Load and filter field configs to create options. - /** @var \Drupal\field\FieldConfigInterface[] $field_configs */ - $field_configs = $this->fieldConfigStorage->loadByProperties([ - 'entity_type' => 'paragraph', - 'bundle' => $this->configuration['paragraphs_type'], - ]); - $field_options = []; - foreach ($field_configs as $field_config) { - if (in_array($field_config->getType(), ['text', 'text_long', 'text_with_summary'])) { - $field_options[$field_config->getName()] = $field_config->label(); - } - } - - $form['paragraph_field'] = [ - '#type' => 'select', - '#title' => $this->t('Paragraph field'), - '#description' => $this->t('<strong>Note:</strong> Field options do not appear until a type has been chosen and saved.'), - '#options' => $field_options, - ]; - - $form = parent::buildConfigurationForm($form, $form_state); - - return $form; - } - - /** - * {@inheritdoc} - */ - public function getSummary() { - $summary = $this->t('Not yet configured.'); - $paragraphs_type_id = $this->configuration['paragraphs_type']; - $paragraph_field_name = $this->configuration['paragraph_field']; - if ($paragraphs_type_id && $paragraphs_type = $this->paragraphsTypeStorage->load($paragraphs_type_id)) { - if ($paragraph_field_name && $paragraph_field = $this->fieldConfigStorage->load('paragraph.' . $paragraphs_type_id . '.' . $paragraph_field_name)) { - $summary = $this->t('Using the %field field on a %type paragraph.', [ - '%field' => $paragraph_field->label(), - '%type' => $paragraphs_type->label(), - ]); - } - } - return $summary . '<br>' . parent::getSummary(); - } - - /** - * {@inheritdoc} - */ - protected function prepareValue($delta, array &$values) { - parent::prepareValue($delta, $values); - $paragraph = $this->paragraphStorage->create([ - 'type' => $this->configuration['paragraphs_type'], - $this->configuration['paragraph_field'] => $values, - ]); - $values = ['entity' => $paragraph]; - } - -} diff --git a/web/modules/contrib/paragraphs/src/Form/ParagraphsTypeDeleteConfirm.php b/web/modules/contrib/paragraphs/src/Form/ParagraphsTypeDeleteConfirm.php deleted file mode 100644 index f3749e2c4..000000000 --- a/web/modules/contrib/paragraphs/src/Form/ParagraphsTypeDeleteConfirm.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Form; - -use Drupal\Core\Entity\EntityDeleteForm; -use Drupal\Core\Form\FormStateInterface; - -/** - * Provides a form for Paragraphs type deletion. - */ -class ParagraphsTypeDeleteConfirm extends EntityDeleteForm { - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $num_paragraphs = $this->entityTypeManager->getStorage('paragraph')->getQuery() - ->condition('type', $this->entity->id()) - ->count() - ->execute(); - if ($num_paragraphs) { - $caption = '<p>' . $this->formatPlural($num_paragraphs, '%type Paragraphs type is used by 1 piece of content on your site. You can not remove this %type Paragraphs type until you have removed all from the content.', '%type Paragraphs type is used by @count pieces of content on your site. You may not remove %type Paragraphs type until you have removed all from the content.', ['%type' => $this->entity->label()]) . '</p>'; - $form['#title'] = $this->getQuestion(); - $form['description'] = ['#markup' => $caption]; - return $form; - } - - return parent::buildForm($form, $form_state); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Form/ParagraphsTypeForm.php b/web/modules/contrib/paragraphs/src/Form/ParagraphsTypeForm.php deleted file mode 100644 index 03d427d6f..000000000 --- a/web/modules/contrib/paragraphs/src/Form/ParagraphsTypeForm.php +++ /dev/null @@ -1,228 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Form; - -use Drupal\Core\Entity\EntityForm; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Form\SubformState; -use Drupal\field_ui\FieldUI; -use Drupal\paragraphs\ParagraphsBehaviorManager; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Form controller for paragraph type forms. - */ -class ParagraphsTypeForm extends EntityForm { - - /** - * The paragraphs behavior plugin manager service. - * - * @var \Drupal\paragraphs\ParagraphsBehaviorManager - */ - protected $paragraphsBehaviorManager; - - /** - * The entity being used by this form. - * - * @var \Drupal\paragraphs\ParagraphsTypeInterface - */ - protected $entity; - - /** - * GeneralSettingsForm constructor. - * - * @param \Drupal\paragraphs\ParagraphsBehaviorManager $paragraphs_behavior_manager - * The paragraphs type feature manager service. - */ - public function __construct(ParagraphsBehaviorManager $paragraphs_behavior_manager) { - $this->paragraphsBehaviorManager = $paragraphs_behavior_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('plugin.manager.paragraphs.behavior') - ); - } - - /** - * {@inheritdoc} - */ - public function form(array $form, FormStateInterface $form_state) { - $form = parent::form($form, $form_state); - - $paragraphs_type = $this->entity; - - if (!$paragraphs_type->isNew()) { - $form['#title'] = (t('Edit %title paragraph type', [ - '%title' => $paragraphs_type->label(), - ])); - } - - $form['label'] = array( - '#type' => 'textfield', - '#title' => $this->t('Label'), - '#maxlength' => 255, - '#default_value' => $paragraphs_type->label(), - '#description' => $this->t("Label for the Paragraphs type."), - '#required' => TRUE, - ); - - $form['id'] = array( - '#type' => 'machine_name', - '#default_value' => $paragraphs_type->id(), - '#machine_name' => array( - 'exists' => 'paragraphs_type_load', - ), - '#maxlength' => 32, - '#disabled' => !$paragraphs_type->isNew(), - ); - - $form['icon_file'] = [ - '#title' => $this->t('Paragraph type icon'), - '#type' => 'managed_file', - '#upload_location' => 'public://paragraphs_type_icon/', - '#upload_validators' => [ - 'file_validate_extensions' => ['png jpg svg'], - ], - ]; - - if ($file = $this->entity->getIconFile()) { - $form['icon_file']['#default_value'] = ['target_id' => $file->id()]; - } - - $form['description'] = [ - '#title' => t('Description'), - '#type' => 'textarea', - '#default_value' => $paragraphs_type->getDescription(), - '#description' => t('This text will be displayed on the <em>Add new paragraph</em> page.'), - ]; - - // Loop over the plugins that can be applied to this paragraph type. - if ($behavior_plugin_definitions = $this->paragraphsBehaviorManager->getApplicableDefinitions($paragraphs_type)) { - $form['message'] = [ - '#type' => 'container', - '#markup' => $this->t('Behavior plugins are only supported by the EXPERIMENTAL paragraphs widget.'), - '#attributes' => ['class' => ['messages', 'messages--warning']] - ]; - $form['behavior_plugins'] = [ - '#type' => 'details', - '#title' => $this->t('Behaviors'), - '#tree' => TRUE, - '#open' => TRUE - ]; - $config = $paragraphs_type->get('behavior_plugins'); - foreach ($behavior_plugin_definitions as $id => $behavior_plugin_definition) { - $description = $behavior_plugin_definition['description']; - $form['behavior_plugins'][$id]['enabled'] = [ - '#type' => 'checkbox', - '#title' => $behavior_plugin_definition['label'], - '#title_display' => 'after', - '#description' => $description, - '#default_value' => !empty($config[$id]['enabled']), - ]; - $form['behavior_plugins'][$id]['settings'] = []; - $subform_state = SubformState::createForSubform($form['behavior_plugins'][$id]['settings'], $form, $form_state); - $behavior_plugin = $paragraphs_type->getBehaviorPlugin($id); - if ($settings = $behavior_plugin->buildConfigurationForm($form['behavior_plugins'][$id]['settings'], $subform_state)) { - $form['behavior_plugins'][$id]['settings'] = $settings + [ - '#type' => 'fieldset', - '#title' => $behavior_plugin_definition['label'], - '#states' => [ - 'visible' => [ - ':input[name="behavior_plugins[' . $id . '][enabled]"]' => ['checked' => TRUE], - ] - ] - ]; - } - } - } - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - parent::validateForm($form, $form_state); - - $paragraphs_type = $this->entity; - - $icon_fild = $form_state->getValue(['icon_file', '0']); - // Set the file UUID to the paragraph configuration. - if (!empty($icon_fild) && $file = $this->entityTypeManager->getStorage('file')->load($icon_fild)) { - $paragraphs_type->set('icon_uuid', $file->uuid()); - } - else { - $paragraphs_type->set('icon_uuid', NULL); - } - - if ($behavior_plugin_definitions = $this->paragraphsBehaviorManager->getApplicableDefinitions($paragraphs_type)) { - foreach ($behavior_plugin_definitions as $id => $behavior_plugin_definition) { - // Only validate if the plugin is enabled and has settings. - if (isset($form['behavior_plugins'][$id]['settings']) && $form_state->getValue(['behavior_plugins', $id, 'enabled'])) { - $subform_state = SubformState::createForSubform($form['behavior_plugins'][$id]['settings'], $form, $form_state); - $behavior_plugin = $paragraphs_type->getBehaviorPlugin($id); - $behavior_plugin->validateConfigurationForm($form['behavior_plugins'][$id]['settings'], $subform_state); - } - } - } - } - - /** - * {@inheritdoc} - */ - public function save(array $form, FormStateInterface $form_state) { - $paragraphs_type = $this->entity; - - if ($behavior_plugin_definitions = $this->paragraphsBehaviorManager->getApplicableDefinitions($paragraphs_type)) { - foreach ($behavior_plugin_definitions as $id => $behavior_plugin_definition) { - $behavior_plugin = $paragraphs_type->getBehaviorPlugin($id); - - // If the behavior is enabled, initialize the configuration with the - // enabled key and then let it process the form input. - if ($form_state->getValue(['behavior_plugins', $id, 'enabled'])) { - $behavior_plugin->setConfiguration(['enabled' => TRUE]); - if (isset($form['behavior_plugins'][$id]['settings'])) { - $subform_state = SubformState::createForSubform($form['behavior_plugins'][$id]['settings'], $form, $form_state); - $behavior_plugin->submitConfigurationForm($form['behavior_plugins'][$id]['settings'], $subform_state); - } - } - else { - // The plugin is not enabled, reset to default configuration. - $behavior_plugin->setConfiguration([]); - } - } - } - - $status = $paragraphs_type->save(); - drupal_set_message($this->t('Saved the %label Paragraphs type.', array( - '%label' => $paragraphs_type->label(), - ))); - if (($status == SAVED_NEW && \Drupal::moduleHandler()->moduleExists('field_ui')) - && $route_info = FieldUI::getOverviewRouteInfo('paragraph', $paragraphs_type->id())) { - $form_state->setRedirectUrl($route_info); - } - else { - $form_state->setRedirect('entity.paragraphs_type.collection'); - } - } - - /** - * {@inheritdoc} - */ - protected function actions(array $form, FormStateInterface $form_state) { - $form = parent::actions($form, $form_state); - - // We want to display the button only on add page. - if ($this->entity->isNew() && \Drupal::moduleHandler()->moduleExists('field_ui')) { - $form['submit']['#value'] = $this->t('Save and manage fields'); - } - - return $form; - } - -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphAccessControlHandler.php b/web/modules/contrib/paragraphs/src/ParagraphAccessControlHandler.php deleted file mode 100644 index 9168590c4..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphAccessControlHandler.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Core\Entity\EntityAccessControlHandler; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Access\AccessResult; - -/** - * Access controller for the paragraphs entity. - * - * @see \Drupal\paragraphs\Entity\Paragraph. - */ -class ParagraphAccessControlHandler extends EntityAccessControlHandler { - - /** - * {@inheritdoc} - */ - protected function checkAccess(EntityInterface $paragraph, $operation, AccountInterface $account) { - // Allowed when the operation is not view or the status is true. - /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ - $access_result = AccessResult::allowedIf($operation != 'view' || $paragraph->status->value); - if ($paragraph->getParentEntity() != NULL) { - // Delete permission on the paragraph, should just depend on 'update' - // access permissions on the parent. - $operation = ($operation == 'delete') ? 'update' : $operation; - // Library items have no support for parent entity access checking. - if ($paragraph->getParentEntity()->getEntityTypeId() != 'paragraphs_library_item') { - $parent_access = $paragraph->getParentEntity()->access($operation, $account, TRUE); - $access_result = $access_result->andIf($parent_access); - } - } - return $access_result; - } - - /** - * {@inheritdoc} - */ - protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - // Allowed when nobody implements. - return AccessResult::allowed(); - } - -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphInterface.php b/web/modules/contrib/paragraphs/src/ParagraphInterface.php deleted file mode 100644 index ed4d64980..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphInterface.php +++ /dev/null @@ -1,113 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\user\EntityOwnerInterface; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\entity_reference_revisions\EntityNeedsSaveInterface; - -/** - * Provides an interface defining a paragraphs entity. - * @ingroup paragraphs - */ -interface ParagraphInterface extends ContentEntityInterface, EntityOwnerInterface, EntityNeedsSaveInterface { - - /** - * Gets the parent entity of the paragraph. - * - * Preserves language context with translated entities. - * - * @return ContentEntityInterface - * The parent entity. - */ - public function getParentEntity(); - - /** - * Returns short summary for paragraph. - * - * @param array $options - * (optional) Array of additional options, with the following elements: - * - 'show_behavior_summary': Whether the summary should contain the - * behavior settings. Defaults to TRUE to show behavior settings in the - * summary. - * - 'depth_limit': Depth limit of how many nested paragraph summaries are - * allowed. Defaults to 1 to show nested paragraphs only on top level. - * - * @return string - * The text without tags. - */ - public function getSummary(array $options = []); - - /** - * Returns a flag whether a current revision has been changed. - * - * The current instance is being compared with the latest saved revision. - * - * @return bool - * TRUE in case the current revision changed. Otherwise, FALSE. - * - * @see \Drupal\Core\Entity\ContentEntityBase::hasTranslationChanges() - */ - public function isChanged(); - - /** - * Returns the paragraph type / bundle name as string. - * - * @return string - * The Paragraph bundle name. - */ - public function getType(); - - /** - * Returns the paragraph type. - * - * @return ParagraphsTypeInterface - * The Paragraph Type. - */ - public function getParagraphType(); - - /** - * Gets all the behavior settings. - * - * @return array - * The array of behavior settings. - */ - public function getAllBehaviorSettings(); - - /** - * Gets the behavior setting of an specific plugin. - * - * @param string $plugin_id - * The plugin ID for which to get the settings. - * @param string|array $key - * Values are stored as a multi-dimensional associative array. If $key is a - * string, it will return $values[$key]. If $key is an array, each element - * of the array will be used as a nested key. If $key = array('foo', 'bar') - * it will return $values['foo']['bar']. - * @param mixed $default - * (optional) The default value if the specified key does not exist. - * - * @return mixed - * The value for the given key. - */ - public function &getBehaviorSetting($plugin_id, $key, $default = NULL); - - /** - * Sets all the behavior settings of a plugin. - * - * @param array $settings - * The behavior settings from the form. - */ - public function setAllBehaviorSettings(array $settings); - - /** - * Sets the behavior settings of a plugin. - * - * @param string $plugin_id - * The plugin ID for which to set the settings. - * @param array $settings - * The behavior settings from the form. - */ - public function setBehaviorSettings($plugin_id, array $settings); - -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphStorageSchema.php b/web/modules/contrib/paragraphs/src/ParagraphStorageSchema.php deleted file mode 100644 index d79c4ce03..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphStorageSchema.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema; -use Drupal\Core\Field\FieldStorageDefinitionInterface; - -/** - * Extends the paragraphs schema handler. - */ -class ParagraphStorageSchema extends SqlContentEntityStorageSchema { - - /** - * {@inheritdoc} - */ - protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { - $schema = parent::getEntitySchema($entity_type, $reset); - - $schema['paragraphs_item_field_data']['indexes'] += array( - 'paragraphs__parent_fields' => array('parent_type', 'parent_id', 'parent_field_name'), - ); - - return $schema; - } - - /** - * {@inheritdoc} - */ - protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) { - // Setting the initial value to 1 when we add a 'status' field. - // @todo this is a workaround for https://www.drupal.org/node/2346019 - $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping); - if ($storage_definition->getName() == 'status') { - $schema['fields']['status']['initial'] = 1; - } - - if ($storage_definition->getName() == 'behavior_settings') { - $schema['fields']['behavior_settings']['initial'] = serialize([]); - } - return $schema; - } -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphViewBuilder.php b/web/modules/contrib/paragraphs/src/ParagraphViewBuilder.php deleted file mode 100644 index 1ebc521bd..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphViewBuilder.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Core\Entity\Entity\EntityViewDisplay; -use Drupal\Core\Entity\EntityViewBuilder; -use Drupal\Core\Render\Element; - -/** - * Render controller for paragraphs. - */ -class ParagraphViewBuilder extends EntityViewBuilder { - - /** - * {@inheritdoc} - */ - public function buildMultiple(array $build_list) { - $build_list = parent::buildMultiple($build_list); - - // Allow enabled behavior plugin to alter the rendering. - foreach (Element::children($build_list) as $key) { - $build = $build_list[$key]; - $display = EntityViewDisplay::load('paragraph.' . $build['#paragraph']->bundle() . '.' . $build['#view_mode']) ?: EntityViewDisplay::load('paragraph.' . $build['#paragraph']->bundle() . '.default'); - $paragraph_type = $build['#paragraph']->getParagraphType(); - - // In case we use paragraphs type with no fields the EntityViewDisplay - // might not be available yet. - if (!$display) { - $display = EntityViewDisplay::create([ - 'targetEntityType' => 'paragraph', - 'bundle' => $build['#paragraph']->bundle(), - 'mode' => 'default', - 'status' => TRUE, - ]); - } - - foreach ($paragraph_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_value) { - $plugin_value->view($build_list[$key], $build['#paragraph'], $display, $build['#view_mode']); - } - } - return $build_list; - } - -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphsBehaviorBase.php b/web/modules/contrib/paragraphs/src/ParagraphsBehaviorBase.php deleted file mode 100644 index a450bfa61..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphsBehaviorBase.php +++ /dev/null @@ -1,189 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Component\Plugin\PluginBase; -use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Entity\EntityFieldManager; -use Drupal\Core\Field\FieldConfigInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\Entity\ParagraphsType; -use Symfony\Component\DependencyInjection\ContainerInterface; - -abstract class ParagraphsBehaviorBase extends PluginBase implements ParagraphsBehaviorInterface, ContainerFactoryPluginInterface { - - use StringTranslationTrait; - - /** - * The entity field manager. - * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface - */ - protected $entityFieldManager; - - /** - * Constructs a ParagraphsBehaviorBase object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Entity\EntityFieldManager $entity_field_manager - * The entity field manager. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityFieldManager $entity_field_manager) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->configuration += $this->defaultConfiguration(); - $this->entityFieldManager = $entity_field_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static($configuration, $plugin_id, $plugin_definition, - $container->get('entity_field.manager') - ); - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return []; - } - - /** - * {@inheritdoc} - */ - public function getConfiguration() { - return $this->configuration; - } - - /** - * {@inheritdoc} - */ - public function setConfiguration(array $configuration) { - $this->configuration = $configuration + $this->defaultConfiguration(); - } - - /** - * {@inheritdoc} - */ - public function calculateDependencies() { - return []; - } - - /** - * {@inheritdoc} - */ - public function preprocess(&$variables) { } - - /** - * {@inheritdoc} - */ - public static function isApplicable(ParagraphsType $paragraphs_type) { - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function settingsSummary(Paragraph $paragraph) { - return []; - } - - /** - * {@inheritdoc} - */ - public function buildBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state) { - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state) {} - - /** - * {@inheritdoc} - */ - public function submitBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state) { - $filtered_values = $this->filterBehaviorFormSubmitValues($paragraph, $form, $form_state); - - $paragraph->setBehaviorSettings($this->getPluginId(), $filtered_values); - } - - /** - * Removes default behavior form values before storing the user-set ones. - * - * Default implementation considers a value to be default if and only if it is - * an empty value. Behavior plugins that do not consider all empty values to - * be default should override this method or - * \Drupal\paragraphs\ParagraphsBehaviorBase::submitBehaviorForm. - * - * @param \Drupal\paragraphs\ParagraphInterface $paragraph - * The paragraph. - * @param array $form - * An associative array containing the initial structure of the plugin form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * An associative array of values submitted to the form with all empty - * leaves removed. Subarrays that only contain empty leaves are also - * removed. - */ - protected function filterBehaviorFormSubmitValues(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state) { - // Keeps removing empty leaves, until there are none left. So if a subarray - // only contains empty leaves, that subarray itself will be removed. - $new_array = $form_state->getValues(); - - do { - $old_array = $new_array; - $new_array = NestedArray::filter($old_array); - } - while ($new_array !== $old_array); - - return $new_array; - } - - /** - * {@inheritdoc} - */ - public function getFieldNameOptions(ParagraphsType $paragraphs_type, $field_type = NULL) { - $fields = []; - $field_definitions = $this->entityFieldManager->getFieldDefinitions('paragraph', $paragraphs_type->id()); - foreach ($field_definitions as $name => $definition) { - if ($field_definitions[$name] instanceof FieldConfigInterface) { - if (empty($field_type) || $definition->getType() == $field_type) { - $fields[$name] = $definition->getLabel(); - } - } - } - return $fields; - } - -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphsBehaviorCollection.php b/web/modules/contrib/paragraphs/src/ParagraphsBehaviorCollection.php deleted file mode 100644 index 6f3e0d118..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphsBehaviorCollection.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Core\Plugin\DefaultLazyPluginCollection; - -/** - * A collection of paragraphs behavior plugins. - */ -class ParagraphsBehaviorCollection extends DefaultLazyPluginCollection { - - /** - * All behavior plugin definitions. - * - * @var array - */ - protected $definitions; - - /** - * {@inheritdoc} - * - * @return \Drupal\paragraphs\ParagraphsBehaviorInterface - */ - public function &get($instance_id) { - return parent::get($instance_id); - } - - /** - * Retrieves all enabled behavior plugins. - */ - public function getEnabled() { - $this->getAll(); - $enabled = []; - foreach ($this->getConfiguration() as $key => $value) { - if (isset($value['enabled']) && $value['enabled'] == TRUE) { - $enabled[$key] = $this->get($key); - } - } - return $enabled; - } - - /** - * Retrieves all behavior plugins definitions and creates an instance for each - * one. - */ - public function getAll() { - // Retrieve all available behavior plugin definitions. - if (!$this->definitions) { - $this->definitions = $this->manager->getDefinitions(); - } - // Ensure that there is an instance of all available behavior plugins. - // Note that getDefinitions() are keyed by $plugin_id. $instance_id is the - // $plugin_id for behavior plugins, since a single behavior plugin can only - // exist once in a paragraphs type. - foreach ($this->definitions as $plugin_id => $definition) { - if (!isset($this->pluginInstances[$plugin_id])) { - $this->initializePlugin($plugin_id); - } - } - return $this->pluginInstances; - } - - /** - * {@inheritdoc} - */ - protected function initializePlugin($instance_id) { - $configuration = isset($this->configurations[$instance_id]) ? $this->configurations[$instance_id] : []; - $this->set($instance_id, $this->manager->createInstance($instance_id, $configuration)); - } - -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphsBehaviorInterface.php b/web/modules/contrib/paragraphs/src/ParagraphsBehaviorInterface.php deleted file mode 100644 index 1b07e34c7..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphsBehaviorInterface.php +++ /dev/null @@ -1,140 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\PluginFormInterface; -use Drupal\Component\Plugin\ConfigurablePluginInterface; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\Entity\ParagraphsType; - -/** - * Provides an interface defining a paragraph behavior. - * - * A paragraph behavior plugin adds extra functionality to the paragraph such as - * adding properties and attributes, it can also add extra classes to the render - * elements so extra styling can be applied. - */ -interface ParagraphsBehaviorInterface extends PluginFormInterface, ConfigurablePluginInterface { - - /** - * Builds a behavior perspective for each paragraph based on its type. - * - * This method is responsible for building the behavior form for each - * Paragraph so the user can set special attributes and properties. - * - * @param \Drupal\paragraphs\ParagraphInterface $paragraph - * The paragraph. - * @param array $form - * An associative array containing the initial structure of the plugin form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The fields build array that the plugin creates. - */ - public function buildBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state); - - /** - * Validates the behavior fields form. - * - * This method is responsible for validating the data in the behavior fields - * form and displaying validation messages. - * - * @param \Drupal\paragraphs\ParagraphInterface $paragraph - * The paragraph. - * @param array $form - * An associative array containing the initial structure of the plugin form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function validateBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state); - - /** - * Submit the values taken from the form to store the values. - * - * This method is responsible for submitting the data and saving it in the - * paragraphs entity. - * - * @param \Drupal\paragraphs\ParagraphInterface $paragraph - * The paragraph. - * @param array $form - * An associative array containing the initial structure of the plugin form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function submitBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state); - - /** - * Adds a default set of helper variables for preprocessors and templates. - * - * This preprocess function is the first in the sequence of preprocessing - * functions that are called when preparing variables of a paragraph. - * - * @param array $variables - * An associative array containing: - * - elements: An array of elements to display in view mode. - * - paragraph: The paragraph object. - * - view_mode: The view mode. - */ - public function preprocess(&$variables); - - /** - * Extends the paragraph render array with behavior. - * - * @param array &$build - * A renderable array representing the paragraph. The module may add - * elements to $build prior to rendering. The structure of $build is a - * renderable array as expected by drupal_render(). - * @param \Drupal\paragraphs\Entity\Paragraph $paragraph - * The paragraph. - * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display - * The entity view display holding the display options configured for the - * entity components. - * @param string $view_mode - * The view mode the entity is rendered in. - * - * @return array - * A render array provided by the plugin. - */ - public function view(array &$build, Paragraph $paragraph, EntityViewDisplayInterface $display, $view_mode); - - /** - * Returns if the plugin can be used for the provided Paragraphs type. - * - * @param \Drupal\paragraphs\Entity\ParagraphsType $paragraphs_type - * The Paragraphs type entity that should be checked. - * - * @return bool - * TRUE if the formatter can be used, FALSE otherwise. - */ - public static function isApplicable(ParagraphsType $paragraphs_type); - - /** - * Returns a short summary for the current behavior settings. - * - * @param \Drupal\paragraphs\Entity\Paragraph $paragraph - * The paragraph. - * - * @return string[] - * The plugin settings. - */ - public function settingsSummary(Paragraph $paragraph); - - /** - * Returns list of field names for the given paragraph type and field type. - * - * - * @param \Drupal\paragraphs\Entity\ParagraphsType $paragraphs_type - * The paragraphs type entity. - * @param string $field_type - * (optional) Field type to check for existence. If field type is not - * provided, returns all entity fields. - * - * @return string[] - * The list of field labels keyed by their field name. - */ - public function getFieldNameOptions(ParagraphsType $paragraphs_type, $field_type = NULL); - -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphsBehaviorManager.php b/web/modules/contrib/paragraphs/src/ParagraphsBehaviorManager.php deleted file mode 100644 index 080cc11ce..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphsBehaviorManager.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Plugin\DefaultPluginManager; - -/** - * Plugin type manager for paragraphs type behavior plugins. - * - * @ingroup paragraphs_behavior - */ -class ParagraphsBehaviorManager extends DefaultPluginManager { - - /** - * Constructs a ParagraphsBehaviorManager object. - * - * @param \Traversable $namespaces - * An object that implements \Traversable which contains the root paths - * keyed by the corresponding namespace to look for plugin implementations. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend - * Cache backend instance to use. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - */ - public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { - parent::__construct('Plugin/paragraphs/Behavior', $namespaces, $module_handler, 'Drupal\paragraphs\ParagraphsBehaviorInterface', 'Drupal\paragraphs\Annotation\ParagraphsBehavior'); - $this->setCacheBackend($cache_backend, 'paragraphs_behavior_plugins'); - $this->alterInfo('paragraphs_behavior_info'); - } - - /** - * {@inheritdoc} - */ - public function getDefinitions() { - $definitions = parent::getDefinitions(); - uasort($definitions, 'Drupal\Component\Utility\SortArray::sortByWeightElement'); - return $definitions; - } - - /** - * Gets the applicable behavior plugins. - * - * Loop over the plugin definitions, check the applicability of each one of - * them and return the array of the applicable plugins. - * - * @return array - * The applicable behavior plugins. - */ - public function getApplicableDefinitions($paragraphs_type) { - $definitions = $this->getDefinitions(); - $applicable_plugins = []; - foreach ($definitions as $key => $definition) { - if ($definition['class']::isApplicable($paragraphs_type)) { - $applicable_plugins[$key] = $definition; - } - } - return $applicable_plugins; - } - -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphsServiceProvider.php b/web/modules/contrib/paragraphs/src/ParagraphsServiceProvider.php deleted file mode 100644 index b8ce007a6..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphsServiceProvider.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\Core\DependencyInjection\ServiceProviderBase; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Reference; - -/** - * Service Provider for Paragraphs. - */ -class ParagraphsServiceProvider extends ServiceProviderBase { - - /** - * {@inheritdoc} - */ - public function alter(ContainerBuilder $container) { - $modules = $container->getParameter('container.modules'); - // Check for installed Replicate module. - if (isset($modules['replicate']) ) { - // Add a Replicate field event subscriber. - $service_definition = new Definition( - 'Drupal\paragraphs\EventSubscriber\ReplicateFieldSubscriber', - [new Reference('replicate.replicator')] - ); - $service_definition->addTag('event_subscriber'); - $container->setDefinition('replicate.event_subscriber.paragraphs', $service_definition); - } - } -} diff --git a/web/modules/contrib/paragraphs/src/ParagraphsTypeInterface.php b/web/modules/contrib/paragraphs/src/ParagraphsTypeInterface.php deleted file mode 100644 index 9eb889ea3..000000000 --- a/web/modules/contrib/paragraphs/src/ParagraphsTypeInterface.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php - -namespace Drupal\paragraphs; - -use Drupal\Core\Config\Entity\ConfigEntityInterface; - -/** - * Provides an interface defining a ParagraphsType entity. - */ -interface ParagraphsTypeInterface extends ConfigEntityInterface { - - /** - * Returns the ordered collection of feature plugin instances. - * - * @return \Drupal\paragraphs\ParagraphsBehaviorCollection - * The behavior plugins collection. - */ - public function getBehaviorPlugins(); - - /** - * Returns an individual plugin instance. - * - * @param string $instance_id - * The ID of a behavior plugin instance to return. - * - * @return \Drupal\paragraphs\ParagraphsBehaviorInterface - * A specific feature plugin instance. - */ - public function getBehaviorPlugin($instance_id); - - /** - * Retrieves all the enabled plugins. - * - * @return \Drupal\paragraphs\ParagraphsBehaviorInterface[] - * Array of the enabled plugins as instances. - */ - public function getEnabledBehaviorPlugins(); - - /** - * Returns the icon file entity. - * - * @return \Drupal\file\FileInterface|bool - * The icon's file entity or FALSE if icon does not exist. - */ - public function getIconFile(); - - /** - * Returns the icon's URL. - * - * @return string|bool - * The icon's URL or FALSE if icon does not exits. - */ - public function getIconUrl(); - - /** - * Gets the description. - * - * @return string - * The description of this paragraph type. - */ - public function getDescription(); - - /** - * Returns TRUE if $plugin_id is enabled on this ParagraphType Entity. - * - * @param string $plugin_id - * The plugin id, as specified in the plugin annotation details. - * - * @return bool - * TRUE if the plugin is enabled, FALSE otherwise. - */ - public function hasEnabledBehaviorPlugin($plugin_id); - -} diff --git a/web/modules/contrib/paragraphs/src/Plugin/EntityReferenceSelection/ParagraphSelection.php b/web/modules/contrib/paragraphs/src/Plugin/EntityReferenceSelection/ParagraphSelection.php deleted file mode 100644 index 3d8e4e27d..000000000 --- a/web/modules/contrib/paragraphs/src/Plugin/EntityReferenceSelection/ParagraphSelection.php +++ /dev/null @@ -1,342 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Plugin\EntityReferenceSelection; - -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityTypeBundleInfoInterface; -use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Url; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Component\Utility\NestedArray; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Default plugin implementation of the Entity Reference Selection plugin. - * - * @EntityReferenceSelection( - * id = "default:paragraph", - * label = @Translation("Paragraphs"), - * group = "default", - * entity_types = {"paragraph"}, - * weight = 0 - * ) - */ -class ParagraphSelection extends DefaultSelection { - - /** - * Entity type bundle info service. - * - * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface - */ - public $entityTypeBundleInfo; - - /** - * ParagraphSelection constructor. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager - * The entity manager service. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler service. - * @param \Drupal\Core\Session\AccountInterface $current_user - * The current user. - * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info - * Entity type bundle info service. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, EntityTypeBundleInfoInterface $entity_type_bundle_info) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_manager, $module_handler, $current_user); - - $this->entityTypeBundleInfo = $entity_type_bundle_info; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity.manager'), - $container->get('module_handler'), - $container->get('current_user'), - $container->get('entity_type.bundle.info') - ); - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $entity_type_id = $this->configuration['target_type']; - $selection_handler_settings = $this->configuration['handler_settings'] ?: array(); - $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); - - // Merge-in default values. - $selection_handler_settings += array( - 'target_bundles' => array(), - 'negate' => 0, - 'target_bundles_drag_drop' => array(), - ); - - $bundle_options = array(); - $bundle_options_simple = array(); - - // Default weight for new items. - $weight = count($bundles) + 1; - - foreach ($bundles as $bundle_name => $bundle_info) { - $bundle_options_simple[$bundle_name] = $bundle_info['label']; - $bundle_options[$bundle_name] = array( - 'label' => $bundle_info['label'], - 'enabled' => isset($selection_handler_settings['target_bundles_drag_drop'][$bundle_name]['enabled']) ? $selection_handler_settings['target_bundles_drag_drop'][$bundle_name]['enabled'] : FALSE, - 'weight' => isset($selection_handler_settings['target_bundles_drag_drop'][$bundle_name]['weight']) ? $selection_handler_settings['target_bundles_drag_drop'][$bundle_name]['weight'] : $weight, - ); - $weight++; - } - - // Do negate the selection. - $form['negate'] = [ - '#type' => 'radios', - '#options' => [ - 1 => $this->t('Exclude the selected below'), - 0 => $this->t('Include the selected below'), - ], - '#title' => $this->t('Which Paragraph types should be allowed?'), - '#default_value' => isset($selection_handler_settings['negate']) ? $selection_handler_settings['negate'] : 0, - ]; - - // Kept for compatibility with other entity reference widgets. - $form['target_bundles'] = array( - '#type' => 'checkboxes', - '#options' => $bundle_options_simple, - '#default_value' => isset($selection_handler_settings['target_bundles']) ? $selection_handler_settings['target_bundles'] : array(), - '#access' => FALSE, - ); - - if ($bundle_options) { - $form['target_bundles_drag_drop'] = [ - '#element_validate' => [[__CLASS__, 'targetTypeValidate']], - '#type' => 'table', - '#header' => [ - $this->t('Type'), - $this->t('Weight'), - ], - '#attributes' => [ - 'id' => 'bundles', - ], - '#prefix' => '<h5>' . $this->t('Paragraph types') . '</h5>', - '#suffix' => '<div class="description">' . $this->t('Selection of Paragraph types for this field. Select none to allow all Paragraph types.') . '</div>', - ]; - - $form['target_bundles_drag_drop']['#tabledrag'][] = [ - 'action' => 'order', - 'relationship' => 'sibling', - 'group' => 'bundle-weight', - ]; - } - - uasort($bundle_options, 'Drupal\Component\Utility\SortArray::sortByWeightElement'); - - $weight_delta = $weight; - - // Default weight for new items. - $weight = count($bundles) + 1; - foreach ($bundle_options as $bundle_name => $bundle_info) { - $form['target_bundles_drag_drop'][$bundle_name] = array( - '#attributes' => array( - 'class' => array('draggable'), - ), - ); - - $form['target_bundles_drag_drop'][$bundle_name]['enabled'] = array( - '#type' => 'checkbox', - '#title' => $bundle_info['label'], - '#title_display' => 'after', - '#default_value' => $bundle_info['enabled'], - ); - - $form['target_bundles_drag_drop'][$bundle_name]['weight'] = array( - '#type' => 'weight', - '#default_value' => (int) $bundle_info['weight'], - '#delta' => $weight_delta, - '#title' => $this->t('Weight for type @type', array('@type' => $bundle_info['label'])), - '#title_display' => 'invisible', - '#attributes' => array( - 'class' => array('bundle-weight', 'bundle-weight-' . $bundle_name), - ), - ); - $weight++; - } - - if (!count($bundle_options)) { - $form['allowed_bundles_explain'] = [ - '#type' => 'markup', - '#markup' => $this->t('You did not add any Paragraph types yet, click <a href=":here">here</a> to add one.', [':here' => Url::fromRoute('paragraphs.type_add')->toString()]), - ]; - } - - return $form; - } - - /** - * Validate helper to have support for other entity reference widgets. - * - * @param $element - * @param FormStateInterface $form_state - * @param $form - */ - public static function targetTypeValidate($element, FormStateInterface $form_state, $form) { - $values = &$form_state->getValues(); - $element_values = NestedArray::getValue($values, $element['#parents']); - $bundle_options = array(); - - if ($element_values) { - $enabled = 0; - foreach ($element_values as $machine_name => $bundle_info) { - if (isset($bundle_info['enabled']) && $bundle_info['enabled']) { - $bundle_options[$machine_name] = $machine_name; - $enabled++; - } - } - - // All disabled = all enabled. - if ($enabled === 0) { - $bundle_options = NULL; - } - } - - // New value parents. - $parents = array_merge(array_slice($element['#parents'], 0, -1), array('target_bundles')); - NestedArray::setValue($values, $parents, $bundle_options); - } - - /** - * Returns the sorted allowed types for the field. - * - * @return array - * A list of arrays keyed by the paragraph type machine name - * with the following properties. - * - label: The label of the paragraph type. - * - weight: The weight of the paragraph type. - */ - public function getSortedAllowedTypes() { - $return_bundles = []; - - $bundles = $this->entityTypeBundleInfo->getBundleInfo('paragraph'); - if (!empty($this->configuration['handler_settings']['target_bundles'])) { - if (isset($this->configuration['handler_settings']['negate']) && $this->configuration['handler_settings']['negate'] == '1') { - $bundles = array_diff_key($bundles, $this->configuration['handler_settings']['target_bundles']); - } - else { - $bundles = array_intersect_key($bundles, $this->configuration['handler_settings']['target_bundles']); - } - } - - // Support for the paragraphs reference type. - if (!empty($this->configuration['handler_settings']['target_bundles_drag_drop'])) { - $drag_drop_settings = $this->configuration['handler_settings']['target_bundles_drag_drop']; - $max_weight = count($bundles); - - foreach ($drag_drop_settings as $bundle_info) { - if (isset($bundle_info['weight']) && $bundle_info['weight'] && $bundle_info['weight'] > $max_weight) { - $max_weight = $bundle_info['weight']; - } - } - - // Default weight for new items. - $weight = $max_weight + 1; - foreach ($bundles as $machine_name => $bundle) { - $return_bundles[$machine_name] = [ - 'label' => $bundle['label'], - 'weight' => isset($drag_drop_settings[$machine_name]['weight']) ? $drag_drop_settings[$machine_name]['weight'] : $weight, - ]; - $weight++; - } - } - else { - $weight = 0; - - foreach ($bundles as $machine_name => $bundle) { - $return_bundles[$machine_name] = [ - 'label' => $bundle['label'], - 'weight' => $weight, - ]; - - $weight++; - } - } - uasort($return_bundles, 'Drupal\Component\Utility\SortArray::sortByWeightElement'); - - return $return_bundles; - } - - /** - * {@inheritdoc} - */ - public function validateReferenceableNewEntities(array $entities) { - $bundles = array_keys($this->getSortedAllowedTypes()); - return array_filter($entities, function ($entity) { - if (isset($bundles)) { - return in_array($entity->bundle(), $bundles); - } - return TRUE; - }); - } - - /** - * {@inheritdoc} - */ - protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { - $target_type = $this->configuration['target_type']; - $handler_settings = $this->configuration['handler_settings']; - $entity_type = $this->entityManager->getDefinition($target_type); - - $query = $this->entityManager->getStorage($target_type)->getQuery(); - - // If 'target_bundles' is NULL, all bundles are referenceable, no further - // conditions are needed. - if (isset($handler_settings['target_bundles']) && is_array($handler_settings['target_bundles'])) { - $target_bundles = array_keys($this->getSortedAllowedTypes()); - - // If 'target_bundles' is an empty array, no bundle is referenceable, - // force the query to never return anything and bail out early. - if ($target_bundles === []) { - $query->condition($entity_type->getKey('id'), NULL, '='); - return $query; - } - else { - $query->condition($entity_type->getKey('bundle'), $target_bundles, 'IN'); - } - } - - if (isset($match) && $label_key = $entity_type->getKey('label')) { - $query->condition($label_key, $match, $match_operator); - } - - // Add entity-access tag. - $query->addTag($target_type . '_access'); - - // Add the Selection handler for system_query_entity_reference_alter(). - $query->addTag('entity_reference'); - $query->addMetaData('entity_reference_selection_handler', $this); - - // Add the sort option. - if (!empty($handler_settings['sort'])) { - $sort_settings = $handler_settings['sort']; - if ($sort_settings['field'] != '_none') { - $query->sort($sort_settings['field'], $sort_settings['direction']); - } - } - - return $query; - } - -} diff --git a/web/modules/contrib/paragraphs/src/Plugin/Field/FieldFormatter/ParagraphsSummaryFormatter.php b/web/modules/contrib/paragraphs/src/Plugin/Field/FieldFormatter/ParagraphsSummaryFormatter.php deleted file mode 100644 index 2416f02ef..000000000 --- a/web/modules/contrib/paragraphs/src/Plugin/Field/FieldFormatter/ParagraphsSummaryFormatter.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Plugin\Field\FieldFormatter; - -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldItemListInterface; -use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\ParagraphInterface; - -/** - * Plugin implementation of the 'paragraph_summary' formatter. - * - * @FieldFormatter( - * id = "paragraph_summary", - * label = @Translation("Paragraph summary"), - * field_types = { - * "entity_reference_revisions" - * } - * ) - */ -class ParagraphsSummaryFormatter extends EntityReferenceFormatterBase { - - /** - * {@inheritdoc} - */ - public function viewElements(FieldItemListInterface $items, $langcode) { - $elements = []; - foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) { - if ($entity->id()) { - $elements[$delta] = [ - '#markup' => $entity->getSummary(), - ]; - } - } - - return $elements; - } - - /** - * {@inheritdoc} - */ - public static function isApplicable(FieldDefinitionInterface $field_definition) { - $target_type = $field_definition->getSetting('target_type'); - $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type); - if ($paragraph_type) { - return $paragraph_type->isSubclassOf(ParagraphInterface::class); - } - - return FALSE; - } -} diff --git a/web/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php b/web/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php deleted file mode 100644 index 36f681b77..000000000 --- a/web/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php +++ /dev/null @@ -1,1465 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Plugin\Field\FieldWidget; - -use Drupal\Component\Utility\NestedArray; -use Drupal\Component\Utility\Html; -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; -use Drupal\Core\Entity\RevisionableInterface; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldFilteredMarkup; -use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\WidgetBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Field\FieldItemListInterface; -use Drupal\Core\Render\Element; -use Drupal\node\Entity\Node; -use Drupal\paragraphs; -use Drupal\paragraphs\ParagraphInterface; -use Symfony\Component\Validator\ConstraintViolationInterface; -use Drupal\paragraphs\Plugin\EntityReferenceSelection\ParagraphSelection; - - -/** - * Plugin implementation of the 'entity_reference paragraphs' widget. - * - * We hide add / remove buttons when translating to avoid accidental loss of - * data because these actions effect all languages. - * - * @FieldWidget( - * id = "entity_reference_paragraphs", - * label = @Translation("Paragraphs Classic"), - * description = @Translation("A paragraphs inline form widget."), - * field_types = { - * "entity_reference_revisions" - * } - * ) - */ -class InlineParagraphsWidget extends WidgetBase { - - /** - * Indicates whether the current widget instance is in translation. - * - * @var bool - */ - private $isTranslating; - - /** - * Id to name ajax buttons that includes field parents and field name. - * - * @var string - */ - protected $fieldIdPrefix; - - /** - * Wrapper id to identify the paragraphs. - * - * @var string - */ - protected $fieldWrapperId; - - /** - * Number of paragraphs item on form. - * - * @var int - */ - protected $realItemCount; - - /** - * Parents for the current paragraph. - * - * @var array - */ - protected $fieldParents; - - /** - * Accessible paragraphs types. - * - * @var array - */ - protected $accessOptions = NULL; - - /** - * {@inheritdoc} - */ - public static function defaultSettings() { - return array( - 'title' => t('Paragraph'), - 'title_plural' => t('Paragraphs'), - 'edit_mode' => 'open', - 'add_mode' => 'dropdown', - 'form_display_mode' => 'default', - 'default_paragraph_type' => '', - ); - } - - /** - * {@inheritdoc} - */ - public function settingsForm(array $form, FormStateInterface $form_state) { - $elements = array(); - - $elements['title'] = array( - '#type' => 'textfield', - '#title' => $this->t('Paragraph Title'), - '#description' => $this->t('Label to appear as title on the button as "Add new [title]", this label is translatable'), - '#default_value' => $this->getSetting('title'), - '#required' => TRUE, - ); - - $elements['title_plural'] = array( - '#type' => 'textfield', - '#title' => $this->t('Plural Paragraph Title'), - '#description' => $this->t('Title in its plural form.'), - '#default_value' => $this->getSetting('title_plural'), - '#required' => TRUE, - ); - - $elements['edit_mode'] = array( - '#type' => 'select', - '#title' => $this->t('Edit mode'), - '#description' => $this->t('The mode the paragraph is in by default. Preview will render the paragraph in the preview view mode.'), - '#options' => array( - 'open' => $this->t('Open'), - 'closed' => $this->t('Closed'), - 'preview' => $this->t('Preview'), - ), - '#default_value' => $this->getSetting('edit_mode'), - '#required' => TRUE, - ); - - $elements['add_mode'] = array( - '#type' => 'select', - '#title' => $this->t('Add mode'), - '#description' => $this->t('The way to add new Paragraphs.'), - '#options' => array( - 'select' => $this->t('Select list'), - 'button' => $this->t('Buttons'), - 'dropdown' => $this->t('Dropdown button') - ), - '#default_value' => $this->getSetting('add_mode'), - '#required' => TRUE, - ); - - $elements['form_display_mode'] = array( - '#type' => 'select', - '#options' => \Drupal::service('entity_display.repository')->getFormModeOptions($this->getFieldSetting('target_type')), - '#description' => $this->t('The form display mode to use when rendering the paragraph form.'), - '#title' => $this->t('Form display mode'), - '#default_value' => $this->getSetting('form_display_mode'), - '#required' => TRUE, - ); - - $options = []; - foreach ($this->getAllowedTypes() as $key => $bundle) { - $options[$key] = $bundle['label']; - } - - $elements['default_paragraph_type'] = [ - '#type' => 'select', - '#title' => $this->t('Default paragraph type'), - '#empty_value' => '_none', - '#default_value' => $this->getDefaultParagraphTypeMachineName(), - '#options' => $options, - '#description' => $this->t('When creating a new host entity, a paragraph of this type is added.'), - ]; - - return $elements; - } - - /** - * {@inheritdoc} - */ - public function settingsSummary() { - $summary = array(); - $summary[] = $this->t('Title: @title', ['@title' => $this->getSetting('title')]); - $summary[] = $this->t('Plural title: @title_plural', [ - '@title_plural' => $this->getSetting('title_plural') - ]); - - switch($this->getSetting('edit_mode')) { - case 'open': - default: - $edit_mode = $this->t('Open'); - break; - case 'closed': - $edit_mode = $this->t('Closed'); - break; - case 'preview': - $edit_mode = $this->t('Preview'); - break; - } - - switch($this->getSetting('add_mode')) { - case 'select': - default: - $add_mode = $this->t('Select list'); - break; - case 'button': - $add_mode = $this->t('Buttons'); - break; - case 'dropdown': - $add_mode = $this->t('Dropdown button'); - break; - } - - $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]); - $summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]); - $summary[] = $this->t('Form display mode: @form_display_mode', [ - '@form_display_mode' => $this->getSetting('form_display_mode') - ]); - if ($this->getDefaultParagraphTypeLabelName() !== NULL) { - $summary[] = $this->t('Default paragraph type: @default_paragraph_type', [ - '@default_paragraph_type' => $this->getDefaultParagraphTypeLabelName() - ]); - } - - return $summary; - } - - /** - * {@inheritdoc} - * - * @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation() - * Uses a similar approach to populate a new translation. - */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $field_name = $this->fieldDefinition->getName(); - $parents = $element['#field_parents']; - $info = []; - - $paragraphs_entity = NULL; - $host = $items->getEntity(); - $widget_state = static::getWidgetState($parents, $field_name, $form_state); - - $entity_manager = \Drupal::entityTypeManager(); - $target_type = $this->getFieldSetting('target_type'); - - $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit'; - $default_edit_mode = $this->getSetting('edit_mode'); - - $show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']); - - if (isset($widget_state['paragraphs'][$delta]['entity'])) { - $paragraphs_entity = $widget_state['paragraphs'][$delta]['entity']; - } - elseif (isset($items[$delta]->entity)) { - $paragraphs_entity = $items[$delta]->entity; - - // We don't have a widget state yet, get from selector settings. - if (!isset($widget_state['paragraphs'][$delta]['mode'])) { - - if ($default_edit_mode == 'open') { - $item_mode = 'edit'; - } - elseif ($default_edit_mode == 'closed') { - $item_mode = 'closed'; - } - elseif ($default_edit_mode == 'preview') { - $item_mode = 'preview'; - } - } - } - elseif (isset($widget_state['selected_bundle'])) { - - $entity_type = $entity_manager->getDefinition($target_type); - $bundle_key = $entity_type->getKey('bundle'); - - $paragraphs_entity = $entity_manager->getStorage($target_type)->create(array( - $bundle_key => $widget_state['selected_bundle'], - )); - - $item_mode = 'edit'; - } - - if ($item_mode == 'collapsed') { - $item_mode = $default_edit_mode; - } - - if ($item_mode == 'closed') { - // Validate closed paragraphs and expand if needed. - // @todo Consider recursion. - $violations = $paragraphs_entity->validate(); - $violations->filterByFieldAccess(); - if (count($violations) > 0) { - $item_mode = 'edit'; - $messages = []; - foreach ($violations as $violation) { - $messages[] = $violation->getMessage(); - } - $info['validation_error'] = array( - '#type' => 'container', - '#markup' => $this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]), - '#attributes' => ['class' => ['messages', 'messages--warning']], - ); - } - } - - if ($paragraphs_entity) { - // Detect if we are translating. - $this->initIsTranslating($form_state, $host); - $langcode = $form_state->get('langcode'); - - if (!$this->isTranslating) { - // Set the langcode if we are not translating. - $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode'); - if ($paragraphs_entity->get($langcode_key)->value != $langcode) { - // If a translation in the given language already exists, switch to - // that. If there is none yet, update the language. - if ($paragraphs_entity->hasTranslation($langcode)) { - $paragraphs_entity = $paragraphs_entity->getTranslation($langcode); - } - else { - $paragraphs_entity->set($langcode_key, $langcode); - } - } - } - else { - // Add translation if missing for the target language. - if (!$paragraphs_entity->hasTranslation($langcode)) { - // Get the selected translation of the paragraph entity. - $entity_langcode = $paragraphs_entity->language()->getId(); - $source = $form_state->get(['content_translation', 'source']); - $source_langcode = $source ? $source->getId() : $entity_langcode; - // Make sure the source language version is used if available. It is a - // valid scenario to have no paragraphs items in the source version of - // the host and fetching the translation without this check could lead - // to an exception. - if ($paragraphs_entity->hasTranslation($source_langcode)) { - $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode); - } - // The paragraphs entity has no content translation source field if - // no paragraph entity field is translatable, even if the host is. - if ($paragraphs_entity->hasField('content_translation_source')) { - // Initialise the translation with source language values. - $paragraphs_entity->addTranslation($langcode, $paragraphs_entity->toArray()); - $translation = $paragraphs_entity->getTranslation($langcode); - $manager = \Drupal::service('content_translation.manager'); - $manager->getTranslationMetadata($translation)->setSource($paragraphs_entity->language()->getId()); - } - } - // If any paragraphs type is translatable do not switch. - if ($paragraphs_entity->hasField('content_translation_source')) { - // Switch the paragraph to the translation. - $paragraphs_entity = $paragraphs_entity->getTranslation($langcode); - } - } - - $element_parents = $parents; - $element_parents[] = $field_name; - $element_parents[] = $delta; - $element_parents[] = 'subform'; - - $id_prefix = implode('-', array_merge($parents, array($field_name, $delta))); - $wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper'); - - $element += array( - '#type' => 'container', - '#element_validate' => array(array($this, 'elementValidate')), - 'subform' => array( - '#type' => 'container', - '#parents' => $element_parents, - ), - ); - - $element['#prefix'] = '<div id="' . $wrapper_id . '">'; - $element['#suffix'] = '</div>'; - - $item_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type); - if (isset($item_bundles[$paragraphs_entity->bundle()])) { - $bundle_info = $item_bundles[$paragraphs_entity->bundle()]; - - $element['top'] = array( - '#type' => 'container', - '#weight' => -1000, - '#attributes' => array( - 'class' => array( - 'paragraph-type-top', - ), - ), - ); - - $element['top']['paragraph_type_title'] = array( - '#type' => 'container', - '#weight' => 0, - '#attributes' => array( - 'class' => array( - 'paragraph-type-title', - ), - ), - ); - - $element['top']['paragraph_type_title']['info'] = array( - '#markup' => $bundle_info['label'], - ); - - $actions = array(); - $links = array(); - - // Hide the button when translating. - $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating; - if ($item_mode != 'remove') { - $links['remove_button'] = [ - '#type' => 'submit', - '#value' => $this->t('Remove'), - '#name' => strtr($id_prefix, '-', '_') . '_remove', - '#weight' => 501, - '#submit' => [[get_class($this), 'paragraphsItemSubmit']], - '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])], - '#delta' => $delta, - '#ajax' => [ - 'callback' => [get_class($this), 'itemAjax'], - 'wrapper' => $widget_state['ajax_wrapper_id'], - 'effect' => 'fade', - ], - '#access' => $button_access, - '#prefix' => '<li class="remove">', - '#suffix' => '</li>', - '#paragraphs_mode' => 'remove', - ]; - - } - - if ($item_mode == 'edit') { - - if (isset($items[$delta]->entity) && ($default_edit_mode == 'preview' || $default_edit_mode == 'closed')) { - $links['collapse_button'] = array( - '#type' => 'submit', - '#value' => $this->t('Collapse'), - '#name' => strtr($id_prefix, '-', '_') . '_collapse', - '#weight' => 499, - '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')), - '#delta' => $delta, - '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])], - '#ajax' => array( - 'callback' => array(get_class($this), 'itemAjax'), - 'wrapper' => $widget_state['ajax_wrapper_id'], - 'effect' => 'fade', - ), - '#access' => $paragraphs_entity->access('update'), - '#prefix' => '<li class="collapse">', - '#suffix' => '</li>', - '#paragraphs_mode' => 'collapsed', - '#paragraphs_show_warning' => TRUE, - ); - } - - // Hide the button when translating. - $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating; - - $info['edit_button_info'] = array( - '#type' => 'container', - '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))), - '#attributes' => ['class' => ['messages', 'messages--warning']], - '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'), - ); - - $info['remove_button_info'] = array( - '#type' => 'container', - '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))), - '#attributes' => ['class' => ['messages', 'messages--warning']], - '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'), - ); - - $info['edit_remove_button_info'] = array( - '#type' => 'container', - '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))), - '#attributes' => ['class' => ['messages', 'messages--warning']], - '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'), - ); - } - elseif ($item_mode == 'preview' || $item_mode == 'closed') { - $links['edit_button'] = array( - '#type' => 'submit', - '#value' => $this->t('Edit'), - '#name' => strtr($id_prefix, '-', '_') . '_edit', - '#weight' => 500, - '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')), - '#limit_validation_errors' => array(array_merge($parents, array($field_name, 'add_more'))), - '#delta' => $delta, - '#ajax' => array( - 'callback' => array(get_class($this), 'itemAjax'), - 'wrapper' => $widget_state['ajax_wrapper_id'], - 'effect' => 'fade', - ), - '#access' => $paragraphs_entity->access('update'), - '#prefix' => '<li class="edit">', - '#suffix' => '</li>', - '#paragraphs_mode' => 'edit', - ); - - if ($show_must_be_saved_warning) { - $info['must_be_saved_info'] = array( - '#type' => 'container', - '#markup' => $this->t('You have unsaved changes on this @title item.', array('@title' => $this->getSetting('title'))), - '#attributes' => ['class' => ['messages', 'messages--warning']], - ); - } - - $info['preview_info'] = array( - '#type' => 'container', - '#markup' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))), - '#attributes' => ['class' => ['messages', 'messages--warning']], - '#access' => !$paragraphs_entity->access('view'), - ); - - $info['edit_button_info'] = array( - '#type' => 'container', - '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))), - '#attributes' => ['class' => ['messages', 'messages--warning']], - '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'), - ); - - $info['remove_button_info'] = array( - '#type' => 'container', - '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))), - '#attributes' => ['class' => ['messages', 'messages--warning']], - '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'), - ); - - $info['edit_remove_button_info'] = array( - '#type' => 'container', - '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))), - '#attributes' => ['class' => ['messages', 'messages--warning']], - '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'), - ); - } - elseif ($item_mode == 'remove') { - - $element['top']['paragraph_type_title']['info'] = [ - '#markup' => $this->t('Deleted @title: %type', ['@title' => $this->getSetting('title'), '%type' => $bundle_info['label']]), - ]; - - $links['confirm_remove_button'] = [ - '#type' => 'submit', - '#value' => $this->t('Confirm removal'), - '#name' => strtr($id_prefix, '-', '_') . '_confirm_remove', - '#weight' => 503, - '#submit' => [[get_class($this), 'paragraphsItemSubmit']], - '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])], - '#delta' => $delta, - '#ajax' => [ - 'callback' => [get_class($this), 'itemAjax'], - 'wrapper' => $widget_state['ajax_wrapper_id'], - 'effect' => 'fade', - ], - '#prefix' => '<li class="confirm-remove">', - '#suffix' => '</li>', - '#paragraphs_mode' => 'removed', - ]; - - $links['restore_button'] = [ - '#type' => 'submit', - '#value' => $this->t('Restore'), - '#name' => strtr($id_prefix, '-', '_') . '_restore', - '#weight' => 504, - '#submit' => [[get_class($this), 'paragraphsItemSubmit']], - '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])], - '#delta' => $delta, - '#ajax' => [ - 'callback' => [get_class($this), 'itemAjax'], - 'wrapper' => $widget_state['ajax_wrapper_id'], - 'effect' => 'fade', - ], - '#prefix' => '<li class="restore">', - '#suffix' => '</li>', - '#paragraphs_mode' => 'edit', - ]; - } - - if (count($links)) { - $show_links = 0; - foreach($links as $link_item) { - if (!isset($link_item['#access']) || $link_item['#access']) { - $show_links++; - } - } - - if ($show_links > 0) { - - $element['top']['links'] = $links; - if ($show_links > 1) { - $element['top']['links']['#theme_wrappers'] = array('dropbutton_wrapper', 'paragraphs_dropbutton_wrapper'); - $element['top']['links']['prefix'] = array( - '#markup' => '<ul class="dropbutton">', - '#weight' => -999, - ); - $element['top']['links']['suffix'] = array( - '#markup' => '</li>', - '#weight' => 999, - ); - } - else { - $element['top']['links']['#theme_wrappers'] = array('paragraphs_dropbutton_wrapper'); - foreach($links as $key => $link_item) { - unset($element['top']['links'][$key]['#prefix']); - unset($element['top']['links'][$key]['#suffix']); - } - } - $element['top']['links']['#weight'] = 2; - } - } - - if (count($info)) { - $show_info = FALSE; - foreach($info as $info_item) { - if (!isset($info_item['#access']) || $info_item['#access']) { - $show_info = TRUE; - break; - } - } - - if ($show_info) { - $element['info'] = $info; - $element['info']['#weight'] = 998; - } - } - - if (count($actions)) { - $show_actions = FALSE; - foreach($actions as $action_item) { - if (!isset($action_item['#access']) || $action_item['#access']) { - $show_actions = TRUE; - break; - } - } - - if ($show_actions) { - $element['actions'] = $actions; - $element['actions']['#type'] = 'actions'; - $element['actions']['#weight'] = 999; - } - } - } - - $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode')); - - // @todo Remove as part of https://www.drupal.org/node/2640056 - if (\Drupal::moduleHandler()->moduleExists('field_group')) { - $context = [ - 'entity_type' => $paragraphs_entity->getEntityTypeId(), - 'bundle' => $paragraphs_entity->bundle(), - 'entity' => $paragraphs_entity, - 'context' => 'form', - 'display_context' => 'form', - 'mode' => $display->getMode(), - ]; - - field_group_attach_groups($element['subform'], $context); - $element['subform']['#pre_render'][] = 'field_group_form_pre_render'; - } - - if ($item_mode == 'edit') { - $display->buildForm($paragraphs_entity, $element['subform'], $form_state); - foreach (Element::children($element['subform']) as $field) { - if ($paragraphs_entity->hasField($field)) { - $translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable(); - if ($translatable) { - $element['subform'][$field]['widget']['#after_build'][] = [ - static::class, - 'removeTranslatabilityClue' - ]; - } - } - } - } - elseif ($item_mode == 'preview') { - $element['subform'] = array(); - $element['behavior_plugins'] = []; - $element['preview'] = entity_view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId()); - $element['preview']['#access'] = $paragraphs_entity->access('view'); - } - elseif ($item_mode == 'closed') { - $element['subform'] = array(); - $element['behavior_plugins'] = []; - if ($paragraphs_entity) { - $summary = $paragraphs_entity->getSummary(); - $element['top']['paragraph_summary']['fields_info'] = [ - '#markup' => $summary, - '#prefix' => '<div class="paragraphs-collapsed-description">', - '#suffix' => '</div>', - ]; - } - } - else { - $element['subform'] = array(); - } - - $element['subform']['#attributes']['class'][] = 'paragraphs-subform'; - $element['subform']['#access'] = $paragraphs_entity->access('update'); - - if ($item_mode == 'removed') { - $element['#access'] = FALSE; - } - - $widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity; - $widget_state['paragraphs'][$delta]['display'] = $display; - $widget_state['paragraphs'][$delta]['mode'] = $item_mode; - - static::setWidgetState($parents, $field_name, $form_state, $widget_state); - } - else { - $element['#access'] = FALSE; - } - - return $element; - } - - /** - * Returns the sorted allowed types for a entity reference field. - * - * @return array - * A list of arrays keyed by the paragraph type machine name with the following properties. - * - label: The label of the paragraph type. - * - weight: The weight of the paragraph type. - */ - public function getAllowedTypes() { - - $return_bundles = array(); - /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */ - $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection'); - $handler = $selection_manager->getSelectionHandler($this->fieldDefinition); - if ($handler instanceof ParagraphSelection) { - $return_bundles = $handler->getSortedAllowedTypes(); - } - // Support for other reference types. - else { - $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getFieldSetting('target_type')); - $weight = 0; - foreach ($bundles as $machine_name => $bundle) { - if (!count($this->getSelectionHandlerSetting('target_bundles')) - || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles'))) { - - $return_bundles[$machine_name] = array( - 'label' => $bundle['label'], - 'weight' => $weight, - ); - - $weight++; - } - } - } - - - return $return_bundles; - } - - public function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) { - $field_name = $this->fieldDefinition->getName(); - $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); - $this->fieldParents = $form['#parents']; - $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state); - - $max = $field_state['items_count']; - $entity_type_manager = \Drupal::entityTypeManager(); - - // Consider adding a default paragraph for new host entities. - if ($max == 0 && $items->getEntity()->isNew()) { - $default_type = $this->getDefaultParagraphTypeMachineName(); - - // Checking if default_type is not none and if is allowed. - if ($default_type) { - // Place the default paragraph. - $target_type = $this->getFieldSetting('target_type'); - $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create([ - 'type' => $default_type, - ]); - $field_state['selected_bundle'] = $default_type; - $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode')); - $field_state['paragraphs'][0] = [ - 'entity' => $paragraphs_entity, - 'display' => $display, - 'mode' => 'edit', - 'original_delta' => 1 - ]; - $max = 1; - $field_state['items_count'] = $max; - } - } - - $this->realItemCount = $max; - $is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(); - - $title = $this->fieldDefinition->getLabel(); - $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription())); - - $elements = array(); - $this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name))); - $this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper'); - $elements['#prefix'] = '<div id="' . $this->fieldWrapperId . '">'; - $elements['#suffix'] = '</div>'; - - $field_state['ajax_wrapper_id'] = $this->fieldWrapperId; - // Persist the widget state so formElement() can access it. - static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state); - - if ($max > 0) { - for ($delta = 0; $delta < $max; $delta++) { - - // Add a new empty item if it doesn't exist yet at this delta. - if (!isset($items[$delta])) { - $items->appendItem(); - } - - // For multiple fields, title and description are handled by the wrapping - // table. - $element = array( - '#title' => $is_multiple ? '' : $title, - '#description' => $is_multiple ? '' : $description, - ); - $element = $this->formSingleElement($items, $delta, $element, $form, $form_state); - - if ($element) { - // Input field for the delta (drag-n-drop reordering). - if ($is_multiple) { - // We name the element '_weight' to avoid clashing with elements - // defined by widget. - $element['_weight'] = array( - '#type' => 'weight', - '#title' => $this->t('Weight for row @number', array('@number' => $delta + 1)), - '#title_display' => 'invisible', - // Note: this 'delta' is the FAPI #type 'weight' element's property. - '#delta' => $max, - '#default_value' => $items[$delta]->_weight ?: $delta, - '#weight' => 100, - ); - } - - // Access for the top element is set to FALSE only when the paragraph - // was removed. A paragraphs that a user can not edit has access on - // lower level. - if (isset($element['#access']) && !$element['#access']) { - $this->realItemCount--; - } - else { - $elements[$delta] = $element; - } - } - } - } - - $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state); - $field_state['real_item_count'] = $this->realItemCount; - static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state); - - $elements += [ - '#element_validate' => [[$this, 'multipleElementValidate']], - '#required' => $this->fieldDefinition->isRequired(), - '#field_name' => $field_name, - '#cardinality' => $cardinality, - '#max_delta' => $max - 1, - ]; - - if ($this->realItemCount > 0) { - $elements += array( - '#theme' => 'field_multiple_value_form', - '#cardinality_multiple' => $is_multiple, - '#title' => $title, - '#description' => $description, - ); - } - else { - $classes = $this->fieldDefinition->isRequired() ? ['form-required'] : []; - $elements += [ - '#type' => 'container', - '#theme_wrappers' => ['container'], - '#cardinality_multiple' => TRUE, - 'title' => [ - '#type' => 'html_tag', - '#tag' => 'strong', - '#value' => $title, - '#attributes' => ['class' => $classes], - ], - 'text' => [ - '#type' => 'container', - 'value' => [ - '#markup' => $this->t('No @title added yet.', ['@title' => $this->getSetting('title')]), - '#prefix' => '<em>', - '#suffix' => '</em>', - ] - ], - ]; - - if ($this->fieldDefinition->isRequired()) { - $elements['title']['#attributes']['class'][] = 'form-required'; - } - - if ($description) { - $elements['description'] = [ - '#type' => 'container', - 'value' => ['#markup' => $description], - '#attributes' => ['class' => ['description']], - ]; - } - } - - $host = $items->getEntity(); - $this->initIsTranslating($form_state, $host); - - if (($this->realItemCount < $cardinality || $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) && !$form_state->isProgrammed() && !$this->isTranslating) { - $elements['add_more'] = $this->buildAddActions(); - } - - $elements['#attached']['library'][] = 'paragraphs/drupal.paragraphs.admin'; - - return $elements; - } - - /** - * {@inheritdoc} - */ - public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) { - $parents = $form['#parents']; - - // Identify the manage field settings default value form. - if (in_array('default_value_input', $parents, TRUE)) { - // Since the entity is not reusable neither cloneable, having a default - // value is not supported. - return ['#markup' => $this->t('No widget available for: %label.', ['%label' => $items->getFieldDefinition()->getLabel()])]; - } - - return parent::form($items, $form, $form_state, $get_delta); - } - - /** - * Add 'add more' button, if not working with a programmed form. - * - * @return array - * The form element array. - */ - protected function buildAddActions() { - if (count($this->getAccessibleOptions()) === 0) { - if (count($this->getAllowedTypes()) === 0) { - $add_more_elements['info'] = [ - '#type' => 'container', - '#markup' => $this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]), - '#attributes' => ['class' => ['messages', 'messages--warning']], - ]; - } - else { - $add_more_elements['info'] = [ - '#type' => 'container', - '#markup' => $this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]), - '#attributes' => ['class' => ['messages', 'messages--warning']], - ]; - } - - return $add_more_elements ; - } - - if ($this->getSetting('add_mode') == 'button' || $this->getSetting('add_mode') == 'dropdown') { - return $this->buildButtonsAddMode(); - } - - return $this->buildSelectAddMode(); - } - - /** - * Returns the available paragraphs type. - * - * @return array - * Available paragraphs types. - */ - protected function getAccessibleOptions() { - if ($this->accessOptions !== NULL) { - return $this->accessOptions; - } - - $entity_type_manager = \Drupal::entityTypeManager(); - $target_type = $this->getFieldSetting('target_type'); - $bundles = $this->getAllowedTypes(); - $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type); - $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop'); - - foreach ($bundles as $machine_name => $bundle) { - if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles')) - || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) { - if ($access_control_handler->createAccess($machine_name)) { - $this->accessOptions[$machine_name] = $bundle['label']; - } - } - } - - return $this->accessOptions; - } - - /** - * Builds dropdown button for adding new paragraph. - * - * @return array - * The form element array. - */ - protected function buildButtonsAddMode() { - // Hide the button when translating. - $add_more_elements = [ - '#type' => 'container', - '#theme_wrappers' => ['paragraphs_dropbutton_wrapper'], - ]; - $field_name = $this->fieldDefinition->getName(); - $title = $this->fieldDefinition->getLabel(); - - $drop_button = FALSE; - if (count($this->getAccessibleOptions()) > 1 && $this->getSetting('add_mode') == 'dropdown') { - $drop_button = TRUE; - $add_more_elements['#theme_wrappers'] = ['dropbutton_wrapper']; - $add_more_elements['prefix'] = [ - '#markup' => '<ul class="dropbutton">', - '#weight' => -999, - ]; - $add_more_elements['suffix'] = [ - '#markup' => '</ul>', - '#weight' => 999, - ]; - $add_more_elements['#suffix'] = $this->t(' to %type', ['%type' => $title]); - } - - foreach ($this->getAccessibleOptions() as $machine_name => $label) { - $add_more_elements['add_more_button_' . $machine_name] = [ - '#type' => 'submit', - '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_' . $machine_name . '_add_more', - '#value' => $this->t('Add @type', ['@type' => $label]), - '#attributes' => ['class' => ['field-add-more-submit']], - '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])], - '#submit' => [[get_class($this), 'addMoreSubmit']], - '#ajax' => [ - 'callback' => [get_class($this), 'addMoreAjax'], - 'wrapper' => $this->fieldWrapperId, - 'effect' => 'fade', - ], - '#bundle_machine_name' => $machine_name, - ]; - - if ($drop_button) { - $add_more_elements['add_more_button_' . $machine_name]['#prefix'] = '<li>'; - $add_more_elements['add_more_button_' . $machine_name]['#suffix'] = '</li>'; - } - } - - return $add_more_elements; - } - - /** - * Builds list of actions based on paragraphs type. - * - * @return array - * The form element array. - */ - protected function buildSelectAddMode() { - $field_name = $this->fieldDefinition->getName(); - $title = $this->fieldDefinition->getLabel(); - $add_more_elements['add_more_select'] = [ - '#type' => 'select', - '#options' => $this->getAccessibleOptions(), - '#title' => $this->t('@title type', ['@title' => $this->getSetting('title')]), - '#label_display' => 'hidden', - ]; - - $text = $this->t('Add @title', ['@title' => $this->getSetting('title')]); - - if ($this->realItemCount > 0) { - $text = $this->t('Add another @title', ['@title' => $this->getSetting('title')]); - } - - $add_more_elements['add_more_button'] = [ - '#type' => 'submit', - '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more', - '#value' => $text, - '#attributes' => ['class' => ['field-add-more-submit']], - '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])], - '#submit' => [[get_class($this), 'addMoreSubmit']], - '#ajax' => [ - 'callback' => [get_class($this), 'addMoreAjax'], - 'wrapper' => $this->fieldWrapperId, - 'effect' => 'fade', - ], - ]; - - $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]); - return $add_more_elements; - } - - /** - * Gets current language code from the form state or item. - * - * Since the paragraph field is not set as translatable, the item language - * code is set to the source language. The intended translation language - * is only accessibly through the form state. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * @param \Drupal\Core\Field\FieldItemListInterface $items - * @return string - */ - protected function getCurrentLangcode(FormStateInterface $form_state, FieldItemListInterface $items) { - return $form_state->get('langcode') ?: $items->getEntity()->language()->getId(); - } - - /** - * {@inheritdoc} - */ - public static function addMoreAjax(array $form, FormStateInterface $form_state) { - $button = $form_state->getTriggeringElement(); - // Go one level up in the form, to the widgets container. - $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2)); - - // Add a DIV around the delta receiving the Ajax effect. - $delta = $element['#max_delta']; - $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : ''); - $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>'; - - return $element; - } - - /** - * {@inheritdoc} - */ - public static function addMoreSubmit(array $form, FormStateInterface $form_state) { - $button = $form_state->getTriggeringElement(); - - // Go one level up in the form, to the widgets container. - $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2)); - $field_name = $element['#field_name']; - $parents = $element['#field_parents']; - - // Increment the items count. - $widget_state = static::getWidgetState($parents, $field_name, $form_state); - - if ($widget_state['real_item_count'] < $element['#cardinality'] || $element['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { - $widget_state['items_count']++; - } - - if (isset($button['#bundle_machine_name'])) { - $widget_state['selected_bundle'] = $button['#bundle_machine_name']; - } - else { - $widget_state['selected_bundle'] = $element['add_more']['add_more_select']['#value']; - } - - static::setWidgetState($parents, $field_name, $form_state, $widget_state); - - $form_state->setRebuild(); - } - - public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) { - $button = $form_state->getTriggeringElement(); - - // Go one level up in the form, to the widgets container. - $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4)); - - $delta = array_slice($button['#array_parents'], -4, -3); - $delta = $delta[0]; - - $field_name = $element['#field_name']; - $parents = $element['#field_parents']; - - $widget_state = static::getWidgetState($parents, $field_name, $form_state); - - $widget_state['paragraphs'][$delta]['mode'] = $button['#paragraphs_mode']; - - if (!empty($button['#paragraphs_show_warning'])) { - $widget_state['paragraphs'][$delta]['show_warning'] = $button['#paragraphs_show_warning']; - } - - static::setWidgetState($parents, $field_name, $form_state, $widget_state); - - $form_state->setRebuild(); - } - - public static function itemAjax(array $form, FormStateInterface $form_state) { - $button = $form_state->getTriggeringElement(); - // Go one level up in the form, to the widgets container. - $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4)); - - $element['#prefix'] = '<div class="ajax-new-content">' . (isset($element['#prefix']) ? $element['#prefix'] : ''); - $element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '</div>'; - - return $element; - } - - /** - * {@inheritdoc} - */ - public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) { - return $element; - } - - /** - * Returns the value of a setting for the entity reference selection handler. - * - * @param string $setting_name - * The setting name. - * - * @return mixed - * The setting value. - */ - protected function getSelectionHandlerSetting($setting_name) { - $settings = $this->getFieldSetting('handler_settings'); - return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL; - } - - /** - * Checks whether a content entity is referenced. - * - * @return bool - */ - protected function isContentReferenced() { - $target_type = $this->getFieldSetting('target_type'); - $target_type_info = \Drupal::entityTypeManager()->getDefinition($target_type); - return $target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface'); - } - - /** - * {@inheritdoc} - */ - public function elementValidate($element, FormStateInterface $form_state, $form) { - $field_name = $this->fieldDefinition->getName(); - $widget_state = static::getWidgetState($element['#field_parents'], $field_name, $form_state); - $delta = $element['#delta']; - - if (isset($widget_state['paragraphs'][$delta]['entity'])) { - $entity = $widget_state['paragraphs'][$delta]['entity']; - - /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */ - $display = $widget_state['paragraphs'][$delta]['display']; - - if ($widget_state['paragraphs'][$delta]['mode'] == 'edit') { - // Extract the form values on submit for getting the current paragraph. - $display->extractFormValues($entity, $element['subform'], $form_state); - $display->validateFormValues($entity, $element['subform'], $form_state); - } - } - - static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state); - } - - /** - * Special handling to validate form elements with multiple values. - * - * @param array $elements - * An associative array containing the substructure of the form to be - * validated in this call. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * @param array $form - * The complete form array. - */ - public function multipleElementValidate(array $elements, FormStateInterface $form_state, array $form) { - $field_name = $this->fieldDefinition->getName(); - $widget_state = static::getWidgetState($elements['#field_parents'], $field_name, $form_state); - - $remove_mode_item_count = $this->getNumberOfParagraphsInMode($widget_state, 'remove'); - $non_remove_mode_item_count = $widget_state['real_item_count'] - $remove_mode_item_count; - - if ($elements['#required'] && $non_remove_mode_item_count < 1) { - $form_state->setError($elements, t('@name field is required.', ['@name' => $this->fieldDefinition->getLabel()])); - } - - static::setWidgetState($elements['#field_parents'], $field_name, $form_state, $widget_state); - } - - /** - * {@inheritdoc} - */ - public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { - $field_name = $this->fieldDefinition->getName(); - $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state); - $element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']); - - foreach ($values as $delta => &$item) { - if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity']) - && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') { - $paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity']; - - /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */ - $display = $widget_state['paragraphs'][$item['_original_delta']]['display']; - if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') { - $display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state); - } - // A content entity form saves without any rebuild. It needs to set the - // language to update it in case of language change. - $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode'); - if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) { - // If a translation in the given language already exists, switch to - // that. If there is none yet, update the language. - if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) { - $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode')); - } - else { - $paragraphs_entity->set($langcode_key, $form_state->get('langcode')); - } - } - - $paragraphs_entity->setNeedsSave(TRUE); - $item['entity'] = $paragraphs_entity; - $item['target_id'] = $paragraphs_entity->id(); - $item['target_revision_id'] = $paragraphs_entity->getRevisionId(); - } - // If our mode is remove don't save or reference this entity. - // @todo: Maybe we should actually delete it here? - elseif($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove' || $widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'removed') { - $item['target_id'] = NULL; - $item['target_revision_id'] = NULL; - } - } - return $values; - } - - /** - * {@inheritdoc} - */ - public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) { - // Filter possible empty items. - $items->filterEmptyItems(); - return parent::extractFormValues($items, $form, $form_state); - } - - /** - * Initializes the translation form state. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * @param \Drupal\Core\Entity\EntityInterface $host - */ - protected function initIsTranslating(FormStateInterface $form_state, EntityInterface $host) { - if ($this->isTranslating != NULL) { - return; - } - $this->isTranslating = FALSE; - if (!$host->isTranslatable()) { - return; - } - if (!$host->getEntityType()->hasKey('default_langcode')) { - return; - } - $default_langcode_key = $host->getEntityType()->getKey('default_langcode'); - if (!$host->hasField($default_langcode_key)) { - return; - } - - if (!empty($form_state->get('content_translation'))) { - // Adding a language through the ContentTranslationController. - $this->isTranslating = TRUE; - } - if ($host->hasTranslation($form_state->get('langcode')) && $host->getTranslation($form_state->get('langcode'))->get($default_langcode_key)->value == 0) { - // Editing a translation. - $this->isTranslating = TRUE; - } - } - - /** - * After-build callback for removing the translatability clue from the widget. - * - * If the fields on the paragraph type are translatable, - * ContentTranslationHandler::addTranslatabilityClue()adds an - * "(all languages)" suffix to the widget title. That suffix is incorrect and - * is being removed by this method using a #after_build on the field widget. - * - * @param array $element - * @param \Drupal\Core\Form\FormStateInterface $form_state - * - * @return array - */ - public static function removeTranslatabilityClue(array $element, FormStateInterface $form_state) { - // Widgets could have multiple elements with their own titles, so remove the - // suffix if it exists, do not recurse lower than this to avoid going into - // nested paragraphs or similar nested field types. - $suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>'; - if (isset($element['#title']) && strpos($element['#title'], $suffix)) { - $element['#title'] = str_replace($suffix, '', $element['#title']); - } - // Loop over all widget deltas. - foreach (Element::children($element) as $delta) { - if (isset($element[$delta]['#title']) && strpos($element[$delta]['#title'], $suffix)) { - $element[$delta]['#title'] = str_replace($suffix, '', $element[$delta]['#title']); - } - // Loop over all form elements within the current delta. - foreach (Element::children($element[$delta]) as $field) { - if (isset($element[$delta][$field]['#title']) && strpos($element[$delta][$field]['#title'], $suffix)) { - $element[$delta][$field]['#title'] = str_replace($suffix, '', $element[$delta][$field]['#title']); - } - } - } - return $element; - } - - /** - * Returns the default paragraph type. - * - * @return string $default_paragraph_type - * Label name for default paragraph type. - */ - protected function getDefaultParagraphTypeLabelName(){ - if ($this->getDefaultParagraphTypeMachineName() !== NULL) { - $allowed_types = $this->getAllowedTypes(); - return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label']; - } - - return NULL; - } - - /** - * Returns the machine name for default paragraph type. - * - * @return string - * Machine name for default paragraph type. - */ - protected function getDefaultParagraphTypeMachineName() { - $default_type = $this->getSetting('default_paragraph_type'); - $allowed_types = $this->getAllowedTypes(); - if ($default_type && isset($allowed_types[$default_type])) { - return $default_type; - } - // Check if the user explicitly selected not to have any default Paragraph - // type. Othewise, if there is only one type available, that one is the - // default. - if ($default_type === '_none') { - return NULL; - } - if (count($allowed_types) === 1) { - return key($allowed_types); - } - - return NULL; - } - - /** - * Counts the number of paragraphs in a certain mode in a form substructure. - * - * @param array $widget_state - * The widget state for the form substructure containing information about - * the paragraphs within. - * @param string $mode - * The mode to look for. - * - * @return int - * The number of paragraphs is the given mode. - */ - protected function getNumberOfParagraphsInMode(array $widget_state, $mode) { - if (!isset($widget_state['paragraphs'])) { - return 0; - } - - $paragraphs_count = 0; - foreach ($widget_state['paragraphs'] as $paragraph) { - if ($paragraph['mode'] == $mode) { - $paragraphs_count++; - } - } - - return $paragraphs_count; - } - - /** - * {@inheritdoc} - */ - public static function isApplicable(FieldDefinitionInterface $field_definition) { - $target_type = $field_definition->getSetting('target_type'); - $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type); - if ($paragraph_type) { - return $paragraph_type->isSubclassOf(ParagraphInterface::class); - } - - return FALSE; - } - -} diff --git a/web/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/ParagraphsWidget.php b/web/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/ParagraphsWidget.php deleted file mode 100644 index 84b47eae4..000000000 --- a/web/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/ParagraphsWidget.php +++ /dev/null @@ -1,2392 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Plugin\Field\FieldWidget; - -use Drupal\Component\Utility\NestedArray; -use Drupal\Component\Utility\Html; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldFilteredMarkup; -use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\WidgetBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Field\FieldItemListInterface; -use Drupal\Core\Form\SubformState; -use Drupal\Core\Render\Element; -use Drupal\paragraphs\ParagraphInterface; -use Drupal\paragraphs\Plugin\EntityReferenceSelection\ParagraphSelection; -use Symfony\Component\Validator\ConstraintViolationListInterface; - -/** - * Plugin implementation of the 'entity_reference_revisions paragraphs' widget. - * - * @FieldWidget( - * id = "paragraphs", - * label = @Translation("Paragraphs EXPERIMENTAL"), - * description = @Translation("An experimental paragraphs inline form widget."), - * field_types = { - * "entity_reference_revisions" - * } - * ) - */ -class ParagraphsWidget extends WidgetBase { - - /** - * Action position is in the add paragraphs place. - */ - const ACTION_POSITION_BASE = 1; - - /** - * Action position is in the table header section. - */ - const ACTION_POSITION_HEADER = 2; - - /** - * Action position is in the actions section of the widget. - */ - const ACTION_POSITION_ACTIONS = 3; - - /** - * Indicates whether the current widget instance is in translation. - * - * @var bool - */ - protected $isTranslating; - - /** - * Id to name ajax buttons that includes field parents and field name. - * - * @var string - */ - protected $fieldIdPrefix; - - /** - * Wrapper id to identify the paragraphs. - * - * @var string - */ - protected $fieldWrapperId; - - /** - * Number of paragraphs item on form. - * - * @var int - */ - protected $realItemCount; - - /** - * Parents for the current paragraph. - * - * @var array - */ - protected $fieldParents; - - /** - * Accessible paragraphs types. - * - * @var array - */ - protected $accessOptions = NULL; - - /** - * Constructs a ParagraphsWidget object. - * - * @param string $plugin_id - * The plugin_id for the widget. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The definition of the field to which the widget is associated. - * @param array $settings - * The widget settings. - * @param array $third_party_settings - * Any third party settings. - */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) { - // Modify settings that were set before https://www.drupal.org/node/2896115. - if(isset($settings['edit_mode']) && $settings['edit_mode'] === 'preview') { - $settings['edit_mode'] = 'closed'; - $settings['closed_mode'] = 'preview'; - } - - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); - } - - /** - * {@inheritdoc} - */ - public static function defaultSettings() { - return array( - 'title' => t('Paragraph'), - 'title_plural' => t('Paragraphs'), - 'edit_mode' => 'open', - 'closed_mode' => 'summary', - 'autocollapse' => 'none', - 'add_mode' => 'dropdown', - 'form_display_mode' => 'default', - 'default_paragraph_type' => '', - ); - } - - /** - * {@inheritdoc} - */ - public function settingsForm(array $form, FormStateInterface $form_state) { - $elements = array(); - - $elements['title'] = array( - '#type' => 'textfield', - '#title' => $this->t('Paragraph Title'), - '#description' => $this->t('Label to appear as title on the button as "Add new [title]", this label is translatable'), - '#default_value' => $this->getSetting('title'), - '#required' => TRUE, - ); - - $elements['title_plural'] = array( - '#type' => 'textfield', - '#title' => $this->t('Plural Paragraph Title'), - '#description' => $this->t('Title in its plural form.'), - '#default_value' => $this->getSetting('title_plural'), - '#required' => TRUE, - ); - - $elements['edit_mode'] = array( - '#type' => 'select', - '#title' => $this->t('Edit mode'), - '#description' => $this->t('The mode the paragraph is in by default.'), - '#options' => $this->getSettingOptions('edit_mode'), - '#default_value' => $this->getSetting('edit_mode'), - '#required' => TRUE, - ); - - $elements['closed_mode'] = [ - '#type' => 'select', - '#title' => $this->t('Closed mode'), - '#description' => $this->t('How to display the paragraphs, when the widget is closed. Preview will render the paragraph in the preview view mode and typically needs a custom admin theme.'), - '#options' => $this->getSettingOptions('closed_mode'), - '#default_value' => $this->getSetting('closed_mode'), - '#required' => TRUE, - ]; - - $elements['autocollapse'] = [ - '#type' => 'select', - '#title' => $this->t('Autocollapse'), - '#description' => $this->t('When a paragraph is opened for editing, close others.'), - '#options' => $this->getSettingOptions('autocollapse'), - '#default_value' => $this->getSetting('autocollapse'), - '#required' => TRUE, - ]; - - $elements['add_mode'] = array( - '#type' => 'select', - '#title' => $this->t('Add mode'), - '#description' => $this->t('The way to add new Paragraphs.'), - '#options' => $this->getSettingOptions('add_mode'), - '#default_value' => $this->getSetting('add_mode'), - '#required' => TRUE, - ); - - $elements['form_display_mode'] = array( - '#type' => 'select', - '#options' => \Drupal::service('entity_display.repository')->getFormModeOptions($this->getFieldSetting('target_type')), - '#description' => $this->t('The form display mode to use when rendering the paragraph form.'), - '#title' => $this->t('Form display mode'), - '#default_value' => $this->getSetting('form_display_mode'), - '#required' => TRUE, - ); - - $options = []; - foreach ($this->getAllowedTypes() as $key => $bundle) { - $options[$key] = $bundle['label']; - } - - $elements['default_paragraph_type'] = [ - '#type' => 'select', - '#title' => $this->t('Default paragraph type'), - '#empty_value' => '_none', - '#default_value' => $this->getDefaultParagraphTypeMachineName(), - '#options' => $options, - '#description' => $this->t('When creating a new host entity, a paragraph of this type is added.'), - ]; - - return $elements; - } - - /** - * Returns select options for a plugin setting. - * - * This is done to allow - * \Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget::settingsSummary() - * to access option labels. Not all plugin setting are available. - * - * @param string $setting_name - * The name of the widget setting. Supported settings: - * - "edit_mode" - * - "closed_mode" - * - "autocollapse" - * - "add_mode" - * - * @return array|null - * An array of setting option usable as a value for a "#options" key. - */ - protected function getSettingOptions($setting_name) { - switch($setting_name) { - case 'edit_mode': - $options = [ - 'open' => $this->t('Open'), - 'closed' => $this->t('Closed'), - ]; - break; - case 'closed_mode': - $options = [ - 'summary' => $this->t('Summary'), - 'preview' => $this->t('Preview'), - ]; - break; - case 'autocollapse': - $options = [ - 'none' => $this->t('None'), - 'all' => $this->t('All'), - ]; - break; - case 'add_mode': - $options = [ - 'select' => $this->t('Select list'), - 'button' => $this->t('Buttons'), - 'dropdown' => $this->t('Dropdown button'), - 'modal' => $this->t('Modal form'), - ]; - break; - } - - return isset($options) ? $options : NULL; - } - - /** - * {@inheritdoc} - */ - public function settingsSummary() { - $summary = array(); - $summary[] = $this->t('Title: @title', ['@title' => $this->getSetting('title')]); - $summary[] = $this->t('Plural title: @title_plural', [ - '@title_plural' => $this->getSetting('title_plural') - ]); - - $edit_mode = $this->getSettingOptions('edit_mode')[$this->getSetting('edit_mode')]; - $closed_mode = $this->getSettingOptions('closed_mode')[$this->getSetting('closed_mode')]; - $autocollapse = $this->getSettingOptions('autocollapse')[$this->getSetting('autocollapse')]; - $add_mode = $this->getSettingOptions('add_mode')[$this->getSetting('add_mode')]; - - $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]); - $summary[] = $this->t('Closed mode: @closed_mode', ['@closed_mode' => $closed_mode]); - $summary[] = $this->t('Autocollapse: @autocollapse', ['@autocollapse' => $autocollapse]); - $summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]); - - $summary[] = $this->t('Form display mode: @form_display_mode', [ - '@form_display_mode' => $this->getSetting('form_display_mode') - ]); - if ($this->getDefaultParagraphTypeLabelName() !== NULL) { - $summary[] = $this->t('Default paragraph type: @default_paragraph_type', [ - '@default_paragraph_type' => $this->getDefaultParagraphTypeLabelName() - ]); - } - - return $summary; - } - - /** - * {@inheritdoc} - * - * @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation() - * Uses a similar approach to populate a new translation. - */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $field_name = $this->fieldDefinition->getName(); - $parents = $element['#field_parents']; - $info = []; - - /** @var \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity */ - $paragraphs_entity = NULL; - $host = $items->getEntity(); - $widget_state = static::getWidgetState($parents, $field_name, $form_state); - - $entity_type_manager = \Drupal::entityTypeManager(); - $target_type = $this->getFieldSetting('target_type'); - - $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit'; - $default_edit_mode = $this->getSetting('edit_mode'); - - $closed_mode_setting = isset($widget_state['closed_mode']) ? $widget_state['closed_mode'] : $this->getSetting('closed_mode'); - $autocollapse_setting = isset($widget_state['autocollapse']) ? $widget_state['autocollapse'] : $this->getSetting('autocollapse'); - - $show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']); - - if (isset($widget_state['paragraphs'][$delta]['entity'])) { - $paragraphs_entity = $widget_state['paragraphs'][$delta]['entity']; - } - elseif (isset($items[$delta]->entity)) { - $paragraphs_entity = $items[$delta]->entity; - - // We don't have a widget state yet, get from selector settings. - if (!isset($widget_state['paragraphs'][$delta]['mode'])) { - - if ($default_edit_mode == 'open') { - $item_mode = 'edit'; - } - elseif ($default_edit_mode == 'closed') { - $item_mode = 'closed'; - } - } - } - elseif (isset($widget_state['selected_bundle'])) { - - $entity_type = $entity_type_manager->getDefinition($target_type); - $bundle_key = $entity_type->getKey('bundle'); - - $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create(array( - $bundle_key => $widget_state['selected_bundle'], - )); - - $item_mode = 'edit'; - } - - if ($item_mode == 'closed') { - // Validate closed paragraphs and expand if needed. - // @todo Consider recursion. - $violations = $paragraphs_entity->validate(); - $violations->filterByFieldAccess(); - if (count($violations) > 0) { - $item_mode = 'edit'; - $messages = []; - foreach ($violations as $violation) { - $messages[] = $violation->getMessage(); - } - $info['validation_error'] = $this->createMessage($this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))])); - } - } - - if ($paragraphs_entity) { - // Detect if we are translating. - $this->initIsTranslating($form_state, $host); - $langcode = $form_state->get('langcode'); - - if (!$this->isTranslating) { - // Set the langcode if we are not translating. - $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode'); - if ($paragraphs_entity->get($langcode_key)->value != $langcode) { - // If a translation in the given language already exists, switch to - // that. If there is none yet, update the language. - if ($paragraphs_entity->hasTranslation($langcode)) { - $paragraphs_entity = $paragraphs_entity->getTranslation($langcode); - } - else { - $paragraphs_entity->set($langcode_key, $langcode); - } - } - } - else { - // Add translation if missing for the target language. - if (!$paragraphs_entity->hasTranslation($langcode)) { - // Get the selected translation of the paragraph entity. - $entity_langcode = $paragraphs_entity->language()->getId(); - $source = $form_state->get(['content_translation', 'source']); - $source_langcode = $source ? $source->getId() : $entity_langcode; - // Make sure the source language version is used if available. It is a - // the host and fetching the translation without this check could lead - // valid scenario to have no paragraphs items in the source version of - // to an exception. - if ($paragraphs_entity->hasTranslation($source_langcode)) { - $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode); - } - // The paragraphs entity has no content translation source field if - // no paragraph entity field is translatable, even if the host is. - if ($paragraphs_entity->hasField('content_translation_source')) { - // Initialise the translation with source language values. - $paragraphs_entity->addTranslation($langcode, $paragraphs_entity->toArray()); - $translation = $paragraphs_entity->getTranslation($langcode); - $manager = \Drupal::service('content_translation.manager'); - $manager->getTranslationMetadata($translation)->setSource($paragraphs_entity->language()->getId()); - } - } - // If any paragraphs type is translatable do not switch. - if ($paragraphs_entity->hasField('content_translation_source')) { - // Switch the paragraph to the translation. - $paragraphs_entity = $paragraphs_entity->getTranslation($langcode); - } - } - - $element_parents = $parents; - $element_parents[] = $field_name; - $element_parents[] = $delta; - $element_parents[] = 'subform'; - - $id_prefix = implode('-', array_merge($parents, array($field_name, $delta))); - $wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper'); - - $element += array( - '#type' => 'container', - '#element_validate' => array(array($this, 'elementValidate')), - 'subform' => array( - '#type' => 'container', - '#parents' => $element_parents, - ), - ); - - $element['#prefix'] = '<div id="' . $wrapper_id . '">'; - $element['#suffix'] = '</div>'; - - $item_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type); - if (isset($item_bundles[$paragraphs_entity->bundle()])) { - $bundle_info = $item_bundles[$paragraphs_entity->bundle()]; - - // Create top section structure with all needed subsections. - $element['top'] = [ - '#type' => 'container', - '#weight' => -1000, - '#attributes' => ['class' => ['paragraph-type-top']], - // Section for paragraph type information. - 'type' => [ - '#type' => 'container', - '#attributes' => ['class' => ['paragraph-type-title']], - 'label' => ['#markup' => $bundle_info['label']], - ], - // Section for information icons. - 'info' => [ - '#type' => 'container', - '#attributes' => ['class' => ['paragraph-type-info']], - ], - 'summary' => [ - '#type' => 'container', - '#attributes' => ['class' => ['paragraph-type-summary']], - ], - // Paragraphs actions element for actions and dropdown actions. - 'actions' => [ - '#type' => 'paragraphs_actions', - ], - ]; - - // Type icon and label bundle. - if ($icon_url = $paragraphs_entity->type->entity->getIconUrl()) { - $element['top']['type']['icon'] = [ - '#theme' => 'image', - '#uri' => $icon_url, - '#attributes' => [ - 'class' => ['paragraph-type-icon'], - 'title' => $bundle_info['label'], - ], - '#weight' => 0, - // We set inline height and width so icon don't resize on first load - // while CSS is still not loaded. - '#height' => 16, - '#width' => 16, - ]; - } - $element['top']['type']['label'] = [ - '#markup' => '<span class="paragraph-type-label">' . $bundle_info['label'] . '</span>', - '#weight' => 1, - ]; - - // Widget actions. - $widget_actions = [ - 'actions' => [], - 'dropdown_actions' => [], - ]; - - $widget_actions['dropdown_actions']['duplicate_button'] = [ - '#type' => 'submit', - '#value' => $this->t('Duplicate'), - '#name' => $id_prefix . '_duplicate', - '#weight' => 502, - '#submit' => [[get_class($this), 'duplicateSubmit']], - '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])], - '#delta' => $delta, - '#ajax' => [ - 'callback' => [get_class($this), 'itemAjax'], - 'wrapper' => $widget_state['ajax_wrapper_id'], - ], - '#access' => $paragraphs_entity->access('update'), - ]; - - if ($item_mode != 'remove') { - $widget_actions['dropdown_actions']['remove_button'] = [ - '#type' => 'submit', - '#value' => $this->t('Remove'), - '#name' => $id_prefix . '_remove', - '#weight' => 501, - '#submit' => [[get_class($this), 'paragraphsItemSubmit']], - '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])], - '#delta' => $delta, - '#ajax' => [ - 'callback' => array(get_class($this), 'itemAjax'), - 'wrapper' => $widget_state['ajax_wrapper_id'], - ], - // Hide the button when translating. - '#access' => $paragraphs_entity->access('delete') && !$this->isTranslating, - '#paragraphs_mode' => 'remove', - ]; - } - - if ($item_mode == 'edit') { - if (isset($paragraphs_entity)) { - $widget_actions['actions']['collapse_button'] = [ - '#value' => $this->t('Collapse'), - '#name' => $id_prefix . '_collapse', - '#weight' => 1, - '#submit' => [[get_class($this), 'paragraphsItemSubmit']], - '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])], - '#delta' => $delta, - '#ajax' => [ - 'callback' => [get_class($this), 'itemAjax'], - 'wrapper' => $widget_state['ajax_wrapper_id'], - ], - '#access' => $paragraphs_entity->access('update'), - '#paragraphs_mode' => 'closed', - '#paragraphs_show_warning' => TRUE, - '#attributes' => [ - 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'], - 'title' => $this->t('Collapse'), - ], - ]; - } - } - else { - $widget_actions['actions']['edit_button'] = $this->expandButton([ - '#type' => 'submit', - '#value' => $this->t('Edit'), - '#name' => $id_prefix . '_edit', - '#weight' => 1, - '#attributes' => ['class' => ['paragraphs-button']], - '#submit' => [[get_class($this), 'paragraphsItemSubmit']], - '#limit_validation_errors' => [ - array_merge($parents, [$field_name, 'add_more']), - ], - '#delta' => $delta, - '#ajax' => [ - 'callback' => [get_class($this), 'itemAjax'], - 'wrapper' => $widget_state['ajax_wrapper_id'], - ], - '#access' => $paragraphs_entity->access('update'), - '#paragraphs_mode' => 'edit', - '#attributes' => [ - 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'], - 'title' => $this->t('Edit'), - ], - ]); - - if ($show_must_be_saved_warning && $paragraphs_entity->isChanged()) { - $info['changed'] = [ - '#theme' => 'paragraphs_info_icon', - '#message' => $this->t('You have unsaved changes on this @title item.', ['@title' => $this->getSetting('title')]), - '#icon' => 'changed', - ]; - } - - if (!$paragraphs_entity->access('view')) { - $info['preview'] = [ - '#theme' => 'paragraphs_info_icon', - '#message' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))), - '#icon' => 'view', - ]; - } - } - - // If update is disabled we will show lock icon in actions section. - if (!$paragraphs_entity->access('update')) { - $widget_actions['actions']['edit_disabled'] = [ - '#theme' => 'paragraphs_info_icon', - '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]), - '#icon' => 'lock', - '#weight' => 1, - ]; - } - - if (!$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete')) { - $info['edit'] = [ - '#theme' => 'paragraphs_info_icon', - '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]), - '#icon' => 'lock', - ]; - } - elseif (!$paragraphs_entity->access('update')) { - $info['edit'] = [ - '#theme' => 'paragraphs_info_icon', - '#message' => $this->t('You are not allowed to edit this @title.', ['@title' => $this->getSetting('title')]), - '#icon' => 'edit-disabled', - ]; - } - elseif (!$paragraphs_entity->access('delete')) { - $info['remove'] = [ - '#theme' => 'paragraphs_info_icon', - '#message' => $this->t('You are not allowed to remove this @title.', ['@title' => $this->getSetting('title')]), - '#icon' => 'delete-disabled', - ]; - } - - $context = [ - 'form' => $form, - 'widget' => self::getWidgetState($parents, $field_name, $form_state, $widget_state), - 'items' => $items, - 'delta' => $delta, - 'element' => $element, - 'form_state' => $form_state, - 'paragraphs_entity' => $paragraphs_entity, - ]; - - // Allow modules to alter widget actions. - \Drupal::moduleHandler()->alter('paragraphs_widget_actions', $widget_actions, $context); - - if (count($widget_actions['actions'])) { - // Expand all actions to proper submit elements and add it to top - // actions sub component. - $element['top']['actions']['actions'] = array_map([$this, 'expandButton'], $widget_actions['actions']); - } - - if (count($widget_actions['dropdown_actions'])) { - // Expand all dropdown actions to proper submit elements and add - // them to top dropdown actions sub component. - $element['top']['actions']['dropdown_actions'] = array_map([$this, 'expandButton'], $widget_actions['dropdown_actions']); - } - - if (count($info)) { - foreach ($info as $info_item) { - if (!isset($info_item['#access']) || $info_item['#access']) { - $element['top']['info']['items'] = $info; - break; - } - } - } - } - - $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode')); - - // @todo Remove as part of https://www.drupal.org/node/2640056 - if (\Drupal::moduleHandler()->moduleExists('field_group')) { - $context = [ - 'entity_type' => $paragraphs_entity->getEntityTypeId(), - 'bundle' => $paragraphs_entity->bundle(), - 'entity' => $paragraphs_entity, - 'context' => 'form', - 'display_context' => 'form', - 'mode' => $display->getMode(), - ]; - - field_group_attach_groups($element['subform'], $context); - $element['subform']['#pre_render'][] = 'field_group_form_pre_render'; - } - - if ($item_mode == 'edit') { - $display->buildForm($paragraphs_entity, $element['subform'], $form_state); - // Get the field definitions of the paragraphs_entity. - // We need them to filter out entity reference revisions fields that - // reference paragraphs, cause otherwise we have problems with showing - // and hiding the right fields in nested paragraphs. - $field_definitions = $paragraphs_entity->getFieldDefinitions(); - - foreach (Element::children($element['subform']) as $field) { - // Do a check if we have to add a class to the form element. We need - // those classes (paragraphs-content and paragraphs-behavior) to show - // and hide elements, depending of the active perspective. - $omit_class = FALSE; - if (isset($field_definitions[$field])) { - $type = $field_definitions[$field]->getType(); - if ($type == 'entity_reference_revisions') { - // Check if we are referencing paragraphs. - $target_entity_type = $field_definitions[$field]->get('entity_type'); - if ($target_entity_type && $target_entity_type == 'paragraph') { - $omit_class = TRUE; - } - } - } - - if ($paragraphs_entity->hasField($field)) { - if (!$omit_class) { - $element['subform'][$field]['#attributes']['class'][] = 'paragraphs-content'; - } - $translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable(); - if ($translatable) { - $element['subform'][$field]['widget']['#after_build'][] = [ - static::class, - 'removeTranslatabilityClue', - ]; - } - } - } - - // Build the behavior plugins fields. - $paragraphs_type = $paragraphs_entity->getParagraphType(); - if ($paragraphs_type && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) { - $element['behavior_plugins']['#weight'] = -99; - foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) { - $element['behavior_plugins'][$plugin_id] = [ - '#type' => 'container', - '#group' => implode('][', array_merge($element_parents, ['paragraph_behavior'])), - ]; - $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form, $form_state); - if ($plugin_form = $plugin->buildBehaviorForm($paragraphs_entity, $element['behavior_plugins'][$plugin_id], $subform_state)) { - $element['behavior_plugins'][$plugin_id] = $plugin_form; - // Add the paragraphs-behavior class, so that we are able to show - // and hide behavior fields, depending on the active perspective. - $element['behavior_plugins'][$plugin_id]['#attributes']['class'][] = 'paragraphs-behavior'; - } - } - } - } - elseif ($item_mode == 'closed') { - $element['subform'] = []; - $element['behavior_plugins'] = []; - if ($closed_mode_setting === 'preview') { - // The closed paragraph is displayed as a rendered preview. - $view_builder = $entity_type_manager->getViewBuilder('paragraph'); - - $element['preview'] = $view_builder->view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId()); - $element['preview']['#access'] = $paragraphs_entity->access('view'); - } - else { - // The closed paragraph is displayed as a summary. - if ($paragraphs_entity) { - $summary = $paragraphs_entity->getSummary(); - if (!empty($summary)) { - $element['top']['summary']['fields_info'] = [ - '#markup' => $summary, - '#prefix' => '<div class="paragraphs-collapsed-description">', - '#suffix' => '</div>', - '#access' => $paragraphs_entity->access('view'), - ]; - } - } - } - } - else { - $element['subform'] = array(); - } - - $element['subform']['#attributes']['class'][] = 'paragraphs-subform'; - $element['subform']['#access'] = $paragraphs_entity->access('update'); - - if ($item_mode == 'remove') { - $element['#access'] = FALSE; - } - - $widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity; - $widget_state['paragraphs'][$delta]['display'] = $display; - $widget_state['paragraphs'][$delta]['mode'] = $item_mode; - $widget_state['closed_mode'] = $closed_mode_setting; - $widget_state['autocollapse'] = $autocollapse_setting; - - static::setWidgetState($parents, $field_name, $form_state, $widget_state); - } - else { - $element['#access'] = FALSE; - } - - return $element; - } - - /** - * Builds an add paragraph button for opening of modal form. - * - * @param array $element - * Render element. - */ - protected function buildModalAddForm(array &$element) { - // Attach the theme for the dialog template. - $element['#theme'] = 'paragraphs_add_dialog'; - - $element['add_modal_form_area'] = [ - '#type' => 'container', - '#attributes' => [ - 'class' => [ - 'paragraph-type-add-modal', - 'first-button', - ], - ], - '#access' => !$this->isTranslating, - '#weight' => -2000, - ]; - - $element['add_modal_form_area']['add_more'] = [ - '#type' => 'submit', - '#value' => $this->t('Add @title', ['@title' => $this->getSetting('title')]), - '#name' => 'button_add_modal', - '#attributes' => [ - 'class' => [ - 'paragraph-type-add-modal-button', - 'js-show', - ], - ], - ]; - - $element['#attached']['library'][] = 'paragraphs/drupal.paragraphs.modal'; - } - - /** - * Returns the sorted allowed types for a entity reference field. - * - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * (optional) The field definition forwhich the allowed types should be - * returned, defaults to the current field. - * - * @return array - * A list of arrays keyed by the paragraph type machine name with the following properties. - * - label: The label of the paragraph type. - * - weight: The weight of the paragraph type. - */ - public function getAllowedTypes(FieldDefinitionInterface $field_definition = NULL) { - - $return_bundles = array(); - /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */ - $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection'); - $handler = $selection_manager->getSelectionHandler($field_definition ?: $this->fieldDefinition); - if ($handler instanceof ParagraphSelection) { - $return_bundles = $handler->getSortedAllowedTypes(); - } - // Support for other reference types. - else { - $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($field_definition ? $field_definition->getSetting('target_type') : $this->fieldDefinition->getSetting('target_type')); - $weight = 0; - foreach ($bundles as $machine_name => $bundle) { - if (!count($this->getSelectionHandlerSetting('target_bundles')) - || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles'))) { - - $return_bundles[$machine_name] = array( - 'label' => $bundle['label'], - 'weight' => $weight, - ); - - $weight++; - } - } - } - - - return $return_bundles; - } - - /** - * {@inheritdoc} - */ - public function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) { - $field_name = $this->fieldDefinition->getName(); - $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); - $this->fieldParents = $form['#parents']; - $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state); - - $max = $field_state['items_count']; - $entity_type_manager = \Drupal::entityTypeManager(); - - // Consider adding a default paragraph for new host entities. - if ($max == 0 && $items->getEntity()->isNew()) { - $default_type = $this->getDefaultParagraphTypeMachineName(); - - // Checking if default_type is not none and if is allowed. - if ($default_type) { - // Place the default paragraph. - $target_type = $this->getFieldSetting('target_type'); - $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create([ - 'type' => $default_type, - ]); - $field_state['selected_bundle'] = $default_type; - $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode')); - $field_state['paragraphs'][0] = [ - 'entity' => $paragraphs_entity, - 'display' => $display, - 'mode' => 'edit', - 'original_delta' => 1 - ]; - $max = 1; - $field_state['items_count'] = $max; - } - } - - $this->realItemCount = $max; - $is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(); - - $field_title = $this->fieldDefinition->getLabel(); - $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription())); - - $elements = array(); - $tabs = ''; - $this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name))); - $this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper'); - - // If the parent entity is paragraph add the nested class if not then add - // the perspective tabs. - $field_prefix = strtr($this->fieldIdPrefix, '_', '-'); - if (count($this->fieldParents) == 0) { - if ($items->getEntity()->getEntityTypeId() != 'paragraph') { - $tabs = '<ul class="paragraphs-tabs tabs primary clearfix"><li id="content" class="tabs__tab"><a href="#' . $field_prefix . '-values">Content</a></li><li id="behavior" class="tabs__tab"><a href="#' . $field_prefix . '-values">Behavior</a></li></ul>'; - } - } - if (count($this->fieldParents) > 0) { - if ($items->getEntity()->getEntityTypeId() === 'paragraph') { - $form['#attributes']['class'][] = 'paragraphs-nested'; - } - } - $elements['#prefix'] = '<div class="is-horizontal paragraphs-tabs-wrapper" id="' . $this->fieldWrapperId . '">' . $tabs; - $elements['#suffix'] = '</div>'; - - $field_state['ajax_wrapper_id'] = $this->fieldWrapperId; - // Persist the widget state so formElement() can access it. - static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state); - - $header_actions = $this->buildHeaderActions($field_state, $form_state); - if ($header_actions) { - $elements['header_actions'] = $header_actions; - // Add a weight element so we guaranty that header actions will stay in - // first row. We will use this later in - // paragraphs_preprocess_field_multiple_value_form(). - $elements['header_actions']['_weight'] = [ - '#type' => 'weight', - '#default_value' => -100, - ]; - } - - if (!empty($field_state['dragdrop'])) { - $elements['#attached']['library'][] = 'paragraphs/paragraphs-dragdrop'; - //$elements['dragdrop_mode']['#button_type'] = 'primary'; - $elements['dragdrop'] = $this->buildNestedParagraphsFoDragDrop($form_state, NULL, []); - return $elements; - } - - if ($max > 0) { - for ($delta = 0; $delta < $max; $delta++) { - - // Add a new empty item if it doesn't exist yet at this delta. - if (!isset($items[$delta])) { - $items->appendItem(); - } - - // For multiple fields, title and description are handled by the wrapping - // table. - $element = array( - '#title' => $is_multiple ? '' : $field_title, - '#description' => $is_multiple ? '' : $description, - ); - $element = $this->formSingleElement($items, $delta, $element, $form, $form_state); - - if ($element) { - // Input field for the delta (drag-n-drop reordering). - if ($is_multiple) { - // We name the element '_weight' to avoid clashing with elements - // defined by widget. - $element['_weight'] = array( - '#type' => 'weight', - '#title' => $this->t('Weight for row @number', array('@number' => $delta + 1)), - '#title_display' => 'invisible', - // Note: this 'delta' is the FAPI #type 'weight' element's property. - '#delta' => $max, - '#default_value' => $items[$delta]->_weight ?: $delta, - '#weight' => 100, - ); - } - - // Access for the top element is set to FALSE only when the paragraph - // was removed. A paragraphs that a user can not edit has access on - // lower level. - if (isset($element['#access']) && !$element['#access']) { - $this->realItemCount--; - } - else { - $elements[$delta] = $element; - } - } - } - } - - $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state); - $field_state['real_item_count'] = $this->realItemCount; - $field_state['add_mode'] = $this->getSetting('add_mode'); - static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state); - - $elements += [ - '#element_validate' => [[$this, 'multipleElementValidate']], - '#required' => $this->fieldDefinition->isRequired(), - '#field_name' => $field_name, - '#cardinality' => $cardinality, - '#max_delta' => $max - 1, - ]; - - if ($this->realItemCount > 0) { - $elements += array( - '#theme' => 'field_multiple_value_form', - '#cardinality_multiple' => $is_multiple, - '#title' => $field_title, - '#description' => $description, - ); - - } - else { - $classes = $this->fieldDefinition->isRequired() ? ['form-required'] : []; - $elements += [ - '#type' => 'container', - '#theme_wrappers' => ['container'], - '#cardinality_multiple' => TRUE, - 'title' => [ - '#type' => 'html_tag', - '#tag' => 'strong', - '#value' => $field_title, - '#attributes' => ['class' => $classes], - ], - 'text' => [ - '#type' => 'container', - 'value' => [ - '#markup' => $this->t('No @title added yet.', ['@title' => $this->getSetting('title')]), - '#prefix' => '<em>', - '#suffix' => '</em>', - ] - ], - ]; - - if ($description) { - $elements['description'] = [ - '#type' => 'container', - 'value' => ['#markup' => $description], - '#attributes' => ['class' => ['description']], - ]; - } - } - - $host = $items->getEntity(); - $this->initIsTranslating($form_state, $host); - - if (($this->realItemCount < $cardinality || $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) && !$form_state->isProgrammed() && !$this->isTranslating) { - $elements['add_more'] = $this->buildAddActions(); - } - - $elements['#attached']['library'][] = 'paragraphs/drupal.paragraphs.widget'; - - return $elements; - } - - /** - * {@inheritdoc} - */ - public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) { - $parents = $form['#parents']; - - // Identify the manage field settings default value form. - if (in_array('default_value_input', $parents, TRUE)) { - // Since the entity is not reusable neither cloneable, having a default - // value is not supported. - return ['#markup' => $this->t('No widget available for: %label.', ['%label' => $items->getFieldDefinition()->getLabel()])]; - } - - return parent::form($items, $form, $form_state, $get_delta); - } - - /** - * Returns a list of child paragraphs for a given field to loop over. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state. - * @param string $field_name - * The field name for which to find child paragraphs. - * @param \Drupal\paragraphs\ParagraphInterface $paragraph - * The current paragraph. - * @param array $array_parents - * The current field parent structure. - * - * @return \Drupal\paragraphs\Entity\Paragraph[] - * Child paragraphs. - */ - protected function getChildParagraphs(FormStateInterface $form_state, $field_name, ParagraphInterface $paragraph = NULL, array $array_parents = []) { - - // Convert the parents structure which only includes field names and delta - // to the full storage array key which includes a prefix and a subform. - $full_parents_key = ['field_storage', '#parents']; - foreach ($array_parents as $i => $parent) { - $full_parents_key[] = $parent; - if ($i % 2) { - $full_parents_key[] = 'subform'; - } - } - - $current_parents = array_merge($full_parents_key, ['#fields', $field_name]); - $child_field_state = NestedArray::getValue($form_state->getStorage(), $current_parents); - $entities = []; - if ($child_field_state && isset($child_field_state['paragraphs'])) { - // Fetch the paragraphs from the field state. Use the original delta - // to get the right position. Also reorder the paragraphs in the widget - // state accordingly. - $new_widget_paragraphs = []; - foreach ($child_field_state['paragraphs'] as $child_delta => $child_field_item_state) { - $entities[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state['entity']; - $new_widget_paragraphs[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state; - } - ksort($entities); - - // Set the orderd paragraphs into the widget state and reset original - // deltas. - ksort($new_widget_paragraphs); - $child_field_state['paragraphs'] = $new_widget_paragraphs; - $child_field_state['original_deltas'] = range(0, count($child_field_state['paragraphs']) - 1); - NestedArray::setValue($form_state->getStorage(), $current_parents, $child_field_state); - } - elseif ($paragraph) { - // If there is no field state, return the paragraphs directly from the - // entity. - foreach ($paragraph->get($field_name) as $child_delta => $item) { - if ($item->entity) { - $entities[$child_delta] = $item->entity; - } - } - } - - return $entities; - } - - /** - * Builds the nested drag and drop structure. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state. - * @param \Drupal\paragraphs\ParagraphInterface|null $paragraph - * The parent paragraph, NULL for the initial call. - * @param string[] $array_parents - * The array parents for nested paragraphs. - * - * @return array - * The built form structure. - */ - protected function buildNestedParagraphsFoDragDrop(FormStateInterface $form_state, ParagraphInterface $paragraph = NULL, array $array_parents = []) { - // Look for nested elements. - $elements = []; - $field_definitions = []; - if ($paragraph) { - foreach ($paragraph->getFieldDefinitions() as $child_field_name => $field_definition) { - /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ - if ($field_definition->getType() == 'entity_reference_revisions' && $field_definition->getSetting('target_type') == 'paragraph') { - $field_definitions[$child_field_name] = $field_definition; - } - } - } - else { - $field_definitions = [$this->fieldDefinition->getName() => $this->fieldDefinition]; - } - - /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ - foreach ($field_definitions as $child_field_name => $field_definition) { - $child_path = implode('][', array_merge($array_parents, [$child_field_name])); - $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality(); - $allowed_types = implode(array_keys($this->getAllowedTypes($field_definition)), ','); - $elements[$child_field_name] = [ - '#type' => 'container', - '#attributes' => ['class' => ['paragraphs-dragdrop-wrapper']], - ]; - - // Only show a field label if there is more than one paragraph field. - $label = count($field_definitions) > 1 || !$paragraph ? '<label><strong>' . $field_definition->getLabel() . '</strong></label>' : ''; - - $elements[$child_field_name]['list'] = [ - '#type' => 'markup', - '#prefix' => $label . '<ul class="paragraphs-dragdrop" data-paragraphs-dragdrop-cardinality="' . $cardinality . '" data-paragraphs-dragdrop-allowed-types="' . $allowed_types . '" data-paragraphs-dragdrop-path="' . $child_path . '">', - '#suffix' => '</ul>', - ]; - - /** @var \Drupal\paragraphs\Entity\Paragraph $child_paragraph */ - foreach ($this->getChildParagraphs($form_state, $child_field_name, $paragraph, $array_parents) as $child_delta => $child_paragraph) { - $element = []; - $element['top'] = [ - '#type' => 'container', - '#attributes' => ['class' => ['paragraphs-summary-wrapper']], - ]; - $element['top']['paragraph_summary']['type'] = [ - '#markup' => '<strong>' . $child_paragraph->getParagraphType()->label() . '</strong>', - ]; - - // We name the element '_weight' to avoid clashing with elements - // defined by widget. - $element['_weight'] = array( - '#type' => 'hidden', - '#default_value' => $child_delta, - '#attributes' => [ - 'class' => ['paragraphs-dragdrop__weight'], - ] - ); - - $element['_path'] = [ - '#type' => 'hidden', - '#title' => $this->t('Current path for @number', ['@number' => $delta = 1]), - '#title_display' => 'invisible', - '#default_value' => $child_path, - '#attributes' => [ - 'class' => ['paragraphs-dragdrop__path'], - ] - ]; - - $summary_options = []; - - $element['#prefix'] = '<li data-paragraphs-dragdrop-bundle="' . $child_paragraph->bundle() . '"><a href="#" class="tabledrag-handle"><div class="handle"> </div></a>'; - $element['#suffix'] = '</li>'; - $child_array_parents = array_merge($array_parents, [$child_field_name, $child_delta]); - - if ($child_elements = $this->buildNestedParagraphsFoDragDrop($form_state, $child_paragraph, $child_array_parents)) { - $element['dragdrop'] = $child_elements; - - // Set the depth limit to 0 to avoid displaying a summary for the - // children. - $summary_options['depth_limit'] = 0; - } - - $element['top']['summary']['fields_info'] = [ - '#markup' => $child_paragraph->getSummary($summary_options), - '#prefix' => '<div class="paragraphs-collapsed-description">', - '#suffix' => '</div>', - ]; - - $elements[$child_field_name]['list'][$child_delta] = $element; - } - } - return $elements; - } - - /** - * Add 'add more' button, if not working with a programmed form. - * - * @return array - * The form element array. - */ - protected function buildAddActions() { - if (count($this->getAccessibleOptions()) === 0) { - if (count($this->getAllowedTypes()) === 0) { - $add_more_elements['info'] = $this->createMessage($this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')])); - } - else { - $add_more_elements['info'] = $this->createMessage($this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')])); - } - - return $add_more_elements; - } - - if (in_array($this->getSetting('add_mode'), ['button', 'dropdown', 'modal'])) { - return $this->buildButtonsAddMode(); - } - - return $this->buildSelectAddMode(); - } - - /** - * Returns the available paragraphs type. - * - * @return array - * Available paragraphs types. - */ - protected function getAccessibleOptions() { - if ($this->accessOptions !== NULL) { - return $this->accessOptions; - } - - $entity_type_manager = \Drupal::entityTypeManager(); - $target_type = $this->getFieldSetting('target_type'); - $bundles = $this->getAllowedTypes(); - $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type); - $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop'); - - foreach ($bundles as $machine_name => $bundle) { - if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles')) - || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) { - if ($access_control_handler->createAccess($machine_name)) { - $this->accessOptions[$machine_name] = $bundle['label']; - } - } - } - - return $this->accessOptions; - } - - /** - * Helper to create a paragraph UI message. - * - * @param string $message - * Message text. - * @param string $type - * Message type. - * - * @return array - * Render array of message. - */ - public function createMessage($message, $type = 'warning') { - return [ - '#type' => 'container', - '#markup' => $message, - '#attributes' => ['class' => ['messages', 'messages--' . $type]], - ]; - } - - /** - * Expand button base array into a paragraph widget action button. - * - * @param array $button_base - * Button base render array. - * - * @return array - * Button render array. - */ - public static function expandButton(array $button_base) { - // Do not expand elements that do not have submit handler. - if (empty($button_base['#submit'])) { - return $button_base; - } - - $button = $button_base + [ - '#type' => 'submit', - '#theme_wrappers' => ['input__submit__paragraph_action'], - ]; - - // Html::getId will give us '-' char in name but we want '_' for now so - // we use strtr to search&replace '-' to '_'. - $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_'); - $button['#id'] = Html::getUniqueId($button['#name']); - - if (isset($button['#ajax'])) { - $button['#ajax'] += [ - 'effect' => 'fade', - // Since a normal throbber is added inline, this has the potential to - // break a layout if the button is located in dropbuttons. Instead, - // it's safer to just show the fullscreen progress element instead. - 'progress' => ['type' => 'fullscreen'], - ]; - } - - return $button; - } - - /** - * Get common submit element information for processing ajax submit handlers. - * - * @param array $form - * Form array. - * @param FormStateInterface $form_state - * Form state object. - * @param int $position - * Position of triggering element. - * - * @return array - * Submit element information. - */ - public static function getSubmitElementInfo(array $form, FormStateInterface $form_state, $position = ParagraphsWidget::ACTION_POSITION_BASE) { - $submit['button'] = $form_state->getTriggeringElement(); - - // Go up in the form, to the widgets container. - if ($position == ParagraphsWidget::ACTION_POSITION_BASE) { - $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -2)); - } - if ($position == ParagraphsWidget::ACTION_POSITION_HEADER) { - $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -3)); - } - elseif ($position == ParagraphsWidget::ACTION_POSITION_ACTIONS) { - $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -5)); - $delta = array_slice($submit['button']['#array_parents'], -5, -4); - $submit['delta'] = $delta[0]; - } - - $submit['field_name'] = $submit['element']['#field_name']; - $submit['parents'] = $submit['element']['#field_parents']; - - // Get widget state. - $submit['widget_state'] = static::getWidgetState($submit['parents'], $submit['field_name'], $form_state); - - return $submit; - } - - /** - * Build drop button. - * - * @param array $elements - * Elements for drop button. - * - * @return array - * Drop button array. - */ - protected function buildDropbutton(array $elements = []) { - $build = [ - '#type' => 'container', - '#attributes' => ['class' => ['paragraphs-dropbutton-wrapper']], - ]; - - $operations = []; - // Because we are cloning the elements into title sub element we need to - // sort children first. - foreach (Element::children($elements, TRUE) as $child) { - // Clone the element as an operation. - $operations[$child] = ['title' => $elements[$child]]; - - // Flag the original element as printed so it doesn't render twice. - $elements[$child]['#printed'] = TRUE; - } - - $build['operations'] = [ - '#type' => 'paragraph_operations', - // Even though operations are run through the "links" element type, the - // theme system will render any render array passed as a link "title". - '#links' => $operations, - ]; - - return $build + $elements; - } - - /** - * Builds dropdown button for adding new paragraph. - * - * @return array - * The form element array. - */ - protected function buildButtonsAddMode() { - $options = $this->getAccessibleOptions(); - $add_mode = $this->getSetting('add_mode'); - $paragraphs_type_storage = \Drupal::entityTypeManager()->getStorage('paragraphs_type'); - - // Build the buttons. - $add_more_elements = []; - foreach ($options as $machine_name => $label) { - $button_key = 'add_more_button_' . $machine_name; - $add_more_elements[$button_key] = $this->expandButton([ - '#type' => 'submit', - '#name' => $this->fieldIdPrefix . '_' . $machine_name . '_add_more', - '#value' => $add_mode == 'modal' ? $label : $this->t('Add @type', ['@type' => $label]), - '#attributes' => ['class' => ['field-add-more-submit']], - '#limit_validation_errors' => [array_merge($this->fieldParents, [$this->fieldDefinition->getName(), 'add_more'])], - '#submit' => [[get_class($this), 'addMoreSubmit']], - '#ajax' => [ - 'callback' => [get_class($this), 'addMoreAjax'], - 'wrapper' => $this->fieldWrapperId, - ], - '#bundle_machine_name' => $machine_name, - ]); - - if ($add_mode === 'modal' && $icon_url = $paragraphs_type_storage->load($machine_name)->getIconUrl()) { - $add_more_elements[$button_key]['#attributes']['style'] = 'background-image: url(' . $icon_url . ');'; - } - } - - // Determine if buttons should be rendered as dropbuttons. - if (count($options) > 1 && $add_mode == 'dropdown') { - $add_more_elements = $this->buildDropbutton($add_more_elements); - $add_more_elements['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]); - } - elseif ($add_mode == 'modal') { - $this->buildModalAddForm($add_more_elements); - $add_more_elements['add_modal_form_area']['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]); - } - $add_more_elements['#weight'] = 1; - - return $add_more_elements; - } - - /** - * Builds list of actions based on paragraphs type. - * - * @return array - * The form element array. - */ - protected function buildSelectAddMode() { - $field_name = $this->fieldDefinition->getName(); - $field_title = $this->fieldDefinition->getLabel(); - $setting_title = $this->getSetting('title'); - $add_more_elements['add_more_select'] = [ - '#type' => 'select', - '#options' => $this->getAccessibleOptions(), - '#title' => $this->t('@title type', ['@title' => $setting_title]), - '#label_display' => 'hidden', - ]; - - $text = $this->t('Add @title', ['@title' => $setting_title]); - - if ($this->realItemCount > 0) { - $text = $this->t('Add another @title', ['@title' => $setting_title]); - } - - $add_more_elements['add_more_button'] = [ - '#type' => 'submit', - '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more', - '#value' => $text, - '#attributes' => ['class' => ['field-add-more-submit']], - '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])], - '#submit' => [[get_class($this), 'addMoreSubmit']], - '#ajax' => [ - 'callback' => [get_class($this), 'addMoreAjax'], - 'wrapper' => $this->fieldWrapperId, - 'effect' => 'fade', - ], - ]; - - $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $field_title]); - return $add_more_elements; - } - - /** - * {@inheritdoc} - */ - public static function addMoreAjax(array $form, FormStateInterface $form_state) { - $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state); - $element = $submit['element']; - - // Add a DIV around the delta receiving the Ajax effect. - $delta = $submit['element']['#max_delta']; - $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : ''); - $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>'; - - return $element; - } - - /** - * Ajax callback for all actions. - */ - public static function allActionsAjax(array $form, FormStateInterface $form_state) { - $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER); - $element = $submit['element']; - - // Add a DIV around the delta receiving the Ajax effect. - $delta = $submit['element']['#max_delta']; - $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : ''); - $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>'; - - return $element; - } - - /** - * {@inheritdoc} - */ - public static function addMoreSubmit(array $form, FormStateInterface $form_state) { - $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state); - - if ($submit['widget_state']['real_item_count'] < $submit['element']['#cardinality'] || $submit['element']['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { - $submit['widget_state']['items_count']++; - } - - if (isset($submit['button']['#bundle_machine_name'])) { - $submit['widget_state']['selected_bundle'] = $submit['button']['#bundle_machine_name']; - } - else { - $submit['widget_state']['selected_bundle'] = $submit['element']['add_more']['add_more_select']['#value']; - } - - $submit['widget_state'] = static::autocollapse($submit['widget_state']); - - static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']); - - $form_state->setRebuild(); - } - - /** - * Creates a duplicate of the paragraph entity. - */ - public static function duplicateSubmit(array $form, FormStateInterface $form_state) { - $button = $form_state->getTriggeringElement(); - // Go one level up in the form, to the widgets container. - $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -5)); - $field_name = $element['#field_name']; - $parents = $element['#field_parents']; - - // Inserting new element in the array. - $widget_state = static::getWidgetState($parents, $field_name, $form_state); - - // Map the button delta to the actual delta. - $original_button_delta = $button['#delta']; - $current_button_delta = array_search($button['#delta'], $widget_state['original_deltas']); - - $widget_state['items_count']++; - $widget_state['real_item_count']++; - - // Initialize the new original delta map with the new entry. - $new_original_deltas = [ - $current_button_delta + 1 => count($widget_state['original_deltas']), - ]; - - $user_input = NestedArray::getValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5)); - $user_input[count($widget_state['original_deltas'])]['_weight'] = $current_button_delta + 1; - - // Increase all original deltas bigger than the delta of the duplicated - // element by one. - foreach ($widget_state['original_deltas'] as $current_delta => $original_delta) { - $new_delta = $current_delta > $current_button_delta ? $current_delta + 1 : $current_delta; - $new_original_deltas[$new_delta] = $original_delta; - $user_input[$original_delta]['_weight'] = $new_delta; - } - $widget_state['original_deltas'] = $new_original_deltas; - /** @var \Drupal\Core\Entity\EntityInterface $entity */ - $entity = $widget_state['paragraphs'][$original_button_delta]['entity']; - - $widget_state = static::autocollapse($widget_state); - - // Check if the replicate module is enabled. - if (\Drupal::hasService('replicate.replicator')) { - $duplicate_entity = \Drupal::getContainer()->get('replicate.replicator')->replicateEntity($entity); - } - else { - $duplicate_entity = $entity->createDuplicate(); - } - // Create the duplicated paragraph and insert it below the original. - $widget_state['paragraphs'][] = [ - 'entity' => $duplicate_entity, - 'display' => $widget_state['paragraphs'][$original_button_delta]['display'], - 'mode' => 'edit', - ]; - - NestedArray::setValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5), $user_input); - static::setWidgetState($parents, $field_name, $form_state, $widget_state); - $form_state->setRebuild(); - } - - public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) { - $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS); - - $new_mode = $submit['button']['#paragraphs_mode']; - - if ($new_mode === 'edit') { - $submit['widget_state'] = static::autocollapse($submit['widget_state']); - } - - $submit['widget_state']['paragraphs'][$submit['delta']]['mode'] = $new_mode; - - if (!empty($submit['button']['#paragraphs_show_warning'])) { - $submit['widget_state']['paragraphs'][$submit['delta']]['show_warning'] = $submit['button']['#paragraphs_show_warning']; - } - - static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']); - - $form_state->setRebuild(); - } - - public static function itemAjax(array $form, FormStateInterface $form_state) { - $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS); - - $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : ''); - $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>'; - - return $submit['element']; - } - - /** - * Sets the form mode accordingly. - * - * @param array $form - * An associate array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public static function dragDropModeSubmit(array $form, FormStateInterface $form_state) { - $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER); - - if (empty($submit['widget_state']['dragdrop'])) { - $submit['widget_state']['dragdrop'] = TRUE; - } - else { - $submit['widget_state']['dragdrop'] = FALSE; - } - - // Make sure that flag that we already reordered is unset when the mode is - // switched. - unset($submit['widget_state']['reordered']); - - // Switch the form mode accordingly. - static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']); - - $form_state->setRebuild(); - } - - - /** - * Reorder paragraphs. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state. - * @param $field_values_parents - * The field value parents. - */ - protected static function reorderParagraphs(FormStateInterface $form_state, $field_values_parents) { - $field_name = end($field_values_parents); - $field_values = NestedArray::getValue($form_state->getValues(), $field_values_parents); - $complete_field_storage = NestedArray::getValue( - $form_state->getStorage(), [ - 'field_storage', - '#parents' - ] - ); - $new_field_storage = $complete_field_storage; - - // Set a flag to prevent this from running twice, as the entity is built - // for validation as well as saving and would fail the second time as we - // already altered the field storage. - if (!empty($new_field_storage['#fields'][$field_name]['reordered'])) { - return; - } - $new_field_storage['#fields'][$field_name]['reordered'] = TRUE; - - // Clear out all current paragraphs keys in all nested paragraph widgets - // as there might be fewer than before or none in a certain widget. - $clear_paragraphs = function ($field_storage) use (&$clear_paragraphs) { - foreach ($field_storage as $key => $value) { - if ($key === '#fields') { - foreach ($value as $field_name => $widget_state) { - if (isset($widget_state['paragraphs'])) { - $field_storage['#fields'][$field_name]['paragraphs'] = []; - } - } - } - else { - $field_storage[$key] = $clear_paragraphs($field_storage[$key]); - } - } - return $field_storage; - }; - - // Only clear the current field and its children to avoid deleting - // paragraph references in other fields. - $new_field_storage['#fields'][$field_name]['paragraphs'] = []; - if (isset($new_field_storage[$field_name])) { - $new_field_storage[$field_name] = $clear_paragraphs($new_field_storage[$field_name]); - } - - $reorder_paragraphs = function ($reorder_values, $parents = [], FieldableEntityInterface $parent_entity = NULL) use ($complete_field_storage, &$new_field_storage, &$reorder_paragraphs) { - foreach ($reorder_values as $field_name => $values) { - foreach ($values['list'] as $delta => $item_values) { - $old_keys = array_merge( - $parents, [ - '#fields', - $field_name, - 'paragraphs', - $delta - ] - ); - $path = explode('][', $item_values['_path']); - $new_field_name = array_pop($path); - $key_parents = []; - foreach ($path as $i => $key) { - $key_parents[] = $key; - if ($i % 2 == 1) { - $key_parents[] = 'subform'; - } - } - $new_keys = array_merge( - $key_parents, [ - '#fields', - $new_field_name, - 'paragraphs', - $item_values['_weight'] - ] - ); - $key_exists = NULL; - $item_state = NestedArray::getValue($complete_field_storage, $old_keys, $key_exists); - if (!$key_exists && $parent_entity) { - // If key does not exist, then this parent widget was previously - // not expanded. This can only happen on nested levels. In that - // case, initialize a new item state and set the widget state to - // an empty array if it is not already set from an earlier item. - // If something else is placed there, it will be put in there, - // otherwise the widget will know that nothing is there anymore. - $item_state = [ - 'entity' => $parent_entity->get($field_name)->get($delta)->entity, - 'mode' => 'closed', - ]; - $widget_state_keys = array_slice($old_keys, 0, count($old_keys) - 2); - if (!NestedArray::getValue($new_field_storage, $widget_state_keys)) { - NestedArray::setValue($new_field_storage, $widget_state_keys, ['paragraphs' => []]); - } - } - - // Ensure the referenced paragraph will be saved. - $item_state['entity']->setNeedsSave(TRUE); - - NestedArray::setValue($new_field_storage, $new_keys, $item_state); - if (isset($item_values['dragdrop'])) { - $reorder_paragraphs( - $item_values['dragdrop'], array_merge( - $parents, [ - $field_name, - $delta, - 'subform' - ] - ), $item_state['entity'] - ); - } - } - } - }; - $reorder_paragraphs($field_values['dragdrop']); - - // Recalculate original deltas. - $recalculate_original_deltas = function ($field_storage, ContentEntityInterface $parent_entity) use (&$recalculate_original_deltas) { - if (isset($field_storage['#fields'])) { - foreach ($field_storage['#fields'] as $field_name => $widget_state) { - if (isset($widget_state['paragraphs'])) { - - // If the parent field does not exist but we have paragraphs in - // widget state, something went wrong and we have a mismatch. - // Throw an exception. - if (!$parent_entity->hasField($field_name) && !empty($widget_state['paragraphs'])) { - throw new \LogicException('Reordering paragraphs resulted in paragraphs on non-existing field ' . $field_name . ' on parent entity ' . $parent_entity->getEntityTypeId() . '/' . $parent_entity->id()); - } - - // Sort the paragraphs by key so that they will be assigned to - // the entity in the right order. Reset the deltas. - ksort($widget_state['paragraphs']); - $widget_state['paragraphs'] = array_values($widget_state['paragraphs']); - - $original_deltas = range(0, count($widget_state['paragraphs']) - 1); - $field_storage['#fields'][$field_name]['original_deltas'] = $original_deltas; - $field_storage['#fields'][$field_name]['items_count'] = count($widget_state['paragraphs']); - $field_storage['#fields'][$field_name]['real_item_count'] = count($widget_state['paragraphs']); - - // Update the parent entity and point to the new children, if the - // parent field does not exist, we also have no paragraphs, so - // we can just skip this, this is a dead leaf after re-ordering. - // @todo Clean this up somehow? - if ($parent_entity->hasField($field_name)) { - $parent_entity->set($field_name, array_column($widget_state['paragraphs'], 'entity')); - - // Next process that field recursively. - foreach (array_keys($widget_state['paragraphs']) as $delta) { - if (isset($field_storage[$field_name][$delta]['subform'])) { - $field_storage[$field_name][$delta]['subform'] = $recalculate_original_deltas($field_storage[$field_name][$delta]['subform'], $parent_entity->get($field_name)->get($delta)->entity); - } - } - } - - } - } - } - return $field_storage; - }; - - $parent_entity = $form_state->getFormObject()->getEntity(); - $new_field_storage = $recalculate_original_deltas($new_field_storage, $parent_entity); - - $form_state->set(['field_storage', '#parents'], $new_field_storage); - } - - /** - * Ajax callback for the dragdrop mode. - * - * @param array $form - * An associate array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The container form element. - */ - public static function dragDropModeAjax(array $form, FormStateInterface $form_state) { - $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER); - - $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : ''); - $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>'; - - return $submit['element']; - } - - /** - * Returns the value of a setting for the entity reference selection handler. - * - * @param string $setting_name - * The setting name. - * - * @return mixed - * The setting value. - */ - protected function getSelectionHandlerSetting($setting_name) { - $settings = $this->getFieldSetting('handler_settings'); - return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL; - } - - /** - * {@inheritdoc} - */ - public function elementValidate($element, FormStateInterface $form_state, $form) { - $field_name = $this->fieldDefinition->getName(); - $widget_state = static::getWidgetState($element['#field_parents'], $field_name, $form_state); - $delta = $element['#delta']; - - if (isset($widget_state['paragraphs'][$delta]['entity'])) { - /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */ - $entity = $widget_state['paragraphs'][$delta]['entity']; - - /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */ - $display = $widget_state['paragraphs'][$delta]['display']; - - if ($widget_state['paragraphs'][$delta]['mode'] == 'edit') { - // Extract the form values on submit for getting the current paragraph. - $display->extractFormValues($entity, $element['subform'], $form_state); - $display->validateFormValues($entity, $element['subform'], $form_state); - - // Validate all enabled behavior plugins. - $paragraphs_type = $entity->getParagraphType(); - if (\Drupal::currentUser()->hasPermission('edit behavior plugin settings')) { - foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) { - $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state); - $plugin_values->validateBehaviorForm($entity, $element['behavior_plugins'][$plugin_id], $subform_state); - } - } - } - } - - static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state); - } - - /** - * {@inheritdoc} - */ - public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { - $field_name = $this->fieldDefinition->getName(); - - $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state); - - // In dragdrop mode, validation errors can not be mapped to form elements, - // add them on the top level widget element. - if (!empty($field_state['dragdrop'])) { - if ($violations->count()) { - $element = NestedArray::getValue($form_state->getCompleteForm(), $field_state['array_parents']); - foreach ($violations as $violation) { - $form_state->setError($element, $violation->getMessage()); - } - } - } - else { - return parent::flagErrors($items, $violations, $form, $form_state); - } - } - - /** - * Special handling to validate form elements with multiple values. - * - * @param array $elements - * An associative array containing the substructure of the form to be - * validated in this call. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * @param array $form - * The complete form array. - */ - public function multipleElementValidate(array $elements, FormStateInterface $form_state, array $form) { - $field_name = $this->fieldDefinition->getName(); - $widget_state = static::getWidgetState($elements['#field_parents'], $field_name, $form_state); - - if ($elements['#required'] && $widget_state['real_item_count'] < 1) { - $form_state->setError($elements, t('@name field is required.', ['@name' => $this->fieldDefinition->getLabel()])); - } - - static::setWidgetState($elements['#field_parents'], $field_name, $form_state, $widget_state); - } - - /** - * {@inheritdoc} - */ - public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { - $field_name = $this->fieldDefinition->getName(); - $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state); - $element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']); - - if (!empty($widget_state['dragdrop'])) { - $path = array_merge($form['#parents'], array($field_name)); - static::reorderParagraphs($form_state, $path); - - // After re-ordering, get the updated widget state. - $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state); - - // Re-create values based on current widget state. - $values = []; - foreach ($widget_state['paragraphs'] as $delta => $paragraph_state) { - $values[$delta]['entity'] = $paragraph_state['entity']; - } - return $values; - } - - foreach ($values as $delta => &$item) { - if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity']) - && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') { - /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */ - $paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity']; - - /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */ - $display = $widget_state['paragraphs'][$item['_original_delta']]['display']; - if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') { - $display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state); - } - // A content entity form saves without any rebuild. It needs to set the - // language to update it in case of language change. - $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode'); - if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) { - // If a translation in the given language already exists, switch to - // that. If there is none yet, update the language. - if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) { - $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode')); - } - else { - $paragraphs_entity->set($langcode_key, $form_state->get('langcode')); - } - } - if (isset($item['behavior_plugins'])) { - // Submit all enabled behavior plugins. - $paragraphs_type = $paragraphs_entity->getParagraphType(); - foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) { - if (!isset($item['behavior_plugins'][$plugin_id])) { - $item['behavior_plugins'][$plugin_id] = []; - } - $original_delta = $item['_original_delta']; - if (isset($element[$original_delta]) && isset($element[$original_delta]['behavior_plugins'][$plugin_id]) && $form_state->getCompleteForm() && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) { - $subform_state = SubformState::createForSubform($element[$original_delta]['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state); - if (isset($item['behavior_plugins'][$plugin_id])) { - $plugin_values->submitBehaviorForm($paragraphs_entity, $item['behavior_plugins'][$plugin_id], $subform_state); - } - } - } - } - - $paragraphs_entity->setNeedsSave(TRUE); - $item['entity'] = $paragraphs_entity; - $item['target_id'] = $paragraphs_entity->id(); - $item['target_revision_id'] = $paragraphs_entity->getRevisionId(); - } - // If our mode is remove don't save or reference this entity. - // @todo: Maybe we should actually delete it here? - elseif (isset($widget_state['paragraphs'][$item['_original_delta']]['mode']) && $widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove') { - $item['target_id'] = NULL; - $item['target_revision_id'] = NULL; - } - } - return $values; - } - - /** - * {@inheritdoc} - */ - public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) { - // Filter possible empty items. - $items->filterEmptyItems(); - - // Remove buttons from header actions. - $field_name = $this->fieldDefinition->getName(); - $path = array_merge($form['#parents'], array($field_name)); - $form_state_variables = $form_state->getValues(); - $key_exists = NULL; - $values = NestedArray::getValue($form_state_variables, $path, $key_exists); - - if ($key_exists) { - unset($values['header_actions']); - - NestedArray::setValue($form_state_variables, $path, $values); - $form_state->setValues($form_state_variables); - } - - return parent::extractFormValues($items, $form, $form_state); - } - - /** - * Initializes the translation form state. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * @param \Drupal\Core\Entity\ContentEntityInterface $host - */ - protected function initIsTranslating(FormStateInterface $form_state, ContentEntityInterface $host) { - if ($this->isTranslating != NULL) { - return; - } - $this->isTranslating = FALSE; - if (!$host->isTranslatable()) { - return; - } - if (!$host->getEntityType()->hasKey('default_langcode')) { - return; - } - $default_langcode_key = $host->getEntityType()->getKey('default_langcode'); - if (!$host->hasField($default_langcode_key)) { - return; - } - - if (!empty($form_state->get('content_translation'))) { - // Adding a language through the ContentTranslationController. - $this->isTranslating = TRUE; - } - if ($host->hasTranslation($form_state->get('langcode')) && $host->getTranslation($form_state->get('langcode'))->get($default_langcode_key)->value == 0) { - // Editing a translation. - $this->isTranslating = TRUE; - } - } - - /** - * After-build callback for removing the translatability clue from the widget. - * - * If the fields on the paragraph type are translatable, - * ContentTranslationHandler::addTranslatabilityClue()adds an - * "(all languages)" suffix to the widget title. That suffix is incorrect and - * is being removed by this method using a #after_build on the field widget. - * - * @param array $element - * @param \Drupal\Core\Form\FormStateInterface $form_state - * - * @return array - */ - public static function removeTranslatabilityClue(array $element, FormStateInterface $form_state) { - // Widgets could have multiple elements with their own titles, so remove the - // suffix if it exists, do not recurse lower than this to avoid going into - // nested paragraphs or similar nested field types. - $suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>'; - if (isset($element['#title']) && strpos($element['#title'], $suffix)) { - $element['#title'] = str_replace($suffix, '', $element['#title']); - } - // Loop over all widget deltas. - foreach (Element::children($element) as $delta) { - if (isset($element[$delta]['#title']) && strpos($element[$delta]['#title'], $suffix)) { - $element[$delta]['#title'] = str_replace($suffix, '', $element[$delta]['#title']); - } - // Loop over all form elements within the current delta. - foreach (Element::children($element[$delta]) as $field) { - if (isset($element[$delta][$field]['#title']) && strpos($element[$delta][$field]['#title'], $suffix)) { - $element[$delta][$field]['#title'] = str_replace($suffix, '', $element[$delta][$field]['#title']); - } - } - } - return $element; - } - - /** - * Returns the default paragraph type. - * - * @return string - * Label name for default paragraph type. - */ - protected function getDefaultParagraphTypeLabelName() { - if ($this->getDefaultParagraphTypeMachineName() !== NULL) { - $allowed_types = $this->getAllowedTypes(); - return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label']; - } - - return NULL; - } - - /** - * Returns the machine name for default paragraph type. - * - * @return string - * Machine name for default paragraph type. - */ - protected function getDefaultParagraphTypeMachineName() { - $default_type = $this->getSetting('default_paragraph_type'); - $allowed_types = $this->getAllowedTypes(); - if ($default_type && isset($allowed_types[$default_type])) { - return $default_type; - } - // Check if the user explicitly selected not to have any default Paragraph - // type. Othewise, if there is only one type available, that one is the - // default. - if ($default_type === '_none') { - return NULL; - } - if (count($allowed_types) === 1) { - return key($allowed_types); - } - - return NULL; - } - - /** - * Counts the number of paragraphs in a certain mode in a form substructure. - * - * @param array $widget_state - * The widget state for the form substructure containing information about - * the paragraphs within. - * @param string $mode - * The mode to look for. - * - * @return int - * The number of paragraphs is the given mode. - */ - protected function getNumberOfParagraphsInMode(array $widget_state, $mode) { - if (!isset($widget_state['paragraphs'])) { - return 0; - } - - $paragraphs_count = 0; - foreach ($widget_state['paragraphs'] as $paragraph) { - if ($paragraph['mode'] == $mode) { - $paragraphs_count++; - } - } - - return $paragraphs_count; - } - - /** - * {@inheritdoc} - */ - public static function isApplicable(FieldDefinitionInterface $field_definition) { - $target_type = $field_definition->getSetting('target_type'); - $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type); - if ($paragraph_type) { - return $paragraph_type->entityClassImplements(ParagraphInterface::class); - } - - return FALSE; - } - - /** - * Builds header actions. - * - * @param array[] $field_state - * Field widget state. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Current form state. - * - * @return array[] - * The form element array. - */ - public function buildHeaderActions(array $field_state, FormStateInterface $form_state) { - $actions = []; - if (empty($this->fieldParents)) { - // Set actions. - $actions = [ - '#type' => 'paragraphs_actions', - ]; - - $field_name = $this->fieldDefinition->getName(); - $id_prefix = implode('-', array_merge($this->fieldParents, [$field_name])); - - // Only show the dragdrop mode if we can find the sortable library. - $library_discovery = \Drupal::service('library.discovery'); - $library = $library_discovery->getLibraryByName('paragraphs', 'paragraphs-dragdrop'); - if ($library || \Drupal::state()->get('paragraphs_test_dragdrop_force_show', FALSE)) { - $dragdrop_mode = $this->expandButton([ - '#type' => 'submit', - '#name' => $this->fieldIdPrefix . '_dragdrop_mode', - '#value' => !empty($field_state['dragdrop']) ? $this->t('Complete drag & drop') : $this->t('Drag & drop'), - '#attributes' => ['class' => ['field-dragdrop-mode-submit']], - '#submit' => [[get_class($this), 'dragDropModeSubmit']], - '#weight' => 8, - '#ajax' => [ - 'callback' => [get_class($this), 'dragDropModeAjax'], - 'wrapper' => $this->fieldWrapperId, - ], - ]); - - // Make the complete button a primary button, limit validation errors - // only for enabling drag and drop mode. - if (!empty($field_state['dragdrop'])) { - $dragdrop_mode['#button_type'] = 'primary'; - $actions['actions']['dragdrop_mode'] = $dragdrop_mode; - } - else { - $dragdrop_mode['#limit_validation_errors'] = [ - array_merge($this->fieldParents, [$field_name, 'dragdrop_mode']), - ]; - $actions['dropdown_actions']['dragdrop_mode'] = $dragdrop_mode; - } - } - - if ($this->realItemCount > 1 && empty($field_state['dragdrop'])) { - - $collapse_all = $this->expandButton([ - '#type' => 'submit', - '#value' => $this->t('Collapse all'), - '#submit' => [[get_class($this), 'changeAllEditModeSubmit']], - '#name' => $id_prefix . '_collapse_all', - '#paragraphs_mode' => 'closed', - '#limit_validation_errors' => [ - array_merge($this->fieldParents, [$field_name, 'collapse_all']), - ], - '#ajax' => [ - 'callback' => [get_class($this), 'allActionsAjax'], - 'wrapper' => $this->fieldWrapperId, - ], - '#weight' => -1, - '#paragraphs_show_warning' => TRUE, - ]); - - $edit_all = $this->expandButton([ - '#type' => 'submit', - '#value' => $this->t('Edit all'), - '#submit' => [[get_class($this), 'changeAllEditModeSubmit']], - '#name' => $id_prefix . '_edit-all', - '#paragraphs_mode' => 'edit', - '#limit_validation_errors' => [], - '#ajax' => [ - 'callback' => [get_class($this), 'allActionsAjax'], - 'wrapper' => $this->fieldWrapperId, - ], - ]); - - if (isset($field_state['paragraphs'][0]['mode']) && $field_state['paragraphs'][0]['mode'] === 'closed') { - $edit_all['#attributes'] = [ - 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'], - 'title' => $this->t('Edit all'), - ]; - $edit_all['#title'] = $this->t('Edit All'); - $actions['actions']['edit_all'] = $edit_all; - $actions['dropdown_actions']['collapse_all'] = $collapse_all; - } - else { - $collapse_all['#attributes'] = [ - 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'], - 'title' => $this->t('Collapse all'), - ]; - $actions['actions']['collapse_all'] = $collapse_all; - $actions['dropdown_actions']['edit_all'] = $edit_all; - } - } - } - - // Add paragraphs_header flag which we use later in preprocessor to move - // header actions to table header. - if ($actions) { - $actions['#paragraphs_header'] = TRUE; - } - - return $actions; - } - - /** - * Loops through all paragraphs and change mode for each paragraph instance. - * - * @param array $form - * Current form state. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Current form state. - */ - public static function changeAllEditModeSubmit(array $form, FormStateInterface $form_state) { - $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER); - - // Change edit mode for each paragraph. - foreach ($submit['widget_state']['paragraphs'] as $delta => &$paragraph) { - if ($submit['widget_state']['paragraphs'][$delta]['mode'] !== 'remove') { - $submit['widget_state']['paragraphs'][$delta]['mode'] = $submit['button']['#paragraphs_mode']; - if (!empty($submit['button']['#paragraphs_show_warning'])) { - $submit['widget_state']['paragraphs'][$delta]['show_warning'] = $submit['button']['#paragraphs_show_warning']; - } - } - } - - // Disable autocollapse when editing all and enable it when closing all. - if ($submit['button']['#paragraphs_mode'] === 'edit') { - $submit['widget_state']['autocollapse'] = 'none'; - } - elseif ($submit['button']['#paragraphs_mode'] === 'closed') { - $submit['widget_state']['autocollapse'] = 'all'; - } - - static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']); - $form_state->setRebuild(); - } - - /** - * Returns a state with all paragraphs closed, if autocollapse is enabled. - * - * @param array $widget_state - * The current widget state. - * - * @return array - * The widget state altered by closing all paragraphs. - */ - public static function autocollapse(array $widget_state) { - if ($widget_state['real_item_count'] > 0 && $widget_state['autocollapse'] !== 'none') { - foreach ($widget_state['paragraphs'] as $delta => $value) { - if ($widget_state['paragraphs'][$delta]['mode'] === 'edit') { - $widget_state['paragraphs'][$delta]['mode'] = 'closed'; - } - } - } - - return $widget_state; - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAccessTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAccessTest.php deleted file mode 100644 index 234b2802b..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAccessTest.php +++ /dev/null @@ -1,145 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\user\RoleInterface; -use Drupal\user\Entity\Role; - -/** - * Tests the access check of paragraphs. - * - * @group paragraphs - */ -class ParagraphsAccessTest extends ParagraphsTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'image', - 'paragraphs_demo', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - } - - /** - * Tests the paragraph translation. - */ - public function testParagraphAccessCheck() { - $admin_user = [ - 'administer site configuration', - 'administer node display', - 'administer paragraph display', - 'create paragraphed_content_demo content', - 'edit any paragraphed_content_demo content', - ]; - $this->loginAsAdmin($admin_user); - - // Remove the "access content" for anonymous users. That results in - // anonymous users not being able to "view" the host entity. - /* @var Role $role */ - $role = \Drupal::entityTypeManager() - ->getStorage('user_role') - ->load(RoleInterface::ANONYMOUS_ID); - $role->revokePermission('access content'); - $role->save(); - - // Set field_images from demo to private file storage. - $edit = array( - 'settings[uri_scheme]' => 'private', - ); - $this->drupalPostForm('admin/structure/paragraphs_type/images/fields/paragraph.images.field_images_demo/storage', $edit, t('Save field settings')); - - // Set the form display to classic. - $form_display = EntityFormDisplay::load('node.paragraphed_content_demo.default') - ->setComponent('field_paragraphs_demo', ['type' => 'entity_reference_paragraphs']); - $form_display->save(); - - // Create a new demo node. - $this->drupalGet('node/add/paragraphed_content_demo'); - - // Add a new paragraphs images item. - $this->drupalPostForm(NULL, NULL, t('Add Images')); - - $images = $this->drupalGetTestFiles('image'); - - // Create a file, upload it. - file_unmanaged_copy($images[0]->uri, 'temporary://privateImage.jpg'); - $file_path = $this->container->get('file_system') - ->realpath('temporary://privateImage.jpg'); - - // Create a file, upload it. - file_unmanaged_copy($images[1]->uri, 'temporary://privateImage2.jpg'); - $file_path_2 = $this->container->get('file_system') - ->realpath('temporary://privateImage2.jpg'); - - $edit = array( - 'title[0][value]' => 'Security test node', - 'files[field_paragraphs_demo_0_subform_field_images_demo_0][]' => [$file_path, $file_path_2], - ); - - $this->drupalPostForm(NULL, $edit, t('Upload')); - $this->drupalPostForm(NULL, [], t('Preview')); - $img1_url = file_create_url(\Drupal::token()->replace('private://privateImage.jpg')); - $image_url = file_url_transform_relative($img1_url); - $this->assertRaw($image_url, 'Image was found in preview'); - $this->clickLink(t('Back to content editing')); - $this->drupalPostForm(NULL, [], t('Save')); - - $node = $this->drupalGetNodeByTitle('Security test node'); - - $this->drupalGet('node/' . $node->id()); - - // Check the text and image after publish. - $this->assertRaw($image_url, 'Image was found in content'); - - $this->drupalGet($image_url); - $this->assertResponse(200, 'Image could be downloaded'); - - // Logout to become anonymous. - $this->drupalLogout(); - - // @todo Requesting the same $img_url again triggers a caching problem on - // drupal.org test bot, thus we request a different file here. - $img_url = file_create_url(\Drupal::token()->replace('private://privateImage2.jpg')); - $image_url = file_url_transform_relative($img_url); - // Check the text and image after publish. Anonymous should not see content. - $this->assertNoRaw($image_url, 'Image was not found in content'); - - $this->drupalGet($image_url); - $this->assertResponse(403, 'Image could not be downloaded'); - - // Login as admin with no delete permissions. - $this->loginAsAdmin($admin_user); - // Create a new demo node. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Text')); - $this->assertText('Text'); - $edit = [ - 'title[0][value]' => 'delete_permissions', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Edit the node. - $this->clickLink(t('Edit')); - // Check the remove button is present. - $this->assertNotNull($this->xpath('//*[@name="field_paragraphs_demo_0_remove"]')); - // Delete the Paragraph and save. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_demo_0_remove'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_demo_0_confirm_remove'); - $this->drupalPostForm(NULL, [], t('Save')); - $node = $this->getNodeByTitle('delete_permissions'); - $this->assertUrl('node/' . $node->id()); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAddModesTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAddModesTest.php deleted file mode 100644 index d8f24c697..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAddModesTest.php +++ /dev/null @@ -1,227 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\paragraphs\Entity\ParagraphsType; - -/** - * Tests paragraphs add modes. - * - * @group paragraphs - */ -class ParagraphsAddModesTest extends ParagraphsTestBase { - - use FieldUiTestTrait; - - /** - * Tests that paragraphs field does not allow default values. - */ - public function testNoDefaultValue() { - $this->loginAsAdmin(); - $this->addParagraphedContentType('paragraphed_test', 'paragraphs_field', 'entity_reference_paragraphs'); - - // Edit the field. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields'); - $this->clickLink(t('Edit')); - - // Check that the current field does not allow to add default values. - $this->assertText('No widget available for: paragraphs_field.'); - $this->drupalPostForm(NULL, [], t('Save settings')); - $this->assertText('Saved paragraphs_field configuration.'); - $this->assertResponse(200); - } - - /** - * Tests the field creation when no Paragraphs types are available. - */ - public function testEmptyAllowedTypes() { - $this->loginAsAdmin(); - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - - // Edit the field and save when there are no Paragraphs types available. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields'); - $this->clickLink(t('Edit')); - $this->drupalPostForm(NULL, [], t('Save settings')); - $this->assertText('Saved paragraphs configuration.'); - } - - /** - * Tests the add drop down button. - */ - public function testDropDownMode() { - $this->loginAsAdmin(); - // Add two Paragraph types. - $this->addParagraphsType('btext'); - $this->addParagraphsType('dtext'); - - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - // Enter to the field config since the weight is set through the form. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $this->drupalPostForm(NULL, [], 'Save settings'); - - $this->setAddMode('paragraphed_test', 'paragraphs', 'dropdown'); - - $this->assertAddButtons(['Add btext', 'Add dtext']); - - $this->addParagraphsType('atext'); - $this->assertAddButtons(['Add btext', 'Add dtext', 'Add atext']); - - $this->setParagraphsTypeWeight('paragraphed_test', 'dtext', 2, 'paragraphs'); - $this->assertAddButtons(['Add dtext', 'Add btext', 'Add atext']); - - $this->setAllowedParagraphsTypes('paragraphed_test', ['dtext', 'atext'], TRUE, 'paragraphs'); - $this->assertAddButtons(['Add dtext', 'Add atext']); - - $this->setParagraphsTypeWeight('paragraphed_test', 'atext', 1, 'paragraphs'); - $this->assertAddButtons(['Add atext', 'Add dtext']); - - $this->setAllowedParagraphsTypes('paragraphed_test', ['atext', 'dtext', 'btext'], TRUE, 'paragraphs'); - $this->assertAddButtons(['Add atext', 'Add dtext', 'Add btext']); - } - - /** - * Tests the add select mode. - */ - public function testSelectMode() { - $this->loginAsAdmin(); - // Add two Paragraph types. - $this->addParagraphsType('btext'); - $this->addParagraphsType('dtext'); - - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - // Enter to the field config since the weight is set through the form. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $this->drupalPostForm(NULL, [], 'Save settings'); - - $this->setAddMode('paragraphed_test', 'paragraphs', 'select'); - - $this->assertSelectOptions(['btext', 'dtext'], 'paragraphs'); - - $this->addParagraphsType('atext'); - $this->assertSelectOptions(['btext', 'dtext', 'atext'], 'paragraphs'); - - $this->setParagraphsTypeWeight('paragraphed_test', 'dtext', 2, 'paragraphs'); - $this->assertSelectOptions(['dtext', 'btext', 'atext'], 'paragraphs'); - - $this->setAllowedParagraphsTypes('paragraphed_test', ['dtext', 'atext'], TRUE, 'paragraphs'); - $this->assertSelectOptions(['dtext', 'atext'], 'paragraphs'); - - $this->setParagraphsTypeWeight('paragraphed_test', 'atext', 1, 'paragraphs'); - $this->assertSelectOptions(['atext', 'dtext'], 'paragraphs'); - - $this->setAllowedParagraphsTypes('paragraphed_test', ['atext', 'dtext', 'btext'], TRUE, 'paragraphs'); - $this->assertSelectOptions(['atext', 'dtext', 'btext'], 'paragraphs'); - } - - /** - * Asserts order and quantity of add buttons. - * - * @param array $options - * Array of expected add buttons in its correct order. - */ - protected function assertAddButtons($options) { - $this->drupalGet('node/add/paragraphed_test'); - $buttons = $this->xpath('//input[@class="field-add-more-submit button js-form-submit form-submit"]'); - // Check if the buttons are in the same order as the given array. - foreach ($buttons as $key => $button) { - $this->assertEqual($button['value'], $options[$key]); - } - $this->assertTrue(count($buttons) == count($options), 'The amount of drop down options matches with the given array'); - } - - /** - * Asserts order and quantity of select add options. - * - * @param array $options - * Array of expected select options in its correct order. - * @param string $paragraphs_field - * Name of the paragraphs field to check. - */ - protected function assertSelectOptions($options, $paragraphs_field) { - $this->drupalGet('node/add/paragraphed_test'); - $buttons = $this->xpath('//*[@name="' . $paragraphs_field . '[add_more][add_more_select]"]/option'); - // Check if the options are in the same order as the given array. - foreach ($buttons as $key => $button) { - $this->assertEqual($button['value'], $options[$key]); - } - $this->assertTrue(count($buttons) == count($options), 'The amount of select options matches with the given array'); - $this->assertNotEqual($this->xpath('//*[@name="' . $paragraphs_field .'_add_more"]'), [], 'The add button is displayed'); - } - - /** - * Tests if setting for default paragraph type is working properly. - */ - public function testSettingDefaultParagraphType() { - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin([ - 'administer content types', - 'administer node form display', - 'edit any paragraphed_test content' - ]); - - // Add a Paragraphed test content. - $paragraphs_type_text_image = ParagraphsType::create([ - 'id' => 'text_image', - 'label' => 'Text + Image', - ]); - $paragraphs_type_text = ParagraphsType::create([ - 'id' => 'text', - 'label' => 'Text', - ]); - $paragraphs_type_text_image->save(); - $paragraphs_type_text->save(); - - $this->setDefaultParagraphType('paragraphed_test', 'paragraphs', 'paragraphs_settings_edit', 'text_image'); - - // Check if default paragraph type is showing. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('Text + Image'); - $this->removeDefaultParagraphType('paragraphed_test'); - - // Disable text_image as default paragraph type. - $this->setDefaultParagraphType('paragraphed_test', 'paragraphs', 'paragraphs_settings_edit', '_none'); - - // Check if is Text + Image is added as default paragraph type. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('No Paragraph added yet.'); - - // Check if default type is created only for new host - $this->setDefaultParagraphType('paragraphed_test', 'paragraphs', 'paragraphs_settings_edit', 'text_image'); - $this->removeDefaultParagraphType('paragraphed_test'); - $edit = ['title[0][value]' => 'New Host']; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/1/edit'); - $this->assertText('No Paragraph added yet.'); - } - - /** - * Tests the default paragraph type behavior for a field with a single type. - */ - public function testDefaultParagraphTypeWithSingleType() { - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin([ - 'administer content types', - 'administer node form display', - 'edit any paragraphed_test content' - ]); - - // Add a Paragraphed test content. - $paragraphs_type_text = ParagraphsType::create([ - 'id' => 'text', - 'label' => 'Text', - ]); - $paragraphs_type_text->save(); - - // Check that when only one paragraph type is allowed in a content type, - // one instance is automatically added in the 'Add content' dialogue. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertNoText('No Paragraph added yet.'); - - // Check that no paragraph type is automatically added, if the defaut - // setting was set to '- None -'. - $this->setDefaultParagraphType('paragraphed_test', 'paragraphs', 'paragraphs_settings_edit', '_none'); - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('No Paragraph added yet.'); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAdministrationTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAdministrationTest.php deleted file mode 100644 index 7d285867f..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsAdministrationTest.php +++ /dev/null @@ -1,573 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\paragraphs\Entity\Paragraph; - -/** - * Tests the configuration of paragraphs. - * - * @group paragraphs - */ -class ParagraphsAdministrationTest extends ParagraphsTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'image', - 'file', - 'views' - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create paragraphs content type. - $this->drupalCreateContentType(array('type' => 'paragraphs', 'name' => 'Paragraphs')); - } - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsRevisions() { - $this->addParagraphedContentType('article', 'paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin([ - 'create paragraphs content', - 'administer node display', - 'edit any paragraphs content', - 'administer nodes', - ]); - - // Create paragraphs type Headline + Block. - $this->addParagraphsType('text'); - // Create field types for the text. - static::fieldUIAddNewField('admin/structure/paragraphs_type/text', 'text', 'Text', 'text', array(), array()); - $this->assertText('Saved Text configuration.'); - - // Create an article with paragraphs field. - static::fieldUIAddNewField('admin/structure/types/manage/paragraphs', 'paragraphs', 'Paragraphs', 'entity_reference_revisions', array( - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ), array( - 'settings[handler_settings][target_bundles_drag_drop][text][enabled]' => TRUE, - )); - // Configure article fields. - $this->drupalGet('admin/structure/types/manage/paragraphs/fields'); - $this->clickLink(t('Manage form display')); - $this->drupalPostForm(NULL, array('fields[field_paragraphs][type]' => 'entity_reference_paragraphs'), t('Save')); - - // Create node with our paragraphs. - $this->drupalGet('node/add/paragraphs'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_add_more'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_add_more'); - $edit = [ - 'title[0][value]' => 'TEST TITEL', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Test text 1', - 'field_paragraphs[1][subform][field_text][0][value]' => 'Test text 2', - 'status[value]' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - $node = $this->drupalGetNodeByTitle('TEST TITEL'); - $paragraph1 = $node->field_paragraphs[0]->target_id; - $paragraph2 = $node->field_paragraphs[1]->target_id; - - $this->countRevisions($node, $paragraph1, $paragraph2, 1); - - // Edit the node without creating a revision. There should still be only 1 - // revision for nodes and paragraphs. - $edit = [ - 'field_paragraphs[0][subform][field_text][0][value]' => 'Foo Bar 1', - 'revision' => FALSE, - ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); - - $this->countRevisions($node, $paragraph1, $paragraph2, 1); - - // Edit the just created node. Create new revision. Now we should have 2 - // revisions for nodes and paragraphs. - $edit = [ - 'title[0][value]' => 'TEST TITLE', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Foo Bar 2', - 'revision' => TRUE, - ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); - - $this->countRevisions($node, $paragraph1, $paragraph2, 2); - - // Assert the paragraphs have been changed. - $this->assertNoText('Foo Bar 1'); - $this->assertText('Test text 2'); - $this->assertText('Foo Bar 2'); - $this->assertText('TEST TITLE'); - - // Check out the revisions page and assert there are 2 revisions. - $this->drupalGet('node/' . $node->id() . '/revisions'); - $rows = $this->xpath('//tbody/tr'); - // Make sure two revisions available. - $this->assertEqual(count($rows), 2); - // Revert to the old version. - $this->clickLink(t('Revert')); - $this->drupalPostForm(NULL, [], t('Revert')); - $this->drupalGet('node/' . $node->id()); - // Assert the node has been reverted. - $this->assertNoText('Foo Bar 2'); - $this->assertText('Test text 2'); - $this->assertText('Foo Bar 1'); - $this->assertText('TEST TITEL'); - } - - - /** - * Tests the paragraph creation. - */ - public function testParagraphsCreation() { - // Create an article with paragraphs field. - $this->addParagraphedContentType('article', 'field_paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin([ - 'administer site configuration', - 'create article content', - 'create paragraphs content', - 'administer node display', - 'administer paragraph display', - 'edit any article content', - 'delete any article content', - 'access files overview', - ]); - - // Assert suggested 'Add a paragraph type' link when there is no type yet. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->assertText('There is no Paragraphs type yet.'); - $this->drupalGet('admin/structure/types/manage/paragraphs/fields/add-field'); - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference_revisions:paragraph', - 'label' => 'Paragraph', - 'field_name' => 'paragraph', - ]; - $this->drupalPostForm(NULL, $edit, 'Save and continue'); - $this->drupalPostForm(NULL, [], 'Save field settings'); - $this->assertLinkByHref('admin/structure/paragraphs_type/add'); - $this->clickLink('here'); - $this->assertUrl('admin/structure/paragraphs_type/add'); - - $this->drupalGet('admin/structure/paragraphs_type'); - $this->clickLink(t('Add paragraph type')); - $this->assertTitle('Add Paragraphs type | Drupal'); - // Create paragraph type text + image. - $this->addParagraphsType('text_image'); - $this->drupalGet('admin/structure/paragraphs_type/text_image'); - $this->assertTitle('Edit text_image paragraph type | Drupal'); - // Create field types for text and image. - static::fieldUIAddNewField('admin/structure/paragraphs_type/text_image', 'text', 'Text', 'text_long', array(), array()); - $this->assertText('Saved Text configuration.'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/text_image', 'image', 'Image', 'image', array(), array('settings[alt_field_required]' => FALSE)); - $this->assertText('Saved Image configuration.'); - - // Create paragraph type Nested test. - $this->addParagraphsType('nested_test'); - - static::fieldUIAddNewField('admin/structure/paragraphs_type/nested_test', 'paragraphs', 'Paragraphs', 'entity_reference_revisions', array( - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ), array()); - - // Change the add more button to select mode. - $this->clickLink(t('Manage form display')); - $this->drupalPostAjaxForm(NULL, ['fields[field_paragraphs][type]' => 'entity_reference_paragraphs'], 'field_paragraphs_settings_edit'); - $this->drupalPostForm(NULL, ['fields[field_paragraphs][settings_edit_form][settings][add_mode]' => 'select'], t('Update')); - $this->drupalPostForm(NULL, [], t('Save')); - - // Create paragraph type image. - $this->addParagraphsType('image'); - // Create field types for image. - static::fieldUIAddNewField('admin/structure/paragraphs_type/image', 'image_only', 'Image only', 'image', array(), array()); - $this->assertText('Saved Image only configuration.'); - - $this->drupalGet('admin/structure/paragraphs_type'); - $rows = $this->xpath('//tbody/tr'); - // Make sure 2 types are available with their label. - $this->assertEqual(count($rows), 3); - $this->assertText('text_image'); - $this->assertText('image'); - // Make sure there is an edit link for each type. - $this->clickLink(t('Edit')); - // Make sure the field UI appears. - $this->assertLink('Manage fields'); - $this->assertLink('Manage form display'); - $this->assertLink('Manage display'); - $this->assertTitle('Edit image paragraph type | Drupal'); - - // Test for "Add mode" setting. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $field_name = 'field_paragraphs'; - - // Click on the widget settings button to open the widget settings form. - $this->drupalPostAjaxForm(NULL, array(), $field_name . "_settings_edit"); - - // Enable setting. - $edit = array('fields[' . $field_name . '][settings_edit_form][settings][add_mode]' => 'button'); - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Check if the setting is stored. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $this->assertText('Add mode: Buttons', 'Checking the settings value.'); - - $this->drupalPostAjaxForm(NULL, array(), $field_name . "_settings_edit"); - // Assert the 'Buttons' option is selected. - $this->assertOptionSelected('edit-fields-field-paragraphs-settings-edit-form-settings-add-mode', 'button', 'Updated value is correct!.'); - - // Add two Text + Image paragraphs in article. - $this->drupalGet('node/add/article'); - - // Checking changes on article. - $this->assertRaw('<div class="paragraphs-dropbutton-wrapper"><input', 'Updated value in article.'); - - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_image_add_more'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_image_add_more'); - // Create an 'image' file, upload it. - $text = 'Trust me I\'m an image'; - file_put_contents('temporary://myImage1.jpg', $text); - file_put_contents('temporary://myImage2.jpg', $text); - - $edit = array( - 'title[0][value]' => 'Test article', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Test text 1', - 'files[field_paragraphs_0_subform_field_image_0]' => drupal_realpath('temporary://myImage1.jpg'), - 'field_paragraphs[1][subform][field_text][0][value]' => 'Test text 2', - 'files[field_paragraphs_1_subform_field_image_0]' => drupal_realpath('temporary://myImage2.jpg'), - ); - $this->drupalPostForm(NULL, $edit, t('Save')); - - $node = $this->drupalGetNodeByTitle('Test article'); - $img1_url = file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/myImage1.jpg')); - $img2_url = file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/myImage2.jpg')); - - // Check the text and image after publish. - $this->assertText('Test text 1'); - $this->assertRaw('<img src="' . file_url_transform_relative($img1_url)); - $this->assertText('Test text 2'); - $this->assertRaw('<img src="' . file_url_transform_relative($img2_url)); - - // Tests for "Edit mode" settings. - // Test for closed setting. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - // Click on the widget settings button to open the widget settings form. - $this->drupalPostAjaxForm(NULL, array(), "field_paragraphs_settings_edit"); - // Enable setting. - $edit = array('fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'closed'); - $this->drupalPostForm(NULL, $edit, t('Save')); - // Check if the setting is stored. - $this->assertText('Edit mode: Closed', 'Checking the settings value.'); - $this->drupalPostAjaxForm(NULL, array(), "field_paragraphs_settings_edit"); - // Assert the 'Closed' option is selected. - $this->assertOptionSelected('edit-fields-field-paragraphs-settings-edit-form-settings-edit-mode', 'closed', 'Updated value correctly.'); - $this->drupalGet('node/1/edit'); - // The textareas for paragraphs should not be visible. - $this->assertNoRaw('field_paragraphs[0][subform][field_text][0][value]'); - $this->assertNoRaw('field_paragraphs[1][subform][field_text][0][value]'); - $this->assertRaw('<div class="paragraphs-collapsed-description">myImage1.jpg, Test text 1'); - $this->assertRaw('<div class="paragraphs-collapsed-description">myImage2.jpg, Test text 2'); - - // Test for preview option. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $this->drupalPostAjaxForm(NULL, array(), "field_paragraphs_settings_edit"); - $edit = array('fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'preview'); - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('Edit mode: Preview', 'Checking the settings value.'); - $this->drupalGet('node/1/edit'); - // The texts in the paragraphs should be visible. - $this->assertNoRaw('field_paragraphs[0][subform][field_text][0][value]'); - $this->assertNoRaw('field_paragraphs[1][subform][field_text][0][value]'); - $this->assertText('Test text 1'); - $this->assertText('Test text 2'); - - // Test for open option. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $this->drupalPostAjaxForm(NULL, array(), "field_paragraphs_settings_edit"); - // Assert the 'Preview' option is selected. - $this->assertOptionSelected('edit-fields-field-paragraphs-settings-edit-form-settings-edit-mode', 'preview', 'Updated value correctly.'); - // Restore the value to Open for next test. - $edit = array('fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'open'); - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/1/edit'); - // The textareas for paragraphs should be visible. - $this->assertRaw('field_paragraphs[0][subform][field_text][0][value]'); - $this->assertRaw('field_paragraphs[1][subform][field_text][0][value]'); - - $paragraphs = Paragraph::loadMultiple(); - $this->assertEqual(count($paragraphs), 2, 'Two paragraphs in article'); - - // Check article edit page. - $this->drupalGet('node/' . $node->id() . '/edit'); - // Check both paragraphs in edit page. - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', 'Test text 1'); - $this->assertRaw('<a href="' . $img1_url . '" type="image/jpeg; length=21">myImage1.jpg</a>'); - $this->assertFieldByName('field_paragraphs[1][subform][field_text][0][value]', 'Test text 2'); - $this->assertRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - // Remove 2nd paragraph. - $this->drupalPostForm(NULL, NULL, t('Remove')); - $this->assertNoField('field_paragraphs[1][subform][field_text][0][value]'); - $this->assertNoRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - // Restore it again. - $this->drupalPostForm(NULL, NULL, t('Restore')); - $this->assertFieldByName('field_paragraphs[1][subform][field_text][0][value]', 'Test text 2'); - $this->assertRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - // Remove the second paragraph. - $this->drupalPostForm(NULL, [], t('Remove')); - $this->assertNoRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - $edit = [ - 'field_paragraphs[0][subform][field_image][0][alt]' => 'test_alt', - 'field_paragraphs[0][subform][field_image][0][width]' => 300, - 'field_paragraphs[0][subform][field_image][0][height]' => 300, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Assert the paragraph is deleted after the user saves the node. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertNoRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - - // Delete the node. - $this->clickLink(t('Delete')); - $this->drupalPostForm(NULL, NULL, t('Delete')); - $this->assertText('Test article has been deleted.'); - - // Check if the publish/unpublish option works. - $this->drupalGet('admin/structure/paragraphs_type/text_image/form-display'); - $edit = [ - 'fields[status][type]' => 'boolean_checkbox', - 'fields[status][region]' => 'content', - ]; - - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/add/article'); - $this->drupalPostForm(NULL, NULL, t('Add text_image')); - $this->assertRaw('edit-field-paragraphs-0-subform-status-value'); - $edit = [ - 'title[0][value]' => 'Example publish/unpublish', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Example published and unpublished', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText(t('Example published and unpublished')); - $this->clickLink(t('Edit')); - $edit = [ - 'field_paragraphs[0][subform][status][value]' => FALSE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertNoText(t('Example published and unpublished')); - - // Set the fields as required. - $this->drupalGet('admin/structure/types/manage/article/fields'); - $this->clickLink('Edit', 1); - $this->drupalPostForm(NULL, ['preview_mode' => '1'], t('Save content type')); - $this->drupalGet('admin/structure/paragraphs_type/nested_test/fields'); - $this->clickLink('Edit'); - $this->drupalPostForm(NULL, ['required' => TRUE], t('Save settings')); - - // Add a new article. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_nested_test_add_more'); - $edit = [ - 'field_paragraphs[0][subform][field_paragraphs][add_more][add_more_select]' => 'image', - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_subform_field_paragraphs_add_more'); - // Test the new field is displayed. - $this->assertFieldByName('files[field_paragraphs_0_subform_field_paragraphs_0_subform_field_image_only_0]'); - - // Add an image to the required field. - $edit = array( - 'title[0][value]' => 'test required', - 'files[field_paragraphs_0_subform_field_paragraphs_0_subform_field_image_only_0]' => drupal_realpath('temporary://myImage2.jpg'), - ); - $this->drupalPostForm(NULL, $edit, t('Save')); - $edit = [ - 'field_paragraphs[0][subform][field_paragraphs][0][subform][field_image_only][0][width]' => 100, - 'field_paragraphs[0][subform][field_paragraphs][0][subform][field_image_only][0][height]' => 100, - 'field_paragraphs[0][subform][field_paragraphs][0][subform][field_image_only][0][alt]' => 'Alternative_text', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('test required has been created.'); - $this->assertNoRaw('This value should not be null.'); - - // Test that unsupported widgets are not displayed. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $select = $this->xpath('//*[@id="edit-fields-field-paragraphs-type"]')[0]; - $this->assertEqual(count($select->option), 2); - $this->assertRaw('value="entity_reference_paragraphs" selected="selected"'); - - // Check that Paragraphs is not displayed as an entity_reference field - // reference option. - $this->drupalGet('admin/structure/types/manage/article/fields/add-field'); - $edit = [ - 'new_storage_type' => 'entity_reference', - 'label' => 'unsupported field', - 'field_name' => 'unsupportedfield', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and continue')); - $this->assertNoOption('edit-settings-target-type', 'paragraph'); - - // Test that all Paragraph types can be referenced if none is selected. - $this->addParagraphsType('nested_double_test'); - static::fieldUIAddExistingField('admin/structure/paragraphs_type/nested_double_test', 'field_paragraphs', 'paragraphs_1'); - $this->clickLink(t('Manage form display')); - $this->drupalPostForm(NULL, [], 'Save'); - //$this->drupalPostForm(NULL, array('fields[field_paragraphs][type]' => 'entity_reference_revisions_entity_view'), t('Save')); - static::fieldUIAddNewField('admin/structure/paragraphs_type/nested_double_test', 'paragraphs_2', 'paragraphs_2', 'entity_reference_revisions', array( - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ), array()); - $this->clickLink(t('Manage form display')); - $this->drupalPostForm(NULL, [], 'Save'); - $this->drupalPostAjaxForm('node/add/article', [], 'field_paragraphs_nested_test_add_more'); - $edit = [ - 'field_paragraphs[0][subform][field_paragraphs][add_more][add_more_select]' => 'nested_double_test', - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_subform_field_paragraphs_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_subform_field_paragraphs_0_subform_field_paragraphs_image_add_more'); - $edit = array( - 'title[0][value]' => 'Nested twins', - ); - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('Nested twins has been created.'); - $this->assertNoText('This entity (paragraph: ) cannot be referenced.'); - - // Set the fields as not required. - $this->drupalGet('admin/structure/types/manage/article/fields'); - $this->clickLink('Edit', 1); - $this->drupalPostForm(NULL, ['required' => FALSE], t('Save settings')); - - // Set the Paragraph field edit mode to 'Closed'. - $this->drupalPostAjaxForm('admin/structure/types/manage/article/form-display', [], 'field_paragraphs_settings_edit'); - $this->drupalPostForm(NULL, ['fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'closed'], t('Update')); - $this->drupalPostForm(NULL, [], t('Save')); - - $this->addParagraphsType('node_test'); - - // Add a required node reference field. - static::fieldUIAddNewField('admin/structure/paragraphs_type/node_test', 'entity_reference', 'Entity reference', 'entity_reference', array( - 'settings[target_type]' => 'node', - 'cardinality' => '-1' - ), [ - 'settings[handler_settings][target_bundles][article]' => TRUE, - 'required' => TRUE, - ]); - $node = $this->drupalGetNodeByTitle('Nested twins'); - - // Create a node with a reference in a Paragraph. - $this->drupalPostAjaxForm('node/add/article', [], 'field_paragraphs_node_test_add_more'); - \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); - $edit = [ - 'field_paragraphs[0][subform][field_entity_reference][0][target_id]' => $node->label() . ' (' . $node->id() . ')', - 'title[0][value]' => 'choke test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Delete the referenced node. - $node->delete(); - // Edit the node with the reference. - $this->clickLink(t('Edit')); - // Since we have validation error (reference to deleted node), paragraph is - // by default in edit mode. - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][0][target_id]'); - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][1][target_id]'); - // Assert the validation error message. - $this->assertText('The referenced entity (node: 4) does not exist'); - // Triggering unrelated button, assert that error message is still present. - $this->drupalPostForm(NULL, [], t('Add another item')); - $this->assertText('The referenced entity (node: 4) does not exist'); - $this->assertText('Entity reference (value 1) field is required.'); - // Try to collapse with an invalid reference. - $this->drupalPostAjaxForm(NULL, ['field_paragraphs[0][subform][field_entity_reference][0][target_id]' => 'foo'], 'field_paragraphs_0_collapse'); - // Paragraph should be still in edit mode. - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][0][target_id]'); - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][1][target_id]'); - $this->drupalPostForm(NULL, [], t('Add another item')); - // Assert the validation message. - $this->assertText('There are no entities matching "foo".'); - // Attempt to remove the Paragraph. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_remove'); - $elements = $this->xpath('//*[@name="field_paragraphs_0_confirm_remove"]'); - $this->assertTrue(!empty($elements), "'Confirm removal' button appears."); - // Restore the Paragraph and fix the broken reference. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_restore'); - $node = $this->drupalGetNodeByTitle('Example publish/unpublish'); - $edit = ['field_paragraphs[0][subform][field_entity_reference][0][target_id]' => $node->label() . ' (' . $node->id() . ')']; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('choke test has been updated.'); - $this->assertLink('Example publish/unpublish'); - // Delete the new referenced node. - $node->delete(); - - // Set the Paragraph field edit mode to 'Preview'. - $this->drupalPostAjaxForm('admin/structure/types/manage/article/form-display', [], 'field_paragraphs_settings_edit'); - $this->drupalPostForm(NULL, ['fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'preview'], t('Update')); - $this->drupalPostForm(NULL, [], t('Save')); - - $node = $this->drupalGetNodeByTitle('choke test'); - // Attempt to edit the Paragraph. - $this->drupalPostAjaxForm('node/' . $node->id() . '/edit', [], 'field_paragraphs_0_edit'); - // Try to save with an invalid reference. - $edit = ['field_paragraphs[0][subform][field_entity_reference][0][target_id]' => 'foo']; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('There are no entities matching "foo".'); - // Remove the Paragraph and save the node. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_remove'); - $elements = $this->xpath('//*[@name="field_paragraphs_0_confirm_remove"]'); - $this->assertTrue(!empty($elements), "'Confirm removal' button appears."); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_confirm_remove'); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('choke test has been updated.'); - - // Verify that the text displayed is correct when no paragraph has been - // added yet. - $this->drupalGet('node/add/article'); - $this->assertText('No Paragraph added yet.'); - - $this->drupalGet('admin/content/files'); - $this->clickLink('1 place'); - $label = $this->xpath('//tbody/tr/td[1]'); - $this->assertEqual(trim(htmlspecialchars_decode(strip_tags($label[0]->asXML()))), 'test required > field_paragraphs > Paragraphs'); - } - - /** - * Asserts that a select option in the current page is checked. - * - * @param string $id - * ID of select field to assert. - * @param string $option - * Option to assert. - * @param string $message - * (optional) A message to display with the assertion. Do not translate - * messages: use format_string() to embed variables in the message text, not - * t(). If left blank, a default message will be displayed. - * @param string $group - * (optional) The group this message is in, which is displayed in a column - * in test output. Use 'Debug' to indicate this is debugging output. Do not - * translate this string. Defaults to 'Browser'; most tests do not override - * this default. - * - * @return bool - * TRUE on pass, FALSE on fail. - * - * @todo Remove function once core issue is resolved: https://www.drupal.org/node/2530092 - */ - protected function assertOptionSelected($id, $option, $message = '', $group = 'Browser') { - $elements = $this->xpath('//select[contains(@id, :id)]//option[@value=:option]', array(':id' => $id, ':option' => $option)); - return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['selected']), $message ? $message : SafeMarkup::format('Option @option for field @id is selected.', array('@option' => $option, '@id' => $id)), $group); - } - - /** - * Helper function for revision counting. - */ - private function countRevisions($node, $paragraph1, $paragraph2, $revisions_count) { - $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); - $this->assertEqual($node_revisions_count, $revisions_count); - $this->assertEqual(\Drupal::entityQuery('paragraph')->condition('id', $paragraph1)->allRevisions()->count()->execute(), $revisions_count); - $this->assertEqual(\Drupal::entityQuery('paragraph')->condition('id', $paragraph2)->allRevisions()->count()->execute(), $revisions_count); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsConfigTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsConfigTest.php deleted file mode 100644 index 64945433e..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsConfigTest.php +++ /dev/null @@ -1,240 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\node\Entity\NodeType; - -/** - * Tests paragraphs configuration. - * - * @group paragraphs - */ -class ParagraphsConfigTest extends ParagraphsTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'content_translation', - ); - - /** - * Tests adding paragraphs with no translation enabled. - */ - public function testFieldTranslationDisabled() { - $this->loginAsAdmin([ - 'administer languages', - 'administer content translation', - 'create content translations', - 'translate any entity', - ]); - - // Add a paragraphed content type. - $this->addParagraphedContentType('paragraphed_test', 'paragraphs_field', 'entity_reference_paragraphs'); - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - // Add a second language. - ConfigurableLanguage::create(['id' => 'de'])->save(); - - // Enable translation for paragraphed content type. Do not enable - // translation for the ERR paragraphs field nor for fields on the - // paragraph type. - $edit = [ - 'entity_types[node]' => TRUE, - 'settings[node][paragraphed_test][translatable]' => TRUE, - 'settings[node][paragraphed_test][fields][paragraphs_field]' => FALSE, - ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); - - // Create a node with a paragraph. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'paragraphs_field_paragraph_type_test_add_more'); - $edit = ['title[0][value]' => 'paragraphed_title']; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Attempt to add a translation. - $node = $this->drupalGetNodeByTitle('paragraphed_title'); - $this->drupalGet('node/' . $node->id() . '/translations'); - $this->clickLink(t('Add')); - // Save the translation. - $this->drupalPostForm(NULL, [], t('Save (this translation)')); - $this->assertText('paragraphed_test paragraphed_title has been updated.'); - } - - /** - * Tests content translation form translatability constraints messages. - */ - public function testContentTranslationForm() { - $this->loginAsAdmin([ - 'administer languages', - 'administer content translation', - 'create content translations', - 'translate any entity', - ]); - - // Check warning message is displayed. - $this->drupalGet('admin/config/regional/content-language'); - $this->assertText('(* unsupported) Paragraphs fields do not support translation.'); - - $this->addParagraphedContentType('paragraphed_test', 'paragraphs_field', 'entity_reference_paragraphs'); - - // Check error message is not displayed. - $this->drupalGet('admin/config/regional/content-language'); - $this->assertText('(* unsupported) Paragraphs fields do not support translation.'); - $this->assertNoRaw('<div class="messages messages--error'); - - // Add a second language. - ConfigurableLanguage::create(['id' => 'de'])->save(); - - // Enable translation for paragraphed content type. - $edit = [ - 'entity_types[node]' => TRUE, - 'settings[node][paragraphed_test][translatable]' => TRUE, - 'settings[node][paragraphed_test][fields][paragraphs_field]' => FALSE, - ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); - - // Check error message is still not displayed. - $this->drupalGet('admin/config/regional/content-language'); - $this->assertText('(* unsupported) Paragraphs fields do not support translation.'); - $this->assertNoRaw('<div class="messages messages--error'); - - // Check content type field management warning. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs_field'); - $this->assertText('Paragraphs fields do not support translation.'); - - // Make the paragraphs field translatable. - $edit = [ - 'entity_types[node]' => TRUE, - 'settings[node][paragraphed_test][translatable]' => TRUE, - 'settings[node][paragraphed_test][fields][paragraphs_field]' => TRUE, - ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); - - // Check content type field management error. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs_field'); - $this->assertText('Paragraphs fields do not support translation.'); - $this->assertRaw('<div class="messages messages--error'); - - // Check a not paragraphs translatable field does not display the message. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/add-field'); - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference:node', - 'label' => 'new_no_paragraphs_field', - 'field_name' => 'new_no_paragraphs_field', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and continue')); - $this->drupalPostForm(NULL, [], t('Save field settings')); - $this->assertNoText('Paragraphs fields do not support translation.'); - $this->assertNoRaw('<div class="messages messages--warning'); - } - - /** - * Tests required Paragraphs field. - */ - public function testRequiredParagraphsField() { - $this->loginAsAdmin(); - - // Add a Paragraph content type and 2 Paragraphs types. - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - // Make the paragraphs field required and save configuration. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $edit = [ - 'required' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, 'Save settings'); - $this->assertText('Saved paragraphs configuration.'); - - // Assert that the field is displayed in the form as required. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertRaw('<strong class="form-required" data-drupal-selector="edit-paragraphs-title">'); - $edit = [ - 'title[0][value]' => 'test_title', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphs field is required.'); - $this->drupalPostAjaxForm(NULL, [], 'paragraphs_paragraph_type_test_add_more'); - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test test_title has been created.'); - } - - /** - * Tests that we can use paragraphs widget only for paragraphs. - */ - public function testAvoidUsingParagraphsWithWrongEntity() { - $node_type = NodeType::create([ - 'type' => 'article', - 'name' => 'article', - ]); - $node_type->save(); - $this->loginAsAdmin([ - 'edit any article content', - ]); - $this->addParagraphsType('paragraphed_type'); - - // Create reference to node. - $this->fieldUIAddNewField('admin/structure/types/manage/article', 'node_reference', 'NodeReference', 'entity_reference_revisions', [ - 'cardinality' => 'number', - 'cardinality_number' => 1, - 'settings[target_type]' => 'node', - ], [ - 'settings[handler_settings][target_bundles][article]' => 'article', - ]); - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $this->assertNoOption('edit-fields-field-node-reference-type', 'entity_reference_paragraphs'); - $this->assertNoOption('edit-fields-field-node-reference-type', 'paragraphs'); - } - - /** - * Test included Paragraph types. - */ - public function testIncludedParagraphTypes() { - $this->loginAsAdmin(); - // Add a Paragraph content type and 2 Paragraphs types. - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $edit = [ - 'settings[handler_settings][negate]' => 0, - 'settings[handler_settings][target_bundles_drag_drop][paragraph_type_test][enabled]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, 'Save settings'); - $this->assertText('Saved paragraphs configuration.'); - - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('Add paragraph_type_test'); - $this->assertNoText('Add text'); - } - - /** - * Test excluded Paragraph types. - */ - public function testExcludedParagraphTypes() { - $this->loginAsAdmin(); - // Add a Paragraph content type and 2 Paragraphs types. - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $edit = [ - 'settings[handler_settings][negate]' => 1, - 'settings[handler_settings][target_bundles_drag_drop][text][enabled]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, 'Save settings'); - $this->assertText('Saved paragraphs configuration.'); - - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('Add paragraph_type_test'); - $this->assertNoText('Add text'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsContactTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsContactTest.php deleted file mode 100644 index 6ed36c0a0..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsContactTest.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\contact\Entity\ContactForm; - -/** - * Tests paragraphs with contact forms. - * - * @group paragraphs - */ -class ParagraphsContactTest extends ParagraphsTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'contact', - ); - - /** - * Tests adding paragraphs in contact forms. - */ - public function testContactForm() { - $this->loginAsAdmin([ - 'administer contact forms', - 'access site-wide contact form' - ]); - // Add a paragraph type. - $this->addParagraphsType('paragraphs_contact'); - $this->addParagraphsType('text'); - - // Create a contact form. - $contact_form = ContactForm::create(['id' => 'test_contact_form']); - $contact_form->save(); - // Add a paragraphs field to the contact form. - $this->addParagraphsField($contact_form->id(), 'paragraphs', 'contact_message', 'entity_reference_paragraphs'); - - // Add a paragraph to the contact form. - $this->drupalGet('contact/test_contact_form'); - $this->drupalPostAjaxForm(NULL, [], 'paragraphs_paragraphs_contact_add_more'); - // Check that the paragraph is displayed. - $this->assertText('paragraphs_contact'); - $this->drupalPostAjaxForm(NULL, [], 'paragraphs_0_remove'); - $this->assertText('Deleted Paragraph: paragraphs_contact'); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsCoreVersionUiTestTrait.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsCoreVersionUiTestTrait.php deleted file mode 100644 index 361c87242..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsCoreVersionUiTestTrait.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -/** - * Provides helper methods for Drupal 8.3.x and 8.4.x versions. - */ -trait ParagraphsCoreVersionUiTestTrait { - - /** - * An adapter for 8.3 > 8.4 Save (and (un)publish) node button change. - * - * Arguments are the same as WebTestBase::drupalPostForm. - * - * @see \Drupal\simpletest\WebTestBase::drupalPostForm - * @see https://www.drupal.org/node/2847274 - * - * @param \Drupal\Core\Url|string $path - * Location of the post form. - * @param array $edit - * Field data in an associative array. - * @param mixed $submit - * Value of the submit button whose click is to be emulated. For example, - * @param array $options - * (optional) Options to be forwarded to the url generator. - * @param array $headers - * (optional) An array containing additional HTTP request headers. - * @param string $form_html_id - * (optional) HTML ID of the form to be submitted. - * @param string $extra_post - * (optional) A string of additional data to append to the POST submission. - */ - protected function paragraphsPostNodeForm($path, $edit, $submit, array $options = [], array $headers = [], $form_html_id = NULL, $extra_post = NULL) { - $drupal_version = (float) substr(\Drupal::VERSION, 0, 3); - if ($drupal_version > 8.3) { - switch ($submit) { - case t('Save and unpublish'): - $submit = t('Save'); - $edit['status[value]'] = FALSE; - break; - - case t('Save and publish'): - $submit = t('Save'); - $edit['status[value]'] = TRUE; - break; - - case t('Save and keep published (this translation)'): - $submit = t('Save (this translation)'); - break; - - default: - $submit = t('Save'); - } - } - parent::drupalPostForm($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsEditModesTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsEditModesTest.php deleted file mode 100644 index 88d4680f3..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsEditModesTest.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests paragraphs edit modes. - * - * @group paragraphs - */ -class ParagraphsEditModesTest extends ParagraphsTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'image', - 'block_field', - ]; - - /** - * Tests the collapsed summary of paragraphs. - */ - public function testCollapsedSummary() { - $this->addParagraphedContentType('paragraphed_test', 'field_paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']); - - // Add a Paragraph type. - $paragraph_type = 'image_text_paragraph'; - $this->addParagraphsType($paragraph_type); - $title_paragraphs_type = 'title'; - $this->addParagraphsType($title_paragraphs_type); - $this->addParagraphsType('text'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'image', 'Image', 'image', [], ['settings[alt_field_required]' => FALSE]); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $title_paragraphs_type, 'title', 'Title', 'string', [], []); - - // Set edit mode to closed. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/form-display'); - $this->drupalPostAjaxForm(NULL, [], "field_paragraphs_settings_edit"); - $edit = ['fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'closed']; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Add a paragraph. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_image_text_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_title_add_more'); - $text = 'Trust me I am an image'; - file_put_contents('temporary://myImage1.jpg', $text); - - // Create a node with an image and text. - $edit = [ - 'title[0][value]' => 'Test article', - 'field_paragraphs[0][subform][field_text][0][value]' => 'text_summary', - 'files[field_paragraphs_0_subform_field_image_0]' => drupal_realpath('temporary://myImage1.jpg'), - 'field_paragraphs[1][subform][field_title][0][value]' => 'Title example', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Assert the summary is correctly generated. - $this->clickLink(t('Edit')); - $this->assertRaw('<div class="paragraphs-collapsed-description">myImage1.jpg, text_summary'); - $this->assertRaw('<div class="paragraphs-collapsed-description">Title example'); - - // Edit and remove alternative text. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $edit = [ - 'field_paragraphs[0][subform][field_image][0][alt]' => 'alternative_text_summary', - 'field_paragraphs[0][subform][field_image][0][width]' => 300, - 'field_paragraphs[0][subform][field_image][0][height]' => 300, - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_collapse'); - // Assert the summary is correctly generated. - $this->assertRaw('<div class="paragraphs-collapsed-description">alternative_text_summary, text_summary'); - - // Remove image. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_subform_field_image_0_remove_button'); - $this->drupalPostForm(NULL, [], t('Save')); - - // Assert the summary is correctly generated. - $this->clickLink(t('Edit')); - $this->assertRaw('<div class="paragraphs-collapsed-description">text_summary'); - - // Add a Block Paragraphs type. - $this->addParagraphsType('block_paragraph'); - $this->addFieldtoParagraphType('block_paragraph', 'field_block', 'block_field'); - - // Test the summary of a Block field. - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_block_paragraph_add_more'); - $edit = [ - 'title[0][value]' => 'Node with a Block Paragraph', - 'field_paragraphs[0][subform][field_block][0][plugin_id]' => 'system_breadcrumb_block', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->clickLink(t('Edit')); - $this->assertRaw('<div class="paragraphs-collapsed-description">Breadcrumbs'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsEntityTranslationWithNonTranslatableParagraphs.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsEntityTranslationWithNonTranslatableParagraphs.php deleted file mode 100644 index 91c236a02..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsEntityTranslationWithNonTranslatableParagraphs.php +++ /dev/null @@ -1,109 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -/** - * Tests the translation of heavily nested / specialized setup. - * - * @group paragraphs - */ -class ParagraphsEntityTranslationWithNonTranslatableParagraphs extends ParagraphsTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'language', - 'content_translation', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->admin_user = $this->drupalCreateUser([], NULL, TRUE); - $this->drupalLogin($this->admin_user); - - // Add a languages. - $edit = array( - 'predefined_langcode' => 'de', - ); - $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); - $edit = array( - 'predefined_langcode' => 'fr', - ); - $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); - - // Create article content type with a paragraphs field. - $this->addParagraphedContentType('article', 'field_paragraphs'); - $this->drupalGet('admin/structure/types/manage/article'); - // Make content type translatable. - $edit = array( - 'language_configuration[content_translation]' => TRUE, - ); - $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type')); - $this->drupalGet('admin/structure/types/manage/article'); - - // Ensue the paragraphs field itself isn't translatable - this would be a - // currently not supported configuration otherwise. - $edit = array( - 'translatable' => FALSE, - ); - $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_paragraphs', $edit, t('Save settings')); - - // Add Paragraphs type. - $this->addParagraphsType('test_paragraph_type'); - // Configure paragraphs type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/test_paragraph_type', 'text', 'Text', 'string', [ - 'cardinality' => '-1', - ]); - - // Just for verbose-sake - check the content language settings. - $this->drupalGet('admin/config/regional/content-language'); - } - - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsIEFTranslation() { - $this->drupalLogin($this->admin_user); - - // Create node with one paragraph. - $this->drupalGet('node/add/article'); - - // Set the values and save. - $edit = [ - 'title[0][value]' => 'Title English', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Add french translation. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add'), 1); - // Make sure that the original paragraph text is displayed. - $this->assertText('Title English'); - - $edit = array( - 'title[0][value]' => 'Title French', - ); - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertText('article Title French has been updated.'); - - // Add german translation. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add')); - // Make sure that the original paragraph text is displayed. - $this->assertText('Title English'); - - $edit = array( - 'title[0][value]' => 'Title German', - ); - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertText('article Title German has been updated.'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsFieldGroupTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsFieldGroupTest.php deleted file mode 100644 index 2adee34a6..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsFieldGroupTest.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -/** - * Tests the field group on node. - * - * @group paragraphs - * @requires module field_group - */ -class ParagraphsFieldGroupTest extends ParagraphsTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'field_group', - ]; - - /** - * Tests the field group inside paragraph. - */ - public function testFieldGroup() { - $this->loginAsAdmin(); - - $paragraph_type = 'paragraph_type_test'; - $content_type = 'paragraphed_test'; - - // Add a Paragraphed test content type. - $this->addParagraphedContentType($content_type, 'field_paragraphs', 'entity_reference_paragraphs'); - $this->addParagraphsType($paragraph_type); - $this->addParagraphsType('text'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - - // Create the field group element on paragraph type. - $edit = [ - 'group_formatter' => 'fieldset', - 'label' => 'paragraph_field_group_title', - 'group_name' => 'field' - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type . '/form-display/add-group', $edit, t('Save and continue')); - $edit = [ - 'format_settings[label]' => 'field_group' - ]; - $this->drupalPostForm(NULL, $edit, t('Create group')); - - // Put the text field into the field group. - $edit = [ - 'fields[field_text][parent]' => 'group_field' - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type . '/form-display', $edit, t('Save')); - - // Create a node with a paragraph. - $this->drupalGet('node/add/' . $content_type); - $this->drupalPostAjaxForm('node/add/' . $content_type, [], 'field_paragraphs_paragraph_type_test_add_more'); - - // Test if the new field group is displayed. - $this->assertText('field_group'); - $this->assertFieldByXPath("//fieldset", NULL, t('Fieldset present')); - - // Save the node. - $edit = [ - 'title[0][value]' => 'paragraphed_title', - 'field_paragraphs[0][subform][field_text][0][value]' => 'paragraph_value', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsInlineEntityFormTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsInlineEntityFormTest.php deleted file mode 100644 index 433a91142..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsInlineEntityFormTest.php +++ /dev/null @@ -1,142 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -/** - * Tests the configuration of paragraphs in relation to ief. - * - * @group paragraphs - */ -class ParagraphsInlineEntityFormTest extends ParagraphsTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'inline_entity_form', - ]; - - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsIEFPreview() { - // Create article content type with a paragraphs field. - $this->addParagraphedContentType('article', 'field_paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin(['create article content', 'edit any article content']); - - // Create the paragraphs type simple. - $this->addParagraphsType('simple'); - $this->addParagraphsType('text'); - - // Create a reference to an article. - $this->fieldUIAddNewField('admin/structure/paragraphs_type/simple', 'article', 'Article', 'field_ui:entity_reference:node', [ - 'settings[target_type]' => 'node', - 'cardinality' => 'number', - 'cardinality_number' => 1, - ], [ - 'required' => TRUE, - 'settings[handler_settings][target_bundles][article]' => TRUE - ]); - - // Enable IEF simple widget. - $this->drupalGet('admin/structure/paragraphs_type/simple/form-display'); - $edit = [ - 'fields[field_article][type]' => 'inline_entity_form_simple', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Set the paragraphs widget mode to preview. - $this->setParagraphsWidgetMode('article', 'field_paragraphs', 'preview'); - - // Create node with one paragraph. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_simple_add_more'); - - // Set the values and save. - $edit = [ - 'title[0][value]' => 'Dummy1', - 'field_paragraphs[0][subform][field_article][0][inline_entity_form][title][0][value]' => 'Dummy2', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Go back into edit page. - $node = $this->getNodeByTitle('Dummy1'); - $this->drupalGet('node/' . $node->id() . '/edit'); - - // Try to open the previewed paragraph. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - } - - /** - * Tests the reordering of previewed paragraphs. - */ - public function testParagraphsIEFChangeOrder() { - - // Create article content type with a paragraphs field. - $this->addParagraphedContentType('article', 'field_paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin(['create article content', 'edit any article content']); - - // Create the paragraphs type simple. - $this->addParagraphsType('simple'); - $this->addParagraphsType('text'); - - - // Create a reference to an article. - $this->fieldUIAddNewField('admin/structure/paragraphs_type/simple', 'article', 'Article', 'field_ui:entity_reference:node', [ - 'settings[target_type]' => 'node', - 'cardinality' => 'number', - 'cardinality_number' => '1', - ], [ - 'required' => TRUE, - 'settings[handler_settings][target_bundles][article]' => TRUE - ]); - - // Set cardinality explicit to -1. - $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_paragraphs/storage'); - $edit = [ - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ]; - $this->drupalPostForm(NULL, $edit, t('Save field settings')); - - // Enable IEF simple widget. - $this->drupalGet('admin/structure/paragraphs_type/simple/form-display'); - $edit = [ - 'fields[field_article][type]' => 'inline_entity_form_simple', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Set the paragraphs widget mode to preview. - $this->setParagraphsWidgetMode('article', 'field_paragraphs', 'preview'); - - // Create node with one paragraph. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_simple_add_more'); - - // Set the values and save. - $edit = [ - 'title[0][value]' => 'Article 1', - 'field_paragraphs[0][subform][field_article][0][inline_entity_form][title][0][value]' => 'Basic page 1', - ]; - - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Go back into edit page. - $node = $this->getNodeByTitle('Article 1'); - $this->drupalGet('node/' . $node->id() . '/edit'); - - // Create second paragraph. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_simple_add_more'); - - // Set the values of second paragraph and change the order. - $edit = [ - 'field_paragraphs[1][subform][field_article][0][inline_entity_form][title][0][value]' => 'Basic 2', - 'field_paragraphs[0][_weight]' => -1, - 'field_paragraphs[1][_weight]' => -2, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsPreviewTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsPreviewTest.php deleted file mode 100644 index 242255421..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsPreviewTest.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests the configuration of paragraphs. - * - * @group paragraphs - */ -class ParagraphsPreviewTest extends ParagraphsTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'image', - ); - - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsPreview() { - // Create paragraph type Headline + Block. - $this->addParagraphedContentType('article', 'field_paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin([ - 'administer node display', - 'create article content', - 'edit any article content', - 'delete any article content', - ]); - - // Create paragraph type Headline + Block. - $this->addParagraphsType('text'); - // Create field types for the text. - $this->fieldUIAddNewField('admin/structure/paragraphs_type/text', 'text', 'Text', 'text', array(), array()); - $this->assertText('Saved Text configuration.'); - - $test_text_1 = 'dummy_preview_text_1'; - $test_text_2 = 'dummy_preview_text_2'; - // Create node with two paragraphs. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_add_more'); - // Set the value of the paragraphs. - $edit = [ - 'title[0][value]' => 'Page_title', - 'field_paragraphs[0][subform][field_text][0][value]' => $test_text_1, - ]; - // Preview the article. - $this->drupalPostForm(NULL, $edit, t('Preview')); - // Check if the text is displayed. - $this->assertRaw($test_text_1); - - // Go back to the editing form. - $this->clickLink('Back to content editing'); - - $paragraph_1 = $this->xpath('//*[@id="edit-field-paragraphs-0-subform-field-text-0-value"]')[0]; - $this->assertEqual($paragraph_1['value'], $test_text_1); - - $this->drupalPostForm(NULL, $edit, t('Save')); - - $this->clickLink('Edit'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_add_more'); - $edit = [ - 'field_paragraphs[1][subform][field_text][0][value]' => $test_text_2, - ]; - // Preview the article. - $this->drupalPostForm(NULL, $edit, t('Preview')); - $this->assertRaw($test_text_1); - $this->assertRaw($test_text_2); - - // Go back to the editing form. - $this->clickLink('Back to content editing'); - $new_test_text_2 = 'less_dummy_preview_text_2'; - - $edit = [ - 'field_paragraphs[1][subform][field_text][0][value]' => $new_test_text_2, - ]; - // Preview the article. - $this->drupalPostForm(NULL, $edit, t('Preview')); - $this->assertRaw($test_text_1); - $this->assertRaw($new_test_text_2); - // Go back to the editing form. - $this->clickLink('Back to content editing'); - $paragraph_1 = $this->xpath('//*[@id="edit-field-paragraphs-0-subform-field-text-0-value"]')[0]; - $paragraph_2 = $this->xpath('//*[@id="edit-field-paragraphs-1-subform-field-text-0-value"]')[0]; - $this->assertEqual($paragraph_1['value'], $test_text_1); - $this->assertEqual($paragraph_2['value'], $new_test_text_2); - $this->drupalPostForm(NULL, [], t('Save')); - - $this->assertRaw($test_text_1); - $this->assertRaw($new_test_text_2); - $this->assertRaw('Page_title'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsSummaryFormatterTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsSummaryFormatterTest.php deleted file mode 100644 index 1d53a731e..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsSummaryFormatterTest.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests the paragraphs summary formatter. - * - * @group paragraphs - */ -class ParagraphsSummaryFormatterTest extends ParagraphsTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'image', - ]; - - /** - * Tests the paragraphs summary formatter. - */ - public function testParagraphsSummaryFormatter() { - $this->addParagraphedContentType('paragraphed_test', 'field_paragraphs', 'entity_reference_paragraphs'); - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content', 'administer node display']); - - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - $title_paragraphs_type = 'title'; - $this->addParagraphsType($title_paragraphs_type); - $this->addParagraphsType('text'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $title_paragraphs_type, 'title', 'Title', 'string', [], []); - - // Add a user Paragraph Type - $paragraph_type = 'user_paragraph'; - $this->addParagraphsType($paragraph_type); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'user', 'User', 'entity_reference', ['settings[target_type]' => 'user'], []); - - // Set display format to paragraphs summary. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/display'); - $edit = ['fields[field_paragraphs][type]' => 'paragraph_summary']; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Add a paragraph. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_text_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_title_add_more'); - - // Create a node with a text. - $edit = [ - 'title[0][value]' => 'Test article', - 'field_paragraphs[0][subform][field_text][0][value]' => 'text_summary', - 'field_paragraphs[1][subform][field_title][0][value]' => 'Title example', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->clickLink(t('Edit')); - $this->drupalPostForm(NULL, [], t('Add user_paragraph')); - $edit = [ - 'field_paragraphs[2][subform][field_user][0][target_id]' => $this->admin_user->label() . ' (' . $this->admin_user->id() . ')', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Assert the summary is correctly generated. - $this->assertText($this->admin_user->label()); - $this->assertText('Title example'); - - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTestBase.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTestBase.php deleted file mode 100644 index efa726fb4..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTestBase.php +++ /dev/null @@ -1,207 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Entity\Entity\EntityViewDisplay; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\node\Entity\NodeType; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\simpletest\WebTestBase; -use Drupal\Tests\paragraphs\FunctionalJavascript\ParagraphsTestBaseTrait; - -/** - * Base class for tests. - */ -abstract class ParagraphsTestBase extends WebTestBase { - - use FieldUiTestTrait, ParagraphsCoreVersionUiTestTrait, ParagraphsTestBaseTrait; - - /** - * Drupal user object created by loginAsAdmin(). - * - * @var \Drupal\user\UserInterface - */ - protected $admin_user = NULL; - - /** - * List of permissions used by loginAsAdmin(). - * - * @var array - */ - protected $admin_permissions = []; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'node', - 'paragraphs', - 'field', - 'field_ui', - 'block', - 'paragraphs_test', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Place the breadcrumb, tested in fieldUIAddNewField(). - $this->drupalPlaceBlock('system_breadcrumb_block'); - $this->drupalPlaceBlock('local_tasks_block'); - $this->drupalPlaceBlock('local_actions_block'); - $this->drupalPlaceBlock('page_title_block'); - - $this->admin_permissions = [ - 'administer content types', - 'administer node fields', - 'administer paragraphs types', - 'administer node form display', - 'administer paragraph fields', - 'administer paragraph form display', - ]; - } - - /** - * Creates an user with admin permissions and log in. - * - * @param array $additional_permissions - * Additional permissions that will be granted to admin user. - * @param bool $reset_permissions - * Flag to determine if default admin permissions will be replaced by - * $additional_permissions. - * - * @return object - * Newly created and logged in user object. - */ - function loginAsAdmin($additional_permissions = [], $reset_permissions = FALSE) { - $permissions = $this->admin_permissions; - - if ($reset_permissions) { - $permissions = $additional_permissions; - } - elseif (!empty($additional_permissions)) { - $permissions = array_merge($permissions, $additional_permissions); - } - - $this->admin_user = $this->drupalCreateUser($permissions); - $this->drupalLogin($this->admin_user); - return $this->admin_user; - } - - /** - * Sets the Paragraphs widget add mode. - * - * @param string $content_type - * Content type name where to set the widget mode. - * @param string $paragraphs_field - * Paragraphs field to change the mode. - * @param string $mode - * Mode to be set. ('dropdown', 'select' or 'button'). - */ - protected function setAddMode($content_type, $paragraphs_field, $mode) { - $form_display = EntityFormDisplay::load('node.' . $content_type . '.default') - ->setComponent($paragraphs_field, [ - 'type' => 'entity_reference_paragraphs', - 'settings' => ['add_mode' => $mode] - ]); - $form_display->save(); - } - - /** - * Sets the allowed Paragraphs types that can be added. - * - * @param string $content_type - * Content type name that contains the paragraphs field. - * @param array $paragraphs_types - * Array of paragraphs types that will be modified. - * @param bool $selected - * Whether or not the paragraphs types will be enabled. - * @param string $paragraphs_field - * Paragraphs field name that does the reference. - */ - protected function setAllowedParagraphsTypes($content_type, $paragraphs_types, $selected, $paragraphs_field) { - $edit = []; - $this->drupalGet('admin/structure/types/manage/' . $content_type . '/fields/node.' . $content_type . '.' . $paragraphs_field); - foreach ($paragraphs_types as $paragraphs_type) { - $edit['settings[handler_settings][target_bundles_drag_drop][' . $paragraphs_type . '][enabled]'] = $selected; - } - $this->drupalPostForm(NULL, $edit, t('Save settings')); - } - - /** - * Sets the weight of a given Paragraphs type. - * - * @param string $content_type - * Content type name that contains the paragraphs field. - * @param string $paragraphs_type - * ID of Paragraph type that will be modified. - * @param int $weight - * Weight to be set. - * @param string $paragraphs_field - * Paragraphs field name that does the reference. - */ - protected function setParagraphsTypeWeight($content_type, $paragraphs_type, $weight, $paragraphs_field) { - $this->drupalGet('admin/structure/types/manage/' . $content_type . '/fields/node.' . $content_type . '.' . $paragraphs_field); - $edit['settings[handler_settings][target_bundles_drag_drop][' . $paragraphs_type . '][weight]'] = $weight; - $this->drupalPostForm(NULL, $edit, t('Save settings')); - } - - /** - * Sets the default paragraph type. - * - * @param $content_type - * Content type name that contains the paragraphs field. - * @param $paragraphs_name - * Paragraphs name. - * @param $paragraphs_field_name - * Paragraphs field name to be used. - * @param $default_type - * Default paragraph type which should be set. - */ - protected function setDefaultParagraphType($content_type, $paragraphs_name, $paragraphs_field_name, $default_type) { - $this->drupalGet('admin/structure/types/manage/' . $content_type . '/form-display'); - $this->drupalPostAjaxForm(NULL, [], $paragraphs_field_name); - $this->drupalPostForm(NULL, ['fields[' . $paragraphs_name . '][settings_edit_form][settings][default_paragraph_type]' => $default_type], t('Update')); - $this->drupalPostForm(NULL, [], t('Save')); - } - - /** - * Removes the default paragraph type. - * - * @param $content_type - * Content type name that contains the paragraphs field. - */ - protected function removeDefaultParagraphType($content_type) { - $this->drupalGet('node/add/' . $content_type); - $this->drupalPostForm(NULL, [], 'Remove'); - $this->drupalPostForm(NULL, [], 'Confirm removal'); - $this->assertNoText('No paragraphs added yet.'); - } - - /** - * Sets the Paragraphs widget display mode. - * - * @param string $content_type - * Content type name where to set the widget mode. - * @param string $paragraphs_field - * Paragraphs field to change the mode. - * @param string $mode - * Mode to be set. ('closed', 'preview' or 'open'). - * 'preview' is only allowed in the classic widget. Use - * setParagraphsWidgetSettings for the experimental widget, instead. - */ - protected function setParagraphsWidgetMode($content_type, $paragraphs_field, $mode) { - $this->drupalGet('admin/structure/types/manage/' . $content_type . '/form-display'); - $this->drupalPostAjaxForm(NULL, [], $paragraphs_field . '_settings_edit'); - $this->drupalPostForm(NULL, ['fields[' . $paragraphs_field . '][settings_edit_form][settings][edit_mode]' => $mode], t('Update')); - $this->drupalPostForm(NULL, [], 'Save'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTranslationTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTranslationTest.php deleted file mode 100644 index efb2cc6b6..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTranslationTest.php +++ /dev/null @@ -1,844 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Language\LanguageInterface; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\node\Entity\Node; -use Drupal\user\Entity\Role; - -/** - * Tests the configuration of paragraphs. - * - * @group paragraphs - */ -class ParagraphsTranslationTest extends ParagraphsTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'paragraphs_demo', - 'content_translation', - 'link', - ); - - /** - * A user with admin permissions. - * - * @var array - */ - protected $admin_user; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->loginAsAdmin([ - 'administer site configuration', - 'create paragraphed_content_demo content', - 'edit any paragraphed_content_demo content', - 'delete any paragraphed_content_demo content', - 'administer content translation', - 'translate any entity', - 'create content translations', - 'administer languages', - ]); - $edit = [ - 'settings[paragraph][nested_paragraph][translatable]' => TRUE, - 'settings[paragraph][nested_paragraph][settings][language][language_alterable]' => TRUE, - 'settings[paragraph][images][fields][field_images_demo]' => TRUE, - ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); - - // Set the form display to classic. - EntityFormDisplay::load('node.paragraphed_content_demo.default') - ->setComponent('field_paragraphs_demo', ['type' => 'entity_reference_paragraphs']) - ->save(); - EntityFormDisplay::load('paragraph.nested_paragraph.default') - ->setComponent('field_paragraphs_demo', ['type' => 'entity_reference_paragraphs']) - ->save(); - - if (version_compare(\Drupal::VERSION, '8.4', '>=')) { - // @todo Workaround for file usage/unable to save the node with no usages. - // Remove when https://www.drupal.org/node/2801777 is fixed. - \Drupal::configFactory()->getEditable('file.settings') - ->set('make_unused_managed_files_temporary', TRUE) - ->save(); - } - } - - /** - * Tests the paragraph translation. - */ - public function testParagraphTranslation() { - // We need to add a permission to administer roles to deal with revisions. - $roles = $this->loggedInUser->getRoles(); - $this->grantPermissions(Role::load(array_shift($roles)), ['administer nodes']); - $this->drupalGet('admin/config/regional/content-language'); - - // Check the settings are saved correctly. - $this->assertFieldChecked('edit-entity-types-paragraph'); - $this->assertFieldChecked('edit-settings-node-paragraphed-content-demo-translatable'); - $this->assertFieldChecked('edit-settings-paragraph-text-image-translatable'); - $this->assertFieldChecked('edit-settings-paragraph-images-columns-field-images-demo-alt'); - $this->assertFieldChecked('edit-settings-paragraph-images-columns-field-images-demo-title'); - - // Check if the publish/unpublish option works. - $this->drupalGet('admin/structure/paragraphs_type/text_image/form-display'); - $edit = array( - 'fields[status][type]' => 'boolean_checkbox', - 'fields[status][region]' => 'content', - ); - - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Text + Image')); - $this->assertRaw('edit-field-paragraphs-demo-0-subform-status-value'); - $edit = [ - 'title[0][value]' => 'example_publish_unpublish', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Example published and unpublished', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText(t('Example published and unpublished')); - $this->clickLink(t('Edit')); - - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_nested_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_1_subform_field_paragraphs_demo_text_add_more'); - $edit = [ - 'field_paragraphs_demo[0][subform][status][value]' => FALSE, - 'field_paragraphs_demo[1][subform][field_paragraphs_demo][0][subform][field_text_demo][0][value]' => 'Dummy text' - ]; - $this->drupalPostForm(NULL, $edit + ['status[value]' => FALSE], t('Save')); - $this->assertNoText(t('Example published and unpublished')); - - // Check the parent fields are set properly. Get the node. - $node = $this->drupalGetNodeByTitle('example_publish_unpublish'); - // Loop over the paragraphs of the node. - foreach ($node->field_paragraphs_demo->referencedEntities() as $paragraph) { - $node_paragraph = Paragraph::load($paragraph->id())->toArray(); - // Check if the fields are set properly. - $this->assertEqual($node_paragraph['parent_id'][0]['value'], $node->id()); - $this->assertEqual($node_paragraph['parent_type'][0]['value'], 'node'); - $this->assertEqual($node_paragraph['parent_field_name'][0]['value'], 'field_paragraphs_demo'); - // If the paragraph is nested type load the child. - if ($node_paragraph['type'][0]['target_id'] == 'nested_paragraph') { - $nested_paragraph = Paragraph::load($node_paragraph['field_paragraphs_demo'][0]['target_id'])->toArray(); - // Check if the fields are properly set. - $this->assertEqual($nested_paragraph['parent_id'][0]['value'], $paragraph->id()); - $this->assertEqual($nested_paragraph['parent_type'][0]['value'], 'paragraph'); - $this->assertEqual($nested_paragraph['parent_field_name'][0]['value'], 'field_paragraphs_demo'); - } - } - - // Add paragraphed content. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Text + Image')); - $edit = array( - 'title[0][value]' => 'Title in english', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Text in english', - ); - // The button to remove a paragraph is present. - $this->assertRaw(t('Remove')); - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('Title in english'); - // The text is present when editing again. - $this->clickLink(t('Edit')); - $this->assertText('Title in english'); - $this->assertText('Text in english'); - - // Add french translation. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add'), 1); - // Make sure the Add / Remove paragraph buttons are hidden. - $this->assertNoRaw(t('Remove')); - $this->assertNoRaw(t('Add Text + Image')); - // Make sure that the original paragraph text is displayed. - $this->assertText('Text in english'); - - $edit = array( - 'title[0][value]' => 'Title in french', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Text in french', - 'revision' => TRUE, - 'revision_log[0][value]' => 'french 1', - ); - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertText('Paragraphed article Title in french has been updated.'); - - // Check the english translation. - $this->drupalGet('node/' . $node->id()); - $this->assertText('Title in english'); - $this->assertText('Text in english'); - $this->assertNoText('Title in french'); - $this->assertNoText('Text in french'); - - // Check the french translation. - $this->drupalGet('fr/node/' . $node->id()); - $this->assertText('Title in french'); - $this->assertText('Text in french'); - $this->assertNoText('Title in english'); - // The translation is still present when editing again. - $this->clickLink(t('Edit')); - $this->assertText('Title in french'); - $this->assertText('Text in french'); - $edit = array( - 'title[0][value]' => 'Title Change in french', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'New text in french', - 'revision' => TRUE, - 'revision_log[0][value]' => 'french 2', - ); - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertText('Title Change in french'); - $this->assertText('New text in french'); - - // Back to the source language. - $this->drupalGet('node/' . $node->id()); - $this->clickLink(t('Edit')); - $this->assertText('Title in english'); - $this->assertText('Text in english'); - // Save the original content on second request. - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - $this->assertText('Paragraphed article Title in english has been updated.'); - - // Test if reverting to old paragraphs revisions works, make sure that - // the reverted node can be saved again. - $this->drupalGet('fr/node/' . $node->id() . '/revisions'); - $this->clickLink(t('Revert')); - $this->drupalPostForm(NULL, ['revert_untranslated_fields' => TRUE], t('Revert')); - $this->clickLink(t('Edit')); - $this->assertRaw('Title in french'); - $this->assertText('Text in french'); - $this->drupalPostForm(NULL, [], t('Save (this translation)')); - $this->assertNoRaw('The content has either been modified by another user, or you have already submitted modifications'); - $this->assertText('Text in french'); - - //Add paragraphed content with untranslatable language - $this->drupalGet('node/add/paragraphed_content_demo'); - $edit = array('langcode[0][value]' => LanguageInterface::LANGCODE_NOT_SPECIFIED); - $this->drupalPostForm(NULL, $edit, t('Add Text + Image')); - $this->assertResponse(200); - - // Make 'Images' paragraph field translatable, enable alt and title fields. - $this->drupalGet('admin/structure/paragraphs_type/images/fields'); - $this->clickLink('Edit'); - $edit = [ - 'translatable' => 1, - 'settings[alt_field]' => 1, - 'settings[title_field]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, t('Save settings')); - - // Create a node with an image paragraph, its alt and title text. - $text = 'Trust me I\'m an image'; - file_put_contents('temporary://Image.jpg', $text); - $file_path = $this->container->get('file_system')->realpath('temporary://Image.jpg'); - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, [], t('Add Images')); - $this->drupalPostForm(NULL, ['files[field_paragraphs_demo_0_subform_field_images_demo_0][]' => $file_path], t('Upload')); - $edit = [ - 'title[0][value]' => 'Title EN', - 'field_paragraphs_demo[0][subform][field_images_demo][0][alt]' => 'Image alt', - 'field_paragraphs_demo[0][subform][field_images_demo][0][title]' => 'Image title', - 'field_paragraphs_demo[0][subform][field_images_demo][0][width]' => 100, - 'field_paragraphs_demo[0][subform][field_images_demo][0][height]' => 100, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Translate the node with the image paragraph. - $this->clickLink('Translate'); - $this->clickLink(t('Add'), 1); - $edit = [ - 'title[0][value]' => 'Title FR', - 'field_paragraphs_demo[0][subform][field_images_demo][0][alt]' => 'Image alt FR', - 'field_paragraphs_demo[0][subform][field_images_demo][0][title]' => 'Image title FR', - ]; - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertRaw('Title FR'); - - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, [], t('Add Text')); - $edit = [ - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'texto', - 'title[0][value]' => 'titulo', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('titulo'); - $this->assertParagraphsLangcode($node->id(), 'de'); - - // Test langcode matching when Paragraphs and node have different language. - $paragraph_1 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'text', - 'langcode' => 'en', - 'field_text_demo' => 'english_text_1', - ]); - $paragraph_1->save(); - - $paragraph_2 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'text', - 'langcode' => 'en', - 'field_text_demo' => 'english_text_2', - ]); - $paragraph_2->save(); - - $paragraph_data = $paragraph_2->toArray(); - $paragraph_data['field_text_demo'] = 'german_text_2'; - $paragraph_2->addTranslation('de', $paragraph_data); - $paragraph_2->save(); - $translated_paragraph = $paragraph_2->getTranslation('en'); - - $node = $this->createNode([ - 'langcode' => 'de', - 'type' => 'paragraphed_content_demo', - 'field_paragraphs_demo' => [$paragraph_1, $translated_paragraph], - ]); - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('Paragraphed article ' . $node->label() . ' has been updated.'); - // Check that first paragraph langcode has been updated. - $paragraph = Paragraph::load($paragraph_1->id()); - $this->assertEqual($paragraph->language()->getId(), 'de'); - $this->assertFalse($paragraph->hasTranslation('en')); - // Check that second paragraph has two translations. - $paragraph = Paragraph::load($paragraph_2->id()); - $this->assertTrue($paragraph->hasTranslation('de')); - $this->assertTrue($paragraph->hasTranslation('en')); - $this->assertRaw('german_text'); - - // Create an english translation of the node. - $edit = [ - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'english_translation_1', - 'field_paragraphs_demo[1][subform][field_text_demo][0][value]' => 'english_translation_2', - ]; - $this->drupalPostForm('node/' . $node->id() . '/translations/add/de/en', $edit, t('Save (this translation)')); - // Attempt to create a french translation. - $this->drupalGet('node/' . $node->id() . '/translations/add/de/fr'); - // Check that the german translation of the paragraphs is displayed. - $this->assertFieldByName('field_paragraphs_demo[0][subform][field_text_demo][0][value]', 'english_text_1'); - $this->assertFieldByName('field_paragraphs_demo[1][subform][field_text_demo][0][value]', 'german_text_2'); - $this->drupalPostForm(NULL, ['source_langcode[source]' => 'en'], t('Change')); - // Check that the english translation of the paragraphs is displayed. - $this->assertFieldByName('field_paragraphs_demo[0][subform][field_text_demo][0][value]', 'english_translation_1'); - $this->assertFieldByName('field_paragraphs_demo[1][subform][field_text_demo][0][value]', 'english_translation_2'); - - // Create a node with empty Paragraphs. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, [], t('Add Nested Paragraph')); - $edit = ['title[0][value]' => 'empty_node']; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Attempt to translate it. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add')); - // Check the add button is not displayed. - $this->assertEqual(count($this->xpath('//*[@name="field_paragraphs_demo_0_subform_field_paragraphs_demo_images_add_more"]')), 0); - - // Add a non translatable field to Text Paragraph type. - $edit = [ - 'new_storage_type' => 'text_long', - 'label' => 'untranslatable_field', - 'field_name' => 'untranslatable_field', - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/add-field', $edit, t('Save and continue')); - $this->drupalPostForm(NULL, [], t('Save field settings')); - $this->drupalPostForm(NULL, [], t('Save settings')); - - // Add a non translatable reference field. - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference:node', - 'label' => 'untranslatable_ref_field', - 'field_name' => 'untranslatable_ref_field', - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/add-field', $edit, t('Save and continue')); - $this->drupalPostForm(NULL, [], t('Save field settings')); - $this->drupalPostForm(NULL, ['settings[handler_settings][target_bundles][paragraphed_content_demo]' => TRUE], t('Save settings')); - - // Add a non translatable link field. - $edit = [ - 'new_storage_type' => 'link', - 'label' => 'untranslatable_link_field', - 'field_name' => 'untranslatable_link_field', - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/add-field', $edit, t('Save and continue')); - $this->drupalPostForm(NULL, [], t('Save field settings')); - $this->drupalPostForm(NULL, [], t('Save settings')); - - // Attempt to add a translation. - $this->drupalGet('node/' . $node->id() . '/translations/add/de/fr'); - $this->assertText('untranslatable_field (all languages)'); - $this->assertText('untranslatable_ref_field (all languages)'); - $this->assertText('untranslatable_link_field (all languages)'); - $this->assertNoText('Text (all languages)'); - - // Enable translations for the reference and link field. - $edit = [ - 'translatable' => TRUE, - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/paragraph.text.field_untranslatable_ref_field', $edit, t('Save settings')); - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/paragraph.text.field_untranslatable_link_field', $edit, t('Save settings')); - - // Attempt to add a translation. - $this->drupalGet('node/' . $node->id() . '/translations/add/de/fr'); - $this->assertText('untranslatable_field (all languages)'); - $this->assertNoText('untranslatable_link_field (all languages)'); - $this->assertNoText('untranslatable_ref_field (all languages)'); - $this->assertNoText('Text (all languages)'); - } - - /** - * Tests the paragraph buttons presence in translation multilingual workflow. - * - * This test covers the following test cases: - * 1) original node langcode in EN, translate in FR, change to DE. - * 2) original node langcode in DE, change site langcode to DE, change node - * langcode to EN. - */ - public function testParagraphTranslationMultilingual() { - // Case 1: original node langcode in EN, translate in FR, change to DE. - - // Add 'Images' paragraph and check the paragraphs buttons are displayed. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Images')); - $this->assertParagraphsButtons(1); - // Upload an image and check the paragraphs buttons are still displayed. - $images = $this->drupalGetTestFiles('image')[0]; - $edit = [ - 'title[0][value]' => 'Title in english', - 'files[field_paragraphs_demo_0_subform_field_images_demo_0][]' => $images->uri, - ]; - $this->drupalPostForm(NULL, $edit, t('Upload')); - $this->assertParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save')); - $this->assertText('Title in english'); - $node = $this->drupalGetNodeByTitle('Title in english'); - // Check the paragraph langcode is 'en'. - $this->assertParagraphsLangcode($node->id()); - - // Add french translation. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add'), 1); - // Make sure the host entity and its paragraphs have valid source language - // and check that the paragraphs buttons are hidden. - $this->assertNoParagraphsButtons(1); - $edit = [ - 'title[0][value]' => 'Title in french', - ]; - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertParagraphsLangcode($node->id(), 'en', 'fr'); - $this->assertText('Paragraphed article Title in french has been updated.'); - $this->assertText('Title in french'); - $this->assertNoText('Title in english'); - // Check the original node and the paragraph langcode is still 'en'. - $this->assertParagraphsLangcode($node->id()); - - // Edit the french translation and upload a new image. - $this->clickLink('Edit'); - $images = $this->drupalGetTestFiles('image')[1]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_0_subform_field_images_demo_1][]' => $images->uri, - ], t('Upload')); - // Check editing a translation does not affect the source langcode and - // check that the paragraphs buttons are still hidden. - $this->assertParagraphsLangcode($node->id(), 'en', 'fr'); - $this->assertNoParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - $this->assertText('Title in french'); - $this->assertNoText('Title in english'); - - // Back to the original node. - $this->drupalGet('node/' . $node->id()); - $this->assertText('Title in english'); - $this->assertNoText('Title in french'); - // Check the original node and the paragraph langcode are still 'en' and - // check that the paragraphs buttons are still displayed. - $this->clickLink('Edit'); - $this->assertParagraphsLangcode($node->id()); - $this->assertParagraphsButtons(1); - // Change the node langcode to 'german', add a 'Nested Paragraph', check - // the paragraphs langcode are still 'en' and their buttons are displayed. - $edit = [ - 'title[0][value]' => 'Title in english (de)', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Nested Paragraph')); - $this->assertParagraphsLangcode($node->id()); - $this->assertParagraphsButtons(2); - // Add an 'Images' paragraph inside the nested one, check the paragraphs - // langcode are still 'en' and the paragraphs buttons are still displayed. - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_1_subform_field_paragraphs_demo_images_add_more'); - $this->assertParagraphsLangcode($node->id()); - $this->assertParagraphsButtons(2); - // Upload a new image, check the paragraphs langcode are still 'en' and the - // paragraphs buttons are displayed. - $images = $this->drupalGetTestFiles('image')[2]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_1_subform_field_paragraphs_demo_0_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node->id()); - $this->assertParagraphsButtons(2); - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - $this->assertText('Title in english (de)'); - $this->assertNoText('Title in french'); - // Check the original node and the paragraphs langcode are now 'de'. - $this->assertParagraphsLangcode($node->id(), 'de'); - - // Check the french translation. - $this->drupalGet('fr/node/' . $node->id()); - $this->assertText('Title in french'); - $this->assertNoText('Title in english (de)'); - // Check editing a translation does not affect the source langcode and - // check that the paragraphs buttons are still hidden. - $this->clickLink('Edit'); - $this->assertParagraphsLangcode($node->id(), 'de', 'fr'); - $this->assertNoParagraphsButtons(2); - - // Case 2: original node langcode in DE, change site langcode to DE, change - // node langcode to EN. - - // Change the site langcode to french. - $this->drupalPostForm('admin/config/regional/language', [ - 'site_default_language' => 'fr', - ], t('Save configuration')); - - // Check the original node and its paragraphs langcode are still 'de' - // and the paragraphs buttons are still displayed. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertParagraphsLangcode($node->id(), 'de'); - $this->assertParagraphsButtons(2); - - // Go to the french translation. - $this->drupalGet('node/' . $node->id() . '/translations'); - $this->clickLink(t('Edit'), 1); - // Check editing a translation does not affect the source langcode and - // check that the paragraphs buttons are still hidden. - $this->assertParagraphsLangcode($node->id(), 'de', 'fr'); - $this->assertNoParagraphsButtons(2); - // Upload another image. - $images = $this->drupalGetTestFiles('image')[3]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_1_subform_field_paragraphs_demo_0_subform_field_images_demo_1][]' => $images->uri, - ], t('Upload')); - // Check editing a translation does not affect the source langcode and - // check that the paragraphs buttons are still hidden. - $this->assertParagraphsLangcode($node->id(), 'de', 'fr'); - $this->assertNoParagraphsButtons(2); - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - // Check the paragraphs langcode are still 'de' after saving the translation. - $this->assertParagraphsLangcode($node->id(), 'de', 'fr'); - $this->assertText('Title in french'); - $this->assertNoText('Title in english (de)'); - - // Back to the original node. - $this->drupalGet('node/' . $node->id()); - $this->assertText('Title in english (de)'); - $this->assertNoText('Title in french'); - // Check the original node and the paragraphs langcode are still 'de' and - // check that the paragraphs buttons are still displayed. - $this->clickLink('Edit'); - $this->assertParagraphsLangcode($node->id(), 'de'); - $this->assertParagraphsButtons(2); - // Change the node langcode back to 'english', add an 'Images' paragraph, - // check the paragraphs langcode are still 'de' and their buttons are shown. - $edit = [ - 'title[0][value]' => 'Title in english', - 'langcode[0][value]' => 'en', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Images')); - $this->assertParagraphsLangcode($node->id(), 'de'); - $this->assertParagraphsButtons(3); - // Upload a new image, check the paragraphs langcode are still 'de' and the - // paragraphs buttons are displayed. - $images = $this->drupalGetTestFiles('image')[4]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_2_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node->id(), 'de'); - $this->assertParagraphsButtons(3); - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - $this->assertText('Paragraphed article Title in english has been updated.'); - // Check the original node and the paragraphs langcode are now 'en'. - $this->assertParagraphsLangcode($node->id()); - } - - /** - * Tests the paragraphs buttons presence in multilingual workflow. - * - * This test covers the following test cases: - * 1) original node langcode in german, change to english. - * 2) original node langcode in english, change to german. - * 3) original node langcode in english, change site langcode to german, - * change node langcode to german. - */ - public function testParagraphsMultilingualWorkflow() { - // Case 1: Check the paragraphs buttons after changing the NODE language - // (original node langcode in GERMAN, default site langcode in english). - - // Create a node and check that the node langcode is 'english'. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->assertOptionSelected('edit-langcode-0-value', 'en'); - // Change the node langcode to 'german' and add a 'Nested Paragraph'. - $edit = [ - 'title[0][value]' => 'Title in german', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Nested Paragraph')); - // Check that the paragraphs buttons are displayed and add an 'Images' - // paragraph inside the nested paragraph. - $this->assertParagraphsButtons(1); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_0_subform_field_paragraphs_demo_images_add_more'); - // Upload an image and check the paragraphs buttons are still displayed. - $images = $this->drupalGetTestFiles('image')[0]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_0_subform_field_paragraphs_demo_0_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save')); - $this->assertText('Title in german'); - $node1 = $this->getNodeByTitle('Title in german'); - - // Check the paragraph langcode is 'de' and its buttons are displayed. - // @todo check for the nested children paragraphs buttons and langcode - // when it's supported. - $this->clickLink(t('Edit')); - $this->assertParagraphsLangcode($node1->id(), 'de'); - $this->assertParagraphsButtons(1); - // Change the node langcode to 'english' and upload another image. - $images = $this->drupalGetTestFiles('image')[1]; - $edit = [ - 'title[0][value]' => 'Title in german (en)', - 'langcode[0][value]' => 'en', - 'files[field_paragraphs_demo_0_subform_field_paragraphs_demo_0_subform_field_images_demo_1][]' => $images->uri, - ]; - $this->drupalPostForm(NULL, $edit, t('Upload')); - // Check the paragraph langcode is still 'de' and its buttons are shown. - $this->assertParagraphsLangcode($node1->id(), 'de'); - $this->assertParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save')); - // Check the paragraph langcode is now 'en' after saving. - $this->assertParagraphsLangcode($node1->id()); - - // Check the paragraph langcode is 'en' and its buttons are still shown. - $this->clickLink(t('Edit')); - $this->assertParagraphsLangcode($node1->id()); - $this->assertParagraphsButtons(1); - - // Case 2: Check the paragraphs buttons after changing the NODE language - // (original node langcode in ENGLISH, default site langcode in english). - - // Create another node. - $this->drupalGet('node/add/paragraphed_content_demo'); - // Check that the node langcode is 'english' and add a 'Nested Paragraph'. - $this->assertOptionSelected('edit-langcode-0-value', 'en'); - $this->drupalPostForm(NULL, NULL, t('Add Nested Paragraph')); - // Check that the paragraphs buttons are displayed and add an 'Images' - // paragraph inside the nested paragraph. - $this->assertParagraphsButtons(1); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_0_subform_field_paragraphs_demo_images_add_more'); - // Upload an image and check the paragraphs buttons are still displayed. - $images = $this->drupalGetTestFiles('image')[0]; - $edit = [ - 'title[0][value]' => 'Title in english', - 'files[field_paragraphs_demo_0_subform_field_paragraphs_demo_0_subform_field_images_demo_0][]' => $images->uri, - ]; - $this->drupalPostForm(NULL, $edit, t('Upload')); - $this->assertParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save')); - $this->assertText('Title in english'); - $node2 = $this->drupalGetNodeByTitle('Title in english'); - - // Check the paragraph langcode is 'en' and its buttons are displayed. - // @todo check for the nested children paragraphs buttons and langcode - // when it's supported. - $this->clickLink(t('Edit')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(1); - // Change the node langcode to 'german' and add another 'Images' paragraph. - $edit = [ - 'title[0][value]' => 'Title in english (de)', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Images')); - // Check the paragraphs langcode are still 'en' and their buttons are shown. - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(2); - // Upload an image, check the paragraphs langcode are still 'en' and their - // buttons are displayed. - $images = $this->drupalGetTestFiles('image')[1]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_1_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(2); - $this->drupalPostForm(NULL, NULL, t('Save')); - // Check the paragraphs langcode are now 'de' after saving. - $this->assertParagraphsLangcode($node2->id(), 'de'); - - // Change node langcode back to 'english' and save. - $this->clickLink(t('Edit')); - $edit = [ - 'title[0][value]' => 'Title in english', - 'langcode[0][value]' => 'en', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Check the paragraphs langcode are now 'en' after saving. - $this->assertParagraphsLangcode($node2->id()); - - // Case 3: Check the paragraphs buttons after changing the SITE language. - - // Change the site langcode to german. - $edit = [ - 'site_default_language' => 'de', - ]; - $this->drupalPostForm('admin/config/regional/language', $edit, t('Save configuration')); - - // Check the original node and the paragraphs langcode are still 'en' and - // check that the paragraphs buttons are still displayed. - $this->drupalGet('node/' . $node2->id() . '/edit'); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(2); - // Add another 'Images' paragraph with node langcode as 'english'. - $this->drupalPostForm(NULL, NULL, t('Add Images')); - // Check the paragraph langcode are still 'en' and their buttons are shown. - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(3); - // Upload an image, check the paragraphs langcode are still 'en' and their - // buttons are displayed. - $images = $this->drupalGetTestFiles('image')[2]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_2_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(3); - $this->drupalPostForm(NULL, NULL, t('Save')); - // Check the paragraphs langcode are still 'en' after saving. - $this->assertParagraphsLangcode($node2->id()); - - // Check the paragraphs langcode are still 'en' and their buttons are shown. - $this->clickLink(t('Edit')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(3); - // Change node langcode to 'german' and add another 'Images' paragraph. - $edit = [ - 'title[0][value]' => 'Title in english (de)', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Images')); - // Check the paragraphs langcode are still 'en' and their buttons are shown. - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(4); - // Upload an image, check the paragraphs langcode are still 'en' and their - // buttons are displayed. - $images = $this->drupalGetTestFiles('image')[3]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_3_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(4); - $this->drupalPostForm(NULL, NULL, t('Save')); - // Check the paragraphs langcode are now 'de' after saving. - $this->assertParagraphsLangcode($node2->id(), 'de'); - } - - /** - * Passes if the paragraphs buttons are present. - * - * @param int $count - * Number of paragraphs buttons to look for. - */ - protected function assertParagraphsButtons($count) { - $this->assertParagraphsButtonsHelper($count, FALSE); - } - - /** - * Passes if the paragraphs buttons are NOT present. - * - * @param int $count - * Number of paragraphs buttons to look for. - */ - protected function assertNoParagraphsButtons($count) { - $this->assertParagraphsButtonsHelper($count, TRUE); - } - - /** - * Helper for assertParagraphsButtons and assertNoParagraphsButtons. - * - * @param int $count - * Number of paragraphs buttons to look for. - * @param bool $hidden - * TRUE if these buttons should not be shown, FALSE otherwise. - * Defaults to TRUE. - */ - protected function assertParagraphsButtonsHelper($count, $hidden = TRUE) { - for ($i = 0; $i < $count; $i++) { - $remove_button = $this->xpath('//*[@name="field_paragraphs_demo_' . $i . '_remove"]'); - if (!$hidden) { - $this->assertNotEqual(count($remove_button), 0); - } - else { - $this->assertEqual(count($remove_button), 0); - } - } - - // It is enough to check for the specific paragraph type 'Images' to assert - // the add more buttons presence for this test class. - $add_button = $this->xpath('//input[@value="Add Images"]'); - if (!$hidden) { - $this->assertNotEqual(count($add_button), 0); - } - else { - $this->assertEqual(count($add_button), 0); - } - } - - /** - * Assert each paragraph items have the same langcode as the node one. - * - * @param string $node_id - * The node ID which contains the paragraph items to be checked. - * @param string $source_lang - * The expected node source langcode. Defaults to 'en'. - * @param string $trans_lang - * The expected translated node langcode. Defaults to NULL. - */ - protected function assertParagraphsLangcode($node_id, $source_lang = 'en', $trans_lang = NULL) { - // Update the outdated node and check all the paragraph items langcodes. - \Drupal::entityTypeManager()->getStorage('node')->resetCache([$node_id]); - /** @var \Drupal\node\NodeInterface $node */ - $node = Node::load($node_id); - $node_langcode = $node->langcode->value; - $this->assertEqual($node_langcode, $source_lang, 'Host langcode matches.'); - - /** @var \Drupal\Core\Entity\ContentEntityBase $paragraph */ - foreach ($node->field_paragraphs_demo->referencedEntities() as $paragraph) { - $paragraph_langcode = $paragraph->language()->getId(); - $message = new FormattableMarkup('Node langcode is "@node", paragraph item langcode is "@item".', ['@node' => $source_lang, '@item' => $paragraph_langcode]); - $this->assertEqual($paragraph_langcode, $source_lang, $message); - } - - // Check the translation. - if (!empty($trans_lang)) { - $this->assertTrue($node->hasTranslation($trans_lang), 'Translation exists.'); - } - if ($node->hasTranslation($trans_lang)) { - $trans_node = $node->getTranslation($trans_lang); - $trans_node_langcode = $trans_node->language()->getId(); - $this->assertEqual($trans_node_langcode, $trans_lang, 'Translated node langcode matches.'); - - // Check the paragraph item langcode matching the translated node langcode. - foreach ($trans_node->field_paragraphs_demo->referencedEntities() as $paragraph) { - if ($paragraph->hasTranslation($trans_lang)) { - $trans_item = $paragraph->getTranslation($trans_lang); - $paragraph_langcode = $trans_item->language()->getId(); - $message = new FormattableMarkup('Translated node langcode is "@node", paragraph item langcode is "@item".', ['@node' => $trans_lang, '@item' => $paragraph_langcode]); - $this->assertEqual($paragraph_langcode, $trans_lang, $message); - } - } - } - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTypesTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTypesTest.php deleted file mode 100644 index 200db0fc5..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsTypesTest.php +++ /dev/null @@ -1,134 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\paragraphs\Entity\ParagraphsType; - -/** - * Tests Paragraphs types. - * - * @group paragraphs - */ -class ParagraphsTypesTest extends ParagraphsTestBase { - - /** - * Tests the deletion of Paragraphs types. - */ - public function testRemoveTypesWithContent() { - $this->loginAsAdmin(); - - // Add a Paragraphed test content. - $this->addParagraphedContentType('paragraphed_test', 'paragraphs', 'entity_reference_paragraphs'); - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - // Attempt to delete the content type not used yet. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->clickLink(t('Delete')); - $this->assertText('This action cannot be undone.'); - $this->clickLink(t('Cancel')); - - // Add a test node with a Paragraph. - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostAjaxForm(NULL, [], 'paragraphs_paragraph_type_test_add_more'); - $edit = ['title[0][value]' => 'test_node']; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test test_node has been created.'); - - // Attempt to delete the paragraph type already used. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->clickLink(t('Delete')); - $this->assertText('paragraph_type_test Paragraphs type is used by 1 piece of content on your site. You can not remove this paragraph_type_test Paragraphs type until you have removed all from the content.'); - - } - - /** - * Tests the paragraph type icon settings. - */ - public function testParagraphTypeIcon() { - - /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */ - $file_usage = \Drupal::service('file.usage'); - - /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */ - $entity_repository = \Drupal::service('entity.repository'); - - $admin_user = $this->drupalCreateUser([ - 'administer paragraphs types', - 'access files overview', - ]); - $this->drupalLogin($admin_user); - // Add the paragraph type with icon. - $this->drupalGet('admin/structure/paragraphs_type/add'); - $this->assertText('Paragraph type icon'); - $test_files = $this->drupalGetTestFiles('image'); - $fileSystem = \Drupal::service('file_system'); - $edit = [ - 'label' => 'Test paragraph type', - 'id' => 'test_paragraph_type_icon', - 'files[icon_file]' => $fileSystem->realpath($test_files[0]->uri), - ]; - $this->drupalPostForm(NULL, $edit, t('Save and manage fields')); - $this->assertText('Saved the Test paragraph type Paragraphs type.'); - - // Check if the icon has been saved. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->assertRaw('image-test.png'); - $this->clickLink('Edit'); - $this->assertText('image-test.png'); - - // Check that the icon file usage has been registered. - $paragraph_type = ParagraphsType::load('test_paragraph_type_icon'); - $file = $entity_repository->loadEntityByUuid('file', $paragraph_type->get('icon_uuid')); - $usages = $file_usage->listUsage($file); - $this->assertEqual($usages['paragraphs']['paragraphs_type']['test_paragraph_type_icon'], 1); - - // Tests calculateDependencies method. - $dependencies = $paragraph_type->getDependencies(); - $dependencies_uuid[] = explode(':', $dependencies['content'][0]); - $this->assertEqual($paragraph_type->get('icon_uuid'), $dependencies_uuid[0][2]); - - // Delete the icon. - $this->drupalGet('admin/structure/paragraphs_type/test_paragraph_type_icon'); - $this->drupalPostAjaxForm(NULL, [], 'icon_file_remove_button'); - $this->drupalPostForm(NULL, [], t('Save')); - - // Check that the icon file usage has been deregistered. - $usages = $file_usage->listUsage($file); - $this->assertEqual($usages, []); - } - - /** - * Test the paragraph type description settings. - */ - public function testParagraphTypeDescription() { - $admin_user = $this->drupalCreateUser(['administer paragraphs types']); - $this->drupalLogin($admin_user); - // Add the paragraph type with description. - $this->drupalGet('admin/structure/paragraphs_type/add'); - $this->assertText('Description'); - $label = 'Test paragraph type'; - $description_markup = 'Use <em>Test paragraph type</em> to test the functionality of descriptions.'; - $description_text = 'Use Test paragraph type to test the functionality of descriptions.'; - $edit = [ - 'label' => $label, - 'id' => 'test_paragraph_type_description', - 'description' => $description_markup, - ]; - $this->drupalPostForm(NULL, $edit, t('Save and manage fields')); - $this->assertText("Saved the $label Paragraphs type."); - - // Check if the description has been saved. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->assertText('Description'); - $this->assertText($description_text); - $this->assertRaw($description_markup); - //Check if description is at Description column - $header_position = count($this->xpath('//table/thead/tr/th[.="Description"]/preceding-sibling::th')); - $row_position = count($this->xpath('//table/tbody/tr/td[.="' . $description_text . '"]/preceding-sibling::td')); - $this->assertEqual($header_position, $row_position); - $this->clickLink('Edit'); - $this->assertText('Use <em>Test paragraph type</em> to test the functionality of descriptions.'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsUiTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsUiTest.php deleted file mode 100644 index c4a72a51f..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsUiTest.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests the Paragraphs user interface. - * - * @group paragraphs - */ -class ParagraphsUiTest extends ParagraphsTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'paragraphs_demo', - ]; - - /** - * Tests displaying an error message a required paragraph field that is empty. - */ - public function testEmptyRequiredField() { - $admin_user = $this->drupalCreateUser([ - 'administer node fields', - 'administer paragraph form display', - 'administer node form display', - 'create paragraphed_content_demo content', - 'edit any paragraphed_content_demo content', - ]); - $this->drupalLogin($admin_user); - - // Add required field to paragraphed content type. - $bundle_path = 'admin/structure/types/manage/paragraphed_content_demo'; - $field_name = 'content'; - $field_title = 'Content Test'; - $field_type = 'field_ui:entity_reference_revisions:paragraph'; - $field_edit = [ - 'required' => TRUE, - ]; - $this->fieldUIAddNewField($bundle_path, $field_name, $field_title, $field_type, [], $field_edit); - - $form_display_edit = [ - 'fields[field_content][type]' => 'entity_reference_paragraphs', - ]; - $this->drupalPostForm($bundle_path . '/form-display', $form_display_edit, t('Save')); - - // Attempt to create a paragraphed node with an empty required field. - $title = 'Empty'; - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, ['title[0][value]' => $title], t('Save')); - $this->assertText($field_title . ' field is required'); - - // Attempt to create a paragraphed node with only a paragraph in the - // "remove" mode in the required field. - $title = 'Remove mode'; - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostAjaxForm(NULL, [], 'field_content_image_text_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_content_0_remove'); - $this->assertNoText($field_title . ' field is required'); - $this->drupalPostForm(NULL, ['title[0][value]' => $title], t('Save')); - $this->assertText($field_title . ' field is required'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsWidgetButtonsTest.php b/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsWidgetButtonsTest.php deleted file mode 100644 index 6917bdbe9..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Classic/ParagraphsWidgetButtonsTest.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Classic; - -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests paragraphs widget buttons. - * - * @group paragraphs - */ -class ParagraphsWidgetButtonsTest extends ParagraphsTestBase { - - use FieldUiTestTrait; - - /** - * Tests the widget buttons of paragraphs. - */ - public function testWidgetButtons() { - $this->addParagraphedContentType('paragraphed_test', 'field_paragraphs', 'entity_reference_paragraphs'); - - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']); - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - $this->addParagraphsType('text'); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_text_paragraph_add_more'); - - // Create a node with a Paragraph. - $text = 'recognizable_text'; - $edit = [ - 'title[0][value]' => 'paragraphs_mode_test', - 'field_paragraphs[0][subform][field_text][0][value]' => $text, - ]; - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_paragraph_add_more'); - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('paragraphs_mode_test'); - - // Test the 'Open' mode. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', $text); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText($text); - - // Test the 'Closed' mode. - $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'closed'); - $this->drupalGet('node/' . $node->id() . '/edit'); - // Click "Edit" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_1_edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', $text); - $closed_mode_text = 'closed_mode_text'; - // Click "Collapse" button on both paragraphs. - $edit = ['field_paragraphs[0][subform][field_text][0][value]' => $closed_mode_text]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_collapse'); - $edit = ['field_paragraphs[1][subform][field_text][0][value]' => $closed_mode_text]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_1_collapse'); - // Verify that we have warning message for each paragraph. - $this->assertNoUniqueText('You have unsaved changes on this Paragraph item.'); - $this->assertRaw('<div class="paragraphs-collapsed-description">' . $closed_mode_text); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('paragraphed_test ' . $node->label() . ' has been updated.'); - $this->assertText($closed_mode_text); - - // Test the 'Preview' mode. - $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'preview'); - $this->drupalGet('node/' . $node->id() . '/edit'); - // Click "Edit" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', $closed_mode_text); - $preview_mode_text = 'preview_mode_text'; - $edit = ['field_paragraphs[0][subform][field_text][0][value]' => $preview_mode_text]; - // Click "Collapse" button. - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_collapse'); - $this->assertText('You have unsaved changes on this Paragraph item.'); - $this->assertText($preview_mode_text); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('paragraphed_test ' . $node->label() . ' has been updated.'); - $this->assertText($preview_mode_text); - - // Test the remove/restore function. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertText($preview_mode_text); - // Click "Remove" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_remove'); - $this->assertText('Deleted Paragraph: text_paragraph'); - // Click "Restore" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_restore'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', $preview_mode_text); - $restore_text = 'restore_text'; - $edit = ['field_paragraphs[0][subform][field_text][0][value]' => $restore_text]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test ' . $node->label() . ' has been updated.'); - $this->assertText($restore_text); - - // Test the remove/confirm remove function. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertText($restore_text); - // Click "Remove" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_remove'); - $this->assertText('Deleted Paragraph: text_paragraph'); - // Click "Confirm Removal" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_confirm_remove'); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('paragraphed_test ' . $node->label() . ' has been updated.'); - $this->assertNoText($restore_text); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAccessTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAccessTest.php deleted file mode 100644 index c37f22b26..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAccessTest.php +++ /dev/null @@ -1,143 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\user\RoleInterface; -use Drupal\user\Entity\Role; - -/** - * Tests the access check of paragraphs. - * - * @group paragraphs - */ -class ParagraphsExperimentalAccessTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'image', - 'paragraphs_demo', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - } - - /** - * Tests the paragraph translation. - */ - public function testParagraphAccessCheck() { - $permissions = [ - 'administer site configuration', - 'administer node display', - 'administer paragraph display', - 'create paragraphed_content_demo content', - 'edit any paragraphed_content_demo content', - ]; - $this->loginAsAdmin($permissions); - - // Remove the "access content" for anonymous users. That results in - // anonymous users not being able to "view" the host entity. - /* @var Role $role */ - $role = \Drupal::entityTypeManager() - ->getStorage('user_role') - ->load(RoleInterface::ANONYMOUS_ID); - $role->revokePermission('access content'); - $role->save(); - - // Set field_images from demo to private file storage. - $edit = array( - 'settings[uri_scheme]' => 'private', - ); - $this->drupalPostForm('admin/structure/paragraphs_type/images/fields/paragraph.images.field_images_demo/storage', $edit, t('Save field settings')); - - // Use the experimental widget. - $form_display = EntityFormDisplay::load('node.paragraphed_content_demo.default') - ->setComponent('field_paragraphs_demo', ['type' => 'paragraphs']); - $form_display->save(); - // Create a new demo node. - $this->drupalGet('node/add/paragraphed_content_demo'); - - // Add a new paragraphs images item. - $this->drupalPostForm(NULL, NULL, t('Add Images')); - - $images = $this->drupalGetTestFiles('image'); - - // Create a file, upload it. - file_unmanaged_copy($images[0]->uri, 'temporary://privateImage.jpg'); - $file_path = $this->container->get('file_system') - ->realpath('temporary://privateImage.jpg'); - - // Create a file, upload it. - file_unmanaged_copy($images[1]->uri, 'temporary://privateImage2.jpg'); - $file_path_2 = $this->container->get('file_system') - ->realpath('temporary://privateImage2.jpg'); - - $edit = array( - 'title[0][value]' => 'Security test node', - 'files[field_paragraphs_demo_0_subform_field_images_demo_0][]' => [$file_path, $file_path_2], - ); - - $this->drupalPostForm(NULL, $edit, t('Upload')); - $this->drupalPostForm(NULL, [], t('Preview')); - $img1_url = file_create_url(\Drupal::token()->replace('private://privateImage.jpg')); - $image_url = file_url_transform_relative($img1_url); - $this->assertRaw($image_url, 'Image was found in preview'); - $this->clickLink(t('Back to content editing')); - $this->drupalPostForm(NULL, [], t('Save')); - - $node = $this->drupalGetNodeByTitle('Security test node'); - - $this->drupalGet('node/' . $node->id()); - - // Check the text and image after publish. - $this->assertRaw($image_url, 'Image was found in content'); - - $this->drupalGet($image_url); - $this->assertResponse(200, 'Image could be downloaded'); - - // Logout to become anonymous. - $this->drupalLogout(); - - // @todo Requesting the same $img_url again triggers a caching problem on - // drupal.org test bot, thus we request a different file here. - $img_url = file_create_url(\Drupal::token()->replace('private://privateImage2.jpg')); - $image_url = file_url_transform_relative($img_url); - // Check the text and image after publish. Anonymous should not see content. - $this->assertNoRaw($image_url, 'Image was not found in content'); - - $this->drupalGet($image_url); - $this->assertResponse(403, 'Image could not be downloaded'); - - // Login as admin with no delete permissions. - $this->loginAsAdmin($permissions); - // Create a new demo node. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Text')); - $this->assertText('Text'); - $edit = [ - 'title[0][value]' => 'delete_permissions', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Edit the node. - $this->clickLink(t('Edit')); - // Check the remove button is present. - $this->assertNotNull($this->xpath('//*[@name="field_paragraphs_demo_0_remove"]')); - // Delete the Paragraph and save. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_demo_0_remove'); - $this->drupalPostForm(NULL, [], t('Save')); - $node = $this->getNodeByTitle('delete_permissions'); - $this->assertUrl('node/' . $node->id()); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAddModesTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAddModesTest.php deleted file mode 100644 index 7ee5c5e96..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAddModesTest.php +++ /dev/null @@ -1,226 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\paragraphs\Entity\ParagraphsType; - -/** - * Tests paragraphs add modes. - * - * @group paragraphs - */ -class ParagraphsExperimentalAddModesTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - - /** - * Tests that paragraphs field does not allow default values. - */ - public function testNoDefaultValue() { - $this->loginAsAdmin(); - $this->addParagraphedContentType('paragraphed_test'); - // Edit the field. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields'); - $this->clickLink(t('Edit')); - - // Check that the current field does not allow to add default values. - $this->assertText('No widget available for: field_paragraphs.'); - $this->drupalPostForm(NULL, [], t('Save settings')); - $this->assertText('Saved field_paragraphs configuration.'); - $this->assertResponse(200); - } - - /** - * Tests the field creation when no Paragraphs types are available. - */ - public function testEmptyAllowedTypes() { - $this->loginAsAdmin(); - $this->addParagraphedContentType('paragraphed_test'); - - // Edit the field and save when there are no Paragraphs types available. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields'); - $this->clickLink(t('Edit')); - $this->drupalPostForm(NULL, [], t('Save settings')); - $this->assertText('Saved field_paragraphs configuration.'); - } - - /** - * Tests the add drop down button. - */ - public function testDropDownMode() { - $this->loginAsAdmin(); - // Add two Paragraph types. - $this->addParagraphsType('btext'); - $this->addParagraphsType('dtext'); - - $this->addParagraphedContentType('paragraphed_test', 'paragraphs'); - // Enter to the field config since the weight is set through the form. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $this->drupalPostForm(NULL, [], 'Save settings'); - - $this->setAddMode('paragraphed_test', 'paragraphs', 'dropdown'); - - $this->assertAddButtons(['Add btext', 'Add dtext']); - - $this->addParagraphsType('atext'); - $this->assertAddButtons(['Add btext', 'Add dtext', 'Add atext']); - - $this->setParagraphsTypeWeight('paragraphed_test', 'dtext', 2, 'paragraphs'); - $this->assertAddButtons(['Add dtext', 'Add btext', 'Add atext']); - - $this->setAllowedParagraphsTypes('paragraphed_test', ['dtext', 'atext'], TRUE, 'paragraphs'); - $this->assertAddButtons(['Add dtext', 'Add atext']); - - $this->setParagraphsTypeWeight('paragraphed_test', 'atext', 1, 'paragraphs'); - $this->assertAddButtons(['Add atext', 'Add dtext']); - - $this->setAllowedParagraphsTypes('paragraphed_test', ['atext', 'dtext', 'btext'], TRUE, 'paragraphs'); - $this->assertAddButtons(['Add atext', 'Add dtext', 'Add btext']); - } - - /** - * Tests the add select mode. - */ - public function testSelectMode() { - $this->loginAsAdmin(); - // Add two Paragraph types. - $this->addParagraphsType('btext'); - $this->addParagraphsType('dtext'); - - $this->addParagraphedContentType('paragraphed_test', 'paragraphs'); - // Enter to the field config since the weight is set through the form. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $this->drupalPostForm(NULL, [], 'Save settings'); - - $this->setAddMode('paragraphed_test', 'paragraphs', 'select'); - - $this->assertSelectOptions(['btext', 'dtext'], 'paragraphs'); - - $this->addParagraphsType('atext'); - $this->assertSelectOptions(['btext', 'dtext', 'atext'], 'paragraphs'); - - $this->setParagraphsTypeWeight('paragraphed_test', 'dtext', 2, 'paragraphs'); - $this->assertSelectOptions(['dtext', 'btext', 'atext'], 'paragraphs'); - - $this->setAllowedParagraphsTypes('paragraphed_test', ['dtext', 'atext'], TRUE, 'paragraphs'); - $this->assertSelectOptions(['dtext', 'atext'], 'paragraphs'); - - $this->setParagraphsTypeWeight('paragraphed_test', 'atext', 1, 'paragraphs'); - $this->assertSelectOptions(['atext', 'dtext'], 'paragraphs'); - - $this->setAllowedParagraphsTypes('paragraphed_test', ['atext', 'dtext', 'btext'], TRUE, 'paragraphs'); - $this->assertSelectOptions(['atext', 'dtext', 'btext'], 'paragraphs'); - } - - /** - * Asserts order and quantity of add buttons. - * - * @param array $options - * Array of expected add buttons in its correct order. - */ - protected function assertAddButtons($options) { - $this->drupalGet('node/add/paragraphed_test'); - $buttons = $this->xpath('//input[@class="field-add-more-submit button js-form-submit form-submit"]'); - // Check if the buttons are in the same order as the given array. - foreach ($buttons as $key => $button) { - $this->assertEqual($button['value'], $options[$key]); - } - $this->assertTrue(count($buttons) == count($options), 'The amount of drop down options matches with the given array'); - } - - /** - * Asserts order and quantity of select add options. - * - * @param array $options - * Array of expected select options in its correct order. - * @param string $paragraphs_field - * Name of the paragraphs field to check. - */ - protected function assertSelectOptions($options, $paragraphs_field) { - $this->drupalGet('node/add/paragraphed_test'); - $buttons = $this->xpath('//*[@name="' . $paragraphs_field . '[add_more][add_more_select]"]/option'); - // Check if the options are in the same order as the given array. - foreach ($buttons as $key => $button) { - $this->assertEqual($button['value'], $options[$key]); - } - $this->assertTrue(count($buttons) == count($options), 'The amount of select options matches with the given array'); - $this->assertNotEqual($this->xpath('//*[@name="' . $paragraphs_field .'_add_more"]'), [], 'The add button is displayed'); - } - - /** - * Tests if setting for default paragraph type is working properly. - */ - public function testSettingDefaultParagraphType() { - $this->addParagraphedContentType('paragraphed_test', 'paragraphs'); - $this->loginAsAdmin([ - 'administer content types', - 'administer node form display', - 'edit any paragraphed_test content' - ]); - - // Add a Paragraphed test content. - $paragraphs_type_text_image = ParagraphsType::create([ - 'id' => 'text_image', - 'label' => 'Text + Image', - ]); - $paragraphs_type_text = ParagraphsType::create([ - 'id' => 'text', - 'label' => 'Text', - ]); - $paragraphs_type_text_image->save(); - $paragraphs_type_text->save(); - - $this->setDefaultParagraphType('paragraphed_test', 'paragraphs', 'paragraphs_settings_edit', 'text_image'); - - // Check if default paragraph type is showing. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('Text + Image'); - $this->removeDefaultParagraphType('paragraphed_test'); - - // Disable text_image as default paragraph type. - $this->setDefaultParagraphType('paragraphed_test', 'paragraphs', 'paragraphs_settings_edit', '_none'); - - // Check if is Text + Image is added as default paragraph type. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('No Paragraph added yet.'); - - // Check if default type is created only for new host - $this->setDefaultParagraphType('paragraphed_test', 'paragraphs', 'paragraphs_settings_edit', 'text_image'); - $this->removeDefaultParagraphType('paragraphed_test'); - $edit = ['title[0][value]' => 'New Host']; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/1/edit'); - $this->assertText('No Paragraph added yet.'); - } - - /** - * Tests the default paragraph type behavior for a field with a single type. - */ - public function testDefaultParagraphTypeWithSingleType() { - $this->addParagraphedContentType('paragraphed_test', 'paragraphs'); - $this->loginAsAdmin([ - 'administer content types', - 'administer node form display', - 'edit any paragraphed_test content' - ]); - - // Add a Paragraphed test content. - $paragraphs_type_text = ParagraphsType::create([ - 'id' => 'text', - 'label' => 'Text', - ]); - $paragraphs_type_text->save(); - - // Check that when only one paragraph type is allowed in a content type, - // one instance is automatically added in the 'Add content' dialogue. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertNoText('No Paragraph added yet.'); - - // Check that no paragraph type is automatically added, if the defaut - // setting was set to '- None -'. - $this->setDefaultParagraphType('paragraphed_test', 'paragraphs', 'paragraphs_settings_edit', '_none'); - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('No Paragraph added yet.'); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAdministrationTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAdministrationTest.php deleted file mode 100644 index e2e309b06..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalAdministrationTest.php +++ /dev/null @@ -1,580 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\Tests\paragraphs\FunctionalJavascript\ParagraphsTestBaseTrait; - -/** - * Tests the configuration of paragraphs. - * - * @group paragraphs - */ -class ParagraphsExperimentalAdministrationTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - use ParagraphsTestBaseTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'image', - 'file', - 'views' - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create paragraphs content type. - $this->drupalCreateContentType(array('type' => 'paragraphs', 'name' => 'Paragraphs')); - } - - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsRevisions() { - $this->addParagraphedContentType('article', 'paragraphs'); - $this->loginAsAdmin([ - 'create paragraphs content', - 'administer node display', - 'edit any paragraphs content', - 'administer nodes', - ]); - - // Create paragraphs type Headline + Block. - $this->addParagraphsType('text'); - // Create field types for the text. - static::fieldUIAddNewField('admin/structure/paragraphs_type/text', 'text', 'Text', 'text', array(), array()); - $this->assertText('Saved Text configuration.'); - - // Create an article with paragraphs field. - static::fieldUIAddNewField('admin/structure/types/manage/paragraphs', 'paragraphs', 'Paragraphs', 'entity_reference_revisions', array( - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ), array( - 'settings[handler_settings][target_bundles_drag_drop][text][enabled]' => TRUE, - )); - // Configure article fields. - $this->drupalGet('admin/structure/types/manage/paragraphs/fields'); - $this->clickLink(t('Manage form display')); - $this->drupalPostForm(NULL, array('fields[field_paragraphs][type]' => 'paragraphs'), t('Save')); - - // Create node with our paragraphs. - $this->drupalGet('node/add/paragraphs'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_add_more'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_add_more'); - $edit = [ - 'title[0][value]' => 'TEST TITEL', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Test text 1', - 'field_paragraphs[1][subform][field_text][0][value]' => 'Test text 2', - ]; - $this->drupalPostForm(NULL, $edit + ['status[value]' => TRUE], t('Save')); - - $node = $this->drupalGetNodeByTitle('TEST TITEL'); - $paragraph1 = $node->field_paragraphs[0]->target_id; - $paragraph2 = $node->field_paragraphs[1]->target_id; - - $this->countRevisions($node, $paragraph1, $paragraph2, 1); - - // Edit the node without creating a revision. There should still be only 1 - // revision for nodes and paragraphs. - $edit = [ - 'field_paragraphs[0][subform][field_text][0][value]' => 'Foo Bar 1', - 'revision' => FALSE, - ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); - - $this->countRevisions($node, $paragraph1, $paragraph2, 1); - - // Edit the just created node. Create new revision. Now we should have 2 - // revisions for nodes and paragraphs. - $edit = [ - 'title[0][value]' => 'TEST TITLE', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Foo Bar 2', - 'revision' => TRUE, - ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); - - $this->countRevisions($node, $paragraph1, $paragraph2, 2); - - // Assert the paragraphs have been changed. - $this->assertNoText('Foo Bar 1'); - $this->assertText('Test text 2'); - $this->assertText('Foo Bar 2'); - $this->assertText('TEST TITLE'); - - // Check out the revisions page and assert there are 2 revisions. - $this->drupalGet('node/' . $node->id() . '/revisions'); - $rows = $this->xpath('//tbody/tr'); - // Make sure two revisions available. - $this->assertEqual(count($rows), 2); - // Revert to the old version. - $this->clickLink(t('Revert')); - $this->drupalPostForm(NULL, [], t('Revert')); - $this->drupalGet('node/' . $node->id()); - // Assert the node has been reverted. - $this->assertNoText('Foo Bar 2'); - $this->assertText('Test text 2'); - $this->assertText('Foo Bar 1'); - $this->assertText('TEST TITEL'); - } - - - /** - * Tests the paragraph creation. - */ - public function testParagraphsCreation() { - // Create an article with paragraphs field. - $this->addParagraphedContentType('article'); - $this->loginAsAdmin([ - 'administer site configuration', - 'create article content', - 'create paragraphs content', - 'administer node display', - 'administer paragraph display', - 'edit any article content', - 'delete any article content', - 'access files overview', - ]); - - // Assert suggested 'Add a paragraph type' link when there is no type yet. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->assertText('There is no Paragraphs type yet.'); - $this->drupalGet('admin/structure/types/manage/paragraphs/fields/add-field'); - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference_revisions:paragraph', - 'label' => 'Paragraph', - 'field_name' => 'paragraph', - ]; - $this->drupalPostForm(NULL, $edit, 'Save and continue'); - $this->drupalPostForm(NULL, [], 'Save field settings'); - $this->assertLinkByHref('admin/structure/paragraphs_type/add'); - $this->clickLink('here'); - $this->assertUrl('admin/structure/paragraphs_type/add'); - - $this->drupalGet('admin/structure/paragraphs_type'); - $this->clickLink(t('Add paragraph type')); - $this->assertTitle('Add Paragraphs type | Drupal'); - // Create paragraph type text + image. - $this->addParagraphsType('text_image'); - $this->drupalGet('admin/structure/paragraphs_type/text_image'); - $this->assertTitle('Edit text_image paragraph type | Drupal'); - // Create field types for text and image. - static::fieldUIAddNewField('admin/structure/paragraphs_type/text_image', 'text', 'Text', 'text_long', array(), array()); - $this->assertText('Saved Text configuration.'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/text_image', 'image', 'Image', 'image', array(), array('settings[alt_field_required]' => FALSE)); - $this->assertText('Saved Image configuration.'); - - // Create paragraph type Nested test. - $this->addParagraphsType('nested_test'); - - static::fieldUIAddNewField('admin/structure/paragraphs_type/nested_test', 'paragraphs', 'Paragraphs', 'entity_reference_revisions', array( - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ), array()); - - // Change the add more button to select mode. - $this->clickLink(t('Manage form display')); - $this->drupalPostAjaxForm(NULL, ['fields[field_paragraphs][type]' => 'paragraphs'], 'field_paragraphs_settings_edit'); - $this->drupalPostForm(NULL, ['fields[field_paragraphs][settings_edit_form][settings][add_mode]' => 'select'], t('Update')); - $this->drupalPostForm(NULL, [], t('Save')); - - // Create paragraph type image. - $this->addParagraphsType('image'); - // Create field types for image. - static::fieldUIAddNewField('admin/structure/paragraphs_type/image', 'image_only', 'Image only', 'image', array(), array()); - $this->assertText('Saved Image only configuration.'); - - $this->drupalGet('admin/structure/paragraphs_type'); - $rows = $this->xpath('//tbody/tr'); - // Make sure 2 types are available with their label. - $this->assertEqual(count($rows), 3); - $this->assertText('text_image'); - $this->assertText('image'); - // Make sure there is an edit link for each type. - $this->clickLink(t('Edit')); - // Make sure the field UI appears. - $this->assertLink('Manage fields'); - $this->assertLink('Manage form display'); - $this->assertLink('Manage display'); - $this->assertTitle('Edit image paragraph type | Drupal'); - - // Test for "Add mode" setting. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $field_name = 'field_paragraphs'; - - // Click on the widget settings button to open the widget settings form. - $this->drupalPostAjaxForm(NULL, ['fields[field_paragraphs][type]' => 'paragraphs'], $field_name . "_settings_edit"); - - // Enable setting. - $edit = array('fields[' . $field_name . '][settings_edit_form][settings][add_mode]' => 'button'); - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Check if the setting is stored. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $this->assertText('Add mode: Buttons', 'Checking the settings value.'); - - $this->drupalPostAjaxForm(NULL, array(), $field_name . "_settings_edit"); - // Assert the 'Buttons' option is selected. - $this->assertOptionSelected('edit-fields-field-paragraphs-settings-edit-form-settings-add-mode', 'button', 'Updated value is correct!.'); - - // Add two Text + Image paragraphs in article. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_image_add_more'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_image_add_more'); - // Create an 'image' file, upload it. - $text = 'Trust me I\'m an image'; - file_put_contents('temporary://myImage1.jpg', $text); - file_put_contents('temporary://myImage2.jpg', $text); - - $edit = array( - 'title[0][value]' => 'Test article', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Test text 1', - 'files[field_paragraphs_0_subform_field_image_0]' => drupal_realpath('temporary://myImage1.jpg'), - 'field_paragraphs[1][subform][field_text][0][value]' => 'Test text 2', - 'files[field_paragraphs_1_subform_field_image_0]' => drupal_realpath('temporary://myImage2.jpg'), - ); - $this->drupalPostForm(NULL, $edit, t('Save')); - - $node = $this->drupalGetNodeByTitle('Test article'); - $img1_url = file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/myImage1.jpg')); - $img2_url = file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/myImage2.jpg')); - - // Check the text and image after publish. - $this->assertText('Test text 1'); - $this->assertRaw('<img src="' . file_url_transform_relative($img1_url)); - $this->assertText('Test text 2'); - $this->assertRaw('<img src="' . file_url_transform_relative($img2_url)); - - // Tests for "Edit mode" settings. - // Test for closed setting. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - // Click on the widget settings button to open the widget settings form. - $this->drupalPostAjaxForm(NULL, array(), "field_paragraphs_settings_edit"); - // Enable setting. - $edit = array('fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'closed'); - $this->drupalPostForm(NULL, $edit, t('Save')); - // Check if the setting is stored. - $this->assertText('Edit mode: Closed', 'Checking the settings value.'); - $this->drupalPostAjaxForm(NULL, array(), "field_paragraphs_settings_edit"); - // Assert the 'Closed' option is selected. - $this->assertOptionSelected('edit-fields-field-paragraphs-settings-edit-form-settings-edit-mode', 'closed', 'Updated value correctly.'); - $this->drupalGet('node/1/edit'); - // The textareas for paragraphs should not be visible. - $this->assertNoRaw('field_paragraphs[0][subform][field_text][0][value]'); - $this->assertNoRaw('field_paragraphs[1][subform][field_text][0][value]'); - $this->assertRaw('<div class="paragraphs-collapsed-description">myImage1.jpg, Test text 1'); - $this->assertRaw('<div class="paragraphs-collapsed-description">myImage2.jpg, Test text 2'); - - // Test for preview option. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $this->drupalPostAjaxForm(NULL, array(), "field_paragraphs_settings_edit"); - $edit = [ - 'fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'closed', - 'fields[field_paragraphs][settings_edit_form][settings][closed_mode]' => 'preview', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('Edit mode: Closed', 'Checking the "Edit mode" setting value.'); - $this->assertText('Closed mode: Preview', 'Checking the "Closed mode" settings value.'); - $this->drupalGet('node/1/edit'); - // The texts in the paragraphs should be visible. - $this->assertNoRaw('field_paragraphs[0][subform][field_text][0][value]'); - $this->assertNoRaw('field_paragraphs[1][subform][field_text][0][value]'); - $this->assertText('Test text 1'); - $this->assertText('Test text 2'); - - // Test for open option. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $this->drupalPostAjaxForm(NULL, array(), "field_paragraphs_settings_edit"); - // Assert the "Closed" and "Preview" options are selected. - $this->assertOptionSelected('edit-fields-field-paragraphs-settings-edit-form-settings-edit-mode', 'closed', 'Correctly updated the "Edit mode" value.'); - $this->assertOptionSelected('edit-fields-field-paragraphs-settings-edit-form-settings-closed-mode', 'preview', 'Correctly updated the "Closed mode" value.'); - // Restore the value to Open for next test. - $edit = array('fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'open'); - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/1/edit'); - // The textareas for paragraphs should be visible. - $this->assertRaw('field_paragraphs[0][subform][field_text][0][value]'); - $this->assertRaw('field_paragraphs[1][subform][field_text][0][value]'); - - $paragraphs = Paragraph::loadMultiple(); - $this->assertEqual(count($paragraphs), 2, 'Two paragraphs in article'); - - // Check article edit page. - $this->drupalGet('node/' . $node->id() . '/edit'); - // Check both paragraphs in edit page. - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', 'Test text 1'); - $this->assertRaw('<a href="' . $img1_url . '" type="image/jpeg; length=21">myImage1.jpg</a>'); - $this->assertFieldByName('field_paragraphs[1][subform][field_text][0][value]', 'Test text 2'); - $this->assertRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - // Remove 2nd paragraph. - $this->drupalPostForm(NULL, NULL, t('Remove')); - $this->assertNoField('field_paragraphs[1][subform][field_text][0][value]'); - $this->assertNoRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - // Assert the paragraph is not deleted unless the user saves the node. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - // Remove the second paragraph. - $this->drupalPostForm(NULL, [], t('Remove')); - $this->assertNoRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - $edit = [ - 'field_paragraphs[0][subform][field_image][0][alt]' => 'test_alt', - 'field_paragraphs[0][subform][field_image][0][width]' => 300, - 'field_paragraphs[0][subform][field_image][0][height]' => 300, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Assert the paragraph is deleted after the user saves the node. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertNoRaw('<a href="' . $img2_url . '" type="image/jpeg; length=21">myImage2.jpg</a>'); - - // Delete the node. - $this->clickLink(t('Delete')); - $this->drupalPostForm(NULL, NULL, t('Delete')); - $this->assertText('Test article has been deleted.'); - - // Check if the publish/unpublish option works. - $this->drupalGet('admin/structure/paragraphs_type/text_image/form-display'); - $edit = [ - 'fields[status][type]' => 'boolean_checkbox', - 'fields[status][region]' => 'content', - ]; - - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/add/article'); - $this->drupalPostForm(NULL, NULL, t('Add text_image')); - $this->assertRaw('edit-field-paragraphs-0-subform-status-value'); - $edit = [ - 'title[0][value]' => 'Example publish/unpublish', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Example published and unpublished', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText(t('Example published and unpublished')); - $this->clickLink(t('Edit')); - $edit = [ - 'field_paragraphs[0][subform][status][value]' => FALSE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertNoText(t('Example published and unpublished')); - - // Set the fields as required. - $this->drupalGet('admin/structure/types/manage/article/fields'); - $this->clickLink('Edit', 1); - $this->drupalPostForm(NULL, ['preview_mode' => '1'], t('Save content type')); - $this->drupalGet('admin/structure/paragraphs_type/nested_test/fields'); - $this->clickLink('Edit'); - $this->drupalPostForm(NULL, ['required' => TRUE], t('Save settings')); - - // Add a new article. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_nested_test_add_more'); - - // Ensure that nested header actions do not add a visible weight field. - $this->assertNoFieldByName('field_paragraphs[0][subform][field_paragraphs][header_actions][_weight]'); - - $edit = [ - 'field_paragraphs[0][subform][field_paragraphs][add_more][add_more_select]' => 'image', - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_subform_field_paragraphs_add_more'); - // Test the new field is displayed. - $this->assertFieldByName('files[field_paragraphs_0_subform_field_paragraphs_0_subform_field_image_only_0]'); - - // Add an image to the required field. - $edit = array( - 'title[0][value]' => 'test required', - 'files[field_paragraphs_0_subform_field_paragraphs_0_subform_field_image_only_0]' => drupal_realpath('temporary://myImage2.jpg'), - ); - $this->drupalPostForm(NULL, $edit, t('Save')); - $edit = [ - 'field_paragraphs[0][subform][field_paragraphs][0][subform][field_image_only][0][width]' => 100, - 'field_paragraphs[0][subform][field_paragraphs][0][subform][field_image_only][0][height]' => 100, - 'field_paragraphs[0][subform][field_paragraphs][0][subform][field_image_only][0][alt]' => 'Alternative_text', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('test required has been created.'); - $this->assertNoRaw('This value should not be null.'); - - // Test that unsupported widgets are not displayed. - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $select = $this->xpath('//*[@id="edit-fields-field-paragraphs-type"]')[0]; - $this->assertEqual(count($select->option), 2); - $this->assertRaw('value="paragraphs" selected="selected"'); - - // Check that Paragraphs is not displayed as an entity_reference field - // reference option. - $this->drupalGet('admin/structure/types/manage/article/fields/add-field'); - $edit = [ - 'new_storage_type' => 'entity_reference', - 'label' => 'unsupported field', - 'field_name' => 'unsupportedfield', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and continue')); - $this->assertNoOption('edit-settings-target-type', 'paragraph'); - - // Test that all Paragraph types can be referenced if none is selected. - $this->addParagraphsType('nested_double_test'); - static::fieldUIAddExistingField('admin/structure/paragraphs_type/nested_double_test', 'field_paragraphs', 'paragraphs_1'); - $this->clickLink(t('Manage form display')); - $this->drupalPostForm(NULL, [], 'Save'); - //$this->drupalPostForm(NULL, array('fields[field_paragraphs][type]' => 'entity_reference_revisions_entity_view'), t('Save')); - static::fieldUIAddNewField('admin/structure/paragraphs_type/nested_double_test', 'paragraphs_2', 'paragraphs_2', 'entity_reference_revisions', array( - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ), array()); - $this->clickLink(t('Manage form display')); - $this->drupalPostForm(NULL, [], 'Save'); - $this->drupalPostAjaxForm('node/add/article', [], 'field_paragraphs_nested_test_add_more'); - $edit = [ - 'field_paragraphs[0][subform][field_paragraphs][add_more][add_more_select]' => 'nested_double_test', - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_subform_field_paragraphs_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_subform_field_paragraphs_0_subform_field_paragraphs_image_add_more'); - $edit = array( - 'title[0][value]' => 'Nested twins', - ); - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('Nested twins has been created.'); - $this->assertNoText('This entity (paragraph: ) cannot be referenced.'); - - // Set the fields as not required. - $this->drupalGet('admin/structure/types/manage/article/fields'); - $this->clickLink('Edit', 1); - $this->drupalPostForm(NULL, ['required' => FALSE], t('Save settings')); - - // Set the Paragraph field edit mode to "Closed" and the closed mode to - // "Summary". - $settings = [ - 'edit_mode' => 'closed', - 'closed_mode' => 'summary', - ]; - $this->setParagraphsWidgetSettings('article', 'field_paragraphs', $settings); - - $this->addParagraphsType('node_test'); - - // Add a required node reference field. - static::fieldUIAddNewField('admin/structure/paragraphs_type/node_test', 'entity_reference', 'Entity reference', 'entity_reference', array( - 'settings[target_type]' => 'node', - 'cardinality' => '-1' - ), [ - 'settings[handler_settings][target_bundles][article]' => TRUE, - 'required' => TRUE, - ]); - $node = $this->drupalGetNodeByTitle('Nested twins'); - - // Create a node with a reference in a Paragraph. - $this->drupalPostAjaxForm('node/add/article', [], 'field_paragraphs_node_test_add_more'); - \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); - $edit = [ - 'field_paragraphs[0][subform][field_entity_reference][0][target_id]' => $node->label() . ' (' . $node->id() . ')', - 'title[0][value]' => 'choke test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Delete the referenced node. - $node->delete(); - // Edit the node with the reference. - $this->clickLink(t('Edit')); - // Since we have validation error (reference to deleted node), paragraph is - // by default in edit mode. - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][0][target_id]'); - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][1][target_id]'); - // Assert the validation error message. - $this->assertText('The referenced entity (node: 4) does not exist'); - // Triggering unrelated button, assert that error message is still present. - $this->drupalPostForm(NULL, [], t('Add another item')); - $this->assertText('The referenced entity (node: 4) does not exist'); - $this->assertText('Entity reference (value 1) field is required.'); - // Try to collapse with an invalid reference. - $this->drupalPostAjaxForm(NULL, ['field_paragraphs[0][subform][field_entity_reference][0][target_id]' => 'foo'], 'field_paragraphs_0_collapse'); - // Paragraph should be still in edit mode. - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][0][target_id]'); - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][1][target_id]'); - $this->drupalPostForm(NULL, [], t('Add another item')); - // Assert the validation message. - $this->assertText('There are no entities matching "foo".'); - // Fix the broken reference. - $node = $this->drupalGetNodeByTitle('Example publish/unpublish'); - $edit = ['field_paragraphs[0][subform][field_entity_reference][0][target_id]' => $node->label() . ' (' . $node->id() . ')']; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('choke test has been updated.'); - $this->assertLink('Example publish/unpublish'); - // Delete the new referenced node. - $node->delete(); - - // Set the Paragraph field closed mode to "Preview". - $settings = [ - 'edit_mode' => 'closed', - 'closed_mode' => 'preview', - ]; - $this->setParagraphsWidgetSettings('article', 'field_paragraphs', $settings); - - $node = $this->drupalGetNodeByTitle('choke test'); - // Attempt to edit the Paragraph. - $this->drupalGet('node/' . $node->id() . '/edit'); - // Since we have another validation error, the paragraph is by default in - // the edit mode again. - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][0][target_id]'); - $this->assertFieldByName('field_paragraphs[0][subform][field_entity_reference][1][target_id]'); - // Try to save with and invalid reference. - $edit = ['field_paragraphs[0][subform][field_entity_reference][0][target_id]' => 'foo']; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('There are no entities matching "foo".'); - // Remove the Paragraph and save the node. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_remove'); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('choke test has been updated.'); - - // Verify that the text displayed is correct when no paragraph has been - // added yet. - $this->drupalGet('node/add/article'); - $this->assertText('No Paragraph added yet.'); - - $this->drupalGet('admin/content/files'); - $this->clickLink('1 place'); - $label = $this->xpath('//tbody/tr/td[1]'); - $this->assertEqual(trim(htmlspecialchars_decode(strip_tags($label[0]->asXML()))), 'test required > field_paragraphs > Paragraphs'); - } - - /** - * Asserts that a select option in the current page is checked. - * - * @param string $id - * ID of select field to assert. - * @param string $option - * Option to assert. - * @param string $message - * (optional) A message to display with the assertion. Do not translate - * messages: use format_string() to embed variables in the message text, not - * t(). If left blank, a default message will be displayed. - * @param string $group - * (optional) The group this message is in, which is displayed in a column - * in test output. Use 'Debug' to indicate this is debugging output. Do not - * translate this string. Defaults to 'Browser'; most tests do not override - * this default. - * - * @return bool - * TRUE on pass, FALSE on fail. - * - * @todo Remove function once core issue is resolved: https://www.drupal.org/node/2530092 - */ - protected function assertOptionSelected($id, $option, $message = '', $group = 'Browser') { - $elements = $this->xpath('//select[contains(@id, :id)]//option[@value=:option]', array(':id' => $id, ':option' => $option)); - return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['selected']), $message ? $message : SafeMarkup::format('Option @option for field @id is selected.', array('@option' => $option, '@id' => $id)), $group); - } - - /** - * Helper function for revision counting. - */ - private function countRevisions($node, $paragraph1, $paragraph2, $revisions_count) { - $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); - $this->assertEqual($node_revisions_count, $revisions_count); - $this->assertEqual(\Drupal::entityQuery('paragraph')->condition('id', $paragraph1)->allRevisions()->count()->execute(), $revisions_count); - $this->assertEqual(\Drupal::entityQuery('paragraph')->condition('id', $paragraph2)->allRevisions()->count()->execute(), $revisions_count); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalBehaviorsTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalBehaviorsTest.php deleted file mode 100644 index c41b744e1..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalBehaviorsTest.php +++ /dev/null @@ -1,323 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\paragraphs\Entity\Paragraph; - -/** - * Tests paragraphs behavior plugins. - * - * @group paragraphs - */ -class ParagraphsExperimentalBehaviorsTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = ['image', 'file', 'views']; - - /** - * Tests the behavior plugins for paragraphs. - */ - public function testBehaviorPluginsFields() { - $this->addParagraphedContentType('paragraphed_test'); - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']); - - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - - // Check default configuration. - $this->drupalGet('admin/structure/paragraphs_type/' . $paragraph_type); - $this->assertFieldByName('behavior_plugins[test_text_color][settings][default_color]', 'blue'); - - $this->assertText('Behavior plugins are only supported by the EXPERIMENTAL paragraphs widget'); - // Enable the test plugins, with an invalid configuration value. - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - 'behavior_plugins[test_text_color][enabled]' => TRUE, - 'behavior_plugins[test_text_color][settings][default_color]' => 'red', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('Red can not be used as the default color.'); - - // Ensure the form can be saved with an invalid configuration value when - // the plugin is not selected. - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - 'behavior_plugins[test_text_color][enabled]' => FALSE, - 'behavior_plugins[test_text_color][settings][default_color]' => 'red', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('Saved the text_paragraph Paragraphs type.'); - - // Ensure it can be saved with a valid value and that the defaults are - // correct. - $this->drupalGet('admin/structure/paragraphs_type/' . $paragraph_type); - $this->assertFieldChecked('edit-behavior-plugins-test-bold-text-enabled'); - $this->assertNoFieldChecked('edit-behavior-plugins-test-text-color-enabled'); - $this->assertFieldByName('behavior_plugins[test_text_color][settings][default_color]', 'blue'); - - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - 'behavior_plugins[test_text_color][enabled]' => TRUE, - 'behavior_plugins[test_text_color][settings][default_color]' => 'green', - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type, $edit, t('Save')); - $this->assertText('Saved the text_paragraph Paragraphs type.'); - - $this->drupalGet('node/add/paragraphed_test'); - - // Behavior plugin settings is not available to users without - // "edit behavior plugin settings" permission. - $this->assertNoFieldByName('field_paragraphs[0][behavior_plugins][test_text_color][text_color]', 'green'); - - $this->loginAsAdmin([ - 'create paragraphed_test content', - 'edit any paragraphed_test content', - 'edit behavior plugin settings', - ]); - - // Create a node with a Paragraph. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertFieldByName('field_paragraphs[0][behavior_plugins][test_text_color][text_color]', 'green'); - // Setting a not allowed value in the text color plugin text field. - $plugin_text = 'green'; - $edit = [ - 'title[0][value]' => 'paragraphs_plugins_test', - 'field_paragraphs[0][subform][field_text][0][value]' => 'amazing_plugin_test', - 'field_paragraphs[0][behavior_plugins][test_text_color][text_color]' => $plugin_text, - ]; - // Assert that the behavior form is after the dropbutton. - $behavior_xpath = $this->xpath("//div[@id = 'edit-field-paragraphs-0-top']/following-sibling::*[1][@id = 'edit-field-paragraphs-0-behavior-plugins-test-text-color']"); - $this->assertNotEqual($behavior_xpath, FALSE, 'Behavior form position incorrect'); - - $this->drupalPostForm(NULL, $edit, t('Save')); - // Asserting that the error message is shown. - $this->assertText('The only allowed values are blue and red.'); - // Updating the text color to an allowed value. - $plugin_text = 'red'; - $edit = [ - 'field_paragraphs[0][behavior_plugins][test_text_color][text_color]' => $plugin_text, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Assert that the class has been added to the element. - $this->assertRaw('class="red_plugin_text'); - - $this->clickLink('Edit'); - // Assert the plugin fields populate the stored values. - $this->assertFieldByName('field_paragraphs[0][behavior_plugins][test_text_color][text_color]', $plugin_text); - - // Update the value of both plugins. - $updated_text = 'blue'; - $edit = [ - 'field_paragraphs[0][behavior_plugins][test_text_color][text_color]' => $updated_text, - 'field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertNoRaw('class="red_plugin_text'); - $this->assertRaw('class="blue_plugin_text bold_plugin_text'); - $this->clickLink('Edit'); - // Assert the plugin fields populate the stored values. - $this->assertFieldByName('field_paragraphs[0][behavior_plugins][test_text_color][text_color]', $updated_text); - $this->assertFieldByName('field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]', TRUE); - - $this->loginAsAdmin([ - 'edit any paragraphed_test content', - ]); - - $node = $this->getNodeByTitle('paragraphs_plugins_test'); - $this->drupalGet('node/' . $node->id() . '/edit'); - - $this->assertNoFieldByName('field_paragraphs[0][behavior_plugins][test_text_color][text_color]', $updated_text); - $this->assertNoFieldByName('field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]', TRUE); - - $this->drupalPostForm(NULL, [], t('Save')); - - // Make sure that values don't change if a user without the 'edit behavior - // plugin settings' permission saves a node with paragraphs and enabled - // behaviors. - $this->assertRaw('class="blue_plugin_text bold_plugin_text'); - $this->assertNoRaw('class="red_plugin_text'); - - // Test plugin applicability. Add a paragraph type. - $paragraph_type = 'text_paragraph_test'; - $this->addParagraphsType($paragraph_type); - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text_test', 'Text', 'text_long', [], []); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'image', 'Image', 'image', [], []); - // Assert if the plugin is listed on the edit form of the paragraphs type. - $this->drupalGet('admin/structure/paragraphs_type/' . $paragraph_type); - $this->assertNoFieldByName('behavior_plugins[test_bold_text][enabled]'); - $this->assertFieldByName('behavior_plugins[test_text_color][enabled]'); - $this->assertFieldByName('behavior_plugins[test_field_selection][enabled]'); - $this->assertText('Choose paragraph field to be applied.'); - // Assert that Field Selection Filter plugin properly filters field types. - $this->assertOptionByText('edit-behavior-plugins-test-field-selection-settings-field-selection-filter', t('Image')); - // Check that Field Selection Plugin does not filter any field types. - $this->assertOptionByText('edit-behavior-plugins-test-field-selection-settings-field-selection', t('Image')); - $this->assertOptionByText('edit-behavior-plugins-test-field-selection-settings-field-selection', t('Text')); - - // Test a plugin without behavior fields. - $edit = [ - 'behavior_plugins[test_dummy_behavior][enabled]' => TRUE, - 'behavior_plugins[test_text_color][enabled]' => TRUE, - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type, $edit, t('Save')); - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_text_paragraph_test_add_more'); - $edit = [ - 'title[0][value]' => 'paragraph with no fields', - 'field_paragraphs[0][subform][field_text_test][0][value]' => 'my behavior plugin does not have any field', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertRaw('dummy_plugin_text'); - - // Tests behavior plugin on paragraph type with no fields. - $this->addParagraphsType('fieldless'); - $this->drupalPostForm('admin/structure/paragraphs_type/fieldless', ['behavior_plugins[test_dummy_behavior][enabled]' => TRUE], t('Save')); - - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_fieldless_add_more'); - $edit = ['title[0][value]' => t('Fieldless')]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - $this->assertResponse(200); - } - - /** - * Tests the behavior plugins summary for paragraphs closed mode. - */ - public function testCollapsedSummary() { - $this->addParagraphedContentType('paragraphed_test'); - $this->loginAsAdmin([ - 'create paragraphed_test content', - 'edit any paragraphed_test content', - 'edit behavior plugin settings', - ]); - - // Add a text paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'closed'); - // Enable plugins for the text paragraph type. - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - 'behavior_plugins[test_text_color][enabled]' => TRUE, - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type, $edit, t('Save')); - - // Add a nested Paragraph type. - $paragraph_type = 'nested_paragraph'; - $this->addParagraphsType($paragraph_type); - $this->addParagraphsField('nested_paragraph', 'paragraphs', 'paragraph'); - // Enable plugins for the nested paragraph type. - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type, $edit, t('Save')); - - // Add a node and enabled plugins. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_nested_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_1_subform_paragraphs_text_paragraph_add_more'); - - $this->assertField('field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]'); - $this->assertField('field_paragraphs[1][behavior_plugins][test_bold_text][bold_text]'); - - $edit = [ - 'title[0][value]' => 'collapsed_test', - 'field_paragraphs[0][subform][field_text][0][value]' => 'first_paragraph', - 'field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]' => TRUE, - 'field_paragraphs[1][subform][paragraphs][0][subform][field_text][0][value]' => 'nested_paragraph', - 'field_paragraphs[1][behavior_plugins][test_bold_text][bold_text]' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Assert that the summary includes the text of the behavior plugins. - $this->clickLink('Edit'); - $this->assertRaw('class="paragraphs-collapsed-description">first_paragraph, Text color: blue, Bold: Yes'); - $this->assertRaw('class="paragraphs-collapsed-description">1 child, nested_paragraph, Text color: blue, Bold: No, Bold: Yes'); - - // Add an empty nested paragraph. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_nested_paragraph_add_more'); - $edit = [ - 'title[0][value]' => 'collapsed_test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Check an empty nested paragraph summary. - $this->clickLink('Edit'); - $this->assertRaw('class="paragraphs-collapsed-description">'); - - } - - /** - * Tests the behavior plugins subform state submit. - */ - public function testBehaviorSubform() { - $this->addParagraphedContentType('paragraphed_test'); - $this->loginAsAdmin([ - 'create paragraphed_test content', - 'edit any paragraphed_test content', - 'edit behavior plugin settings', - ]); - - // Add a text paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - // Enable plugins for the text paragraph type. - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - 'behavior_plugins[test_text_color][enabled]' => TRUE, - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type, $edit, t('Save')); - - // Add a nested Paragraph type. - $paragraph_type = 'nested_paragraph'; - $this->addParagraphsType($paragraph_type); - static::fieldUIAddNewField('admin/structure/paragraphs_type/nested_paragraph', 'nested', 'Nested', 'field_ui:entity_reference_revisions:paragraph', [ - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ], []); - // Enable plugins for the nested paragraph type. - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type, $edit, t('Save')); - - // Add a node and enabled plugins. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_nested_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_subform_field_nested_text_paragraph_add_more'); - $edit = [ - 'title[0][value]' => 'collapsed_test', - 'field_paragraphs[0][subform][field_nested][0][subform][field_text][0][value]' => 'nested text paragraph', - 'field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]' => TRUE, - 'field_paragraphs[1][subform][field_text][0][value]' => 'first_paragraph', - 'field_paragraphs[1][behavior_plugins][test_bold_text][bold_text]' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - $this->clickLink('Edit'); - $edit = [ - 'field_paragraphs[0][_weight]' => 1, - 'field_paragraphs[1][behavior_plugins][test_bold_text][bold_text]' => FALSE, - 'field_paragraphs[1][behavior_plugins][test_text_color][text_color]' => 'red', - 'field_paragraphs[1][_weight]' => 0, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertNoErrorsLogged(); - $this->clickLink('Edit'); - $this->assertFieldByName('field_paragraphs[0][behavior_plugins][test_text_color][text_color]', 'red'); - - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalConfigTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalConfigTest.php deleted file mode 100644 index 728136208..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalConfigTest.php +++ /dev/null @@ -1,252 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\node\Entity\NodeType; - -/** - * Tests paragraphs configuration. - * - * @group paragraphs - */ -class ParagraphsExperimentalConfigTest extends ParagraphsExperimentalTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'content_translation', - ); - - /** - * Tests adding paragraphs with no translation enabled. - */ - public function testFieldTranslationDisabled() { - $this->loginAsAdmin([ - 'administer languages', - 'administer content translation', - 'create content translations', - 'translate any entity', - ]); - - // Add a paragraphed content type. - - $this->addParagraphedContentType('paragraphed_test'); - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - // Add a second language. - ConfigurableLanguage::create(['id' => 'de'])->save(); - - // Enable translation for paragraphed content type. Do not enable - // translation for the ERR paragraphs field nor for fields on the - // paragraph type. - $edit = [ - 'entity_types[node]' => TRUE, - 'settings[node][paragraphed_test][translatable]' => TRUE, - 'settings[node][paragraphed_test][fields][field_paragraphs]' => FALSE, - ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); - - // Create a node with a paragraph. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_paragraph_type_test_add_more'); - $edit = ['title[0][value]' => 'paragraphed_title']; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Attempt to add a translation. - $node = $this->drupalGetNodeByTitle('paragraphed_title'); - $this->drupalGet('node/' . $node->id() . '/translations'); - $this->clickLink(t('Add')); - // Save the translation. - $this->drupalPostForm(NULL, [], t('Save (this translation)')); - $this->assertText('paragraphed_test paragraphed_title has been updated.'); - } - - /** - * Tests content translation form translatability constraints messages. - */ - public function testContentTranslationForm() { - $this->loginAsAdmin([ - 'administer languages', - 'administer content translation', - 'create content translations', - 'translate any entity', - ]); - - // Check warning message is displayed. - $this->drupalGet('admin/config/regional/content-language'); - $this->assertText('(* unsupported) Paragraphs fields do not support translation.'); - - $this->addParagraphedContentType('paragraphed_test'); - // Check error message is not displayed. - $this->drupalGet('admin/config/regional/content-language'); - $this->assertText('(* unsupported) Paragraphs fields do not support translation.'); - $this->assertNoRaw('<div class="messages messages--error'); - - // Add a second language. - ConfigurableLanguage::create(['id' => 'de'])->save(); - - // Enable translation for paragraphed content type. - $edit = [ - 'entity_types[node]' => TRUE, - 'settings[node][paragraphed_test][translatable]' => TRUE, - 'settings[node][paragraphed_test][fields][field_paragraphs]' => FALSE, - ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); - - // Check error message is still not displayed. - $this->drupalGet('admin/config/regional/content-language'); - $this->assertText('(* unsupported) Paragraphs fields do not support translation.'); - $this->assertNoRaw('<div class="messages messages--error'); - - // Check content type field management warning. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.field_paragraphs'); - $this->assertText('Paragraphs fields do not support translation.'); - - // Make the paragraphs field translatable. - $edit = [ - 'entity_types[node]' => TRUE, - 'settings[node][paragraphed_test][translatable]' => TRUE, - 'settings[node][paragraphed_test][fields][field_paragraphs]' => TRUE, - ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); - - // Check content type field management error. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.field_paragraphs'); - $this->assertText('Paragraphs fields do not support translation.'); - $this->assertRaw('<div class="messages messages--error'); - - // Check a not paragraphs translatable field does not display the message. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/add-field'); - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference:node', - 'label' => 'new_no_field_paragraphs', - 'field_name' => 'new_no_field_paragraphs', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and continue')); - $this->drupalPostForm(NULL, [], t('Save field settings')); - $this->assertNoText('Paragraphs fields do not support translation.'); - $this->assertNoRaw('<div class="messages messages--warning'); - } - - /** - * Tests that we can use paragraphs widget only for paragraphs. - */ - public function testAvoidUsingParagraphsWithWrongEntity() { - $node_type = NodeType::create([ - 'type' => 'article', - 'name' => 'article', - ]); - $node_type->save(); - $this->loginAsAdmin([ - 'edit any article content', - ]); - $this->addParagraphsType('paragraphed_type'); - - // Create reference to node. - $this->fieldUIAddNewField('admin/structure/types/manage/article', 'node_reference', 'NodeReference', 'entity_reference_revisions', [ - 'cardinality' => 'number', - 'cardinality_number' => 1, - 'settings[target_type]' => 'node', - ], [ - 'settings[handler_settings][target_bundles][article]' => 'article', - ]); - $this->drupalGet('admin/structure/types/manage/article/form-display'); - $this->assertNoOption('edit-fields-field-node-reference-type', 'entity_reference_paragraphs'); - $this->assertNoOption('edit-fields-field-node-reference-type', 'paragraphs'); - } - - /** - * Test included Paragraph types. - */ - public function testIncludedParagraphTypes() { - $this->loginAsAdmin(); - // Add a Paragraph content type and 2 Paragraphs types. - $this->addParagraphedContentType('paragraphed_test', 'paragraphs'); - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $edit = [ - 'settings[handler_settings][negate]' => 0, - 'settings[handler_settings][target_bundles_drag_drop][paragraph_type_test][enabled]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, 'Save settings'); - $this->assertText('Saved paragraphs configuration.'); - - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('Add paragraph_type_test'); - $this->assertNoText('Add text'); - $edit = [ - 'title[0][value]' => 'Testing included types' - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test Testing included types has been created.'); - - // Include all types. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $edit = [ - 'settings[handler_settings][negate]' => 0, - 'settings[handler_settings][target_bundles_drag_drop][text][enabled]' => 1, - 'settings[handler_settings][target_bundles_drag_drop][paragraph_type_test][enabled]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, 'Save settings'); - $this->drupalGet('node/add/paragraphed_test'); - $button_paragraphed_type_test = $this->xpath('//input[@id=:id]', [':id' => 'paragraphs-paragraph-type-test-add-more']); - $button_text = $this->xpath('//input[@id=:id]', [':id' => 'paragraphs-text-add-more']); - $this->assertNotNull($button_paragraphed_type_test); - $this->assertNotNull($button_text); - $edit = [ - 'title[0][value]' => 'Testing all excluded types' - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test Testing all excluded types has been created.'); - } - - /** - * Test excluded Paragraph types. - */ - public function testExcludedParagraphTypes() { - $this->loginAsAdmin(); - // Add a Paragraph content type and 2 Paragraphs types. - $this->addParagraphedContentType('paragraphed_test', 'paragraphs'); - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $edit = [ - 'settings[handler_settings][negate]' => 1, - 'settings[handler_settings][target_bundles_drag_drop][text][enabled]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, 'Save settings'); - $this->assertText('Saved paragraphs configuration.'); - - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('Add paragraph_type_test'); - $this->assertNoText('Add text'); - $edit = [ - 'title[0][value]' => 'Testing excluded types' - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test Testing excluded types has been created.'); - - // Exclude all types. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/fields/node.paragraphed_test.paragraphs'); - $edit = [ - 'settings[handler_settings][negate]' => 1, - 'settings[handler_settings][target_bundles_drag_drop][text][enabled]' => 1, - 'settings[handler_settings][target_bundles_drag_drop][paragraph_type_test][enabled]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, 'Save settings'); - $this->drupalGet('node/add/paragraphed_test'); - $this->assertText('You are not allowed to add any of the Paragraph types.'); - $edit = [ - 'title[0][value]' => 'Testing all excluded types' - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test Testing all excluded types has been created.'); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalContactTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalContactTest.php deleted file mode 100644 index 30133c903..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalContactTest.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\contact\Entity\ContactForm; - -/** - * Tests paragraphs with contact forms. - * - * @group paragraphs - */ -class ParagraphsExperimentalContactTest extends ParagraphsExperimentalTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'contact', - ); - - /** - * Tests adding paragraphs in contact forms. - */ - public function testContactForm() { - $this->loginAsAdmin([ - 'administer contact forms', - 'access site-wide contact form' - ]); - // Add a paragraph type. - $this->addParagraphsType('paragraphs_contact'); - $this->addParagraphsType('text'); - - // Create a contact form. - $contact_form = ContactForm::create(['id' => 'test_contact_form']); - $contact_form->save(); - // Add a paragraphs field to the contact form. - $this->addParagraphsField($contact_form->id(), 'paragraphs', 'contact_message'); - // Add a paragraph to the contact form. - $this->drupalGet('contact/test_contact_form'); - $this->drupalPostAjaxForm(NULL, [], 'paragraphs_paragraphs_contact_add_more'); - // Check that the paragraph is displayed. - $this->assertText('paragraphs_contact'); - $this->drupalPostAjaxForm(NULL, [], 'paragraphs_0_remove'); - $this->assertText('No Paragraph added yet.'); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalDuplicateFeatureTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalDuplicateFeatureTest.php deleted file mode 100644 index 278e0a7b9..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalDuplicateFeatureTest.php +++ /dev/null @@ -1,153 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -/** - * Tests paragraphs duplicate feature. - * - * @group paragraphs - */ -class ParagraphsExperimentalDuplicateFeatureTest extends ParagraphsExperimentalTestBase { - - public static $modules = [ - 'node', - 'paragraphs', - 'field', - 'field_ui', - 'block', - 'paragraphs_test', - ]; - - /** - * Tests duplicate paragraph feature. - */ - public function testDuplicateButton() { - $this->addParagraphedContentType('paragraphed_test'); - - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']); - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_text_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_paragraph_add_more'); - - // Create a node with a Paragraph. - $text_01 = 'recognizable_text_01'; - $text_02 = 'recognizable_text_02'; - $edit = [ - 'title[0][value]' => 'paragraphs_mode_test', - 'field_paragraphs[0][subform][field_text][0][value]' => 'A', - 'field_paragraphs[1][subform][field_text][0][value]' => 'B', - 'field_paragraphs[2][subform][field_text][0][value]' => 'C', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('paragraphs_mode_test'); - - $this->drupalGet('node/' . $node->id() . '/edit'); - - // Click "Duplicate" button on A and move C to the first position. - $edit = ['field_paragraphs[2][_weight]' => -1]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_duplicate'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', 'A'); - $this->assertFieldByName('field_paragraphs[0][_weight]', 1); - $this->assertFieldByName('field_paragraphs[1][subform][field_text][0][value]', 'B'); - $this->assertFieldByName('field_paragraphs[1][_weight]', 3); - $this->assertFieldByName('field_paragraphs[2][subform][field_text][0][value]', 'C'); - $this->assertFieldByName('field_paragraphs[2][_weight]', 0); - $this->assertFieldByName('field_paragraphs[3][subform][field_text][0][value]', 'A'); - $this->assertFieldByName('field_paragraphs[3][_weight]', 2); - - // Move C after the A's and save. - $edit = [ - 'field_paragraphs[0][_weight]' => -2, - 'field_paragraphs[1][_weight]' => 2, - 'field_paragraphs[2][_weight]' => 1, - 'field_paragraphs[3][_weight]' => -1, - ]; - - // Save and check if all paragraphs are present in the correct order. - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', 'A'); - $this->assertFieldByName('field_paragraphs[1][subform][field_text][0][value]', 'A'); - $this->assertFieldByName('field_paragraphs[2][subform][field_text][0][value]', 'C'); - $this->assertFieldByName('field_paragraphs[3][subform][field_text][0][value]', 'B'); - - // Delete the second A, then duplicate C. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_1_remove'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_2_duplicate'); - $this->drupalPostForm(NULL, [], t('Save')); - - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', 'A'); - $this->assertFieldByName('field_paragraphs[1][subform][field_text][0][value]', 'C'); - $this->assertFieldByName('field_paragraphs[2][subform][field_text][0][value]', 'C'); - $this->assertFieldByName('field_paragraphs[3][subform][field_text][0][value]', 'B'); - } - - /** - * Tests duplicate paragraph feature with nested paragraphs. - */ - public function testDuplicateButtonWithNesting() { - $this->addParagraphedContentType('paragraphed_test'); - - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']); - // Add nested Paragraph type. - $nested_paragraph_type = 'nested_paragraph'; - $this->addParagraphsType($nested_paragraph_type); - // Add text Paragraph type. - $paragraph_type = 'text'; - $this->addParagraphsType($paragraph_type); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - - // Add a ERR paragraph field to the nested_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $nested_paragraph_type, 'nested', 'Nested', 'field_ui:entity_reference_revisions:paragraph', [ - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ], []); - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_nested_paragraph_add_more'); - - // Create a node with a Paragraph. - $edit = [ - 'title[0][value]' => 'paragraphs_mode_test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('paragraphs_mode_test'); - - // Add a text field to nested paragraph. - $text = 'recognizable_text'; - $this->drupalPostAjaxForm('node/' . $node->id() . '/edit', [], 'field_paragraphs_0_subform_field_nested_text_add_more'); - $edit = [ - 'field_paragraphs[0][subform][field_nested][0][subform][field_text][0][value]' => $text, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Switch mode to closed. - $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'closed'); - $this->drupalGet('node/' . $node->id() . '/edit'); - - // Click "Duplicate" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_duplicate'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_nested][0][subform][field_text][0][value]', $text); - $this->assertFieldByName('field_paragraphs[1][subform][field_nested][0][subform][field_text][0][value]', $text); - - // Change the text paragraph value of duplicated nested paragraph. - $second_paragraph_text = 'duplicated_text'; - $edit = [ - 'field_paragraphs[1][subform][field_nested][0][subform][field_text][0][value]' => $second_paragraph_text, - ]; - - // Save and check if the changed text paragraph value of the duplicated - // paragraph is not the same as in the original paragraph. - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertUniqueText($text); - $this->assertUniqueText($second_paragraph_text); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalEditModesTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalEditModesTest.php deleted file mode 100644 index e7ff0ea61..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalEditModesTest.php +++ /dev/null @@ -1,140 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests paragraphs edit modes. - * - * @group paragraphs - */ -class ParagraphsExperimentalEditModesTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'image', - 'block_field', - ]; - - /** - * Tests the collapsed summary of paragraphs. - */ - public function testCollapsedSummary() { - $this->addParagraphedContentType('paragraphed_test'); - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']); - - // Add a Paragraph type. - $paragraph_type = 'image_text_paragraph'; - $this->addParagraphsType($paragraph_type); - $title_paragraphs_type = 'title'; - $this->addParagraphsType($title_paragraphs_type); - $this->addParagraphsType('text'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'image', 'Image', 'image', [], ['settings[alt_field_required]' => FALSE]); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $title_paragraphs_type, 'title', 'Title', 'string', [], []); - - // Add a user Paragraph Type - $paragraph_type = 'user_paragraph'; - $this->addParagraphsType($paragraph_type); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'user', 'User', 'entity_reference', ['settings[target_type]' => 'user'], []); - - // Set edit mode to closed. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/form-display'); - $this->drupalPostAjaxForm(NULL, [], "field_paragraphs_settings_edit"); - $edit = ['fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'closed']; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Add a paragraph. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_image_text_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_title_add_more'); - - $text = 'Trust me I am an image'; - file_put_contents('temporary://myImage1.jpg', $text); - - // Create a node with an image and text. - $edit = [ - 'title[0][value]' => 'Test article', - 'field_paragraphs[0][subform][field_text][0][value]' => 'text_summary', - 'files[field_paragraphs_0_subform_field_image_0]' => drupal_realpath('temporary://myImage1.jpg'), - 'field_paragraphs[1][subform][field_title][0][value]' => 'Title example', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->clickLink(t('Edit')); - $this->drupalPostForm(NULL, [], t('Add user_paragraph')); - $edit = [ - 'field_paragraphs[2][subform][field_user][0][target_id]' => $this->admin_user->label() . ' (' . $this->admin_user->id() . ')', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Assert the summary is correctly generated. - $this->clickLink(t('Edit')); - $this->assertRaw('<div class="paragraphs-collapsed-description">myImage1.jpg, text_summary'); - $this->assertRaw('<div class="paragraphs-collapsed-description">' . $this->admin_user->label()); - $this->assertRaw('<div class="paragraphs-collapsed-description">Title example'); - - // Edit and remove alternative text. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $edit = [ - 'field_paragraphs[0][subform][field_image][0][alt]' => 'alternative_text_summary', - 'field_paragraphs[0][subform][field_image][0][width]' => 300, - 'field_paragraphs[0][subform][field_image][0][height]' => 300, - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_collapse'); - // Assert the summary is correctly generated. - $this->assertRaw('<div class="paragraphs-collapsed-description">alternative_text_summary, text_summary'); - - // Remove image. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_subform_field_image_0_remove_button'); - $this->drupalPostForm(NULL, [], t('Save')); - - // Assert the summary is correctly generated. - $this->clickLink(t('Edit')); - $this->assertRaw('<div class="paragraphs-collapsed-description">text_summary'); - - $this->addParagraphsType('nested_paragraph'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/nested_paragraph', 'nested_content', 'Nested Content', 'entity_reference_revisions', ['settings[target_type]' => 'paragraph'], []); - $this->drupalGet('admin/structure/paragraphs_type/nested_paragraph/form-display'); - $this->drupalPostForm(NULL, ['fields[field_nested_content][type]' => 'entity_reference_paragraphs'], t('Save')); - - $test_user = $this->drupalCreateUser([]); - - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostForm(NULL, NULL, t('Add nested_paragraph')); - $this->drupalPostAjaxForm(NULL, NULL, t('field_paragraphs_0_subform_field_nested_content_user_paragraph_add_more')); - $edit = [ - 'title[0][value]' => 'Node title', - 'field_paragraphs[0][subform][field_nested_content][0][subform][field_user][0][target_id]' => $test_user->label() . ' (' . $test_user->id() . ')', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Create an orphaned ER field item by deleting the target entity. - $test_user->delete(); - - $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties(['title' => 'Node title']); - $this->drupalGet('node/' . current($nodes)->id() . '/edit'); - $this->drupalPostAjaxForm(NULL, [], t('field_paragraphs_0_edit')); - $this->drupalPostAjaxForm(NULL, [], t('field_paragraphs_0_collapse')); - $this->assertResponse(200); - - // Add a Block Paragraphs type. - $this->addParagraphsType('block_paragraph'); - $this->addFieldtoParagraphType('block_paragraph', 'field_block', 'block_field'); - - // Test the summary of a Block field. - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_block_paragraph_add_more'); - $edit = [ - 'field_paragraphs[0][subform][field_block][0][plugin_id]' => 'system_breadcrumb_block', - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_collapse'); - $this->assertRaw('<div class="paragraphs-collapsed-description">Breadcrumbs'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalEntityTranslationWithNonTranslatableParagraphs.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalEntityTranslationWithNonTranslatableParagraphs.php deleted file mode 100644 index 247ebf9e8..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalEntityTranslationWithNonTranslatableParagraphs.php +++ /dev/null @@ -1,109 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -/** - * Tests the translation of heavily nested / specialized setup. - * - * @group paragraphs - */ -class ParagraphsExperimentalEntityTranslationWithNonTranslatableParagraphs extends ParagraphsExperimentalTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'language', - 'content_translation', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->admin_user = $this->drupalCreateUser([], NULL, TRUE); - $this->drupalLogin($this->admin_user); - - // Add a languages. - $edit = array( - 'predefined_langcode' => 'de', - ); - $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); - $edit = array( - 'predefined_langcode' => 'fr', - ); - $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); - - // Create article content type with a paragraphs field. - $this->addParagraphedContentType('article', 'field_paragraphs'); - $this->drupalGet('admin/structure/types/manage/article'); - // Make content type translatable. - $edit = array( - 'language_configuration[content_translation]' => TRUE, - ); - $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type')); - $this->drupalGet('admin/structure/types/manage/article'); - - // Ensue the paragraphs field itself isn't translatable - this would be a - // currently not supported configuration otherwise. - $edit = array( - 'translatable' => FALSE, - ); - $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_paragraphs', $edit, t('Save settings')); - - // Add Paragraphs type. - $this->addParagraphsType('test_paragraph_type'); - // Configure Paragraphs type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/test_paragraph_type', 'text', 'Text', 'string', [ - 'cardinality' => '-1', - ]); - - // Just for verbose-sake - check the content language settings. - $this->drupalGet('admin/config/regional/content-language'); - } - - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsIEFTranslation() { - $this->drupalLogin($this->admin_user); - - // Create node with one paragraph. - $this->drupalGet('node/add/article'); - - // Set the values and save. - $edit = [ - 'title[0][value]' => 'Title English', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Add french translation. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add'), 1); - // Make sure that the original paragraph text is displayed. - $this->assertText('Title English'); - - $edit = array( - 'title[0][value]' => 'Title French', - ); - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertText('article Title French has been updated.'); - - // Add german translation. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add')); - // Make sure that the original paragraph text is displayed. - $this->assertText('Title English'); - - $edit = array( - 'title[0][value]' => 'Title German', - ); - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertText('article Title German has been updated.'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalFieldGroupTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalFieldGroupTest.php deleted file mode 100644 index 111f5782f..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalFieldGroupTest.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -/** - * Tests the field group on node. - * - * @group paragraphs - * @requires module field_group - */ -class ParagraphsExperimentalFieldGroupTest extends ParagraphsExperimentalTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'field_group', - ]; - - /** - * Tests the field group inside paragraph. - */ - public function testFieldGroup() { - $this->loginAsAdmin(); - - $paragraph_type = 'paragraph_type_test'; - $content_type = 'paragraphed_test'; - - // Add a Paragraphed test content type. - $this->addParagraphedContentType($content_type); - - $this->addParagraphsType($paragraph_type); - $this->addParagraphsType('text'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - - // Create the field group element on paragraph type. - $edit = [ - 'group_formatter' => 'fieldset', - 'label' => 'paragraph_field_group_title', - 'group_name' => 'field' - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type . '/form-display/add-group', $edit, t('Save and continue')); - $edit = [ - 'format_settings[label]' => 'field_group' - ]; - $this->drupalPostForm(NULL, $edit, t('Create group')); - - // Put the text field into the field group. - $edit = [ - 'fields[field_text][parent]' => 'group_field' - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type . '/form-display', $edit, t('Save')); - - // Create a node with a paragraph. - $this->drupalGet('node/add/' . $content_type); - $this->drupalPostAjaxForm('node/add/' . $content_type, [], 'field_paragraphs_paragraph_type_test_add_more'); - - // Test if the new field group is displayed. - $this->assertText('field_group'); - $this->assertFieldByXPath("//fieldset", NULL, t('Fieldset present')); - - // Save the node. - $edit = [ - 'title[0][value]' => 'paragraphed_title', - 'field_paragraphs[0][subform][field_text][0][value]' => 'paragraph_value', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalHeaderActionsTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalHeaderActionsTest.php deleted file mode 100644 index df1ddcd35..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalHeaderActionsTest.php +++ /dev/null @@ -1,256 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Symfony\Component\CssSelector\CssSelectorConverter; - -/** - * Tests collapse all button. - * - * @group paragraphs - */ -class ParagraphsExperimentalHeaderActionsTest extends ParagraphsExperimentalTestBase { - - /** - * Tests header actions. - */ - public function testHeaderActions() { - - // To test with a single header action, ensure the drag and drop action is - // shown, even without the library. - \Drupal::state()->set('paragraphs_test_dragdrop_force_show', TRUE); - - $this->addParagraphedContentType('paragraphed_test'); - - $this->loginAsAdmin([ - 'create paragraphed_test content', - 'edit any paragraphed_test content', - ]); - - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField( - 'admin/structure/paragraphs_type/' . $paragraph_type, - 'text', - 'Text', - 'text_long', - [], - [] - ); - - // Add 2 paragraphs and check for Collapse/Edit all button. - $this->drupalGet('node/add/paragraphed_test'); - $this->assertNoRaw('field_paragraphs_collapse_all'); - $this->assertNoRaw('field_paragraphs_edit_all'); - $this->assertRaw('field_paragraphs_dragdrop_mode'); - - // Ensure there is only a single table row. - $table_rows = $this->xpath('//table[contains(@class, :class)]/tbody/tr', [':class' => 'field-multiple-table']); - $this->assertEqual(1, count($table_rows)); - - // Add second paragraph and check for Collapse/Edit all button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_paragraph_add_more'); - $this->assertRaw('field_paragraphs_collapse_all'); - $this->assertRaw('field_paragraphs_edit_all'); - - $edit = [ - 'field_paragraphs[0][subform][field_text][0][value]' => 'First text', - 'field_paragraphs[1][subform][field_text][0][value]' => 'Second text', - ]; - $this->drupalPostForm(NULL, $edit, 'Collapse all'); - - // Checks that after collapsing all we can edit again these paragraphs. - $this->assertRaw('field_paragraphs_0_edit'); - $this->assertRaw('field_paragraphs_1_edit'); - - // Test Edit all button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_edit_all'); - $this->assertRaw('field_paragraphs_0_collapse'); - $this->assertRaw('field_paragraphs_1_collapse'); - - $edit = [ - 'title[0][value]' => 'Test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test Test has been created.'); - - $node = $this->getNodeByTitle('Test'); - $this->drupalGet('node/' . $node->id()); - $this->clickLink('Edit'); - $this->assertNoText('No Paragraph added yet.'); - - // Add and remove another paragraph. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_paragraph_add_more'); - $edit = [ - 'field_paragraphs[2][subform][field_text][0][value]' => 'Third text', - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_2_remove'); - - // Check that pressing "Collapse all" does not restore the removed - // paragraph. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_edit_all'); - $this->assertText('First text'); - $this->assertText('Second text'); - $this->assertNoText('Third text'); - - // Check that pressing "Edit all" does not restore the removed paragraph, - // either. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_collapse_all'); - $this->assertText('First text'); - $this->assertText('Second text'); - $this->assertNoText('Third text'); - } - - /** - * Tests that header actions works fine with nesting. - */ - public function testHeaderActionsWithNesting() { - $this->addParagraphedContentType('paragraphed_test'); - - $this->loginAsAdmin([ - 'create paragraphed_test content', - 'edit any paragraphed_test content', - ]); - - // Add Paragraph types. - $nested_paragraph_type = 'nested_paragraph'; - $this->addParagraphsType($nested_paragraph_type); - $paragraph_type = 'text'; - $this->addParagraphsType($paragraph_type); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField( - 'admin/structure/paragraphs_type/' . $paragraph_type, - 'text', - 'Text', - 'text_long', - [], - [] - ); - - // Add a ERR paragraph field to the nested_paragraph type. - static::fieldUIAddNewField( - 'admin/structure/paragraphs_type/' . $nested_paragraph_type, - 'nested', - 'Nested', - 'field_ui:entity_reference_revisions:paragraph', [ - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ], - [] - ); - - // Checks that Collapse/Edit all button is presented. - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_nested_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_add_more'); - $this->assertRaw('field_paragraphs_collapse_all'); - $this->assertRaw('field_paragraphs_edit_all'); - - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_subform_field_nested_text_add_more'); - $this->assertNoRaw('field_paragraphs_0_collapse_all'); - $this->assertNoRaw('field_paragraphs_0_edit_all'); - $edit = [ - 'field_paragraphs[0][subform][field_nested][0][subform][field_text][0][value]' => 'Nested text', - 'field_paragraphs[1][subform][field_text][0][value]' => 'Second text paragraph', - ]; - $this->drupalPostForm(NULL, $edit, 'Collapse all'); - $this->assertRaw('field-paragraphs-0-edit'); - $this->assertFieldByXPath((new CssSelectorConverter())->toXPath('[name="field_paragraphs_1_edit"] + .paragraphs-dropdown')); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_edit_all'); - $this->assertRaw('field-paragraphs-0-collapse'); - - $edit = [ - 'title[0][value]' => 'Test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test Test has been created.'); - - $node = $this->getNodeByTitle('Test'); - $this->drupalGet('node/' . $node->id()); - $this->clickLink('Edit'); - $this->assertNoText('No Paragraph added yet.'); - } - - /** - * Tests header actions with multi fields. - */ - public function testHeaderActionsWithMultiFields() { - $this->addParagraphedContentType('paragraphed_test'); - $this->loginAsAdmin([ - 'create paragraphed_test content', - 'edit any paragraphed_test content', - ]); - $this->drupalGet('/admin/structure/types/manage/paragraphed_test/fields/add-field'); - - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference_revisions:paragraph', - 'label' => 'Second paragraph', - 'field_name' => 'second', - ]; - $this->drupalPostForm(NULL, $edit, 'Save and continue'); - $this->drupalPostForm(NULL, [], 'Save field settings'); - $this->drupalPostForm(NULL, [], 'Save settings'); - - $this->drupalGet('/admin/structure/types/manage/paragraphed_test/form-display'); - $edit = [ - 'fields[field_second][type]' => 'paragraphs', - ]; - $this->drupalPostForm(NULL, $edit, 'Save'); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField( - 'admin/structure/paragraphs_type/' . $paragraph_type, - 'text', - 'Text', - 'text_long', - [], - [] - ); - - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_second_text_paragraph_add_more'); - - // Checks that we have Collapse\Edit all for each field. - $this->assertRaw('field_paragraphs_collapse_all'); - $this->assertRaw('field_paragraphs_edit_all'); - $this->assertRaw('field_second_collapse_all'); - $this->assertRaw('field_second_edit_all'); - - $edit = [ - 'field_second[0][subform][field_text][0][value]' => 'Second field', - ]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_second_collapse_all'); - - // Checks that we collapsed only children from second field. - $this->assertNoRaw('field_paragraphs_0_edit'); - $this->assertRaw('field_second_0_edit'); - - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_collapse_all'); - $this->assertRaw('field_paragraphs_0_edit'); - $this->assertRaw('field_second_0_edit'); - - $this->drupalPostAjaxForm(NULL, [], 'field_second_edit_all'); - $this->assertRaw('field_second_0_collapse'); - - $edit = [ - 'title[0][value]' => 'Test', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test Test has been created.'); - - $node = $this->getNodeByTitle('Test'); - $this->drupalGet('node/' . $node->id()); - $this->clickLink('Edit'); - $this->assertNoText('No Paragraph added yet.'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalInlineEntityFormTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalInlineEntityFormTest.php deleted file mode 100644 index 1ddd6189b..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalInlineEntityFormTest.php +++ /dev/null @@ -1,148 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\Tests\paragraphs\FunctionalJavascript\ParagraphsTestBaseTrait; - -/** - * Tests the configuration of paragraphs in relation to ief. - * - * @group paragraphs - */ -class ParagraphsExperimentalInlineEntityFormTest extends ParagraphsExperimentalTestBase { - - use ParagraphsTestBaseTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'inline_entity_form', - ]; - - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsIEFPreview() { - // Create article content type with a paragraphs field. - $this->addParagraphedContentType('article'); - $this->loginAsAdmin(['create article content', 'edit any article content']); - - // Create the paragraphs type simple. - $this->addParagraphsType('simple'); - $this->addParagraphsType('text'); - - // Create a reference to an article. - $this->fieldUIAddNewField('admin/structure/paragraphs_type/simple', 'article', 'Article', 'field_ui:entity_reference:node', [ - 'settings[target_type]' => 'node', - 'cardinality' => 'number', - 'cardinality_number' => 1, - ], [ - 'required' => TRUE, - 'settings[handler_settings][target_bundles][article]' => TRUE - ]); - - // Enable IEF simple widget. - $this->drupalGet('admin/structure/paragraphs_type/simple/form-display'); - $edit = [ - 'fields[field_article][type]' => 'inline_entity_form_simple', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Set the paragraphs widget edit mode to "Closed" and the closed mode to - // "Preview". - $settings = [ - 'edit_mode' => 'closed', - 'closed_mode' => 'preview', - ]; - $this->setParagraphsWidgetSettings('article', 'field_paragraphs', $settings); - - // Create node with one paragraph. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_simple_add_more'); - - // Set the values and save. - $edit = [ - 'title[0][value]' => 'Dummy1', - 'field_paragraphs[0][subform][field_article][0][inline_entity_form][title][0][value]' => 'Dummy2', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Go back into edit page. - $node = $this->getNodeByTitle('Dummy1'); - $this->drupalGet('node/' . $node->id() . '/edit'); - - // Try to open the previewed paragraph. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - } - - /** - * Tests the reordering of previewed paragraphs. - */ - public function testParagraphsIEFChangeOrder() { - // Create article content type with a paragraphs field. - $this->addParagraphedContentType('article'); - $this->loginAsAdmin(['create article content', 'edit any article content']); - - // Create the paragraphs type simple. - $this->addParagraphsType('simple'); - $this->addParagraphsType('text'); - - - // Create a reference to an article. - $this->fieldUIAddNewField('admin/structure/paragraphs_type/simple', 'article', 'Article', 'field_ui:entity_reference:node', [ - 'settings[target_type]' => 'node', - 'cardinality' => 'number', - 'cardinality_number' => '1', - ], [ - 'required' => TRUE, - 'settings[handler_settings][target_bundles][article]' => TRUE - ]); - - // Set cardinality explicit to -1. - $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_paragraphs/storage'); - $edit = [ - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ]; - $this->drupalPostForm(NULL, $edit, t('Save field settings')); - - // Enable IEF simple widget. - $this->drupalGet('admin/structure/paragraphs_type/simple/form-display'); - $edit = [ - 'fields[field_article][type]' => 'inline_entity_form_simple', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Set the paragraphs widget closed mode to preview. - $this->setParagraphsWidgetSettings('article', 'field_paragraphs', ['closed_mode' => 'preview']); - - // Create node with one paragraph. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_simple_add_more'); - - // Set the values and save. - $edit = [ - 'title[0][value]' => 'Article 1', - 'field_paragraphs[0][subform][field_article][0][inline_entity_form][title][0][value]' => 'Basic page 1', - ]; - - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Go back into edit page. - $node = $this->getNodeByTitle('Article 1'); - $this->drupalGet('node/' . $node->id() . '/edit'); - - // Create second paragraph. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_simple_add_more'); - - // Set the values of second paragraph. - $edit = [ - 'field_paragraphs[1][subform][field_article][0][inline_entity_form][title][0][value]' => 'Basic 2' - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalPreviewTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalPreviewTest.php deleted file mode 100644 index 8280b0775..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalPreviewTest.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests the configuration of paragraphs. - * - * @group paragraphs - */ -class ParagraphsExperimentalPreviewTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'image', - ); - - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsPreview() { - // Create paragraph type Headline + Block. - $this->addParagraphedContentType('article'); - $this->loginAsAdmin([ - 'administer node display', - 'create article content', - 'edit any article content', - 'delete any article content', - ]); - - // Create paragraph type Headline + Block. - $this->addParagraphsType('text'); - // Create field types for the text. - $this->fieldUIAddNewField('admin/structure/paragraphs_type/text', 'text', 'Text', 'text', array(), array()); - $this->assertText('Saved Text configuration.'); - - $test_text_1 = 'dummy_preview_text_1'; - $test_text_2 = 'dummy_preview_text_2'; - // Create node with two paragraphs. - $this->drupalGet('node/add/article'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_add_more'); - // Set the value of the paragraphs. - $edit = [ - 'title[0][value]' => 'Page_title', - 'field_paragraphs[0][subform][field_text][0][value]' => $test_text_1, - ]; - // Preview the article. - $this->drupalPostForm(NULL, $edit, t('Preview')); - // Check if the text is displayed. - $this->assertRaw($test_text_1); - - // Go back to the editing form. - $this->clickLink('Back to content editing'); - - $paragraph_1 = $this->xpath('//*[@id="edit-field-paragraphs-0-subform-field-text-0-value"]')[0]; - $this->assertEqual($paragraph_1['value'], $test_text_1); - - $this->drupalPostForm(NULL, $edit, t('Save')); - - $this->clickLink('Edit'); - $this->drupalPostAjaxForm(NULL, array(), 'field_paragraphs_text_add_more'); - $edit = [ - 'field_paragraphs[1][subform][field_text][0][value]' => $test_text_2, - ]; - // Preview the article. - $this->drupalPostForm(NULL, $edit, t('Preview')); - $this->assertRaw($test_text_1); - $this->assertRaw($test_text_2); - - // Go back to the editing form. - $this->clickLink('Back to content editing'); - $new_test_text_2 = 'less_dummy_preview_text_2'; - - $edit = [ - 'field_paragraphs[1][subform][field_text][0][value]' => $new_test_text_2, - ]; - // Preview the article. - $this->drupalPostForm(NULL, $edit, t('Preview')); - $this->assertRaw($test_text_1); - $this->assertRaw($new_test_text_2); - // Go back to the editing form. - $this->clickLink('Back to content editing'); - $paragraph_1 = $this->xpath('//*[@id="edit-field-paragraphs-0-subform-field-text-0-value"]')[0]; - $paragraph_2 = $this->xpath('//*[@id="edit-field-paragraphs-1-subform-field-text-0-value"]')[0]; - $this->assertEqual($paragraph_1['value'], $test_text_1); - $this->assertEqual($paragraph_2['value'], $new_test_text_2); - $this->drupalPostForm(NULL, [], t('Save')); - - $this->assertRaw($test_text_1); - $this->assertRaw($new_test_text_2); - $this->assertRaw('Page_title'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalReplicateEnableTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalReplicateEnableTest.php deleted file mode 100644 index b81d73330..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalReplicateEnableTest.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -/** - * Enables replicate module. - * - * @group paragraphs - */ - -class ParagraphsExperimentalReplicateEnableTest extends ParagraphsExperimentalDuplicateFeatureTest { - - public static $modules = [ - 'replicate', - ]; - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalSummaryFormatterTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalSummaryFormatterTest.php deleted file mode 100644 index 87a0e2963..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalSummaryFormatterTest.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests the paragraphs summary formatter. - * - * @group paragraphs - */ -class ParagraphsExperimentalSummaryFormatterTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'image', - ]; - - /** - * Tests the paragraphs summary formatter. - */ - public function testParagraphsSummaryFormatter() { - $this->addParagraphedContentType('paragraphed_test'); - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content', 'administer node display']); - - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - $title_paragraphs_type = 'title'; - $this->addParagraphsType($title_paragraphs_type); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $title_paragraphs_type, 'title', 'Title', 'string', [], []); - - // Add a user Paragraph Type - $paragraph_type = 'user_paragraph'; - $this->addParagraphsType($paragraph_type); - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'user', 'User', 'entity_reference', ['settings[target_type]' => 'user'], []); - - // Set display format to paragraphs summary. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/display'); - $edit = ['fields[field_paragraphs][type]' => 'paragraph_summary']; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Add a paragraph. - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_text_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_title_add_more'); - - // Create a node with a text. - $edit = [ - 'title[0][value]' => 'Test article', - 'field_paragraphs[0][subform][field_text][0][value]' => 'text_summary', - 'field_paragraphs[1][subform][field_title][0][value]' => 'Title example', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->clickLink(t('Edit')); - $this->drupalPostForm(NULL, [], t('Add user_paragraph')); - $edit = [ - 'field_paragraphs[2][subform][field_user][0][target_id]' => $this->admin_user->label() . ' (' . $this->admin_user->id() . ')', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Assert the summary is correctly generated. - $this->assertText($this->admin_user->label()); - $this->assertText('Title example'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTestBase.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTestBase.php deleted file mode 100644 index 752b5de6b..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTestBase.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Entity\Entity\EntityViewDisplay; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\paragraphs\Tests\Classic\ParagraphsTestBase; -use Drupal\Tests\paragraphs\FunctionalJavascript\ParagraphsTestBaseTrait; - -/** - * Base class for tests. - */ -abstract class ParagraphsExperimentalTestBase extends ParagraphsTestBase { - - use FieldUiTestTrait, ParagraphsTestBaseTrait; - - /** - * Sets the Paragraphs widget add mode. - * - * @param string $content_type - * Content type name where to set the widget mode. - * @param string $paragraphs_field - * Paragraphs field to change the mode. - * @param string $mode - * Mode to be set. ('dropdown', 'select' or 'button'). - */ - protected function setAddMode($content_type, $paragraphs_field, $mode) { - $form_display = EntityFormDisplay::load('node.' . $content_type . '.default') - ->setComponent($paragraphs_field, [ - 'type' => 'paragraphs', - 'settings' => ['add_mode' => $mode] - ]); - $form_display->save(); - } - - /** - * Removes the default paragraph type. - * - * @param $content_type - * Content type name that contains the paragraphs field. - */ - protected function removeDefaultParagraphType($content_type) { - $this->drupalGet('node/add/' . $content_type); - $this->drupalPostForm(NULL, [], 'Remove'); - $this->assertNoText('No paragraphs added yet.'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTranslationTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTranslationTest.php deleted file mode 100644 index 63710a4a2..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTranslationTest.php +++ /dev/null @@ -1,862 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Language\LanguageInterface; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\node\Entity\Node; -use Drupal\user\Entity\Role; - -/** - * Tests the configuration of paragraphs. - * - * @group paragraphs - */ -class ParagraphsExperimentalTranslationTest extends ParagraphsExperimentalTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'paragraphs_demo', - 'content_translation', - 'link', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->loginAsAdmin([ - 'administer site configuration', - 'create paragraphed_content_demo content', - 'edit any paragraphed_content_demo content', - 'delete any paragraphed_content_demo content', - 'administer content translation', - 'translate any entity', - 'create content translations', - 'administer languages', - ]); - $edit = [ - 'settings[paragraph][nested_paragraph][translatable]' => TRUE, - 'settings[paragraph][nested_paragraph][settings][language][language_alterable]' => TRUE, - 'settings[paragraph][images][fields][field_images_demo]' => TRUE, - ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); - - if (version_compare(\Drupal::VERSION, '8.4', '>=')) { - // @todo Workaround for file usage/unable to save the node with no usages. - // Remove when https://www.drupal.org/node/2801777 is fixed. - \Drupal::configFactory()->getEditable('file.settings') - ->set('make_unused_managed_files_temporary', TRUE) - ->save(); - } - } - - /** - * Tests the paragraph translation. - */ - public function testParagraphTranslation() { - // We need to add a permission to administer roles to deal with revisions. - $roles = $this->loggedInUser->getRoles(); - $this->grantPermissions(Role::load(array_shift($roles)), ['administer nodes']); - $this->drupalGet('admin/config/regional/content-language'); - - // Check the settings are saved correctly. - $this->assertFieldChecked('edit-entity-types-paragraph'); - $this->assertFieldChecked('edit-settings-node-paragraphed-content-demo-translatable'); - $this->assertFieldChecked('edit-settings-paragraph-text-image-translatable'); - $this->assertFieldChecked('edit-settings-paragraph-images-columns-field-images-demo-alt'); - $this->assertFieldChecked('edit-settings-paragraph-images-columns-field-images-demo-title'); - - // Check if the publish/unpublish option works. - $this->drupalGet('admin/structure/paragraphs_type/text_image/form-display'); - $edit = array( - 'fields[status][type]' => 'boolean_checkbox', - 'fields[status][region]' => 'content', - ); - // Use the experimental widget. - $form_display = EntityFormDisplay::load('node.paragraphed_content_demo.default') - ->setComponent('field_paragraphs_demo', [ - 'type' => 'paragraphs', - ]); - $form_display->save(); - // Use the experimental widget. - $form_display = EntityFormDisplay::load('paragraph.nested_paragraph.default') - ->setComponent('field_paragraphs_demo', [ - 'type' => 'paragraphs', - ]); - $form_display->save(); - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Text + Image')); - $this->assertRaw('edit-field-paragraphs-demo-0-subform-status-value'); - $edit = [ - 'title[0][value]' => 'example_publish_unpublish', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Example published and unpublished', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText(t('Example published and unpublished')); - $this->clickLink(t('Edit')); - - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_nested_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_1_subform_field_paragraphs_demo_text_add_more'); - $edit = [ - 'field_paragraphs_demo[0][subform][status][value]' => FALSE, - 'field_paragraphs_demo[1][subform][field_paragraphs_demo][0][subform][field_text_demo][0][value]' => 'Dummy text' - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertNoText(t('Example published and unpublished')); - - // Check the parent fields are set properly. Get the node. - $node = $this->drupalGetNodeByTitle('example_publish_unpublish'); - // Loop over the paragraphs of the node. - foreach ($node->field_paragraphs_demo->referencedEntities() as $paragraph) { - $node_paragraph = Paragraph::load($paragraph->id())->toArray(); - // Check if the fields are set properly. - $this->assertEqual($node_paragraph['parent_id'][0]['value'], $node->id()); - $this->assertEqual($node_paragraph['parent_type'][0]['value'], 'node'); - $this->assertEqual($node_paragraph['parent_field_name'][0]['value'], 'field_paragraphs_demo'); - // If the paragraph is nested type load the child. - if ($node_paragraph['type'][0]['target_id'] == 'nested_paragraph') { - $nested_paragraph = Paragraph::load($node_paragraph['field_paragraphs_demo'][0]['target_id'])->toArray(); - // Check if the fields are properly set. - $this->assertEqual($nested_paragraph['parent_id'][0]['value'], $paragraph->id()); - $this->assertEqual($nested_paragraph['parent_type'][0]['value'], 'paragraph'); - $this->assertEqual($nested_paragraph['parent_field_name'][0]['value'], 'field_paragraphs_demo'); - } - } - - // Add paragraphed content. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Text + Image')); - $edit = array( - 'title[0][value]' => 'Title in english', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Text in english', - ); - // The button to remove a paragraph is present. - $this->assertRaw(t('Remove')); - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('Title in english'); - // The text is present when editing again. - $this->clickLink(t('Edit')); - $this->assertText('Title in english'); - $this->assertText('Text in english'); - - // Add french translation. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add'), 1); - // Make sure the Add / Remove paragraph buttons are hidden. - $this->assertNoRaw(t('Remove')); - $this->assertNoRaw(t('Add Text + Image')); - // Make sure that the original paragraph text is displayed. - $this->assertText('Text in english'); - - $edit = array( - 'title[0][value]' => 'Title in french', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Text in french', - 'revision' => TRUE, - 'revision_log[0][value]' => 'french 1', - ); - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertText('Paragraphed article Title in french has been updated.'); - - // Check the english translation. - $this->drupalGet('node/' . $node->id()); - $this->assertText('Title in english'); - $this->assertText('Text in english'); - $this->assertNoText('Title in french'); - $this->assertNoText('Text in french'); - - // Check the french translation. - $this->drupalGet('fr/node/' . $node->id()); - $this->assertText('Title in french'); - $this->assertText('Text in french'); - $this->assertNoText('Title in english'); - // The translation is still present when editing again. - $this->clickLink(t('Edit')); - $this->assertText('Title in french'); - $this->assertText('Text in french'); - $edit = array( - 'title[0][value]' => 'Title Change in french', - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'New text in french', - 'revision' => TRUE, - 'revision_log[0][value]' => 'french 2', - ); - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertText('Title Change in french'); - $this->assertText('New text in french'); - - // Back to the source language. - $this->drupalGet('node/' . $node->id()); - $this->clickLink(t('Edit')); - $this->assertText('Title in english'); - $this->assertText('Text in english'); - // Save the original content on second request. - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - $this->assertText('Paragraphed article Title in english has been updated.'); - - // Test if reverting to old paragraphs revisions works, make sure that - // the reverted node can be saved again. - $this->drupalGet('fr/node/' . $node->id() . '/revisions'); - $this->clickLink(t('Revert')); - $this->drupalPostForm(NULL, ['revert_untranslated_fields' => TRUE], t('Revert')); - $this->clickLink(t('Edit')); - $this->assertRaw('Title in french'); - $this->assertText('Text in french'); - $this->drupalPostForm(NULL, [], t('Save (this translation)')); - $this->assertNoRaw('The content has either been modified by another user, or you have already submitted modifications'); - $this->assertText('Text in french'); - - //Add paragraphed content with untranslatable language - $this->drupalGet('node/add/paragraphed_content_demo'); - $edit = array('langcode[0][value]' => LanguageInterface::LANGCODE_NOT_SPECIFIED); - $this->drupalPostForm(NULL, $edit, t('Add Text + Image')); - $this->assertResponse(200); - - // Make 'Images' paragraph field translatable, enable alt and title fields. - $this->drupalGet('admin/structure/paragraphs_type/images/fields'); - $this->clickLink('Edit'); - $edit = [ - 'translatable' => 1, - 'settings[alt_field]' => 1, - 'settings[title_field]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, t('Save settings')); - - // Create a node with an image paragraph, its alt and title text. - $text = 'Trust me I\'m an image'; - file_put_contents('temporary://Image.jpg', $text); - $file_path = $this->container->get('file_system')->realpath('temporary://Image.jpg'); - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, [], t('Add Images')); - $this->drupalPostForm(NULL, ['files[field_paragraphs_demo_0_subform_field_images_demo_0][]' => $file_path], t('Upload')); - $edit = [ - 'title[0][value]' => 'Title EN', - 'field_paragraphs_demo[0][subform][field_images_demo][0][alt]' => 'Image alt', - 'field_paragraphs_demo[0][subform][field_images_demo][0][title]' => 'Image title', - 'field_paragraphs_demo[0][subform][field_images_demo][0][width]' => 100, - 'field_paragraphs_demo[0][subform][field_images_demo][0][height]' => 100, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - // Translate the node with the image paragraph. - $this->clickLink('Translate'); - $this->clickLink(t('Add'), 1); - $edit = [ - 'title[0][value]' => 'Title FR', - 'field_paragraphs_demo[0][subform][field_images_demo][0][alt]' => 'Image alt FR', - 'field_paragraphs_demo[0][subform][field_images_demo][0][title]' => 'Image title FR', - ]; - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertRaw('Title FR'); - - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, [], t('Add Text')); - $edit = [ - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'texto', - 'title[0][value]' => 'titulo', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('titulo'); - $this->assertParagraphsLangcode($node->id(), 'de'); - - // Test langcode matching when Paragraphs and node have different language. - $paragraph_1 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'text', - 'langcode' => 'en', - 'field_text_demo' => 'english_text_1', - ]); - $paragraph_1->save(); - - $paragraph_2 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'text', - 'langcode' => 'en', - 'field_text_demo' => 'english_text_2', - ]); - $paragraph_2->save(); - - $paragraph_data = $paragraph_2->toArray(); - $paragraph_data['field_text_demo'] = 'german_text_2'; - $paragraph_2->addTranslation('de', $paragraph_data); - $paragraph_2->save(); - $translated_paragraph = $paragraph_2->getTranslation('en'); - - $node = $this->createNode([ - 'langcode' => 'de', - 'type' => 'paragraphed_content_demo', - 'field_paragraphs_demo' => [$paragraph_1, $translated_paragraph], - ]); - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('Paragraphed article ' . $node->label() . ' has been updated.'); - // Check that first paragraph langcode has been updated. - $paragraph = Paragraph::load($paragraph_1->id()); - $this->assertEqual($paragraph->language()->getId(), 'de'); - $this->assertFalse($paragraph->hasTranslation('en')); - // Check that second paragraph has two translations. - $paragraph = Paragraph::load($paragraph_2->id()); - $this->assertTrue($paragraph->hasTranslation('de')); - $this->assertTrue($paragraph->hasTranslation('en')); - $this->assertRaw('german_text'); - - // Create an english translation of the node. - $edit = [ - 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'english_translation_1', - 'field_paragraphs_demo[1][subform][field_text_demo][0][value]' => 'english_translation_2', - ]; - $this->drupalPostForm('node/' . $node->id() . '/translations/add/de/en', $edit, t('Save (this translation)')); - // Attempt to create a french translation. - $this->drupalGet('node/' . $node->id() . '/translations/add/de/fr'); - // Check that the german translation of the paragraphs is displayed. - $this->assertFieldByName('field_paragraphs_demo[0][subform][field_text_demo][0][value]', 'english_text_1'); - $this->assertFieldByName('field_paragraphs_demo[1][subform][field_text_demo][0][value]', 'german_text_2'); - $this->drupalPostForm(NULL, ['source_langcode[source]' => 'en'], t('Change')); - // Check that the english translation of the paragraphs is displayed. - $this->assertFieldByName('field_paragraphs_demo[0][subform][field_text_demo][0][value]', 'english_translation_1'); - $this->assertFieldByName('field_paragraphs_demo[1][subform][field_text_demo][0][value]', 'english_translation_2'); - - // Create a node with empty Paragraphs. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, [], t('Add Nested Paragraph')); - $edit = ['title[0][value]' => 'empty_node']; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Attempt to translate it. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add')); - // Check the add button is not displayed. - $this->assertEqual(count($this->xpath('//*[@name="field_paragraphs_demo_0_subform_field_paragraphs_demo_images_add_more"]')), 0); - - // Add a non translatable field to Text Paragraph type. - $edit = [ - 'new_storage_type' => 'text_long', - 'label' => 'untranslatable_field', - 'field_name' => 'untranslatable_field', - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/add-field', $edit, t('Save and continue')); - $this->drupalPostForm(NULL, [], t('Save field settings')); - $this->drupalPostForm(NULL, [], t('Save settings')); - - // Add a non translatable reference field. - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference:node', - 'label' => 'untranslatable_ref_field', - 'field_name' => 'untranslatable_ref_field', - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/add-field', $edit, t('Save and continue')); - $this->drupalPostForm(NULL, [], t('Save field settings')); - $this->drupalPostForm(NULL, ['settings[handler_settings][target_bundles][paragraphed_content_demo]' => TRUE], t('Save settings')); - - // Add a non translatable link field. - $edit = [ - 'new_storage_type' => 'link', - 'label' => 'untranslatable_link_field', - 'field_name' => 'untranslatable_link_field', - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/add-field', $edit, t('Save and continue')); - $this->drupalPostForm(NULL, [], t('Save field settings')); - $this->drupalPostForm(NULL, [], t('Save settings')); - - // Attempt to add a translation. - $this->drupalGet('node/' . $node->id() . '/translations/add/de/fr'); - $this->assertText('untranslatable_field (all languages)'); - $this->assertText('untranslatable_ref_field (all languages)'); - $this->assertText('untranslatable_link_field (all languages)'); - $this->assertNoText('Text (all languages)'); - - // Enable translations for the reference and link field. - $edit = [ - 'translatable' => TRUE, - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/paragraph.text.field_untranslatable_ref_field', $edit, t('Save settings')); - $this->drupalPostForm('admin/structure/paragraphs_type/text/fields/paragraph.text.field_untranslatable_link_field', $edit, t('Save settings')); - - // Attempt to add a translation. - $this->drupalGet('node/' . $node->id() . '/translations/add/de/fr'); - $this->assertText('untranslatable_field (all languages)'); - $this->assertNoText('untranslatable_link_field (all languages)'); - $this->assertNoText('untranslatable_ref_field (all languages)'); - $this->assertNoText('Text (all languages)'); - } - - /** - * Tests the paragraph buttons presence in translation multilingual workflow. - * - * This test covers the following test cases: - * 1) original node langcode in EN, translate in FR, change to DE. - * 2) original node langcode in DE, change site langcode to DE, change node - * langcode to EN. - */ - public function testParagraphTranslationMultilingual() { - // Case 1: original node langcode in EN, translate in FR, change to DE. - - // Add 'Images' paragraph and check the paragraphs buttons are displayed. - // Use the experimental widget. - $form_display = EntityFormDisplay::load('node.paragraphed_content_demo.default') - ->setComponent('field_paragraphs_demo', [ - 'type' => 'paragraphs', - ]); - $form_display->save(); - // Use the experimental widget. - $form_display = EntityFormDisplay::load('paragraph.nested_paragraph.default') - ->setComponent('field_paragraphs_demo', [ - 'type' => 'paragraphs', - ]); - $form_display->save(); - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, NULL, t('Add Images')); - $this->assertParagraphsButtons(1); - // Upload an image and check the paragraphs buttons are still displayed. - $images = $this->drupalGetTestFiles('image')[0]; - $edit = [ - 'title[0][value]' => 'Title in english', - 'files[field_paragraphs_demo_0_subform_field_images_demo_0][]' => $images->uri, - ]; - $this->drupalPostForm(NULL, $edit, t('Upload')); - $this->assertParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save')); - $this->assertText('Title in english'); - $node = $this->drupalGetNodeByTitle('Title in english'); - // Check the paragraph langcode is 'en'. - $this->assertParagraphsLangcode($node->id()); - - // Add french translation. - $this->clickLink(t('Translate')); - $this->clickLink(t('Add'), 1); - // Make sure the host entity and its paragraphs have valid source language - // and check that the paragraphs buttons are hidden. - $this->assertNoParagraphsButtons(1); - $edit = [ - 'title[0][value]' => 'Title in french', - ]; - $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); - $this->assertParagraphsLangcode($node->id(), 'en', 'fr'); - $this->assertText('Paragraphed article Title in french has been updated.'); - $this->assertText('Title in french'); - $this->assertNoText('Title in english'); - // Check the original node and the paragraph langcode is still 'en'. - $this->assertParagraphsLangcode($node->id()); - - // Edit the french translation and upload a new image. - $this->clickLink('Edit'); - $images = $this->drupalGetTestFiles('image')[1]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_0_subform_field_images_demo_1][]' => $images->uri, - ], t('Upload')); - // Check editing a translation does not affect the source langcode and - // check that the paragraphs buttons are still hidden. - $this->assertParagraphsLangcode($node->id(), 'en', 'fr'); - $this->assertNoParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - $this->assertText('Title in french'); - $this->assertNoText('Title in english'); - - // Back to the original node. - $this->drupalGet('node/' . $node->id()); - $this->assertText('Title in english'); - $this->assertNoText('Title in french'); - // Check the original node and the paragraph langcode are still 'en' and - // check that the paragraphs buttons are still displayed. - $this->clickLink('Edit'); - $this->assertParagraphsLangcode($node->id()); - $this->assertParagraphsButtons(1); - // Change the node langcode to 'german', add a 'Nested Paragraph', check - // the paragraphs langcode are still 'en' and their buttons are displayed. - $edit = [ - 'title[0][value]' => 'Title in english (de)', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Nested Paragraph')); - $this->assertParagraphsLangcode($node->id()); - $this->assertParagraphsButtons(2); - // Add an 'Images' paragraph inside the nested one, check the paragraphs - // langcode are still 'en' and the paragraphs buttons are still displayed. - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_1_subform_field_paragraphs_demo_images_add_more'); - $this->assertParagraphsLangcode($node->id()); - $this->assertParagraphsButtons(2); - // Upload a new image, check the paragraphs langcode are still 'en' and the - // paragraphs buttons are displayed. - $images = $this->drupalGetTestFiles('image')[2]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_1_subform_field_paragraphs_demo_0_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node->id()); - $this->assertParagraphsButtons(2); - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - $this->assertText('Title in english (de)'); - $this->assertNoText('Title in french'); - // Check the original node and the paragraphs langcode are now 'de'. - $this->assertParagraphsLangcode($node->id(), 'de'); - - // Check the french translation. - $this->drupalGet('fr/node/' . $node->id()); - $this->assertText('Title in french'); - $this->assertNoText('Title in english (de)'); - // Check editing a translation does not affect the source langcode and - // check that the paragraphs buttons are still hidden. - $this->clickLink('Edit'); - $this->assertParagraphsLangcode($node->id(), 'de', 'fr'); - $this->assertNoParagraphsButtons(2); - - // Case 2: original node langcode in DE, change site langcode to DE, change - // node langcode to EN. - - // Change the site langcode to french. - $this->drupalPostForm('admin/config/regional/language', [ - 'site_default_language' => 'fr', - ], t('Save configuration')); - - // Check the original node and its paragraphs langcode are still 'de' - // and the paragraphs buttons are still displayed. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertParagraphsLangcode($node->id(), 'de'); - $this->assertParagraphsButtons(2); - - // Go to the french translation. - $this->drupalGet('node/' . $node->id() . '/translations'); - $this->clickLink(t('Edit'), 1); - // Check editing a translation does not affect the source langcode and - // check that the paragraphs buttons are still hidden. - $this->assertParagraphsLangcode($node->id(), 'de', 'fr'); - $this->assertNoParagraphsButtons(2); - // Upload another image. - $images = $this->drupalGetTestFiles('image')[3]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_1_subform_field_paragraphs_demo_0_subform_field_images_demo_1][]' => $images->uri, - ], t('Upload')); - // Check editing a translation does not affect the source langcode and - // check that the paragraphs buttons are still hidden. - $this->assertParagraphsLangcode($node->id(), 'de', 'fr'); - $this->assertNoParagraphsButtons(2); - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - // Check the paragraphs langcode are still 'de' after saving the translation. - $this->assertParagraphsLangcode($node->id(), 'de', 'fr'); - $this->assertText('Title in french'); - $this->assertNoText('Title in english (de)'); - - // Back to the original node. - $this->drupalGet('node/' . $node->id()); - $this->assertText('Title in english (de)'); - $this->assertNoText('Title in french'); - // Check the original node and the paragraphs langcode are still 'de' and - // check that the paragraphs buttons are still displayed. - $this->clickLink('Edit'); - $this->assertParagraphsLangcode($node->id(), 'de'); - $this->assertParagraphsButtons(2); - // Change the node langcode back to 'english', add an 'Images' paragraph, - // check the paragraphs langcode are still 'de' and their buttons are shown. - $edit = [ - 'title[0][value]' => 'Title in english', - 'langcode[0][value]' => 'en', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Images')); - $this->assertParagraphsLangcode($node->id(), 'de'); - $this->assertParagraphsButtons(3); - // Upload a new image, check the paragraphs langcode are still 'de' and the - // paragraphs buttons are displayed. - $images = $this->drupalGetTestFiles('image')[4]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_2_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node->id(), 'de'); - $this->assertParagraphsButtons(3); - $this->drupalPostForm(NULL, NULL, t('Save (this translation)')); - // Check the original node and the paragraphs langcode are now 'en'. - $this->assertParagraphsLangcode($node->id()); - } - - /** - * Tests the paragraphs buttons presence in multilingual workflow. - * - * This test covers the following test cases: - * 1) original node langcode in german, change to english. - * 2) original node langcode in english, change to german. - * 3) original node langcode in english, change site langcode to german, - * change node langcode to german. - */ - public function testParagraphsMultilingualWorkflow() { - // Case 1: Check the paragraphs buttons after changing the NODE language - // (original node langcode in GERMAN, default site langcode in english). - // Use the experimental widget. - $form_display = EntityFormDisplay::load('node.paragraphed_content_demo.default') - ->setComponent('field_paragraphs_demo', [ - 'type' => 'paragraphs', - ]); - $form_display->save(); - // Use the experimental widget. - $form_display = EntityFormDisplay::load('paragraph.nested_paragraph.default') - ->setComponent('field_paragraphs_demo', [ - 'type' => 'paragraphs', - ]); - $form_display->save(); - // Create a node and check that the node langcode is 'english'. - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->assertOptionSelected('edit-langcode-0-value', 'en'); - // Change the node langcode to 'german' and add a 'Nested Paragraph'. - $edit = [ - 'title[0][value]' => 'Title in german', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Nested Paragraph')); - // Check that the paragraphs buttons are displayed and add an 'Images' - // paragraph inside the nested paragraph. - $this->assertParagraphsButtons(1); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_0_subform_field_paragraphs_demo_images_add_more'); - // Upload an image and check the paragraphs buttons are still displayed. - $images = $this->drupalGetTestFiles('image')[0]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_0_subform_field_paragraphs_demo_0_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save')); - $this->assertText('Title in german'); - $node1 = $this->getNodeByTitle('Title in german'); - - // Check the paragraph langcode is 'de' and its buttons are displayed. - // @todo check for the nested children paragraphs buttons and langcode - // when it's supported. - $this->clickLink(t('Edit')); - $this->assertParagraphsLangcode($node1->id(), 'de'); - $this->assertParagraphsButtons(1); - // Change the node langcode to 'english' and upload another image. - $images = $this->drupalGetTestFiles('image')[1]; - $edit = [ - 'title[0][value]' => 'Title in german (en)', - 'langcode[0][value]' => 'en', - 'files[field_paragraphs_demo_0_subform_field_paragraphs_demo_0_subform_field_images_demo_1][]' => $images->uri, - ]; - $this->drupalPostForm(NULL, $edit, t('Upload')); - // Check the paragraph langcode is still 'de' and its buttons are shown. - $this->assertParagraphsLangcode($node1->id(), 'de'); - $this->assertParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save')); - // Check the paragraph langcode is now 'en' after saving. - $this->assertParagraphsLangcode($node1->id()); - - // Check the paragraph langcode is 'en' and its buttons are still shown. - $this->clickLink(t('Edit')); - $this->assertParagraphsLangcode($node1->id()); - $this->assertParagraphsButtons(1); - - // Case 2: Check the paragraphs buttons after changing the NODE language - // (original node langcode in ENGLISH, default site langcode in english). - - // Create another node. - $this->drupalGet('node/add/paragraphed_content_demo'); - // Check that the node langcode is 'english' and add a 'Nested Paragraph'. - $this->assertOptionSelected('edit-langcode-0-value', 'en'); - $this->drupalPostForm(NULL, NULL, t('Add Nested Paragraph')); - // Check that the paragraphs buttons are displayed and add an 'Images' - // paragraph inside the nested paragraph. - $this->assertParagraphsButtons(1); - $this->drupalPostAjaxForm(NULL, NULL, 'field_paragraphs_demo_0_subform_field_paragraphs_demo_images_add_more'); - // Upload an image and check the paragraphs buttons are still displayed. - $images = $this->drupalGetTestFiles('image')[0]; - $edit = [ - 'title[0][value]' => 'Title in english', - 'files[field_paragraphs_demo_0_subform_field_paragraphs_demo_0_subform_field_images_demo_0][]' => $images->uri, - ]; - $this->drupalPostForm(NULL, $edit, t('Upload')); - $this->assertParagraphsButtons(1); - $this->drupalPostForm(NULL, NULL, t('Save')); - $this->assertText('Title in english'); - $node2 = $this->drupalGetNodeByTitle('Title in english'); - - // Check the paragraph langcode is 'en' and its buttons are displayed. - // @todo check for the nested children paragraphs buttons and langcode - // when it's supported. - $this->clickLink(t('Edit')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(1); - // Change the node langcode to 'german' and add another 'Images' paragraph. - $edit = [ - 'title[0][value]' => 'Title in english (de)', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Images')); - // Check the paragraphs langcode are still 'en' and their buttons are shown. - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(2); - // Upload an image, check the paragraphs langcode are still 'en' and their - // buttons are displayed. - $images = $this->drupalGetTestFiles('image')[1]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_1_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(2); - $this->drupalPostForm(NULL, NULL, t('Save')); - // Check the paragraphs langcode are now 'de' after saving. - $this->assertParagraphsLangcode($node2->id(), 'de'); - - // Change node langcode back to 'english' and save. - $this->clickLink(t('Edit')); - $edit = [ - 'title[0][value]' => 'Title in english', - 'langcode[0][value]' => 'en', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - // Check the paragraphs langcode are now 'en' after saving. - $this->assertParagraphsLangcode($node2->id()); - - // Case 3: Check the paragraphs buttons after changing the SITE language. - - // Change the site langcode to german. - $edit = [ - 'site_default_language' => 'de', - ]; - $this->drupalPostForm('admin/config/regional/language', $edit, t('Save configuration')); - - // Check the original node and the paragraphs langcode are still 'en' and - // check that the paragraphs buttons are still displayed. - $this->drupalGet('node/' . $node2->id() . '/edit'); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(2); - // Add another 'Images' paragraph with node langcode as 'english'. - $this->drupalPostForm(NULL, NULL, t('Add Images')); - // Check the paragraph langcode are still 'en' and their buttons are shown. - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(3); - // Upload an image, check the paragraphs langcode are still 'en' and their - // buttons are displayed. - $images = $this->drupalGetTestFiles('image')[2]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_2_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(3); - $this->drupalPostForm(NULL, NULL, t('Save')); - // Check the paragraphs langcode are still 'en' after saving. - $this->assertParagraphsLangcode($node2->id()); - - // Check the paragraphs langcode are still 'en' and their buttons are shown. - $this->clickLink(t('Edit')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(3); - // Change node langcode to 'german' and add another 'Images' paragraph. - $edit = [ - 'title[0][value]' => 'Title in english (de)', - 'langcode[0][value]' => 'de', - ]; - $this->drupalPostForm(NULL, $edit, t('Add Images')); - // Check the paragraphs langcode are still 'en' and their buttons are shown. - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(4); - // Upload an image, check the paragraphs langcode are still 'en' and their - // buttons are displayed. - $images = $this->drupalGetTestFiles('image')[3]; - $this->drupalPostForm(NULL, [ - 'files[field_paragraphs_demo_3_subform_field_images_demo_0][]' => $images->uri, - ], t('Upload')); - $this->assertParagraphsLangcode($node2->id()); - $this->assertParagraphsButtons(4); - $this->drupalPostForm(NULL, NULL, t('Save')); - // Check the paragraphs langcode are now 'de' after saving. - $this->assertParagraphsLangcode($node2->id(), 'de'); - } - - /** - * Passes if the paragraphs buttons are present. - * - * @param int $count - * Number of paragraphs buttons to look for. - */ - protected function assertParagraphsButtons($count) { - $this->assertParagraphsButtonsHelper($count, FALSE); - } - - /** - * Passes if the paragraphs buttons are NOT present. - * - * @param int $count - * Number of paragraphs buttons to look for. - */ - protected function assertNoParagraphsButtons($count) { - $this->assertParagraphsButtonsHelper($count, TRUE); - } - - /** - * Helper for assertParagraphsButtons and assertNoParagraphsButtons. - * - * @param int $count - * Number of paragraphs buttons to look for. - * @param bool $hidden - * TRUE if these buttons should not be shown, FALSE otherwise. - * Defaults to TRUE. - */ - protected function assertParagraphsButtonsHelper($count, $hidden = TRUE) { - for ($i = 0; $i < $count; $i++) { - $remove_button = $this->xpath('//*[@name="field_paragraphs_demo_' . $i . '_remove"]'); - if (!$hidden) { - $this->assertNotEqual(count($remove_button), 0); - } - else { - $this->assertEqual(count($remove_button), 0); - } - } - - // It is enough to check for the specific paragraph type 'Images' to assert - // the add more buttons presence for this test class. - $add_button = $this->xpath('//input[@value="Add Images"]'); - if (!$hidden) { - $this->assertNotEqual(count($add_button), 0); - } - else { - $this->assertEqual(count($add_button), 0); - } - } - - /** - * Assert each paragraph items have the same langcode as the node one. - * - * @param string $node_id - * The node ID which contains the paragraph items to be checked. - * @param string $source_lang - * The expected node source langcode. Defaults to 'en'. - * @param string $trans_lang - * The expected translated node langcode. Defaults to NULL. - */ - protected function assertParagraphsLangcode($node_id, $source_lang = 'en', $trans_lang = NULL) { - // Update the outdated node and check all the paragraph items langcodes. - \Drupal::entityTypeManager()->getStorage('node')->resetCache([$node_id]); - /** @var \Drupal\node\NodeInterface $node */ - $node = Node::load($node_id); - $node_langcode = $node->langcode->value; - $this->assertEqual($node_langcode, $source_lang, 'Host langcode matches.'); - - /** @var \Drupal\Core\Entity\ContentEntityBase $paragraph */ - foreach ($node->field_paragraphs_demo->referencedEntities() as $paragraph) { - $paragraph_langcode = $paragraph->language()->getId(); - $message = new FormattableMarkup('Node langcode is "@node", paragraph item langcode is "@item".', ['@node' => $source_lang, '@item' => $paragraph_langcode]); - $this->assertEqual($paragraph_langcode, $source_lang, $message); - } - - // Check the translation. - if (!empty($trans_lang)) { - $this->assertTrue($node->hasTranslation($trans_lang), 'Translation exists.'); - } - if ($node->hasTranslation($trans_lang)) { - $trans_node = $node->getTranslation($trans_lang); - $trans_node_langcode = $trans_node->language()->getId(); - $this->assertEqual($trans_node_langcode, $trans_lang, 'Translated node langcode matches.'); - - // Check the paragraph item langcode matching the translated node langcode. - foreach ($trans_node->field_paragraphs_demo->referencedEntities() as $paragraph) { - if ($paragraph->hasTranslation($trans_lang)) { - $trans_item = $paragraph->getTranslation($trans_lang); - $paragraph_langcode = $trans_item->language()->getId(); - $message = new FormattableMarkup('Translated node langcode is "@node", paragraph item langcode is "@item".', ['@node' => $trans_lang, '@item' => $paragraph_langcode]); - $this->assertEqual($paragraph_langcode, $trans_lang, $message); - } - } - } - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTypesTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTypesTest.php deleted file mode 100644 index e02520fdc..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalTypesTest.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -/** - * Tests paragraphs types. - * - * @group paragraphs - */ -class ParagraphsExperimentalTypesTest extends ParagraphsExperimentalTestBase { - - /** - * Tests the deletion of Paragraphs types. - */ - public function testRemoveTypesWithContent() { - $this->loginAsAdmin(); - // Add a Paragraphed test content. - $this->addParagraphedContentType('paragraphed_test', 'paragraphs'); - - $this->addParagraphsType('paragraph_type_test'); - $this->addParagraphsType('text'); - - // Attempt to delete the content type not used yet. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->clickLink(t('Delete')); - $this->assertText('This action cannot be undone.'); - $this->clickLink(t('Cancel')); - - // Add a test node with a Paragraph. - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostAjaxForm(NULL, [], 'paragraphs_paragraph_type_test_add_more'); - $edit = ['title[0][value]' => 'test_node']; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('paragraphed_test test_node has been created.'); - - // Attempt to delete the paragraph type already used. - $this->drupalGet('admin/structure/paragraphs_type'); - $this->clickLink(t('Delete')); - $this->assertText('paragraph_type_test Paragraphs type is used by 1 piece of content on your site. You can not remove this paragraph_type_test Paragraphs type until you have removed all from the content.'); - } - - /** - * Tests creating paragraph type. - */ - public function testCreateParagraphType() { - $this->loginAsAdmin(); - - // Add a paragraph type. - $this->drupalGet('/admin/structure/paragraphs_type/add'); - - // Create a paragraph type with label and id more than 32 characters. - $edit = [ - 'label' => 'Test', - 'id' => 'test_name_with_more_than_32_characters' - ]; - $this->drupalPostForm(NULL, $edit, 'Save and manage fields'); - $this->assertNoErrorsLogged(); - $this->assertText('Machine-readable name cannot be longer than 32 characters but is currently 38 characters long.'); - $edit['id'] = 'new_test_id'; - $this->drupalPostForm(NULL, $edit, 'Save and manage fields'); - $this->assertText('Saved the Test Paragraphs type.'); - } -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalUiTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalUiTest.php deleted file mode 100644 index b77d41aaf..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalUiTest.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\field_ui\Tests\FieldUiTestTrait; - -/** - * Tests the Paragraphs user interface. - * - * @group paragraphs - */ -class ParagraphsExperimentalUiTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'paragraphs_demo', - ]; - - /** - * Tests displaying an error message a required paragraph field that is empty. - */ - public function testEmptyRequiredField() { - $admin_user = $this->drupalCreateUser([ - 'administer node fields', - 'administer paragraph form display', - 'administer node form display', - 'create paragraphed_content_demo content', - 'edit any paragraphed_content_demo content', - ]); - $this->drupalLogin($admin_user); - - // Add required field to paragraphed content type. - $bundle_path = 'admin/structure/types/manage/paragraphed_content_demo'; - $field_title = 'Content Test'; - $field_type = 'field_ui:entity_reference_revisions:paragraph'; - $field_edit = [ - 'required' => TRUE, - ]; - $this->fieldUIAddNewField($bundle_path, 'content', $field_title, $field_type, [], $field_edit); - - $form_display_edit = [ - 'fields[field_content][type]' => 'paragraphs', - ]; - $this->drupalPostForm($bundle_path . '/form-display', $form_display_edit, t('Save')); - - // Attempt to create a paragraphed node with an empty required field. - $title = 'Empty'; - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostForm(NULL, ['title[0][value]' => $title], t('Save')); - $this->assertText($field_title . ' field is required'); - - // Attempt to create a paragraphed node with only a paragraph in the - // "remove" mode in the required field. - $title = 'Remove all items'; - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostAjaxForm(NULL, [], 'field_content_image_text_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_content_0_remove'); - $this->assertNoText($field_title . ' field is required'); - $this->drupalPostForm(NULL, ['title[0][value]' => $title], t('Save')); - $this->assertText($field_title . ' field is required'); - - // Attempt to create a paragraphed node with a valid paragraph and a - // removed paragraph. - $title = 'Valid Removal'; - $this->drupalGet('node/add/paragraphed_content_demo'); - $this->drupalPostAjaxForm(NULL, [], 'field_content_image_text_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_content_image_text_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_content_1_remove'); - $this->assertNoText($field_title . ' field is required'); - $this->drupalPostForm(NULL, ['title[0][value]' => $title], t('Save')); - $this->assertNoText($field_title . ' field is required'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalWidgetButtonsTest.php b/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalWidgetButtonsTest.php deleted file mode 100644 index 886d7898a..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/Experimental/ParagraphsExperimentalWidgetButtonsTest.php +++ /dev/null @@ -1,187 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests\Experimental; - -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\Tests\paragraphs\FunctionalJavascript\ParagraphsTestBaseTrait; - -/** - * Tests paragraphs experimental widget buttons. - * - * @group paragraphs - */ -class ParagraphsExperimentalWidgetButtonsTest extends ParagraphsExperimentalTestBase { - - use FieldUiTestTrait; - use ParagraphsTestBaseTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'paragraphs_test', - ]; - - /** - * Tests the widget buttons of paragraphs. - */ - public function testWidgetButtons() { - $this->addParagraphedContentType('paragraphed_test'); - - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']); - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - $this->addParagraphsType('text'); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - $edit = [ - 'fields[field_paragraphs][type]' => 'paragraphs', - ]; - $this->drupalPostForm('admin/structure/types/manage/paragraphed_test/form-display', $edit, t('Save')); - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_text_paragraph_add_more'); - - // Create a node with a Paragraph. - $text = 'recognizable_text'; - $edit = [ - 'title[0][value]' => 'paragraphs_mode_test', - 'field_paragraphs[0][subform][field_text][0][value]' => $text, - ]; - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_paragraph_add_more'); - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('paragraphs_mode_test'); - - // Test the 'Open' edit mode. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', $text); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText($text); - - // Test the 'Closed' edit mode. - $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'closed'); - $this->drupalGet('node/' . $node->id() . '/edit'); - // Click "Edit" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_1_edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', $text); - $closed_mode_text = 'closed_mode_text'; - // Click "Collapse" button on both paragraphs. - $edit = ['field_paragraphs[0][subform][field_text][0][value]' => $closed_mode_text]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_collapse'); - $edit = ['field_paragraphs[1][subform][field_text][0][value]' => $closed_mode_text]; - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_1_collapse'); - // Verify that we have warning message for each paragraph. - $this->assertEqual(2, count($this->xpath("//*[contains(@class, 'paragraphs-icon-changed')]"))); - $this->assertRaw('<div class="paragraphs-collapsed-description">' . $closed_mode_text); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('paragraphed_test ' . $node->label() . ' has been updated.'); - $this->assertText($closed_mode_text); - - // Test the 'Preview' closed mode. - $this->setParagraphsWidgetSettings('paragraphed_test', 'field_paragraphs', ['closed_mode' => 'preview']); - $this->drupalGet('node/' . $node->id() . '/edit'); - // Click "Edit" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit'); - $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', $closed_mode_text); - $preview_mode_text = 'preview_mode_text'; - $edit = ['field_paragraphs[0][subform][field_text][0][value]' => $preview_mode_text]; - // Click "Collapse" button. - $this->drupalPostAjaxForm(NULL, $edit, 'field_paragraphs_0_collapse'); - $this->assertText('You have unsaved changes on this Paragraph item.'); - $this->assertEqual(1, count($this->xpath("//*[contains(@class, 'paragraphs-icon-changed')]"))); - $this->assertText($preview_mode_text); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('paragraphed_test ' . $node->label() . ' has been updated.'); - $this->assertText($preview_mode_text); - - // Test the remove function. - $this->drupalGet('node/' . $node->id() . '/edit'); - // Click "Remove" button. - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_remove'); - $this->drupalPostForm(NULL, [], t('Save')); - $this->assertText('paragraphed_test ' . $node->label() . ' has been updated.'); - $this->assertNoText($preview_mode_text); - } - - /** - * Tests if buttons are present for each widget mode. - */ - public function testButtonsVisibility() { - $this->addParagraphedContentType('paragraphed_test'); - - $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']); - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - $this->addParagraphsType('text'); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - $edit = [ - 'fields[field_paragraphs][type]' => 'paragraphs', - ]; - $this->drupalPostForm('admin/structure/types/manage/paragraphed_test/form-display', $edit, t('Save')); - $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_text_paragraph_add_more'); - - // Create a node with a Paragraph. - $text = 'recognizable_text'; - $edit = [ - 'title[0][value]' => 'paragraphs_mode_test', - 'field_paragraphs[0][subform][field_text][0][value]' => $text, - ]; - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_text_paragraph_add_more'); - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('paragraphs_mode_test'); - - // Checking visible buttons on "Open" mode. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertField('field_paragraphs_0_collapse'); - $this->assertField('field_paragraphs_0_remove'); - $this->assertField('field_paragraphs_0_duplicate'); - - // Checking visible buttons on "Closed" mode. - $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'closed'); - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertField('field_paragraphs_0_edit'); - $this->assertField('field_paragraphs_0_remove'); - $this->assertField('field_paragraphs_0_duplicate'); - - // Checking visible buttons on "Preview" mode. - $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'closed'); - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertField('field_paragraphs_0_edit'); - $this->assertField('field_paragraphs_0_remove'); - $this->assertField('field_paragraphs_0_duplicate'); - - // Checking always show collapse and edit actions. - $this->addParagraphsType('nested_paragraph'); - static::fieldUIAddNewField('admin/structure/paragraphs_type/nested_paragraph', 'nested', 'Nested', 'field_ui:entity_reference_revisions:paragraph', [ - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ], []); - $this->drupalGet('admin/structure/paragraphs_type/nested_paragraph/form-display'); - $edit = [ - 'fields[field_nested][type]' => 'paragraphs', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_nested_paragraph_add_more'); - $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_2_subform_field_nested_nested_paragraph_add_more'); - // Collapse is present on each nesting level. - $this->assertFieldByName('field_paragraphs_2_collapse'); - $this->assertFieldByName('field_paragraphs_2_subform_field_nested_0_collapse'); - - // Tests hook_paragraphs_widget_actions_alter. - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostForm(NULL, NULL, t('Add text')); - $this->assertNoField('edit-field-paragraphs-0-top-links-test-button'); - \Drupal::state()->set('paragraphs_test_dropbutton', TRUE); - $this->drupalGet('node/add/paragraphed_test'); - $this->drupalPostForm(NULL, NULL, t('Add text')); - $this->assertNoField('edit-field-paragraphs-0-top-links-test-button'); - } - -} diff --git a/web/modules/contrib/paragraphs/src/Tests/ParagraphsUninstallTest.php b/web/modules/contrib/paragraphs/src/Tests/ParagraphsUninstallTest.php deleted file mode 100644 index 0d30362c0..000000000 --- a/web/modules/contrib/paragraphs/src/Tests/ParagraphsUninstallTest.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php - -namespace Drupal\paragraphs\Tests; - -use Drupal\simpletest\WebTestBase; - -/** - * Tests that Paragraphs module can be uninstalled. - * - * @group paragraphs - */ -class ParagraphsUninstallTest extends WebTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array('paragraphs_demo'); - - /** - * {@inheritdoc} - */ - public function setUp() { - parent::setUp(); - - $admin_user = $this->drupalCreateUser(array( - 'administer paragraphs types', - 'administer modules', - )); - $this->drupalLogin($admin_user); - } - - /** - * Tests that Paragraphs module can be uninstalled. - */ - public function testUninstall() { - - // Uninstall the module paragraphs_demo. - $this->drupalPostForm('admin/modules/uninstall', ['uninstall[paragraphs_demo]' => TRUE], t('Uninstall')); - $this->drupalPostForm(NULL, [], t('Uninstall')); - - // Delete paragraphs data. - $this->clickLink('Remove paragraph entities'); - $this->drupalPostForm(NULL, [], t('Delete all paragraph entities')); - - // Uninstall the module paragraphs. - $this->drupalPostForm('admin/modules/uninstall', ['uninstall[paragraphs]' => TRUE], t('Uninstall')); - $this->drupalPostForm(NULL, [], t('Uninstall')); - $this->assertText(t('The selected modules have been uninstalled.')); - $this->assertNoText(t('Paragraphs demo')); - $this->assertNoText(t('Paragraphs')); - } - -} diff --git a/web/modules/contrib/paragraphs/templates/paragraph.html.twig b/web/modules/contrib/paragraphs/templates/paragraph.html.twig deleted file mode 100644 index f2ef92b92..000000000 --- a/web/modules/contrib/paragraphs/templates/paragraph.html.twig +++ /dev/null @@ -1,54 +0,0 @@ -{# -/** - * @file - * Default theme implementation to display a paragraph. - * - * Available variables: - * - paragraph: Full paragraph entity. - * Only method names starting with "get", "has", or "is" and a few common - * methods such as "id", "label", and "bundle" are available. For example: - * - paragraph.getCreatedTime() will return the paragraph creation timestamp. - * - paragraph.id(): The paragraph ID. - * - paragraph.bundle(): The type of the paragraph, for example, "image" or "text". - * - paragraph.getOwnerId(): The user ID of the paragraph author. - * See Drupal\paragraphs\Entity\Paragraph for a full list of public properties - * and methods for the paragraph object. - * - content: All paragraph items. Use {{ content }} to print them all, - * or print a subset such as {{ content.field_example }}. Use - * {{ content|without('field_example') }} to temporarily suppress the printing - * of a given child element. - * - attributes: HTML attributes for the containing element. - * The attributes.class element may contain one or more of the following - * classes: - * - paragraphs: The current template type (also known as a "theming hook"). - * - paragraphs--type-[type]: The current paragraphs type. For example, if the paragraph is an - * "Image" it would result in "paragraphs--type--image". Note that the machine - * name will often be in a short form of the human readable label. - * - paragraphs--view-mode--[view_mode]: The View Mode of the paragraph; for example, a - * preview would result in: "paragraphs--view-mode--preview", and - * default: "paragraphs--view-mode--default". - * - view_mode: View mode; for example, "preview" or "full". - * - logged_in: Flag for authenticated user status. Will be true when the - * current user is a logged-in member. - * - is_admin: Flag for admin user status. Will be true when the current user - * is an administrator. - * - * @see template_preprocess_paragraph() - * - * @ingroup themeable - */ -#} -{% - set classes = [ - 'paragraph', - 'paragraph--type--' ~ paragraph.bundle|clean_class, - view_mode ? 'paragraph--view-mode--' ~ view_mode|clean_class, - ] -%} -{% block paragraph %} - <div{{ attributes.addClass(classes) }}> - {% block content %} - {{ content }} - {% endblock %} - </div> -{% endblock paragraph %} diff --git a/web/modules/contrib/paragraphs/templates/paragraphs-actions.html.twig b/web/modules/contrib/paragraphs/templates/paragraphs-actions.html.twig deleted file mode 100644 index b319c4a51..000000000 --- a/web/modules/contrib/paragraphs/templates/paragraphs-actions.html.twig +++ /dev/null @@ -1,29 +0,0 @@ -{# -/** - * @file - * Default theme implementation for a paragraphs actions component. - * - * Available variables: - * - actions - default actions, always visible and not in dropdown. - * - dropdown_actions - actions for dropdown subcomponent. - * - * @see template_preprocess() - * - * @ingroup themeable - */ -#} -<div class="paragraphs-actions"> - {{ actions }} - {# We are still using access attribute on some places to disable dropdown - actions and that is why we will first render dropdown_actions and then - render dropdown subcomoponent if needed. #} - {% set dropdown_actions_output = render_var(dropdown_actions) %} - {% if dropdown_actions_output %} - <div class="paragraphs-dropdown"> - <button class="paragraphs-dropdown-toggle"><span class="visually-hidden">{% trans %}Toggle Actions{% endtrans %}</span></button> - <div class="paragraphs-dropdown-actions"> - {{ dropdown_actions_output }} - </div> - </div> - {% endif %} -</div> diff --git a/web/modules/contrib/paragraphs/templates/paragraphs-add-dialog.html.twig b/web/modules/contrib/paragraphs/templates/paragraphs-add-dialog.html.twig deleted file mode 100644 index 970671291..000000000 --- a/web/modules/contrib/paragraphs/templates/paragraphs-add-dialog.html.twig +++ /dev/null @@ -1,23 +0,0 @@ -{# -/** - * @file - * Default theme implementation of modal add paragraph dialog template. - * - * Following classes have custom use: - * - paragraphs-add-dialog - is used to wrap the dialog. - * - paragraphs-add-dialog-row - is used to wrap the paragraph type rows. - * - * - * @ingroup themeable - */ -#} -{{ add }} -<div class="paragraphs-add-dialog js-hide"> - <ul class="paragraphs-add-dialog-list"> - {% for button in buttons %} - <li class="paragraphs-add-dialog-row"> - {{ button }} - </li> - {% endfor %} - </ul> -</div> diff --git a/web/modules/contrib/paragraphs/templates/paragraphs-dropbutton-wrapper.html.twig b/web/modules/contrib/paragraphs/templates/paragraphs-dropbutton-wrapper.html.twig deleted file mode 100644 index 38cff6739..000000000 --- a/web/modules/contrib/paragraphs/templates/paragraphs-dropbutton-wrapper.html.twig +++ /dev/null @@ -1,21 +0,0 @@ -{# -/** - * @file - * Default theme implementation for a paragraphs dropbutton wrapper. - * - * Available variables: - * - children: Contains the child elements of the paragraphs dropbutton menu. - * - * @see template_preprocess() - * @see template_preprocess_paragraphs_dropbutton_wrapper() - * - * @ingroup themeable - */ -#} -{% if children %} - {% spaceless %} - <div class="paragraphs-dropbutton-wrapper"> - {{ children }} - </div> - {% endspaceless %} -{% endif %} diff --git a/web/modules/contrib/paragraphs/templates/paragraphs-info-icon.html.twig b/web/modules/contrib/paragraphs/templates/paragraphs-info-icon.html.twig deleted file mode 100644 index 67c91a68d..000000000 --- a/web/modules/contrib/paragraphs/templates/paragraphs-info-icon.html.twig +++ /dev/null @@ -1,17 +0,0 @@ -{# -/** - * @file - * Default theme implementation for a paragraphs info icon. - * - * Available variables: - * - icon: Name of the icon to use. - * - message: Information message. - * - * @see template_preprocess() - * - * @ingroup themeable - */ -#} -<span class="paragraphs-icon paragraphs-icon-{{ icon }}" title="{{ message }}"> - <span class="paragraphs-icon__message visually-hidden">{{ message }}</span> -</span> diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/config/schema/paragraphs_test.schema.yml b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/config/schema/paragraphs_test.schema.yml deleted file mode 100644 index 9027218da..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/config/schema/paragraphs_test.schema.yml +++ /dev/null @@ -1,15 +0,0 @@ -paragraphs.behavior.settings.test_text_color: - type: paragraphs.behavior.settings_base - mapping: - default_color: - type: string - label: Default color -paragraphs.behavior.settings.test_field_selection: - type: paragraphs.behavior.settings_base - mapping: - field_selection: - type: string - label: Field names - field_selection_filter: - type: string - label: Filtered field names diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/css/bold-text.css b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/css/bold-text.css deleted file mode 100644 index df2f28b15..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/css/bold-text.css +++ /dev/null @@ -1,3 +0,0 @@ -.bold_plugin_text { - font-weight: 700; -} diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/css/color-text.css b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/css/color-text.css deleted file mode 100644 index e35e602b1..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/css/color-text.css +++ /dev/null @@ -1,6 +0,0 @@ -.blue_plugin_text { - color: blue; -} -.red_plugin_text { - color: red; -} diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.info.yml b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.info.yml deleted file mode 100644 index 39aaf1a7d..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.info.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Paragraphs test -type: module -description: Resources for Paragraphs tests -# core: 8.x -hidden: true -package: Paragraphs - -dependencies: - - paragraphs - -# Information added by Drupal.org packaging script on 2017-09-19 -version: '8.x-1.2' -core: '8.x' -project: 'paragraphs' -datestamp: 1505802547 diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.libraries.yml b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.libraries.yml deleted file mode 100644 index a2682f92c..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.libraries.yml +++ /dev/null @@ -1,9 +0,0 @@ -drupal.paragraphs_test.bold_text: - css: - theme: - css/bold-text.css: {} - -drupal.paragraphs_test.color_text: - css: - theme: - css/color-text.css: {} diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.module b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.module deleted file mode 100644 index cbe8c317e..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/paragraphs_test.module +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -/** - * @file - * Test module for testing the paragraphs module. - */ - -use Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget; - -/** - * Implements hook_paragraphs_widget_actions_alter(). - */ -function paragraphs_test_paragraphs_widget_actions_alter(&$widget_actions, &$context) { - if (\Drupal::state()->get('paragraphs_test_dropbutton')) { - $widget_actions['dropdown_actions']['test_button'] = ParagraphsWidget::expandButton([ - '#type' => 'submit', - '#value' => t('Add to library'), - '#delta' => 0, - '#name' => 'field_paragraphs_test', - '#weight' => 504, - '#paragraphs_mode' => 'remove', - '#access' => TRUE, - ]); - } -} diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestBoldTextBehavior.php b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestBoldTextBehavior.php deleted file mode 100644 index 5c5d65311..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestBoldTextBehavior.php +++ /dev/null @@ -1,66 +0,0 @@ -<?php - -namespace Drupal\paragraphs_test\Plugin\paragraphs\Behavior; - -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\paragraphs\ParagraphInterface; -use Drupal\paragraphs\ParagraphsBehaviorBase; - -/** - * Provides a test feature plugin. - * - * @ParagraphsBehavior( - * id = "test_bold_text", - * label = @Translation("Test bold text plugin"), - * description = @Translation("Test bold text plugin"), - * weight = 2 - * ) - */ -class TestBoldTextBehavior extends ParagraphsBehaviorBase { - - /** - * {@inheritdoc} - */ - public function buildBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state) { - $form['bold_text'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Bold Text'), - '#default_value' => $paragraph->getBehaviorSetting($this->getPluginId(), 'bold_text', FALSE), - '#description' => $this->t("Bold text for the paragraph."), - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function view(array &$build, Paragraph $paragraphs_entity, EntityViewDisplayInterface $display, $view_mode) { - if ($paragraphs_entity->getBehaviorSetting($this->getPluginId(), 'bold_text')) { - $build['#attributes']['class'][] = 'bold_plugin_text'; - $build['#attached']['library'][] = 'paragraphs_test/drupal.paragraphs_test.bold_text'; - } - } - - /** - * {@inheritdoc} - */ - public static function isApplicable(ParagraphsType $paragraphs_type) { - // If the name of the field is not text_paragraph_test then allow using this - // plugin. - if ($paragraphs_type->id() != 'text_paragraph_test') { - return TRUE; - } - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function settingsSummary(Paragraph $paragraph) { - $bold_setting = $paragraph->getBehaviorSetting($this->getPluginId(), 'bold_text'); - return [$bold_setting ? $this->t('Bold: Yes') : $this->t('Bold: No')]; - } -} diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestDummyBehavior.php b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestDummyBehavior.php deleted file mode 100644 index fbb36c5f1..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestDummyBehavior.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php - -namespace Drupal\paragraphs_test\Plugin\paragraphs\Behavior; - -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\ParagraphInterface; -use Drupal\paragraphs\ParagraphsBehaviorBase; - -/** - * Provides a test feature plugin. - * - * @ParagraphsBehavior( - * id = "test_dummy_behavior", - * label = @Translation("Test dummy plugin"), - * description = @Translation("Test dummy plugin"), - * weight = 2 - * ) - */ -class TestDummyBehavior extends ParagraphsBehaviorBase { - - /** - * {@inheritdoc} - */ - public function view(array &$build, Paragraph $paragraphs_entity, EntityViewDisplayInterface $display, $view_mode) { - $build['#attributes']['class'][] = 'dummy_plugin_text'; - } - - /** - * {@inheritdoc} - */ - public function buildBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state) { - // Used to test that returning NULL does not return an error. - return NULL; - } -} diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestFieldsSelectionBehavior.php b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestFieldsSelectionBehavior.php deleted file mode 100644 index 38deb7b7d..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestFieldsSelectionBehavior.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php - -namespace Drupal\paragraphs_test\Plugin\paragraphs\Behavior; -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\ParagraphsBehaviorBase; - -/** - * Test plugin with field selection. - * - * @ParagraphsBehavior( - * id = "test_field_selection", - * label = @Translation("Test field selection for behavior plugin"), - * description = @Translation("Test field selection for behavior plugin"), - * weight = 0 - * ) - */ -class TestFieldsSelectionBehavior extends ParagraphsBehaviorBase { - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form['field_selection_filter'] = [ - '#type' => 'select', - '#options' => $this->getFieldNameOptions($form_state->getFormObject()->getEntity(), 'image'), - '#title' => $this->t('Paragraph fields'), - '#default_value' => $this->configuration['field_selection_filter'], - '#description' => $this->t("Choose filtered paragraph field to be applied."), - ]; - - $form['field_selection'] = [ - '#type' => 'select', - '#options' => $this->getFieldNameOptions($form_state->getFormObject()->getEntity()), - '#title' => $this->t('Paragraph fields'), - '#default_value' => $this->configuration['field_selection'], - '#description' => $this->t("Choose paragraph field to be applied."), - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return [ - 'field_selection' => '', - 'field_selection_filter' => '', - ]; - } - - /** - * {@inheritdoc} - */ - public function view(array &$build, Paragraph $paragraphs_entity, EntityViewDisplayInterface $display, $view_mode) {} - -} diff --git a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestTextColorBehavior.php b/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestTextColorBehavior.php deleted file mode 100644 index 16f0bd3d5..000000000 --- a/web/modules/contrib/paragraphs/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestTextColorBehavior.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php - -namespace Drupal\paragraphs_test\Plugin\paragraphs\Behavior; - -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\ParagraphInterface; -use Drupal\paragraphs\ParagraphsBehaviorBase; - -/** - * Provides a test feature plugin. - * - * @ParagraphsBehavior( - * id = "test_text_color", - * label = @Translation("Test text color behavior plugin"), - * description = @Translation("Test text color behavior plugin"), - * weight = 1 - * ) - */ -class TestTextColorBehavior extends ParagraphsBehaviorBase { - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form['default_color'] = [ - '#type' => 'textfield', - '#title' => $this->t('Default Color'), - '#maxlength' => 255, - '#default_value' => $this->configuration['default_color'], - '#description' => $this->t("Text color for the paragraph."), - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { - if ($form_state->getValue('default_color') == 'red') { - $form_state->setErrorByName('default_color', $this->t('Red can not be used as the default color.')); - } - } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - $this->configuration['default_color'] = $form_state->getValue('default_color'); - } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return [ - 'default_color' => 'blue', - ]; - } - - /** - * {@inheritdoc} - */ - public function buildBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state) { - $form['text_color'] = [ - '#type' => 'textfield', - '#title' => $this->t('Color'), - '#maxlength' => 255, - '#default_value' => $paragraph->getBehaviorSetting($this->getPluginId(), 'text_color', $this->configuration['default_color']), - '#description' => $this->t("Text color for the paragraph."), - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateBehaviorForm(ParagraphInterface $paragraph, array &$form, FormStateInterface $form_state) { - if ($form_state->getValue('text_color') != 'blue' && $form_state->getValue('text_color') != 'red') { - $form_state->setError($form, 'The only allowed values are blue and red.'); - } - } - - /** - * {@inheritdoc} - */ - public function view(array &$build, Paragraph $paragraphs_entity, EntityViewDisplayInterface $display, $view_mode) { - if ($color = $paragraphs_entity->getBehaviorSetting($this->getPluginId(), 'text_color')) { - $build['#attributes']['class'][] = $color . '_plugin_text'; - $build['#attached']['library'][] = 'paragraphs_test/drupal.paragraphs_test.color_text'; - } - } - - /** - * {@inheritdoc} - */ - public function settingsSummary(Paragraph $paragraph) { - $text_color = $paragraph->getBehaviorSetting($this->pluginId, 'text_color'); - return [$this->t('Text color: @color', ['@color' => $text_color])]; - } -} diff --git a/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalBehaviorsTest.php b/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalBehaviorsTest.php deleted file mode 100644 index d0d3d513a..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalBehaviorsTest.php +++ /dev/null @@ -1,95 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Functional; - -use Drupal\paragraphs\Tests\Classic\ParagraphsCoreVersionUiTestTrait; -use Drupal\Tests\BrowserTestBase; -use Drupal\Tests\paragraphs\FunctionalJavascript\ParagraphsTestBaseTrait; - -/** - * Tests support for Paragraphs behavior plugins. - * - * @group paragraphs - */ -class ParagraphsExperimentalBehaviorsTest extends BrowserTestBase { - - use ParagraphsCoreVersionUiTestTrait; - use ParagraphsTestBaseTrait; - - /** - * Modules to enable. - * - * @var string[] - */ - public static $modules = [ - 'node', - 'paragraphs_test', - ]; - - /** - * Tests that behavior settings have empty leaves removed before being saved. - */ - public function testBehaviorPluginsSettingsFiltering() { - $this->addParagraphedContentType('paragraphed_test'); - - $admin = $this->drupalCreateUser([ - 'create paragraphed_test content', - 'edit any paragraphed_test content', - 'edit behavior plugin settings', - 'administer paragraphs types', - ]); - $this->drupalLogin($admin); - - // Add a text Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - $this->addFieldtoParagraphType($paragraph_type, 'field_text', 'text_long'); - - // Enable the "Test bold text plugin" to have a behavior form. - $this->drupalGet('/admin/structure/paragraphs_type/' . $paragraph_type); - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, 'Save'); - - // Add a note that uses the behavior plugin give it an empty setting. - $this->drupalGet('node/add/paragraphed_test'); - $edit = [ - 'title[0][value]' => 'Test Node', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Non-bold text', - 'field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]' => FALSE, - ]; - $this->drupalPostForm(NULL, $edit, 'Save'); - $bolded_elements = $this->getSession()->getPage()->findAll('css', '.bold_plugin_text'); - $this->assertFalse(count($bolded_elements), 'Test plugin did not add a CSS class.'); - - // Check that empty leaves are not saved in the behavior settings. - $node = $this->getNodeByTitle('Test Node', TRUE); - /** @var \Drupal\paragraphs\ParagraphInterface $paragraph */ - $paragraph = $node->get('field_paragraphs')->entity; - $behavior_settings = $paragraph->getBehaviorSetting('test_bold_text', []); - $expected_settings = []; - self::assertEquals($expected_settings, $behavior_settings); - - // Save a non-empty setting. - $this->drupalGet('node/' . $node->id() . '/edit'); - $edit = [ - 'field_paragraphs[0][subform][field_text][0][value]' => 'Bold text', - 'field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, 'Save'); - $bolded_elements = $this->getSession()->getPage()->findAll('css', '.bold_plugin_text'); - $this->assertTrue(count($bolded_elements), 'Test plugin added a CSS class.'); - - // Check that non-empty leaves are saved in the behavior settings. - $node = $this->getNodeByTitle('Test Node', TRUE); - /** @var \Drupal\paragraphs\ParagraphInterface $paragraph */ - $paragraph = $node->get('field_paragraphs')->entity; - $behavior_settings = $paragraph->getBehaviorSetting('test_bold_text', []); - $expected_settings = [ - 'bold_text' => 1, - ]; - self::assertEquals($expected_settings, $behavior_settings); - } - -} diff --git a/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalDragAndDropModeTest.php b/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalDragAndDropModeTest.php deleted file mode 100644 index 7e7d6ccb2..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalDragAndDropModeTest.php +++ /dev/null @@ -1,672 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Functional; - -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\node\Entity\Node; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\Tests\BrowserTestBase; -use Drupal\Tests\paragraphs\FunctionalJavascript\ParagraphsTestBaseTrait; - -/** - * Tests the drag and drop mode of paragraphs. - * - * @group paragraphs - */ -class ParagraphsExperimentalDragAndDropModeTest extends BrowserTestBase { - - use ParagraphsTestBaseTrait; - - /** - * Modules to be enabled. - */ - public static $modules = [ - 'node', - 'paragraphs', - 'field' - ]; - - /** - * {@inheritdoc} - */ - public function setUp() { - parent::setUp(); - $this->addParagraphedContentType('paragraphed_test', 'field_paragraphs'); - $this->addParagraphsType('paragraphs_container'); - $this->addParagraphsType('text'); - $this->addFieldtoParagraphType('text', 'field_text', 'text'); - - $this->addParagraphsField('paragraphs_container', 'paragraphs_container_paragraphs', 'paragraph'); - - // Make sure the paragraph fields use closed edit mode by default. - $component = [ - 'type' => 'paragraphs', - 'region' => 'content', - 'settings' => [ - 'edit_mode' => 'closed', - 'add_mode' => 'modal', - 'form_display_mode' => 'default', - ], - ]; - - EntityFormDisplay::load('paragraph.paragraphs_container.default') - ->setComponent('paragraphs_container_paragraphs', $component) - ->save(); - - EntityFormDisplay::load('node.paragraphed_test.default') - ->setComponent('field_paragraphs', $component) - ->save(); - - $admin = $this->drupalCreateUser([ - 'create paragraphed_test content', - 'edit any paragraphed_test content' - ]); - $this->drupalLogin($admin); - - // By default, paragraphs does not show the Drag & drop button if the - // library is not present. Override this for tests, as they don't need the - // JS. - \Drupal::state()->set('paragraphs_test_dragdrop_force_show', TRUE); - } - - /** - * Tests moving a paragraph from a container to top-level. - */ - public function testChangeParagraphParentWeight() { - // Create text paragraph. - $text_paragraph_1 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 1', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_1->save(); - - // Create a second text paragraph. - $text_paragraph_2 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 2.', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_2->save(); - - // Create container that contains the first two text paragraphs. - $paragraph = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$text_paragraph_1, $text_paragraph_2], - ]); - $paragraph->save(); - - // Add test content with paragraph container and the third text paragraph. - $node = Node::create([ - 'type' => 'paragraphed_test', - 'title' => 'Paragraphs Test', - 'field_paragraphs' => [$paragraph], - ]); - $node->save(); - - // Check that the parent of the second text paragraph is the paragraph - // container. - $text_paragraph_2 = Paragraph::load($text_paragraph_2->id()); - $this->assertEquals($text_paragraph_2->get('parent_id')->value, $paragraph->id()); - $this->assertEquals($text_paragraph_2->get('parent_type')->value, 'paragraph'); - - $this->drupalGet('/node/' . $node->id() . '/edit'); - $this->drupalPostForm(NULL, [], 'Drag & drop'); - - $assert_session = $this->assertSession(); - $assert_session->hiddenFieldValueEquals('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][0][_path]', 'field_paragraphs][0][paragraphs_container_paragraphs'); - $assert_session->hiddenFieldValueEquals('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][1][_path]', 'field_paragraphs][0][paragraphs_container_paragraphs'); - - // Change the path of the first text paragraph to the node as its parent. - // This also requires an update of the path of the second paragraph in the - // container as that moves down as well as the weight to prevent multiple - // identical weights. - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][0][_path]') - ->setValue('field_paragraphs'); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][1][_path]') - ->setValue('field_paragraphs][1][paragraphs_container_paragraphs'); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][_weight]') - ->setValue(1); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][1][_weight]') - ->setValue(0); - - $this->drupalPostForm(NULL, [], 'Complete drag & drop'); - $this->drupalPostForm(NULL, [], 'Save'); - - // Check the new structure of the node and its paragraphs. - \Drupal::entityTypeManager()->getStorage('node')->resetCache(); - $node = Node::load($node->id()); - $this->assertEquals(count($node->get('field_paragraphs')), 2); - - $this->assertEquals($node->get('field_paragraphs')->get(0)->target_id, $text_paragraph_1->id()); - $text_paragraph_1 = $node->get('field_paragraphs')->get(0)->entity; - $this->assertEquals($text_paragraph_1->get('parent_id')->value, $node->id()); - $this->assertEquals($text_paragraph_1->get('parent_type')->value, 'node'); - - $this->assertEquals($node->get('field_paragraphs')->get(1)->target_id, $paragraph->id()); - $paragraph = $node->get('field_paragraphs')->get(1)->entity; - $this->assertEquals($paragraph->get('parent_id')->value, $node->id()); - $this->assertEquals($paragraph->get('parent_type')->value, 'node'); - - $this->assertEquals(count($paragraph->get('paragraphs_container_paragraphs')), 1); - $this->assertEquals($paragraph->get('paragraphs_container_paragraphs')->target_id, $text_paragraph_2->id()); - - $text_paragraph_2 = $paragraph->get('paragraphs_container_paragraphs')->entity; - $this->assertEquals($text_paragraph_2->get('parent_id')->value, $paragraph->id()); - $this->assertEquals($text_paragraph_2->get('parent_type')->value, 'paragraph'); - - // If the library does not exist, test that the button is not visible - // without forcing it. This can not be tested if the library exists. - // @todo: Implement a library alter in a test module to do this? - $library_discovery = \Drupal::service('library.discovery'); - $library = $library_discovery->getLibraryByName('paragraphs', 'paragraphs-dragdrop'); - if (!$library) { - \Drupal::state()->set('paragraphs_test_dragdrop_force_show', FALSE); - $this->drupalGet('/node/' . $node->id() . '/edit'); - $this->assertSession()->buttonNotExists('Drag & drop'); - } - } - - /** - * Tests moving a paragraph from one container to another. - */ - public function testChangeParagraphContainerMove() { - // Create text paragraph. - $text_paragraph_1 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 1', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_1->save(); - - // Create container that contains the first two text paragraphs. - $paragraph = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$text_paragraph_1], - ]); - $paragraph->save(); - - // Create an empty container paragraph. - $paragraph_1 = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [], - ]); - $paragraph_1->save(); - - // Add test content with paragraph container and the third text paragraph. - $node = Node::create([ - 'type' => 'paragraphed_test', - 'title' => 'Paragraphs Test', - 'field_paragraphs' => [$paragraph, $paragraph_1], - ]); - $node->save(); - - // Change the path of the text paragraph to the empty container as its - // parent. - $this->drupalGet('/node/' . $node->id() . '/edit'); - $this->drupalPostForm(NULL, [], 'Drag & drop'); - - // Ensure that the summary is displayed correctly. - $this->assertSession()->elementTextContains('css', '.paragraphs-dragdrop-wrapper li:nth-of-type(1)', 'Test text 1'); - $this->assertSession()->elementTextNotContains('css', '.paragraphs-dragdrop-wrapper li:nth-of-type(2)', 'Test text 1'); - - $this->assertSession() - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][0][_path]') - ->setValue('field_paragraphs][1][paragraphs_container_paragraphs'); - $this->drupalPostForm(NULL, [], 'Complete drag & drop'); - - // Ensure the summary is displayed correctly for the collapsed paragraphs. - $this->assertSession()->elementTextNotContains('css', '.field--name-field-paragraphs tbody tr:nth-of-type(1) .paragraph-type-summary', 'Test text 1'); - $this->assertSession()->elementTextContains('css', '.field--name-field-paragraphs tbody tr:nth-of-type(2) .paragraph-type-summary', 'Test text 1'); - - // Ensure that the summary was updated correctly when going back to drag and - // drop mode. - $this->drupalPostForm(NULL, [], 'Drag & drop'); - $this->assertSession()->elementTextNotContains('css', '.paragraphs-dragdrop-wrapper li:nth-of-type(1)', 'Test text 1'); - $this->assertSession()->elementTextContains('css', '.paragraphs-dragdrop-wrapper li:nth-of-type(2)', 'Test text 1'); - $this->drupalPostForm(NULL, [], 'Complete drag & drop'); - - $this->drupalPostForm(NULL, [], 'Save'); - - // Check that the parent of the text paragraph is the second paragraph - // container. - \Drupal::entityTypeManager()->getStorage('node')->resetCache(); - $node = Node::load($node->id()); - $this->assertEquals(count($node->get('field_paragraphs')), 2); - - $this->assertEquals($node->get('field_paragraphs')->get(0)->target_id, $paragraph->id()); - $this->assertEquals($node->get('field_paragraphs')->get(1)->target_id, $paragraph_1->id()); - $paragraph = $node->get('field_paragraphs')->get(0)->entity; - - $this->assertEquals(count($paragraph->get('paragraphs_container_paragraphs')), 0); - - $paragraph_1 = $node->get('field_paragraphs')->get(1)->entity; - $this->assertEquals(count($paragraph_1->get('paragraphs_container_paragraphs')), 1); - $this->assertEquals($paragraph_1->get('paragraphs_container_paragraphs')->get(0)->target_id, $text_paragraph_1->id()); - - $text_paragraph_1 = $paragraph_1->get('paragraphs_container_paragraphs')->entity; - $this->assertEquals($text_paragraph_1->get('parent_id')->value, $paragraph_1->id()); - $this->assertEquals($text_paragraph_1->get('parent_type')->value, 'paragraph'); - } - - /** - * Tests drag and drop mode with multiple changes on the paragraphs. - */ - public function testMultipleChangesParagraphs() { - // Create text paragraph. - $text_paragraph_1 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 1', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_1->save(); - - // Create a second text paragraph. - $text_paragraph_2 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 2.', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_2->save(); - - // Create container that contains the first two text paragraphs. - $paragraph_1 = Paragraph::create([ - 'title' => 'Test Paragraph 1', - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$text_paragraph_1, $text_paragraph_2], - ]); - $paragraph_1->save(); - - // Create another text paragraph. - $text_paragraph_3 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 3.', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_3->save(); - - // Create a container that contains the third text paragraph. - $paragraph_2 = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$text_paragraph_3], - ]); - $paragraph_2->save(); - - // Create a container that contains the second paragraph. - $paragraph_3 = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$paragraph_2], - ]); - $paragraph_3->save(); - - // Create an empty container paragraph. - $paragraph_4 = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [], - ]); - $paragraph_4->save(); - - // Create a node with the structure of three nested paragraphs, first - // paragraph with two text paragraphs, second paragraph with a nested - // paragraph containing a text paragraph and the third empty paragraph. - $node = Node::create([ - 'type' => 'paragraphed_test', - 'title' => 'Paragraphs Test', - 'field_paragraphs' => [$paragraph_1, $paragraph_3, $paragraph_4], - ]); - $node->save(); - - // Edit the node. - $this->drupalGet('/node/' . $node->id() . '/edit'); - $this->getSession()->getPage()->findButton('field_paragraphs_2_edit')->press(); - $this->getSession()->getPage()->findButton('field_paragraphs_2_subform_paragraphs_container_paragraphs_text_add_more')->press(); - - $edit = [ - 'field_paragraphs[2][subform][paragraphs_container_paragraphs][0][subform][field_text][0][value]' => 'new paragraph', - ]; - $this->drupalPostForm(NULL, $edit, 'Drag & drop'); - - // Change the structure of the node, third text paragraph goes to first - // container, the first text paragraph goes to the second container (child - // of third container) and the third container goes to the fourth container. - // This also affects weights and paths of child and related paragraphs. - $assert_session = $this->assertSession(); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][1][dragdrop][paragraphs_container_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][0][_path]') - ->setValue('field_paragraphs][0][paragraphs_container_paragraphs'); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][0][_path]') - ->setValue('field_paragraphs][1][paragraphs_container_paragraphs][0][paragraphs_container_paragraphs][0][paragraphs_container_paragraphs'); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][1][dragdrop][paragraphs_container_paragraphs][list][0][_path]') - ->setValue('field_paragraphs][1][paragraphs_container_paragraphs][0][paragraphs_container_paragraphs'); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][1][_path]') - ->setValue('field_paragraphs][1][paragraphs_container_paragraphs'); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][2][dragdrop][paragraphs_container_paragraphs][list][0][_path]') - ->setValue('field_paragraphs][1][paragraphs_container_paragraphs'); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][1][_weight]') - ->setValue(0); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][2][dragdrop][paragraphs_container_paragraphs][list][0][_weight]') - ->setValue(1); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][2][_weight]') - ->setValue(1); - - // Save immediately, without separately confirming the widget changes. - $this->drupalPostForm(NULL, [], 'Save'); - - // Reset the cache to make sure that the loaded parents are the new ones. - \Drupal::entityTypeManager()->getStorage('paragraph')->resetCache(); - // Assert the new parents of the text paragraphs. - $text_paragraph_1 = Paragraph::load($text_paragraph_1->id()); - $this->assertEquals($text_paragraph_1->get('parent_id')->value, $paragraph_2->id()); - $this->assertEquals($text_paragraph_1->get('parent_type')->value, 'paragraph'); - - $text_paragraph_3 = Paragraph::load($text_paragraph_3->id()); - $this->assertEquals($text_paragraph_3->get('parent_id')->value, $paragraph_1->id()); - $this->assertEquals($text_paragraph_3->get('parent_type')->value, 'paragraph'); - - // Assert the new parent of the container. - $paragraph_3 =Paragraph::load($paragraph_3->id()); - $this->assertEquals($paragraph_3->get('parent_id')->value, $paragraph_4->id()); - $this->assertEquals($paragraph_3->get('parent_type')->value, 'paragraph'); - } - - /** - * Tests that a separate field is not affected by reordering one field. - */ - public function testChangeParagraphContainerMultipleFields() { - $this->addParagraphsField('paragraphed_test', 'field_paragraphs_second', 'node'); - - // Create text paragraph. - $text_paragraph_1 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 1', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_1->save(); - - // Create second text paragraph. - $text_paragraph_2 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 2', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_2->save(); - - // Create container that contains the first two text paragraphs. - $paragraph = Paragraph::create([ - 'title' => 'Test Paragraph', - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$text_paragraph_1], - ]); - $paragraph->save(); - - // Create an empty container paragraph. - $paragraph_1 = Paragraph::create([ - 'title' => 'Test Paragraph 1', - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [], - ]); - $paragraph_1->save(); - - // Create a container paragraph for the second field. - $paragraph_second = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$text_paragraph_2], - ]); - $paragraph_second->save(); - - // Add test content with paragraph container and the third text paragraph. - $node = Node::create([ - 'type' => 'paragraphed_test', - 'title' => 'Paragraphs Test', - 'field_paragraphs' => [$paragraph, $paragraph_1], - 'field_paragraphs_second' => [$paragraph_second], - ]); - $node->save(); - - // Change the path of the text paragraph to the empty container as its - // parent. - $this->drupalGet('/node/' . $node->id() . '/edit'); - $this->drupalPostForm(NULL, [], 'Drag & drop'); - - // Make sure that the second paragraph field is still displayed normally by - // checking that it displays the edit button, as it is closed by default. - // @todo: Introduce a global drag and drop mode? - $this->assertSession()->buttonExists('field_paragraphs_second_0_subform_paragraphs_container_paragraphs_0_edit'); - - // Change the path of the text paragraph to the empty container as its - // parent. - $this->assertSession() - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][0][_path]') - ->setValue('field_paragraphs][1][paragraphs_container_paragraphs'); - $this->drupalPostForm(NULL, [], 'Save'); - - // Check that the parent of the text paragraph is the second paragraph - // container. - \Drupal::entityTypeManager()->getStorage('node')->resetCache(); - $node = Node::load($node->id()); - $this->assertEquals(count($node->get('field_paragraphs')), 2); - - $this->assertEquals($node->get('field_paragraphs')->get(0)->target_id, $paragraph->id()); - $this->assertEquals($node->get('field_paragraphs')->get(1)->target_id, $paragraph_1->id()); - $paragraph = $node->get('field_paragraphs')->get(0)->entity; - - $this->assertEquals(count($paragraph->get('paragraphs_container_paragraphs')), 0); - - $paragraph_1 = $node->get('field_paragraphs')->get(1)->entity; - $this->assertEquals(count($paragraph_1->get('paragraphs_container_paragraphs')), 1); - $this->assertEquals($paragraph_1->get('paragraphs_container_paragraphs')->get(0)->target_id, $text_paragraph_1->id()); - - $text_paragraph_1 = $paragraph_1->get('paragraphs_container_paragraphs')->entity; - $this->assertEquals($text_paragraph_1->get('parent_id')->value, $paragraph_1->id()); - $this->assertEquals($text_paragraph_1->get('parent_type')->value, 'paragraph'); - - // Assert the second field. - $this->assertEquals(count($node->get('field_paragraphs_second')), 1); - - $this->assertEquals($node->get('field_paragraphs_second')->get(0)->target_id, $paragraph_second->id()); - $paragraph_second = $node->get('field_paragraphs_second')->get(0)->entity; - - $this->assertEquals(count($paragraph_second->get('paragraphs_container_paragraphs')), 1); - $this->assertEquals($paragraph_second->get('paragraphs_container_paragraphs')->get(0)->target_id, $text_paragraph_2->id()); - } - - /** - * Tests moving a paragraph and after that enable the drag and drop mode. - */ - public function testChangeParagraphMoveBeforeReorder() { - // Create text paragraph. - $text_paragraph_1 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 1', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_1->save(); - - // Create text paragraph. - $text_paragraph_2 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 2', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_2->save(); - - // Create container that contains the first text paragraphs. - $paragraph = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$text_paragraph_1], - ]); - $paragraph->save(); - - // Create an empty container paragraph. - $paragraph_1 = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [], - ]); - $paragraph_1->save(); - - // Create the node with two containers and the second text in the middle. - $node = Node::create([ - 'type' => 'paragraphed_test', - 'title' => 'Paragraphs Test', - 'field_paragraphs' => [$paragraph, $text_paragraph_2, $paragraph_1], - ]); - $node->save(); - - $this->drupalGet('/node/' . $node->id() . '/edit'); - - // Move the second text below the container. - $edit = [ - 'field_paragraphs[1][_weight]' => 2, - 'field_paragraphs[2][_weight]' => 1, - ]; - $this->drupalPostForm(NULL, $edit, 'Drag & drop'); - - // Change the path of the text paragraph to the empty container as its - // parent. - $this->assertSession() - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][dragdrop][paragraphs_container_paragraphs][list][0][_path]') - ->setValue('field_paragraphs][1][paragraphs_container_paragraphs'); - $this->drupalPostForm(NULL, [], 'Complete drag & drop'); - - $this->drupalPostForm(NULL, [], 'Save'); - - // Check that the parent of the text paragraph is the second paragraph - // container. - \Drupal::entityTypeManager()->getStorage('node')->resetCache(); - $node = Node::load($node->id()); - $this->assertEquals(count($node->get('field_paragraphs')), 3); - - $this->assertEquals($node->get('field_paragraphs')->get(0)->target_id, $paragraph->id()); - $this->assertEquals($node->get('field_paragraphs')->get(1)->target_id, $paragraph_1->id()); - $this->assertEquals($node->get('field_paragraphs')->get(2)->target_id, $text_paragraph_2->id()); - $paragraph = $node->get('field_paragraphs')->get(0)->entity; - - $this->assertEquals(count($paragraph->get('paragraphs_container_paragraphs')), 0); - - $paragraph_1 = $node->get('field_paragraphs')->get(1)->entity; - $this->assertEquals(count($paragraph_1->get('paragraphs_container_paragraphs')), 1); - $this->assertEquals($paragraph_1->get('paragraphs_container_paragraphs')->get(0)->target_id, $text_paragraph_1->id()); - - $text_paragraph_1 = $paragraph_1->get('paragraphs_container_paragraphs')->entity; - $this->assertEquals($text_paragraph_1->get('parent_id')->value, $paragraph_1->id()); - $this->assertEquals($text_paragraph_1->get('parent_type')->value, 'paragraph'); - } - - /** - * Tests deleting a paragraph and after that enable the drag and drop mode. - */ - public function testChangeParagraphMoveAfterDelete() { - // Create text paragraph. - $text_paragraph_1 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 1', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_1->save(); - - // Create text paragraph. - $text_paragraph_2 = Paragraph::create([ - 'type' => 'text', - 'field_text' => [ - 'value' => 'Test text 2', - 'format' => 'plain_text', - ], - ]); - $text_paragraph_2->save(); - - // Create container that contains the first text paragraphs. - $paragraph = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [$text_paragraph_1], - ]); - $paragraph->save(); - - // Create an empty container paragraph. - $paragraph_1 = Paragraph::create([ - 'type' => 'paragraphs_container', - 'paragraphs_container_paragraphs' => [], - ]); - $paragraph_1->save(); - - // Create the node with two containers and the second text in the middle. - $node = Node::create([ - 'type' => 'paragraphed_test', - 'title' => 'Paragraphs Test', - 'field_paragraphs' => [$paragraph, $text_paragraph_2, $paragraph_1], - ]); - $node->save(); - - $this->drupalGet('/node/' . $node->id() . '/edit'); - - // Delete the first container, move the text 2 paragraph into the second - // container. - $this->getSession()->getPage()->pressButton('field_paragraphs_0_remove'); - $this->drupalPostForm(NULL, [], 'Drag & drop'); - - $assert_session = $this->assertSession(); - - $assert_session->pageTextNotContains('Test text 1'); - $assert_session->pageTextContains('Test text 2'); - - // Change the path of the text 2 paragraph to the empty container as its - // parent. - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][0][_path]') - ->setValue('field_paragraphs][0][paragraphs_container_paragraphs'); - $assert_session - ->hiddenFieldExists('field_paragraphs[dragdrop][field_paragraphs][list][1][_weight]') - ->setValue(0); - - $this->drupalPostForm(NULL, [], 'Complete drag & drop'); - $this->drupalPostForm(NULL, [], 'Save'); - - // Check that the parent of the text paragraph is the second paragraph - // container. - \Drupal::entityTypeManager()->getStorage('node')->resetCache(); - $node = Node::load($node->id()); - $this->assertEquals(count($node->get('field_paragraphs')), 1); - - $this->assertEquals($node->get('field_paragraphs')->get(0)->target_id, $paragraph_1->id()); - $paragraph_1 = $node->get('field_paragraphs')->get(0)->entity; - $this->assertEquals(count($paragraph_1->get('paragraphs_container_paragraphs')), 1); - $this->assertEquals($paragraph_1->get('paragraphs_container_paragraphs')->get(0)->target_id, $text_paragraph_2->id()); - - $text_paragraph_2 = $paragraph_1->get('paragraphs_container_paragraphs')->entity; - $this->assertEquals($text_paragraph_2->get('parent_id')->value, $paragraph_1->id()); - $this->assertEquals($text_paragraph_2->get('parent_type')->value, 'paragraph'); - } - -} diff --git a/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalWidgetButtonsTest.php b/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalWidgetButtonsTest.php deleted file mode 100644 index c89217324..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Functional/ParagraphsExperimentalWidgetButtonsTest.php +++ /dev/null @@ -1,238 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Functional; - -use Drupal\paragraphs\Tests\Classic\ParagraphsCoreVersionUiTestTrait; -use Drupal\Tests\BrowserTestBase; -use Drupal\Tests\paragraphs\FunctionalJavascript\LoginAdminTrait; -use Drupal\Tests\paragraphs\FunctionalJavascript\ParagraphsTestBaseTrait; - -/** - * Tests paragraphs experimental widget buttons. - * - * @group paragraphs - */ -class ParagraphsExperimentalWidgetButtonsTest extends BrowserTestBase { - - use LoginAdminTrait; - use ParagraphsCoreVersionUiTestTrait; - use ParagraphsTestBaseTrait; - - /** - * Modules to enable. - * - * @var string[] - */ - public static $modules = [ - 'paragraphs_test', - 'node', - 'paragraphs', - 'field', - 'field_ui', - 'block', - ]; - - /** - * Tests the autocollapse functionality. - */ - public function testAutocollapse() { - $this->addParagraphedContentType('paragraphed_test'); - - $permissions = [ - 'administer content types', - 'administer node fields', - 'administer paragraphs types', - 'administer node form display', - 'administer paragraph fields', - 'administer paragraph form display', - 'create paragraphed_test content', - 'edit any paragraphed_test content', - ]; - $this->loginAsAdmin($permissions, TRUE); - - // Add a text Paragraph type. - $this->addParagraphsType('text_paragraph'); - $this->addFieldtoParagraphType('text_paragraph', 'field_text', 'text_long'); - - // Add another Paragraph type so that there is no default Paragraphs type. - $this->addParagraphsType('another_paragraph'); - - // Check that the paragraphs field uses the experimental widget. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/form-display'); - $option = $this->assertSession()->optionExists('fields[field_paragraphs][type]', 'paragraphs'); - $this->assertTrue($option->isSelected()); - // Check that the autocollapse is disabled by default. - $this->assertSession()->pageTextContains('Autocollapse: None'); - - // Create a new node with 2 paragraphs. - $this->drupalGet('node/add/paragraphed_test'); - $this->getSession()->getPage()->findButton('field_paragraphs_text_paragraph_add_more')->press(); - $this->getSession()->getPage()->findButton('field_paragraphs_text_paragraph_add_more')->press(); - $edit = [ - 'title[0][value]' => 'Autocollapse test node', - 'field_paragraphs[0][subform][field_text][0][value]' => 'Fist paragraph', - 'field_paragraphs[1][subform][field_text][0][value]' => 'Second paragraph', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle('Autocollapse test node'); - - // Set the settings to "Open" edit mode without autocollapse. - $settings = [ - 'edit_mode' => 'open', - 'closed_mode' => 'summary', - 'autocollapse' => 'none', - ]; - $this->setParagraphsWidgetSettings('paragraphed_test', 'field_paragraphs', $settings); - - // Edit the node. Edit mode is "Open". All paragraphs are in the "Edit" - // mode. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // Autocollapse is disabled. Closing and opening a paragraphs does not - // affect the other one. - $this->getSession()->getPage()->findButton('field_paragraphs_0_collapse')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - $this->getSession()->getPage()->findButton('field_paragraphs_0_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // "Collapse all" enables autocollapse. - $this->getSession()->getPage()->findButton('field_paragraphs_collapse_all')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'closed'); - - // Open the first paragraph and then the second. Opening the second closes - // the first. - $this->getSession()->getPage()->findButton('field_paragraphs_0_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'closed'); - - $this->getSession()->getPage()->findButton('field_paragraphs_1_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // "Edit all" disables autocollapse. - $this->getSession()->getPage()->findButton('field_paragraphs_edit_all')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // Closing and opening a paragraphs does not affect the other one anymore. - $this->getSession()->getPage()->findButton('field_paragraphs_0_collapse')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - $this->getSession()->getPage()->findButton('field_paragraphs_0_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // Enable autocollapse. Set edit mode to "Closed". - $settings = [ - 'edit_mode' => 'closed', - 'closed_mode' => 'summary', - 'autocollapse' => 'all', - ]; - $this->setParagraphsWidgetSettings('paragraphed_test', 'field_paragraphs', $settings); - - // Edit the node. All paragraphs are closed. - $this->drupalGet('node/' . $node->id() . '/edit'); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'closed'); - - // Open the first paragraph and then the second. Opening the second closes - // the first. - $this->getSession()->getPage()->findButton('field_paragraphs_0_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'closed'); - - $this->getSession()->getPage()->findButton('field_paragraphs_1_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // "Edit all" disables auto collapse. - $this->getSession()->getPage()->findButton('field_paragraphs_edit_all')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // Closing and opening a paragraphs does not affect the other one anymore. - $this->getSession()->getPage()->findButton('field_paragraphs_0_collapse')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - $this->getSession()->getPage()->findButton('field_paragraphs_0_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // "Collapse all" re-enables autocollapse. - $this->getSession()->getPage()->findButton('field_paragraphs_collapse_all')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'closed'); - - // Open the first paragraph and then the second. Opening the second closes - // the first. - $this->getSession()->getPage()->findButton('field_paragraphs_0_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'edit'); - $this->checkParagraphInMode('field_paragraphs_1', 'closed'); - - $this->getSession()->getPage()->findButton('field_paragraphs_1_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'edit'); - - // Check that adding a new paragraphs closes the others. - $this->getSession()->getPage()->findButton('field_paragraphs_text_paragraph_add_more')->press(); - $this->getSession()->getPage()->fillField('field_paragraphs[2][subform][field_text][0][value]', 'Third paragraph'); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'closed'); - $this->checkParagraphInMode('field_paragraphs_2', 'edit'); - - // Check that duplicating closes the other paragraphs. - $this->getSession()->getPage()->findButton('field_paragraphs_2_duplicate')->press(); - $this->getSession()->getPage()->fillField('field_paragraphs[3][subform][field_text][0][value]', 'Fourth paragraph'); - $this->checkParagraphInMode('field_paragraphs_0', 'closed'); - $this->checkParagraphInMode('field_paragraphs_1', 'closed'); - $this->checkParagraphInMode('field_paragraphs_2', 'closed'); - $this->checkParagraphInMode('field_paragraphs_3', 'edit'); - - // Check that autocollapse does not restore removed paragraphs. - $this->getSession()->getPage()->findButton('field_paragraphs_3_remove')->press(); - $this->checkParagraphInMode('field_paragraphs_3', 'removed'); - $this->getSession()->getPage()->findButton('field_paragraphs_2_edit')->press(); - $this->checkParagraphInMode('field_paragraphs_3', 'removed'); - } - - /** - * Asserts that a paragraph is in a particular mode. - * - * It does this indirectly by checking checking what buttons are available. - * - * @param string $button_prefix - * An initial part of the button name; namely "<paragraphs_field>_<delta>". - * - * @param string $mode - * Assert that the paragraphs is in this widget item mode. Supported modes - * are "edit", "closed" and "removed". A paragraph in the "removed" mode - * cannot be distinguished from one that has never been added. - */ - public function checkParagraphInMode($button_prefix, $mode) { - switch ($mode) { - case 'edit': - $this->assertSession()->buttonNotExists($button_prefix . '_edit'); - $this->assertSession()->buttonExists($button_prefix . '_collapse'); - break; - case 'closed': - $this->assertSession()->buttonExists($button_prefix . '_edit'); - $this->assertSession()->buttonNotExists($button_prefix . '_collapse'); - break; - case 'removed': - $this->assertSession()->buttonNotExists($button_prefix . '_edit'); - $this->assertSession()->buttonNotExists($button_prefix . '_collapse'); - break; - default: - throw new \InvalidArgumentException('This function does not support "' . $mode . '" as an argument for "$mode" parameter'); - } - } - -} diff --git a/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/LoginAdminTrait.php b/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/LoginAdminTrait.php deleted file mode 100644 index baf303201..000000000 --- a/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/LoginAdminTrait.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\FunctionalJavascript; - -/** - * Test trait for logging admin in JS tests. - */ -trait LoginAdminTrait { - - /** - * Creates an user with admin permissions and log in. - * - * @param array $additional_permissions - * Additional permissions that will be granted to admin user. - * @param bool $reset_permissions - * Flag to determine if default admin permissions will be replaced by - * $additional_permissions. - * - * @return object - * Newly created and logged in user object. - */ - public function loginAsAdmin($additional_permissions = [], $reset_permissions = FALSE) { - - $permissions = [ - 'administer content types', - 'administer node fields', - 'administer paragraphs types', - 'administer node form display', - 'administer paragraph fields', - 'administer paragraph form display', - ]; - - if ($reset_permissions) { - $permissions = $additional_permissions; - } - elseif (!empty($additional_permissions)) { - $permissions = array_merge($permissions, $additional_permissions); - } - - $this->admin_user = $this->drupalCreateUser($permissions); - $this->drupalLogin($this->admin_user); - return $this->admin_user; - } -} diff --git a/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsExperimentalAddWidgetTest.php b/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsExperimentalAddWidgetTest.php deleted file mode 100644 index 91bc7ad12..000000000 --- a/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsExperimentalAddWidgetTest.php +++ /dev/null @@ -1,176 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\FunctionalJavascript; - -use Drupal\Component\Utility\Unicode; -use Drupal\Core\Entity\Display\EntityFormDisplayInterface; -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Entity\Entity\EntityViewDisplay; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\FunctionalJavascriptTests\JavascriptTestBase; -use Drupal\node\Entity\NodeType; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\paragraphs\Tests\Classic\ParagraphsCoreVersionUiTestTrait; -use Drupal\Tests\paragraphs\FunctionalJavascript\LoginAdminTrait; - -/** - * Test paragraphs user interface. - * - * @group paragraphs - */ -class ParagraphsExperimentalAddWidgetTest extends JavascriptTestBase { - - use LoginAdminTrait; - use FieldUiTestTrait; - use ParagraphsTestBaseTrait; - use ParagraphsCoreVersionUiTestTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'node', - 'paragraphs_test', - 'paragraphs', - 'field', - 'field_ui', - 'block', - 'link', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() - { - parent::setUp(); - // Place the breadcrumb, tested in fieldUIAddNewField(). - $this->drupalPlaceBlock('system_breadcrumb_block'); - $this->drupalPlaceBlock('local_tasks_block'); - $this->drupalPlaceBlock('local_actions_block'); - $this->drupalPlaceBlock('page_title_block'); - - } - - /** - * Tests the add widget button with modal form. - */ - public function testAddWidgetButton() { - $this->addParagraphedContentType('paragraphed_test'); - $this->loginAsAdmin([ - 'administer content types', - 'administer node form display', - 'edit any paragraphed_test content', - 'create paragraphed_test content', - ]); - // Set the add mode on the content type to modal form widget. - $this->drupalGet('admin/structure/types/manage/paragraphed_test/form-display'); - $page = $this->getSession()->getPage(); - $page->pressButton('field_paragraphs_settings_edit'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $edit = [ - 'fields[field_paragraphs][settings_edit_form][settings][edit_mode]' => 'closed', - 'fields[field_paragraphs][settings_edit_form][settings][add_mode]' => 'modal' - ]; - $this->drupalPostForm(NULL, $edit, 'Update'); - $this->assertSession()->assertWaitOnAjaxRequest(); - - $this->drupalPostForm(NULL, [], t('Save')); - - // Add a Paragraph type. - $paragraph_type = 'text_paragraph'; - $this->addParagraphsType($paragraph_type); - $this->addParagraphsType('text'); - - // Add icons to the paragraphs types. - $icon_one = $this->addParagraphsTypeIcon($paragraph_type); - $icon_two = $this->addParagraphsTypeIcon('text'); - - // Add a text field to the text_paragraph type. - static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []); - - // Create paragraph type Nested test. - $this->addParagraphsType('nested_test'); - - static::fieldUIAddNewField('admin/structure/paragraphs_type/nested_test', 'paragraphs', 'Paragraphs', 'entity_reference_revisions', [ - 'settings[target_type]' => 'paragraph', - 'cardinality' => '-1', - ], []); - - // Set the settings for the field in the nested paragraph. - $component = [ - 'type' => 'paragraphs', - 'region' => 'content', - 'settings' => [ - 'edit_mode' => 'closed', - 'add_mode' => 'modal', - 'form_display_mode' => 'default', - ], - ]; - EntityFormDisplay::load('paragraph.nested_test.default')->setComponent('field_paragraphs', $component)->save(); - - // Add a paragraphed test. - $this->drupalGet('node/add/paragraphed_test'); - - // Add a nested paragraph with the add widget. - $page->pressButton('Add Paragraph'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->assertSession()->elementTextContains('css', '.ui-dialog-title', 'Add Paragraph'); - $page->pressButton('nested_test'); - $this->assertSession()->assertWaitOnAjaxRequest(); - - // Verify that the paragraphs type icons are being displayed. - $button_one = $this->assertSession()->buttonExists($paragraph_type); - $button_two = $this->assertSession()->buttonExists('text'); - $this->assertContains($icon_one->getFilename(), $button_one->getAttribute('style')); - $this->assertContains($icon_two->getFilename(), $button_two->getAttribute('style')); - - // Find the add button in the nested paragraph with xpath. - $element = $this->xpath('//div[contains(@class, "form-wrapper")]/div[contains(@class, "paragraph-type-add-modal")]/input'); - $element[0]->click(); - $this->assertSession()->assertWaitOnAjaxRequest(); - - // Add a text inside the nested paragraph. - $page = $this->getSession()->getPage(); - $dialog = $page->find('xpath', '//div[contains(@class, "ui-dialog")]'); - $dialog->pressButton('text'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $edit = [ - 'title[0][value]' => 'Example title', - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - - - // Check the created paragraphed test. - $this->assertText('paragraphed_test Example title has been created.'); - $this->assertRaw('paragraph--type--nested-test'); - $this->assertRaw('paragraph--type--text'); - - // Add a paragraphs field with another paragraphs widget title to the - // paragraphed_test content type. - $this->addParagraphsField('paragraphed_test', 'field_paragraphs_two', 'node'); - $settings = [ - 'title' => 'Renamed paragraph', - 'title_plural' => 'Renamed paragraphs', - 'add_mode' => 'modal', - ]; - $this->setParagraphsWidgetSettings('paragraphed_test', 'field_paragraphs_two', $settings); - - // Check that the "add" buttons and modal form windows are labeled - // correctly. - $this->drupalGet('node/add/paragraphed_test'); - $page->pressButton('Add Paragraph'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->assertSession()->elementTextContains('css', '.ui-dialog-title', 'Add Paragraph'); - $this->assertSession()->elementTextNotContains('css', '.ui-dialog-title', 'Add Renamed paragraph'); - $this->assertSession()->elementExists('css', '.ui-dialog-titlebar-close')->press(); - $page->pressButton('Add Renamed paragraph'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->assertSession()->elementTextContains('css', '.ui-dialog-title', 'Add Renamed paragraph'); - $this->assertSession()->elementTextNotContains('css', '.ui-dialog-title', 'Add Paragraph'); - } -} diff --git a/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsExperimentalEditPerspectivesUiTest.php b/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsExperimentalEditPerspectivesUiTest.php deleted file mode 100644 index adfdb8155..000000000 --- a/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsExperimentalEditPerspectivesUiTest.php +++ /dev/null @@ -1,207 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\FunctionalJavascript; - -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\FunctionalJavascriptTests\JavascriptTestBase; - -/** - * Test paragraphs user interface. - * - * @group paragraphs - */ -class ParagraphsExperimentalEditPerspectivesUiTest extends JavascriptTestBase { - - use LoginAdminTrait; - use ParagraphsTestBaseTrait; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'node', - 'paragraphs_test', - 'paragraphs', - 'field', - 'field_ui', - 'block', - 'link', - 'text', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - } - - /** - * Test paragraphs user interface. - */ - public function testEditPerspectives() { - - $this->loginAsAdmin([ - 'access content overview', - 'edit behavior plugin settings' - ]); - - $page = $this->getSession()->getPage(); - $this->drupalGet('admin/structure/paragraphs_type/add'); - $edit = [ - 'label' => 'TestPlugin', - 'id' => 'testplugin', - 'behavior_plugins[test_text_color][enabled]' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save and manage fields')); - $this->drupalGet('admin/structure/types/add'); - $edit = [ - 'name' => 'TestContent', - 'type' => 'testcontent', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and manage fields')); - $this->drupalGet('admin/structure/types/manage/testcontent/fields/add-field'); - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference_revisions:paragraph', - 'label' => 'testparagraphfield', - 'field_name' => 'testparagraphfield', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and continue')); - $edit = [ - 'settings[target_type]' => 'paragraph', - ]; - $this->drupalPostForm(NULL, $edit, t('Save field settings')); - $edit = [ - 'settings[handler_settings][target_bundles_drag_drop][testplugin][enabled]' => TRUE, - ]; - $this->drupalPostForm(NULL, $edit, t('Save settings')); - $this->drupalGet('admin/structure/types/manage/testcontent/form-display'); - $page->selectFieldOption('fields[field_testparagraphfield][type]', 'paragraphs'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->drupalPostForm(NULL, [], t('Save')); - $this->drupalGet('node/add/testcontent'); - $this->clickLink('Behavior'); - $style_selector = $page->find('css', '.form-item-field-testparagraphfield-0-behavior-plugins-test-text-color-text-color'); - $this->assertTrue($style_selector->isVisible()); - $this->clickLink('Content'); - $this->assertFalse($style_selector->isVisible()); - } - - /** - * Test if tabs are visible with no behavior elements. - */ - public function testTabsVisibility() { - $this->loginAsAdmin([ - 'access content overview', - ]); - - $page = $this->getSession()->getPage(); - $this->drupalGet('admin/structure/paragraphs_type/add'); - $edit = [ - 'label' => 'TestPlugin', - 'id' => 'testplugin', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and manage fields')); - $this->drupalGet('admin/structure/types/add'); - $edit = [ - 'name' => 'TestContent', - 'type' => 'testcontent', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and manage fields')); - $this->drupalGet('admin/structure/types/manage/testcontent/fields/add-field'); - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference_revisions:paragraph', - 'label' => 'testparagraphfield', - 'field_name' => 'testparagraphfield', - ]; - $this->drupalPostForm(NULL, $edit, t('Save and continue')); - $edit = [ - 'settings[target_type]' => 'paragraph', - ]; - $this->drupalPostForm(NULL, $edit, t('Save field settings')); - $this->drupalPostForm(NULL, NULL, t('Save settings')); - $this->drupalGet('admin/structure/types/manage/testcontent/form-display'); - $page->selectFieldOption('fields[field_testparagraphfield][type]', 'paragraphs'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->drupalPostForm(NULL, [], t('Save')); - $this->drupalGet('node/add/testcontent'); - $style_selector = $page->find('css', '.paragraphs-tabs'); - $this->assertFalse($style_selector->isVisible()); - } - - /** - * Test edit perspectives works fine with multiple fields. - */ - public function testPerspectivesWithMultipleFields() { - $this->loginAsAdmin([ - 'edit behavior plugin settings' - ]); - - // Add a nested Paragraph type. - $paragraph_type = 'nested_paragraph'; - $this->addParagraphsType($paragraph_type); - $this->addParagraphsField('nested_paragraph', 'paragraphs', 'paragraph'); - $edit = [ - 'behavior_plugins[test_bold_text][enabled]' => TRUE, - ]; - $this->drupalPostForm('admin/structure/paragraphs_type/' . $paragraph_type, $edit, t('Save')); - - $this->addParagraphedContentType('testcontent'); - $this->addParagraphsField('testcontent', 'field_paragraphs2', 'node'); - - // Disable the default paragraph on both the node and the nested paragraph - // to explicitly test with no paragraph and avoid a loop. - EntityFormDisplay::load('node.testcontent.default') - ->setComponent('field_paragraphs', ['type' => 'paragraphs', 'settings' => ['default_paragraph_type' => '_none']]) - ->setComponent('field_paragraphs2', ['type' => 'paragraphs', 'settings' => ['default_paragraph_type' => '_none']]) - ->save(); - EntityFormDisplay::load('paragraph' . '.' . $paragraph_type . '.default') - ->setComponent('paragraphs', ['type' => 'paragraphs', 'settings' => ['default_paragraph_type' => '_none']]) - ->save(); - - $assert_session = $this->assertSession(); - - $this->drupalGet('node/add/testcontent'); - $assert_session->elementNotExists('css', '.paragraphs-nested'); - - // Add a nested paragraph to the first field. - $button = $this->getSession()->getPage()->findButton('Add nested_paragraph'); - $button->press(); - - $assert_session->assertWaitOnAjaxRequest(); - $assert_session->elementExists('css', '.paragraphs-nested'); - - // Add a paragraph to the second field. - $region_field2 = $this->getSession()->getPage()->find('css', '.field--name-field-paragraphs2'); - $button_field2 = $region_field2->findButton('Add nested_paragraph'); - $button_field2->press(); - $assert_session->assertWaitOnAjaxRequest(); - - // Ge the style checkboxes from each field, make sure they are not visible - // by default. - $page = $this->getSession()->getPage(); - $style_selector = $page->findField('field_paragraphs[0][behavior_plugins][test_bold_text][bold_text]'); - $this->assertFalse($style_selector->isVisible()); - $style_selector2 = $page->findField('field_paragraphs2[0][behavior_plugins][test_bold_text][bold_text]'); - $this->assertFalse($style_selector2->isVisible()); - - // Switch to Behavior on the first field, then the second, make sure - // the visibility of the checkboxes is correct after each change. - $this->clickLink('Behavior', 0); - $this->assertTrue($style_selector->isVisible()); - $this->assertFalse($style_selector2->isVisible()); - $this->clickLink('Behavior', 1); - $this->assertTrue($style_selector->isVisible()); - $this->assertTrue($style_selector2->isVisible()); - - // Switch the second field back to Content, verify visibility again. - $this->clickLink('Content', 1); - $this->assertTrue($style_selector->isVisible()); - $this->assertFalse($style_selector2->isVisible()); - } - -} diff --git a/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsTestBaseTrait.php b/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsTestBaseTrait.php deleted file mode 100644 index 8b2faee5a..000000000 --- a/web/modules/contrib/paragraphs/tests/src/FunctionalJavascript/ParagraphsTestBaseTrait.php +++ /dev/null @@ -1,224 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\FunctionalJavascript; - -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Entity\Entity\EntityViewDisplay; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\file\Entity\File; -use Drupal\node\Entity\NodeType; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\Tests\TestFileCreationTrait; - -/** - * Test trait for Paragraphs JS tests. - */ -trait ParagraphsTestBaseTrait { - - use TestFileCreationTrait; - - /** - * Adds a content type with a Paragraphs field. - * - * @param string $content_type_name - * Content type name to be used. - * @param string $paragraphs_field_name - * (optional) Field name to be used. Defaults to 'field_paragraphs'. - * @param string $widget_type - * (optional) Declares if we use experimental or classic widget. - * Defaults to 'paragraphs' for experimental widget. - * Use 'entity_reference_paragraphs' for classic widget. - */ - protected function addParagraphedContentType($content_type_name, $paragraphs_field_name = 'field_paragraphs', $widget_type = 'paragraphs') { - // Create the content type. - $node_type = NodeType::create([ - 'type' => $content_type_name, - 'name' => $content_type_name, - ]); - $node_type->save(); - - $this->addParagraphsField($content_type_name, $paragraphs_field_name, 'node', $widget_type); - } - - /** - * Adds a Paragraphs field to a given entity type. - * - * @param string $entity_type_name - * Entity type name to be used. - * @param string $paragraphs_field_name - * Paragraphs field name to be used. - * @param string $entity_type - * Entity type where to add the field. - * @param string $widget_type - * (optional) Declares if we use experimental or classic widget. - * Defaults to 'paragraphs' for experimental widget. - * Use 'entity_reference_paragraphs' for classic widget. - */ - protected function addParagraphsField($entity_type_name, $paragraphs_field_name, $entity_type, $widget_type = 'paragraphs') { - $field_storage = FieldStorageConfig::loadByName($entity_type, $paragraphs_field_name); - if (!$field_storage) { - // Add a paragraphs field. - $field_storage = FieldStorageConfig::create([ - 'field_name' => $paragraphs_field_name, - 'entity_type' => $entity_type, - 'type' => 'entity_reference_revisions', - 'cardinality' => '-1', - 'settings' => [ - 'target_type' => 'paragraph', - ], - ]); - $field_storage->save(); - } - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => $entity_type_name, - 'settings' => [ - 'handler' => 'default:paragraph', - 'handler_settings' => ['target_bundles' => NULL], - ], - ]); - $field->save(); - - $form_display = entity_get_form_display($entity_type, $entity_type_name, 'default') - ->setComponent($paragraphs_field_name, ['type' => $widget_type]); - $form_display->save(); - - $view_display = entity_get_display($entity_type, $entity_type_name, 'default') - ->setComponent($paragraphs_field_name, ['type' => 'entity_reference_revisions_entity_view']); - $view_display->save(); - } - - /** - * Adds a Paragraphs type. - * - * @param string $paragraphs_type_name - * Paragraph type name used to create. - */ - protected function addParagraphsType($paragraphs_type_name) { - $paragraphs_type = ParagraphsType::create([ - 'id' => $paragraphs_type_name, - 'label' => $paragraphs_type_name, - ]); - $paragraphs_type->save(); - } - - /** - * Adds an icon to a paragraphs type. - * - * @param string $paragraphs_type - * Machine name of the paragraph type to add the icon to. - * - * @return \Drupal\file\Entity\File - * The file entity used as the icon. - */ - protected function addParagraphsTypeIcon($paragraphs_type) { - // Get an image. - $image_files = $this->getTestFiles('image'); - $uri = current($image_files)->uri; - - // Create a copy of the image, so that multiple file entities don't - // reference the same file. - $copy_uri = file_unmanaged_copy($uri); - - // Create a new file entity. - $file_entity = File::create([ - 'uri' => $copy_uri, - ]); - $file_entity->save(); - - // Add the file entity to the paragraphs type as its icon. - $paragraphs_type_entity = ParagraphsType::load($paragraphs_type); - $paragraphs_type_entity->set('icon_uuid', $file_entity->uuid()); - $paragraphs_type_entity->save(); - - return $file_entity; - } - - /** - * Adds a field to a given paragraph type. - * - * @param string $paragraph_type_id - * Paragraph type ID to be used. - * @param string $field_name - * Field name to be used. - * @param string $field_type - * Type of the field. - * @param array $storage_settings - * Settings for the field storage. - */ - protected function addFieldtoParagraphType($paragraph_type_id, $field_name, $field_type, array $storage_settings = []) { - // Add a paragraphs field. - $field_storage = FieldStorageConfig::create([ - 'field_name' => $field_name, - 'entity_type' => 'paragraph', - 'type' => $field_type, - 'cardinality' => 1, - 'settings' => $storage_settings, - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => $paragraph_type_id, - 'settings' => [], - ]); - $field->save(); - - $field_type_definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field_type); - - entity_get_form_display('paragraph', $paragraph_type_id, 'default') - ->setComponent($field_name, ['type' => $field_type_definition['default_widget']]) - ->save(); - - entity_get_display('paragraph', $paragraph_type_id, 'default') - ->setComponent($field_name, ['type' => $field_type_definition['default_formatter']]) - ->save(); - } - - /** - * Sets some of the settings of a paragraphs field widget. - * - * @param string $bundle - * Machine name of the bundle (e.g., a content type for nodes, a paragraphs - * type for paragraphs, etc.) with a paragraphs field. - * @param string $paragraphs_field - * Machine name of the paragraphs field whose widget (settings) to change. - * @param array $settings - * New setting values keyed by names of settings that are to be set. - * @param string|null $field_widget - * (optional) Machine name of the paragraphs field widget to use. NULL to - * keep the current widget. - * @param string $entity_type - * (optional) Machine name of the content entity type that the bundle - * belongs to. Defaults to "node". - */ - protected function setParagraphsWidgetSettings($bundle, $paragraphs_field, array $settings, $field_widget = NULL, $entity_type = 'node') { - /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $default_form_display */ - $default_form_display = \Drupal::entityTypeManager() - ->getStorage('entity_form_display') - ->load($entity_type . '.' . $bundle . '.default'); - $component = $default_form_display->getComponent($paragraphs_field); - - $updated_component = $component; - if ($field_widget === NULL || $field_widget === $component['type']) { - // The widget stays the same. - $updated_component['settings'] = $settings + $component['settings']; - } - else { - // Change the widget. - $updated_component['type'] = $field_widget; - - $widget_definition = \Drupal::service('plugin.manager.field.widget') - ->getDefinition($field_widget); - /** @var \Drupal\Core\Field\WidgetInterface $class */ - $class = $widget_definition['class']; - $default_settings = $class::defaultSettings(); - - $updated_component['settings'] = $settings + $default_settings; - } - - $default_form_display->setComponent($paragraphs_field, $updated_component) - ->save(); - } - -} diff --git a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsBehaviorPluginsTest.php b/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsBehaviorPluginsTest.php deleted file mode 100644 index 7916deb46..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsBehaviorPluginsTest.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Kernel; - -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\KernelTests\KernelTestBase; - -/** - * Tests the behavior plugins API. - * - * @group paragraphs - */ -class ParagraphsBehaviorPluginsTest extends KernelTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'paragraphs', - 'user', - 'system', - 'field', - 'entity_reference_revisions', - 'paragraphs_test', - 'file', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->installEntitySchema('user'); - $this->installEntitySchema('paragraph'); - $this->installSchema('system', ['sequences']); - \Drupal::moduleHandler()->loadInclude('paragraphs', 'install'); - } - - /** - * Tests the behavior settings API. - */ - public function testBehaviorSettings() { - // Create a paragraph type. - $paragraph_type = ParagraphsType::create(array( - 'label' => 'test_text', - 'id' => 'test_text', - 'behavior_plugins' => [ - 'test_text_color' => [ - 'enabled' => TRUE, - ] - ], - )); - $paragraph_type->save(); - - // Create a paragraph and set its feature settings. - $paragraph = Paragraph::create([ - 'type' => 'test_text', - ]); - $feature_settings = [ - 'test_text_color' => [ - 'text_color' => 'red' - ], - ]; - $paragraph->setAllBehaviorSettings($feature_settings); - $paragraph->save(); - - // Load the paragraph and assert its stored feature settings. - $paragraph = Paragraph::load($paragraph->id()); - $this->assertEquals($paragraph->getAllBehaviorSettings(), $feature_settings); - - // Check the text color plugin settings summary. - $plugin = $paragraph->getParagraphType()->getBehaviorPlugins()->getEnabled(); - $this->assertEquals($plugin['test_text_color']->settingsSummary($paragraph)[0], 'Text color: red'); - - // Update the value of an specific plugin. - $paragraph->setBehaviorSettings('test_text_color', ['text_color' => 'blue']); - $paragraph->save(); - - // Assert the values have been updated. - $paragraph = Paragraph::load($paragraph->id()); - $this->assertEquals($paragraph->getBehaviorSetting('test_text_color', 'text_color'), 'blue'); - - // Check the text color plugin settings summary. - $plugin = $paragraph->getParagraphType()->getBehaviorPlugins()->getEnabled(); - $this->assertEquals($plugin['test_text_color']->settingsSummary($paragraph)[0], 'Text color: blue'); - - } - -} diff --git a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsCollapsedSummaryTest.php b/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsCollapsedSummaryTest.php deleted file mode 100644 index 0b6bb74e4..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsCollapsedSummaryTest.php +++ /dev/null @@ -1,166 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Kernel; - -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\KernelTests\KernelTestBase; - -/** - * Tests the collapsed summary options. - * - * @group paragraphs - */ -class ParagraphsCollapsedSummaryTest extends KernelTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'paragraphs', - 'user', - 'system', - 'field', - 'entity_reference_revisions', - 'paragraphs_test', - 'file', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->installEntitySchema('user'); - $this->installEntitySchema('paragraph'); - $this->installSchema('system', ['sequences']); - \Drupal::moduleHandler()->loadInclude('paragraphs', 'install'); - - // Create a text paragraph type with test_text_color plugin enabled. - $paragraph_type = ParagraphsType::create(array( - 'label' => 'text_paragraph', - 'id' => 'text_paragraph', - 'behavior_plugins' => [ - 'test_text_color' => [ - 'enabled' => TRUE, - ] - ], - )); - $paragraph_type->save(); - $this->addParagraphsField('text_paragraph', 'text', 'string'); - - // Add a nested Paragraph type. - $paragraphs_type = ParagraphsType::create([ - 'id' => 'nested_paragraph', - 'label' => 'nested_paragraph', - ]); - $paragraphs_type->save(); - $this->addParagraphsField('nested_paragraph', 'nested_paragraph_field', 'entity_reference_revisions', ['target_type' => 'paragraph']); - } - - /** - * Tests the collapsed summary additional options. - */ - public function testCollapsedSummaryOptions() { - // Create a paragraph and set its feature settings. - $paragraph = Paragraph::create([ - 'type' => 'text_paragraph', - 'text' => 'Example text for a text paragraph', - ]); - $feature_settings = [ - 'test_text_color' => [ - 'text_color' => 'red' - ], - ]; - $paragraph->setAllBehaviorSettings($feature_settings); - $paragraph->save(); - - // Load the paragraph and assert its stored feature settings. - $paragraph = Paragraph::load($paragraph->id()); - $this->assertEquals($paragraph->getAllBehaviorSettings(), $feature_settings); - $this->assertEquals($paragraph->getSummary(), 'Example text for a text paragraph, Text color: red'); - - // Check the summary and the additional options. - $paragraph_1 = Paragraph::create([ - 'type' => 'nested_paragraph', - 'nested_paragraph_field' => [$paragraph], - ]); - $paragraph_1->save(); - $this->assertEquals($paragraph_1->getSummary(), '1 child, Example text for a text paragraph, Text color: red'); - $this->assertEquals($paragraph_1->getSummary(['show_behavior_summary' => FALSE]), '1 child, Example text for a text paragraph'); - $this->assertEquals($paragraph_1->getSummary(['depth_limit' => 0]), ''); - } - - /** - * Tests nested paragraph summary. - */ - public function testNestedParagraphSummary() { - // Create a text paragraph. - $paragraph_text_1 = Paragraph::create([ - 'type' => 'text_paragraph', - 'text' => 'Text paragraph on nested level', - ]); - $paragraph_text_1->save(); - - // Add a nested paragraph with the text inside. - $paragraph_nested_1 = Paragraph::create([ - 'type' => 'nested_paragraph', - 'nested_paragraph_field' => [$paragraph_text_1], - ]); - $paragraph_nested_1->save(); - - // Create a new text paragraph. - $paragraph_text_2 = Paragraph::create([ - 'type' => 'text_paragraph', - 'text' => 'Text paragraph on top level', - ]); - $paragraph_text_2->save(); - - // Add a nested paragraph with the new text and nested paragraph inside. - $paragraph_nested_2 = Paragraph::create([ - 'type' => 'nested_paragraph', - 'nested_paragraph_field' => [$paragraph_text_2, $paragraph_nested_1], - ]); - $paragraph_nested_2->save(); - $this->assertEquals($paragraph_nested_2->getSummary(['show_behavior_summary' => FALSE]), '2 children, Text paragraph on top level'); - $this->assertEquals($paragraph_nested_2->getSummary(['show_behavior_summary' => FALSE, 'depth_limit' => 2]), '2 children, Text paragraph on top level, 1 child, Text paragraph on nested level'); - } - - /** - * Adds a field to a given paragraph type. - * - * @param string $paragraph_type_name - * Paragraph type name to be used. - * @param string $field_name - * Paragraphs field name to be used. - * @param string $field_type - * Type of the field. - * @param array $field_edit - * Edit settings for the field. - */ - protected function addParagraphsField($paragraph_type_name, $field_name, $field_type, $field_edit = []) { - // Add a paragraphs field. - $field_storage = FieldStorageConfig::create([ - 'field_name' => $field_name, - 'entity_type' => 'paragraph', - 'type' => $field_type, - 'cardinality' => '-1', - 'settings' => $field_edit - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => $paragraph_type_name, - 'settings' => [ - 'handler' => 'default:paragraph', - 'handler_settings' => ['target_bundles' => NULL], - ], - ]); - $field->save(); - } -} diff --git a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsCompositeRelationshipTest.php b/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsCompositeRelationshipTest.php deleted file mode 100644 index c63d89461..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsCompositeRelationshipTest.php +++ /dev/null @@ -1,348 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Kernel; - -use Drupal\Core\Entity\Entity; -use Drupal\Core\Site\Settings; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\node\Entity\Node; -use Drupal\node\Entity\NodeType; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\KernelTests\KernelTestBase; -use Drupal\user\Entity\User; - -/** - * Tests the ERR composite relationship upgrade path. - * - * @group paragraphs - */ -class ParagraphsCompositeRelationshipTest extends KernelTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array( - 'paragraphs', - 'node', - 'user', - 'system', - 'field', - 'entity_reference_revisions', - 'language', - 'file', - ); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create paragraphs and article content types. - $values = ['type' => 'article', 'name' => 'Article']; - $node_type = NodeType::create($values); - $node_type->save(); - $this->installEntitySchema('user'); - $this->installEntitySchema('node'); - $this->installEntitySchema('paragraph'); - $this->installSchema('system', ['sequences']); - $this->installSchema('node', ['node_access']); - \Drupal::moduleHandler()->loadInclude('paragraphs', 'install'); - } - - /** - * Tests the revision of paragraphs. - */ - public function testParagraphsRevisions() { - // Create the paragraph type. - $paragraph_type = ParagraphsType::create(array( - 'label' => 'test_text', - 'id' => 'test_text', - )); - $paragraph_type->save(); - - $paragraph_type_nested = ParagraphsType::create(array( - 'label' => 'test_nested', - 'id' => 'test_nested', - )); - $paragraph_type_nested->save(); - - // Add a paragraph field to the article. - $field_storage = FieldStorageConfig::create(array( - 'field_name' => 'nested_paragraph_field', - 'entity_type' => 'paragraph', - 'type' => 'entity_reference_revisions', - 'cardinality' => '-1', - 'settings' => array( - 'target_type' => 'paragraph' - ), - )); - $field_storage->save(); - $field = FieldConfig::create(array( - 'field_storage' => $field_storage, - 'bundle' => 'test_nested', - )); - $field->save(); - - // Add a paragraph field to the article. - $field_storage = FieldStorageConfig::create(array( - 'field_name' => 'node_paragraph_field', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'cardinality' => '-1', - 'settings' => array( - 'target_type' => 'paragraph' - ), - )); - $field_storage->save(); - $field = FieldConfig::create(array( - 'field_storage' => $field_storage, - 'bundle' => 'article', - )); - $field->save(); - - // Add a paragraph field to the user. - $field_storage = FieldStorageConfig::create(array( - 'field_name' => 'user_paragraph_field', - 'entity_type' => 'user', - 'type' => 'entity_reference_revisions', - 'settings' => array( - 'target_type' => 'paragraph' - ), - )); - $field_storage->save(); - $field = FieldConfig::create(array( - 'field_storage' => $field_storage, - 'bundle' => 'user', - )); - $field->save(); - - // Create a paragraph. - $paragraph1 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_text', - ]); - $paragraph1->save(); - // Create another paragraph. - $paragraph2 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_text', - ]); - $paragraph2->save(); - // Create another paragraph. - $paragraph3 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_text', - ]); - $paragraph3->save(); - // Create another paragraph. - $paragraph_nested_children1 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_text', - ]); - $paragraph_nested_children1->save(); - // Create another paragraph. - $paragraph_nested_children2 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_text', - ]); - $paragraph_nested_children2->save(); - - // Create another paragraph. - $paragraph4_nested_parent = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_nested', - 'nested_paragraph_field' => [$paragraph_nested_children1, $paragraph_nested_children2], - ]); - $paragraph4_nested_parent->save(); - - // Create another paragraph. - $paragraph_user_1 = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_text', - ]); - $paragraph_user_1->save(); - - // Create a node with two paragraphs. - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'node_paragraph_field' => array($paragraph1, $paragraph2, $paragraph3, $paragraph4_nested_parent), - ]); - $node->save(); - - // Create an user with a paragraph. - $user = User::create([ - 'name' => 'test', - 'user_paragraph_field' => $paragraph_user_1, - ]); - $user->save(); - $settings = Settings::getAll(); - $settings['paragraph_limit'] = 1; - new Settings($settings); - - // Unset the parent field name, type and id of paragraph1. - /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ - $paragraph = Paragraph::load($paragraph1->id()); - $paragraph->set('parent_field_name', NULL); - $paragraph->set('parent_type', NULL); - $paragraph->set('parent_id', NULL); - $paragraph->setNewRevision(FALSE); - $paragraph->save(); - - // Unset the parent field name, type and id of paragraph2. - $paragraph = Paragraph::load($paragraph2->id()); - $paragraph->set('parent_field_name', NULL); - $paragraph->set('parent_type', NULL); - $paragraph->set('parent_id', NULL); - $paragraph->setNewRevision(FALSE); - $paragraph->save(); - - // Unset the parent field name, type and id of $paragraph_nested_parent. - $paragraph = Paragraph::load($paragraph4_nested_parent->id()); - $paragraph->set('parent_field_name', NULL); - $paragraph->set('parent_type', NULL); - $paragraph->set('parent_id', NULL); - $paragraph->setNewRevision(FALSE); - $paragraph->save(); - - // Unset the parent field name, type and id of $paragraph_nested_children1. - $paragraph = Paragraph::load($paragraph_nested_children1->id()); - $paragraph->set('parent_field_name', NULL); - $paragraph->set('parent_type', NULL); - $paragraph->set('parent_id', NULL); - $paragraph->setNewRevision(FALSE); - $paragraph->save(); - - // Unset the parent field name, type and id of paragraph_user_1. - /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ - $paragraph = Paragraph::load($paragraph_user_1->id()); - $paragraph->set('parent_field_name', NULL); - $paragraph->set('parent_type', NULL); - $paragraph->set('parent_id', NULL); - $paragraph->setNewRevision(FALSE); - $paragraph->save(); - - // Create a revision for node. - /** @var \Drupal\node\Entity\Node $node_revision1 */ - $node_revision1 = Node::load($node->id()); - /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph1_revision1 */ - $paragraph1_revision1 = Paragraph::load($paragraph1->id()); - $paragraph1_revision1->setNewRevision(TRUE); - $paragraph1_revision1->save(); - /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph2_revision1 */ - $paragraph2_revision1 = Paragraph::load($paragraph2->id()); - $paragraph2_revision1->setNewRevision(TRUE); - $paragraph2_revision1->save(); - $node_revision1->set('node_paragraph_field', [$paragraph1_revision1, $paragraph2_revision1]); - $node_revision1->setNewRevision(TRUE); - $node_revision1->save(); - - // Unset the parent field name, type and id of paragraph2_revision1. - $paragraph2_revision1 = Paragraph::load($paragraph2_revision1->id()); - $paragraph2_revision1->set('parent_field_name', NULL); - $paragraph2_revision1->set('parent_type', NULL); - $paragraph2_revision1->set('parent_id', NULL); - $paragraph2_revision1->setNewRevision(FALSE); - $paragraph2_revision1->save(); - - // Create another revision for node. - /** @var \Drupal\node\Entity\Node $node_revision2 */ - $node_revision2 = Node::load($node->id()); - /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph1_revision2 */ - $paragraph1_revision2 = Paragraph::load($paragraph1->id()); - $paragraph1_revision2->setNewRevision(TRUE); - $paragraph1_revision2->save(); - $node_revision2->set('node_paragraph_field', [$paragraph1_revision2]); - $node_revision2->setNewRevision(TRUE); - $node_revision2->save(); - - // Deletion of referenced paragraphs should not break updates. - $paragraph3->delete(); - \Drupal::moduleHandler()->loadInclude('paragraphs', 'post_update.php'); - // Run update function and check #finished. - $sandbox = []; - do { - paragraphs_post_update_set_paragraphs_parent_fields($sandbox); - } while ($sandbox['#finished'] < 1); - - $node_paragraph1 = Paragraph::load($paragraph1->id())->toArray(); - $this->assertParagraphField($node_paragraph1, $node->id(), $node->getEntityTypeId(), 'node_paragraph_field'); - - $paragraph1_revision1 = \Drupal::entityTypeManager()->getStorage('paragraph')->loadRevision($paragraph1_revision1->getRevisionId())->toArray(); - $this->assertParagraphField($paragraph1_revision1, $node->id(), $node->getEntityTypeId(), 'node_paragraph_field'); - - $paragraph1_revision2 = \Drupal::entityTypeManager()->getStorage('paragraph')->loadRevision($paragraph1_revision2->getRevisionId())->toArray(); - $this->assertParagraphField($paragraph1_revision2, $node->id(), $node->getEntityTypeId(), 'node_paragraph_field'); - - $node_paragraph2 = Paragraph::load($paragraph2->id())->toArray(); - $this->assertParagraphField($node_paragraph2, $node->id(), $node->getEntityTypeId(), 'node_paragraph_field'); - - $user_paragraph = Paragraph::load($paragraph_user_1->id())->toArray(); - $this->assertParagraphField($user_paragraph, $user->id(), $user->getEntityTypeId(), 'user_paragraph_field'); - - $nested_paragraph_parent = Paragraph::load($paragraph4_nested_parent->id())->toArray(); - $this->assertParagraphField($nested_paragraph_parent, $node->id(), $node->getEntityTypeId(), 'node_paragraph_field'); - - $nested_paragraph_children = Paragraph::load($paragraph_nested_children1->id())->toArray(); - $this->assertParagraphField($nested_paragraph_children, $paragraph4_nested_parent->id(), $paragraph4_nested_parent->getEntityTypeId(), 'nested_paragraph_field'); - - // Add the german language. - ConfigurableLanguage::create(['id' => 'de'])->save(); - - // Create a new paragraph and add a german translation. - $paragraph = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_text' - ]); - $paragraph->addTranslation('de'); - $paragraph->save(); - - // Load a node and add a german translation. - $node = Node::load($node->id()); - $node->addTranslation('de', [ - 'title' => 'german', - 'node_paragraph_field' => $paragraph - ]); - $node->save(); - - // Load the paragraph and its german translation. - $paragraph = Paragraph::load($paragraph->id()); - $paragraph = $paragraph->getTranslation('de'); - - // Get the parent entity. - $parent = $paragraph->getParentEntity(); - static::assertEquals($parent->language()->getId(), 'de'); - - // Test if the needs save variable is set as false after saving. - $paragraph_needs_save = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'test_text', - ]); - $paragraph_needs_save->setNeedsSave(TRUE); - $paragraph_needs_save->save(); - $this->assertFalse($paragraph_needs_save->needsSave()); - } - - /** - * Checks if $paragraph fields match with host / parent. - * - * @param $paragraph - * The paragraph entity to check. - * @param $id - * The parent entity id. - * @param $entity_type - * The parent entity type. - * @param $field_name - * The parent entity field name. - */ - public function assertParagraphField($paragraph, $id, $entity_type, $field_name) { - self::assertEquals($paragraph['parent_id'][0]['value'], $id, 'Match parent id.'); - self::assertEquals($paragraph['parent_type'][0]['value'], $entity_type, 'Matching parent type.'); - self::assertEquals($paragraph['parent_field_name'][0]['value'], $field_name, 'Matching parent field name.'); - } -} diff --git a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsIsChangedTest.php b/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsIsChangedTest.php deleted file mode 100644 index de98760a0..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsIsChangedTest.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Kernel; - -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\KernelTests\KernelTestBase; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\Entity\ParagraphsType; - -/** - * Tests \Drupal\Paragraphs\Entity\Paragraph::isChanged(). - * - * @group paragraphs - */ -class ParagraphsIsChangedTest extends KernelTestBase { - - /** - * {@inheritdoc} - */ - public static $modules = [ - 'paragraphs', - 'user', - 'system', - 'field', - 'entity_reference_revisions', - 'file', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->installEntitySchema('user'); - $this->installEntitySchema('paragraph'); - $this->installSchema('system', ['sequences']); - \Drupal::moduleHandler()->loadInclude('paragraphs', 'install'); - - // Create a text paragraph type. - $paragraph_type = ParagraphsType::create([ - 'label' => 'text_paragraph', - 'id' => 'text_paragraph', - ]); - $paragraph_type->save(); - $this->addParagraphsField('text_paragraph', 'text', 'string'); - } - - /** - * Tests the functionality of the isChanged() function. - */ - public function testIsChanged() { - // Create a paragraph. - $paragraph = Paragraph::create([ - 'title' => 'Paragraph', - 'type' => 'text_paragraph', - 'text' => 'Text Paragraph', - ]); - $this->assertTrue($paragraph->isChanged(), 'The paragraph is a new entity.'); - - // Save the paragraph and assert no changes. - $paragraph->save(); - $this->assertFalse($paragraph->isChanged(), 'Paragraph::isChanged() found no changes after the entity has been saved.'); - - // Update the revision author field, which should be skipped from checking - // for changes in Paragraph::isChanged(). - $paragraph->setRevisionAuthorId(3); - $this->assertFalse($paragraph->isChanged(), 'Paragraph::isChanged() found no changes after updating revision_uid field.'); - - $paragraph->set('text', 'New text'); - $this->assertTrue($paragraph->isChanged(), 'Paragraph::isChanged() found changes after updating text field.'); - } - - /** - * Adds a field to a given paragraph type. - * - * @param string $paragraph_type_name - * Paragraph type name to be used. - * @param string $field_name - * Paragraphs field name to be used. - * @param string $field_type - * Type of the field. - * @param array $field_edit - * Edit settings for the field. - */ - protected function addParagraphsField($paragraph_type_name, $field_name, $field_type, array $field_edit = []) { - // Add a paragraphs field. - $field_storage = FieldStorageConfig::create([ - 'field_name' => $field_name, - 'entity_type' => 'paragraph', - 'type' => $field_type, - 'cardinality' => '-1', - 'settings' => $field_edit, - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => $paragraph_type_name, - 'settings' => [ - 'handler' => 'default:paragraph', - 'handler_settings' => ['target_bundles' => NULL], - ], - ]); - $field->save(); - } - -} diff --git a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsReplicateTest.php b/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsReplicateTest.php deleted file mode 100644 index a7b294fb6..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsReplicateTest.php +++ /dev/null @@ -1,190 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Kernel; - -use Drupal\Core\Entity\Entity; -use Drupal\Core\Site\Settings; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\node\Entity\Node; -use Drupal\node\Entity\NodeType; -use Drupal\paragraphs\Entity\Paragraph; -use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\KernelTests\KernelTestBase; -use Drupal\user\Entity\User; - -/** - * Tests the replication functionality provided by Replicate module. - * - * @group paragraphs - */ -class ParagraphsReplicateTest extends KernelTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'paragraphs', - 'replicate', - 'node', - 'user', - 'system', - 'field', - 'entity_reference_revisions', - 'file', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - // Create paragraphs and article content types. - $values = ['type' => 'article', 'name' => 'Article']; - $node_type = NodeType::create($values); - $node_type->save(); - $this->installEntitySchema('user'); - $this->installEntitySchema('node'); - $this->installEntitySchema('paragraph'); - $this->installSchema('system', ['sequences']); - $this->installSchema('node', ['node_access']); - \Drupal::moduleHandler()->loadInclude('paragraphs', 'install'); - } - - /** - * Tests the replication of the parent entity. - */ - public function testReplication() { - // Create the paragraph type. - $paragraph_type = ParagraphsType::create([ - 'label' => 'test_text', - 'id' => 'test_text', - ]); - $paragraph_type->save(); - - $paragraph_type_nested = ParagraphsType::create([ - 'label' => 'test_nested', - 'id' => 'test_nested', - ]); - $paragraph_type_nested->save(); - - // Add a title field to both paragraph bundles. - $field_storage = FieldStorageConfig::create([ - 'field_name' => 'title', - 'entity_type' => 'paragraph', - 'type' => 'string', - 'cardinality' => '1', - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'test_text', - ]); - $field->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'test_nested', - ]); - $field->save(); - - // Add a paragraph field to the nested paragraph. - $field_storage = FieldStorageConfig::create([ - 'field_name' => 'nested_paragraph_field', - 'entity_type' => 'paragraph', - 'type' => 'entity_reference_revisions', - 'cardinality' => '-1', - 'settings' => [ - 'target_type' => 'paragraph', - ], - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'test_nested', - ]); - $field->save(); - - // Add a paragraph field to the article. - $field_storage = FieldStorageConfig::create([ - 'field_name' => 'node_paragraph_field', - 'entity_type' => 'node', - 'type' => 'entity_reference_revisions', - 'cardinality' => '-1', - 'settings' => [ - 'target_type' => 'paragraph', - ], - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'article', - ]); - $field->save(); - - // Create a paragraph. - $paragraph = Paragraph::create([ - 'title' => 'Simple paragraph', - 'type' => 'test_text', - ]); - $paragraph->save(); - - // Create nested paragraph. - $paragraph_nested = Paragraph::create([ - 'title' => 'Nested paragraph', - 'type' => 'test_text', - ]); - $paragraph_nested->save(); - - // Create another paragraph. - $paragraph_nested_parent = Paragraph::create([ - 'title' => 'Parent paragraph', - 'type' => 'test_nested', - 'nested_paragraph_field' => [$paragraph_nested], - ]); - $paragraph_nested_parent->save(); - - // Create a node with two paragraphs. - $node = Node::create([ - 'title' => $this->randomMachineName(), - 'type' => 'article', - 'node_paragraph_field' => array($paragraph, $paragraph_nested_parent), - ]); - $node->save(); - - $replicated_node = $this->container->get('replicate.replicator') - ->replicateEntity($node); - - // Check that all paragraphs on the replicated node were replicated too. - $this->assertNotEquals($replicated_node->id(), $node->id(), 'We have two different nodes.'); - $this->assertNotEquals($replicated_node->node_paragraph_field[0]->target_id, $node->node_paragraph_field[0]->target_id, 'Simple paragraph was duplicated.'); - $this->assertEquals('Simple paragraph', $replicated_node->node_paragraph_field[0]->entity->title->value, "Simple paragraph inherited title from it's original."); - $this->assertNotEquals($replicated_node->node_paragraph_field[1]->target_id, $node->node_paragraph_field[1]->target_id, 'Parent paragraph was duplicated.'); - $this->assertEquals('Parent paragraph', $replicated_node->node_paragraph_field[1]->entity->title->value, "Parent paragraph inherited title from it's original."); - $this->assertNotEquals($replicated_node->node_paragraph_field[1]->entity->nested_paragraph_field[0]->target_id, $node->node_paragraph_field[1]->entity->nested_paragraph_field[0]->target_id, 'Nested paragraph was duplicated.'); - $this->assertEquals('Nested paragraph', $replicated_node->node_paragraph_field[1]->entity->nested_paragraph_field[0]->entity->title->value, "Nested paragraph inherited title from it's original."); - - // Try to edit replicated paragraphs and check that changes do not propagate. - /** @var \Drupal\paragraphs\ParagraphInterface $simple_paragraph */ - $simple_paragraph = $replicated_node->node_paragraph_field[0]->entity; - $simple_paragraph->title->value = 'Simple paragraph - replicated'; - $simple_paragraph->save(); - /** @var \Drupal\paragraphs\ParagraphInterface $parent_paragraph */ - $parent_paragraph = $replicated_node->node_paragraph_field[1]->entity; - $parent_paragraph->title->value = 'Parent paragraph - replicated'; - $parent_paragraph->save(); - /** @var \Drupal\paragraphs\ParagraphInterface $nested_paragraph */ - $nested_paragraph = $replicated_node->node_paragraph_field[1]->entity->nested_paragraph_field[0]->entity; - $nested_paragraph->title->value = 'Nested paragraph - replicated'; - $nested_paragraph->save(); - - $this->assertEquals('Simple paragraph', $node->node_paragraph_field[0]->entity->title->value, 'Field value on the original simple paragraph are unchanged.'); - $this->assertEquals('Parent paragraph', $node->node_paragraph_field[1]->entity->title->value, 'Field value on the original parent paragraph are unchanged.'); - $this->assertEquals('Nested paragraph', $node->node_paragraph_field[1]->entity->nested_paragraph_field[0]->entity->title->value, 'Field value on the original nested paragraph are unchanged.'); - - $this->assertEquals('Simple paragraph - replicated', $replicated_node->node_paragraph_field[0]->entity->title->value, 'Field value on the replicated simple paragraph are updated.'); - $this->assertEquals('Parent paragraph - replicated', $replicated_node->node_paragraph_field[1]->entity->title->value, 'Field value on the replicated parent paragraph are updated.'); - $this->assertEquals('Nested paragraph - replicated', $replicated_node->node_paragraph_field[1]->entity->nested_paragraph_field[0]->entity->title->value, 'Field value on the replicated nested paragraph are updated.'); - } -} diff --git a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsTypeHasEnabledBehaviorPluginTest.php b/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsTypeHasEnabledBehaviorPluginTest.php deleted file mode 100644 index 05de58617..000000000 --- a/web/modules/contrib/paragraphs/tests/src/Kernel/ParagraphsTypeHasEnabledBehaviorPluginTest.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php - -namespace Drupal\Tests\paragraphs\Kernel; - -use Drupal\KernelTests\KernelTestBase; -use Drupal\paragraphs\Entity\ParagraphsType; - -/** - * Tests the ParagraphsType entity hasEnabledBehaviorPlugin functionality. - * - * @group paragraphs - */ -class ParagraphsTypeHasEnabledBehaviorPluginTest extends KernelTestBase { - - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'paragraphs', - 'user', - 'paragraphs_test', - 'file', - ]; - - /** - * ParagraphsType entity build in setUp() - * - * @var ParagraphsType - */ - protected $paragraphsType; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->installEntitySchema('user'); - $this->installEntitySchema('paragraph'); - \Drupal::moduleHandler()->loadInclude('paragraphs', 'install'); - - // Create a paragraph with an enabled and disabled plugin. - $this->paragraphsType = ParagraphsType::create([ - 'label' => 'test_text', - 'id' => 'test_text', - 'behavior_plugins' => [ - 'test_text_color' => [ - 'enabled' => TRUE, - ], - 'test_dummy_behavior' => [ - 'enabled' => FALSE, - ], - ], - ]); - $this->paragraphsType->save(); - } - - /** - * Tests the behavior settings API. - */ - public function testValidPluginIds() { - $this->assertTrue($this->paragraphsType->hasEnabledBehaviorPlugin('test_text_color')); - $this->assertFalse($this->paragraphsType->hasEnabledBehaviorPlugin('test_dummy_behavior')); - } - - /** - * Test that invalid plugin id's return false. - */ - public function testInvalidPluginId() { - $this->assertFalse($this->paragraphsType->hasEnabledBehaviorPlugin('i_do_not_exist')); - } - -} diff --git a/web/modules/contrib/pathauto/config/install/pathauto.settings.yml b/web/modules/contrib/pathauto/config/install/pathauto.settings.yml index 74a75cdd2..03004aa1d 100644 --- a/web/modules/contrib/pathauto/config/install/pathauto.settings.yml +++ b/web/modules/contrib/pathauto/config/install/pathauto.settings.yml @@ -11,3 +11,10 @@ reduce_ascii : FALSE case : TRUE ignore_words : 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with' update_action : 2 +safe_tokens: + - alias + - path + - join-path + - login-url + - url + - url-brief diff --git a/web/modules/contrib/pathauto/config/schema/pathauto.schema.yml b/web/modules/contrib/pathauto/config/schema/pathauto.schema.yml index ca04411e8..1da59f187 100644 --- a/web/modules/contrib/pathauto/config/schema/pathauto.schema.yml +++ b/web/modules/contrib/pathauto/config/schema/pathauto.schema.yml @@ -22,14 +22,17 @@ pathauto.settings: type: boolean reduce_ascii: type: boolean - ignore_words: - type: string case: type: boolean ignore_words: type: string update_action: type: integer + safe_tokens: + label: Tokens that are safe to use and do not need to be cleaned. + type: sequence + sequence: + type: string action.configuration.pathauto_update_alias: type: action_configuration_default diff --git a/web/modules/contrib/pathauto/pathauto.api.php b/web/modules/contrib/pathauto/pathauto.api.php index fc9272f8f..6595116fd 100644 --- a/web/modules/contrib/pathauto/pathauto.api.php +++ b/web/modules/contrib/pathauto/pathauto.api.php @@ -93,9 +93,8 @@ function hook_pathauto_is_alias_reserved($alias, $source, $langcode) { * This hook will only be called if a default pattern is configured (on * admin/config/search/path/patterns). * - * @param string $pattern - * The alias pattern for Pathauto to pass to token_replace() to generate the - * URL alias. + * @param \Drupal\pathauto\PathautoPatternInterface $pattern + * The Pathauto pattern to be used. * @param array $context * An associative array of additional options, with the following elements: * - 'module': The module or entity type being aliased. @@ -103,14 +102,14 @@ function hook_pathauto_is_alias_reserved($alias, $source, $langcode) { * aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'. * - 'source': A string of the source path for the alias (e.g. 'node/1'). * - 'data': An array of keyed objects to pass to token_replace(). - * - 'type': The sub-type or bundle of the object being aliased. + * - 'bundle': The sub-type or bundle of the object being aliased. * - 'language': A string of the language code for the alias (e.g. 'en'). * This can be altered by reference. */ -function hook_pathauto_pattern_alter(&$pattern, array $context) { +function hook_pathauto_pattern_alter(\Drupal\pathauto\PathautoPatternInterface $pattern, array $context) { // Switch out any [node:created:*] tokens with [node:updated:*] on update. if ($context['module'] == 'node' && ($context['op'] == 'update')) { - $pattern = preg_replace('/\[node:created(\:[^]]*)?\]/', '[node:updated$1]', $pattern); + $pattern->setPattern(preg_replace('/\[node:created(\:[^]]*)?\]/', '[node:updated$1]', $pattern->getPattern())); } } @@ -133,7 +132,7 @@ function hook_pathauto_pattern_alter(&$pattern, array $context) { * - 'pattern': A string of the pattern used for aliasing the object. */ function hook_pathauto_alias_alter(&$alias, array &$context) { - // Add a suffix so that all aliases get saved as 'content/my-title.html' + // Add a suffix so that all aliases get saved as 'content/my-title.html'. $alias .= '.html'; // Force all aliases to be saved as language neutral. @@ -143,7 +142,7 @@ function hook_pathauto_alias_alter(&$alias, array &$context) { /** * Alter the list of punctuation characters for Pathauto control. * - * @param $punctuation + * @param array $punctuation * An array of punctuation to be controlled by Pathauto during replacement * keyed by punctuation name. Each punctuation record should be an array * with the following key/value pairs: diff --git a/web/modules/contrib/pathauto/pathauto.info.yml b/web/modules/contrib/pathauto/pathauto.info.yml index e2812c759..c661dae56 100644 --- a/web/modules/contrib/pathauto/pathauto.info.yml +++ b/web/modules/contrib/pathauto/pathauto.info.yml @@ -6,6 +6,7 @@ type: module dependencies: - ctools:ctools - drupal:path +- drupal:system (>=8.5) - token:token configure: entity.pathauto_pattern.collection @@ -13,8 +14,8 @@ configure: entity.pathauto_pattern.collection recommends: - redirect:redirect -# Information added by Drupal.org packaging script on 2018-04-22 -version: '8.x-1.2' +# Information added by Drupal.org packaging script on 2018-09-08 +version: '8.x-1.3' core: '8.x' project: 'pathauto' -datestamp: 1524421087 +datestamp: 1536407890 diff --git a/web/modules/contrib/pathauto/pathauto.install b/web/modules/contrib/pathauto/pathauto.install index f8de322f6..4b160ba10 100644 --- a/web/modules/contrib/pathauto/pathauto.install +++ b/web/modules/contrib/pathauto/pathauto.install @@ -8,15 +8,13 @@ */ use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Plugin\Context\Context; -use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\pathauto\Entity\PathautoPattern; /** * Implements hook_install(). */ function pathauto_install() { - // Set the weight to 1 + // Set the weight to 1. module_set_weight('pathauto', 1); // Ensure the url_alias table exists. @@ -170,7 +168,8 @@ function pathauto_update_8100() { continue; } - // This is a pattern for a bundle and a language, such as "node_article_es". + // This is a pattern for a bundle and a language, such as + // "node_article_es". $pattern = PathautoPattern::create([ 'id' => $entity_type . '_' . $extracted_bundle . '_' . str_replace('-', '_', $langcode), 'label' => $entity_label . ' ' . $bundle_info[$extracted_bundle]['label'] . ' ' . $language->getName(), @@ -299,3 +298,23 @@ function pathauto_update_8106() { $config->set('enabled_entity_types', ['user']); $config->save(); } + +/** + * Initialize the new safe tokens setting. + */ +function pathauto_update_8107() { + + $safe_tokens = [ + 'alias', + 'alias', + 'path', + 'join-path', + 'login-url', + 'url', + 'url-brief', + ]; + + \Drupal::configFactory()->getEditable('pathauto.settings') + ->set('safe_tokens', $safe_tokens) + ->save(); +} diff --git a/web/modules/contrib/pathauto/pathauto.js b/web/modules/contrib/pathauto/pathauto.js index d4e068f6b..3bdc3d561 100644 --- a/web/modules/contrib/pathauto/pathauto.js +++ b/web/modules/contrib/pathauto/pathauto.js @@ -2,9 +2,9 @@ 'use strict'; Drupal.behaviors.pathFieldsetSummaries = { attach: function (context) { - $('fieldset.path-form', context).drupalSetSummary(function (context) { - var path = $('.form-item-path-alias input', context).val(); - var automatic = $('.form-item-path-pathauto input', context).attr('checked'); + $(context).find('.path-form').drupalSetSummary(function (context) { + var path = $('.js-form-item-path-0-alias input', context).val(); + var automatic = $('.js-form-item-path-0-pathauto input', context).prop('checked'); if (automatic) { return Drupal.t('Automatic alias'); diff --git a/web/modules/contrib/pathauto/pathauto.links.action.yml b/web/modules/contrib/pathauto/pathauto.links.action.yml index d68b94a88..0afa384db 100644 --- a/web/modules/contrib/pathauto/pathauto.links.action.yml +++ b/web/modules/contrib/pathauto/pathauto.links.action.yml @@ -3,4 +3,3 @@ entity.pathauto_pattern.add_form: title: 'Add Pathauto pattern' appears_on: - entity.pathauto_pattern.collection - diff --git a/web/modules/contrib/pathauto/pathauto.module b/web/modules/contrib/pathauto/pathauto.module index 67b3359ee..ac7d9b0c0 100644 --- a/web/modules/contrib/pathauto/pathauto.module +++ b/web/modules/contrib/pathauto/pathauto.module @@ -99,7 +99,7 @@ function pathauto_entity_update(EntityInterface $entity) { } /** - * Implements hook_entity_update(). + * Implements hook_entity_delete(). */ function pathauto_entity_delete(EntityInterface $entity) { if ($entity->hasLinkTemplate('canonical') && $entity instanceof ContentEntityInterface && $entity->hasField('path')) { diff --git a/web/modules/contrib/pathauto/pathauto.services.yml b/web/modules/contrib/pathauto/pathauto.services.yml index 3ce704ec7..8848f7f33 100644 --- a/web/modules/contrib/pathauto/pathauto.services.yml +++ b/web/modules/contrib/pathauto/pathauto.services.yml @@ -15,7 +15,7 @@ services: arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.route_provider', '@path.alias_manager'] pathauto.verbose_messenger: class: Drupal\pathauto\VerboseMessenger - arguments: ['@config.factory', '@current_user'] + arguments: ['@config.factory', '@current_user', '@messenger'] plugin.manager.alias_type: class: Drupal\pathauto\AliasTypeManager parent: default_plugin_manager diff --git a/web/modules/contrib/pathauto/src/AliasCleaner.php b/web/modules/contrib/pathauto/src/AliasCleaner.php index 340a26673..cb8b6d125 100644 --- a/web/modules/contrib/pathauto/src/AliasCleaner.php +++ b/web/modules/contrib/pathauto/src/AliasCleaner.php @@ -104,7 +104,7 @@ class AliasCleaner implements AliasCleanerInterface { // Trim duplicate, leading, and trailing separators. Do this before cleaning // backslashes since a pattern like "[token1]/[token2]-[token3]/[token4]" // could end up like "value1/-/value2" and if backslashes were cleaned first - // this would result in a duplicate blackslash. + // this would result in a duplicate backslash. $output = $this->getCleanSeparators($output); // Trim duplicate, leading, and trailing backslashes. @@ -247,7 +247,7 @@ class AliasCleaner implements AliasCleanerInterface { // Get rid of words that are on the ignore list. if ($this->cleanStringCache['ignore_words_regex']) { $words_removed = $this->cleanStringCache['ignore_words_callback']($this->cleanStringCache['ignore_words_regex'], '', $output); - if (Unicode::strlen(trim($words_removed)) > 0) { + if (mb_strlen(trim($words_removed)) > 0) { $output = $words_removed; } } @@ -260,7 +260,7 @@ class AliasCleaner implements AliasCleanerInterface { // Optionally convert to lower case. if ($this->cleanStringCache['lowercase']) { - $output = Unicode::strtolower($output); + $output = mb_strtolower($output); } // Shorten to a logical place based on word boundaries. @@ -335,7 +335,9 @@ class AliasCleaner implements AliasCleanerInterface { public function cleanTokenValues(&$replacements, $data = array(), $options = array()) { foreach ($replacements as $token => $value) { // Only clean non-path tokens. - if (!preg_match('/(path|alias|url|url-brief)\]$/', $token)) { + $config = $this->configFactory->get('pathauto.settings'); + $safe_tokens = implode('|', (array) $config->get('safe_tokens')); + if (!preg_match('/:(' . $safe_tokens . ')(:|\]$)/', $token)) { $replacements[$token] = $this->cleanString($value, $options); } } diff --git a/web/modules/contrib/pathauto/src/AliasCleanerInterface.php b/web/modules/contrib/pathauto/src/AliasCleanerInterface.php index 87cc27e7d..95d2920f7 100644 --- a/web/modules/contrib/pathauto/src/AliasCleanerInterface.php +++ b/web/modules/contrib/pathauto/src/AliasCleanerInterface.php @@ -72,8 +72,8 @@ interface AliasCleanerInterface { /** * Return an array of arrays for punctuation values. * - * Returns an array of arrays for punctuation values keyed by a name, including - * the value and a textual description. + * Returns an array of arrays for punctuation values keyed by a name, + * including the value and a textual description. * Can and should be expanded to include "all" non text punctuation values. * * @return array diff --git a/web/modules/contrib/pathauto/src/AliasStorageHelperInterface.php b/web/modules/contrib/pathauto/src/AliasStorageHelperInterface.php index 4f1788017..9b3b2bd86 100644 --- a/web/modules/contrib/pathauto/src/AliasStorageHelperInterface.php +++ b/web/modules/contrib/pathauto/src/AliasStorageHelperInterface.php @@ -1,6 +1,7 @@ <?php namespace Drupal\pathauto; + use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\LanguageInterface; @@ -94,7 +95,6 @@ interface AliasStorageHelperInterface { */ public function loadBySourcePrefix($source); - /** * Returns the count of url aliases for the source. * diff --git a/web/modules/contrib/pathauto/src/AliasTypeBatchUpdateInterface.php b/web/modules/contrib/pathauto/src/AliasTypeBatchUpdateInterface.php index 03f392961..9ec60a29f 100644 --- a/web/modules/contrib/pathauto/src/AliasTypeBatchUpdateInterface.php +++ b/web/modules/contrib/pathauto/src/AliasTypeBatchUpdateInterface.php @@ -13,7 +13,8 @@ interface AliasTypeBatchUpdateInterface extends AliasTypeInterface { * @param string $action * One of: * - 'create' to generate a URL alias for paths having none. - * - 'update' to recreate the URL alias for paths already having one, useful if the pattern changed. + * - 'update' to recreate the URL alias for paths already having one, useful + * if the pattern changed. * - 'all' to do both actions above at the same time. * @param array $context * Batch context. diff --git a/web/modules/contrib/pathauto/src/AliasUniquifier.php b/web/modules/contrib/pathauto/src/AliasUniquifier.php index 43fc5fa3f..70784e6f4 100644 --- a/web/modules/contrib/pathauto/src/AliasUniquifier.php +++ b/web/modules/contrib/pathauto/src/AliasUniquifier.php @@ -38,7 +38,7 @@ class AliasUniquifier implements AliasUniquifierInterface { /** * The route provider service. * - * @var \Drupal\Core\Routing\RouteProviderInterface. + * @var \Drupal\Core\Routing\RouteProviderInterface */ protected $routeProvider; @@ -60,6 +60,8 @@ class AliasUniquifier implements AliasUniquifierInterface { * The module handler. * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider * The route provider service. + * @param \Drupal\Core\Path\AliasManagerInterface $alias_manager + * The alias manager. */ public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, AliasManagerInterface $alias_manager) { $this->configFactory = $config_factory; diff --git a/web/modules/contrib/pathauto/src/AliasUniquifierInterface.php b/web/modules/contrib/pathauto/src/AliasUniquifierInterface.php index 2da4ba27a..89649eab3 100644 --- a/web/modules/contrib/pathauto/src/AliasUniquifierInterface.php +++ b/web/modules/contrib/pathauto/src/AliasUniquifierInterface.php @@ -1,6 +1,7 @@ <?php namespace Drupal\pathauto; + use Drupal\Core\Language\LanguageInterface; /** diff --git a/web/modules/contrib/pathauto/src/Annotation/AliasType.php b/web/modules/contrib/pathauto/src/Annotation/AliasType.php index d5081a77c..42fb36649 100644 --- a/web/modules/contrib/pathauto/src/Annotation/AliasType.php +++ b/web/modules/contrib/pathauto/src/Annotation/AliasType.php @@ -21,9 +21,9 @@ class AliasType extends Plugin { /** * The human-readable name of the action plugin. * - * @ingroup plugin_translatable - * * @var \Drupal\Core\Annotation\Translation + * + * @ingroup plugin_translatable */ public $label; diff --git a/web/modules/contrib/pathauto/src/EventSubscriber/PathautoSettingsCacheTag.php b/web/modules/contrib/pathauto/src/EventSubscriber/PathautoSettingsCacheTag.php index 371fe2e2f..698d76b75 100644 --- a/web/modules/contrib/pathauto/src/EventSubscriber/PathautoSettingsCacheTag.php +++ b/web/modules/contrib/pathauto/src/EventSubscriber/PathautoSettingsCacheTag.php @@ -13,11 +13,25 @@ use Drupal\pathauto\AliasTypeManager; */ class PathautoSettingsCacheTag implements EventSubscriberInterface { + /** + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ protected $entityFieldManager; + + /** + * The alias type manager. + * + * @var \Drupal\pathauto\AliasTypeManager + */ protected $aliasTypeManager; /** * Constructs a PathautoSettingsCacheTag object. + * + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. + * @param \Drupal\pathauto\AliasTypeManager $alias_type_manager + * The alias type manager. */ public function __construct(EntityFieldManagerInterface $entity_field_manager, AliasTypeManager $alias_type_manager) { $this->entityFieldManager = $entity_field_manager; diff --git a/web/modules/contrib/pathauto/src/Form/PathautoAdminDelete.php b/web/modules/contrib/pathauto/src/Form/PathautoAdminDelete.php index a23052520..277c30700 100644 --- a/web/modules/contrib/pathauto/src/Form/PathautoAdminDelete.php +++ b/web/modules/contrib/pathauto/src/Form/PathautoAdminDelete.php @@ -133,14 +133,14 @@ class PathautoAdminDelete extends FormBase { } else if ($delete_all) { \Drupal::service('pathauto.alias_storage_helper')->deleteAll(); - drupal_set_message($this->t('All of your path aliases have been deleted.')); + $this->messenger()->addMessage($this->t('All of your path aliases have been deleted.')); } else { $storage_helper = \Drupal::service('pathauto.alias_storage_helper'); foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) { $alias_type = $this->aliasTypeManager->createInstance($id); $storage_helper->deleteBySourcePrefix((string) $alias_type->getSourcePrefix()); - drupal_set_message($this->t('All of your %label path aliases have been deleted.', ['%label' => $alias_type->getLabel()])); + $this->messenger()->addMessage($this->t('All of your %label path aliases have been deleted.', ['%label' => $alias_type->getLabel()])); } } } @@ -168,17 +168,25 @@ class PathautoAdminDelete extends FormBase { public static function batchFinished($success, $results, $operations) { if ($success) { if ($results['delete_all']) { - drupal_set_message(t('All of your automatically generated path aliases have been deleted.')); + \Drupal::service('messenger') + ->addMessage(t('All of your automatically generated path aliases have been deleted.')); } else if (isset($results['deletions'])) { foreach (array_values($results['deletions']) as $label) { - drupal_set_message(t('All of your automatically generated %label path aliases have been deleted.', ['%label' => $label])); + \Drupal::service('messenger') + ->addMessage(t('All of your automatically generated %label path aliases have been deleted.', [ + '%label' => $label, + ])); } } } else { $error_operation = reset($operations); - drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE)))); + \Drupal::service('messenger') + ->addMessage(t('An error occurred while processing @operation with arguments : @args', [ + '@operation' => $error_operation[0], + '@args' => print_r($error_operation[0]), + ])); } } diff --git a/web/modules/contrib/pathauto/src/Form/PathautoBulkUpdateForm.php b/web/modules/contrib/pathauto/src/Form/PathautoBulkUpdateForm.php index e66cd9df5..67a026652 100644 --- a/web/modules/contrib/pathauto/src/Form/PathautoBulkUpdateForm.php +++ b/web/modules/contrib/pathauto/src/Form/PathautoBulkUpdateForm.php @@ -149,15 +149,21 @@ class PathautoBulkUpdateForm extends FormBase { public static function batchFinished($success, $results, $operations) { if ($success) { if ($results['updates']) { - drupal_set_message(\Drupal::translation()->formatPlural($results['updates'], 'Generated 1 URL alias.', 'Generated @count URL aliases.')); + \Drupal::service('messenger')->addMessage(\Drupal::translation() + ->formatPlural($results['updates'], 'Generated 1 URL alias.', 'Generated @count URL aliases.')); } else { - drupal_set_message(t('No new URL aliases to generate.')); + \Drupal::service('messenger') + ->addMessage(t('No new URL aliases to generate.')); } } else { $error_operation = reset($operations); - drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE)))); + \Drupal::service('messenger') + ->addMessage(t('An error occurred while processing @operation with arguments : @args'), [ + '@operation' => $error_operation[0], + '@args' => print_r($error_operation[0]), + ]); } } diff --git a/web/modules/contrib/pathauto/src/Form/PathautoSettingsForm.php b/web/modules/contrib/pathauto/src/Form/PathautoSettingsForm.php index cbf240798..64a7f6053 100644 --- a/web/modules/contrib/pathauto/src/Form/PathautoSettingsForm.php +++ b/web/modules/contrib/pathauto/src/Form/PathautoSettingsForm.php @@ -35,7 +35,7 @@ class PathautoSettingsForm extends ConfigFormBase { protected $aliasTypeManager; /** - * {@inheritDoc} + * {@inheritdoc} */ public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AliasTypeManager $alias_type_manager) { parent::__construct($config_factory); @@ -45,7 +45,7 @@ class PathautoSettingsForm extends ConfigFormBase { } /** - * {@inheritDoc} + * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( @@ -195,7 +195,13 @@ class PathautoSettingsForm extends ConfigFormBase { '#title' => $this->t('Strings to Remove'), '#default_value' => $config->get('ignore_words'), '#description' => $this->t('Words to strip out of the URL alias, separated by commas. Do not use this to remove punctuation.'), - '#wysiwyg' => FALSE, + ); + + $form['safe_tokens'] = array( + '#type' => 'textarea', + '#title' => $this->t('Safe tokens'), + '#default_value' => implode(', ', $config->get('safe_tokens')), + '#description' => $this->t('List of tokens that are safe to use in alias patterns and do not need to be cleaned. For example urls, aliases, machine names. Separated with a comma.'), ); $form['punctuation'] = array( @@ -254,6 +260,9 @@ class PathautoSettingsForm extends ConfigFormBase { } $value = $enabled_entity_types; } + elseif ($key == 'safe_tokens') { + $value = array_filter(array_map('trim', explode(',', $value))); + } $config->set($key, $value); } $config->save(); diff --git a/web/modules/contrib/pathauto/src/Form/PatternDisableForm.php b/web/modules/contrib/pathauto/src/Form/PatternDisableForm.php index c87b88b6a..1e1ac3f9c 100644 --- a/web/modules/contrib/pathauto/src/Form/PatternDisableForm.php +++ b/web/modules/contrib/pathauto/src/Form/PatternDisableForm.php @@ -44,7 +44,9 @@ class PatternDisableForm extends EntityConfirmFormBase { */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity->disable()->save(); - drupal_set_message($this->t('Disabled pattern %label.', array('%label' => $this->entity->label()))); + $this->messenger()->addMessage($this->t('Disabled pattern %label.', [ + '%label' => $this->entity->label(), + ])); $form_state->setRedirectUrl($this->getCancelUrl()); } diff --git a/web/modules/contrib/pathauto/src/Form/PatternEditForm.php b/web/modules/contrib/pathauto/src/Form/PatternEditForm.php index 49efc7e77..454c4a1b1 100644 --- a/web/modules/contrib/pathauto/src/Form/PatternEditForm.php +++ b/web/modules/contrib/pathauto/src/Form/PatternEditForm.php @@ -16,6 +16,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class PatternEditForm extends EntityForm { /** + * The alias type manager. + * * @var \Drupal\pathauto\AliasTypeManager */ protected $manager; @@ -33,11 +35,15 @@ class PatternEditForm extends EntityForm { protected $entityTypeBundleInfo; /** + * The entity manager service. + * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** + * The language manager service. + * * @var \Drupal\Core\Language\LanguageManagerInterface */ protected $languageManager; @@ -58,9 +64,13 @@ class PatternEditForm extends EntityForm { * PatternEditForm constructor. * * @param \Drupal\pathauto\AliasTypeManager $manager + * The alias type manager. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle info service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity manager service. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager service. */ function __construct(AliasTypeManager $manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) { $this->manager = $manager; @@ -70,7 +80,7 @@ class PatternEditForm extends EntityForm { } /** - * {@inheritDoc} + * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { @@ -199,7 +209,7 @@ class PatternEditForm extends EntityForm { } /** - * {@inheritDoc} + * {@inheritdoc} */ public function buildEntity(array $form, FormStateInterface $form_state) { /** @var \Drupal\pathauto\PathautoPatternInterface $entity */ @@ -259,11 +269,13 @@ class PatternEditForm extends EntityForm { } /** - * {@inheritDoc} + * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { parent::save($form, $form_state); - drupal_set_message($this->t('Pattern @label saved.', ['@label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Pattern %label saved.', [ + '%label' => $this->entity->label(), + ])); $form_state->setRedirectUrl($this->entity->toUrl('collection')); } diff --git a/web/modules/contrib/pathauto/src/Form/PatternEnableForm.php b/web/modules/contrib/pathauto/src/Form/PatternEnableForm.php index 253ca319e..18f136b55 100644 --- a/web/modules/contrib/pathauto/src/Form/PatternEnableForm.php +++ b/web/modules/contrib/pathauto/src/Form/PatternEnableForm.php @@ -44,7 +44,9 @@ class PatternEnableForm extends EntityConfirmFormBase { */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity->enable()->save(); - drupal_set_message($this->t('Enabled pattern %label.', array('%label' => $this->entity->label()))); + $this->messenger()->addMessage($this->t('Enabled pattern %label.', [ + '%label' => $this->entity->label(), + ])); $form_state->setRedirectUrl($this->getCancelUrl()); } diff --git a/web/modules/contrib/pathauto/src/PathautoFieldItemList.php b/web/modules/contrib/pathauto/src/PathautoFieldItemList.php index f8cde1697..300675ca9 100644 --- a/web/modules/contrib/pathauto/src/PathautoFieldItemList.php +++ b/web/modules/contrib/pathauto/src/PathautoFieldItemList.php @@ -7,7 +7,7 @@ use Drupal\path\Plugin\Field\FieldType\PathFieldItemList; class PathautoFieldItemList extends PathFieldItemList { /** - * @inheritDoc + * @{inheritdoc} */ protected function delegateMethod($method) { // @todo Workaround until this is fixed, see @@ -27,7 +27,7 @@ class PathautoFieldItemList extends PathFieldItemList { } /** - * @inheritDoc + * @{inheritdoc} */ protected function computeValue() { parent::computeValue(); diff --git a/web/modules/contrib/pathauto/src/PathautoGenerator.php b/web/modules/contrib/pathauto/src/PathautoGenerator.php index 6c56a5c34..d70ce3b6f 100644 --- a/web/modules/contrib/pathauto/src/PathautoGenerator.php +++ b/web/modules/contrib/pathauto/src/PathautoGenerator.php @@ -2,7 +2,6 @@ namespace Drupal\pathauto; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; @@ -87,12 +86,16 @@ class PathautoGenerator implements PathautoGeneratorInterface { protected $messenger; /** + * The token entity mapper. + * * @var \Drupal\token\TokenEntityMapperInterface */ protected $tokenEntityMapper; /** - * @var Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; @@ -115,10 +118,12 @@ class PathautoGenerator implements PathautoGeneratorInterface { * The messenger service. * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation * The string translation service. - * @param Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager + * @param \Drupal\token\TokenEntityMapperInterface $token_entity_mapper + * The token entity mapper. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ - public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, Token $token, AliasCleanerInterface $alias_cleaner, AliasStorageHelperInterface $alias_storage_helper, AliasUniquifierInterface $alias_uniquifier, MessengerInterface $messenger, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mappper, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, Token $token, AliasCleanerInterface $alias_cleaner, AliasStorageHelperInterface $alias_storage_helper, AliasUniquifierInterface $alias_uniquifier, MessengerInterface $messenger, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mapper, EntityTypeManagerInterface $entity_type_manager) { $this->configFactory = $config_factory; $this->moduleHandler = $module_handler; $this->token = $token; @@ -127,7 +132,7 @@ class PathautoGenerator implements PathautoGeneratorInterface { $this->aliasUniquifier = $alias_uniquifier; $this->messenger = $messenger; $this->stringTranslation = $string_translation; - $this->tokenEntityMapper = $token_entity_mappper; + $this->tokenEntityMapper = $token_entity_mapper; $this->entityTypeManager = $entity_type_manager; } @@ -165,8 +170,9 @@ class PathautoGenerator implements PathautoGeneratorInterface { 'bundle' => $entity->bundle(), 'language' => &$langcode, ); - // @todo Is still hook still useful? + $pattern_original = $pattern->getPattern(); $this->moduleHandler->alter('pathauto_pattern', $pattern, $context); + $pattern_altered = $pattern->getPattern(); // Special handling when updating an item which is already aliased. $existing_alias = NULL; @@ -209,7 +215,7 @@ class PathautoGenerator implements PathautoGeneratorInterface { $this->moduleHandler->alter('pathauto_alias', $alias, $context); // If we have arrived at an empty string, discontinue. - if (!Unicode::strlen($alias)) { + if (!mb_strlen($alias)) { return NULL; } @@ -236,11 +242,19 @@ class PathautoGenerator implements PathautoGeneratorInterface { 'language' => $langcode, ); - return $this->aliasStorageHelper->save($path, $existing_alias, $op); + $return = $this->aliasStorageHelper->save($path, $existing_alias, $op); + + // Because there is no way to set an altered pattern to not be cached, + // change it back to the original value. + if ($pattern_altered !== $pattern_original) { + $pattern->setPattern($pattern_original); + } + + return $return; } /** - * Loads pathauto patterns for a given entity type ID + * Loads pathauto patterns for a given entity type ID. * * @param string $entity_type_id * An entity type ID. @@ -335,7 +349,7 @@ class PathautoGenerator implements PathautoGeneratorInterface { $result = $this->createEntityAlias($entity, $op); } catch (\InvalidArgumentException $e) { - drupal_set_message($e->getMessage(), 'error'); + $this->messenger->addError($e->getMessage()); return NULL; } diff --git a/web/modules/contrib/pathauto/src/PathautoGeneratorInterface.php b/web/modules/contrib/pathauto/src/PathautoGeneratorInterface.php index 5880adbc6..337df3c53 100644 --- a/web/modules/contrib/pathauto/src/PathautoGeneratorInterface.php +++ b/web/modules/contrib/pathauto/src/PathautoGeneratorInterface.php @@ -49,6 +49,7 @@ interface PathautoGeneratorInterface { * * @param \Drupal\Core\Entity\EntityInterface $entity * An entity. + * * @return \Drupal\pathauto\PathautoPatternInterface|null */ public function getPatternByEntity(EntityInterface $entity); diff --git a/web/modules/contrib/pathauto/src/PathautoState.php b/web/modules/contrib/pathauto/src/PathautoState.php index 466db1c38..4e1f63bc3 100644 --- a/web/modules/contrib/pathauto/src/PathautoState.php +++ b/web/modules/contrib/pathauto/src/PathautoState.php @@ -88,6 +88,7 @@ class PathautoState extends TypedData { /** * Returns the key value collection that should be used for the given entity. + * * @return string */ protected function getCollection() { diff --git a/web/modules/contrib/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php b/web/modules/contrib/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php index cf7b4a2fa..c6b03311a 100644 --- a/web/modules/contrib/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php +++ b/web/modules/contrib/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php @@ -26,6 +26,8 @@ class EntityAliasTypeDeriver extends DeriverBase implements ContainerDeriverInte protected $entityTypeManager; /** + * The entity field manager. + * * @var \Drupal\Core\Entity\EntityFieldManagerInterface */ protected $entityFieldManager; @@ -44,7 +46,7 @@ class EntityAliasTypeDeriver extends DeriverBase implements ContainerDeriverInte * The entity field manager. * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation * The string translation service. - * @apram \Drupal\Token\TokenEntityMapperInterface $token_entity_mapper + * @param \Drupal\Token\TokenEntityMapperInterface $token_entity_mapper * The token entity mapper. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mapper) { diff --git a/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php b/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php index c612a2b6d..cdade9424 100644 --- a/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php +++ b/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php @@ -11,6 +11,7 @@ use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\ContextAwarePluginBase; +use Drupal\Core\Messenger\MessengerTrait; use Drupal\pathauto\AliasTypeBatchUpdateInterface; use Drupal\pathauto\AliasTypeInterface; use Drupal\pathauto\PathautoState; @@ -26,6 +27,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class EntityAliasTypeBase extends ContextAwarePluginBase implements AliasTypeInterface, AliasTypeBatchUpdateInterface, ContainerFactoryPluginInterface { + use MessengerTrait; + /** * The module handler service. * @@ -151,12 +154,15 @@ class EntityAliasTypeBase extends ContextAwarePluginBase implements AliasTypeInt case 'create': $query->isNull('ua.source'); break; + case 'update': $query->isNotNull('ua.source'); break; + case 'all': // Nothing to do. We want all paths. break; + default: // Unknown action. Abort! return; @@ -182,7 +188,7 @@ class EntityAliasTypeBase extends ContextAwarePluginBase implements AliasTypeInt $updates = $this->bulkUpdate($ids); $context['sandbox']['count'] += count($ids); - $context['sandbox']['current'] = max($ids); + $context['sandbox']['current'] = !empty($ids) ? max($ids) : 0; $context['results']['updates'] += $updates; $context['message'] = $this->t('Updated alias for %label @id.', array('%label' => $entity_type->getLabel(), '@id' => end($ids))); @@ -255,7 +261,7 @@ class EntityAliasTypeBase extends ContextAwarePluginBase implements AliasTypeInt * An optional array of additional options. * * @return int - * The number of updated URL aliases. + * The number of updated URL aliases. */ protected function bulkUpdate(array $ids, array $options = array()) { $options += array('message' => FALSE); @@ -274,7 +280,10 @@ class EntityAliasTypeBase extends ContextAwarePluginBase implements AliasTypeInt } if (!empty($options['message'])) { - drupal_set_message(\Drupal::translation()->formatPlural(count($ids), 'Updated 1 %label URL alias.', 'Updated @count %label URL aliases.'), array('%label' => $this->getLabel())); + $this->messenger->addMessage($this->translationManager + ->formatPlural(count($ids), 'Updated 1 %label URL alias.', 'Updated @count %label URL aliases.'), [ + '%label' => $this->getLabel(), + ]); } return $updates; @@ -330,5 +339,4 @@ class EntityAliasTypeBase extends ContextAwarePluginBase implements AliasTypeInt return $this; } - } diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoBulkUpdateTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoBulkUpdateTest.php index 2fa2194ab..1db84e9d3 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoBulkUpdateTest.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoBulkUpdateTest.php @@ -44,7 +44,7 @@ class PathautoBulkUpdateTest extends WebTestBase { protected $patterns; /** - * {inheritdoc} + * {@inheritdoc} */ function setUp() { parent::setUp(); @@ -103,7 +103,7 @@ class PathautoBulkUpdateTest extends WebTestBase { $this->assertText('No new URL aliases to generate.'); $this->assertNoEntityAliasExists($new_node); - // Make sure existing aliases can be overriden. + // Make sure existing aliases can be overridden. $this->drupalPostForm('admin/config/search/path/settings', ['update_action' => PathautoGeneratorInterface::UPDATE_ACTION_DELETE], t('Save configuration')); // Patterns did not change, so no aliases should be regenerated. @@ -111,16 +111,17 @@ class PathautoBulkUpdateTest extends WebTestBase { $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); $this->assertText('No new URL aliases to generate.'); - // Update the node pattern, and leave other patterns alone. Existing nodes should get a new alias, - // except the node above whose alias is manually set. Other aliases must be left alone. + // Update the node pattern, and leave other patterns alone. Existing nodes + // should get a new alias, except the node above whose alias is manually + // set. Other aliases must be left alone. $this->patterns['node']->delete(); $this->patterns['node'] = $this->createPattern('node', '/archive/node-[node:nid]'); $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); $this->assertText('Generated 5 URL aliases.'); - // Prevent existing aliases to be overriden. The bulk generate page should only offer - // to create an alias for paths which have none. + // Prevent existing aliases to be overridden. The bulk generate page should + // only offer to create an alias for paths which have none. $this->drupalPostForm('admin/config/search/path/settings', ['update_action' => PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW], t('Save configuration')); $this->drupalGet('admin/config/search/path/update_bulk'); diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoEnablingEntityTypesTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoEnablingEntityTypesTest.php index c8cc1fff9..acaa87be7 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoEnablingEntityTypesTest.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoEnablingEntityTypesTest.php @@ -13,6 +13,7 @@ use Drupal\comment\Tests\CommentTestTrait; class PathautoEnablingEntityTypesTest extends WebTestBase { use PathautoTestHelperTrait; + use CommentTestTrait; /** @@ -30,7 +31,7 @@ class PathautoEnablingEntityTypesTest extends WebTestBase { protected $adminUser; /** - * {inheritdoc} + * {@inheritdoc} */ function setUp() { parent::setUp(); diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoMassDeleteTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoMassDeleteTest.php index 93f4ca32d..691e5a89c 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoMassDeleteTest.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoMassDeleteTest.php @@ -49,9 +49,8 @@ class PathautoMassDeleteTest extends WebTestBase { */ protected $terms; - /** - * {inheritdoc} + * {@inheritdoc} */ function setUp() { parent::setUp(); @@ -136,14 +135,16 @@ class PathautoMassDeleteTest extends WebTestBase { * Helper function to generate aliases. */ function generateAliases() { - // Delete all aliases to avoid duplicated aliases. They will be recreated below. + // Delete all aliases to avoid duplicated aliases. They will be recreated + // below. $this->deleteAllAliases(); // We generate a bunch of aliases for nodes, users and taxonomy terms. If // the entities are already created we just update them, otherwise we create // them. if (empty($this->nodes)) { - // Create a large number of nodes (100+) to make sure that the batch code works. + // Create a large number of nodes (100+) to make sure that the batch code + // works. for ($i = 1; $i <= 105; $i++) { // Set the alias of two nodes manually. $settings = ($i > 103) ? ['path' => ['alias' => "/custom_alias_$i", 'pathauto' => PathautoState::SKIP]] : []; diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoNodeWebTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoNodeWebTest.php index 78213d2bc..a9376f7ac 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoNodeWebTest.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoNodeWebTest.php @@ -1,6 +1,7 @@ <?php namespace Drupal\pathauto\Tests; + use Drupal\pathauto\Entity\PathautoPattern; use Drupal\node\Entity\Node; use Drupal\pathauto\PathautoState; @@ -30,7 +31,7 @@ class PathautoNodeWebTest extends WebTestBase { protected $adminUser; /** - * {inheritdoc} + * {@inheritdoc} */ function setUp() { parent::setUp(); @@ -56,7 +57,8 @@ class PathautoNodeWebTest extends WebTestBase { * Tests editing nodes with different settings. */ function testNodeEditing() { - // Ensure that the Pathauto checkbox is checked by default on the node add form. + // Ensure that the Pathauto checkbox is checked by default on the node add + // form. $this->drupalGet('node/add/page'); $this->assertFieldChecked('edit-path-0-pathauto'); diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoSettingsFormWebTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoSettingsFormWebTest.php index 40ede78bc..cbcb6b856 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoSettingsFormWebTest.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoSettingsFormWebTest.php @@ -85,7 +85,7 @@ class PathautoSettingsFormWebTest extends WebTestBase { ); /** - * {inheritdoc} + * {@inheritdoc} */ function setUp() { parent::setUp(); diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoTaxonomyWebTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoTaxonomyWebTest.php index e1a7e88a1..3b92569f7 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoTaxonomyWebTest.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoTaxonomyWebTest.php @@ -27,7 +27,7 @@ class PathautoTaxonomyWebTest extends WebTestBase { protected $adminUser; /** - * {inheritdoc} + * {@inheritdoc} */ function setUp() { parent::setUp(); @@ -45,7 +45,6 @@ class PathautoTaxonomyWebTest extends WebTestBase { $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]'); } - /** * Basic functional testing of Pathauto with taxonomy terms. */ diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoTestHelperTrait.php b/web/modules/contrib/pathauto/src/Tests/PathautoTestHelperTrait.php index 5fc6b4070..8c5c97045 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoTestHelperTrait.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoTestHelperTrait.php @@ -2,7 +2,6 @@ namespace Drupal\pathauto\Tests; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\Language; use Drupal\Core\Render\BubbleableMetadata; @@ -34,7 +33,7 @@ trait PathautoTestHelperTrait { $type = ($entity_type_id == 'forum') ? 'forum' : 'canonical_entities:' . $entity_type_id; $pattern = PathautoPattern::create([ - 'id' => Unicode::strtolower($this->randomMachineName()), + 'id' => mb_strtolower($this->randomMachineName()), 'type' => $type, 'pattern' => $pattern, 'weight' => $weight, @@ -51,7 +50,7 @@ trait PathautoTestHelperTrait { * @param string $entity_type * The entity type ID. * @param string $bundle - * The bundle + * The bundle. */ protected function addBundleCondition(PathautoPatternInterface $pattern, $entity_type, $bundle) { $plugin_id = $entity_type == 'node' ? 'node_type' : 'entity_bundle:' . $entity_type; @@ -142,10 +141,11 @@ trait PathautoTestHelperTrait { /** * @param array $values + * * @return \Drupal\taxonomy\VocabularyInterface */ public function addVocabulary(array $values = array()) { - $name = Unicode::strtolower($this->randomMachineName(5)); + $name = mb_strtolower($this->randomMachineName(5)); $values += array( 'name' => $name, 'vid' => $name, @@ -158,7 +158,7 @@ trait PathautoTestHelperTrait { public function addTerm(VocabularyInterface $vocabulary, array $values = array()) { $values += array( - 'name' => Unicode::strtolower($this->randomMachineName(5)), + 'name' => mb_strtolower($this->randomMachineName(5)), 'vid' => $vocabulary->id(), ); diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoUiTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoUiTest.php index 9d461b5f5..b6b9e6643 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoUiTest.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoUiTest.php @@ -30,7 +30,7 @@ class PathautoUiTest extends WebTestBase { protected $adminUser; /** - * {inheritdoc} + * {@inheritdoc} */ function setUp() { parent::setUp(); diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoUserWebTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoUserWebTest.php index 54832df9d..8d73a3f34 100644 --- a/web/modules/contrib/pathauto/src/Tests/PathautoUserWebTest.php +++ b/web/modules/contrib/pathauto/src/Tests/PathautoUserWebTest.php @@ -1,7 +1,7 @@ <?php namespace Drupal\pathauto\Tests; -use Drupal\Component\Utility\Unicode; + use Drupal\simpletest\WebTestBase; use Drupal\views\Views; @@ -29,7 +29,7 @@ class PathautoUserWebTest extends WebTestBase { protected $adminUser; /** - * {inheritdoc} + * {@inheritdoc} */ function setUp() { parent::setUp(); @@ -47,7 +47,6 @@ class PathautoUserWebTest extends WebTestBase { $this->createPattern('user', '/users/[user:name]'); } - /** * Basic functional testing of Pathauto with users. */ @@ -71,7 +70,6 @@ class PathautoUserWebTest extends WebTestBase { $view->initDisplay(); $view->preview('page_1'); - foreach ($view->result as $key => $row) { if ($view->field['name']->getValue($row) == $account->getUsername()) { break; @@ -85,7 +83,7 @@ class PathautoUserWebTest extends WebTestBase { $this->drupalPostForm('admin/people', $edit, t('Apply to selected items')); $this->assertText('Update URL alias was applied to 1 item.'); - $this->assertEntityAlias($account, '/users/' . Unicode::strtolower($account->getUsername())); + $this->assertEntityAlias($account, '/users/' . mb_strtolower($account->getUsername())); $this->assertEntityAlias($this->adminUser, '/user/' . $this->adminUser->id()); } diff --git a/web/modules/contrib/pathauto/src/VerboseMessenger.php b/web/modules/contrib/pathauto/src/VerboseMessenger.php index bc60b5c90..a151b9507 100644 --- a/web/modules/contrib/pathauto/src/VerboseMessenger.php +++ b/web/modules/contrib/pathauto/src/VerboseMessenger.php @@ -3,6 +3,7 @@ namespace Drupal\pathauto; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Messenger\MessengerInterface as CoreMessengerInterface; use Drupal\Core\Session\AccountInterface; /** @@ -31,12 +32,27 @@ class VerboseMessenger implements MessengerInterface { */ protected $account; + /** + * The messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Creates a verbose messenger. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Session\AccountInterface $account + * The current user account. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. */ - public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $account) { + public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $account, CoreMessengerInterface $messenger) { $this->configFactory = $config_factory; $this->account = $account; + $this->messenger = $messenger; } /** @@ -54,7 +70,7 @@ class VerboseMessenger implements MessengerInterface { } if ($message) { - drupal_set_message($message); + $this->messenger->addMessage($message); } return TRUE; diff --git a/web/modules/contrib/pathauto/tests/modules/pathauto_string_id_test/pathauto_string_id_test.info.yml b/web/modules/contrib/pathauto/tests/modules/pathauto_string_id_test/pathauto_string_id_test.info.yml index 8601e9e35..c95b45a7d 100644 --- a/web/modules/contrib/pathauto/tests/modules/pathauto_string_id_test/pathauto_string_id_test.info.yml +++ b/web/modules/contrib/pathauto/tests/modules/pathauto_string_id_test/pathauto_string_id_test.info.yml @@ -6,8 +6,8 @@ package: Testing dependencies: - token -# Information added by Drupal.org packaging script on 2018-04-22 -version: '8.x-1.2' +# Information added by Drupal.org packaging script on 2018-09-08 +version: '8.x-1.3' core: '8.x' project: 'pathauto' -datestamp: 1524421087 +datestamp: 1536407890 diff --git a/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml b/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml index e63e50b5d..c11eb783a 100644 --- a/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml +++ b/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml @@ -7,8 +7,8 @@ package: Testing dependencies: - views -# Information added by Drupal.org packaging script on 2018-04-22 -version: '8.x-1.2' +# Information added by Drupal.org packaging script on 2018-09-08 +version: '8.x-1.3' core: '8.x' project: 'pathauto' -datestamp: 1524421087 +datestamp: 1536407890 diff --git a/web/modules/contrib/pathauto/tests/src/Kernel/PathautoEntityWithStringIdTest.php b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoEntityWithStringIdTest.php index f134d6666..e72f42d40 100644 --- a/web/modules/contrib/pathauto/tests/src/Kernel/PathautoEntityWithStringIdTest.php +++ b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoEntityWithStringIdTest.php @@ -61,7 +61,7 @@ class PathautoEntityWithStringIdTest extends KernelTestBase { protected function setUp() { parent::setUp(); $this->installSchema('system', ['key_value']); - $this->installConfig(['system']); + $this->installConfig(['system', 'pathauto']); $this->installEntitySchema('pathauto_string_id_test'); $this->createPattern('pathauto_string_id_test', '/[pathauto_string_id_test:name]'); /** @var \Drupal\pathauto\AliasTypeManager $alias_type_manager */ @@ -87,7 +87,7 @@ class PathautoEntityWithStringIdTest extends KernelTestBase { $entity->save(); // Check that the path was generated. - $this->assertEntityAlias($entity, "/$name"); + $this->assertEntityAlias($entity, mb_strtolower("/$name")); // Check that the path auto state was saved with the expected key. $value = \Drupal::keyValue('pathauto_state.pathauto_string_id_test')->get($expected_key); $this->assertEquals(PathautoState::CREATE, $value); diff --git a/web/modules/contrib/pathauto/tests/src/Kernel/PathautoKernelTest.php b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoKernelTest.php index 1d5db68b2..1fffa4d08 100644 --- a/web/modules/contrib/pathauto/tests/src/Kernel/PathautoKernelTest.php +++ b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoKernelTest.php @@ -3,7 +3,6 @@ namespace Drupal\Tests\pathauto\Kernel; use Drupal\Component\Utility\Html; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageInterface; use Drupal\language\Entity\ConfigurableLanguage; @@ -285,7 +284,7 @@ class PathautoKernelTest extends KernelTestBase { $this->assertEntityAlias($node, '/content/second-title'); $this->assertNoAliasExists(array('alias' => '/content/first-title')); - // Test PATHAUTO_UPDATE_ACTION_LEAVE + // Test PATHAUTO_UPDATE_ACTION_LEAVE. $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_LEAVE); $config->save(); $node->setTitle('Third title'); @@ -323,8 +322,8 @@ class PathautoKernelTest extends KernelTestBase { } /** - * Test that \Drupal::service('pathauto.generator')->createEntityAlias() will not create an alias for a pattern - * that does not get any tokens replaced. + * Test that \Drupal::service('pathauto.generator')->createEntityAlias() will + * not create an alias for a pattern that does not get any tokens replaced. */ public function testNoTokensNoAlias() { $this->installConfig(['filter']); @@ -365,12 +364,11 @@ class PathautoKernelTest extends KernelTestBase { function testParentChildPathTokens() { // First create a field which will be used to create the path. It must // begin with a letter. - $this->installEntitySchema('taxonomy_term'); Vocabulary::create(['vid' => 'tags'])->save(); - $fieldname = 'a' . Unicode::strtolower($this->randomMachineName()); + $fieldname = 'a' . mb_strtolower($this->randomMachineName()); $field_storage = FieldStorageConfig::create(['entity_type' => 'taxonomy_term', 'field_name' => $fieldname, 'type' => 'string']); $field_storage->save(); $field = FieldConfig::create(['field_storage' => $field_storage, 'bundle' => 'tags']); @@ -391,11 +389,11 @@ class PathautoKernelTest extends KernelTestBase { // Create the child term. $child = Term::create(['vid' => 'tags', $fieldname => $this->randomMachineName(), 'parent' => $parent, 'name' => $this->randomMachineName()]); $child->save(); - $this->assertEntityAlias($child, '/' . Unicode::strtolower($parent->getName() . '/' . $child->$fieldname->value)); + $this->assertEntityAlias($child, '/' . mb_strtolower($parent->getName() . '/' . $child->$fieldname->value)); // Re-saving the parent term should not modify the child term's alias. $parent->save(); - $this->assertEntityAlias($child, '/' . Unicode::strtolower($parent->getName() . '/' . $child->$fieldname->value)); + $this->assertEntityAlias($child, '/' . mb_strtolower($parent->getName() . '/' . $child->$fieldname->value)); } /** @@ -506,7 +504,7 @@ class PathautoKernelTest extends KernelTestBase { } /** - * Tests that enabled entity types genrates the necessary fields and plugins. + * Tests that enabled entity types generates the necessary fields and plugins. */ public function testSettingChangeInvalidatesCache() { diff --git a/web/modules/contrib/pathauto/tests/src/Kernel/PathautoTokenTest.php b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoTokenTest.php index 30c5ad1d2..1a05007f6 100644 --- a/web/modules/contrib/pathauto/tests/src/Kernel/PathautoTokenTest.php +++ b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoTokenTest.php @@ -39,6 +39,42 @@ class PathautoTokenTest extends KernelTestBase { $alias_cleaner = \Drupal::service('pathauto.alias_cleaner'); $alias_cleaner->cleanTokenValues($replacements, $data, array()); $this->assertEqual($replacements['[array:join-path]'], 'test-first-arg/array-value'); + + // Test additional token cleaning and its configuration. + $safe_tokens = $this->config('pathauto.settings')->get('safe_tokens'); + $safe_tokens[] = 'safe'; + $this->config('pathauto.settings') + ->set('safe_tokens', $safe_tokens) + ->save(); + + $safe_tokens = [ + '[example:path]', + '[example:url]', + '[example:url-brief]', + '[example:login-url]', + '[example:login-url:relative]', + '[example:url:relative]', + '[example:safe]', + ]; + $unsafe_tokens = [ + '[example:path_part]', + '[example:something_url]', + '[example:unsafe]', + ]; + foreach ($safe_tokens as $token) { + $replacements = [ + $token => 'this/is/a/path', + ]; + $alias_cleaner->cleanTokenValues($replacements); + $this->assertEquals('this/is/a/path', $replacements[$token], "Token $token cleaned."); + } + foreach ($unsafe_tokens as $token) { + $replacements = [ + $token => 'This is not a / path', + ]; + $alias_cleaner->cleanTokenValues($replacements); + $this->assertEquals('not-path', $replacements[$token], "Token $token not cleaned."); + } } /** diff --git a/web/modules/contrib/pathauto/tests/src/Unit/VerboseMessengerTest.php b/web/modules/contrib/pathauto/tests/src/Unit/VerboseMessengerTest.php index 9567dd54b..13c5c058e 100644 --- a/web/modules/contrib/pathauto/tests/src/Unit/VerboseMessengerTest.php +++ b/web/modules/contrib/pathauto/tests/src/Unit/VerboseMessengerTest.php @@ -1,59 +1,54 @@ <?php -namespace Drupal\Tests\pathauto\Unit { +namespace Drupal\Tests\pathauto\Unit; - use Drupal\pathauto\VerboseMessenger; - use Drupal\Tests\UnitTestCase; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\pathauto\VerboseMessenger; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\pathauto\VerboseMessenger + * @group pathauto + */ +class VerboseMessengerTest extends UnitTestCase { /** - * @coversDefaultClass \Drupal\pathauto\VerboseMessenger - * @group pathauto + * The messenger under test. + * + * @var \Drupal\pathauto\VerboseMessenger */ - class VerboseMessengerTest extends UnitTestCase { - - /** - * The messenger under test. - * - * @var \Drupal\pathauto\VerboseMessenger - */ - protected $messenger; - - /** - * {@inheritdoc} - */ - protected function setUp() { - $config_factory = $this->getConfigFactoryStub(array('pathauto.settings' => array('verbose' => TRUE))); - $account = $this->getMock('\Drupal\Core\Session\AccountInterface'); - $account->expects($this->once()) - ->method('hasPermission') - ->withAnyParameters() - ->willReturn(TRUE); - - $this->messenger = new VerboseMessenger($config_factory, $account); - } - - /** - * Tests add messages. - * @covers ::addMessage - */ - public function testAddMessage() { - $this->assertTrue($this->messenger->addMessage("Test message"), "The message was added"); - } - - /** - * @covers ::addMessage - */ - public function testDoNotAddMessageWhileBulkupdate() { - $this->assertFalse($this->messenger->addMessage("Test message", "bulkupdate"), "The message was NOT added"); - } + protected $messenger; -} + /** + * {@inheritdoc} + */ + protected function setUp() { + $config_factory = $this->getConfigFactoryStub(['pathauto.settings' => ['verbose' => TRUE]]); + $account = $this->createMock(AccountInterface::class); + $account->expects($this->once()) + ->method('hasPermission') + ->withAnyParameters() + ->willReturn(TRUE); + $messenger = $this->createMock(MessengerInterface::class); + + $this->messenger = new VerboseMessenger($config_factory, $account, $messenger); + } -} -namespace { - // @todo Delete after https://drupal.org/node/1858196 is in. - if (!function_exists('drupal_set_message')) { - function drupal_set_message() { - } + /** + * Tests add messages. + * + * @covers ::addMessage + */ + public function testAddMessage() { + $this->assertTrue($this->messenger->addMessage("Test message"), "The message was added"); + } + + /** + * @covers ::addMessage + */ + public function testDoNotAddMessageWhileBulkupdate() { + $this->assertFalse($this->messenger->addMessage("Test message", "bulkupdate"), "The message was NOT added"); } + } diff --git a/web/modules/contrib/permissions_by_term/.gitignore b/web/modules/contrib/permissions_by_term/.gitignore index 6011ac4dd..fe4e7284e 100644 --- a/web/modules/contrib/permissions_by_term/.gitignore +++ b/web/modules/contrib/permissions_by_term/.gitignore @@ -6,3 +6,5 @@ js/node_modules npm-debug.log # Ignore Mac DS_Store files .DS_Store +js/babel-compiled +js/babel-compiled-test \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/bitbucket-pipelines.yml b/web/modules/contrib/permissions_by_term/bitbucket-pipelines.yml new file mode 100644 index 000000000..b283196fa --- /dev/null +++ b/web/modules/contrib/permissions_by_term/bitbucket-pipelines.yml @@ -0,0 +1,53 @@ +# This is a sample build configuration for PHP. +# Check our guides at https://confluence.atlassian.com/x/e8YWN for more examples. +# Only use spaces to indent your .yml configuration. +# ----- +# You can specify a custom docker image from Docker Hub as your build environment. +image: derh4nnes/pipeline-behat:latest +clone: + depth: full + +pipelines: + default: + - step: + name: Build and test + caches: + - composer + script: + - apt-get update + - apt-get install -y screen php7.1-sqlite libsqlite3-dev sqlite3 + - curl -sL https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh -o install_nvm.sh + - bash install_nvm.sh + - source ~/.profile + - nvm install 9.11.1 + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - cd .. + - mkdir permissions_by_term/ + - mv build/* permissions_by_term/ + - mv permissions_by_term/ build/ + - cd build + - composer create-project drupal-composer/drupal-project:8.x-dev drupal --stability dev --no-interaction + - (cd drupal && composer require behat/behat:^3.0 behat/mink-extension:^2.2 drupal/drupal-extension:^3.2 --dev) + - mv permissions_by_term drupal/web/modules/ + - (cd drupal/web/modules/permissions_by_term/tests/ && ../../../../vendor/bin/phpunit --testdox) + - (cd drupal/web/modules/permissions_by_term/js/ && npm install && npm run bat) + - (cd drupal/web && ../vendor/drush/drush/drush si standard --db-url=sqlite://sites/default/files/db.sqlite -y && ../vendor/drush/drush/drush en permissions_by_term -y) + - (cd drupal && cp web/modules/permissions_by_term/tests/src/Behat/composer-require-namespace.php . && php composer-require-namespace.php && composer dump-autoload) + - (cd drupal && cat composer.json) + - screen -dmS php-server php -S localhost:8000 -t ./drupal/web + - (cd drupal/web && cp modules/permissions_by_term/tests/src/Behat/behat.yml.dist behat.yml && ../vendor/bin/behat) + services: + - testing + - step: + name: Deploy + script: + - apt-get update && apt-get install -y unzip git + - git remote add d.o jepSter@git.drupal.org:project/permissions_by_term.git + - git config --global push.default simple + - git config --global user.name "Peter Majmesku" + - git push -u d.o + - git push --tags +definitions: + services: + testing: + image: darksolar/selenium-chrome-headless diff --git a/web/modules/contrib/permissions_by_term/js/.babelrc b/web/modules/contrib/permissions_by_term/js/.babelrc new file mode 100644 index 000000000..fe265fd5d --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/.babelrc @@ -0,0 +1,12 @@ +{ + "ignore": [ + "node_modules", + "package.json", + "package-json.lock", + "drupal-behavior-function", + "webpack-dist" + ], + "plugins": [ + "transform-es2015-modules-commonjs" + ] +} \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/README.md b/web/modules/contrib/permissions_by_term/js/README.md index 63840ca08..b48eed539 100644 --- a/web/modules/contrib/permissions_by_term/js/README.md +++ b/web/modules/contrib/permissions_by_term/js/README.md @@ -1,2 +1,29 @@ -JavaScript code has been tested with [Jasmine](https://jasmine.github.io/). Install the dependencies by [Yarn](https://yarnpkg.com/lang/en/). -Just run `yarn`. To run the tests, switch to the `js` folder and run `yarn test`. \ No newline at end of file +# Folder structure + +## NPM +NPM in version 5.6.0 and NodeJS in version 9.11.1 are proven in building the JavaScript code. The build assumes a +*NIX environment (Mac or Linux). + +## babel-compiled +Files which are needed for tests in cli. Because [Babel](https://babeljs.io/) is transpiling the modern ES6 import +statement to CommonJS, which can be parsed by NodeJS. + +## node-modules +Third-party JavaScript libraries. + +## src +The application code. + +## test +Automated tests. + +They can be executed via command line ("bat" stands for "build and test"): + +`npm run bat` + + Or they can be executed via the webbrowser by opening the .html files. Google Chrome + is good for JavaScript debugging. Therefor this option is provided by the QUnit JavaScript + testing tool. + +## Building the application +`npm run build` \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/node-form.js b/web/modules/contrib/permissions_by_term/js/node-form.js deleted file mode 100644 index a2d6d6db2..000000000 --- a/web/modules/contrib/permissions_by_term/js/node-form.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * @file - * Info behaviors on node edit form. - */ - -(function ($, window) { - - 'use strict'; - - if ($("#edit-permissions-by-term-info").length > 0) { - - var relationFieldsPathByContentType = "/admin/permissions-by-term/access-info-by-content-type/", - relationFieldsPathByUrl = "/admin/permissions-by-term/access-info-by-url?url="; - - /** - * @type {Drupal~behavior} - */ - Drupal.behaviors.nodeForm = { - attach: function () { - - var contentType = getContentType(), - getFormInfo = null; - - if (contentType !== null) { - getFormInfo = $.get(relationFieldsPathByContentType + contentType); - } else { - getFormInfo = $.get(relationFieldsPathByUrl + window.location.pathname); - } - - $.when(getFormInfo).done(function(formInfo){ - - if (formInfo['taxonomyRelationFieldNames'] !== null) { - - var nodeForm = new NodeForm($), - fieldWrapperCSSClasses = nodeForm.computeFieldWrapperCSSClasses(formInfo['taxonomyRelationFieldNames']); - - initPermissionInfoByFormElements(nodeForm, fieldWrapperCSSClasses, formInfo); - - for (var index = 0; index < fieldWrapperCSSClasses.length; ++index) { - - var formElementCssClass = fieldWrapperCSSClasses[index]; - - nodeForm.addFormElementCssClass(formElementCssClass); - - $(formElementCssClass + ' select').change(function (){ - nodeForm.displayPermissionsBySelect(fieldWrapperCSSClasses, formInfo['permissions']); - }); - - $(formElementCssClass + ' input[type="text"]').on('autocomplete-select', function (){ - nodeForm.displayPermissionsByAutocomplete(fieldWrapperCSSClasses, formInfo['permissions']); - }); - - $(formElementCssClass + ' input[type="text"]').on('keyup', function (){ - nodeForm.displayPermissionsByAutocomplete(fieldWrapperCSSClasses, formInfo['permissions']); - }); - - $(formElementCssClass + ' input[type="checkbox"]').change(function (){ - nodeForm.displayPermissionsByCheckbox($(this).prop('value'), $(this).prop('checked'), formInfo['permissions']); - }); - } - } - - }); - - function initPermissionInfoByFormElements(nodeForm, fieldWrapperCSSClasses, formInfo) { - nodeForm.displayPermissionsBySelect(fieldWrapperCSSClasses, formInfo['permissions']); - nodeForm.displayPermissionsByAutocomplete(fieldWrapperCSSClasses, formInfo['permissions']); - nodeForm.displayPermissionsByCheckbox($(this).prop('value'), $(this).prop('checked'), formInfo['permissions']); - } - - function getContentType() { - if (window.location.href.indexOf('/node/add') !== -1) { - return window.location.href.split("/").pop(); - } - - return null; - } - - } - }; - - if (Drupal.autocomplete) { - /** - * Handles an auto-complete select event. - * - * Override the autocomplete method to add a custom event. Overriding is - * happening to get full input. - * - * @param {jQuery.Event} event - * The event triggered. - * @param {object} ui - * The jQuery UI settings object. - * - * @return {boolean} - * Returns false to indicate the event status. - */ - Drupal.autocomplete.options.select = function selectHandler(event, ui) { - var terms = Drupal.autocomplete.splitValues(event.target.value); - // Remove the current input. - terms.pop(); - // Add the selected item. - if (ui.item.value.search(',') > 0) { - terms.push('"' + ui.item.value + '"'); - } - else { - terms.push(ui.item.value); - } - event.target.value = terms.join(', '); - // Fire custom event that other controllers can listen to. - jQuery(event.target).trigger('autocomplete-select'); - - // Return false to tell jQuery UI that we've filled in the value already. - return false; - } - } - } - -})(jQuery, window); diff --git a/web/modules/contrib/permissions_by_term/js/node-form.prototype.js b/web/modules/contrib/permissions_by_term/js/node-form.prototype.js deleted file mode 100644 index a67037cd3..000000000 --- a/web/modules/contrib/permissions_by_term/js/node-form.prototype.js +++ /dev/null @@ -1,242 +0,0 @@ -var NodeForm = function($){ - this.jQuery = $; - this.selectedTids = []; - this.formElementCssClasses = []; -}; - -NodeForm.prototype.getSelectedTids = function() { - var tids = []; - - for (var index = 0; index < this.formElementCssClasses.length; ++index) { - if (this.selectedTids[this.formElementCssClasses[index]] !== undefined && this.selectedTids[this.formElementCssClasses[index]].constructor === Array) { - - this.selectedTids[this.formElementCssClasses[index]].forEach(function(tid){ - tids.push(tid); - }) - } - } - - return tids; -} - -NodeForm.prototype.addFormElementCssClass = function(formElementCssClass) { - this.formElementCssClasses.push(formElementCssClass); -} - - -NodeForm.prototype.keyExists = function(key, array) { - if (!array || (array.constructor !== Array && array.constructor !== Object)) { - return false; - } - for (var i = 0; i < array.length; i++) { - if (array[i] === key) { - return true; - } - } - return key in array; -} - -NodeForm.prototype.addSelectedTid = function(tid, formElementCssClass) { - if (!this.keyExists(formElementCssClass, this.formElementCssClasses)) { - this.formElementCssClasses.push(formElementCssClass); - } - - if (this.selectedTids[formElementCssClass] === undefined) { - - this.selectedTids[formElementCssClass] = []; - } - - this.selectedTids[formElementCssClass].push(tid); -} - -NodeForm.prototype.removeTid = function(value, formElementCssClass) { - const index = this.selectedTids[formElementCssClass].indexOf(parseInt(value)); - - if (index !== -1) { - this.selectedTids[formElementCssClass].splice(index, 1); - } -} - -NodeForm.prototype.resetData = function(formElementCssClass) { - this.selectedTids[formElementCssClass] = []; -} - -NodeForm.prototype.computeFieldWrapperCSSClasses = function(fieldNames) { - var wrapperCssClasses = []; - - for (var index = 0; index < fieldNames.length; ++index) { - var fieldWrapperClass = '.field--name-' + fieldNames[index].replace(/_/g, '-'); - - wrapperCssClasses.push(fieldWrapperClass); - } - - return wrapperCssClasses; -} - -NodeForm.prototype.displayPermissionsByCheckbox = function(tid, checked, permissions) { - if (checked === false) { - this.resetData('checkbox_tid_' + tid); - } else if (checked === true){ - this.addSelectedTid(parseInt(tid), 'checkbox_tid_' + tid); - } - - this.renderPermissionsInfo(permissions); -} - -NodeForm.prototype.displayPermissionsBySelect = function(fieldWrapperCSSClasses, permissions) { - for (var index = 0; index < fieldWrapperCSSClasses.length; ++index) { - var inputTypes = ['select', 'input']; - - var fieldWrapperCSSClass = fieldWrapperCSSClasses[index]; - - for (var inputTypesIndex = 0; inputTypesIndex <= inputTypes.length; inputTypesIndex++) { - var values = this.jQuery(fieldWrapperCSSClass + ' select').val(); - - if (values !== undefined && values !== null && values.constructor === Array) { - if (values[0] === '_none') { - this.resetData(fieldWrapperCSSClass); - } - - for (var i = 0; i < values.length; ++i) { - if (isNaN(values[i]) === false) { - this.addSelectedTid(parseInt(values[i]), fieldWrapperCSSClass); - } - } - } - - } - - } - - this.renderPermissionsInfo(permissions); - -} - -NodeForm.prototype.displayPermissionsByAutocomplete = function(fieldWrapperCSSClasses, permissions) { - for (var index = 0; index < fieldWrapperCSSClasses.length; ++index) { - var fieldWrapperCSSClass = fieldWrapperCSSClasses[index]; - - var values = this.jQuery(fieldWrapperCSSClass + ' input').val(); - - this.resetData(fieldWrapperCSSClass); - - if (values !== undefined && values.indexOf('(') !== -1 && values.indexOf(')')) { - - var tidsInBrackets = values.match(/\(\d+\)/g); - - if (tidsInBrackets !== undefined && tidsInBrackets !== null && tidsInBrackets.constructor === Array) { - - for (var i = 0; i < tidsInBrackets.length; ++i) { - var selectedTid = parseInt(tidsInBrackets[i].replace('(', '').replace(')', '')); - this.addSelectedTid(selectedTid, fieldWrapperCSSClass); - } - - } - - } - - } - - this.renderPermissionsInfo(permissions); - -} - -NodeForm.prototype.separateByComma = function(values) { - return values.join(', '); -} - -NodeForm.prototype.renderPermissionsInfo = function(permissions) { - - var permissionsToDisplay = this.getPermissionsByTids(this.getSelectedTids(), permissions); - - var allowedUsersHtml = '<b>' + Drupal.t('Allowed users:') + '</b> '; - - if (this.isAllowedUsersRestriction(permissionsToDisplay)) { - allowedUsersHtml += this.separateByComma(permissionsToDisplay['permittedUsers']); - } else { - allowedUsersHtml += '<i>' + Drupal.t('No user restrictions.') + '</i>'; - } - - var allowedRolesHtml = '<b>' + Drupal.t('Allowed roles:') + '</b> '; - - if (this.isAllowedRolesRestriction(permissionsToDisplay)) { - allowedRolesHtml += this.separateByComma(permissionsToDisplay['permittedRoles']); - } else { - allowedRolesHtml += '<i>' + Drupal.t('No role restrictions.') + '</i>';; - } - - var generalInfoText = Drupal.t("This widget shows information about taxonomy term related permissions. It's being updated, as soon you make any related changes in the form."); - - this.jQuery('#edit-permissions-by-term-info .form-type-item').html(generalInfoText + '<br /><br />' + allowedUsersHtml + '<br />' + allowedRolesHtml); -} - -NodeForm.prototype.isAllowedUsersRestriction = function(permissionsToDisplay) { - if (permissionsToDisplay['permittedUsers'].length > 0 && permissionsToDisplay['permittedUsers'] !== null) { - return true; - } - - return false; -} - -NodeForm.prototype.isAllowedRolesRestriction = function(permissionsToDisplay) { - if (permissionsToDisplay['permittedRoles'].length > 0 && permissionsToDisplay['permittedRoles'] !== null) { - return true; - } - - return false; -} - -NodeForm.prototype.pushUserDisplayNames = function(tids, permissionsToDisplay, permissions) { - for (var index = 0; index < tids.length; ++index) { - if (permissions.hasOwnProperty('userDisplayNames') && permissions['userDisplayNames'].hasOwnProperty(tids[index]) && permissions['userDisplayNames'][tids[index]] !== null && - permissionsToDisplay['permittedUsers'].indexOf(permissions['userDisplayNames'][tids[index]]) === -1) { - - var userDisplayNames = permissions['userDisplayNames'][tids[index]]; - - if (userDisplayNames.constructor === Array && userDisplayNames.length > 1) { - userDisplayNames.forEach(function(value){ - if (permissionsToDisplay['permittedUsers'].indexOf(value) === -1) { - permissionsToDisplay['permittedUsers'].push(value); - } - }); - } else { - if (permissionsToDisplay['permittedUsers'].indexOf(userDisplayNames) === -1) { - permissionsToDisplay['permittedUsers'].push(userDisplayNames); - } - } - } - } - - return permissionsToDisplay; -} - -NodeForm.prototype.pushRoles = function(tids, permissionsToDisplay, permissions) { - for (var index = 0; index < tids.length; ++index) { - - if (permissions['roleLabels'] === undefined) { - return permissionsToDisplay; - } - - if (permissions['roleLabels'][tids[index]] !== undefined && permissions['roleLabels'][tids[index]] !== null) { - permissions['roleLabels'][tids[index]].forEach(function(role){ - if (permissionsToDisplay['permittedRoles'].indexOf(role) === -1) { - permissionsToDisplay['permittedRoles'].push(role); - } - }); - } - } - - return permissionsToDisplay; -} - -NodeForm.prototype.getPermissionsByTids = function(tids, permissions) { - var permissionsToDisplay = { - permittedUsers: [], - permittedRoles: [] - }; - - permissionsToDisplay = this.pushRoles(tids, permissionsToDisplay, permissions); - permissionsToDisplay = this.pushUserDisplayNames(tids, permissionsToDisplay, permissions); - - return permissionsToDisplay; -} diff --git a/web/modules/contrib/permissions_by_term/js/package-lock.json b/web/modules/contrib/permissions_by_term/js/package-lock.json new file mode 100644 index 000000000..c0cac8096 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/package-lock.json @@ -0,0 +1,5283 @@ +{ + "name": "pbt", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "requires": { + "samsam": "1.3.0" + } + }, + "@webassemblyjs/ast": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.5.12.tgz", + "integrity": "sha512-bmTBEKuuhSU6dC95QIW250xO769cdYGx9rWn3uBLTw2pUpud0Z5kVuMw9m9fqbNzGeuOU2HpyuZa+yUt2CTEDA==", + "requires": { + "@webassemblyjs/helper-module-context": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/wast-parser": "1.5.12", + "debug": "3.1.0", + "mamacro": "0.0.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.12.tgz", + "integrity": "sha512-epTvkdwOIPpTE9edHS+V+shetYzpTbd91XOzUli1zAS0+NSgSe6ZsNggIqUNzhma1s4bN2f/m8c6B1NMdCERAg==" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.12.tgz", + "integrity": "sha512-Goxag86JvLq8ucHLXFNSLYzf9wrR+CJr37DsESTAzSnGoqDTgw5eqiXSQVd/D9Biih7+DIn8UIQCxMs8emRRwg==" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.12.tgz", + "integrity": "sha512-tJNUjttL5CxiiS/KLxT4/Zk0Nbl/poFhztFxktb46zoQEUWaGHR9ZJ0SnvE7DbFX5PY5JNJDMZ0Li4lm246fWw==", + "requires": { + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.12.tgz", + "integrity": "sha512-0FrJgiST+MQDMvPigzs+UIk1vslLIqGadkEWdn53Lr0NsUC2JbheG9QaO3Zf6ycK2JwsHiUpGaMFcHYXStTPMA==", + "requires": { + "@webassemblyjs/wast-printer": "1.5.12" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.12.tgz", + "integrity": "sha512-QBHZ45VPUJ7UyYKvUFoaxrSS9H5hbkC9U7tdWgFHmnTMutkXSEgDg2gZg3I/QTsiKOCIwx4qJUJwPd7J4D5CNQ==" + }, + "@webassemblyjs/helper-module-context": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.12.tgz", + "integrity": "sha512-SCXR8hPI4JOG3cdy9HAO8W5/VQ68YXG/Hfs7qDf1cd64zWuMNshyEour5NYnLMVkrrtc0XzfVS/MdeV94woFHA==", + "requires": { + "debug": "3.1.0", + "mamacro": "0.0.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.12.tgz", + "integrity": "sha512-0Gz5lQcyvElNVbOTKwjEmIxGwdWf+zpAW/WGzGo95B7IgMEzyyfZU+PrGHDwiSH9c0knol9G7smQnY0ljrSA6g==" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.12.tgz", + "integrity": "sha512-ge/CKVKBGpiJhFN9PIOQ7sPtGYJhxm/mW1Y3SpG1L6XBunfRz0YnLjW3TmhcOEFozIVyODPS1HZ9f7VR3GBGow==", + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-buffer": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/wasm-gen": "1.5.12", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/ieee754": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.5.12.tgz", + "integrity": "sha512-F+PEv9QBzPi1ThLBouUJbuxhEr+Sy/oua1ftXFKHiaYYS5Z9tKPvK/hgCxlSdq+RY4MSG15jU2JYb/K5pkoybg==", + "requires": { + "ieee754": "1.1.11" + } + }, + "@webassemblyjs/leb128": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.5.12.tgz", + "integrity": "sha512-cCOx/LVGiWyCwVrVlvGmTdnwHzIP4+zflLjGkZxWpYCpdNax9krVIJh1Pm7O86Ox/c5PrJpbvZU1cZLxndlPEw==", + "requires": { + "leb": "0.3.0" + } + }, + "@webassemblyjs/utf8": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.5.12.tgz", + "integrity": "sha512-FX8NYQMiTRU0TfK/tJVntsi9IEKsedSsna8qtsndWVE0x3zLndugiApxdNMIOoElBV9o4j0BUqR+iwU58QfPxQ==" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.12.tgz", + "integrity": "sha512-r/oZAyC4EZl0ToOYJgvj+b0X6gVEKQMLT34pNNbtvWBehQOnaSXvVUA5FIYlH8ubWjFNAFqYaVGgQTjR1yuJdQ==", + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-buffer": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/helper-wasm-section": "1.5.12", + "@webassemblyjs/wasm-gen": "1.5.12", + "@webassemblyjs/wasm-opt": "1.5.12", + "@webassemblyjs/wasm-parser": "1.5.12", + "@webassemblyjs/wast-printer": "1.5.12", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.12.tgz", + "integrity": "sha512-LTu+cr1YRxGGiVIXWhei/35lXXEwTnQU18x4V/gE+qCSJN21QcVTMjJuasTUh8WtmBZtOlqJbOQIeN7fGnHWhg==", + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/ieee754": "1.5.12", + "@webassemblyjs/leb128": "1.5.12", + "@webassemblyjs/utf8": "1.5.12" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.12.tgz", + "integrity": "sha512-LBwG5KPA9u/uigZVyTsDpS3CVxx3AePCnTItVL+OPkRCp5LqmLsOp4a3/c5CQE0Lecm0Ss9hjUTDcbYFZkXlfQ==", + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-buffer": "1.5.12", + "@webassemblyjs/wasm-gen": "1.5.12", + "@webassemblyjs/wasm-parser": "1.5.12", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.12.tgz", + "integrity": "sha512-xset3+1AtoFYEfMg30nzCGBnhKmTBzbIKvMyLhqJT06TvYV+kA884AOUpUvhSmP6XPF3G+HVZPm/PbCGxH4/VQ==", + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-api-error": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/ieee754": "1.5.12", + "@webassemblyjs/leb128": "1.5.12", + "@webassemblyjs/utf8": "1.5.12" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.5.12.tgz", + "integrity": "sha512-QWUtzhvfY7Ue9GlJ3HeOB6w5g9vNYUUnG+Y96TWPkFHJTxZlcvGfNrUoACCw6eDb9gKaHrjt77aPq41a7y8svg==", + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/floating-point-hex-parser": "1.5.12", + "@webassemblyjs/helper-api-error": "1.5.12", + "@webassemblyjs/helper-code-frame": "1.5.12", + "@webassemblyjs/helper-fsm": "1.5.12", + "long": "3.2.0", + "mamacro": "0.0.3" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.5.12.tgz", + "integrity": "sha512-XF9RTeckFgDyl196uRKZWHFFfbkzsMK96QTXp+TC0R9gsV9DMiDGMSIllgy/WdrZ3y3dsQp4fTA5r4GoaOBchA==", + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/wast-parser": "1.5.12", + "long": "3.2.0" + } + }, + "acorn": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.6.2.tgz", + "integrity": "sha512-zUzo1E5dI2Ey8+82egfnttyMlMZ2y0D8xOCO3PNPPlYXpl8NZvF6Qk9L9BEtJs+43FqEmfBViDqc5d1ckRDguw==" + }, + "acorn-dynamic-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", + "requires": { + "acorn": "5.6.2" + } + }, + "ajv": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.0.tgz", + "integrity": "sha512-VDUX1oSajablmiyFyED9L1DFndg0P9h7p1F+NO8FkIzei6EPrR6Zu1n18rd5P8PqaSRd/FrWv3G1TVBqpM83gA==", + "requires": { + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1", + "uri-js": "4.2.2" + } + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=" + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" + }, + "babel-cli": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", + "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", + "requires": { + "babel-core": "6.26.3", + "babel-polyfill": "6.26.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "chokidar": "1.7.0", + "commander": "2.15.1", + "convert-source-map": "1.5.1", + "fs-readdir-recursive": "1.1.0", + "glob": "7.1.2", + "lodash": "4.17.10", + "output-file-sync": "1.1.2", + "path-is-absolute": "1.0.1", + "slash": "1.0.0", + "source-map": "0.5.7", + "v8flags": "2.1.1" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.1", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.1", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.10", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.10", + "source-map": "0.5.7", + "trim-right": "1.0.1" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.4.tgz", + "integrity": "sha512-/hbyEvPzBJuGpk9o80R0ZyTej6heEOr59GoEUtn8qFKbnx4cJm9FWES6J/iv644sYgrtVw9JJQkjaLW/bqb5gw==", + "requires": { + "find-cache-dir": "1.0.0", + "loader-utils": "1.1.0", + "mkdirp": "0.5.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "requires": { + "babel-runtime": "6.26.0", + "core-js": "2.5.7", + "regenerator-runtime": "0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" + } + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "requires": { + "babel-core": "6.26.3", + "babel-runtime": "6.26.0", + "core-js": "2.5.7", + "home-or-tmp": "2.0.0", + "lodash": "4.17.10", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.7", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.10" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.10" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.10", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "1.2.0", + "browserify-des": "1.0.1", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", + "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.6" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.1" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "requires": { + "pako": "1.0.6" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "1.3.0", + "ieee754": "1.1.11", + "isarray": "1.0.0" + } + }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "cacache": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", + "requires": { + "bluebird": "3.5.1", + "chownr": "1.0.1", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "lru-cache": "4.1.3", + "mississippi": "2.0.0", + "mkdirp": "0.5.1", + "move-concurrently": "1.0.1", + "promise-inflight": "1.0.1", + "rimraf": "2.6.2", + "ssri": "5.3.0", + "unique-filename": "1.1.0", + "y18n": "4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "chardet": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.5.0.tgz", + "integrity": "sha512-9ZTaoBaePSCFvNlNGrsyI8ZVACP2svUtq0DkM7t4K2ClAa96sqOIRjAzDTc8zXzFt1cZR46rRzLTiHFSJ+Qw0g==" + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.2.4", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + }, + "chrome-trace-event": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz", + "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==", + "requires": { + "tslib": "1.9.2" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "1.1.0", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "typedarray": "0.0.6" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "requires": { + "date-now": "0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "requires": { + "aproba": "1.2.0", + "fs-write-stream-atomic": "1.0.10", + "iferr": "0.1.5", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "md5.js": "1.3.4", + "ripemd160": "2.0.2", + "sha.js": "2.4.11" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "inherits": "2.0.3", + "ripemd160": "2.0.2", + "safe-buffer": "5.1.2", + "sha.js": "2.4.11" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "1.0.4", + "path-key": "2.0.1", + "semver": "5.5.0", + "shebang-command": "1.2.0", + "which": "1.3.1" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "1.0.1", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.3", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "diffie-hellman": "5.0.3", + "inherits": "2.0.3", + "pbkdf2": "3.0.16", + "public-encrypt": "4.0.2", + "randombytes": "2.0.6", + "randomfill": "1.0.4" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "requires": { + "repeating": "2.0.1" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.6" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + }, + "duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "requires": { + "end-of-stream": "1.4.1", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "stream-shift": "1.0.0" + } + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz", + "integrity": "sha512-jox/62b2GofV1qTUQTMPEJSDIGycS43evqYzD/KVtEb9OCoki9cnacUPxCrZa7JfPzZSYOCZhu9O9luaMxAX8g==", + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "tapable": "1.0.0" + } + }, + "ensure-posix-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.0.2.tgz", + "integrity": "sha1-pls+QtC3HPxYXrd0+ZQ8jZuRsMI=" + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "requires": { + "prr": "1.0.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "requires": { + "esrecurse": "4.2.1", + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "requires": { + "estraverse": "4.2.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.2" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "4.1.3", + "shebang-command": "1.2.0", + "which": "1.3.1" + } + } + } + }, + "exists-stat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/exists-stat/-/exists-stat-1.0.0.tgz", + "integrity": "sha1-BmDjUlouidnkRhKUQMJy7foktSk=" + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "2.2.4" + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "1.0.1" + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-mpkfj0FEdxrIhOC04zk85X7StNtr0yXnG7zCb+8ikO8OJi2jsHh5YGoknNTyXgsbHOf1WOOcVU3kPFWT2WgCkQ==", + "requires": { + "chardet": "0.5.0", + "iconv-lite": "0.4.23", + "tmp": "0.0.33" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "3.0.0", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "requires": { + "commondir": "1.0.1", + "make-dir": "1.3.0", + "pkg-dir": "2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "2.0.0" + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "requires": { + "detect-file": "1.0.0", + "is-glob": "3.1.0", + "micromatch": "3.1.10", + "resolve-dir": "1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + } + } + }, + "flush-write-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", + "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "1.0.2" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "4.1.11", + "iferr": "0.1.5", + "imurmurhash": "0.1.4", + "readable-stream": "2.3.6" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "optional": true, + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.7", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true + } + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "2.0.1" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "requires": { + "global-prefix": "1.0.2", + "is-windows": "1.0.2", + "resolve-dir": "1.0.1" + } + }, + "global-modules-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.1.0.tgz", + "integrity": "sha512-3DrmGj2TP+96cABk9TfMp6f3knH/Y46dqvWznTU3Tf6/bDGLDAn15tFluQ7BcloykOcdY16U0WGq0BQblYOxJQ==" + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "requires": { + "expand-tilde": "2.0.2", + "homedir-polyfill": "1.0.1", + "ini": "1.3.5", + "is-windows": "1.0.2", + "which": "1.3.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "requires": { + "parse-passwd": "1.0.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ieee754": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", + "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==" + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + }, + "import-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", + "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", + "requires": { + "pkg-dir": "2.0.0", + "resolve-cwd": "2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "inquirer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.0.0.tgz", + "integrity": "sha512-tISQWRwtcAgrz+SHPhTH7d3e73k31gsOy6i1csonLc0u1dVK/wYvuOnFeiWqC5OXFIYbmrIFInef31wbT8MEJg==", + "requires": { + "ansi-escapes": "3.1.0", + "chalk": "2.4.1", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "3.0.0", + "figures": "2.0.0", + "lodash": "4.17.10", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rxjs": "6.2.0", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "js-reporters": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/js-reporters/-/js-reporters-1.2.1.tgz", + "integrity": "sha1-+IxgjjJKM3OpW8xFrTBeXJecRZs=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "just-extend": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz", + "integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, + "leb": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/leb/-/leb-0.3.0.tgz", + "integrity": "sha1-Mr7p+tFoMo1q6oUi2DP0GA7tHaM=" + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=" + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lolex": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.0.tgz", + "integrity": "sha512-uJkH2e0BVfU5KOJUevbTOtpDduooSarH5PopO+LfM/vZf8Z9sJzODqKev804JYM2i++ktJfUmC1le4LwFQ1VMg==" + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "3.0.0" + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==" + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "1.0.1" + } + }, + "matcher-collection": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-1.0.5.tgz", + "integrity": "sha512-nUCmzKipcJEwYsBVAFh5P+d7JBuhJaW1xs85Hara9xuMLqtCVUrW6DSC0JVIkluxEH2W45nPBM/wjHtBXa/tYA==", + "requires": { + "minimatch": "3.0.4" + } + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "1.2.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "requires": { + "errno": "0.1.7", + "readable-stream": "2.3.6" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mississippi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", + "requires": { + "concat-stream": "1.6.2", + "duplexify": "3.6.0", + "end-of-stream": "1.4.1", + "flush-write-stream": "1.0.3", + "from2": "2.3.0", + "parallel-transform": "1.1.0", + "pump": "2.0.1", + "pumpify": "1.5.1", + "stream-each": "1.2.2", + "through2": "2.0.3" + } + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "requires": { + "aproba": "1.2.0", + "copy-concurrently": "1.0.5", + "fs-write-stream-atomic": "1.0.10", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "neo-async": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.1.tgz", + "integrity": "sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA==" + }, + "nice-try": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz", + "integrity": "sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==" + }, + "nise": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.1.tgz", + "integrity": "sha512-9JX3YwoIt3kS237scmSSOpEv7vCukVzLfwK0I0XhocDSHUANid8ZHnLEULbbSkfeMn98B2y5kphIWzZUylESRQ==", + "requires": { + "@sinonjs/formatio": "2.0.0", + "just-extend": "1.1.27", + "lolex": "2.7.0", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" + } + }, + "node-libs-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", + "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.2.0", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "domain-browser": "1.2.0", + "events": "1.1.1", + "https-browserify": "1.0.0", + "os-browserify": "0.3.0", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.6", + "stream-browserify": "2.0.1", + "stream-http": "2.8.3", + "string_decoder": "1.1.1", + "timers-browserify": "2.0.10", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.4", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "1.2.0" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "output-file-sync": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", + "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "requires": { + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "object-assign": "4.1.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "1.3.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "requires": { + "cyclist": "0.2.2", + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "requires": { + "asn1.js": "4.10.1", + "browserify-aes": "1.2.0", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.16" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, + "pbkdf2": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", + "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", + "requires": { + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "ripemd160": "2.0.2", + "safe-buffer": "5.1.2", + "sha.js": "2.4.11" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "requires": { + "find-up": "2.1.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "public-encrypt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", + "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "parse-asn1": "5.1.1", + "randombytes": "2.0.6" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "3.6.0", + "inherits": "2.0.3", + "pump": "2.0.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "qunit": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.6.1.tgz", + "integrity": "sha512-AaILHe41G+fVC8h5wrp8U31iM2tRxLAVwH1tICtDkRbC1HDgJBjjYq0SMCZE8K3Z16MiZq3vhNhLu18KeDtS6Q==", + "requires": { + "chokidar": "1.7.0", + "commander": "2.12.2", + "exists-stat": "1.0.0", + "findup-sync": "2.0.0", + "js-reporters": "1.2.1", + "resolve": "1.5.0", + "walk-sync": "0.3.2" + }, + "dependencies": { + "commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==" + } + } + }, + "randomatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "requires": { + "is-number": "4.0.0", + "kind-of": "6.0.2", + "math-random": "1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "2.0.6", + "safe-buffer": "5.1.2" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.6", + "set-immediate-shim": "1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "1.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "requires": { + "resolve-from": "3.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "requires": { + "expand-tilde": "2.0.2", + "global-modules": "1.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "2.1.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "requires": { + "aproba": "1.2.0" + } + }, + "rxjs": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.0.tgz", + "integrity": "sha512-qBzf5uu6eOKiCZuAE0SgZ0/Qp+l54oeVxFfC2t+mJ2SFI6IB8gmMdJHs5DUMu5kqifqcCtsKS2XHjhZu6RKvAw==", + "requires": { + "tslib": "1.9.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "0.1.15" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==" + }, + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", + "requires": { + "ajv": "6.5.0", + "ajv-keywords": "3.2.0" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "serialize-javascript": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz", + "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sinon": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-5.1.1.tgz", + "integrity": "sha512-h/3uHscbt5pQNxkf7Y/Lb9/OM44YNCicHakcq73ncbrIS8lXg+ZGOZbtuU+/km4YnyiCYfQQEwANaReJz7KDfw==", + "requires": { + "@sinonjs/formatio": "2.0.0", + "diff": "3.5.0", + "lodash.get": "4.4.2", + "lolex": "2.7.0", + "nise": "1.4.1", + "supports-color": "5.4.0", + "type-detect": "4.0.8" + }, + "dependencies": { + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.2", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "3.2.2" + } + }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "requires": { + "atob": "2.1.1", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "requires": { + "source-map": "0.5.7" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "3.0.2" + } + }, + "ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "stream-each": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", + "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "requires": { + "end-of-stream": "1.4.1", + "stream-shift": "1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==" + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "2.3.6", + "xtend": "4.0.1" + } + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "requires": { + "setimmediate": "1.0.5" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "3.2.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + } + } + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "tslib": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", + "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==" + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "requires": { + "commander": "2.13.0", + "source-map": "0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "uglifyjs-webpack-plugin": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz", + "integrity": "sha512-hIQJ1yxAPhEA2yW/i7Fr+SXZVMp+VEI3d42RTHBgQd2yhp/1UdBcR3QEWPV5ahBxlqQDMEMTuTEvDHSFINfwSw==", + "requires": { + "cacache": "10.0.4", + "find-cache-dir": "1.0.0", + "schema-utils": "0.4.5", + "serialize-javascript": "1.5.0", + "source-map": "0.6.1", + "uglify-es": "3.3.9", + "webpack-sources": "1.1.0", + "worker-farm": "1.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unique-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", + "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", + "requires": { + "unique-slug": "2.0.0" + } + }, + "unique-slug": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", + "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "requires": { + "imurmurhash": "0.1.4" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "2.1.1" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=" + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "v8-compile-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.0.tgz", + "integrity": "sha512-qNdTUMaCjPs4eEnM3W9H94R3sU70YCuT+/ST7nUf+id1bVOrdjrpUaeZLqPBPRph3hsgn4a4BvwpxhHZx+oSDg==" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "requires": { + "user-home": "1.1.1" + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "requires": { + "indexof": "0.0.1" + } + }, + "walk-sync": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-0.3.2.tgz", + "integrity": "sha512-FMB5VqpLqOCcqrzA9okZFc0wq0Qbmdm396qJxvQZhDpyu0W95G9JCmp74tx7iyYnyOcBtUuKJsgIKAqjozvmmQ==", + "requires": { + "ensure-posix-path": "1.0.2", + "matcher-collection": "1.0.5" + } + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "requires": { + "chokidar": "2.0.3", + "graceful-fs": "4.1.11", + "neo-async": "2.5.1" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "3.1.10", + "normalize-path": "2.1.1" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "chokidar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "requires": { + "anymatch": "2.0.0", + "async-each": "1.0.1", + "braces": "2.3.2", + "fsevents": "1.2.4", + "glob-parent": "3.1.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "4.0.0", + "normalize-path": "2.1.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0", + "upath": "1.1.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "2.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + } + } + }, + "webpack": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.12.0.tgz", + "integrity": "sha512-EJj2FfhgtjrTbJbJaNulcVpDxi9vsQVvTahHN7xJvIv6W+k4r/E6Hxy4eyOrj+IAFWqYgaUtnpxmSGYP8MSZJw==", + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-module-context": "1.5.12", + "@webassemblyjs/wasm-edit": "1.5.12", + "@webassemblyjs/wasm-opt": "1.5.12", + "@webassemblyjs/wasm-parser": "1.5.12", + "acorn": "5.6.2", + "acorn-dynamic-import": "3.0.0", + "ajv": "6.5.0", + "ajv-keywords": "3.2.0", + "chrome-trace-event": "1.0.0", + "enhanced-resolve": "4.0.0", + "eslint-scope": "3.7.1", + "json-parse-better-errors": "1.0.2", + "loader-runner": "2.3.0", + "loader-utils": "1.1.0", + "memory-fs": "0.4.1", + "micromatch": "3.1.10", + "mkdirp": "0.5.1", + "neo-async": "2.5.1", + "node-libs-browser": "2.1.0", + "schema-utils": "0.4.5", + "tapable": "1.0.0", + "uglifyjs-webpack-plugin": "1.2.5", + "watchpack": "1.6.0", + "webpack-sources": "1.1.0" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + } + } + }, + "webpack-cli": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.0.3.tgz", + "integrity": "sha512-65a3T3SDIozJjRU4UJMdK+LXJt73gNs2qpdjsOeq6jIrfBvAKApy59Glof1qDG3wYEo38HRxb+KrwsrsAtsaiA==", + "requires": { + "chalk": "2.4.1", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.0.0", + "global-modules-path": "2.1.0", + "import-local": "1.0.0", + "inquirer": "6.0.0", + "interpret": "1.1.0", + "loader-utils": "1.1.0", + "supports-color": "5.4.0", + "v8-compile-cache": "2.0.0", + "yargs": "11.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "webpack-sources": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", + "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "requires": { + "source-list-map": "2.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "worker-farm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", + "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "requires": { + "errno": "0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", + "requires": { + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + }, + "dependencies": { + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + } + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "requires": { + "camelcase": "4.1.0" + } + } + } +} diff --git a/web/modules/contrib/permissions_by_term/js/package.json b/web/modules/contrib/permissions_by_term/js/package.json index de5822921..d56585e4a 100644 --- a/web/modules/contrib/permissions_by_term/js/package.json +++ b/web/modules/contrib/permissions_by_term/js/package.json @@ -1,9 +1,27 @@ { + "name": "pbt", + "version": "1.0.0", + "description": "JavaScript code has been tested with [Jasmine](https://jasmine.github.io/). Install the dependencies by [Yarn](https://yarnpkg.com/lang/en/). Just run `yarn`. To run the tests, switch to the `js` folder and run `yarn test`.", + "main": "node-form.js", "dependencies": { - "jasmine": "^2.8.0" + "babel-cli": "^6.26.0", + "babel-loader": "^7.1.4", + "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", + "lodash": "^4.17.10", + "qunit": "^2.6.1", + "sinon": "^5.0.10", + "webpack": "^4.12.0", + "webpack-cli": "^3.0" }, "scripts": { - "test": "jasmine $NODE_DEBUG_OPTION" + "test": "npx qunit webpack-dist/bundle-test.js", + "babel-build-for-test": "babel src/**/*.js test/*.js -d babel-compiled-test", + "cleanup-babel-test": "rm -rf babel-compiled-test/", + "build-and-test": "mv src/drupal-behavior-function /tmp && npm run-script webpack-test && mv /tmp/drupal-behavior-function src/ && npm run-script test", + "build": "webpack --config webpack.production.js --mode production", + "webpack-test": "npx webpack --entry ./src/**/*.js ./test/*.js --output ./webpack-dist/bundle-test.js --mode development", + "bat": "npm run-script build-and-test" }, - "license": "GPL-2.0+" + "author": "", + "license": "ISC" } diff --git a/web/modules/contrib/permissions_by_term/js/spec/node-form.spec.js b/web/modules/contrib/permissions_by_term/js/spec/node-form.spec.js deleted file mode 100644 index b1a3069a6..000000000 --- a/web/modules/contrib/permissions_by_term/js/spec/node-form.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -describe("Permissions by Term Suite", function() { - - it("Compute field wrapper CSS classes", function() { - fs = require('fs') - prototypeClass = fs.readFileSync('node-form.prototype.js','utf-8') - eval(prototypeClass) - - var NodeForm = new NodeForm(); - - var fieldNames = ['field_name-one', 'field_name_two']; - - expect(NodeForm.computeFieldWrapperCSSClasses(fieldNames)).toEqual(['.field--name-field-name-one', '.field--name-field-name-two']); - }); - -}); \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/spec/support/jasmine.json b/web/modules/contrib/permissions_by_term/js/spec/support/jasmine.json deleted file mode 100644 index 3ea316690..000000000 --- a/web/modules/contrib/permissions_by_term/js/spec/support/jasmine.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "spec_dir": "spec", - "spec_files": [ - "**/*[sS]pec.js" - ], - "helpers": [ - "helpers/**/*.js" - ], - "stopSpecOnExpectationFailure": false, - "random": false -} diff --git a/web/modules/contrib/permissions_by_term/js/src/async-function/create-permission.js b/web/modules/contrib/permissions_by_term/js/src/async-function/create-permission.js new file mode 100644 index 000000000..ec8b4a626 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/src/async-function/create-permission.js @@ -0,0 +1,27 @@ +import _ from 'lodash'; +import Backend from '../model/backend.prototype'; + +/** + * @returns Access + */ +const createPermission = async (fetchFromBackend) => { + let data = await fetchFromBackend(), + fieldCssClasses = []; + + if (!_.isEmpty(data.taxonomyRelationFieldNames)) { + data.taxonomyRelationFieldNames.forEach((fieldName) => { + const fieldWrapperClass = '.field--name-' + fieldName.replace(/_/g, '-'); + + fieldCssClasses.push(fieldWrapperClass); + }); + } + + return new Backend( + data.taxonomyRelationFieldNames, + data.permissions.userDisplayNames, + data.permissions.roleLabels, + fieldCssClasses + ); +} + +export default createPermission; \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/src/async-function/fetch-from-backend.js b/web/modules/contrib/permissions_by_term/js/src/async-function/fetch-from-backend.js new file mode 100644 index 000000000..717a3c4a1 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/src/async-function/fetch-from-backend.js @@ -0,0 +1,23 @@ +/** + * @returns array + */ +const fetchFromBackend = async () => { + let contentType = null; + if (window.location.href.indexOf('/node/add') !== -1) { + contentType = window.location.href.split("/").pop(); + } + + let url = '/admin/permissions-by-term/access-info-by-url?url=' + window.location.pathname; + if (contentType !== null) { + url = '/admin/permissions-by-term/access-info-by-content-type/' + contentType; + } + + return await fetch(url, { credentials:'include' }) + .then(function(response) { + return response.json(); + }).then(function(data) { + return data; + }); +}; + +export default fetchFromBackend; \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/src/client/dom-client.prototype.js b/web/modules/contrib/permissions_by_term/js/src/client/dom-client.prototype.js new file mode 100644 index 000000000..26d6bdcfd --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/src/client/dom-client.prototype.js @@ -0,0 +1,142 @@ +const _ = require('lodash'); + +/** + * @param PermissionsOutput permissionsOutput + * @param document + */ +let DomClient = function(document, permissionsOutput, drupal) { + this.document = document; + this.drupal = drupal; + this.permissionsOutput = permissionsOutput; +} + +DomClient.prototype.renderPermissionsInfo = function() { + + let allowedUsersHtml = '<b>' + this.drupal.t('Allowed users:') + '</b> '; + + if (!_.isEmpty(this.permissionsOutput.getUsernames())) { + allowedUsersHtml += this.permissionsOutput.getUsernames().join(', '); + } else { + allowedUsersHtml += '<i>' + this.drupal.t('No user restrictions.') + '</i>'; + } + + let allowedRolesHtml = '<b>' + this.drupal.t('Allowed roles:') + '</b> '; + + if (!_.isEmpty(this.permissionsOutput.getRoles())) { + allowedRolesHtml += this.permissionsOutput.getRoles().join(', '); + } else { + allowedRolesHtml += '<i>' + this.drupal.t('No role restrictions.') + '</i>';; + } + + let generalInfoText = this.drupal.t("This widget shows information about taxonomy term related permissions. It's being updated, as soon you make any related changes in the form."); + + let newTermInfo = this.document.createElement('div'); + newTermInfo.innerHTML = '<div id="edit-permissions-by-term-info"><div class="form-type-item">' + generalInfoText + '<br /><br />' + allowedUsersHtml + '<br />' + allowedRolesHtml + '</div></div>'; + this.document.querySelector('#edit-permissions-by-term-info .form-type-item').replaceWith(newTermInfo); + +} + +DomClient.prototype._computeTidsByAutocomplete = function(fieldWrapperCSSClass) { + let selectedTids = [] + + let autocompleteInputs = this.document.querySelectorAll(fieldWrapperCSSClass + ' input.form-autocomplete'); + + for (let autocompleteInput of autocompleteInputs) { + + if (autocompleteInput.value !== undefined && _.includes(autocompleteInput.value, '(') && _.includes(autocompleteInput.value, ')')) { + + let tidsInBrackets = autocompleteInput.value.match(/\(\d+\)/g); + + if (tidsInBrackets !== undefined && tidsInBrackets !== null && tidsInBrackets.constructor === Array) { + + for (let i = 0; i < tidsInBrackets.length; ++i) { + let selectedTid = parseInt(tidsInBrackets[i].replace('(', '').replace(')', '')); + if (!_.includes(selectedTids, selectedTid)) { + selectedTids.push(String(selectedTid)); + } + } + + } + + } + + } + + return selectedTids; +} + +DomClient.prototype._computeTidsBySelect = function(fieldWrapperCSSClass) { + let tids = [], + inputTypes = ['select', 'input']; + + for (let inputTypesIndex = 0; inputTypesIndex <= inputTypes.length; inputTypesIndex++) { + let value = this.document.querySelector(fieldWrapperCSSClass + ' select').value; + + if (typeof value === "string") { + tids.push(value); + } else { + throw "Value must be type of string."; + } + + } + + return tids; +} + +DomClient.prototype._computeTidsByCheckbox = function(formElementCssClass) { + let tids = []; + + for (let checkbox of this.document.querySelectorAll(formElementCssClass + ' input[type="checkbox"]')) { + if (checkbox.checked === true) { + tids.push(checkbox.value); + } + } + + return tids; +} + +DomClient.prototype.computeTids = function(formElementCssClass) { + let tids = []; + + let lookup = { + checkbox: '_computeTidsByCheckbox', + text: '_computeTidsByAutocomplete', + select: '_computeTidsBySelect', + }; + + let inputType = this._getInputType(formElementCssClass); + + tids.push(this[lookup[inputType]](formElementCssClass)); + + return tids; +} + +DomClient.prototype._getInputType = function(formElementCssClass) { + let formElement = null; + + if (!_.isEmpty(this.document.querySelector(formElementCssClass + ' select'))) { + formElement = 'select'; + } + + if (!_.isEmpty(this.document.querySelector(formElementCssClass + ' input'))) { + formElement = 'input'; + } + + if (formElement === 'input') { + if (_.get(this.document.querySelector(formElementCssClass + ' input.form-autocomplete'), 'type') && this.document.querySelector(formElementCssClass + ' input.form-autocomplete').type === "text") { + return 'text'; + } + if (this.document.querySelector(formElementCssClass + ' input').type === "checkbox") { + return 'checkbox'; + } + if (this.document.querySelector(formElementCssClass + ' input').type === "radio") { + return 'radio'; + } + } + if (!_.isEmpty(formElement) && this.document.querySelector(formElementCssClass + ' select').tagName === "SELECT") { + return 'select'; + } + +} + +export default DomClient; \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/src/client/permission-output-collector.prototype.js b/web/modules/contrib/permissions_by_term/js/src/client/permission-output-collector.prototype.js new file mode 100644 index 000000000..4a967b655 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/src/client/permission-output-collector.prototype.js @@ -0,0 +1,63 @@ +import _ from 'lodash'; + +/** + * @param PermissionOutput permissionOutput + */ +let PermissionsOutputCollector = function(permissionOutput) { + this.permissionOutput = permissionOutput; +} + +PermissionsOutputCollector.prototype.collect = function(permissions, tidsByInputs) { + this._collectRoles(permissions, tidsByInputs); + this._collectUsers(permissions, tidsByInputs); +} + +PermissionsOutputCollector.prototype._collectRoles = function(permissions, tidsByInputs) { + + for (let tids of tidsByInputs) { + + for (let tidToRole in permissions.tidsToRoles) { + + if (_.includes(tids, tidToRole)) { + + for (let role of permissions.tidsToRoles[tidToRole]) { + if (!_.includes(this.permissionOutput.getRoles(), role)) { + this.permissionOutput.addRole(role); + } + } + + } + + } + + } + +} + +PermissionsOutputCollector.prototype._collectUsers = function(permissions, tidsByInputs) { + + for (let tids of tidsByInputs) { + + for (let tidToUsername in permissions.tidToUsernames) { + if (_.includes(tids, tidToUsername)) { + for (let username of permissions.tidToUsernames[tidToUsername]) { + if (!_.includes(this.permissionOutput.getUsernames(), username)) { + this.permissionOutput.addUsername(username); + } + } + + } + + } + + } +} + +/** + * @returns PermissionOutput + */ +PermissionsOutputCollector.prototype.getPermissionOutput = function() { + return this.permissionOutput; +} + +export default PermissionsOutputCollector; \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/src/client/term-collector.prototype.js b/web/modules/contrib/permissions_by_term/js/src/client/term-collector.prototype.js new file mode 100644 index 000000000..07b52c43f --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/src/client/term-collector.prototype.js @@ -0,0 +1,54 @@ +const _ = require('lodash'); + +let TermCollector = function(){ + this.selectedTids = []; +}; + +TermCollector.prototype.getSelectedTids = function() { + return this.selectedTids; +} + +TermCollector.prototype.termExists = function(key) { + if (!this.selectedTids || (this.selectedTids.constructor !== Array && this.selectedTids.constructor !== Object)) { + return false; + } + for (let i = 0; i < this.selectedTids.length; i++) { + if (this.selectedTids[i] === key) { + return true; + } + } + return key in this.selectedTids; +} + +TermCollector.prototype.addSelectedTid = function(tid) { + if (!this.termExists(tid)) { + this.selectedTids.push(tid); + } +} + +TermCollector.prototype.addSelectedTids = function(tids) { + if (!_.isEmpty(tids)) { + _.flatten(tids).forEach((tid) => { + + if (_.isArray(tid)) { + throw 'Wanted to add array. Must be string.'; + } + + this.addSelectedTid(tid); + }); + } +} + +TermCollector.prototype.removeTid = function(value, formElementCssClass) { + const index = this.selectedTids[formElementCssClass].indexOf(parseInt(value)); + + if (index !== -1) { + this.selectedTids[formElementCssClass].splice(index, 1); + } +} + +TermCollector.prototype.resetData = function(formElementCssClass) { + this.selectedTids[formElementCssClass] = []; +} + +export default TermCollector; \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/src/drupal-behavior-function/node-form-client.js b/web/modules/contrib/permissions_by_term/js/src/drupal-behavior-function/node-form-client.js new file mode 100644 index 000000000..208fbcfd5 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/src/drupal-behavior-function/node-form-client.js @@ -0,0 +1,115 @@ +import createPermission from '../async-function/create-permission'; +import fetchFromBackend from '../async-function/fetch-from-backend'; +import DomClient from '../client/dom-client.prototype'; +import PermissionOutput from '../model/permission-output.prototype'; +import PermissionOutputCollector from '../client/permission-output-collector.prototype'; +import TermCollector from "../client/term-collector.prototype"; + +(($) => { + + 'use strict'; + + if (document.querySelector("#edit-permissions-by-term-info") !== null) { + + /** + * @type {Drupal~behavior} + */ + Drupal.behaviors.nodeForm = { + attach: async () => { + /** + * @var Backend backend + */ + let backend = await createPermission(fetchFromBackend); + + const hasTaxonomyFormFields = (permissions) => { + if (permissions.taxonomyRelationFieldNames.length !== 0) { + return true; + } + + return false; + } + + if (hasTaxonomyFormFields(backend)) { + + const processPermissionsDisplay = () => { + + const permissionOutput = new PermissionOutput, + termCollector = new TermCollector, + domClient = new DomClient(document, permissionOutput, Drupal); + + const permissionOutputCollector = new PermissionOutputCollector(permissionOutput); + + for (let formElementCssClass of backend.getFieldWrapperCSSClasses()) { + termCollector.addSelectedTids(domClient.computeTids(formElementCssClass)); + + permissionOutputCollector.collect(backend, termCollector.getSelectedTids()); + } + + domClient.renderPermissionsInfo(); + } + + for (let formElementCssClass of backend.getFieldWrapperCSSClasses()) { + + $(formElementCssClass + ' input[type="text"]').on('autocomplete-select', () => { + processPermissionsDisplay(); + }); + + $(formElementCssClass + ' select').change(function (){ + processPermissionsDisplay(); + }); + + $(formElementCssClass + ' input[type="text"]').on('keyup', function (){ + processPermissionsDisplay(); + }); + + $(formElementCssClass + ' input[type="checkbox"]').change(function (){ + processPermissionsDisplay(); + }); + + } + + }; + + } + + }; + + if (Drupal.autocomplete) { + /** + * Handles an auto-complete select event. + * + * Override the autocomplete method to add a custom event. Overriding is + * happening to get full input. + * + * @param {jQuery.Event} event + * The event triggered. + * @param {object} ui + * The jQuery UI settings object. + * + * @return {boolean} + * Returns false to indicate the event status. + */ + Drupal.autocomplete.options.select = function selectHandler(event, ui) { + var terms = Drupal.autocomplete.splitValues(event.target.value); + // Remove the current input. + terms.pop(); + // Add the selected item. + if (ui.item.value.search(',') > 0) { + terms.push('"' + ui.item.value + '"'); + } + else { + terms.push(ui.item.value); + } + event.target.value = terms.join(', '); + // Fire custom event that other controllers can listen to. + jQuery(event.target).trigger('autocomplete-select'); + + // Return false to tell jQuery UI that we've filled in the value + // already. + return false; + } + } + + } + +})(jQuery); diff --git a/web/modules/contrib/permissions_by_term/js/src/model/backend.prototype.js b/web/modules/contrib/permissions_by_term/js/src/model/backend.prototype.js new file mode 100644 index 000000000..bcdadcd79 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/src/model/backend.prototype.js @@ -0,0 +1,33 @@ +let Backend = function(taxonomyRelationFieldNames = [], tidToUsernames = [], tidToRoles = [], fieldWrapperCSSClasses = []) { + this.taxonomyRelationFieldNames = taxonomyRelationFieldNames; + this.tidsToRoles = tidToRoles; + this.tidToUsernames = tidToUsernames; + this.fieldWrapperCSSClasses = fieldWrapperCSSClasses; +} + +/** + * @returns object[] + */ +Backend.prototype.getTidToUsername = function() { + return this.tidToUsernames; +} + +/** + * @returns object[] + */ +Backend.prototype.getTidToRoles = function() { + return this.tidsToRoles; +} + +/** + * @returns string[] + */ +Backend.prototype.getTaxonomyRelationFieldNames = function() { + return this.taxonomyRelationFieldNames; +} + +Backend.prototype.getFieldWrapperCSSClasses = function() { + return this.fieldWrapperCSSClasses; +} + +export default Backend; \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/src/model/permission-output.prototype.js b/web/modules/contrib/permissions_by_term/js/src/model/permission-output.prototype.js new file mode 100644 index 000000000..c72774374 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/src/model/permission-output.prototype.js @@ -0,0 +1,34 @@ +let PermissionOutput = function() { + this.roles = []; + this.usernames = []; +} + +/** + * @returns array + */ +PermissionOutput.prototype.getUsernames = function() { + return this.usernames; +} + +/** + * @returns array + */ +PermissionOutput.prototype.getRoles = function() { + return this.roles; +} + +/** + * @returns array + */ +PermissionOutput.prototype.addUsername = function(username) { + return this.usernames.push(username); +} + +/** + * @returns array + */ +PermissionOutput.prototype.addRole = function(role) { + return this.roles.push(role); +} + +export default PermissionOutput; \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/test/dom-client.tests.js b/web/modules/contrib/permissions_by_term/js/test/dom-client.tests.js new file mode 100644 index 000000000..a94c25b76 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/test/dom-client.tests.js @@ -0,0 +1,31 @@ +import DomClient from "../src/client/dom-client.prototype"; +import sinon from "sinon"; + +QUnit.test( "Get permission objects by querying backend with all params", async function( assert ) { + + const spyReplaceWith = sinon.spy(); + + const document = { + querySelector: sinon.stub().returns({ + replaceWith: spyReplaceWith + }), + createElement: sinon.stub().returns({innerHTML: sinon.stub()}), + } + + const permissionsOutput = { + getRoles: sinon.stub().returns(['admin', 'editor']), + getUsernames: sinon.stub().returns(['jeff', 'brandon']) + } + + const drupal = { + t: function(text) { + return text; + } + } + + const domClient = new DomClient(document, permissionsOutput, drupal); + domClient.renderPermissionsInfo(); + + assert.deepEqual({innerHTML: '<div id="edit-permissions-by-term-info"><div class="form-type-item">This widget shows information about taxonomy term related permissions. It\'s being updated, as soon you make any related changes in the form.<br /><br /><b>Allowed users:</b> jeff, brandon<br /><b>Allowed roles:</b> admin, editor</div></div>'}, spyReplaceWith.getCall(0).args[0]); + +}); \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/test/permission-creator.tests.js b/web/modules/contrib/permissions_by_term/js/test/permission-creator.tests.js new file mode 100644 index 000000000..f46dc10bf --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/test/permission-creator.tests.js @@ -0,0 +1,47 @@ +import createPermission from '../src/async-function/create-permission'; +import _ from 'lodash'; + +QUnit.test( "Get permission objects by querying backend with all params", async function( assert ) { + + const fetchFromBackend = async() => { + return { + taxonomyRelationFieldNames: ['field-one', 'field-two', 'field-thrid'], + permissions: { + userDisplayNames: ['jeff', 'brandon', 'brian'], + roleLabels: ['admin', 'editor'] + } + }; + }; + + /** + * @var Permission permission + */ + let permission = await createPermission(fetchFromBackend); + + assert.ok(permission.getTidToUsername().length > 0, 'tid to username key-value store is contained'); + assert.ok(permission.getTidToRoles().length > 0, 'tid to roles key-value store is contained'); + assert.ok(permission.getFieldWrapperCSSClasses().length > 0, 'field wrapper css classes are contained'); + assert.ok(permission.getTaxonomyRelationFieldNames().length > 0, 'taxonomy relation field names are contained'); +}); + +QUnit.test( "Get permission objects by querying backend with partly params", async function( assert ) { + + const fetchFromBackend = async() => { + return { + taxonomyRelationFieldNames: undefined, + permissions: { + userDisplayNames: ['jeff', 'brandon', 'brian'], + } + }; + }; + + /** + * @var Access permission + */ + let permission = await createPermission(fetchFromBackend); + + assert.ok(permission.getTidToUsername().length > 0, 'tid to username key-value store is contained'); + assert.ok(_.isEmpty(permission.getTidToRoles()), 'tid to roles key-value store is contained'); + assert.ok(_.isEmpty(permission.getFieldWrapperCSSClasses()), 'field wrapper css classes are contained'); + assert.ok(_.isEmpty(permission.getTaxonomyRelationFieldNames()), 'taxonomy relation field names are contained'); +}); \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/test/permission-output.tests.js b/web/modules/contrib/permissions_by_term/js/test/permission-output.tests.js new file mode 100644 index 000000000..f4eb10c04 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/test/permission-output.tests.js @@ -0,0 +1,82 @@ +import PermissionOutputCollector from '../src/client/permission-output-collector.prototype'; +import PermissionOutput from '../src/model/permission-output.prototype'; +import createPermission from "../src/async-function/create-permission"; +import TermCollector from "../src/client/term-collector.prototype"; +import sinon from "sinon"; + +QUnit.test("Collect output roles and usernames", async ( assert ) => { + + const fetchFromBackend = async() => { + return { + taxonomyRelationFieldNames: ['field-one', 'field-two', 'field-thrid'], + permissions: { + userDisplayNames: { + '2': ['jeff'], + '4': ['brandon', 'brian'] + }, + roleLabels: { + '1': ['admin','editor'], + '2': ['editor'] + } + } + }; + }, + domClient = { + computeTids: sinon.stub().returns(['2']) + }, + termCollector = new TermCollector; + + termCollector.addSelectedTids(domClient.computeTids(['first-field', 'second-field'])); + + /** + * @var Backend[] permissions + * @var PermissionOutputCollector permissionOutputCollector + */ + let permissions = await createPermission(fetchFromBackend), + permissionOutput = new PermissionOutput, + permissionOutputCollector = new PermissionOutputCollector(permissionOutput); + + permissionOutputCollector.collect(permissions, termCollector.getSelectedTids()); + + assert.deepEqual(permissionOutputCollector.getPermissionOutput().getRoles(), ['editor']); + assert.deepEqual(permissionOutputCollector.getPermissionOutput().getUsernames(), ['jeff']); + +}); + +QUnit.test("Collect permissions without duplicates", async (assert) => { + + const fetchFromBackend = async() => { + return { + taxonomyRelationFieldNames: ['field-one', 'field-two', 'field-thrid'], + permissions: { + userDisplayNames: { + '2': ['jeff','jeff'], + '4': ['brandon', 'brian'] + }, + roleLabels: { + '1': ['admin','admin','editor'], + '2': ['editor','editor'] + } + } + }; + }, + domClient = { + computeTids: sinon.stub().returns(['2']) + }, + termCollector = new TermCollector; + + termCollector.addSelectedTids(domClient.computeTids(['first-field', 'second-field'])); + + /** + * @var Backend[] permissions + */ + let permissions = await createPermission(fetchFromBackend), + permissionOutput = new PermissionOutput, + permissionOutputCollector = new PermissionOutputCollector(permissionOutput); + + permissionOutputCollector.collect(permissions, termCollector.getSelectedTids()); + + assert.deepEqual(permissionOutputCollector.getPermissionOutput().getRoles(), ['editor']); + assert.deepEqual(permissionOutputCollector.getPermissionOutput().getUsernames(), ['jeff']); + +}); \ No newline at end of file diff --git a/web/modules/contrib/permissions_by_term/js/test/term-selector.tests.js b/web/modules/contrib/permissions_by_term/js/test/term-selector.tests.js new file mode 100644 index 000000000..e3e405b67 --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/test/term-selector.tests.js @@ -0,0 +1,32 @@ +import DomClient from '../src/client/dom-client.prototype.js'; +import TermCollector from '../src/client/term-collector.prototype'; +import _ from 'lodash'; +import sinon from 'sinon'; + +QUnit.test( "Term selector retrieves empty array if no tids selected", function( assert ) { + + const domClient = { + computeTids: sinon.stub().returns([]) + }, + termCollector = new TermCollector; + termCollector.addSelectedTids(domClient.computeTids()); + + assert.ok(_.isEmpty(termCollector.getSelectedTids())); +}); + +QUnit.test( "Term selector retrieves array with tids if tids selected", function( assert ) { + const domClient = { + computeTids: sinon.stub().returns(['1','2','3']) + }, + termCollector = new TermCollector; + termCollector.addSelectedTids(domClient.computeTids(['first-field', 'second-field'])); + + assert.deepEqual(termCollector.getSelectedTids(), ['1','2','3']); +}); + +QUnit.test( "Term selector retrieves tid array with no duplicates", function( assert ) { + const termCollector = new TermCollector; + termCollector.addSelectedTids(['1','1','1','2','2','2']); + + assert.deepEqual(termCollector.getSelectedTids(), ['1','2']); +}); diff --git a/web/modules/contrib/permissions_by_term/js/test/tests.html b/web/modules/contrib/permissions_by_term/js/test/tests.html new file mode 100644 index 000000000..384a6459a --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/test/tests.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> + <title>Permissions by Term tests suite + + + +
+
+ + + + diff --git a/web/modules/contrib/permissions_by_term/js/webpack-dist/bundle-test.js b/web/modules/contrib/permissions_by_term/js/webpack-dist/bundle-test.js new file mode 100644 index 000000000..de478401d --- /dev/null +++ b/web/modules/contrib/permissions_by_term/js/webpack-dist/bundle-test.js @@ -0,0 +1,890 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "./node_modules/@sinonjs/formatio/lib/formatio.js": +/*!********************************************************!*\ + !*** ./node_modules/@sinonjs/formatio/lib/formatio.js ***! + \********************************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +eval("/* WEBPACK VAR INJECTION */(function(global) {var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (root, factory) {\n \"use strict\";\n\n if (true) {\n // AMD. Register as an anonymous module.\n !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! samsam */ \"./node_modules/samsam/lib/samsam.js\")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?\n\t\t\t\t(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));\n } else {}\n}(typeof self !== \"undefined\" ? self : this, function (samsam) {\n \"use strict\";\n\n var formatio = {\n excludeConstructors: [\"Object\", /^.$/],\n quoteStrings: true,\n limitChildrenCount: 0\n };\n\n var specialObjects = [];\n if (typeof global !== \"undefined\") {\n specialObjects.push({ object: global, value: \"[object global]\" });\n }\n if (typeof document !== \"undefined\") {\n specialObjects.push({\n object: document,\n value: \"[object HTMLDocument]\"\n });\n }\n if (typeof window !== \"undefined\") {\n specialObjects.push({ object: window, value: \"[object Window]\" });\n }\n\n function functionName(func) {\n if (!func) { return \"\"; }\n if (func.displayName) { return func.displayName; }\n if (func.name) { return func.name; }\n var matches = func.toString().match(/function\\s+([^\\(]+)/m);\n return (matches && matches[1]) || \"\";\n }\n\n function constructorName(f, object) {\n var name = functionName(object && object.constructor);\n var excludes = f.excludeConstructors ||\n formatio.excludeConstructors || [];\n\n var i, l;\n for (i = 0, l = excludes.length; i < l; ++i) {\n if (typeof excludes[i] === \"string\" && excludes[i] === name) {\n return \"\";\n } else if (excludes[i].test && excludes[i].test(name)) {\n return \"\";\n }\n }\n\n return name;\n }\n\n function isCircular(object, objects) {\n if (typeof object !== \"object\") { return false; }\n var i, l;\n for (i = 0, l = objects.length; i < l; ++i) {\n if (objects[i] === object) { return true; }\n }\n return false;\n }\n\n function ascii(f, object, processed, indent) {\n if (typeof object === \"string\") {\n if (object.length === 0) { return \"(empty string)\"; }\n var qs = f.quoteStrings;\n var quote = typeof qs !== \"boolean\" || qs;\n return processed || quote ? \"\\\"\" + object + \"\\\"\" : object;\n }\n\n if (typeof object === \"function\" && !(object instanceof RegExp)) {\n return ascii.func(object);\n }\n\n processed = processed || [];\n\n if (isCircular(object, processed)) { return \"[Circular]\"; }\n\n if (Object.prototype.toString.call(object) === \"[object Array]\") {\n return ascii.array.call(f, object, processed);\n }\n\n if (!object) { return String((1 / object) === -Infinity ? \"-0\" : object); }\n if (samsam.isElement(object)) { return ascii.element(object); }\n\n if (typeof object.toString === \"function\" &&\n object.toString !== Object.prototype.toString) {\n return object.toString();\n }\n\n var i, l;\n for (i = 0, l = specialObjects.length; i < l; i++) {\n if (object === specialObjects[i].object) {\n return specialObjects[i].value;\n }\n }\n\n if (typeof Set !== \"undefined\" && object instanceof Set) {\n return ascii.set.call(f, object, processed);\n }\n\n return ascii.object.call(f, object, processed, indent);\n }\n\n ascii.func = function (func) {\n return \"function \" + functionName(func) + \"() {}\";\n };\n\n function delimit(str, delimiters) {\n delimiters = delimiters || [\"[\", \"]\"];\n return delimiters[0] + str + delimiters[1];\n }\n\n ascii.array = function (array, processed, delimiters) {\n processed = processed || [];\n processed.push(array);\n var pieces = [];\n var i, l;\n l = (this.limitChildrenCount > 0) ?\n Math.min(this.limitChildrenCount, array.length) : array.length;\n\n for (i = 0; i < l; ++i) {\n pieces.push(ascii(this, array[i], processed));\n }\n\n if (l < array.length) {\n pieces.push(\"[... \" + (array.length - l) + \" more elements]\");\n }\n\n return delimit(pieces.join(\", \"), delimiters);\n };\n\n ascii.set = function (set, processed) {\n return ascii.array.call(this, Array.from(set), processed, [\"Set {\", \"}\"]);\n };\n\n ascii.object = function (object, processed, indent) {\n processed = processed || [];\n processed.push(object);\n indent = indent || 0;\n var pieces = [];\n var properties = samsam.keys(object).sort();\n var length = 3;\n var prop, str, obj, i, k, l;\n l = (this.limitChildrenCount > 0) ?\n Math.min(this.limitChildrenCount, properties.length) : properties.length;\n\n for (i = 0; i < l; ++i) {\n prop = properties[i];\n obj = object[prop];\n\n if (isCircular(obj, processed)) {\n str = \"[Circular]\";\n } else {\n str = ascii(this, obj, processed, indent + 2);\n }\n\n str = (/\\s/.test(prop) ? \"\\\"\" + prop + \"\\\"\" : prop) + \": \" + str;\n length += str.length;\n pieces.push(str);\n }\n\n var cons = constructorName(this, object);\n var prefix = cons ? \"[\" + cons + \"] \" : \"\";\n var is = \"\";\n for (i = 0, k = indent; i < k; ++i) { is += \" \"; }\n\n if (l < properties.length)\n {pieces.push(\"[... \" + (properties.length - l) + \" more elements]\");}\n\n if (length + indent > 80) {\n return prefix + \"{\\n \" + is + pieces.join(\",\\n \" + is) + \"\\n\" +\n is + \"}\";\n }\n return prefix + \"{ \" + pieces.join(\", \") + \" }\";\n };\n\n ascii.element = function (element) {\n var tagName = element.tagName.toLowerCase();\n var attrs = element.attributes;\n var pairs = [];\n var attr, attrName, i, l, val;\n\n for (i = 0, l = attrs.length; i < l; ++i) {\n attr = attrs.item(i);\n attrName = attr.nodeName.toLowerCase().replace(\"html:\", \"\");\n val = attr.nodeValue;\n if (attrName !== \"contenteditable\" || val !== \"inherit\") {\n if (val) { pairs.push(attrName + \"=\\\"\" + val + \"\\\"\"); }\n }\n }\n\n var formatted = \"<\" + tagName + (pairs.length > 0 ? \" \" : \"\");\n // SVG elements have undefined innerHTML\n var content = element.innerHTML || \"\";\n\n if (content.length > 20) {\n content = content.substr(0, 20) + \"[...]\";\n }\n\n var res = formatted + pairs.join(\" \") + \">\" + content +\n \"\";\n\n return res.replace(/ contentEditable=\"inherit\"/, \"\");\n };\n\n function Formatio(options) {\n // eslint-disable-next-line guard-for-in\n for (var opt in options) {\n this[opt] = options[opt];\n }\n }\n\n Formatio.prototype = {\n functionName: functionName,\n\n configure: function (options) {\n return new Formatio(options);\n },\n\n constructorName: function (object) {\n return constructorName(this, object);\n },\n\n ascii: function (object, processed, indent) {\n return ascii(this, object, processed, indent);\n }\n };\n\n return Formatio.prototype;\n}));\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../webpack/buildin/global.js */ \"./node_modules/webpack/buildin/global.js\")))\n\n//# sourceURL=webpack:///./node_modules/@sinonjs/formatio/lib/formatio.js?"); + +/***/ }), + +/***/ "./node_modules/diff/dist/diff.js": +/*!****************************************!*\ + !*** ./node_modules/diff/dist/diff.js ***! + \****************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +eval("/*!\n\n diff v3.5.0\n\nSoftware License Agreement (BSD License)\n\nCopyright (c) 2009-2015, Kevin Decker \n\nAll rights reserved.\n\nRedistribution and use of this software in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above\n copyright notice, this list of conditions and the\n following disclaimer.\n\n* Redistributions in binary form must reproduce the above\n copyright notice, this list of conditions and the\n following disclaimer in the documentation and/or other\n materials provided with the distribution.\n\n* Neither the name of Kevin Decker nor the names of its\n contributors may be used to endorse or promote products\n derived from this software without specific prior\n written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR\nIMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER\nIN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT\nOF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n@license\n*/\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(true)\n\t\tmodule.exports = factory();\n\telse {}\n})(this, function() {\nreturn /******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId])\n/******/ \t\t\treturn installedModules[moduleId].exports;\n\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\texports: {},\n/******/ \t\t\tid: moduleId,\n/******/ \t\t\tloaded: false\n/******/ \t\t};\n\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.loaded = true;\n\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n\n\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"\";\n\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(0);\n/******/ })\n/************************************************************************/\n/******/ ([\n/* 0 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports.canonicalize = exports.convertChangesToXML = exports.convertChangesToDMP = exports.merge = exports.parsePatch = exports.applyPatches = exports.applyPatch = exports.createPatch = exports.createTwoFilesPatch = exports.structuredPatch = exports.diffArrays = exports.diffJson = exports.diffCss = exports.diffSentences = exports.diffTrimmedLines = exports.diffLines = exports.diffWordsWithSpace = exports.diffWords = exports.diffChars = exports.Diff = undefined;\n\n\t/*istanbul ignore end*/var /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _base2 = _interopRequireDefault(_base);\n\n\t/*istanbul ignore end*/var /*istanbul ignore start*/_character = __webpack_require__(2) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_word = __webpack_require__(3) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_line = __webpack_require__(5) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_sentence = __webpack_require__(6) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_css = __webpack_require__(7) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_json = __webpack_require__(8) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_array = __webpack_require__(9) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_apply = __webpack_require__(10) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_parse = __webpack_require__(11) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_merge = __webpack_require__(13) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_create = __webpack_require__(14) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_dmp = __webpack_require__(16) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_xml = __webpack_require__(17) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/* See LICENSE file for terms of use */\n\n\t/*\n\t * Text diff implementation.\n\t *\n\t * This library supports the following APIS:\n\t * JsDiff.diffChars: Character by character diff\n\t * JsDiff.diffWords: Word (as defined by \\b regex) diff which ignores whitespace\n\t * JsDiff.diffLines: Line based diff\n\t *\n\t * JsDiff.diffCss: Diff targeted at CSS content\n\t *\n\t * These methods are based on the implementation proposed in\n\t * \"An O(ND) Difference Algorithm and its Variations\" (Myers, 1986).\n\t * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927\n\t */\n\texports. /*istanbul ignore end*/Diff = _base2['default'];\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffChars = _character.diffChars;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffWords = _word.diffWords;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffWordsWithSpace = _word.diffWordsWithSpace;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffLines = _line.diffLines;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffTrimmedLines = _line.diffTrimmedLines;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffSentences = _sentence.diffSentences;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffCss = _css.diffCss;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffJson = _json.diffJson;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffArrays = _array.diffArrays;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/structuredPatch = _create.structuredPatch;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/createTwoFilesPatch = _create.createTwoFilesPatch;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/createPatch = _create.createPatch;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/applyPatch = _apply.applyPatch;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/applyPatches = _apply.applyPatches;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/parsePatch = _parse.parsePatch;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/merge = _merge.merge;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/convertChangesToDMP = _dmp.convertChangesToDMP;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/convertChangesToXML = _xml.convertChangesToXML;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/canonicalize = _json.canonicalize;\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC5qcyJdLCJuYW1lcyI6WyJEaWZmIiwiZGlmZkNoYXJzIiwiZGlmZldvcmRzIiwiZGlmZldvcmRzV2l0aFNwYWNlIiwiZGlmZkxpbmVzIiwiZGlmZlRyaW1tZWRMaW5lcyIsImRpZmZTZW50ZW5jZXMiLCJkaWZmQ3NzIiwiZGlmZkpzb24iLCJkaWZmQXJyYXlzIiwic3RydWN0dXJlZFBhdGNoIiwiY3JlYXRlVHdvRmlsZXNQYXRjaCIsImNyZWF0ZVBhdGNoIiwiYXBwbHlQYXRjaCIsImFwcGx5UGF0Y2hlcyIsInBhcnNlUGF0Y2giLCJtZXJnZSIsImNvbnZlcnRDaGFuZ2VzVG9ETVAiLCJjb252ZXJ0Q2hhbmdlc1RvWE1MIiwiY2Fub25pY2FsaXplIl0sIm1hcHBpbmdzIjoiOzs7Ozt1QkFnQkE7Ozs7dUJBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBRUE7O0FBQ0E7O0FBRUE7O0FBRUE7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBRUE7O0FBQ0E7Ozs7QUFqQ0E7O0FBRUE7Ozs7Ozs7Ozs7Ozs7O2dDQWtDRUEsSTt5REFFQUMsUzt5REFDQUMsUzt5REFDQUMsa0I7eURBQ0FDLFM7eURBQ0FDLGdCO3lEQUNBQyxhO3lEQUVBQyxPO3lEQUNBQyxRO3lEQUVBQyxVO3lEQUVBQyxlO3lEQUNBQyxtQjt5REFDQUMsVzt5REFDQUMsVTt5REFDQUMsWTt5REFDQUMsVTt5REFDQUMsSzt5REFDQUMsbUI7eURBQ0FDLG1CO3lEQUNBQyxZIiwiZmlsZSI6ImluZGV4LmpzIiwic291cmNlc0NvbnRlbnQiOlsiLyogU2VlIExJQ0VOU0UgZmlsZSBmb3IgdGVybXMgb2YgdXNlICovXG5cbi8qXG4gKiBUZXh0IGRpZmYgaW1wbGVtZW50YXRpb24uXG4gKlxuICogVGhpcyBsaWJyYXJ5IHN1cHBvcnRzIHRoZSBmb2xsb3dpbmcgQVBJUzpcbiAqIEpzRGlmZi5kaWZmQ2hhcnM6IENoYXJhY3RlciBieSBjaGFyYWN0ZXIgZGlmZlxuICogSnNEaWZmLmRpZmZXb3JkczogV29yZCAoYXMgZGVmaW5lZCBieSBcXGIgcmVnZXgpIGRpZmYgd2hpY2ggaWdub3JlcyB3aGl0ZXNwYWNlXG4gKiBKc0RpZmYuZGlmZkxpbmVzOiBMaW5lIGJhc2VkIGRpZmZcbiAqXG4gKiBKc0RpZmYuZGlmZkNzczogRGlmZiB0YXJnZXRlZCBhdCBDU1MgY29udGVudFxuICpcbiAqIFRoZXNlIG1ldGhvZHMgYXJlIGJhc2VkIG9uIHRoZSBpbXBsZW1lbnRhdGlvbiBwcm9wb3NlZCBpblxuICogXCJBbiBPKE5EKSBEaWZmZXJlbmNlIEFsZ29yaXRobSBhbmQgaXRzIFZhcmlhdGlvbnNcIiAoTXllcnMsIDE5ODYpLlxuICogaHR0cDovL2NpdGVzZWVyeC5pc3QucHN1LmVkdS92aWV3ZG9jL3N1bW1hcnk/ZG9pPTEwLjEuMS40LjY5MjdcbiAqL1xuaW1wb3J0IERpZmYgZnJvbSAnLi9kaWZmL2Jhc2UnO1xuaW1wb3J0IHtkaWZmQ2hhcnN9IGZyb20gJy4vZGlmZi9jaGFyYWN0ZXInO1xuaW1wb3J0IHtkaWZmV29yZHMsIGRpZmZXb3Jkc1dpdGhTcGFjZX0gZnJvbSAnLi9kaWZmL3dvcmQnO1xuaW1wb3J0IHtkaWZmTGluZXMsIGRpZmZUcmltbWVkTGluZXN9IGZyb20gJy4vZGlmZi9saW5lJztcbmltcG9ydCB7ZGlmZlNlbnRlbmNlc30gZnJvbSAnLi9kaWZmL3NlbnRlbmNlJztcblxuaW1wb3J0IHtkaWZmQ3NzfSBmcm9tICcuL2RpZmYvY3NzJztcbmltcG9ydCB7ZGlmZkpzb24sIGNhbm9uaWNhbGl6ZX0gZnJvbSAnLi9kaWZmL2pzb24nO1xuXG5pbXBvcnQge2RpZmZBcnJheXN9IGZyb20gJy4vZGlmZi9hcnJheSc7XG5cbmltcG9ydCB7YXBwbHlQYXRjaCwgYXBwbHlQYXRjaGVzfSBmcm9tICcuL3BhdGNoL2FwcGx5JztcbmltcG9ydCB7cGFyc2VQYXRjaH0gZnJvbSAnLi9wYXRjaC9wYXJzZSc7XG5pbXBvcnQge21lcmdlfSBmcm9tICcuL3BhdGNoL21lcmdlJztcbmltcG9ydCB7c3RydWN0dXJlZFBhdGNoLCBjcmVhdGVUd29GaWxlc1BhdGNoLCBjcmVhdGVQYXRjaH0gZnJvbSAnLi9wYXRjaC9jcmVhdGUnO1xuXG5pbXBvcnQge2NvbnZlcnRDaGFuZ2VzVG9ETVB9IGZyb20gJy4vY29udmVydC9kbXAnO1xuaW1wb3J0IHtjb252ZXJ0Q2hhbmdlc1RvWE1MfSBmcm9tICcuL2NvbnZlcnQveG1sJztcblxuZXhwb3J0IHtcbiAgRGlmZixcblxuICBkaWZmQ2hhcnMsXG4gIGRpZmZXb3JkcyxcbiAgZGlmZldvcmRzV2l0aFNwYWNlLFxuICBkaWZmTGluZXMsXG4gIGRpZmZUcmltbWVkTGluZXMsXG4gIGRpZmZTZW50ZW5jZXMsXG5cbiAgZGlmZkNzcyxcbiAgZGlmZkpzb24sXG5cbiAgZGlmZkFycmF5cyxcblxuICBzdHJ1Y3R1cmVkUGF0Y2gsXG4gIGNyZWF0ZVR3b0ZpbGVzUGF0Y2gsXG4gIGNyZWF0ZVBhdGNoLFxuICBhcHBseVBhdGNoLFxuICBhcHBseVBhdGNoZXMsXG4gIHBhcnNlUGF0Y2gsXG4gIG1lcmdlLFxuICBjb252ZXJ0Q2hhbmdlc1RvRE1QLFxuICBjb252ZXJ0Q2hhbmdlc1RvWE1MLFxuICBjYW5vbmljYWxpemVcbn07XG4iXX0=\n\n\n/***/ }),\n/* 1 */\n/***/ (function(module, exports) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports['default'] = /*istanbul ignore end*/Diff;\n\tfunction Diff() {}\n\n\tDiff.prototype = {\n\t /*istanbul ignore start*/ /*istanbul ignore end*/diff: function diff(oldString, newString) {\n\t /*istanbul ignore start*/var /*istanbul ignore end*/options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n\n\t var callback = options.callback;\n\t if (typeof options === 'function') {\n\t callback = options;\n\t options = {};\n\t }\n\t this.options = options;\n\n\t var self = this;\n\n\t function done(value) {\n\t if (callback) {\n\t setTimeout(function () {\n\t callback(undefined, value);\n\t }, 0);\n\t return true;\n\t } else {\n\t return value;\n\t }\n\t }\n\n\t // Allow subclasses to massage the input prior to running\n\t oldString = this.castInput(oldString);\n\t newString = this.castInput(newString);\n\n\t oldString = this.removeEmpty(this.tokenize(oldString));\n\t newString = this.removeEmpty(this.tokenize(newString));\n\n\t var newLen = newString.length,\n\t oldLen = oldString.length;\n\t var editLength = 1;\n\t var maxEditLength = newLen + oldLen;\n\t var bestPath = [{ newPos: -1, components: [] }];\n\n\t // Seed editLength = 0, i.e. the content starts with the same values\n\t var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0);\n\t if (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen) {\n\t // Identity per the equality and tokenizer\n\t return done([{ value: this.join(newString), count: newString.length }]);\n\t }\n\n\t // Main worker method. checks all permutations of a given edit length for acceptance.\n\t function execEditLength() {\n\t for (var diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) {\n\t var basePath = /*istanbul ignore start*/void 0 /*istanbul ignore end*/;\n\t var addPath = bestPath[diagonalPath - 1],\n\t removePath = bestPath[diagonalPath + 1],\n\t _oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;\n\t if (addPath) {\n\t // No one else is going to attempt to use this value, clear it\n\t bestPath[diagonalPath - 1] = undefined;\n\t }\n\n\t var canAdd = addPath && addPath.newPos + 1 < newLen,\n\t canRemove = removePath && 0 <= _oldPos && _oldPos < oldLen;\n\t if (!canAdd && !canRemove) {\n\t // If this path is a terminal then prune\n\t bestPath[diagonalPath] = undefined;\n\t continue;\n\t }\n\n\t // Select the diagonal that we want to branch from. We select the prior\n\t // path whose position in the new string is the farthest from the origin\n\t // and does not pass the bounds of the diff graph\n\t if (!canAdd || canRemove && addPath.newPos < removePath.newPos) {\n\t basePath = clonePath(removePath);\n\t self.pushComponent(basePath.components, undefined, true);\n\t } else {\n\t basePath = addPath; // No need to clone, we've pulled it from the list\n\t basePath.newPos++;\n\t self.pushComponent(basePath.components, true, undefined);\n\t }\n\n\t _oldPos = self.extractCommon(basePath, newString, oldString, diagonalPath);\n\n\t // If we have hit the end of both strings, then we are done\n\t if (basePath.newPos + 1 >= newLen && _oldPos + 1 >= oldLen) {\n\t return done(buildValues(self, basePath.components, newString, oldString, self.useLongestToken));\n\t } else {\n\t // Otherwise track this path as a potential candidate and continue.\n\t bestPath[diagonalPath] = basePath;\n\t }\n\t }\n\n\t editLength++;\n\t }\n\n\t // Performs the length of edit iteration. Is a bit fugly as this has to support the\n\t // sync and async mode which is never fun. Loops over execEditLength until a value\n\t // is produced.\n\t if (callback) {\n\t (function exec() {\n\t setTimeout(function () {\n\t // This should not happen, but we want to be safe.\n\t /* istanbul ignore next */\n\t if (editLength > maxEditLength) {\n\t return callback();\n\t }\n\n\t if (!execEditLength()) {\n\t exec();\n\t }\n\t }, 0);\n\t })();\n\t } else {\n\t while (editLength <= maxEditLength) {\n\t var ret = execEditLength();\n\t if (ret) {\n\t return ret;\n\t }\n\t }\n\t }\n\t },\n\t /*istanbul ignore start*/ /*istanbul ignore end*/pushComponent: function pushComponent(components, added, removed) {\n\t var last = components[components.length - 1];\n\t if (last && last.added === added && last.removed === removed) {\n\t // We need to clone here as the component clone operation is just\n\t // as shallow array clone\n\t components[components.length - 1] = { count: last.count + 1, added: added, removed: removed };\n\t } else {\n\t components.push({ count: 1, added: added, removed: removed });\n\t }\n\t },\n\t /*istanbul ignore start*/ /*istanbul ignore end*/extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) {\n\t var newLen = newString.length,\n\t oldLen = oldString.length,\n\t newPos = basePath.newPos,\n\t oldPos = newPos - diagonalPath,\n\t commonCount = 0;\n\t while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {\n\t newPos++;\n\t oldPos++;\n\t commonCount++;\n\t }\n\n\t if (commonCount) {\n\t basePath.components.push({ count: commonCount });\n\t }\n\n\t basePath.newPos = newPos;\n\t return oldPos;\n\t },\n\t /*istanbul ignore start*/ /*istanbul ignore end*/equals: function equals(left, right) {\n\t if (this.options.comparator) {\n\t return this.options.comparator(left, right);\n\t } else {\n\t return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();\n\t }\n\t },\n\t /*istanbul ignore start*/ /*istanbul ignore end*/removeEmpty: function removeEmpty(array) {\n\t var ret = [];\n\t for (var i = 0; i < array.length; i++) {\n\t if (array[i]) {\n\t ret.push(array[i]);\n\t }\n\t }\n\t return ret;\n\t },\n\t /*istanbul ignore start*/ /*istanbul ignore end*/castInput: function castInput(value) {\n\t return value;\n\t },\n\t /*istanbul ignore start*/ /*istanbul ignore end*/tokenize: function tokenize(value) {\n\t return value.split('');\n\t },\n\t /*istanbul ignore start*/ /*istanbul ignore end*/join: function join(chars) {\n\t return chars.join('');\n\t }\n\t};\n\n\tfunction buildValues(diff, components, newString, oldString, useLongestToken) {\n\t var componentPos = 0,\n\t componentLen = components.length,\n\t newPos = 0,\n\t oldPos = 0;\n\n\t for (; componentPos < componentLen; componentPos++) {\n\t var component = components[componentPos];\n\t if (!component.removed) {\n\t if (!component.added && useLongestToken) {\n\t var value = newString.slice(newPos, newPos + component.count);\n\t value = value.map(function (value, i) {\n\t var oldValue = oldString[oldPos + i];\n\t return oldValue.length > value.length ? oldValue : value;\n\t });\n\n\t component.value = diff.join(value);\n\t } else {\n\t component.value = diff.join(newString.slice(newPos, newPos + component.count));\n\t }\n\t newPos += component.count;\n\n\t // Common case\n\t if (!component.added) {\n\t oldPos += component.count;\n\t }\n\t } else {\n\t component.value = diff.join(oldString.slice(oldPos, oldPos + component.count));\n\t oldPos += component.count;\n\n\t // Reverse add and remove so removes are output first to match common convention\n\t // The diffing algorithm is tied to add then remove output and this is the simplest\n\t // route to get the desired output with minimal overhead.\n\t if (componentPos && components[componentPos - 1].added) {\n\t var tmp = components[componentPos - 1];\n\t components[componentPos - 1] = components[componentPos];\n\t components[componentPos] = tmp;\n\t }\n\t }\n\t }\n\n\t // Special case handle for when one terminal is ignored (i.e. whitespace).\n\t // For this case we merge the terminal into the prior string and drop the change.\n\t // This is only available for string mode.\n\t var lastComponent = components[componentLen - 1];\n\t if (componentLen > 1 && typeof lastComponent.value === 'string' && (lastComponent.added || lastComponent.removed) && diff.equals('', lastComponent.value)) {\n\t components[componentLen - 2].value += lastComponent.value;\n\t components.pop();\n\t }\n\n\t return components;\n\t}\n\n\tfunction clonePath(path) {\n\t return { newPos: path.newPos, components: path.components.slice(0) };\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaWZmL2Jhc2UuanMiXSwibmFtZXMiOlsiRGlmZiIsInByb3RvdHlwZSIsImRpZmYiLCJvbGRTdHJpbmciLCJuZXdTdHJpbmciLCJvcHRpb25zIiwiY2FsbGJhY2siLCJzZWxmIiwiZG9uZSIsInZhbHVlIiwic2V0VGltZW91dCIsInVuZGVmaW5lZCIsImNhc3RJbnB1dCIsInJlbW92ZUVtcHR5IiwidG9rZW5pemUiLCJuZXdMZW4iLCJsZW5ndGgiLCJvbGRMZW4iLCJlZGl0TGVuZ3RoIiwibWF4RWRpdExlbmd0aCIsImJlc3RQYXRoIiwibmV3UG9zIiwiY29tcG9uZW50cyIsIm9sZFBvcyIsImV4dHJhY3RDb21tb24iLCJqb2luIiwiY291bnQiLCJleGVjRWRpdExlbmd0aCIsImRpYWdvbmFsUGF0aCIsImJhc2VQYXRoIiwiYWRkUGF0aCIsInJlbW92ZVBhdGgiLCJjYW5BZGQiLCJjYW5SZW1vdmUiLCJjbG9uZVBhdGgiLCJwdXNoQ29tcG9uZW50IiwiYnVpbGRWYWx1ZXMiLCJ1c2VMb25nZXN0VG9rZW4iLCJleGVjIiwicmV0IiwiYWRkZWQiLCJyZW1vdmVkIiwibGFzdCIsInB1c2giLCJjb21tb25Db3VudCIsImVxdWFscyIsImxlZnQiLCJyaWdodCIsImNvbXBhcmF0b3IiLCJpZ25vcmVDYXNlIiwidG9Mb3dlckNhc2UiLCJhcnJheSIsImkiLCJzcGxpdCIsImNoYXJzIiwiY29tcG9uZW50UG9zIiwiY29tcG9uZW50TGVuIiwiY29tcG9uZW50Iiwic2xpY2UiLCJtYXAiLCJvbGRWYWx1ZSIsInRtcCIsImxhc3RDb21wb25lbnQiLCJwb3AiLCJwYXRoIl0sIm1hcHBpbmdzIjoiOzs7NENBQXdCQSxJO0FBQVQsU0FBU0EsSUFBVCxHQUFnQixDQUFFOztBQUVqQ0EsS0FBS0MsU0FBTCxHQUFpQjtBQUFBLG1EQUNmQyxJQURlLGdCQUNWQyxTQURVLEVBQ0NDLFNBREQsRUFDMEI7QUFBQSx3REFBZEMsT0FBYyx1RUFBSixFQUFJOztBQUN2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RUFBd0JBLFNBQVNrQyxVQUFVL0IsS0FBM0MsQ0FBVixDQUFsQjtBQUNBSCxnQkFBVWtDLFVBQVUvQixLQUFwQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxVQUFJNkIsZ0JBQWdCakMsV0FBV2lDLGVBQWUsQ0FBMUIsRUFBNkJmLEtBQWpELEVBQXdEO0FBQ3RELFlBQUlxQixNQUFNdkMsV0FBV2lDLGVBQWUsQ0FBMUIsQ0FBVjtBQUNBakMsbUJBQVdpQyxlQUFlLENBQTFCLElBQStCakMsV0FBV2lDLFlBQVgsQ0FBL0I7QUFDQWpDLG1CQUFXaUMsWUFBWCxJQUEyQk0sR0FBM0I7QUFDRDtBQUNGO0FBQ0Y7O0FBRUQ7QUFDQTtBQUNBO0FBQ0EsTUFBSUMsZ0JBQWdCeEMsV0FBV2tDLGVBQWUsQ0FBMUIsQ0FBcEI7QUFDQSxNQUFJQSxlQUFlLENBQWYsSUFDRyxPQUFPTSxjQUFjckQsS0FBckIsS0FBK0IsUUFEbEMsS0FFSXFELGNBQWN0QixLQUFkLElBQXVCc0IsY0FBY3JCLE9BRnpDLEtBR0d2QyxLQUFLMkMsTUFBTCxDQUFZLEVBQVosRUFBZ0JpQixjQUFjckQsS0FBOUIsQ0FIUCxFQUc2QztBQUMzQ2EsZUFBV2tDLGVBQWUsQ0FBMUIsRUFBNkIvQyxLQUE3QixJQUFzQ3FELGNBQWNyRCxLQUFwRDtBQUNBYSxlQUFXeUMsR0FBWDtBQUNEOztBQUVELFNBQU96QyxVQUFQO0FBQ0Q7O0FBRUQsU0FBU1ksU0FBVCxDQUFtQjhCLElBQW5CLEVBQXlCO0FBQ3ZCLFNBQU8sRUFBRTNDLFFBQVEyQyxLQUFLM0MsTUFBZixFQUF1QkMsWUFBWTBDLEtBQUsxQyxVQUFMLENBQWdCb0MsS0FBaEIsQ0FBc0IsQ0FBdEIsQ0FBbkMsRUFBUDtBQUNEIiwiZmlsZSI6ImJhc2UuanMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBEaWZmKCkge31cblxuRGlmZi5wcm90b3R5cGUgPSB7XG4gIGRpZmYob2xkU3RyaW5nLCBuZXdTdHJpbmcsIG9wdGlvbnMgPSB7fSkge1xuICAgIGxldCBjYWxsYmFjayA9IG9wdGlvbnMuY2FsbGJhY2s7XG4gICAgaWYgKHR5cGVvZiBvcHRpb25zID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICBjYWxsYmFjayA9IG9wdGlvbnM7XG4gICAgICBvcHRpb25zID0ge307XG4gICAgfVxuICAgIHRoaXMub3B0aW9ucyA9IG9wdGlvbnM7XG5cbiAgICBsZXQgc2VsZiA9IHRoaXM7XG5cbiAgICBmdW5jdGlvbiBkb25lKHZhbHVlKSB7XG4gICAgICBpZiAoY2FsbGJhY2spIHtcbiAgICAgICAgc2V0VGltZW91dChmdW5jdGlvbigpIHsgY2FsbGJhY2sodW5kZWZpbmVkLCB2YWx1ZSk7IH0sIDApO1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJldHVybiB2YWx1ZTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvLyBBbGxvdyBzdWJjbGFzc2VzIHRvIG1hc3NhZ2UgdGhlIGlucHV0IHByaW9yIHRvIHJ1bm5pbmdcbiAgICBvbGRTdHJpbmcgPSB0aGlzLmNhc3RJbnB1dChvbGRTdHJpbmcpO1xuICAgIG5ld1N0cmluZyA9IHRoaXMuY2FzdElucHV0KG5ld1N0cmluZyk7XG5cbiAgICBvbGRTdHJpbmcgPSB0aGlzLnJlbW92ZUVtcHR5KHRoaXMudG9rZW5pemUob2xkU3RyaW5nKSk7XG4gICAgbmV3U3RyaW5nID0gdGhpcy5yZW1vdmVFbXB0eSh0aGlzLnRva2VuaXplKG5ld1N0cmluZykpO1xuXG4gICAgbGV0IG5ld0xlbiA9IG5ld1N0cmluZy5sZW5ndGgsIG9sZExlbiA9IG9sZFN0cmluZy5sZW5ndGg7XG4gICAgbGV0IGVkaXRMZW5ndGggPSAxO1xuICAgIGxldCBtYXhFZGl0TGVuZ3RoID0gbmV3TGVuICsgb2xkTGVuO1xuICAgIGxldCBiZXN0UGF0aCA9IFt7IG5ld1BvczogLTEsIGNvbXBvbmVudHM6IFtdIH1dO1xuXG4gICAgLy8gU2VlZCBlZGl0TGVuZ3RoID0gMCwgaS5lLiB0aGUgY29udGVudCBzdGFydHMgd2l0aCB0aGUgc2FtZSB2YWx1ZXNcbiAgICBsZXQgb2xkUG9zID0gdGhpcy5leHRyYWN0Q29tbW9uKGJlc3RQYXRoWzBdLCBuZXdTdHJpbmcsIG9sZFN0cmluZywgMCk7XG4gICAgaWYgKGJlc3RQYXRoWzBdLm5ld1BvcyArIDEgPj0gbmV3TGVuICYmIG9sZFBvcyArIDEgPj0gb2xkTGVuKSB7XG4gICAgICAvLyBJZGVudGl0eSBwZXIgdGhlIGVxdWFsaXR5IGFuZCB0b2tlbml6ZXJcbiAgICAgIHJldHVybiBkb25lKFt7dmFsdWU6IHRoaXMuam9pbihuZXdTdHJpbmcpLCBjb3VudDogbmV3U3RyaW5nLmxlbmd0aH1dKTtcbiAgICB9XG5cbiAgICAvLyBNYWluIHdvcmtlciBtZXRob2QuIGNoZWNrcyBhbGwgcGVybXV0YXRpb25zIG9mIGEgZ2l2ZW4gZWRpdCBsZW5ndGggZm9yIGFjY2VwdGFuY2UuXG4gICAgZnVuY3Rpb24gZXhlY0VkaXRMZW5ndGgoKSB7XG4gICAgICBmb3IgKGxldCBkaWFnb25hbFBhdGggPSAtMSAqIGVkaXRMZW5ndGg7IGRpYWdvbmFsUGF0aCA8PSBlZGl0TGVuZ3RoOyBkaWFnb25hbFBhdGggKz0gMikge1xuICAgICAgICBsZXQgYmFzZVBhdGg7XG4gICAgICAgIGxldCBhZGRQYXRoID0gYmVzdFBhdGhbZGlhZ29uYWxQYXRoIC0gMV0sXG4gICAgICAgICAgICByZW1vdmVQYXRoID0gYmVzdFBhdGhbZGlhZ29uYWxQYXRoICsgMV0sXG4gICAgICAgICAgICBvbGRQb3MgPSAocmVtb3ZlUGF0aCA/IHJlbW92ZVBhdGgubmV3UG9zIDogMCkgLSBkaWFnb25hbFBhdGg7XG4gICAgICAgIGlmIChhZGRQYXRoKSB7XG4gICAgICAgICAgLy8gTm8gb25lIGVsc2UgaXMgZ29pbmcgdG8gYXR0ZW1wdCB0byB1c2UgdGhpcyB2YWx1ZSwgY2xlYXIgaXRcbiAgICAgICAgICBiZXN0UGF0aFtkaWFnb25hbFBhdGggLSAxXSA9IHVuZGVmaW5lZDtcbiAgICAgICAgfVxuXG4gICAgICAgIGxldCBjYW5BZGQgPSBhZGRQYXRoICYmIGFkZFBhdGgubmV3UG9zICsgMSA8IG5ld0xlbixcbiAgICAgICAgICAgIGNhblJlbW92ZSA9IHJlbW92ZVBhdGggJiYgMCA8PSBvbGRQb3MgJiYgb2xkUG9zIDwgb2xkTGVuO1xuICAgICAgICBpZiAoIWNhbkFkZCAmJiAhY2FuUmVtb3ZlKSB7XG4gICAgICAgICAgLy8gSWYgdGhpcyBwYXRoIGlzIGEgdGVybWluYWwgdGhlbiBwcnVuZVxuICAgICAgICAgIGJlc3RQYXRoW2RpYWdvbmFsUGF0aF0gPSB1bmRlZmluZWQ7XG4gICAgICAgICAgY29udGludWU7XG4gICAgICAgIH1cblxuICAgICAgICAvLyBTZWxlY3QgdGhlIGRpYWdvbmFsIHRoYXQgd2Ugd2FudCB0byBicmFuY2ggZnJvbS4gV2Ugc2VsZWN0IHRoZSBwcmlvclxuICAgICAgICAvLyBwYXRoIHdob3NlIHBvc2l0aW9uIGluIHRoZSBuZXcgc3RyaW5nIGlzIHRoZSBmYXJ0aGVzdCBmcm9tIHRoZSBvcmlnaW5cbiAgICAgICAgLy8gYW5kIGRvZXMgbm90IHBhc3MgdGhlIGJvdW5kcyBvZiB0aGUgZGlmZiBncmFwaFxuICAgICAgICBpZiAoIWNhbkFkZCB8fCAoY2FuUmVtb3ZlICYmIGFkZFBhdGgubmV3UG9zIDwgcmVtb3ZlUGF0aC5uZXdQb3MpKSB7XG4gICAgICAgICAgYmFzZVBhdGggPSBjbG9uZVBhdGgocmVtb3ZlUGF0aCk7XG4gICAgICAgICAgc2VsZi5wdXNoQ29tcG9uZW50KGJhc2VQYXRoLmNvbXBvbmVudHMsIHVuZGVmaW5lZCwgdHJ1ZSk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgYmFzZVBhdGggPSBhZGRQYXRoOyAgIC8vIE5vIG5lZWQgdG8gY2xvbmUsIHdlJ3ZlIHB1bGxlZCBpdCBmcm9tIHRoZSBsaXN0XG4gICAgICAgICAgYmFzZVBhdGgubmV3UG9zKys7XG4gICAgICAgICAgc2VsZi5wdXNoQ29tcG9uZW50KGJhc2VQYXRoLmNvbXBvbmVudHMsIHRydWUsIHVuZGVmaW5lZCk7XG4gICAgICAgIH1cblxuICAgICAgICBvbGRQb3MgPSBzZWxmLmV4dHJhY3RDb21tb24oYmFzZVBhdGgsIG5ld1N0cmluZywgb2xkU3RyaW5nLCBkaWFnb25hbFBhdGgpO1xuXG4gICAgICAgIC8vIElmIHdlIGhhdmUgaGl0IHRoZSBlbmQgb2YgYm90aCBzdHJpbmdzLCB0aGVuIHdlIGFyZSBkb25lXG4gICAgICAgIGlmIChiYXNlUGF0aC5uZXdQb3MgKyAxID49IG5ld0xlbiAmJiBvbGRQb3MgKyAxID49IG9sZExlbikge1xuICAgICAgICAgIHJldHVybiBkb25lKGJ1aWxkVmFsdWVzKHNlbGYsIGJhc2VQYXRoLmNvbXBvbmVudHMsIG5ld1N0cmluZywgb2xkU3RyaW5nLCBzZWxmLnVzZUxvbmdlc3RUb2tlbikpO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIC8vIE90aGVyd2lzZSB0cmFjayB0aGlzIHBhdGggYXMgYSBwb3RlbnRpYWwgY2FuZGlkYXRlIGFuZCBjb250aW51ZS5cbiAgICAgICAgICBiZXN0UGF0aFtkaWFnb25hbFBhdGhdID0gYmFzZVBhdGg7XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgZWRpdExlbmd0aCsrO1xuICAgIH1cblxuICAgIC8vIFBlcmZvcm1zIHRoZSBsZW5ndGggb2YgZWRpdCBpdGVyYXRpb24uIElzIGEgYml0IGZ1Z2x5IGFzIHRoaXMgaGFzIHRvIHN1cHBvcnQgdGhlXG4gICAgLy8gc3luYyBhbmQgYXN5bmMgbW9kZSB3aGljaCBpcyBuZXZlciBmdW4uIExvb3BzIG92ZXIgZXhlY0VkaXRMZW5ndGggdW50aWwgYSB2YWx1ZVxuICAgIC8vIGlzIHByb2R1Y2VkLlxuICAgIGlmIChjYWxsYmFjaykge1xuICAgICAgKGZ1bmN0aW9uIGV4ZWMoKSB7XG4gICAgICAgIHNldFRpbWVvdXQoZnVuY3Rpb24oKSB7XG4gICAgICAgICAgLy8gVGhpcyBzaG91bGQgbm90IGhhcHBlbiwgYnV0IHdlIHdhbnQgdG8gYmUgc2FmZS5cbiAgICAgICAgICAvKiBpc3RhbmJ1bCBpZ25vcmUgbmV4dCAqL1xuICAgICAgICAgIGlmIChlZGl0TGVuZ3RoID4gbWF4RWRpdExlbmd0aCkge1xuICAgICAgICAgICAgcmV0dXJuIGNhbGxiYWNrKCk7XG4gICAgICAgICAgfVxuXG4gICAgICAgICAgaWYgKCFleGVjRWRpdExlbmd0aCgpKSB7XG4gICAgICAgICAgICBleGVjKCk7XG4gICAgICAgICAgfVxuICAgICAgICB9LCAwKTtcbiAgICAgIH0oKSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHdoaWxlIChlZGl0TGVuZ3RoIDw9IG1heEVkaXRMZW5ndGgpIHtcbiAgICAgICAgbGV0IHJldCA9IGV4ZWNFZGl0TGVuZ3RoKCk7XG4gICAgICAgIGlmIChyZXQpIHtcbiAgICAgICAgICByZXR1cm4gcmV0O1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9LFxuXG4gIHB1c2hDb21wb25lbnQoY29tcG9uZW50cywgYWRkZWQsIHJlbW92ZWQpIHtcbiAgICBsZXQgbGFzdCA9IGNvbXBvbmVudHNbY29tcG9uZW50cy5sZW5ndGggLSAxXTtcbiAgICBpZiAobGFzdCAmJiBsYXN0LmFkZGVkID09PSBhZGRlZCAmJiBsYXN0LnJlbW92ZWQgPT09IHJlbW92ZWQpIHtcbiAgICAgIC8vIFdlIG5lZWQgdG8gY2xvbmUgaGVyZSBhcyB0aGUgY29tcG9uZW50IGNsb25lIG9wZXJhdGlvbiBpcyBqdXN0XG4gICAgICAvLyBhcyBzaGFsbG93IGFycmF5IGNsb25lXG4gICAgICBjb21wb25lbnRzW2NvbXBvbmVudHMubGVuZ3RoIC0gMV0gPSB7Y291bnQ6IGxhc3QuY291bnQgKyAxLCBhZGRlZDogYWRkZWQsIHJlbW92ZWQ6IHJlbW92ZWQgfTtcbiAgICB9IGVsc2Uge1xuICAgICAgY29tcG9uZW50cy5wdXNoKHtjb3VudDogMSwgYWRkZWQ6IGFkZGVkLCByZW1vdmVkOiByZW1vdmVkIH0pO1xuICAgIH1cbiAgfSxcbiAgZXh0cmFjdENvbW1vbihiYXNlUGF0aCwgbmV3U3RyaW5nLCBvbGRTdHJpbmcsIGRpYWdvbmFsUGF0aCkge1xuICAgIGxldCBuZXdMZW4gPSBuZXdTdHJpbmcubGVuZ3RoLFxuICAgICAgICBvbGRMZW4gPSBvbGRTdHJpbmcubGVuZ3RoLFxuICAgICAgICBuZXdQb3MgPSBiYXNlUGF0aC5uZXdQb3MsXG4gICAgICAgIG9sZFBvcyA9IG5ld1BvcyAtIGRpYWdvbmFsUGF0aCxcblxuICAgICAgICBjb21tb25Db3VudCA9IDA7XG4gICAgd2hpbGUgKG5ld1BvcyArIDEgPCBuZXdMZW4gJiYgb2xkUG9zICsgMSA8IG9sZExlbiAmJiB0aGlzLmVxdWFscyhuZXdTdHJpbmdbbmV3UG9zICsgMV0sIG9sZFN0cmluZ1tvbGRQb3MgKyAxXSkpIHtcbiAgICAgIG5ld1BvcysrO1xuICAgICAgb2xkUG9zKys7XG4gICAgICBjb21tb25Db3VudCsrO1xuICAgIH1cblxuICAgIGlmIChjb21tb25Db3VudCkge1xuICAgICAgYmFzZVBhdGguY29tcG9uZW50cy5wdXNoKHtjb3VudDogY29tbW9uQ291bnR9KTtcbiAgICB9XG5cbiAgICBiYXNlUGF0aC5uZXdQb3MgPSBuZXdQb3M7XG4gICAgcmV0dXJuIG9sZFBvcztcbiAgfSxcblxuICBlcXVhbHMobGVmdCwgcmlnaHQpIHtcbiAgICBpZiAodGhpcy5vcHRpb25zLmNvbXBhcmF0b3IpIHtcbiAgICAgIHJldHVybiB0aGlzLm9wdGlvbnMuY29tcGFyYXRvcihsZWZ0LCByaWdodCk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHJldHVybiBsZWZ0ID09PSByaWdodFxuICAgICAgICB8fCAodGhpcy5vcHRpb25zLmlnbm9yZUNhc2UgJiYgbGVmdC50b0xvd2VyQ2FzZSgpID09PSByaWdodC50b0xvd2VyQ2FzZSgpKTtcbiAgICB9XG4gIH0sXG4gIHJlbW92ZUVtcHR5KGFycmF5KSB7XG4gICAgbGV0IHJldCA9IFtdO1xuICAgIGZvciAobGV0IGkgPSAwOyBpIDwgYXJyYXkubGVuZ3RoOyBpKyspIHtcbiAgICAgIGlmIChhcnJheVtpXSkge1xuICAgICAgICByZXQucHVzaChhcnJheVtpXSk7XG4gICAgICB9XG4gICAgfVxuICAgIHJldHVybiByZXQ7XG4gIH0sXG4gIGNhc3RJbnB1dCh2YWx1ZSkge1xuICAgIHJldHVybiB2YWx1ZTtcbiAgfSxcbiAgdG9rZW5pemUodmFsdWUpIHtcbiAgICByZXR1cm4gdmFsdWUuc3BsaXQoJycpO1xuICB9LFxuICBqb2luKGNoYXJzKSB7XG4gICAgcmV0dXJuIGNoYXJzLmpvaW4oJycpO1xuICB9XG59O1xuXG5mdW5jdGlvbiBidWlsZFZhbHVlcyhkaWZmLCBjb21wb25lbnRzLCBuZXdTdHJpbmcsIG9sZFN0cmluZywgdXNlTG9uZ2VzdFRva2VuKSB7XG4gIGxldCBjb21wb25lbnRQb3MgPSAwLFxuICAgICAgY29tcG9uZW50TGVuID0gY29tcG9uZW50cy5sZW5ndGgsXG4gICAgICBuZXdQb3MgPSAwLFxuICAgICAgb2xkUG9zID0gMDtcblxuICBmb3IgKDsgY29tcG9uZW50UG9zIDwgY29tcG9uZW50TGVuOyBjb21wb25lbnRQb3MrKykge1xuICAgIGxldCBjb21wb25lbnQgPSBjb21wb25lbnRzW2NvbXBvbmVudFBvc107XG4gICAgaWYgKCFjb21wb25lbnQucmVtb3ZlZCkge1xuICAgICAgaWYgKCFjb21wb25lbnQuYWRkZWQgJiYgdXNlTG9uZ2VzdFRva2VuKSB7XG4gICAgICAgIGxldCB2YWx1ZSA9IG5ld1N0cmluZy5zbGljZShuZXdQb3MsIG5ld1BvcyArIGNvbXBvbmVudC5jb3VudCk7XG4gICAgICAgIHZhbHVlID0gdmFsdWUubWFwKGZ1bmN0aW9uKHZhbHVlLCBpKSB7XG4gICAgICAgICAgbGV0IG9sZFZhbHVlID0gb2xkU3RyaW5nW29sZFBvcyArIGldO1xuICAgICAgICAgIHJldHVybiBvbGRWYWx1ZS5sZW5ndGggPiB2YWx1ZS5sZW5ndGggPyBvbGRWYWx1ZSA6IHZhbHVlO1xuICAgICAgICB9KTtcblxuICAgICAgICBjb21wb25lbnQudmFsdWUgPSBkaWZmLmpvaW4odmFsdWUpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgY29tcG9uZW50LnZhbHVlID0gZGlmZi5qb2luKG5ld1N0cmluZy5zbGljZShuZXdQb3MsIG5ld1BvcyArIGNvbXBvbmVudC5jb3VudCkpO1xuICAgICAgfVxuICAgICAgbmV3UG9zICs9IGNvbXBvbmVudC5jb3VudDtcblxuICAgICAgLy8gQ29tbW9uIGNhc2VcbiAgICAgIGlmICghY29tcG9uZW50LmFkZGVkKSB7XG4gICAgICAgIG9sZFBvcyArPSBjb21wb25lbnQuY291bnQ7XG4gICAgICB9XG4gICAgfSBlbHNlIHtcbiAgICAgIGNvbXBvbmVudC52YWx1ZSA9IGRpZmYuam9pbihvbGRTdHJpbmcuc2xpY2Uob2xkUG9zLCBvbGRQb3MgKyBjb21wb25lbnQuY291bnQpKTtcbiAgICAgIG9sZFBvcyArPSBjb21wb25lbnQuY291bnQ7XG5cbiAgICAgIC8vIFJldmVyc2UgYWRkIGFuZCByZW1vdmUgc28gcmVtb3ZlcyBhcmUgb3V0cHV0IGZpcnN0IHRvIG1hdGNoIGNvbW1vbiBjb252ZW50aW9uXG4gICAgICAvLyBUaGUgZGlmZmluZyBhbGdvcml0aG0gaXMgdGllZCB0byBhZGQgdGhlbiByZW1vdmUgb3V0cHV0IGFuZCB0aGlzIGlzIHRoZSBzaW1wbGVzdFxuICAgICAgLy8gcm91dGUgdG8gZ2V0IHRoZSBkZXNpcmVkIG91dHB1dCB3aXRoIG1pbmltYWwgb3ZlcmhlYWQuXG4gICAgICBpZiAoY29tcG9uZW50UG9zICYmIGNvbXBvbmVudHNbY29tcG9uZW50UG9zIC0gMV0uYWRkZWQpIHtcbiAgICAgICAgbGV0IHRtcCA9IGNvbXBvbmVudHNbY29tcG9uZW50UG9zIC0gMV07XG4gICAgICAgIGNvbXBvbmVudHNbY29tcG9uZW50UG9zIC0gMV0gPSBjb21wb25lbnRzW2NvbXBvbmVudFBvc107XG4gICAgICAgIGNvbXBvbmVudHNbY29tcG9uZW50UG9zXSA9IHRtcDtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAvLyBTcGVjaWFsIGNhc2UgaGFuZGxlIGZvciB3aGVuIG9uZSB0ZXJtaW5hbCBpcyBpZ25vcmVkIChpLmUuIHdoaXRlc3BhY2UpLlxuICAvLyBGb3IgdGhpcyBjYXNlIHdlIG1lcmdlIHRoZSB0ZXJtaW5hbCBpbnRvIHRoZSBwcmlvciBzdHJpbmcgYW5kIGRyb3AgdGhlIGNoYW5nZS5cbiAgLy8gVGhpcyBpcyBvbmx5IGF2YWlsYWJsZSBmb3Igc3RyaW5nIG1vZGUuXG4gIGxldCBsYXN0Q29tcG9uZW50ID0gY29tcG9uZW50c1tjb21wb25lbnRMZW4gLSAxXTtcbiAgaWYgKGNvbXBvbmVudExlbiA+IDFcbiAgICAgICYmIHR5cGVvZiBsYXN0Q29tcG9uZW50LnZhbHVlID09PSAnc3RyaW5nJ1xuICAgICAgJiYgKGxhc3RDb21wb25lbnQuYWRkZWQgfHwgbGFzdENvbXBvbmVudC5yZW1vdmVkKVxuICAgICAgJiYgZGlmZi5lcXVhbHMoJycsIGxhc3RDb21wb25lbnQudmFsdWUpKSB7XG4gICAgY29tcG9uZW50c1tjb21wb25lbnRMZW4gLSAyXS52YWx1ZSArPSBsYXN0Q29tcG9uZW50LnZhbHVlO1xuICAgIGNvbXBvbmVudHMucG9wKCk7XG4gIH1cblxuICByZXR1cm4gY29tcG9uZW50cztcbn1cblxuZnVuY3Rpb24gY2xvbmVQYXRoKHBhdGgpIHtcbiAgcmV0dXJuIHsgbmV3UG9zOiBwYXRoLm5ld1BvcywgY29tcG9uZW50czogcGF0aC5jb21wb25lbnRzLnNsaWNlKDApIH07XG59XG4iXX0=\n\n\n/***/ }),\n/* 2 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports.characterDiff = undefined;\n\texports. /*istanbul ignore end*/diffChars = diffChars;\n\n\tvar /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _base2 = _interopRequireDefault(_base);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*istanbul ignore end*/var characterDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/characterDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/();\n\tfunction diffChars(oldStr, newStr, options) {\n\t return characterDiff.diff(oldStr, newStr, options);\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaWZmL2NoYXJhY3Rlci5qcyJdLCJuYW1lcyI6WyJkaWZmQ2hhcnMiLCJjaGFyYWN0ZXJEaWZmIiwib2xkU3RyIiwibmV3U3RyIiwib3B0aW9ucyIsImRpZmYiXSwibWFwcGluZ3MiOiI7Ozs7Z0NBR2dCQSxTLEdBQUFBLFM7O0FBSGhCOzs7Ozs7dUJBRU8sSUFBTUMseUZBQWdCLHdFQUF0QjtBQUNBLFNBQVNELFNBQVQsQ0FBbUJFLE1BQW5CLEVBQTJCQyxNQUEzQixFQUFtQ0MsT0FBbkMsRUFBNEM7QUFBRSxTQUFPSCxjQUFjSSxJQUFkLENBQW1CSCxNQUFuQixFQUEyQkMsTUFBM0IsRUFBbUNDLE9BQW5DLENBQVA7QUFBcUQiLCJmaWxlIjoiY2hhcmFjdGVyLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IERpZmYgZnJvbSAnLi9iYXNlJztcblxuZXhwb3J0IGNvbnN0IGNoYXJhY3RlckRpZmYgPSBuZXcgRGlmZigpO1xuZXhwb3J0IGZ1bmN0aW9uIGRpZmZDaGFycyhvbGRTdHIsIG5ld1N0ciwgb3B0aW9ucykgeyByZXR1cm4gY2hhcmFjdGVyRGlmZi5kaWZmKG9sZFN0ciwgbmV3U3RyLCBvcHRpb25zKTsgfVxuIl19\n\n\n/***/ }),\n/* 3 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports.wordDiff = undefined;\n\texports. /*istanbul ignore end*/diffWords = diffWords;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffWordsWithSpace = diffWordsWithSpace;\n\n\tvar /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _base2 = _interopRequireDefault(_base);\n\n\t/*istanbul ignore end*/var /*istanbul ignore start*/_params = __webpack_require__(4) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*istanbul ignore end*/ // Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode\n\t//\n\t// Ranges and exceptions:\n\t// Latin-1 Supplement, 0080–00FF\n\t// - U+00D7 × Multiplication sign\n\t// - U+00F7 ÷ Division sign\n\t// Latin Extended-A, 0100–017F\n\t// Latin Extended-B, 0180–024F\n\t// IPA Extensions, 0250–02AF\n\t// Spacing Modifier Letters, 02B0–02FF\n\t// - U+02C7 ˇ ˇ Caron\n\t// - U+02D8 ˘ ˘ Breve\n\t// - U+02D9 ˙ ˙ Dot Above\n\t// - U+02DA ˚ ˚ Ring Above\n\t// - U+02DB ˛ ˛ Ogonek\n\t// - U+02DC ˜ ˜ Small Tilde\n\t// - U+02DD ˝ ˝ Double Acute Accent\n\t// Latin Extended Additional, 1E00–1EFF\n\tvar extendedWordChars = /^[A-Za-z\\xC0-\\u02C6\\u02C8-\\u02D7\\u02DE-\\u02FF\\u1E00-\\u1EFF]+$/;\n\n\tvar reWhitespace = /\\S/;\n\n\tvar wordDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/wordDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/();\n\twordDiff.equals = function (left, right) {\n\t if (this.options.ignoreCase) {\n\t left = left.toLowerCase();\n\t right = right.toLowerCase();\n\t }\n\t return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);\n\t};\n\twordDiff.tokenize = function (value) {\n\t var tokens = value.split(/(\\s+|\\b)/);\n\n\t // Join the boundary splits that we do not consider to be boundaries. This is primarily the extended Latin character set.\n\t for (var i = 0; i < tokens.length - 1; i++) {\n\t // If we have an empty string in the next field and we have only word chars before and after, merge\n\t if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {\n\t tokens[i] += tokens[i + 2];\n\t tokens.splice(i + 1, 2);\n\t i--;\n\t }\n\t }\n\n\t return tokens;\n\t};\n\n\tfunction diffWords(oldStr, newStr, options) {\n\t options = /*istanbul ignore start*/(0, _params.generateOptions) /*istanbul ignore end*/(options, { ignoreWhitespace: true });\n\t return wordDiff.diff(oldStr, newStr, options);\n\t}\n\n\tfunction diffWordsWithSpace(oldStr, newStr, options) {\n\t return wordDiff.diff(oldStr, newStr, options);\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaWZmL3dvcmQuanMiXSwibmFtZXMiOlsiZGlmZldvcmRzIiwiZGlmZldvcmRzV2l0aFNwYWNlIiwiZXh0ZW5kZWRXb3JkQ2hhcnMiLCJyZVdoaXRlc3BhY2UiLCJ3b3JkRGlmZiIsImVxdWFscyIsImxlZnQiLCJyaWdodCIsIm9wdGlvbnMiLCJpZ25vcmVDYXNlIiwidG9Mb3dlckNhc2UiLCJpZ25vcmVXaGl0ZXNwYWNlIiwidGVzdCIsInRva2VuaXplIiwidmFsdWUiLCJ0b2tlbnMiLCJzcGxpdCIsImkiLCJsZW5ndGgiLCJzcGxpY2UiLCJvbGRTdHIiLCJuZXdTdHIiLCJkaWZmIl0sIm1hcHBpbmdzIjoiOzs7O2dDQW1EZ0JBLFMsR0FBQUEsUzt5REFLQUMsa0IsR0FBQUEsa0I7O0FBeERoQjs7Ozt1QkFDQTs7Ozt3QkFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFNQyxvQkFBb0IsK0RBQTFCOztBQUVBLElBQU1DLGVBQWUsSUFBckI7O0FBRU8sSUFBTUMsK0VBQVcsd0VBQWpCO0FBQ1BBLFNBQVNDLE1BQVQsR0FBa0IsVUFBU0MsSUFBVCxFQUFlQyxLQUFmLEVBQXNCO0FBQ3RDLE1BQUksS0FBS0MsT0FBTCxDQUFhQyxVQUFqQixFQUE2QjtBQUMzQkgsV0FBT0EsS0FBS0ksV0FBTCxFQUFQO0FBQ0FILFlBQVFBLE1BQU1HLFdBQU4sRUFBUjtBQUNEO0FBQ0QsU0FBT0osU0FBU0MsS0FBVCxJQUFtQixLQUFLQyxPQUFMLENBQWFHLGdCQUFiLElBQWlDLENBQUNSLGFBQWFTLElBQWIsQ0FBa0JOLElBQWxCLENBQWxDLElBQTZELENBQUNILGFBQWFTLElBQWIsQ0FBa0JMLEtBQWxCLENBQXhGO0FBQ0QsQ0FORDtBQU9BSCxTQUFTUyxRQUFULEdBQW9CLFVBQVNDLEtBQVQsRUFBZ0I7QUFDbEMsTUFBSUMsU0FBU0QsTUFBTUUsS0FBTixDQUFZLFVBQVosQ0FBYjs7QUFFQTtBQUNBLE9BQUssSUFBSUMsSUFBSSxDQUFiLEVBQWdCQSxJQUFJRixPQUFPRyxNQUFQLEdBQWdCLENBQXBDLEVBQXVDRCxHQUF2QyxFQUE0QztBQUMxQztBQUNBLFFBQUksQ0FBQ0YsT0FBT0UsSUFBSSxDQUFYLENBQUQsSUFBa0JGLE9BQU9FLElBQUksQ0FBWCxDQUFsQixJQUNLZixrQkFBa0JVLElBQWxCLENBQXVCRyxPQUFPRSxDQUFQLENBQXZCLENBREwsSUFFS2Ysa0JBQWtCVSxJQUFsQixDQUF1QkcsT0FBT0UsSUFBSSxDQUFYLENBQXZCLENBRlQsRUFFZ0Q7QUFDOUNGLGFBQU9FLENBQVAsS0FBYUYsT0FBT0UsSUFBSSxDQUFYLENBQWI7QUFDQUYsYUFBT0ksTUFBUCxDQUFjRixJQUFJLENBQWxCLEVBQXFCLENBQXJCO0FBQ0FBO0FBQ0Q7QUFDRjs7QUFFRCxTQUFPRixNQUFQO0FBQ0QsQ0FoQkQ7O0FBa0JPLFNBQVNmLFNBQVQsQ0FBbUJvQixNQUFuQixFQUEyQkMsTUFBM0IsRUFBbUNiLE9BQW5DLEVBQTRDO0FBQ2pEQSxZQUFVLDhFQUFnQkEsT0FBaEIsRUFBeUIsRUFBQ0csa0JBQWtCLElBQW5CLEVBQXpCLENBQVY7QUFDQSxTQUFPUCxTQUFTa0IsSUFBVCxDQUFjRixNQUFkLEVBQXNCQyxNQUF0QixFQUE4QmIsT0FBOUIsQ0FBUDtBQUNEOztBQUVNLFNBQVNQLGtCQUFULENBQTRCbUIsTUFBNUIsRUFBb0NDLE1BQXBDLEVBQTRDYixPQUE1QyxFQUFxRDtBQUMxRCxTQUFPSixTQUFTa0IsSUFBVCxDQUFjRixNQUFkLEVBQXNCQyxNQUF0QixFQUE4QmIsT0FBOUIsQ0FBUDtBQUNEIiwiZmlsZSI6IndvcmQuanMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgRGlmZiBmcm9tICcuL2Jhc2UnO1xuaW1wb3J0IHtnZW5lcmF0ZU9wdGlvbnN9IGZyb20gJy4uL3V0aWwvcGFyYW1zJztcblxuLy8gQmFzZWQgb24gaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvTGF0aW5fc2NyaXB0X2luX1VuaWNvZGVcbi8vXG4vLyBSYW5nZXMgYW5kIGV4Y2VwdGlvbnM6XG4vLyBMYXRpbi0xIFN1cHBsZW1lbnQsIDAwODDigJMwMEZGXG4vLyAgLSBVKzAwRDcgIMOXIE11bHRpcGxpY2F0aW9uIHNpZ25cbi8vICAtIFUrMDBGNyAgw7cgRGl2aXNpb24gc2lnblxuLy8gTGF0aW4gRXh0ZW5kZWQtQSwgMDEwMOKAkzAxN0Zcbi8vIExhdGluIEV4dGVuZGVkLUIsIDAxODDigJMwMjRGXG4vLyBJUEEgRXh0ZW5zaW9ucywgMDI1MOKAkzAyQUZcbi8vIFNwYWNpbmcgTW9kaWZpZXIgTGV0dGVycywgMDJCMOKAkzAyRkZcbi8vICAtIFUrMDJDNyAgy4cgJiM3MTE7ICBDYXJvblxuLy8gIC0gVSswMkQ4ICDLmCAmIzcyODsgIEJyZXZlXG4vLyAgLSBVKzAyRDkgIMuZICYjNzI5OyAgRG90IEFib3ZlXG4vLyAgLSBVKzAyREEgIMuaICYjNzMwOyAgUmluZyBBYm92ZVxuLy8gIC0gVSswMkRCICDLmyAmIzczMTsgIE9nb25la1xuLy8gIC0gVSswMkRDICDLnCAmIzczMjsgIFNtYWxsIFRpbGRlXG4vLyAgLSBVKzAyREQgIMudICYjNzMzOyAgRG91YmxlIEFjdXRlIEFjY2VudFxuLy8gTGF0aW4gRXh0ZW5kZWQgQWRkaXRpb25hbCwgMUUwMOKAkzFFRkZcbmNvbnN0IGV4dGVuZGVkV29yZENoYXJzID0gL15bYS16QS1aXFx1e0MwfS1cXHV7RkZ9XFx1e0Q4fS1cXHV7RjZ9XFx1e0Y4fS1cXHV7MkM2fVxcdXsyQzh9LVxcdXsyRDd9XFx1ezJERX0tXFx1ezJGRn1cXHV7MUUwMH0tXFx1ezFFRkZ9XSskL3U7XG5cbmNvbnN0IHJlV2hpdGVzcGFjZSA9IC9cXFMvO1xuXG5leHBvcnQgY29uc3Qgd29yZERpZmYgPSBuZXcgRGlmZigpO1xud29yZERpZmYuZXF1YWxzID0gZnVuY3Rpb24obGVmdCwgcmlnaHQpIHtcbiAgaWYgKHRoaXMub3B0aW9ucy5pZ25vcmVDYXNlKSB7XG4gICAgbGVmdCA9IGxlZnQudG9Mb3dlckNhc2UoKTtcbiAgICByaWdodCA9IHJpZ2h0LnRvTG93ZXJDYXNlKCk7XG4gIH1cbiAgcmV0dXJuIGxlZnQgPT09IHJpZ2h0IHx8ICh0aGlzLm9wdGlvbnMuaWdub3JlV2hpdGVzcGFjZSAmJiAhcmVXaGl0ZXNwYWNlLnRlc3QobGVmdCkgJiYgIXJlV2hpdGVzcGFjZS50ZXN0KHJpZ2h0KSk7XG59O1xud29yZERpZmYudG9rZW5pemUgPSBmdW5jdGlvbih2YWx1ZSkge1xuICBsZXQgdG9rZW5zID0gdmFsdWUuc3BsaXQoLyhcXHMrfFxcYikvKTtcblxuICAvLyBKb2luIHRoZSBib3VuZGFyeSBzcGxpdHMgdGhhdCB3ZSBkbyBub3QgY29uc2lkZXIgdG8gYmUgYm91bmRhcmllcy4gVGhpcyBpcyBwcmltYXJpbHkgdGhlIGV4dGVuZGVkIExhdGluIGNoYXJhY3RlciBzZXQuXG4gIGZvciAobGV0IGkgPSAwOyBpIDwgdG9rZW5zLmxlbmd0aCAtIDE7IGkrKykge1xuICAgIC8vIElmIHdlIGhhdmUgYW4gZW1wdHkgc3RyaW5nIGluIHRoZSBuZXh0IGZpZWxkIGFuZCB3ZSBoYXZlIG9ubHkgd29yZCBjaGFycyBiZWZvcmUgYW5kIGFmdGVyLCBtZXJnZVxuICAgIGlmICghdG9rZW5zW2kgKyAxXSAmJiB0b2tlbnNbaSArIDJdXG4gICAgICAgICAgJiYgZXh0ZW5kZWRXb3JkQ2hhcnMudGVzdCh0b2tlbnNbaV0pXG4gICAgICAgICAgJiYgZXh0ZW5kZWRXb3JkQ2hhcnMudGVzdCh0b2tlbnNbaSArIDJdKSkge1xuICAgICAgdG9rZW5zW2ldICs9IHRva2Vuc1tpICsgMl07XG4gICAgICB0b2tlbnMuc3BsaWNlKGkgKyAxLCAyKTtcbiAgICAgIGktLTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gdG9rZW5zO1xufTtcblxuZXhwb3J0IGZ1bmN0aW9uIGRpZmZXb3JkcyhvbGRTdHIsIG5ld1N0ciwgb3B0aW9ucykge1xuICBvcHRpb25zID0gZ2VuZXJhdGVPcHRpb25zKG9wdGlvbnMsIHtpZ25vcmVXaGl0ZXNwYWNlOiB0cnVlfSk7XG4gIHJldHVybiB3b3JkRGlmZi5kaWZmKG9sZFN0ciwgbmV3U3RyLCBvcHRpb25zKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGRpZmZXb3Jkc1dpdGhTcGFjZShvbGRTdHIsIG5ld1N0ciwgb3B0aW9ucykge1xuICByZXR1cm4gd29yZERpZmYuZGlmZihvbGRTdHIsIG5ld1N0ciwgb3B0aW9ucyk7XG59XG4iXX0=\n\n\n/***/ }),\n/* 4 */\n/***/ (function(module, exports) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports. /*istanbul ignore end*/generateOptions = generateOptions;\n\tfunction generateOptions(options, defaults) {\n\t if (typeof options === 'function') {\n\t defaults.callback = options;\n\t } else if (options) {\n\t for (var name in options) {\n\t /* istanbul ignore else */\n\t if (options.hasOwnProperty(name)) {\n\t defaults[name] = options[name];\n\t }\n\t }\n\t }\n\t return defaults;\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlsL3BhcmFtcy5qcyJdLCJuYW1lcyI6WyJnZW5lcmF0ZU9wdGlvbnMiLCJvcHRpb25zIiwiZGVmYXVsdHMiLCJjYWxsYmFjayIsIm5hbWUiLCJoYXNPd25Qcm9wZXJ0eSJdLCJtYXBwaW5ncyI6Ijs7O2dDQUFnQkEsZSxHQUFBQSxlO0FBQVQsU0FBU0EsZUFBVCxDQUF5QkMsT0FBekIsRUFBa0NDLFFBQWxDLEVBQTRDO0FBQ2pELE1BQUksT0FBT0QsT0FBUCxLQUFtQixVQUF2QixFQUFtQztBQUNqQ0MsYUFBU0MsUUFBVCxHQUFvQkYsT0FBcEI7QUFDRCxHQUZELE1BRU8sSUFBSUEsT0FBSixFQUFhO0FBQ2xCLFNBQUssSUFBSUcsSUFBVCxJQUFpQkgsT0FBakIsRUFBMEI7QUFDeEI7QUFDQSxVQUFJQSxRQUFRSSxjQUFSLENBQXVCRCxJQUF2QixDQUFKLEVBQWtDO0FBQ2hDRixpQkFBU0UsSUFBVCxJQUFpQkgsUUFBUUcsSUFBUixDQUFqQjtBQUNEO0FBQ0Y7QUFDRjtBQUNELFNBQU9GLFFBQVA7QUFDRCIsImZpbGUiOiJwYXJhbXMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZ2VuZXJhdGVPcHRpb25zKG9wdGlvbnMsIGRlZmF1bHRzKSB7XG4gIGlmICh0eXBlb2Ygb3B0aW9ucyA9PT0gJ2Z1bmN0aW9uJykge1xuICAgIGRlZmF1bHRzLmNhbGxiYWNrID0gb3B0aW9ucztcbiAgfSBlbHNlIGlmIChvcHRpb25zKSB7XG4gICAgZm9yIChsZXQgbmFtZSBpbiBvcHRpb25zKSB7XG4gICAgICAvKiBpc3RhbmJ1bCBpZ25vcmUgZWxzZSAqL1xuICAgICAgaWYgKG9wdGlvbnMuaGFzT3duUHJvcGVydHkobmFtZSkpIHtcbiAgICAgICAgZGVmYXVsdHNbbmFtZV0gPSBvcHRpb25zW25hbWVdO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICByZXR1cm4gZGVmYXVsdHM7XG59XG4iXX0=\n\n\n/***/ }),\n/* 5 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports.lineDiff = undefined;\n\texports. /*istanbul ignore end*/diffLines = diffLines;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/diffTrimmedLines = diffTrimmedLines;\n\n\tvar /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _base2 = _interopRequireDefault(_base);\n\n\t/*istanbul ignore end*/var /*istanbul ignore start*/_params = __webpack_require__(4) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*istanbul ignore end*/var lineDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/lineDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/();\n\tlineDiff.tokenize = function (value) {\n\t var retLines = [],\n\t linesAndNewlines = value.split(/(\\n|\\r\\n)/);\n\n\t // Ignore the final empty token that occurs if the string ends with a new line\n\t if (!linesAndNewlines[linesAndNewlines.length - 1]) {\n\t linesAndNewlines.pop();\n\t }\n\n\t // Merge the content and line separators into single tokens\n\t for (var i = 0; i < linesAndNewlines.length; i++) {\n\t var line = linesAndNewlines[i];\n\n\t if (i % 2 && !this.options.newlineIsToken) {\n\t retLines[retLines.length - 1] += line;\n\t } else {\n\t if (this.options.ignoreWhitespace) {\n\t line = line.trim();\n\t }\n\t retLines.push(line);\n\t }\n\t }\n\n\t return retLines;\n\t};\n\n\tfunction diffLines(oldStr, newStr, callback) {\n\t return lineDiff.diff(oldStr, newStr, callback);\n\t}\n\tfunction diffTrimmedLines(oldStr, newStr, callback) {\n\t var options = /*istanbul ignore start*/(0, _params.generateOptions) /*istanbul ignore end*/(callback, { ignoreWhitespace: true });\n\t return lineDiff.diff(oldStr, newStr, options);\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaWZmL2xpbmUuanMiXSwibmFtZXMiOlsiZGlmZkxpbmVzIiwiZGlmZlRyaW1tZWRMaW5lcyIsImxpbmVEaWZmIiwidG9rZW5pemUiLCJ2YWx1ZSIsInJldExpbmVzIiwibGluZXNBbmROZXdsaW5lcyIsInNwbGl0IiwibGVuZ3RoIiwicG9wIiwiaSIsImxpbmUiLCJvcHRpb25zIiwibmV3bGluZUlzVG9rZW4iLCJpZ25vcmVXaGl0ZXNwYWNlIiwidHJpbSIsInB1c2giLCJvbGRTdHIiLCJuZXdTdHIiLCJjYWxsYmFjayIsImRpZmYiXSwibWFwcGluZ3MiOiI7Ozs7Z0NBOEJnQkEsUyxHQUFBQSxTO3lEQUNBQyxnQixHQUFBQSxnQjs7QUEvQmhCOzs7O3VCQUNBOzs7O3VCQUVPLElBQU1DLCtFQUFXLHdFQUFqQjtBQUNQQSxTQUFTQyxRQUFULEdBQW9CLFVBQVNDLEtBQVQsRUFBZ0I7QUFDbEMsTUFBSUMsV0FBVyxFQUFmO0FBQUEsTUFDSUMsbUJBQW1CRixNQUFNRyxLQUFOLENBQVksV0FBWixDQUR2Qjs7QUFHQTtBQUNBLE1BQUksQ0FBQ0QsaUJBQWlCQSxpQkFBaUJFLE1BQWpCLEdBQTBCLENBQTNDLENBQUwsRUFBb0Q7QUFDbERGLHFCQUFpQkcsR0FBakI7QUFDRDs7QUFFRDtBQUNBLE9BQUssSUFBSUMsSUFBSSxDQUFiLEVBQWdCQSxJQUFJSixpQkFBaUJFLE1BQXJDLEVBQTZDRSxHQUE3QyxFQUFrRDtBQUNoRCxRQUFJQyxPQUFPTCxpQkFBaUJJLENBQWpCLENBQVg7O0FBRUEsUUFBSUEsSUFBSSxDQUFKLElBQVMsQ0FBQyxLQUFLRSxPQUFMLENBQWFDLGNBQTNCLEVBQTJDO0FBQ3pDUixlQUFTQSxTQUFTRyxNQUFULEdBQWtCLENBQTNCLEtBQWlDRyxJQUFqQztBQUNELEtBRkQsTUFFTztBQUNMLFVBQUksS0FBS0MsT0FBTCxDQUFhRSxnQkFBakIsRUFBbUM7QUFDakNILGVBQU9BLEtBQUtJLElBQUwsRUFBUDtBQUNEO0FBQ0RWLGVBQVNXLElBQVQsQ0FBY0wsSUFBZDtBQUNEO0FBQ0Y7O0FBRUQsU0FBT04sUUFBUDtBQUNELENBeEJEOztBQTBCTyxTQUFTTCxTQUFULENBQW1CaUIsTUFBbkIsRUFBMkJDLE1BQTNCLEVBQW1DQyxRQUFuQyxFQUE2QztBQUFFLFNBQU9qQixTQUFTa0IsSUFBVCxDQUFjSCxNQUFkLEVBQXNCQyxNQUF0QixFQUE4QkMsUUFBOUIsQ0FBUDtBQUFpRDtBQUNoRyxTQUFTbEIsZ0JBQVQsQ0FBMEJnQixNQUExQixFQUFrQ0MsTUFBbEMsRUFBMENDLFFBQTFDLEVBQW9EO0FBQ3pELE1BQUlQLFVBQVUsOEVBQWdCTyxRQUFoQixFQUEwQixFQUFDTCxrQkFBa0IsSUFBbkIsRUFBMUIsQ0FBZDtBQUNBLFNBQU9aLFNBQVNrQixJQUFULENBQWNILE1BQWQsRUFBc0JDLE1BQXRCLEVBQThCTixPQUE5QixDQUFQO0FBQ0QiLCJmaWxlIjoibGluZS5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBEaWZmIGZyb20gJy4vYmFzZSc7XG5pbXBvcnQge2dlbmVyYXRlT3B0aW9uc30gZnJvbSAnLi4vdXRpbC9wYXJhbXMnO1xuXG5leHBvcnQgY29uc3QgbGluZURpZmYgPSBuZXcgRGlmZigpO1xubGluZURpZmYudG9rZW5pemUgPSBmdW5jdGlvbih2YWx1ZSkge1xuICBsZXQgcmV0TGluZXMgPSBbXSxcbiAgICAgIGxpbmVzQW5kTmV3bGluZXMgPSB2YWx1ZS5zcGxpdCgvKFxcbnxcXHJcXG4pLyk7XG5cbiAgLy8gSWdub3JlIHRoZSBmaW5hbCBlbXB0eSB0b2tlbiB0aGF0IG9jY3VycyBpZiB0aGUgc3RyaW5nIGVuZHMgd2l0aCBhIG5ldyBsaW5lXG4gIGlmICghbGluZXNBbmROZXdsaW5lc1tsaW5lc0FuZE5ld2xpbmVzLmxlbmd0aCAtIDFdKSB7XG4gICAgbGluZXNBbmROZXdsaW5lcy5wb3AoKTtcbiAgfVxuXG4gIC8vIE1lcmdlIHRoZSBjb250ZW50IGFuZCBsaW5lIHNlcGFyYXRvcnMgaW50byBzaW5nbGUgdG9rZW5zXG4gIGZvciAobGV0IGkgPSAwOyBpIDwgbGluZXNBbmROZXdsaW5lcy5sZW5ndGg7IGkrKykge1xuICAgIGxldCBsaW5lID0gbGluZXNBbmROZXdsaW5lc1tpXTtcblxuICAgIGlmIChpICUgMiAmJiAhdGhpcy5vcHRpb25zLm5ld2xpbmVJc1Rva2VuKSB7XG4gICAgICByZXRMaW5lc1tyZXRMaW5lcy5sZW5ndGggLSAxXSArPSBsaW5lO1xuICAgIH0gZWxzZSB7XG4gICAgICBpZiAodGhpcy5vcHRpb25zLmlnbm9yZVdoaXRlc3BhY2UpIHtcbiAgICAgICAgbGluZSA9IGxpbmUudHJpbSgpO1xuICAgICAgfVxuICAgICAgcmV0TGluZXMucHVzaChsaW5lKTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gcmV0TGluZXM7XG59O1xuXG5leHBvcnQgZnVuY3Rpb24gZGlmZkxpbmVzKG9sZFN0ciwgbmV3U3RyLCBjYWxsYmFjaykgeyByZXR1cm4gbGluZURpZmYuZGlmZihvbGRTdHIsIG5ld1N0ciwgY2FsbGJhY2spOyB9XG5leHBvcnQgZnVuY3Rpb24gZGlmZlRyaW1tZWRMaW5lcyhvbGRTdHIsIG5ld1N0ciwgY2FsbGJhY2spIHtcbiAgbGV0IG9wdGlvbnMgPSBnZW5lcmF0ZU9wdGlvbnMoY2FsbGJhY2ssIHtpZ25vcmVXaGl0ZXNwYWNlOiB0cnVlfSk7XG4gIHJldHVybiBsaW5lRGlmZi5kaWZmKG9sZFN0ciwgbmV3U3RyLCBvcHRpb25zKTtcbn1cbiJdfQ==\n\n\n/***/ }),\n/* 6 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports.sentenceDiff = undefined;\n\texports. /*istanbul ignore end*/diffSentences = diffSentences;\n\n\tvar /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _base2 = _interopRequireDefault(_base);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*istanbul ignore end*/var sentenceDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/sentenceDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/();\n\tsentenceDiff.tokenize = function (value) {\n\t return value.split(/(\\S.+?[.!?])(?=\\s+|$)/);\n\t};\n\n\tfunction diffSentences(oldStr, newStr, callback) {\n\t return sentenceDiff.diff(oldStr, newStr, callback);\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaWZmL3NlbnRlbmNlLmpzIl0sIm5hbWVzIjpbImRpZmZTZW50ZW5jZXMiLCJzZW50ZW5jZURpZmYiLCJ0b2tlbml6ZSIsInZhbHVlIiwic3BsaXQiLCJvbGRTdHIiLCJuZXdTdHIiLCJjYWxsYmFjayIsImRpZmYiXSwibWFwcGluZ3MiOiI7Ozs7Z0NBUWdCQSxhLEdBQUFBLGE7O0FBUmhCOzs7Ozs7dUJBR08sSUFBTUMsdUZBQWUsd0VBQXJCO0FBQ1BBLGFBQWFDLFFBQWIsR0FBd0IsVUFBU0MsS0FBVCxFQUFnQjtBQUN0QyxTQUFPQSxNQUFNQyxLQUFOLENBQVksdUJBQVosQ0FBUDtBQUNELENBRkQ7O0FBSU8sU0FBU0osYUFBVCxDQUF1QkssTUFBdkIsRUFBK0JDLE1BQS9CLEVBQXVDQyxRQUF2QyxFQUFpRDtBQUFFLFNBQU9OLGFBQWFPLElBQWIsQ0FBa0JILE1BQWxCLEVBQTBCQyxNQUExQixFQUFrQ0MsUUFBbEMsQ0FBUDtBQUFxRCIsImZpbGUiOiJzZW50ZW5jZS5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBEaWZmIGZyb20gJy4vYmFzZSc7XG5cblxuZXhwb3J0IGNvbnN0IHNlbnRlbmNlRGlmZiA9IG5ldyBEaWZmKCk7XG5zZW50ZW5jZURpZmYudG9rZW5pemUgPSBmdW5jdGlvbih2YWx1ZSkge1xuICByZXR1cm4gdmFsdWUuc3BsaXQoLyhcXFMuKz9bLiE/XSkoPz1cXHMrfCQpLyk7XG59O1xuXG5leHBvcnQgZnVuY3Rpb24gZGlmZlNlbnRlbmNlcyhvbGRTdHIsIG5ld1N0ciwgY2FsbGJhY2spIHsgcmV0dXJuIHNlbnRlbmNlRGlmZi5kaWZmKG9sZFN0ciwgbmV3U3RyLCBjYWxsYmFjayk7IH1cbiJdfQ==\n\n\n/***/ }),\n/* 7 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports.cssDiff = undefined;\n\texports. /*istanbul ignore end*/diffCss = diffCss;\n\n\tvar /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _base2 = _interopRequireDefault(_base);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*istanbul ignore end*/var cssDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/cssDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/();\n\tcssDiff.tokenize = function (value) {\n\t return value.split(/([{}:;,]|\\s+)/);\n\t};\n\n\tfunction diffCss(oldStr, newStr, callback) {\n\t return cssDiff.diff(oldStr, newStr, callback);\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaWZmL2Nzcy5qcyJdLCJuYW1lcyI6WyJkaWZmQ3NzIiwiY3NzRGlmZiIsInRva2VuaXplIiwidmFsdWUiLCJzcGxpdCIsIm9sZFN0ciIsIm5ld1N0ciIsImNhbGxiYWNrIiwiZGlmZiJdLCJtYXBwaW5ncyI6Ijs7OztnQ0FPZ0JBLE8sR0FBQUEsTzs7QUFQaEI7Ozs7Ozt1QkFFTyxJQUFNQyw2RUFBVSx3RUFBaEI7QUFDUEEsUUFBUUMsUUFBUixHQUFtQixVQUFTQyxLQUFULEVBQWdCO0FBQ2pDLFNBQU9BLE1BQU1DLEtBQU4sQ0FBWSxlQUFaLENBQVA7QUFDRCxDQUZEOztBQUlPLFNBQVNKLE9BQVQsQ0FBaUJLLE1BQWpCLEVBQXlCQyxNQUF6QixFQUFpQ0MsUUFBakMsRUFBMkM7QUFBRSxTQUFPTixRQUFRTyxJQUFSLENBQWFILE1BQWIsRUFBcUJDLE1BQXJCLEVBQTZCQyxRQUE3QixDQUFQO0FBQWdEIiwiZmlsZSI6ImNzcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBEaWZmIGZyb20gJy4vYmFzZSc7XG5cbmV4cG9ydCBjb25zdCBjc3NEaWZmID0gbmV3IERpZmYoKTtcbmNzc0RpZmYudG9rZW5pemUgPSBmdW5jdGlvbih2YWx1ZSkge1xuICByZXR1cm4gdmFsdWUuc3BsaXQoLyhbe306OyxdfFxccyspLyk7XG59O1xuXG5leHBvcnQgZnVuY3Rpb24gZGlmZkNzcyhvbGRTdHIsIG5ld1N0ciwgY2FsbGJhY2spIHsgcmV0dXJuIGNzc0RpZmYuZGlmZihvbGRTdHIsIG5ld1N0ciwgY2FsbGJhY2spOyB9XG4iXX0=\n\n\n/***/ }),\n/* 8 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports.jsonDiff = undefined;\n\n\tvar _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; };\n\n\texports. /*istanbul ignore end*/diffJson = diffJson;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/canonicalize = canonicalize;\n\n\tvar /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _base2 = _interopRequireDefault(_base);\n\n\t/*istanbul ignore end*/var /*istanbul ignore start*/_line = __webpack_require__(5) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*istanbul ignore end*/var objectPrototypeToString = Object.prototype.toString;\n\n\tvar jsonDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/jsonDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/();\n\t// Discriminate between two lines of pretty-printed, serialized JSON where one of them has a\n\t// dangling comma and the other doesn't. Turns out including the dangling comma yields the nicest output:\n\tjsonDiff.useLongestToken = true;\n\n\tjsonDiff.tokenize = /*istanbul ignore start*/_line.lineDiff /*istanbul ignore end*/.tokenize;\n\tjsonDiff.castInput = function (value) {\n\t /*istanbul ignore start*/var _options = /*istanbul ignore end*/this.options,\n\t undefinedReplacement = _options.undefinedReplacement,\n\t _options$stringifyRep = _options.stringifyReplacer,\n\t stringifyReplacer = _options$stringifyRep === undefined ? function (k, v) /*istanbul ignore start*/{\n\t return (/*istanbul ignore end*/typeof v === 'undefined' ? undefinedReplacement : v\n\t );\n\t } : _options$stringifyRep;\n\n\n\t return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, ' ');\n\t};\n\tjsonDiff.equals = function (left, right) {\n\t return (/*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/.prototype.equals.call(jsonDiff, left.replace(/,([\\r\\n])/g, '$1'), right.replace(/,([\\r\\n])/g, '$1'))\n\t );\n\t};\n\n\tfunction diffJson(oldObj, newObj, options) {\n\t return jsonDiff.diff(oldObj, newObj, options);\n\t}\n\n\t// This function handles the presence of circular references by bailing out when encountering an\n\t// object that is already on the \"stack\" of items being processed. Accepts an optional replacer\n\tfunction canonicalize(obj, stack, replacementStack, replacer, key) {\n\t stack = stack || [];\n\t replacementStack = replacementStack || [];\n\n\t if (replacer) {\n\t obj = replacer(key, obj);\n\t }\n\n\t var i = /*istanbul ignore start*/void 0 /*istanbul ignore end*/;\n\n\t for (i = 0; i < stack.length; i += 1) {\n\t if (stack[i] === obj) {\n\t return replacementStack[i];\n\t }\n\t }\n\n\t var canonicalizedObj = /*istanbul ignore start*/void 0 /*istanbul ignore end*/;\n\n\t if ('[object Array]' === objectPrototypeToString.call(obj)) {\n\t stack.push(obj);\n\t canonicalizedObj = new Array(obj.length);\n\t replacementStack.push(canonicalizedObj);\n\t for (i = 0; i < obj.length; i += 1) {\n\t canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);\n\t }\n\t stack.pop();\n\t replacementStack.pop();\n\t return canonicalizedObj;\n\t }\n\n\t if (obj && obj.toJSON) {\n\t obj = obj.toJSON();\n\t }\n\n\t if ( /*istanbul ignore start*/(typeof /*istanbul ignore end*/obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object' && obj !== null) {\n\t stack.push(obj);\n\t canonicalizedObj = {};\n\t replacementStack.push(canonicalizedObj);\n\t var sortedKeys = [],\n\t _key = /*istanbul ignore start*/void 0 /*istanbul ignore end*/;\n\t for (_key in obj) {\n\t /* istanbul ignore else */\n\t if (obj.hasOwnProperty(_key)) {\n\t sortedKeys.push(_key);\n\t }\n\t }\n\t sortedKeys.sort();\n\t for (i = 0; i < sortedKeys.length; i += 1) {\n\t _key = sortedKeys[i];\n\t canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);\n\t }\n\t stack.pop();\n\t replacementStack.pop();\n\t } else {\n\t canonicalizedObj = obj;\n\t }\n\t return canonicalizedObj;\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaWZmL2pzb24uanMiXSwibmFtZXMiOlsiZGlmZkpzb24iLCJjYW5vbmljYWxpemUiLCJvYmplY3RQcm90b3R5cGVUb1N0cmluZyIsIk9iamVjdCIsInByb3RvdHlwZSIsInRvU3RyaW5nIiwianNvbkRpZmYiLCJ1c2VMb25nZXN0VG9rZW4iLCJ0b2tlbml6ZSIsImNhc3RJbnB1dCIsInZhbHVlIiwib3B0aW9ucyIsInVuZGVmaW5lZFJlcGxhY2VtZW50Iiwic3RyaW5naWZ5UmVwbGFjZXIiLCJrIiwidiIsIkpTT04iLCJzdHJpbmdpZnkiLCJlcXVhbHMiLCJsZWZ0IiwicmlnaHQiLCJjYWxsIiwicmVwbGFjZSIsIm9sZE9iaiIsIm5ld09iaiIsImRpZmYiLCJvYmoiLCJzdGFjayIsInJlcGxhY2VtZW50U3RhY2siLCJyZXBsYWNlciIsImtleSIsImkiLCJsZW5ndGgiLCJjYW5vbmljYWxpemVkT2JqIiwicHVzaCIsIkFycmF5IiwicG9wIiwidG9KU09OIiwic29ydGVkS2V5cyIsImhhc093blByb3BlcnR5Iiwic29ydCJdLCJtYXBwaW5ncyI6Ijs7Ozs7OztnQ0FxQmdCQSxRLEdBQUFBLFE7eURBSUFDLFksR0FBQUEsWTs7QUF6QmhCOzs7O3VCQUNBOzs7O3VCQUVBLElBQU1DLDBCQUEwQkMsT0FBT0MsU0FBUCxDQUFpQkMsUUFBakQ7O0FBR08sSUFBTUMsK0VBQVcsd0VBQWpCO0FBQ1A7QUFDQTtBQUNBQSxTQUFTQyxlQUFULEdBQTJCLElBQTNCOztBQUVBRCxTQUFTRSxRQUFULEdBQW9CLGdFQUFTQSxRQUE3QjtBQUNBRixTQUFTRyxTQUFULEdBQXFCLFVBQVNDLEtBQVQsRUFBZ0I7QUFBQSxpRUFDK0UsS0FBS0MsT0FEcEY7QUFBQSxNQUM1QkMsb0JBRDRCLFlBQzVCQSxvQkFENEI7QUFBQSx1Q0FDTkMsaUJBRE07QUFBQSxNQUNOQSxpQkFETSx5Q0FDYyxVQUFDQyxDQUFELEVBQUlDLENBQUo7QUFBQSxtQ0FBVSxPQUFPQSxDQUFQ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QUFLVCxJQUFJLENBQVQsRUFBWUEsSUFBSU8sV0FBV04sTUFBM0IsRUFBbUNELEtBQUssQ0FBeEMsRUFBMkM7QUFDekNELGFBQU1RLFdBQVdQLENBQVgsQ0FBTjtBQUNBRSx1QkFBaUJILElBQWpCLElBQXdCN0IsYUFBYXlCLElBQUlJLElBQUosQ0FBYixFQUF1QkgsS0FBdkIsRUFBOEJDLGdCQUE5QixFQUFnREMsUUFBaEQsRUFBMERDLElBQTFELENBQXhCO0FBQ0Q7QUFDREgsVUFBTVMsR0FBTjtBQUNBUixxQkFBaUJRLEdBQWpCO0FBQ0QsR0FuQkQsTUFtQk87QUFDTEgsdUJBQW1CUCxHQUFuQjtBQUNEO0FBQ0QsU0FBT08sZ0JBQVA7QUFDRCIsImZpbGUiOiJqc29uLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IERpZmYgZnJvbSAnLi9iYXNlJztcbmltcG9ydCB7bGluZURpZmZ9IGZyb20gJy4vbGluZSc7XG5cbmNvbnN0IG9iamVjdFByb3RvdHlwZVRvU3RyaW5nID0gT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZztcblxuXG5leHBvcnQgY29uc3QganNvbkRpZmYgPSBuZXcgRGlmZigpO1xuLy8gRGlzY3JpbWluYXRlIGJldHdlZW4gdHdvIGxpbmVzIG9mIHByZXR0eS1wcmludGVkLCBzZXJpYWxpemVkIEpTT04gd2hlcmUgb25lIG9mIHRoZW0gaGFzIGFcbi8vIGRhbmdsaW5nIGNvbW1hIGFuZCB0aGUgb3RoZXIgZG9lc24ndC4gVHVybnMgb3V0IGluY2x1ZGluZyB0aGUgZGFuZ2xpbmcgY29tbWEgeWllbGRzIHRoZSBuaWNlc3Qgb3V0cHV0OlxuanNvbkRpZmYudXNlTG9uZ2VzdFRva2VuID0gdHJ1ZTtcblxuanNvbkRpZmYudG9rZW5pemUgPSBsaW5lRGlmZi50b2tlbml6ZTtcbmpzb25EaWZmLmNhc3RJbnB1dCA9IGZ1bmN0aW9uKHZhbHVlKSB7XG4gIGNvbnN0IHt1bmRlZmluZWRSZXBsYWNlbWVudCwgc3RyaW5naWZ5UmVwbGFjZXIgPSAoaywgdikgPT4gdHlwZW9mIHYgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkUmVwbGFjZW1lbnQgOiB2fSA9IHRoaXMub3B0aW9ucztcblxuICByZXR1cm4gdHlwZW9mIHZhbHVlID09PSAnc3RyaW5nJyA/IHZhbHVlIDogSlNPTi5zdHJpbmdpZnkoY2Fub25pY2FsaXplKHZhbHVlLCBudWxsLCBudWxsLCBzdHJpbmdpZnlSZXBsYWNlciksIHN0cmluZ2lmeVJlcGxhY2VyLCAnICAnKTtcbn07XG5qc29uRGlmZi5lcXVhbHMgPSBmdW5jdGlvbihsZWZ0LCByaWdodCkge1xuICByZXR1cm4gRGlmZi5wcm90b3R5cGUuZXF1YWxzLmNhbGwoanNvbkRpZmYsIGxlZnQucmVwbGFjZSgvLChbXFxyXFxuXSkvZywgJyQxJyksIHJpZ2h0LnJlcGxhY2UoLywoW1xcclxcbl0pL2csICckMScpKTtcbn07XG5cbmV4cG9ydCBmdW5jdGlvbiBkaWZmSnNvbihvbGRPYmosIG5ld09iaiwgb3B0aW9ucykgeyByZXR1cm4ganNvbkRpZmYuZGlmZihvbGRPYmosIG5ld09iaiwgb3B0aW9ucyk7IH1cblxuLy8gVGhpcyBmdW5jdGlvbiBoYW5kbGVzIHRoZSBwcmVzZW5jZSBvZiBjaXJjdWxhciByZWZlcmVuY2VzIGJ5IGJhaWxpbmcgb3V0IHdoZW4gZW5jb3VudGVyaW5nIGFuXG4vLyBvYmplY3QgdGhhdCBpcyBhbHJlYWR5IG9uIHRoZSBcInN0YWNrXCIgb2YgaXRlbXMgYmVpbmcgcHJvY2Vzc2VkLiBBY2NlcHRzIGFuIG9wdGlvbmFsIHJlcGxhY2VyXG5leHBvcnQgZnVuY3Rpb24gY2Fub25pY2FsaXplKG9iaiwgc3RhY2ssIHJlcGxhY2VtZW50U3RhY2ssIHJlcGxhY2VyLCBrZXkpIHtcbiAgc3RhY2sgPSBzdGFjayB8fCBbXTtcbiAgcmVwbGFjZW1lbnRTdGFjayA9IHJlcGxhY2VtZW50U3RhY2sgfHwgW107XG5cbiAgaWYgKHJlcGxhY2VyKSB7XG4gICAgb2JqID0gcmVwbGFjZXIoa2V5LCBvYmopO1xuICB9XG5cbiAgbGV0IGk7XG5cbiAgZm9yIChpID0gMDsgaSA8IHN0YWNrLmxlbmd0aDsgaSArPSAxKSB7XG4gICAgaWYgKHN0YWNrW2ldID09PSBvYmopIHtcbiAgICAgIHJldHVybiByZXBsYWNlbWVudFN0YWNrW2ldO1xuICAgIH1cbiAgfVxuXG4gIGxldCBjYW5vbmljYWxpemVkT2JqO1xuXG4gIGlmICgnW29iamVjdCBBcnJheV0nID09PSBvYmplY3RQcm90b3R5cGVUb1N0cmluZy5jYWxsKG9iaikpIHtcbiAgICBzdGFjay5wdXNoKG9iaik7XG4gICAgY2Fub25pY2FsaXplZE9iaiA9IG5ldyBBcnJheShvYmoubGVuZ3RoKTtcbiAgICByZXBsYWNlbWVudFN0YWNrLnB1c2goY2Fub25pY2FsaXplZE9iaik7XG4gICAgZm9yIChpID0gMDsgaSA8IG9iai5sZW5ndGg7IGkgKz0gMSkge1xuICAgICAgY2Fub25pY2FsaXplZE9ialtpXSA9IGNhbm9uaWNhbGl6ZShvYmpbaV0sIHN0YWNrLCByZXBsYWNlbWVudFN0YWNrLCByZXBsYWNlciwga2V5KTtcbiAgICB9XG4gICAgc3RhY2sucG9wKCk7XG4gICAgcmVwbGFjZW1lbnRTdGFjay5wb3AoKTtcbiAgICByZXR1cm4gY2Fub25pY2FsaXplZE9iajtcbiAgfVxuXG4gIGlmIChvYmogJiYgb2JqLnRvSlNPTikge1xuICAgIG9iaiA9IG9iai50b0pTT04oKTtcbiAgfVxuXG4gIGlmICh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiBvYmogIT09IG51bGwpIHtcbiAgICBzdGFjay5wdXNoKG9iaik7XG4gICAgY2Fub25pY2FsaXplZE9iaiA9IHt9O1xuICAgIHJlcGxhY2VtZW50U3RhY2sucHVzaChjYW5vbmljYWxpemVkT2JqKTtcbiAgICBsZXQgc29ydGVkS2V5cyA9IFtdLFxuICAgICAgICBrZXk7XG4gICAgZm9yIChrZXkgaW4gb2JqKSB7XG4gICAgICAvKiBpc3RhbmJ1bCBpZ25vcmUgZWxzZSAqL1xuICAgICAgaWYgKG9iai5oYXNPd25Qcm9wZXJ0eShrZXkpKSB7XG4gICAgICAgIHNvcnRlZEtleXMucHVzaChrZXkpO1xuICAgICAgfVxuICAgIH1cbiAgICBzb3J0ZWRLZXlzLnNvcnQoKTtcbiAgICBmb3IgKGkgPSAwOyBpIDwgc29ydGVkS2V5cy5sZW5ndGg7IGkgKz0gMSkge1xuICAgICAga2V5ID0gc29ydGVkS2V5c1tpXTtcbiAgICAgIGNhbm9uaWNhbGl6ZWRPYmpba2V5XSA9IGNhbm9uaWNhbGl6ZShvYmpba2V5XSwgc3RhY2ssIHJlcGxhY2VtZW50U3RhY2ssIHJlcGxhY2VyLCBrZXkpO1xuICAgIH1cbiAgICBzdGFjay5wb3AoKTtcbiAgICByZXBsYWNlbWVudFN0YWNrLnBvcCgpO1xuICB9IGVsc2Uge1xuICAgIGNhbm9uaWNhbGl6ZWRPYmogPSBvYmo7XG4gIH1cbiAgcmV0dXJuIGNhbm9uaWNhbGl6ZWRPYmo7XG59XG4iXX0=\n\n\n/***/ }),\n/* 9 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports.arrayDiff = undefined;\n\texports. /*istanbul ignore end*/diffArrays = diffArrays;\n\n\tvar /*istanbul ignore start*/_base = __webpack_require__(1) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _base2 = _interopRequireDefault(_base);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*istanbul ignore end*/var arrayDiff = /*istanbul ignore start*/exports. /*istanbul ignore end*/arrayDiff = new /*istanbul ignore start*/_base2['default'] /*istanbul ignore end*/();\n\tarrayDiff.tokenize = function (value) {\n\t return value.slice();\n\t};\n\tarrayDiff.join = arrayDiff.removeEmpty = function (value) {\n\t return value;\n\t};\n\n\tfunction diffArrays(oldArr, newArr, callback) {\n\t return arrayDiff.diff(oldArr, newArr, callback);\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaWZmL2FycmF5LmpzIl0sIm5hbWVzIjpbImRpZmZBcnJheXMiLCJhcnJheURpZmYiLCJ0b2tlbml6ZSIsInZhbHVlIiwic2xpY2UiLCJqb2luIiwicmVtb3ZlRW1wdHkiLCJvbGRBcnIiLCJuZXdBcnIiLCJjYWxsYmFjayIsImRpZmYiXSwibWFwcGluZ3MiOiI7Ozs7Z0NBVWdCQSxVLEdBQUFBLFU7O0FBVmhCOzs7Ozs7dUJBRU8sSUFBTUMsaUZBQVksd0VBQWxCO0FBQ1BBLFVBQVVDLFFBQVYsR0FBcUIsVUFBU0MsS0FBVCxFQUFnQjtBQUNuQyxTQUFPQSxNQUFNQyxLQUFOLEVBQVA7QUFDRCxDQUZEO0FBR0FILFVBQVVJLElBQVYsR0FBaUJKLFVBQVVLLFdBQVYsR0FBd0IsVUFBU0gsS0FBVCxFQUFnQjtBQUN2RCxTQUFPQSxLQUFQO0FBQ0QsQ0FGRDs7QUFJTyxTQUFTSCxVQUFULENBQW9CTyxNQUFwQixFQUE0QkMsTUFBNUIsRUFBb0NDLFFBQXBDLEVBQThDO0FBQUUsU0FBT1IsVUFBVVMsSUFBVixDQUFlSCxNQUFmLEVBQXVCQyxNQUF2QixFQUErQkMsUUFBL0IsQ0FBUDtBQUFrRCIsImZpbGUiOiJhcnJheS5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBEaWZmIGZyb20gJy4vYmFzZSc7XG5cbmV4cG9ydCBjb25zdCBhcnJheURpZmYgPSBuZXcgRGlmZigpO1xuYXJyYXlEaWZmLnRva2VuaXplID0gZnVuY3Rpb24odmFsdWUpIHtcbiAgcmV0dXJuIHZhbHVlLnNsaWNlKCk7XG59O1xuYXJyYXlEaWZmLmpvaW4gPSBhcnJheURpZmYucmVtb3ZlRW1wdHkgPSBmdW5jdGlvbih2YWx1ZSkge1xuICByZXR1cm4gdmFsdWU7XG59O1xuXG5leHBvcnQgZnVuY3Rpb24gZGlmZkFycmF5cyhvbGRBcnIsIG5ld0FyciwgY2FsbGJhY2spIHsgcmV0dXJuIGFycmF5RGlmZi5kaWZmKG9sZEFyciwgbmV3QXJyLCBjYWxsYmFjayk7IH1cbiJdfQ==\n\n\n/***/ }),\n/* 10 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports. /*istanbul ignore end*/applyPatch = applyPatch;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/applyPatches = applyPatches;\n\n\tvar /*istanbul ignore start*/_parse = __webpack_require__(11) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_distanceIterator = __webpack_require__(12) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/var _distanceIterator2 = _interopRequireDefault(_distanceIterator);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*istanbul ignore end*/function applyPatch(source, uniDiff) {\n\t /*istanbul ignore start*/var /*istanbul ignore end*/options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n\n\t if (typeof uniDiff === 'string') {\n\t uniDiff = /*istanbul ignore start*/(0, _parse.parsePatch) /*istanbul ignore end*/(uniDiff);\n\t }\n\n\t if (Array.isArray(uniDiff)) {\n\t if (uniDiff.length > 1) {\n\t throw new Error('applyPatch only works with a single input.');\n\t }\n\n\t uniDiff = uniDiff[0];\n\t }\n\n\t // Apply the diff to the input\n\t var lines = source.split(/\\r\\n|[\\n\\v\\f\\r\\x85]/),\n\t delimiters = source.match(/\\r\\n|[\\n\\v\\f\\r\\x85]/g) || [],\n\t hunks = uniDiff.hunks,\n\t compareLine = options.compareLine || function (lineNumber, line, operation, patchContent) /*istanbul ignore start*/{\n\t return (/*istanbul ignore end*/line === patchContent\n\t );\n\t },\n\t errorCount = 0,\n\t fuzzFactor = options.fuzzFactor || 0,\n\t minLine = 0,\n\t offset = 0,\n\t removeEOFNL = /*istanbul ignore start*/void 0 /*istanbul ignore end*/,\n\t addEOFNL = /*istanbul ignore start*/void 0 /*istanbul ignore end*/;\n\n\t /**\n\t * Checks if the hunk exactly fits on the provided location\n\t */\n\t function hunkFits(hunk, toPos) {\n\t for (var j = 0; j < hunk.lines.length; j++) {\n\t var line = hunk.lines[j],\n\t operation = line.length > 0 ? line[0] : ' ',\n\t content = line.length > 0 ? line.substr(1) : line;\n\n\t if (operation === ' ' || operation === '-') {\n\t // Context sanity check\n\t if (!compareLine(toPos + 1, lines[toPos], operation, content)) {\n\t errorCount++;\n\n\t if (errorCount > fuzzFactor) {\n\t return false;\n\t }\n\t }\n\t toPos++;\n\t }\n\t }\n\n\t return true;\n\t }\n\n\t // Search best fit offsets for each hunk based on the previous ones\n\t for (var i = 0; i < hunks.length; i++) {\n\t var hunk = hunks[i],\n\t maxLine = lines.length - hunk.oldLines,\n\t localOffset = 0,\n\t toPos = offset + hunk.oldStart - 1;\n\n\t var iterator = /*istanbul ignore start*/(0, _distanceIterator2['default']) /*istanbul ignore end*/(toPos, minLine, maxLine);\n\n\t for (; localOffset !== undefined; localOffset = iterator()) {\n\t if (hunkFits(hunk, toPos + localOffset)) {\n\t hunk.offset = offset += localOffset;\n\t break;\n\t }\n\t }\n\n\t if (localOffset === undefined) {\n\t return false;\n\t }\n\n\t // Set lower text limit to end of the current hunk, so next ones don't try\n\t // to fit over already patched text\n\t minLine = hunk.offset + hunk.oldStart + hunk.oldLines;\n\t }\n\n\t // Apply patch hunks\n\t var diffOffset = 0;\n\t for (var _i = 0; _i < hunks.length; _i++) {\n\t var _hunk = hunks[_i],\n\t _toPos = _hunk.oldStart + _hunk.offset + diffOffset - 1;\n\t diffOffset += _hunk.newLines - _hunk.oldLines;\n\n\t if (_toPos < 0) {\n\t // Creating a new file\n\t _toPos = 0;\n\t }\n\n\t for (var j = 0; j < _hunk.lines.length; j++) {\n\t var line = _hunk.lines[j],\n\t operation = line.length > 0 ? line[0] : ' ',\n\t content = line.length > 0 ? line.substr(1) : line,\n\t delimiter = _hunk.linedelimiters[j];\n\n\t if (operation === ' ') {\n\t _toPos++;\n\t } else if (operation === '-') {\n\t lines.splice(_toPos, 1);\n\t delimiters.splice(_toPos, 1);\n\t /* istanbul ignore else */\n\t } else if (operation === '+') {\n\t lines.splice(_toPos, 0, content);\n\t delimiters.splice(_toPos, 0, delimiter);\n\t _toPos++;\n\t } else if (operation === '\\\\') {\n\t var previousOperation = _hunk.lines[j - 1] ? _hunk.lines[j - 1][0] : null;\n\t if (previousOperation === '+') {\n\t removeEOFNL = true;\n\t } else if (previousOperation === '-') {\n\t addEOFNL = true;\n\t }\n\t }\n\t }\n\t }\n\n\t // Handle EOFNL insertion/removal\n\t if (removeEOFNL) {\n\t while (!lines[lines.length - 1]) {\n\t lines.pop();\n\t delimiters.pop();\n\t }\n\t } else if (addEOFNL) {\n\t lines.push('');\n\t delimiters.push('\\n');\n\t }\n\t for (var _k = 0; _k < lines.length - 1; _k++) {\n\t lines[_k] = lines[_k] + delimiters[_k];\n\t }\n\t return lines.join('');\n\t}\n\n\t// Wrapper that supports multiple file patches via callbacks.\n\tfunction applyPatches(uniDiff, options) {\n\t if (typeof uniDiff === 'string') {\n\t uniDiff = /*istanbul ignore start*/(0, _parse.parsePatch) /*istanbul ignore end*/(uniDiff);\n\t }\n\n\t var currentIndex = 0;\n\t function processIndex() {\n\t var index = uniDiff[currentIndex++];\n\t if (!index) {\n\t return options.complete();\n\t }\n\n\t options.loadFile(index, function (err, data) {\n\t if (err) {\n\t return options.complete(err);\n\t }\n\n\t var updatedContent = applyPatch(data, index, options);\n\t options.patched(index, updatedContent, function (err) {\n\t if (err) {\n\t return options.complete(err);\n\t }\n\n\t processIndex();\n\t });\n\t });\n\t }\n\t processIndex();\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wYXRjaC9hcHBseS5qcyJdLCJuYW1lcyI6WyJhcHBseVBhdGNoIiwiYXBwbHlQYXRjaGVzIiwic291cmNlIiwidW5pRGlmZiIsIm9wdGlvbnMiLCJBcnJheSIsImlzQXJyYXkiLCJsZW5ndGgiLCJFcnJvciIsImxpbmVzIiwic3BsaXQiLCJkZWxpbWl0ZXJzIiwibWF0Y2giLCJodW5rcyIsImNvbXBhcmVMaW5lIiwibGluZU51bWJlciIsImxpbmUiLCJvcGVyYXRpb24iLCJwYXRjaENvbnRlbnQiLCJlcnJvckNvdW50IiwiZnV6ekZhY3RvciIsIm1pbkxpbmUiLCJvZmZzZXQiLCJyZW1vdmVFT0ZOTCIsImFkZEVPRk5MIiwiaHVua0ZpdHMiLCJodW5rIiwidG9Qb3MiLCJqIiwiY29udGVudCIsInN1YnN0ciIsImkiLCJtYXhMaW5lIiwib2xkTGluZXMiLCJsb2NhbE9mZnNldCIsIm9sZFN0YXJ0IiwiaXRlcmF0b3IiLCJ1bmRlZmluZWQiLCJkaWZmT2Zmc2V0IiwibmV3TGluZXMiLCJkZWxpbWl0ZXIiLCJsaW5lZGVsaW1pdGVycyIsInNwbGljZSIsInByZXZpb3VzT3BlcmF0aW9uIiwicG9wIiwicHVzaCIsIl9rIiwiam9pbiIsImN1cnJlbnRJbmRleCIsInByb2Nlc3NJbmRleCIsImluZGV4IiwiY29tcGxldGUiLCJsb2FkRmlsZSIsImVyciIsImRhdGEiLCJ1cGRhdGVkQ29udGVudCIsInBhdGNoZWQiXSwibWFwcGluZ3MiOiI7OztnQ0FHZ0JBLFUsR0FBQUEsVTt5REFvSUFDLFksR0FBQUEsWTs7QUF2SWhCOztBQUNBOzs7Ozs7dUJBRU8sU0FBU0QsVUFBVCxDQUFvQkUsTUFBcEIsRUFBNEJDLE9BQTVCLEVBQW1EO0FBQUEsc0RBQWRDLE9BQWMsdUVBQUosRUFBSTs7QUFDeEQsTUFBSSxPQUFPRCxPQUFQLEtBQW1CLFFBQXZCLEVBQWlDO0FBQy9CQSxjQUFVLHdFQUFXQSxPQUFYLENBQVY7QUFDRDs7QUFFRCxNQUFJRSxNQUFNQyxP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QUFZO0FBQ1YsYUFBTzlDLFFBQVErQyxRQUFSLEVBQVA7QUFDRDs7QUFFRC9DLFlBQVFnRCxRQUFSLENBQWlCRixLQUFqQixFQUF3QixVQUFTRyxHQUFULEVBQWNDLElBQWQsRUFBb0I7QUFDMUMsVUFBSUQsR0FBSixFQUFTO0FBQ1AsZUFBT2pELFFBQVErQyxRQUFSLENBQWlCRSxHQUFqQixDQUFQO0FBQ0Q7O0FBRUQsVUFBSUUsaUJBQWlCdkQsV0FBV3NELElBQVgsRUFBaUJKLEtBQWpCLEVBQXdCOUMsT0FBeEIsQ0FBckI7QUFDQUEsY0FBUW9ELE9BQVIsQ0FBZ0JOLEtBQWhCLEVBQXVCSyxjQUF2QixFQUF1QyxVQUFTRixHQUFULEVBQWM7QUFDbkQsWUFBSUEsR0FBSixFQUFTO0FBQ1AsaUJBQU9qRCxRQUFRK0MsUUFBUixDQUFpQkUsR0FBakIsQ0FBUDtBQUNEOztBQUVESjtBQUNELE9BTkQ7QUFPRCxLQWJEO0FBY0Q7QUFDREE7QUFDRCIsImZpbGUiOiJhcHBseS5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7cGFyc2VQYXRjaH0gZnJvbSAnLi9wYXJzZSc7XG5pbXBvcnQgZGlzdGFuY2VJdGVyYXRvciBmcm9tICcuLi91dGlsL2Rpc3RhbmNlLWl0ZXJhdG9yJztcblxuZXhwb3J0IGZ1bmN0aW9uIGFwcGx5UGF0Y2goc291cmNlLCB1bmlEaWZmLCBvcHRpb25zID0ge30pIHtcbiAgaWYgKHR5cGVvZiB1bmlEaWZmID09PSAnc3RyaW5nJykge1xuICAgIHVuaURpZmYgPSBwYXJzZVBhdGNoKHVuaURpZmYpO1xuICB9XG5cbiAgaWYgKEFycmF5LmlzQXJyYXkodW5pRGlmZikpIHtcbiAgICBpZiAodW5pRGlmZi5sZW5ndGggPiAxKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ2FwcGx5UGF0Y2ggb25seSB3b3JrcyB3aXRoIGEgc2luZ2xlIGlucHV0LicpO1xuICAgIH1cblxuICAgIHVuaURpZmYgPSB1bmlEaWZmWzBdO1xuICB9XG5cbiAgLy8gQXBwbHkgdGhlIGRpZmYgdG8gdGhlIGlucHV0XG4gIGxldCBsaW5lcyA9IHNvdXJjZS5zcGxpdCgvXFxyXFxufFtcXG5cXHZcXGZcXHJcXHg4NV0vKSxcbiAgICAgIGRlbGltaXRlcnMgPSBzb3VyY2UubWF0Y2goL1xcclxcbnxbXFxuXFx2XFxmXFxyXFx4ODVdL2cpIHx8IFtdLFxuICAgICAgaHVua3MgPSB1bmlEaWZmLmh1bmtzLFxuXG4gICAgICBjb21wYXJlTGluZSA9IG9wdGlvbnMuY29tcGFyZUxpbmUgfHwgKChsaW5lTnVtYmVyLCBsaW5lLCBvcGVyYXRpb24sIHBhdGNoQ29udGVudCkgPT4gbGluZSA9PT0gcGF0Y2hDb250ZW50KSxcbiAgICAgIGVycm9yQ291bnQgPSAwLFxuICAgICAgZnV6ekZhY3RvciA9IG9wdGlvbnMuZnV6ekZhY3RvciB8fCAwLFxuICAgICAgbWluTGluZSA9IDAsXG4gICAgICBvZmZzZXQgPSAwLFxuXG4gICAgICByZW1vdmVFT0ZOTCxcbiAgICAgIGFkZEVPRk5MO1xuXG4gIC8qKlxuICAgKiBDaGVja3MgaWYgdGhlIGh1bmsgZXhhY3RseSBmaXRzIG9uIHRoZSBwcm92aWRlZCBsb2NhdGlvblxuICAgKi9cbiAgZnVuY3Rpb24gaHVua0ZpdHMoaHVuaywgdG9Qb3MpIHtcbiAgICBmb3IgKGxldCBqID0gMDsgaiA8IGh1bmsubGluZXMubGVuZ3RoOyBqKyspIHtcbiAgICAgIGxldCBsaW5lID0gaHVuay5saW5lc1tqXSxcbiAgICAgICAgICBvcGVyYXRpb24gPSAobGluZS5sZW5ndGggPiAwID8gbGluZVswXSA6ICcgJyksXG4gICAgICAgICAgY29udGVudCA9IChsaW5lLmxlbmd0aCA+IDAgPyBsaW5lLnN1YnN0cigxKSA6IGxpbmUpO1xuXG4gICAgICBpZiAob3BlcmF0aW9uID09PSAnICcgfHwgb3BlcmF0aW9uID09PSAnLScpIHtcbiAgICAgICAgLy8gQ29udGV4dCBzYW5pdHkgY2hlY2tcbiAgICAgICAgaWYgKCFjb21wYXJlTGluZSh0b1BvcyArIDEsIGxpbmVzW3RvUG9zXSwgb3BlcmF0aW9uLCBjb250ZW50KSkge1xuICAgICAgICAgIGVycm9yQ291bnQrKztcblxuICAgICAgICAgIGlmIChlcnJvckNvdW50ID4gZnV6ekZhY3Rvcikge1xuICAgICAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICB0b1BvcysrO1xuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiB0cnVlO1xuICB9XG5cbiAgLy8gU2VhcmNoIGJlc3QgZml0IG9mZnNldHMgZm9yIGVhY2ggaHVuayBiYXNlZCBvbiB0aGUgcHJldmlvdXMgb25lc1xuICBmb3IgKGxldCBpID0gMDsgaSA8IGh1bmtzLmxlbmd0aDsgaSsrKSB7XG4gICAgbGV0IGh1bmsgPSBodW5rc1tpXSxcbiAgICAgICAgbWF4TGluZSA9IGxpbmVzLmxlbmd0aCAtIGh1bmsub2xkTGluZXMsXG4gICAgICAgIGxvY2FsT2Zmc2V0ID0gMCxcbiAgICAgICAgdG9Qb3MgPSBvZmZzZXQgKyBodW5rLm9sZFN0YXJ0IC0gMTtcblxuICAgIGxldCBpdGVyYXRvciA9IGRpc3RhbmNlSXRlcmF0b3IodG9Qb3MsIG1pbkxpbmUsIG1heExpbmUpO1xuXG4gICAgZm9yICg7IGxvY2FsT2Zmc2V0ICE9PSB1bmRlZmluZWQ7IGxvY2FsT2Zmc2V0ID0gaXRlcmF0b3IoKSkge1xuICAgICAgaWYgKGh1bmtGaXRzKGh1bmssIHRvUG9zICsgbG9jYWxPZmZzZXQpKSB7XG4gICAgICAgIGh1bmsub2Zmc2V0ID0gb2Zmc2V0ICs9IGxvY2FsT2Zmc2V0O1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAobG9jYWxPZmZzZXQgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cblxuICAgIC8vIFNldCBsb3dlciB0ZXh0IGxpbWl0IHRvIGVuZCBvZiB0aGUgY3VycmVudCBodW5rLCBzbyBuZXh0IG9uZXMgZG9uJ3QgdHJ5XG4gICAgLy8gdG8gZml0IG92ZXIgYWxyZWFkeSBwYXRjaGVkIHRleHRcbiAgICBtaW5MaW5lID0gaHVuay5vZmZzZXQgKyBodW5rLm9sZFN0YXJ0ICsgaHVuay5vbGRMaW5lcztcbiAgfVxuXG4gIC8vIEFwcGx5IHBhdGNoIGh1bmtzXG4gIGxldCBkaWZmT2Zmc2V0ID0gMDtcbiAgZm9yIChsZXQgaSA9IDA7IGkgPCBodW5rcy5sZW5ndGg7IGkrKykge1xuICAgIGxldCBodW5rID0gaHVua3NbaV0sXG4gICAgICAgIHRvUG9zID0gaHVuay5vbGRTdGFydCArIGh1bmsub2Zmc2V0ICsgZGlmZk9mZnNldCAtIDE7XG4gICAgZGlmZk9mZnNldCArPSBodW5rLm5ld0xpbmVzIC0gaHVuay5vbGRMaW5lcztcblxuICAgIGlmICh0b1BvcyA8IDApIHsgLy8gQ3JlYXRpbmcgYSBuZXcgZmlsZVxuICAgICAgdG9Qb3MgPSAwO1xuICAgIH1cblxuICAgIGZvciAobGV0IGogPSAwOyBqIDwgaHVuay5saW5lcy5sZW5ndGg7IGorKykge1xuICAgICAgbGV0IGxpbmUgPSBodW5rLmxpbmVzW2pdLFxuICAgICAgICAgIG9wZXJhdGlvbiA9IChsaW5lLmxlbmd0aCA+IDAgPyBsaW5lWzBdIDogJyAnKSxcbiAgICAgICAgICBjb250ZW50ID0gKGxpbmUubGVuZ3RoID4gMCA/IGxpbmUuc3Vic3RyKDEpIDogbGluZSksXG4gICAgICAgICAgZGVsaW1pdGVyID0gaHVuay5saW5lZGVsaW1pdGVyc1tqXTtcblxuICAgICAgaWYgKG9wZXJhdGlvbiA9PT0gJyAnKSB7XG4gICAgICAgIHRvUG9zKys7XG4gICAgICB9IGVsc2UgaWYgKG9wZXJhdGlvbiA9PT0gJy0nKSB7XG4gICAgICAgIGxpbmVzLnNwbGljZSh0b1BvcywgMSk7XG4gICAgICAgIGRlbGltaXRlcnMuc3BsaWNlKHRvUG9zLCAxKTtcbiAgICAgIC8qIGlzdGFuYnVsIGlnbm9yZSBlbHNlICovXG4gICAgICB9IGVsc2UgaWYgKG9wZXJhdGlvbiA9PT0gJysnKSB7XG4gICAgICAgIGxpbmVzLnNwbGljZSh0b1BvcywgMCwgY29udGVudCk7XG4gICAgICAgIGRlbGltaXRlcnMuc3BsaWNlKHRvUG9zLCAwLCBkZWxpbWl0ZXIpO1xuICAgICAgICB0b1BvcysrO1xuICAgICAgfSBlbHNlIGlmIChvcGVyYXRpb24gPT09ICdcXFxcJykge1xuICAgICAgICBsZXQgcHJldmlvdXNPcGVyYXRpb24gPSBodW5rLmxpbmVzW2ogLSAxXSA/IGh1bmsubGluZXNbaiAtIDFdWzBdIDogbnVsbDtcbiAgICAgICAgaWYgKHByZXZpb3VzT3BlcmF0aW9uID09PSAnKycpIHtcbiAgICAgICAgICByZW1vdmVFT0ZOTCA9IHRydWU7XG4gICAgICAgIH0gZWxzZSBpZiAocHJldmlvdXNPcGVyYXRpb24gPT09ICctJykge1xuICAgICAgICAgIGFkZEVPRk5MID0gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC8vIEhhbmRsZSBFT0ZOTCBpbnNlcnRpb24vcmVtb3ZhbFxuICBpZiAocmVtb3ZlRU9GTkwpIHtcbiAgICB3aGlsZSAoIWxpbmVzW2xpbmVzLmxlbmd0aCAtIDFdKSB7XG4gICAgICBsaW5lcy5wb3AoKTtcbiAgICAgIGRlbGltaXRlcnMucG9wKCk7XG4gICAgfVxuICB9IGVsc2UgaWYgKGFkZEVPRk5MKSB7XG4gICAgbGluZXMucHVzaCgnJyk7XG4gICAgZGVsaW1pdGVycy5wdXNoKCdcXG4nKTtcbiAgfVxuICBmb3IgKGxldCBfayA9IDA7IF9rIDwgbGluZXMubGVuZ3RoIC0gMTsgX2srKykge1xuICAgIGxpbmVzW19rXSA9IGxpbmVzW19rXSArIGRlbGltaXRlcnNbX2tdO1xuICB9XG4gIHJldHVybiBsaW5lcy5qb2luKCcnKTtcbn1cblxuLy8gV3JhcHBlciB0aGF0IHN1cHBvcnRzIG11bHRpcGxlIGZpbGUgcGF0Y2hlcyB2aWEgY2FsbGJhY2tzLlxuZXhwb3J0IGZ1bmN0aW9uIGFwcGx5UGF0Y2hlcyh1bmlEaWZmLCBvcHRpb25zKSB7XG4gIGlmICh0eXBlb2YgdW5pRGlmZiA9PT0gJ3N0cmluZycpIHtcbiAgICB1bmlEaWZmID0gcGFyc2VQYXRjaCh1bmlEaWZmKTtcbiAgfVxuXG4gIGxldCBjdXJyZW50SW5kZXggPSAwO1xuICBmdW5jdGlvbiBwcm9jZXNzSW5kZXgoKSB7XG4gICAgbGV0IGluZGV4ID0gdW5pRGlmZltjdXJyZW50SW5kZXgrK107XG4gICAgaWYgKCFpbmRleCkge1xuICAgICAgcmV0dXJuIG9wdGlvbnMuY29tcGxldGUoKTtcbiAgICB9XG5cbiAgICBvcHRpb25zLmxvYWRGaWxlKGluZGV4LCBmdW5jdGlvbihlcnIsIGRhdGEpIHtcbiAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgcmV0dXJuIG9wdGlvbnMuY29tcGxldGUoZXJyKTtcbiAgICAgIH1cblxuICAgICAgbGV0IHVwZGF0ZWRDb250ZW50ID0gYXBwbHlQYXRjaChkYXRhLCBpbmRleCwgb3B0aW9ucyk7XG4gICAgICBvcHRpb25zLnBhdGNoZWQoaW5kZXgsIHVwZGF0ZWRDb250ZW50LCBmdW5jdGlvbihlcnIpIHtcbiAgICAgICAgaWYgKGVycikge1xuICAgICAgICAgIHJldHVybiBvcHRpb25zLmNvbXBsZXRlKGVycik7XG4gICAgICAgIH1cblxuICAgICAgICBwcm9jZXNzSW5kZXgoKTtcbiAgICAgIH0pO1xuICAgIH0pO1xuICB9XG4gIHByb2Nlc3NJbmRleCgpO1xufVxuIl19\n\n\n/***/ }),\n/* 11 */\n/***/ (function(module, exports) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports. /*istanbul ignore end*/parsePatch = parsePatch;\n\tfunction parsePatch(uniDiff) {\n\t /*istanbul ignore start*/var /*istanbul ignore end*/options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n\n\t var diffstr = uniDiff.split(/\\r\\n|[\\n\\v\\f\\r\\x85]/),\n\t delimiters = uniDiff.match(/\\r\\n|[\\n\\v\\f\\r\\x85]/g) || [],\n\t list = [],\n\t i = 0;\n\n\t function parseIndex() {\n\t var index = {};\n\t list.push(index);\n\n\t // Parse diff metadata\n\t while (i < diffstr.length) {\n\t var line = diffstr[i];\n\n\t // File header found, end parsing diff metadata\n\t if (/^(\\-\\-\\-|\\+\\+\\+|@@)\\s/.test(line)) {\n\t break;\n\t }\n\n\t // Diff index\n\t var header = /^(?:Index:|diff(?: -r \\w+)+)\\s+(.+?)\\s*$/.exec(line);\n\t if (header) {\n\t index.index = header[1];\n\t }\n\n\t i++;\n\t }\n\n\t // Parse file headers if they are defined. Unified diff requires them, but\n\t // there's no technical issues to have an isolated hunk without file header\n\t parseFileHeader(index);\n\t parseFileHeader(index);\n\n\t // Parse hunks\n\t index.hunks = [];\n\n\t while (i < diffstr.length) {\n\t var _line = diffstr[i];\n\n\t if (/^(Index:|diff|\\-\\-\\-|\\+\\+\\+)\\s/.test(_line)) {\n\t break;\n\t } else if (/^@@/.test(_line)) {\n\t index.hunks.push(parseHunk());\n\t } else if (_line && options.strict) {\n\t // Ignore unexpected content unless in strict mode\n\t throw new Error('Unknown line ' + (i + 1) + ' ' + JSON.stringify(_line));\n\t } else {\n\t i++;\n\t }\n\t }\n\t }\n\n\t // Parses the --- and +++ headers, if none are found, no lines\n\t // are consumed.\n\t function parseFileHeader(index) {\n\t var fileHeader = /^(---|\\+\\+\\+)\\s+(.*)$/.exec(diffstr[i]);\n\t if (fileHeader) {\n\t var keyPrefix = fileHeader[1] === '---' ? 'old' : 'new';\n\t var data = fileHeader[2].split('\\t', 2);\n\t var fileName = data[0].replace(/\\\\\\\\/g, '\\\\');\n\t if (/^\".*\"$/.test(fileName)) {\n\t fileName = fileName.substr(1, fileName.length - 2);\n\t }\n\t index[keyPrefix + 'FileName'] = fileName;\n\t index[keyPrefix + 'Header'] = (data[1] || '').trim();\n\n\t i++;\n\t }\n\t }\n\n\t // Parses a hunk\n\t // This assumes that we are at the start of a hunk.\n\t function parseHunk() {\n\t var chunkHeaderIndex = i,\n\t chunkHeaderLine = diffstr[i++],\n\t chunkHeader = chunkHeaderLine.split(/@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@/);\n\n\t var hunk = {\n\t oldStart: +chunkHeader[1],\n\t oldLines: +chunkHeader[2] || 1,\n\t newStart: +chunkHeader[3],\n\t newLines: +chunkHeader[4] || 1,\n\t lines: [],\n\t linedelimiters: []\n\t };\n\n\t var addCount = 0,\n\t removeCount = 0;\n\t for (; i < diffstr.length; i++) {\n\t // Lines starting with '---' could be mistaken for the \"remove line\" operation\n\t // But they could be the header for the next file. Therefore prune such cases out.\n\t if (diffstr[i].indexOf('--- ') === 0 && i + 2 < diffstr.length && diffstr[i + 1].indexOf('+++ ') === 0 && diffstr[i + 2].indexOf('@@') === 0) {\n\t break;\n\t }\n\t var operation = diffstr[i].length == 0 && i != diffstr.length - 1 ? ' ' : diffstr[i][0];\n\n\t if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\\\') {\n\t hunk.lines.push(diffstr[i]);\n\t hunk.linedelimiters.push(delimiters[i] || '\\n');\n\n\t if (operation === '+') {\n\t addCount++;\n\t } else if (operation === '-') {\n\t removeCount++;\n\t } else if (operation === ' ') {\n\t addCount++;\n\t removeCount++;\n\t }\n\t } else {\n\t break;\n\t }\n\t }\n\n\t // Handle the empty block count case\n\t if (!addCount && hunk.newLines === 1) {\n\t hunk.newLines = 0;\n\t }\n\t if (!removeCount && hunk.oldLines === 1) {\n\t hunk.oldLines = 0;\n\t }\n\n\t // Perform optional sanity checking\n\t if (options.strict) {\n\t if (addCount !== hunk.newLines) {\n\t throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1));\n\t }\n\t if (removeCount !== hunk.oldLines) {\n\t throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1));\n\t }\n\t }\n\n\t return hunk;\n\t }\n\n\t while (i < diffstr.length) {\n\t parseIndex();\n\t }\n\n\t return list;\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wYXRjaC9wYXJzZS5qcyJdLCJuYW1lcyI6WyJwYXJzZVBhdGNoIiwidW5pRGlmZiIsIm9wdGlvbnMiLCJkaWZmc3RyIiwic3BsaXQiLCJkZWxpbWl0ZXJzIiwibWF0Y2giLCJsaXN0IiwiaSIsInBhcnNlSW5kZXgiLCJpbmRleCIsInB1c2giLCJsZW5ndGgiLCJsaW5lIiwidGVzdCIsImhlYWRlciIsImV4ZWMiLCJwYXJzZUZpbGVIZWFkZXIiLCJodW5rcyIsInBhcnNlSHVuayIsInN0cmljdCIsIkVycm9yIiwiSlNPTiIsInN0cmluZ2lmeSIsImZpbGVIZWFkZXIiLCJrZXlQcmVmaXgiLCJkYXRhIiwiZmlsZU5hbWUiLCJyZXBsYWNlIiwic3Vic3RyIiwidHJpbSIsImNodW5rSGVhZGVySW5kZXgiLCJjaHVua0hlYWRlckxpbmUiLCJjaHVua0hlYWRlciIsImh1bmsiLCJvbGRTdGFydCIsIm9sZExpbmVzIiwibmV3U3RhcnQiLCJuZXdMaW5lcyIsImxpbmVzIiwibGluZWRlbGltaXRlcnMiLCJhZGRDb3VudCIsInJlbW92ZUNvdW50IiwiaW5kZXhPZiIsIm9wZXJhdGlvbiJdLCJtYXBwaW5ncyI6Ijs7O2dDQUFnQkEsVSxHQUFBQSxVO0FBQVQsU0FBU0EsVUFBVCxDQUFvQkMsT0FBcEIsRUFBMkM7QUFBQSxzREFBZEMsT0FBYyx1RUFBSixFQUFJOztBQUNoRCxNQUFJQyxVQUFVRixRQUFRRyxLQUFSLENBQWMscUJBQWQsQ0FBZDtBQUFBLE1BQ0lDLGFBQWFKLFFBQVFLLEtBQVIsQ0FBYyxzQkFBZCxLQUF5QyxFQUQxRDtBQUFBLE1BRUlDLE9BQU8sRUFGWDtBQUFBLE1BR0lDLElBQUksQ0FIUjs7QUFLQSxXQUFTQyxVQUFULEdBQXNCO0FBQ3BCLFFBQUlDLFFBQVEsRUFBWjtBQUNBSCxTQUFLSSxJQUFMLENBQVVELEtBQVY7O0FBRUE7QUFDQSxXQUFPRixJQUFJTCxRQUFRUyxNQUFuQixFQUEyQjtBQUN6QixVQUFJQyxPQUFPVixRQUFR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QW9CO0FBQ2xCLFVBQUlxQixhQUFhUCxLQUFLSSxRQUF0QixFQUFnQztBQUM5QixjQUFNLElBQUlqQixLQUFKLENBQVUsc0RBQXNEVSxtQkFBbUIsQ0FBekUsQ0FBVixDQUFOO0FBQ0Q7QUFDRCxVQUFJVyxnQkFBZ0JSLEtBQUtFLFFBQXpCLEVBQW1DO0FBQ2pDLGNBQU0sSUFBSWYsS0FBSixDQUFVLHdEQUF3RFUsbUJBQW1CLENBQTNFLENBQVYsQ0FBTjtBQUNEO0FBQ0Y7O0FBRUQsV0FBT0csSUFBUDtBQUNEOztBQUVELFNBQU8xQixJQUFJTCxRQUFRUyxNQUFuQixFQUEyQjtBQUN6Qkg7QUFDRDs7QUFFRCxTQUFPRixJQUFQO0FBQ0QiLCJmaWxlIjoicGFyc2UuanMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gcGFyc2VQYXRjaCh1bmlEaWZmLCBvcHRpb25zID0ge30pIHtcbiAgbGV0IGRpZmZzdHIgPSB1bmlEaWZmLnNwbGl0KC9cXHJcXG58W1xcblxcdlxcZlxcclxceDg1XS8pLFxuICAgICAgZGVsaW1pdGVycyA9IHVuaURpZmYubWF0Y2goL1xcclxcbnxbXFxuXFx2XFxmXFxyXFx4ODVdL2cpIHx8IFtdLFxuICAgICAgbGlzdCA9IFtdLFxuICAgICAgaSA9IDA7XG5cbiAgZnVuY3Rpb24gcGFyc2VJbmRleCgpIHtcbiAgICBsZXQgaW5kZXggPSB7fTtcbiAgICBsaXN0LnB1c2goaW5kZXgpO1xuXG4gICAgLy8gUGFyc2UgZGlmZiBtZXRhZGF0YVxuICAgIHdoaWxlIChpIDwgZGlmZnN0ci5sZW5ndGgpIHtcbiAgICAgIGxldCBsaW5lID0gZGlmZnN0cltpXTtcblxuICAgICAgLy8gRmlsZSBoZWFkZXIgZm91bmQsIGVuZCBwYXJzaW5nIGRpZmYgbWV0YWRhdGFcbiAgICAgIGlmICgvXihcXC1cXC1cXC18XFwrXFwrXFwrfEBAKVxccy8udGVzdChsaW5lKSkge1xuICAgICAgICBicmVhaztcbiAgICAgIH1cblxuICAgICAgLy8gRGlmZiBpbmRleFxuICAgICAgbGV0IGhlYWRlciA9ICgvXig/OkluZGV4OnxkaWZmKD86IC1yIFxcdyspKylcXHMrKC4rPylcXHMqJC8pLmV4ZWMobGluZSk7XG4gICAgICBpZiAoaGVhZGVyKSB7XG4gICAgICAgIGluZGV4LmluZGV4ID0gaGVhZGVyWzFdO1xuICAgICAgfVxuXG4gICAgICBpKys7XG4gICAgfVxuXG4gICAgLy8gUGFyc2UgZmlsZSBoZWFkZXJzIGlmIHRoZXkgYXJlIGRlZmluZWQuIFVuaWZpZWQgZGlmZiByZXF1aXJlcyB0aGVtLCBidXRcbiAgICAvLyB0aGVyZSdzIG5vIHRlY2huaWNhbCBpc3N1ZXMgdG8gaGF2ZSBhbiBpc29sYXRlZCBodW5rIHdpdGhvdXQgZmlsZSBoZWFkZXJcbiAgICBwYXJzZUZpbGVIZWFkZXIoaW5kZXgpO1xuICAgIHBhcnNlRmlsZUhlYWRlcihpbmRleCk7XG5cbiAgICAvLyBQYXJzZSBodW5rc1xuICAgIGluZGV4Lmh1bmtzID0gW107XG5cbiAgICB3aGlsZSAoaSA8IGRpZmZzdHIubGVuZ3RoKSB7XG4gICAgICBsZXQgbGluZSA9IGRpZmZzdHJbaV07XG5cbiAgICAgIGlmICgvXihJbmRleDp8ZGlmZnxcXC1cXC1cXC18XFwrXFwrXFwrKVxccy8udGVzdChsaW5lKSkge1xuICAgICAgICBicmVhaztcbiAgICAgIH0gZWxzZSBpZiAoL15AQC8udGVzdChsaW5lKSkge1xuICAgICAgICBpbmRleC5odW5rcy5wdXNoKHBhcnNlSHVuaygpKTtcbiAgICAgIH0gZWxzZSBpZiAobGluZSAmJiBvcHRpb25zLnN0cmljdCkge1xuICAgICAgICAvLyBJZ25vcmUgdW5leHBlY3RlZCBjb250ZW50IHVubGVzcyBpbiBzdHJpY3QgbW9kZVxuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ1Vua25vd24gbGluZSAnICsgKGkgKyAxKSArICcgJyArIEpTT04uc3RyaW5naWZ5KGxpbmUpKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGkrKztcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAvLyBQYXJzZXMgdGhlIC0tLSBhbmQgKysrIGhlYWRlcnMsIGlmIG5vbmUgYXJlIGZvdW5kLCBubyBsaW5lc1xuICAvLyBhcmUgY29uc3VtZWQuXG4gIGZ1bmN0aW9uIHBhcnNlRmlsZUhlYWRlcihpbmRleCkge1xuICAgIGNvbnN0IGZpbGVIZWFkZXIgPSAoL14oLS0tfFxcK1xcK1xcKylcXHMrKC4qKSQvKS5leGVjKGRpZmZzdHJbaV0pO1xuICAgIGlmIChmaWxlSGVhZGVyKSB7XG4gICAgICBsZXQga2V5UHJlZml4ID0gZmlsZUhlYWRlclsxXSA9PT0gJy0tLScgPyAnb2xkJyA6ICduZXcnO1xuICAgICAgY29uc3QgZGF0YSA9IGZpbGVIZWFkZXJbMl0uc3BsaXQoJ1xcdCcsIDIpO1xuICAgICAgbGV0IGZpbGVOYW1lID0gZGF0YVswXS5yZXBsYWNlKC9cXFxcXFxcXC9nLCAnXFxcXCcpO1xuICAgICAgaWYgKC9eXCIuKlwiJC8udGVzdChmaWxlTmFtZSkpIHtcbiAgICAgICAgZmlsZU5hbWUgPSBmaWxlTmFtZS5zdWJzdHIoMSwgZmlsZU5hbWUubGVuZ3RoIC0gMik7XG4gICAgICB9XG4gICAgICBpbmRleFtrZXlQcmVmaXggKyAnRmlsZU5hbWUnXSA9IGZpbGVOYW1lO1xuICAgICAgaW5kZXhba2V5UHJlZml4ICsgJ0hlYWRlciddID0gKGRhdGFbMV0gfHwgJycpLnRyaW0oKTtcblxuICAgICAgaSsrO1xuICAgIH1cbiAgfVxuXG4gIC8vIFBhcnNlcyBhIGh1bmtcbiAgLy8gVGhpcyBhc3N1bWVzIHRoYXQgd2UgYXJlIGF0IHRoZSBzdGFydCBvZiBhIGh1bmsuXG4gIGZ1bmN0aW9uIHBhcnNlSHVuaygpIHtcbiAgICBsZXQgY2h1bmtIZWFkZXJJbmRleCA9IGksXG4gICAgICAgIGNodW5rSGVhZGVyTGluZSA9IGRpZmZzdHJbaSsrXSxcbiAgICAgICAgY2h1bmtIZWFkZXIgPSBjaHVua0hlYWRlckxpbmUuc3BsaXQoL0BAIC0oXFxkKykoPzosKFxcZCspKT8gXFwrKFxcZCspKD86LChcXGQrKSk/IEBALyk7XG5cbiAgICBsZXQgaHVuayA9IHtcbiAgICAgIG9sZFN0YXJ0OiArY2h1bmtIZWFkZXJbMV0sXG4gICAgICBvbGRMaW5lczogK2NodW5rSGVhZGVyWzJdIHx8IDEsXG4gICAgICBuZXdTdGFydDogK2NodW5rSGVhZGVyWzNdLFxuICAgICAgbmV3TGluZXM6ICtjaHVua0hlYWRlcls0XSB8fCAxLFxuICAgICAgbGluZXM6IFtdLFxuICAgICAgbGluZWRlbGltaXRlcnM6IFtdXG4gICAgfTtcblxuICAgIGxldCBhZGRDb3VudCA9IDAsXG4gICAgICAgIHJlbW92ZUNvdW50ID0gMDtcbiAgICBmb3IgKDsgaSA8IGRpZmZzdHIubGVuZ3RoOyBpKyspIHtcbiAgICAgIC8vIExpbmVzIHN0YXJ0aW5nIHdpdGggJy0tLScgY291bGQgYmUgbWlzdGFrZW4gZm9yIHRoZSBcInJlbW92ZSBsaW5lXCIgb3BlcmF0aW9uXG4gICAgICAvLyBCdXQgdGhleSBjb3VsZCBiZSB0aGUgaGVhZGVyIGZvciB0aGUgbmV4dCBmaWxlLiBUaGVyZWZvcmUgcHJ1bmUgc3VjaCBjYXNlcyBvdXQuXG4gICAgICBpZiAoZGlmZnN0cltpXS5pbmRleE9mKCctLS0gJykgPT09IDBcbiAgICAgICAgICAgICYmIChpICsgMiA8IGRpZmZzdHIubGVuZ3RoKVxuICAgICAgICAgICAgJiYgZGlmZnN0cltpICsgMV0uaW5kZXhPZignKysrICcpID09PSAwXG4gICAgICAgICAgICAmJiBkaWZmc3RyW2kgKyAyXS5pbmRleE9mKCdAQCcpID09PSAwKSB7XG4gICAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgICBsZXQgb3BlcmF0aW9uID0gKGRpZmZzdHJbaV0ubGVuZ3RoID09IDAgJiYgaSAhPSAoZGlmZnN0ci5sZW5ndGggLSAxKSkgPyAnICcgOiBkaWZmc3RyW2ldWzBdO1xuXG4gICAgICBpZiAob3BlcmF0aW9uID09PSAnKycgfHwgb3BlcmF0aW9uID09PSAnLScgfHwgb3BlcmF0aW9uID09PSAnICcgfHwgb3BlcmF0aW9uID09PSAnXFxcXCcpIHtcbiAgICAgICAgaHVuay5saW5lcy5wdXNoKGRpZmZzdHJbaV0pO1xuICAgICAgICBodW5rLmxpbmVkZWxpbWl0ZXJzLnB1c2goZGVsaW1pdGVyc1tpXSB8fCAnXFxuJyk7XG5cbiAgICAgICAgaWYgKG9wZXJhdGlvbiA9PT0gJysnKSB7XG4gICAgICAgICAgYWRkQ291bnQrKztcbiAgICAgICAgfSBlbHNlIGlmIChvcGVyYXRpb24gPT09ICctJykge1xuICAgICAgICAgIHJlbW92ZUNvdW50Kys7XG4gICAgICAgIH0gZWxzZSBpZiAob3BlcmF0aW9uID09PSAnICcpIHtcbiAgICAgICAgICBhZGRDb3VudCsrO1xuICAgICAgICAgIHJlbW92ZUNvdW50Kys7XG4gICAgICAgIH1cbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8vIEhhbmRsZSB0aGUgZW1wdHkgYmxvY2sgY291bnQgY2FzZVxuICAgIGlmICghYWRkQ291bnQgJiYgaHVuay5uZXdMaW5lcyA9PT0gMSkge1xuICAgICAgaHVuay5uZXdMaW5lcyA9IDA7XG4gICAgfVxuICAgIGlmICghcmVtb3ZlQ291bnQgJiYgaHVuay5vbGRMaW5lcyA9PT0gMSkge1xuICAgICAgaHVuay5vbGRMaW5lcyA9IDA7XG4gICAgfVxuXG4gICAgLy8gUGVyZm9ybSBvcHRpb25hbCBzYW5pdHkgY2hlY2tpbmdcbiAgICBpZiAob3B0aW9ucy5zdHJpY3QpIHtcbiAgICAgIGlmIChhZGRDb3VudCAhPT0gaHVuay5uZXdMaW5lcykge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ0FkZGVkIGxpbmUgY291bnQgZGlkIG5vdCBtYXRjaCBmb3IgaHVuayBhdCBsaW5lICcgKyAoY2h1bmtIZWFkZXJJbmRleCArIDEpKTtcbiAgICAgIH1cbiAgICAgIGlmIChyZW1vdmVDb3VudCAhPT0gaHVuay5vbGRMaW5lcykge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ1JlbW92ZWQgbGluZSBjb3VudCBkaWQgbm90IG1hdGNoIGZvciBodW5rIGF0IGxpbmUgJyArIChjaHVua0hlYWRlckluZGV4ICsgMSkpO1xuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiBodW5rO1xuICB9XG5cbiAgd2hpbGUgKGkgPCBkaWZmc3RyLmxlbmd0aCkge1xuICAgIHBhcnNlSW5kZXgoKTtcbiAgfVxuXG4gIHJldHVybiBsaXN0O1xufVxuIl19\n\n\n/***/ }),\n/* 12 */\n/***/ (function(module, exports) {\n\n\t/*istanbul ignore start*/\"use strict\";\n\n\texports.__esModule = true;\n\n\texports[\"default\"] = /*istanbul ignore end*/function (start, minLine, maxLine) {\n\t var wantForward = true,\n\t backwardExhausted = false,\n\t forwardExhausted = false,\n\t localOffset = 1;\n\n\t return function iterator() {\n\t if (wantForward && !forwardExhausted) {\n\t if (backwardExhausted) {\n\t localOffset++;\n\t } else {\n\t wantForward = false;\n\t }\n\n\t // Check if trying to fit beyond text length, and if not, check it fits\n\t // after offset location (or desired location on first iteration)\n\t if (start + localOffset <= maxLine) {\n\t return localOffset;\n\t }\n\n\t forwardExhausted = true;\n\t }\n\n\t if (!backwardExhausted) {\n\t if (!forwardExhausted) {\n\t wantForward = true;\n\t }\n\n\t // Check if trying to fit before text beginning, and if not, check it fits\n\t // before offset location\n\t if (minLine <= start - localOffset) {\n\t return -localOffset++;\n\t }\n\n\t backwardExhausted = true;\n\t return iterator();\n\t }\n\n\t // We tried to fit hunk before text beginning and beyond text length, then\n\t // hunk can't fit on the text. Return undefined\n\t };\n\t};\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlsL2Rpc3RhbmNlLWl0ZXJhdG9yLmpzIl0sIm5hbWVzIjpbInN0YXJ0IiwibWluTGluZSIsIm1heExpbmUiLCJ3YW50Rm9yd2FyZCIsImJhY2t3YXJkRXhoYXVzdGVkIiwiZm9yd2FyZEV4aGF1c3RlZCIsImxvY2FsT2Zmc2V0IiwiaXRlcmF0b3IiXSwibWFwcGluZ3MiOiI7Ozs7NENBR2UsVUFBU0EsS0FBVCxFQUFnQkMsT0FBaEIsRUFBeUJDLE9BQXpCLEVBQWtDO0FBQy9DLE1BQUlDLGNBQWMsSUFBbEI7QUFBQSxNQUNJQyxvQkFBb0IsS0FEeEI7QUFBQSxNQUVJQyxtQkFBbUIsS0FGdkI7QUFBQSxNQUdJQyxjQUFjLENBSGxCOztBQUtBLFNBQU8sU0FBU0MsUUFBVCxHQUFvQjtBQUN6QixRQUFJSixlQUFlLENBQUNFLGdCQUFwQixFQUFzQztBQUNwQyxVQUFJRCxpQkFBSixFQUF1QjtBQUNyQkU7QUFDRCxPQUZELE1BRU87QUFDTEgsc0JBQWMsS0FBZDtBQUNEOztBQUVEO0FBQ0E7QUFDQSxVQUFJSCxRQUFRTSxXQUFSLElBQXVCSixPQUEzQixFQUFvQztBQUNsQyxlQUFPSSxXQUFQO0FBQ0Q7O0FBRURELHlCQUFtQixJQUFuQjtBQUNEOztBQUVELFFBQUksQ0FBQ0QsaUJBQUwsRUFBd0I7QUFDdEIsVUFBSSxDQUFDQyxnQkFBTCxFQUF1QjtBQUNyQkYsc0JBQWMsSUFBZDtBQUNEOztBQUVEO0FBQ0E7QUFDQSxVQUFJRixXQUFXRCxRQUFRTSxXQUF2QixFQUFvQztBQUNsQyxlQUFPLENBQUNBLGFBQVI7QUFDRDs7QUFFREYsMEJBQW9CLElBQXBCO0FBQ0EsYUFBT0csVUFBUDtBQUNEOztBQUVEO0FBQ0E7QUFDRCxHQWxDRDtBQW1DRCxDIiwiZmlsZSI6ImRpc3RhbmNlLWl0ZXJhdG9yLmpzIiwic291cmNlc0NvbnRlbnQiOlsiLy8gSXRlcmF0b3IgdGhhdCB0cmF2ZXJzZXMgaW4gdGhlIHJhbmdlIG9mIFttaW4sIG1heF0sIHN0ZXBwaW5nXG4vLyBieSBkaXN0YW5jZSBmcm9tIGEgZ2l2ZW4gc3RhcnQgcG9zaXRpb24uIEkuZS4gZm9yIFswLCA0XSwgd2l0aFxuLy8gc3RhcnQgb2YgMiwgdGhpcyB3aWxsIGl0ZXJhdGUgMiwgMywgMSwgNCwgMC5cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uKHN0YXJ0LCBtaW5MaW5lLCBtYXhMaW5lKSB7XG4gIGxldCB3YW50Rm9yd2FyZCA9IHRydWUsXG4gICAgICBiYWNrd2FyZEV4aGF1c3RlZCA9IGZhbHNlLFxuICAgICAgZm9yd2FyZEV4aGF1c3RlZCA9IGZhbHNlLFxuICAgICAgbG9jYWxPZmZzZXQgPSAxO1xuXG4gIHJldHVybiBmdW5jdGlvbiBpdGVyYXRvcigpIHtcbiAgICBpZiAod2FudEZvcndhcmQgJiYgIWZvcndhcmRFeGhhdXN0ZWQpIHtcbiAgICAgIGlmIChiYWNrd2FyZEV4aGF1c3RlZCkge1xuICAgICAgICBsb2NhbE9mZnNldCsrO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgd2FudEZvcndhcmQgPSBmYWxzZTtcbiAgICAgIH1cblxuICAgICAgLy8gQ2hlY2sgaWYgdHJ5aW5nIHRvIGZpdCBiZXlvbmQgdGV4dCBsZW5ndGgsIGFuZCBpZiBub3QsIGNoZWNrIGl0IGZpdHNcbiAgICAgIC8vIGFmdGVyIG9mZnNldCBsb2NhdGlvbiAob3IgZGVzaXJlZCBsb2NhdGlvbiBvbiBmaXJzdCBpdGVyYXRpb24pXG4gICAgICBpZiAoc3RhcnQgKyBsb2NhbE9mZnNldCA8PSBtYXhMaW5lKSB7XG4gICAgICAgIHJldHVybiBsb2NhbE9mZnNldDtcbiAgICAgIH1cblxuICAgICAgZm9yd2FyZEV4aGF1c3RlZCA9IHRydWU7XG4gICAgfVxuXG4gICAgaWYgKCFiYWNrd2FyZEV4aGF1c3RlZCkge1xuICAgICAgaWYgKCFmb3J3YXJkRXhoYXVzdGVkKSB7XG4gICAgICAgIHdhbnRGb3J3YXJkID0gdHJ1ZTtcbiAgICAgIH1cblxuICAgICAgLy8gQ2hlY2sgaWYgdHJ5aW5nIHRvIGZpdCBiZWZvcmUgdGV4dCBiZWdpbm5pbmcsIGFuZCBpZiBub3QsIGNoZWNrIGl0IGZpdHNcbiAgICAgIC8vIGJlZm9yZSBvZmZzZXQgbG9jYXRpb25cbiAgICAgIGlmIChtaW5MaW5lIDw9IHN0YXJ0IC0gbG9jYWxPZmZzZXQpIHtcbiAgICAgICAgcmV0dXJuIC1sb2NhbE9mZnNldCsrO1xuICAgICAgfVxuXG4gICAgICBiYWNrd2FyZEV4aGF1c3RlZCA9IHRydWU7XG4gICAgICByZXR1cm4gaXRlcmF0b3IoKTtcbiAgICB9XG5cbiAgICAvLyBXZSB0cmllZCB0byBmaXQgaHVuayBiZWZvcmUgdGV4dCBiZWdpbm5pbmcgYW5kIGJleW9uZCB0ZXh0IGxlbmd0aCwgdGhlblxuICAgIC8vIGh1bmsgY2FuJ3QgZml0IG9uIHRoZSB0ZXh0LiBSZXR1cm4gdW5kZWZpbmVkXG4gIH07XG59XG4iXX0=\n\n\n/***/ }),\n/* 13 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports. /*istanbul ignore end*/calcLineCount = calcLineCount;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/merge = merge;\n\n\tvar /*istanbul ignore start*/_create = __webpack_require__(14) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_parse = __webpack_require__(11) /*istanbul ignore end*/;\n\n\tvar /*istanbul ignore start*/_array = __webpack_require__(15) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }\n\n\t/*istanbul ignore end*/function calcLineCount(hunk) {\n\t /*istanbul ignore start*/var _calcOldNewLineCount = /*istanbul ignore end*/calcOldNewLineCount(hunk.lines),\n\t oldLines = _calcOldNewLineCount.oldLines,\n\t newLines = _calcOldNewLineCount.newLines;\n\n\t if (oldLines !== undefined) {\n\t hunk.oldLines = oldLines;\n\t } else {\n\t delete hunk.oldLines;\n\t }\n\n\t if (newLines !== undefined) {\n\t hunk.newLines = newLines;\n\t } else {\n\t delete hunk.newLines;\n\t }\n\t}\n\n\tfunction merge(mine, theirs, base) {\n\t mine = loadPatch(mine, base);\n\t theirs = loadPatch(theirs, base);\n\n\t var ret = {};\n\n\t // For index we just let it pass through as it doesn't have any necessary meaning.\n\t // Leaving sanity checks on this to the API consumer that may know more about the\n\t // meaning in their own context.\n\t if (mine.index || theirs.index) {\n\t ret.index = mine.index || theirs.index;\n\t }\n\n\t if (mine.newFileName || theirs.newFileName) {\n\t if (!fileNameChanged(mine)) {\n\t // No header or no change in ours, use theirs (and ours if theirs does not exist)\n\t ret.oldFileName = theirs.oldFileName || mine.oldFileName;\n\t ret.newFileName = theirs.newFileName || mine.newFileName;\n\t ret.oldHeader = theirs.oldHeader || mine.oldHeader;\n\t ret.newHeader = theirs.newHeader || mine.newHeader;\n\t } else if (!fileNameChanged(theirs)) {\n\t // No header or no change in theirs, use ours\n\t ret.oldFileName = mine.oldFileName;\n\t ret.newFileName = mine.newFileName;\n\t ret.oldHeader = mine.oldHeader;\n\t ret.newHeader = mine.newHeader;\n\t } else {\n\t // Both changed... figure it out\n\t ret.oldFileName = selectField(ret, mine.oldFileName, theirs.oldFileName);\n\t ret.newFileName = selectField(ret, mine.newFileName, theirs.newFileName);\n\t ret.oldHeader = selectField(ret, mine.oldHeader, theirs.oldHeader);\n\t ret.newHeader = selectField(ret, mine.newHeader, theirs.newHeader);\n\t }\n\t }\n\n\t ret.hunks = [];\n\n\t var mineIndex = 0,\n\t theirsIndex = 0,\n\t mineOffset = 0,\n\t theirsOffset = 0;\n\n\t while (mineIndex < mine.hunks.length || theirsIndex < theirs.hunks.length) {\n\t var mineCurrent = mine.hunks[mineIndex] || { oldStart: Infinity },\n\t theirsCurrent = theirs.hunks[theirsIndex] || { oldStart: Infinity };\n\n\t if (hunkBefore(mineCurrent, theirsCurrent)) {\n\t // This patch does not overlap with any of the others, yay.\n\t ret.hunks.push(cloneHunk(mineCurrent, mineOffset));\n\t mineIndex++;\n\t theirsOffset += mineCurrent.newLines - mineCurrent.oldLines;\n\t } else if (hunkBefore(theirsCurrent, mineCurrent)) {\n\t // This patch does not overlap with any of the others, yay.\n\t ret.hunks.push(cloneHunk(theirsCurrent, theirsOffset));\n\t theirsIndex++;\n\t mineOffset += theirsCurrent.newLines - theirsCurrent.oldLines;\n\t } else {\n\t // Overlap, merge as best we can\n\t var mergedHunk = {\n\t oldStart: Math.min(mineCurrent.oldStart, theirsCurrent.oldStart),\n\t oldLines: 0,\n\t newStart: Math.min(mineCurrent.newStart + mineOffset, theirsCurrent.oldStart + theirsOffset),\n\t newLines: 0,\n\t lines: []\n\t };\n\t mergeLines(mergedHunk, mineCurrent.oldStart, mineCurrent.lines, theirsCurrent.oldStart, theirsCurrent.lines);\n\t theirsIndex++;\n\t mineIndex++;\n\n\t ret.hunks.push(mergedHunk);\n\t }\n\t }\n\n\t return ret;\n\t}\n\n\tfunction loadPatch(param, base) {\n\t if (typeof param === 'string') {\n\t if (/^@@/m.test(param) || /^Index:/m.test(param)) {\n\t return (/*istanbul ignore start*/(0, _parse.parsePatch) /*istanbul ignore end*/(param)[0]\n\t );\n\t }\n\n\t if (!base) {\n\t throw new Error('Must provide a base reference or pass in a patch');\n\t }\n\t return (/*istanbul ignore start*/(0, _create.structuredPatch) /*istanbul ignore end*/(undefined, undefined, base, param)\n\t );\n\t }\n\n\t return param;\n\t}\n\n\tfunction fileNameChanged(patch) {\n\t return patch.newFileName && patch.newFileName !== patch.oldFileName;\n\t}\n\n\tfunction selectField(index, mine, theirs) {\n\t if (mine === theirs) {\n\t return mine;\n\t } else {\n\t index.conflict = true;\n\t return { mine: mine, theirs: theirs };\n\t }\n\t}\n\n\tfunction hunkBefore(test, check) {\n\t return test.oldStart < check.oldStart && test.oldStart + test.oldLines < check.oldStart;\n\t}\n\n\tfunction cloneHunk(hunk, offset) {\n\t return {\n\t oldStart: hunk.oldStart, oldLines: hunk.oldLines,\n\t newStart: hunk.newStart + offset, newLines: hunk.newLines,\n\t lines: hunk.lines\n\t };\n\t}\n\n\tfunction mergeLines(hunk, mineOffset, mineLines, theirOffset, theirLines) {\n\t // This will generally result in a conflicted hunk, but there are cases where the context\n\t // is the only overlap where we can successfully merge the content here.\n\t var mine = { offset: mineOffset, lines: mineLines, index: 0 },\n\t their = { offset: theirOffset, lines: theirLines, index: 0 };\n\n\t // Handle any leading content\n\t insertLeading(hunk, mine, their);\n\t insertLeading(hunk, their, mine);\n\n\t // Now in the overlap content. Scan through and select the best changes from each.\n\t while (mine.index < mine.lines.length && their.index < their.lines.length) {\n\t var mineCurrent = mine.lines[mine.index],\n\t theirCurrent = their.lines[their.index];\n\n\t if ((mineCurrent[0] === '-' || mineCurrent[0] === '+') && (theirCurrent[0] === '-' || theirCurrent[0] === '+')) {\n\t // Both modified ...\n\t mutualChange(hunk, mine, their);\n\t } else if (mineCurrent[0] === '+' && theirCurrent[0] === ' ') {\n\t /*istanbul ignore start*/var _hunk$lines;\n\n\t /*istanbul ignore end*/ // Mine inserted\n\t /*istanbul ignore start*/(_hunk$lines = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/collectChange(mine)));\n\t } else if (theirCurrent[0] === '+' && mineCurrent[0] === ' ') {\n\t /*istanbul ignore start*/var _hunk$lines2;\n\n\t /*istanbul ignore end*/ // Theirs inserted\n\t /*istanbul ignore start*/(_hunk$lines2 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines2 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/collectChange(their)));\n\t } else if (mineCurrent[0] === '-' && theirCurrent[0] === ' ') {\n\t // Mine removed or edited\n\t removal(hunk, mine, their);\n\t } else if (theirCurrent[0] === '-' && mineCurrent[0] === ' ') {\n\t // Their removed or edited\n\t removal(hunk, their, mine, true);\n\t } else if (mineCurrent === theirCurrent) {\n\t // Context identity\n\t hunk.lines.push(mineCurrent);\n\t mine.index++;\n\t their.index++;\n\t } else {\n\t // Context mismatch\n\t conflict(hunk, collectChange(mine), collectChange(their));\n\t }\n\t }\n\n\t // Now push anything that may be remaining\n\t insertTrailing(hunk, mine);\n\t insertTrailing(hunk, their);\n\n\t calcLineCount(hunk);\n\t}\n\n\tfunction mutualChange(hunk, mine, their) {\n\t var myChanges = collectChange(mine),\n\t theirChanges = collectChange(their);\n\n\t if (allRemoves(myChanges) && allRemoves(theirChanges)) {\n\t // Special case for remove changes that are supersets of one another\n\t if ( /*istanbul ignore start*/(0, _array.arrayStartsWith) /*istanbul ignore end*/(myChanges, theirChanges) && skipRemoveSuperset(their, myChanges, myChanges.length - theirChanges.length)) {\n\t /*istanbul ignore start*/var _hunk$lines3;\n\n\t /*istanbul ignore end*/ /*istanbul ignore start*/(_hunk$lines3 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines3 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/myChanges));\n\t return;\n\t } else if ( /*istanbul ignore start*/(0, _array.arrayStartsWith) /*istanbul ignore end*/(theirChanges, myChanges) && skipRemoveSuperset(mine, theirChanges, theirChanges.length - myChanges.length)) {\n\t /*istanbul ignore start*/var _hunk$lines4;\n\n\t /*istanbul ignore end*/ /*istanbul ignore start*/(_hunk$lines4 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines4 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/theirChanges));\n\t return;\n\t }\n\t } else if ( /*istanbul ignore start*/(0, _array.arrayEqual) /*istanbul ignore end*/(myChanges, theirChanges)) {\n\t /*istanbul ignore start*/var _hunk$lines5;\n\n\t /*istanbul ignore end*/ /*istanbul ignore start*/(_hunk$lines5 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines5 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/myChanges));\n\t return;\n\t }\n\n\t conflict(hunk, myChanges, theirChanges);\n\t}\n\n\tfunction removal(hunk, mine, their, swap) {\n\t var myChanges = collectChange(mine),\n\t theirChanges = collectContext(their, myChanges);\n\t if (theirChanges.merged) {\n\t /*istanbul ignore start*/var _hunk$lines6;\n\n\t /*istanbul ignore end*/ /*istanbul ignore start*/(_hunk$lines6 = /*istanbul ignore end*/hunk.lines).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_hunk$lines6 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/theirChanges.merged));\n\t } else {\n\t conflict(hunk, swap ? theirChanges : myChanges, swap ? myChanges : theirChanges);\n\t }\n\t}\n\n\tfunction conflict(hunk, mine, their) {\n\t hunk.conflict = true;\n\t hunk.lines.push({\n\t conflict: true,\n\t mine: mine,\n\t theirs: their\n\t });\n\t}\n\n\tfunction insertLeading(hunk, insert, their) {\n\t while (insert.offset < their.offset && insert.index < insert.lines.length) {\n\t var line = insert.lines[insert.index++];\n\t hunk.lines.push(line);\n\t insert.offset++;\n\t }\n\t}\n\tfunction insertTrailing(hunk, insert) {\n\t while (insert.index < insert.lines.length) {\n\t var line = insert.lines[insert.index++];\n\t hunk.lines.push(line);\n\t }\n\t}\n\n\tfunction collectChange(state) {\n\t var ret = [],\n\t operation = state.lines[state.index][0];\n\t while (state.index < state.lines.length) {\n\t var line = state.lines[state.index];\n\n\t // Group additions that are immediately after subtractions and treat them as one \"atomic\" modify change.\n\t if (operation === '-' && line[0] === '+') {\n\t operation = '+';\n\t }\n\n\t if (operation === line[0]) {\n\t ret.push(line);\n\t state.index++;\n\t } else {\n\t break;\n\t }\n\t }\n\n\t return ret;\n\t}\n\tfunction collectContext(state, matchChanges) {\n\t var changes = [],\n\t merged = [],\n\t matchIndex = 0,\n\t contextChanges = false,\n\t conflicted = false;\n\t while (matchIndex < matchChanges.length && state.index < state.lines.length) {\n\t var change = state.lines[state.index],\n\t match = matchChanges[matchIndex];\n\n\t // Once we've hit our add, then we are done\n\t if (match[0] === '+') {\n\t break;\n\t }\n\n\t contextChanges = contextChanges || change[0] !== ' ';\n\n\t merged.push(match);\n\t matchIndex++;\n\n\t // Consume any additions in the other block as a conflict to attempt\n\t // to pull in the remaining context after this\n\t if (change[0] === '+') {\n\t conflicted = true;\n\n\t while (change[0] === '+') {\n\t changes.push(change);\n\t change = state.lines[++state.index];\n\t }\n\t }\n\n\t if (match.substr(1) === change.substr(1)) {\n\t changes.push(change);\n\t state.index++;\n\t } else {\n\t conflicted = true;\n\t }\n\t }\n\n\t if ((matchChanges[matchIndex] || '')[0] === '+' && contextChanges) {\n\t conflicted = true;\n\t }\n\n\t if (conflicted) {\n\t return changes;\n\t }\n\n\t while (matchIndex < matchChanges.length) {\n\t merged.push(matchChanges[matchIndex++]);\n\t }\n\n\t return {\n\t merged: merged,\n\t changes: changes\n\t };\n\t}\n\n\tfunction allRemoves(changes) {\n\t return changes.reduce(function (prev, change) {\n\t return prev && change[0] === '-';\n\t }, true);\n\t}\n\tfunction skipRemoveSuperset(state, removeChanges, delta) {\n\t for (var i = 0; i < delta; i++) {\n\t var changeContent = removeChanges[removeChanges.length - delta + i].substr(1);\n\t if (state.lines[state.index + i] !== ' ' + changeContent) {\n\t return false;\n\t }\n\t }\n\n\t state.index += delta;\n\t return true;\n\t}\n\n\tfunction calcOldNewLineCount(lines) {\n\t var oldLines = 0;\n\t var newLines = 0;\n\n\t lines.forEach(function (line) {\n\t if (typeof line !== 'string') {\n\t var myCount = calcOldNewLineCount(line.mine);\n\t var theirCount = calcOldNewLineCount(line.theirs);\n\n\t if (oldLines !== undefined) {\n\t if (myCount.oldLines === theirCount.oldLines) {\n\t oldLines += myCount.oldLines;\n\t } else {\n\t oldLines = undefined;\n\t }\n\t }\n\n\t if (newLines !== undefined) {\n\t if (myCount.newLines === theirCount.newLines) {\n\t newLines += myCount.newLines;\n\t } else {\n\t newLines = undefined;\n\t }\n\t }\n\t } else {\n\t if (newLines !== undefined && (line[0] === '+' || line[0] === ' ')) {\n\t newLines++;\n\t }\n\t if (oldLines !== undefined && (line[0] === '-' || line[0] === ' ')) {\n\t oldLines++;\n\t }\n\t }\n\t });\n\n\t return { oldLines: oldLines, newLines: newLines };\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wYXRjaC9tZXJnZS5qcyJdLCJuYW1lcyI6WyJjYWxjTGluZUNvdW50IiwibWVyZ2UiLCJodW5rIiwiY2FsY09sZE5ld0xpbmVDb3VudCIsImxpbmVzIiwib2xkTGluZXMiLCJuZXdMaW5lcyIsInVuZGVmaW5lZCIsIm1pbmUiLCJ0aGVpcnMiLCJiYXNlIiwibG9hZFBhdGNoIiwicmV0IiwiaW5kZXgiLCJuZXdGaWxlTmFtZSIsImZpbGVOYW1lQ2hhbmdlZCIsIm9sZEZpbGVOYW1lIiwib2xkSGVhZGVyIiwibmV3SGVhZGVyIiwic2VsZWN0RmllbGQiLCJodW5rcyIsIm1pbmVJbmRleCIsInRoZWlyc0luZGV4IiwibWluZU9mZnNldCIsInRoZWlyc09mZnNldCIsImxlbmd0aCIsIm1pbmVDdXJyZW50Iiwib2xkU3RhcnQiLCJJbmZpbml0eSIsInRoZWlyc0N1cnJlbnQiLCJodW5rQmVmb3JlIiwicHVzaCIsImNsb25lSHVuayIsIm1lcmdlZEh1bmsiLCJNYXRoIiwibWluIiwibmV3U3RhcnQiLCJtZXJnZUxpbmVzIiwicGFyYW0iLCJ0ZXN0IiwiRXJyb3IiLCJwYXRjaCIsImNvbmZsaWN0IiwiY2hlY2siLCJvZmZzZXQiLCJtaW5lTGluZXMiLCJ0aGVpck9mZnNldCIsInRoZWlyTGluZXMiLCJ0aGVpciIsImluc2VydExlYWRpbmciLCJ0aGVpckN1cnJlbnQiLCJtdXR1YWxDaGFuZ2UiLCJjb2xsZWN0Q2hhbmdlIiwicmVtb3ZhbCIsImluc2VydFRyYWlsaW5nIiwibXlDaGFuZ2VzIiwidGhlaXJDaGFuZ2VzIiwiYWxsUmVtb3ZlcyIsInNraXBSZW1vdmVTdXBlcnNldCIsInN3YXAiLCJjb2xsZWN0Q29udGV4dCIsIm1lcmdlZCIsImluc2VydCIsImxpbmUiLCJzdGF0ZSIsIm9wZXJhdGlvbiIsIm1hdGNoQ2hhbmdlcyIsImNoYW5nZXMiLCJtYXRjaEluZGV4IiwiY29udGV4dENoYW5nZXMiLCJjb25mbGljdGVkIiwiY2hhbmdlIiwibWF0Y2giLCJzdWJzdHIiLCJyZWR1Y2UiLCJwcmV2IiwicmVtb3ZlQ2hhbmdlcyIsImRlbHRhIiwiaSIsImNoYW5nZUNvbnRlbnQiLCJmb3JFYWNoIiwibXlDb3VudCIsInRoZWlyQ291bnQiXSwibWFwcGluZ3MiOiI7OztnQ0FLZ0JBLGEsR0FBQUEsYTt5REFnQkFDLEssR0FBQUEsSzs7QUFyQmhCOztBQUNBOztBQUVBOzs7O3VCQUVPLFNBQVNELGFBQVQsQ0FBdUJFLElBQXZCLEVBQTZCO0FBQUEsNkVBQ0xDLG9CQUFvQkQsS0FBS0UsS0FBekIsQ0FESztBQUFBLE1BQzNCQyxRQUQyQix3QkFDM0JBLFFBRDJCO0FBQUEsTUFDakJDLFFBRGlCLHdCQUNqQkEsUUFEaUI7O0FBR2xDLE1BQUlELGFBQWFFLFNBQWpCLEVBQTRCO0FBQzFCTCxTQUFLRyxRQUFMLEdBQWdCQSxRQUFoQjtBQUNELEdBRkQsTUFFTztBQUNMLFdBQU9ILEtBQUtHLFFBQVo7QUFDRDs7QUFFRCxNQUFJQyxhQUFhQyxTQUFqQixFQUE0QjtBQUMxQkwsU0FBS0ksUUFBTCxHQUFnQkEsUUFBaEI7QUFDRCxHQUZELE1BRU87QUFDTCxXQUFPSixLQUFLSSxRQUFaO0FBQ0Q7QUFDRjs7QUFFTSxTQUFTTCxLQUFULENBQWVPLElBQWYsRUFBcUJDLE1BQXJCLEVBQTZCQyxJQUE3QixFQUFtQztBQUN4Q0YsU0FBT0csVUFBVUgsSUFBVixFQUFnQkUsSUFBaEIsQ0FBUDtBQUNBRCxXQUFTRSxVQUFVRixNQUFWLEVBQWtCQyxJQUFsQixDQUFUOztBQUVBLE1BQUlFLE1BQU0sRUFBVjs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxNQUFJSixLQUFLSyxLQUFMLElBQWNKLE9BQU9JLEtBQXpCLEVBQWdDO0FBQzlCRCxRQUFJQyxLQUFKLEdBQVlMLEtBQUtLLEtBQUwsSUFBY0osT0FBT0ksS0FBakM7QUFDRDs7QUFFRCxNQUFJTCxLQUFLTSxXQUFMLElBQW9CTCxPQUFPSyxXQUEvQixFQUE0QztBQUMxQyxRQUFJLENBQUNDLGdCQUFnQlAsSUFBaEIsQ0FBTCxF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Q0Q7QUFDRjs7QUFFRCxVQUFJRCxhQUFhQyxTQUFqQixFQUE0QjtBQUMxQixZQUFJMEUsUUFBUTNFLFFBQVIsS0FBcUI0RSxXQUFXNUUsUUFBcEMsRUFBOEM7QUFDNUNBLHNCQUFZMkUsUUFBUTNFLFFBQXBCO0FBQ0QsU0FGRCxNQUVPO0FBQ0xBLHFCQUFXQyxTQUFYO0FBQ0Q7QUFDRjtBQUNGLEtBbkJELE1BbUJPO0FBQ0wsVUFBSUQsYUFBYUMsU0FBYixLQUEyQndELEtBQUssQ0FBTCxNQUFZLEdBQVosSUFBbUJBLEtBQUssQ0FBTCxNQUFZLEdBQTFELENBQUosRUFBb0U7QUFDbEV6RDtBQUNEO0FBQ0QsVUFBSUQsYUFBYUUsU0FBYixLQUEyQndELEtBQUssQ0FBTCxNQUFZLEdBQVosSUFBbUJBLEtBQUssQ0FBTCxNQUFZLEdBQTFELENBQUosRUFBb0U7QUFDbEUxRDtBQUNEO0FBQ0Y7QUFDRixHQTVCRDs7QUE4QkEsU0FBTyxFQUFDQSxrQkFBRCxFQUFXQyxrQkFBWCxFQUFQO0FBQ0QiLCJmaWxlIjoibWVyZ2UuanMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQge3N0cnVjdHVyZWRQYXRjaH0gZnJvbSAnLi9jcmVhdGUnO1xuaW1wb3J0IHtwYXJzZVBhdGNofSBmcm9tICcuL3BhcnNlJztcblxuaW1wb3J0IHthcnJheUVxdWFsLCBhcnJheVN0YXJ0c1dpdGh9IGZyb20gJy4uL3V0aWwvYXJyYXknO1xuXG5leHBvcnQgZnVuY3Rpb24gY2FsY0xpbmVDb3VudChodW5rKSB7XG4gIGNvbnN0IHtvbGRMaW5lcywgbmV3TGluZXN9ID0gY2FsY09sZE5ld0xpbmVDb3VudChodW5rLmxpbmVzKTtcblxuICBpZiAob2xkTGluZXMgIT09IHVuZGVmaW5lZCkge1xuICAgIGh1bmsub2xkTGluZXMgPSBvbGRMaW5lcztcbiAgfSBlbHNlIHtcbiAgICBkZWxldGUgaHVuay5vbGRMaW5lcztcbiAgfVxuXG4gIGlmIChuZXdMaW5lcyAhPT0gdW5kZWZpbmVkKSB7XG4gICAgaHVuay5uZXdMaW5lcyA9IG5ld0xpbmVzO1xuICB9IGVsc2Uge1xuICAgIGRlbGV0ZSBodW5rLm5ld0xpbmVzO1xuICB9XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBtZXJnZShtaW5lLCB0aGVpcnMsIGJhc2UpIHtcbiAgbWluZSA9IGxvYWRQYXRjaChtaW5lLCBiYXNlKTtcbiAgdGhlaXJzID0gbG9hZFBhdGNoKHRoZWlycywgYmFzZSk7XG5cbiAgbGV0IHJldCA9IHt9O1xuXG4gIC8vIEZvciBpbmRleCB3ZSBqdXN0IGxldCBpdCBwYXNzIHRocm91Z2ggYXMgaXQgZG9lc24ndCBoYXZlIGFueSBuZWNlc3NhcnkgbWVhbmluZy5cbiAgLy8gTGVhdmluZyBzYW5pdHkgY2hlY2tzIG9uIHRoaXMgdG8gdGhlIEFQSSBjb25zdW1lciB0aGF0IG1heSBrbm93IG1vcmUgYWJvdXQgdGhlXG4gIC8vIG1lYW5pbmcgaW4gdGhlaXIgb3duIGNvbnRleHQuXG4gIGlmIChtaW5lLmluZGV4IHx8IHRoZWlycy5pbmRleCkge1xuICAgIHJldC5pbmRleCA9IG1pbmUuaW5kZXggfHwgdGhlaXJzLmluZGV4O1xuICB9XG5cbiAgaWYgKG1pbmUubmV3RmlsZU5hbWUgfHwgdGhlaXJzLm5ld0ZpbGVOYW1lKSB7XG4gICAgaWYgKCFmaWxlTmFtZUNoYW5nZWQobWluZSkpIHtcbiAgICAgIC8vIE5vIGhlYWRlciBvciBubyBjaGFuZ2UgaW4gb3VycywgdXNlIHRoZWlycyAoYW5kIG91cnMgaWYgdGhlaXJzIGRvZXMgbm90IGV4aXN0KVxuICAgICAgcmV0Lm9sZEZpbGVOYW1lID0gdGhlaXJzLm9sZEZpbGVOYW1lIHx8IG1pbmUub2xkRmlsZU5hbWU7XG4gICAgICByZXQubmV3RmlsZU5hbWUgPSB0aGVpcnMubmV3RmlsZU5hbWUgfHwgbWluZS5uZXdGaWxlTmFtZTtcbiAgICAgIHJldC5vbGRIZWFkZXIgPSB0aGVpcnMub2xkSGVhZGVyIHx8IG1pbmUub2xkSGVhZGVyO1xuICAgICAgcmV0Lm5ld0hlYWRlciA9IHRoZWlycy5uZXdIZWFkZXIgfHwgbWluZS5uZXdIZWFkZXI7XG4gICAgfSBlbHNlIGlmICghZmlsZU5hbWVDaGFuZ2VkKHRoZWlycykpIHtcbiAgICAgIC8vIE5vIGhlYWRlciBvciBubyBjaGFuZ2UgaW4gdGhlaXJzLCB1c2Ugb3Vyc1xuICAgICAgcmV0Lm9sZEZpbGVOYW1lID0gbWluZS5vbGRGaWxlTmFtZTtcbiAgICAgIHJldC5uZXdGaWxlTmFtZSA9IG1pbmUubmV3RmlsZU5hbWU7XG4gICAgICByZXQub2xkSGVhZGVyID0gbWluZS5vbGRIZWFkZXI7XG4gICAgICByZXQubmV3SGVhZGVyID0gbWluZS5uZXdIZWFkZXI7XG4gICAgfSBlbHNlIHtcbiAgICAgIC8vIEJvdGggY2hhbmdlZC4uLiBmaWd1cmUgaXQgb3V0XG4gICAgICByZXQub2xkRmlsZU5hbWUgPSBzZWxlY3RGaWVsZChyZXQsIG1pbmUub2xkRmlsZU5hbWUsIHRoZWlycy5vbGRGaWxlTmFtZSk7XG4gICAgICByZXQubmV3RmlsZU5hbWUgPSBzZWxlY3RGaWVsZChyZXQsIG1pbmUubmV3RmlsZU5hbWUsIHRoZWlycy5uZXdGaWxlTmFtZSk7XG4gICAgICByZXQub2xkSGVhZGVyID0gc2VsZWN0RmllbGQocmV0LCBtaW5lLm9sZEhlYWRlciwgdGhlaXJzLm9sZEhlYWRlcik7XG4gICAgICByZXQubmV3SGVhZGVyID0gc2VsZWN0RmllbGQocmV0LCBtaW5lLm5ld0hlYWRlciwgdGhlaXJzLm5ld0hlYWRlcik7XG4gICAgfVxuICB9XG5cbiAgcmV0Lmh1bmtzID0gW107XG5cbiAgbGV0IG1pbmVJbmRleCA9IDAsXG4gICAgICB0aGVpcnNJbmRleCA9IDAsXG4gICAgICBtaW5lT2Zmc2V0ID0gMCxcbiAgICAgIHRoZWlyc09mZnNldCA9IDA7XG5cbiAgd2hpbGUgKG1pbmVJbmRleCA8IG1pbmUuaHVua3MubGVuZ3RoIHx8IHRoZWlyc0luZGV4IDwgdGhlaXJzLmh1bmtzLmxlbmd0aCkge1xuICAgIGxldCBtaW5lQ3VycmVudCA9IG1pbmUuaHVua3NbbWluZUluZGV4XSB8fCB7b2xkU3RhcnQ6IEluZmluaXR5fSxcbiAgICAgICAgdGhlaXJzQ3VycmVudCA9IHRoZWlycy5odW5rc1t0aGVpcnNJbmRleF0gfHwge29sZFN0YXJ0OiBJbmZpbml0eX07XG5cbiAgICBpZiAoaHVua0JlZm9yZShtaW5lQ3VycmVudCwgdGhlaXJzQ3VycmVudCkpIHtcbiAgICAgIC8vIFRoaXMgcGF0Y2ggZG9lcyBub3Qgb3ZlcmxhcCB3aXRoIGFueSBvZiB0aGUgb3RoZXJzLCB5YXkuXG4gICAgICByZXQuaHVua3MucHVzaChjbG9uZUh1bmsobWluZUN1cnJlbnQsIG1pbmVPZmZzZXQpKTtcbiAgICAgIG1pbmVJbmRleCsrO1xuICAgICAgdGhlaXJzT2Zmc2V0ICs9IG1pbmVDdXJyZW50Lm5ld0xpbmVzIC0gbWluZUN1cnJlbnQub2xkTGluZXM7XG4gICAgfSBlbHNlIGlmIChodW5rQmVmb3JlKHRoZWlyc0N1cnJlbnQsIG1pbmVDdXJyZW50KSkge1xuICAgICAgLy8gVGhpcyBwYXRjaCBkb2VzIG5vdCBvdmVybGFwIHdpdGggYW55IG9mIHRoZSBvdGhlcnMsIHlheS5cbiAgICAgIHJldC5odW5rcy5wdXNoKGNsb25lSHVuayh0aGVpcnNDdXJyZW50LCB0aGVpcnNPZmZzZXQpKTtcbiAgICAgIHRoZWlyc0luZGV4Kys7XG4gICAgICBtaW5lT2Zmc2V0ICs9IHRoZWlyc0N1cnJlbnQubmV3TGluZXMgLSB0aGVpcnNDdXJyZW50Lm9sZExpbmVzO1xuICAgIH0gZWxzZSB7XG4gICAgICAvLyBPdmVybGFwLCBtZXJnZSBhcyBiZXN0IHdlIGNhblxuICAgICAgbGV0IG1lcmdlZEh1bmsgPSB7XG4gICAgICAgIG9sZFN0YXJ0OiBNYXRoLm1pbihtaW5lQ3VycmVudC5vbGRTdGFydCwgdGhlaXJzQ3VycmVudC5vbGRTdGFydCksXG4gICAgICAgIG9sZExpbmVzOiAwLFxuICAgICAgICBuZXdTdGFydDogTWF0aC5taW4obWluZUN1cnJlbnQubmV3U3RhcnQgKyBtaW5lT2Zmc2V0LCB0aGVpcnNDdXJyZW50Lm9sZFN0YXJ0ICsgdGhlaXJzT2Zmc2V0KSxcbiAgICAgICAgbmV3TGluZXM6IDAsXG4gICAgICAgIGxpbmVzOiBbXVxuICAgICAgfTtcbiAgICAgIG1lcmdlTGluZXMobWVyZ2VkSHVuaywgbWluZUN1cnJlbnQub2xkU3RhcnQsIG1pbmVDdXJyZW50LmxpbmVzLCB0aGVpcnNDdXJyZW50Lm9sZFN0YXJ0LCB0aGVpcnNDdXJyZW50LmxpbmVzKTtcbiAgICAgIHRoZWlyc0luZGV4Kys7XG4gICAgICBtaW5lSW5kZXgrKztcblxuICAgICAgcmV0Lmh1bmtzLnB1c2gobWVyZ2VkSHVuayk7XG4gICAgfVxuICB9XG5cbiAgcmV0dXJuIHJldDtcbn1cblxuZnVuY3Rpb24gbG9hZFBhdGNoKHBhcmFtLCBiYXNlKSB7XG4gIGlmICh0eXBlb2YgcGFyYW0gPT09ICdzdHJpbmcnKSB7XG4gICAgaWYgKC9eQEAvbS50ZXN0KHBhcmFtKSB8fCAoL15JbmRleDovbS50ZXN0KHBhcmFtKSkpIHtcbiAgICAgIHJldHVybiBwYXJzZVBhdGNoKHBhcmFtKVswXTtcbiAgICB9XG5cbiAgICBpZiAoIWJhc2UpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignTXVzdCBwcm92aWRlIGEgYmFzZSByZWZlcmVuY2Ugb3IgcGFzcyBpbiBhIHBhdGNoJyk7XG4gICAgfVxuICAgIHJldHVybiBzdHJ1Y3R1cmVkUGF0Y2godW5kZWZpbmVkLCB1bmRlZmluZWQsIGJhc2UsIHBhcmFtKTtcbiAgfVxuXG4gIHJldHVybiBwYXJhbTtcbn1cblxuZnVuY3Rpb24gZmlsZU5hbWVDaGFuZ2VkKHBhdGNoKSB7XG4gIHJldHVybiBwYXRjaC5uZXdGaWxlTmFtZSAmJiBwYXRjaC5uZXdGaWxlTmFtZSAhPT0gcGF0Y2gub2xkRmlsZU5hbWU7XG59XG5cbmZ1bmN0aW9uIHNlbGVjdEZpZWxkKGluZGV4LCBtaW5lLCB0aGVpcnMpIHtcbiAgaWYgKG1pbmUgPT09IHRoZWlycykge1xuICAgIHJldHVybiBtaW5lO1xuICB9IGVsc2Uge1xuICAgIGluZGV4LmNvbmZsaWN0ID0gdHJ1ZTtcbiAgICByZXR1cm4ge21pbmUsIHRoZWlyc307XG4gIH1cbn1cblxuZnVuY3Rpb24gaHVua0JlZm9yZSh0ZXN0LCBjaGVjaykge1xuICByZXR1cm4gdGVzdC5vbGRTdGFydCA8IGNoZWNrLm9sZFN0YXJ0XG4gICAgJiYgKHRlc3Qub2xkU3RhcnQgKyB0ZXN0Lm9sZExpbmVzKSA8IGNoZWNrLm9sZFN0YXJ0O1xufVxuXG5mdW5jdGlvbiBjbG9uZUh1bmsoaHVuaywgb2Zmc2V0KSB7XG4gIHJldHVybiB7XG4gICAgb2xkU3RhcnQ6IGh1bmsub2xkU3RhcnQsIG9sZExpbmVzOiBodW5rLm9sZExpbmVzLFxuICAgIG5ld1N0YXJ0OiBodW5rLm5ld1N0YXJ0ICsgb2Zmc2V0LCBuZXdMaW5lczogaHVuay5uZXdMaW5lcyxcbiAgICBsaW5lczogaHVuay5saW5lc1xuICB9O1xufVxuXG5mdW5jdGlvbiBtZXJnZUxpbmVzKGh1bmssIG1pbmVPZmZzZXQsIG1pbmVMaW5lcywgdGhlaXJPZmZzZXQsIHRoZWlyTGluZXMpIHtcbiAgLy8gVGhpcyB3aWxsIGdlbmVyYWxseSByZXN1bHQgaW4gYSBjb25mbGljdGVkIGh1bmssIGJ1dCB0aGVyZSBhcmUgY2FzZXMgd2hlcmUgdGhlIGNvbnRleHRcbiAgLy8gaXMgdGhlIG9ubHkgb3ZlcmxhcCB3aGVyZSB3ZSBjYW4gc3VjY2Vzc2Z1bGx5IG1lcmdlIHRoZSBjb250ZW50IGhlcmUuXG4gIGxldCBtaW5lID0ge29mZnNldDogbWluZU9mZnNldCwgbGluZXM6IG1pbmVMaW5lcywgaW5kZXg6IDB9LFxuICAgICAgdGhlaXIgPSB7b2Zmc2V0OiB0aGVpck9mZnNldCwgbGluZXM6IHRoZWlyTGluZXMsIGluZGV4OiAwfTtcblxuICAvLyBIYW5kbGUgYW55IGxlYWRpbmcgY29udGVudFxuICBpbnNlcnRMZWFkaW5nKGh1bmssIG1pbmUsIHRoZWlyKTtcbiAgaW5zZXJ0TGVhZGluZyhodW5rLCB0aGVpciwgbWluZSk7XG5cbiAgLy8gTm93IGluIHRoZSBvdmVybGFwIGNvbnRlbnQuIFNjYW4gdGhyb3VnaCBhbmQgc2VsZWN0IHRoZSBiZXN0IGNoYW5nZXMgZnJvbSBlYWNoLlxuICB3aGlsZSAobWluZS5pbmRleCA8IG1pbmUubGluZXMubGVuZ3RoICYmIHRoZWlyLmluZGV4IDwgdGhlaXIubGluZXMubGVuZ3RoKSB7XG4gICAgbGV0IG1pbmVDdXJyZW50ID0gbWluZS5saW5lc1ttaW5lLmluZGV4XSxcbiAgICAgICAgdGhlaXJDdXJyZW50ID0gdGhlaXIubGluZXNbdGhlaXIuaW5kZXhdO1xuXG4gICAgaWYgKChtaW5lQ3VycmVudFswXSA9PT0gJy0nIHx8IG1pbmVDdXJyZW50WzBdID09PSAnKycpXG4gICAgICAgICYmICh0aGVpckN1cnJlbnRbMF0gPT09ICctJyB8fCB0aGVpckN1cnJlbnRbMF0gPT09ICcrJykpIHtcbiAgICAgIC8vIEJvdGggbW9kaWZpZWQgLi4uXG4gICAgICBtdXR1YWxDaGFuZ2UoaHVuaywgbWluZSwgdGhlaXIpO1xuICAgIH0gZWxzZSBpZiAobWluZUN1cnJlbnRbMF0gPT09ICcrJyAmJiB0aGVpckN1cnJlbnRbMF0gPT09ICcgJykge1xuICAgICAgLy8gTWluZSBpbnNlcnRlZFxuICAgICAgaHVuay5saW5lcy5wdXNoKC4uLiBjb2xsZWN0Q2hhbmdlKG1pbmUpKTtcbiAgICB9IGVsc2UgaWYgKHRoZWlyQ3VycmVudFswXSA9PT0gJysnICYmIG1pbmVDdXJyZW50WzBdID09PSAnICcpIHtcbiAgICAgIC8vIFRoZWlycyBpbnNlcnRlZFxuICAgICAgaHVuay5saW5lcy5wdXNoKC4uLiBjb2xsZWN0Q2hhbmdlKHRoZWlyKSk7XG4gICAgfSBlbHNlIGlmIChtaW5lQ3VycmVudFswXSA9PT0gJy0nICYmIHRoZWlyQ3VycmVudFswXSA9PT0gJyAnKSB7XG4gICAgICAvLyBNaW5lIHJlbW92ZWQgb3IgZWRpdGVkXG4gICAgICByZW1vdmFsKGh1bmssIG1pbmUsIHRoZWlyKTtcbiAgICB9IGVsc2UgaWYgKHRoZWlyQ3VycmVudFswXSA9PT0gJy0nICYmIG1pbmVDdXJyZW50WzBdID09PSAnICcpIHtcbiAgICAgIC8vIFRoZWlyIHJlbW92ZWQgb3IgZWRpdGVkXG4gICAgICByZW1vdmFsKGh1bmssIHRoZWlyLCBtaW5lLCB0cnVlKTtcbiAgICB9IGVsc2UgaWYgKG1pbmVDdXJyZW50ID09PSB0aGVpckN1cnJlbnQpIHtcbiAgICAgIC8vIENvbnRleHQgaWRlbnRpdHlcbiAgICAgIGh1bmsubGluZXMucHVzaChtaW5lQ3VycmVudCk7XG4gICAgICBtaW5lLmluZGV4Kys7XG4gICAgICB0aGVpci5pbmRleCsrO1xuICAgIH0gZWxzZSB7XG4gICAgICAvLyBDb250ZXh0IG1pc21hdGNoXG4gICAgICBjb25mbGljdChodW5rLCBjb2xsZWN0Q2hhbmdlKG1pbmUpLCBjb2xsZWN0Q2hhbmdlKHRoZWlyKSk7XG4gICAgfVxuICB9XG5cbiAgLy8gTm93IHB1c2ggYW55dGhpbmcgdGhhdCBtYXkgYmUgcmVtYWluaW5nXG4gIGluc2VydFRyYWlsaW5nKGh1bmssIG1pbmUpO1xuICBpbnNlcnRUcmFpbGluZyhodW5rLCB0aGVpcik7XG5cbiAgY2FsY0xpbmVDb3VudChodW5rKTtcbn1cblxuZnVuY3Rpb24gbXV0dWFsQ2hhbmdlKGh1bmssIG1pbmUsIHRoZWlyKSB7XG4gIGxldCBteUNoYW5nZXMgPSBjb2xsZWN0Q2hhbmdlKG1pbmUpLFxuICAgICAgdGhlaXJDaGFuZ2VzID0gY29sbGVjdENoYW5nZSh0aGVpcik7XG5cbiAgaWYgKGFsbFJlbW92ZXMobXlDaGFuZ2VzKSAmJiBhbGxSZW1vdmVzKHRoZWlyQ2hhbmdlcykpIHtcbiAgICAvLyBTcGVjaWFsIGNhc2UgZm9yIHJlbW92ZSBjaGFuZ2VzIHRoYXQgYXJlIHN1cGVyc2V0cyBvZiBvbmUgYW5vdGhlclxuICAgIGlmIChhcnJheVN0YXJ0c1dpdGgobXlDaGFuZ2VzLCB0aGVpckNoYW5nZXMpXG4gICAgICAgICYmIHNraXBSZW1vdmVTdXBlcnNldCh0aGVpciwgbXlDaGFuZ2VzLCBteUNoYW5nZXMubGVuZ3RoIC0gdGhlaXJDaGFuZ2VzLmxlbmd0aCkpIHtcbiAgICAgIGh1bmsubGluZXMucHVzaCguLi4gbXlDaGFuZ2VzKTtcbiAgICAgIHJldHVybjtcbiAgICB9IGVsc2UgaWYgKGFycmF5U3RhcnRzV2l0aCh0aGVpckNoYW5nZXMsIG15Q2hhbmdlcylcbiAgICAgICAgJiYgc2tpcFJlbW92ZVN1cGVyc2V0KG1pbmUsIHRoZWlyQ2hhbmdlcywgdGhlaXJDaGFuZ2VzLmxlbmd0aCAtIG15Q2hhbmdlcy5sZW5ndGgpKSB7XG4gICAgICBodW5rLmxpbmVzLnB1c2goLi4uIHRoZWlyQ2hhbmdlcyk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICB9IGVsc2UgaWYgKGFycmF5RXF1YWwobXlDaGFuZ2VzLCB0aGVpckNoYW5nZXMpKSB7XG4gICAgaHVuay5saW5lcy5wdXNoKC4uLiBteUNoYW5nZXMpO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbmZsaWN0KGh1bmssIG15Q2hhbmdlcywgdGhlaXJDaGFuZ2VzKTtcbn1cblxuZnVuY3Rpb24gcmVtb3ZhbChodW5rLCBtaW5lLCB0aGVpciwgc3dhcCkge1xuICBsZXQgbXlDaGFuZ2VzID0gY29sbGVjdENoYW5nZShtaW5lKSxcbiAgICAgIHRoZWlyQ2hhbmdlcyA9IGNvbGxlY3RDb250ZXh0KHRoZWlyLCBteUNoYW5nZXMpO1xuICBpZiAodGhlaXJDaGFuZ2VzLm1lcmdlZCkge1xuICAgIGh1bmsubGluZXMucHVzaCguLi4gdGhlaXJDaGFuZ2VzLm1lcmdlZCk7XG4gIH0gZWxzZSB7XG4gICAgY29uZmxpY3QoaHVuaywgc3dhcCA/IHRoZWlyQ2hhbmdlcyA6IG15Q2hhbmdlcywgc3dhcCA/IG15Q2hhbmdlcyA6IHRoZWlyQ2hhbmdlcyk7XG4gIH1cbn1cblxuZnVuY3Rpb24gY29uZmxpY3QoaHVuaywgbWluZSwgdGhlaXIpIHtcbiAgaHVuay5jb25mbGljdCA9IHRydWU7XG4gIGh1bmsubGluZXMucHVzaCh7XG4gICAgY29uZmxpY3Q6IHRydWUsXG4gICAgbWluZTogbWluZSxcbiAgICB0aGVpcnM6IHRoZWlyXG4gIH0pO1xufVxuXG5mdW5jdGlvbiBpbnNlcnRMZWFkaW5nKGh1bmssIGluc2VydCwgdGhlaXIpIHtcbiAgd2hpbGUgKGluc2VydC5vZmZzZXQgPCB0aGVpci5vZmZzZXQgJiYgaW5zZXJ0LmluZGV4IDwgaW5zZXJ0LmxpbmVzLmxlbmd0aCkge1xuICAgIGxldCBsaW5lID0gaW5zZXJ0LmxpbmVzW2luc2VydC5pbmRleCsrXTtcbiAgICBodW5rLmxpbmVzLnB1c2gobGluZSk7XG4gICAgaW5zZXJ0Lm9mZnNldCsrO1xuICB9XG59XG5mdW5jdGlvbiBpbnNlcnRUcmFpbGluZyhodW5rLCBpbnNlcnQpIHtcbiAgd2hpbGUgKGluc2VydC5pbmRleCA8IGluc2VydC5saW5lcy5sZW5ndGgpIHtcbiAgICBsZXQgbGluZSA9IGluc2VydC5saW5lc1tpbnNlcnQuaW5kZXgrK107XG4gICAgaHVuay5saW5lcy5wdXNoKGxpbmUpO1xuICB9XG59XG5cbmZ1bmN0aW9uIGNvbGxlY3RDaGFuZ2Uoc3RhdGUpIHtcbiAgbGV0IHJldCA9IFtdLFxuICAgICAgb3BlcmF0aW9uID0gc3RhdGUubGluZXNbc3RhdGUuaW5kZXhdWzBdO1xuICB3aGlsZSAoc3RhdGUuaW5kZXggPCBzdGF0ZS5saW5lcy5sZW5ndGgpIHtcbiAgICBsZXQgbGluZSA9IHN0YXRlLmxpbmVzW3N0YXRlLmluZGV4XTtcblxuICAgIC8vIEdyb3VwIGFkZGl0aW9ucyB0aGF0IGFyZSBpbW1lZGlhdGVseSBhZnRlciBzdWJ0cmFjdGlvbnMgYW5kIHRyZWF0IHRoZW0gYXMgb25lIFwiYXRvbWljXCIgbW9kaWZ5IGNoYW5nZS5cbiAgICBpZiAob3BlcmF0aW9uID09PSAnLScgJiYgbGluZVswXSA9PT0gJysnKSB7XG4gICAgICBvcGVyYXRpb24gPSAnKyc7XG4gICAgfVxuXG4gICAgaWYgKG9wZXJhdGlvbiA9PT0gbGluZVswXSkge1xuICAgICAgcmV0LnB1c2gobGluZSk7XG4gICAgICBzdGF0ZS5pbmRleCsrO1xuICAgIH0gZWxzZSB7XG4gICAgICBicmVhaztcbiAgICB9XG4gIH1cblxuICByZXR1cm4gcmV0O1xufVxuZnVuY3Rpb24gY29sbGVjdENvbnRleHQoc3RhdGUsIG1hdGNoQ2hhbmdlcykge1xuICBsZXQgY2hhbmdlcyA9IFtdLFxuICAgICAgbWVyZ2VkID0gW10sXG4gICAgICBtYXRjaEluZGV4ID0gMCxcbiAgICAgIGNvbnRleHRDaGFuZ2VzID0gZmFsc2UsXG4gICAgICBjb25mbGljdGVkID0gZmFsc2U7XG4gIHdoaWxlIChtYXRjaEluZGV4IDwgbWF0Y2hDaGFuZ2VzLmxlbmd0aFxuICAgICAgICAmJiBzdGF0ZS5pbmRleCA8IHN0YXRlLmxpbmVzLmxlbmd0aCkge1xuICAgIGxldCBjaGFuZ2UgPSBzdGF0ZS5saW5lc1tzdGF0ZS5pbmRleF0sXG4gICAgICAgIG1hdGNoID0gbWF0Y2hDaGFuZ2VzW21hdGNoSW5kZXhdO1xuXG4gICAgLy8gT25jZSB3ZSd2ZSBoaXQgb3VyIGFkZCwgdGhlbiB3ZSBhcmUgZG9uZVxuICAgIGlmIChtYXRjaFswXSA9PT0gJysnKSB7XG4gICAgICBicmVhaztcbiAgICB9XG5cbiAgICBjb250ZXh0Q2hhbmdlcyA9IGNvbnRleHRDaGFuZ2VzIHx8IGNoYW5nZVswXSAhPT0gJyAnO1xuXG4gICAgbWVyZ2VkLnB1c2gobWF0Y2gpO1xuICAgIG1hdGNoSW5kZXgrKztcblxuICAgIC8vIENvbnN1bWUgYW55IGFkZGl0aW9ucyBpbiB0aGUgb3RoZXIgYmxvY2sgYXMgYSBjb25mbGljdCB0byBhdHRlbXB0XG4gICAgLy8gdG8gcHVsbCBpbiB0aGUgcmVtYWluaW5nIGNvbnRleHQgYWZ0ZXIgdGhpc1xuICAgIGlmIChjaGFuZ2VbMF0gPT09ICcrJykge1xuICAgICAgY29uZmxpY3RlZCA9IHRydWU7XG5cbiAgICAgIHdoaWxlIChjaGFuZ2VbMF0gPT09ICcrJykge1xuICAgICAgICBjaGFuZ2VzLnB1c2goY2hhbmdlKTtcbiAgICAgICAgY2hhbmdlID0gc3RhdGUubGluZXNbKytzdGF0ZS5pbmRleF07XG4gICAgICB9XG4gICAgfVxuXG4gICAgaWYgKG1hdGNoLnN1YnN0cigxKSA9PT0gY2hhbmdlLnN1YnN0cigxKSkge1xuICAgICAgY2hhbmdlcy5wdXNoKGNoYW5nZSk7XG4gICAgICBzdGF0ZS5pbmRleCsrO1xuICAgIH0gZWxzZSB7XG4gICAgICBjb25mbGljdGVkID0gdHJ1ZTtcbiAgICB9XG4gIH1cblxuICBpZiAoKG1hdGNoQ2hhbmdlc1ttYXRjaEluZGV4XSB8fCAnJylbMF0gPT09ICcrJ1xuICAgICAgJiYgY29udGV4dENoYW5nZXMpIHtcbiAgICBjb25mbGljdGVkID0gdHJ1ZTtcbiAgfVxuXG4gIGlmIChjb25mbGljdGVkKSB7XG4gICAgcmV0dXJuIGNoYW5nZXM7XG4gIH1cblxuICB3aGlsZSAobWF0Y2hJbmRleCA8IG1hdGNoQ2hhbmdlcy5sZW5ndGgpIHtcbiAgICBtZXJnZWQucHVzaChtYXRjaENoYW5nZXNbbWF0Y2hJbmRleCsrXSk7XG4gIH1cblxuICByZXR1cm4ge1xuICAgIG1lcmdlZCxcbiAgICBjaGFuZ2VzXG4gIH07XG59XG5cbmZ1bmN0aW9uIGFsbFJlbW92ZXMoY2hhbmdlcykge1xuICByZXR1cm4gY2hhbmdlcy5yZWR1Y2UoZnVuY3Rpb24ocHJldiwgY2hhbmdlKSB7XG4gICAgcmV0dXJuIHByZXYgJiYgY2hhbmdlWzBdID09PSAnLSc7XG4gIH0sIHRydWUpO1xufVxuZnVuY3Rpb24gc2tpcFJlbW92ZVN1cGVyc2V0KHN0YXRlLCByZW1vdmVDaGFuZ2VzLCBkZWx0YSkge1xuICBmb3IgKGxldCBpID0gMDsgaSA8IGRlbHRhOyBpKyspIHtcbiAgICBsZXQgY2hhbmdlQ29udGVudCA9IHJlbW92ZUNoYW5nZXNbcmVtb3ZlQ2hhbmdlcy5sZW5ndGggLSBkZWx0YSArIGldLnN1YnN0cigxKTtcbiAgICBpZiAoc3RhdGUubGluZXNbc3RhdGUuaW5kZXggKyBpXSAhPT0gJyAnICsgY2hhbmdlQ29udGVudCkge1xuICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgfVxuXG4gIHN0YXRlLmluZGV4ICs9IGRlbHRhO1xuICByZXR1cm4gdHJ1ZTtcbn1cblxuZnVuY3Rpb24gY2FsY09sZE5ld0xpbmVDb3VudChsaW5lcykge1xuICBsZXQgb2xkTGluZXMgPSAwO1xuICBsZXQgbmV3TGluZXMgPSAwO1xuXG4gIGxpbmVzLmZvckVhY2goZnVuY3Rpb24obGluZSkge1xuICAgIGlmICh0eXBlb2YgbGluZSAhPT0gJ3N0cmluZycpIHtcbiAgICAgIGxldCBteUNvdW50ID0gY2FsY09sZE5ld0xpbmVDb3VudChsaW5lLm1pbmUpO1xuICAgICAgbGV0IHRoZWlyQ291bnQgPSBjYWxjT2xkTmV3TGluZUNvdW50KGxpbmUudGhlaXJzKTtcblxuICAgICAgaWYgKG9sZExpbmVzICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKG15Q291bnQub2xkTGluZXMgPT09IHRoZWlyQ291bnQub2xkTGluZXMpIHtcbiAgICAgICAgICBvbGRMaW5lcyArPSBteUNvdW50Lm9sZExpbmVzO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIG9sZExpbmVzID0gdW5kZWZpbmVkO1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIGlmIChuZXdMaW5lcyAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmIChteUNvdW50Lm5ld0xpbmVzID09PSB0aGVpckNvdW50Lm5ld0xpbmVzKSB7XG4gICAgICAgICAgbmV3TGluZXMgKz0gbXlDb3VudC5uZXdMaW5lcztcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBuZXdMaW5lcyA9IHVuZGVmaW5lZDtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICBpZiAobmV3TGluZXMgIT09IHVuZGVmaW5lZCAmJiAobGluZVswXSA9PT0gJysnIHx8IGxpbmVbMF0gPT09ICcgJykpIHtcbiAgICAgICAgbmV3TGluZXMrKztcbiAgICAgIH1cbiAgICAgIGlmIChvbGRMaW5lcyAhPT0gdW5kZWZpbmVkICYmIChsaW5lWzBdID09PSAnLScgfHwgbGluZVswXSA9PT0gJyAnKSkge1xuICAgICAgICBvbGRMaW5lcysrO1xuICAgICAgfVxuICAgIH1cbiAgfSk7XG5cbiAgcmV0dXJuIHtvbGRMaW5lcywgbmV3TGluZXN9O1xufVxuIl19\n\n\n/***/ }),\n/* 14 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports. /*istanbul ignore end*/structuredPatch = structuredPatch;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/createTwoFilesPatch = createTwoFilesPatch;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/createPatch = createPatch;\n\n\tvar /*istanbul ignore start*/_line = __webpack_require__(5) /*istanbul ignore end*/;\n\n\t/*istanbul ignore start*/function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }\n\n\t/*istanbul ignore end*/function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {\n\t if (!options) {\n\t options = {};\n\t }\n\t if (typeof options.context === 'undefined') {\n\t options.context = 4;\n\t }\n\n\t var diff = /*istanbul ignore start*/(0, _line.diffLines) /*istanbul ignore end*/(oldStr, newStr, options);\n\t diff.push({ value: '', lines: [] }); // Append an empty value to make cleanup easier\n\n\t function contextLines(lines) {\n\t return lines.map(function (entry) {\n\t return ' ' + entry;\n\t });\n\t }\n\n\t var hunks = [];\n\t var oldRangeStart = 0,\n\t newRangeStart = 0,\n\t curRange = [],\n\t oldLine = 1,\n\t newLine = 1;\n\n\t /*istanbul ignore start*/var _loop = function _loop( /*istanbul ignore end*/i) {\n\t var current = diff[i],\n\t lines = current.lines || current.value.replace(/\\n$/, '').split('\\n');\n\t current.lines = lines;\n\n\t if (current.added || current.removed) {\n\t /*istanbul ignore start*/var _curRange;\n\n\t /*istanbul ignore end*/ // If we have previous context, start with that\n\t if (!oldRangeStart) {\n\t var prev = diff[i - 1];\n\t oldRangeStart = oldLine;\n\t newRangeStart = newLine;\n\n\t if (prev) {\n\t curRange = options.context > 0 ? contextLines(prev.lines.slice(-options.context)) : [];\n\t oldRangeStart -= curRange.length;\n\t newRangeStart -= curRange.length;\n\t }\n\t }\n\n\t // Output our changes\n\t /*istanbul ignore start*/(_curRange = /*istanbul ignore end*/curRange).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_curRange /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/lines.map(function (entry) {\n\t return (current.added ? '+' : '-') + entry;\n\t })));\n\n\t // Track the updated file position\n\t if (current.added) {\n\t newLine += lines.length;\n\t } else {\n\t oldLine += lines.length;\n\t }\n\t } else {\n\t // Identical context lines. Track line changes\n\t if (oldRangeStart) {\n\t // Close out any changes that have been output (or join overlapping)\n\t if (lines.length <= options.context * 2 && i < diff.length - 2) {\n\t /*istanbul ignore start*/var _curRange2;\n\n\t /*istanbul ignore end*/ // Overlapping\n\t /*istanbul ignore start*/(_curRange2 = /*istanbul ignore end*/curRange).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_curRange2 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/contextLines(lines)));\n\t } else {\n\t /*istanbul ignore start*/var _curRange3;\n\n\t /*istanbul ignore end*/ // end the range and output\n\t var contextSize = Math.min(lines.length, options.context);\n\t /*istanbul ignore start*/(_curRange3 = /*istanbul ignore end*/curRange).push. /*istanbul ignore start*/apply /*istanbul ignore end*/( /*istanbul ignore start*/_curRange3 /*istanbul ignore end*/, /*istanbul ignore start*/_toConsumableArray( /*istanbul ignore end*/contextLines(lines.slice(0, contextSize))));\n\n\t var hunk = {\n\t oldStart: oldRangeStart,\n\t oldLines: oldLine - oldRangeStart + contextSize,\n\t newStart: newRangeStart,\n\t newLines: newLine - newRangeStart + contextSize,\n\t lines: curRange\n\t };\n\t if (i >= diff.length - 2 && lines.length <= options.context) {\n\t // EOF is inside this hunk\n\t var oldEOFNewline = /\\n$/.test(oldStr);\n\t var newEOFNewline = /\\n$/.test(newStr);\n\t if (lines.length == 0 && !oldEOFNewline) {\n\t // special case: old has no eol and no trailing context; no-nl can end up before adds\n\t curRange.splice(hunk.oldLines, 0, '\\\\ No newline at end of file');\n\t } else if (!oldEOFNewline || !newEOFNewline) {\n\t curRange.push('\\\\ No newline at end of file');\n\t }\n\t }\n\t hunks.push(hunk);\n\n\t oldRangeStart = 0;\n\t newRangeStart = 0;\n\t curRange = [];\n\t }\n\t }\n\t oldLine += lines.length;\n\t newLine += lines.length;\n\t }\n\t };\n\n\t for (var i = 0; i < diff.length; i++) {\n\t /*istanbul ignore start*/_loop( /*istanbul ignore end*/i);\n\t }\n\n\t return {\n\t oldFileName: oldFileName, newFileName: newFileName,\n\t oldHeader: oldHeader, newHeader: newHeader,\n\t hunks: hunks\n\t };\n\t}\n\n\tfunction createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {\n\t var diff = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options);\n\n\t var ret = [];\n\t if (oldFileName == newFileName) {\n\t ret.push('Index: ' + oldFileName);\n\t }\n\t ret.push('===================================================================');\n\t ret.push('--- ' + diff.oldFileName + (typeof diff.oldHeader === 'undefined' ? '' : '\\t' + diff.oldHeader));\n\t ret.push('+++ ' + diff.newFileName + (typeof diff.newHeader === 'undefined' ? '' : '\\t' + diff.newHeader));\n\n\t for (var i = 0; i < diff.hunks.length; i++) {\n\t var hunk = diff.hunks[i];\n\t ret.push('@@ -' + hunk.oldStart + ',' + hunk.oldLines + ' +' + hunk.newStart + ',' + hunk.newLines + ' @@');\n\t ret.push.apply(ret, hunk.lines);\n\t }\n\n\t return ret.join('\\n') + '\\n';\n\t}\n\n\tfunction createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) {\n\t return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options);\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wYXRjaC9jcmVhdGUuanMiXSwibmFtZXMiOlsic3RydWN0dXJlZFBhdGNoIiwiY3JlYXRlVHdvRmlsZXNQYXRjaCIsImNyZWF0ZVBhdGNoIiwib2xkRmlsZU5hbWUiLCJuZXdGaWxlTmFtZSIsIm9sZFN0ciIsIm5ld1N0ciIsIm9sZEhlYWRlciIsIm5ld0hlYWRlciIsIm9wdGlvbnMiLCJjb250ZXh0IiwiZGlmZiIsInB1c2giLCJ2YWx1ZSIsImxpbmVzIiwiY29udGV4dExpbmVzIiwibWFwIiwiZW50cnkiLCJodW5rcyIsIm9sZFJhbmdlU3RhcnQiLCJuZXdSYW5nZVN0YXJ0IiwiY3VyUmFuZ2UiLCJvbGRMaW5lIiwibmV3TGluZSIsImkiLCJjdXJyZW50IiwicmVwbGFjZSIsInNwbGl0IiwiYWRkZWQiLCJyZW1vdmVkIiwicHJldiIsInNsaWNlIiwibGVuZ3RoIiwiY29udGV4dFNpemUiLCJNYXRoIiwibWluIiwiaHVuayIsIm9sZFN0YXJ0Iiwib2xkTGluZXMiLCJuZXdTdGFydCIsIm5ld0xpbmVzIiwib2xkRU9GTmV3bGluZSIsInRlc3QiLCJuZXdFT0ZOZXdsaW5lIiwic3BsaWNlIiwicmV0IiwiYXBwbHkiLCJqb2luIiwiZmlsZU5hbWUiXSwibWFwcGluZ3MiOiI7OztnQ0FFZ0JBLGUsR0FBQUEsZTt5REFpR0FDLG1CLEdBQUFBLG1CO3lEQXdCQUMsVyxHQUFBQSxXOztBQTNIaEI7Ozs7dUJBRU8sU0FBU0YsZUFBVCxDQUF5QkcsV0FBekIsRUFBc0NDLFdBQXRDLEVBQW1EQyxNQUFuRCxFQUEyREMsTUFBM0QsRUFBbUVDLFNBQW5FLEVBQThFQyxTQUE5RSxFQUF5RkMsT0FBekYsRUFBa0c7QUFDdkcsTUFBSSxDQUFDQSxPQUFMLEVBQWM7QUFDWkEsY0FBVSxFQUFWO0FBQ0Q7QUFDRCxNQUFJLE9BQU9BLFFBQVFDLE9BQWYsS0FBMkIsV0FBL0IsRUFBNEM7QUFDMUNELFlBQVFDLE9BQVIsR0FBa0IsQ0FBbEI7QUFDRDs7QUFFRCxN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QUEvQyxFQUEwREMsU0FBMUQsRUFBcUVDLE9BQXJFLEVBQThFO0FBQ25GLFNBQU9SLG9CQUFvQitDLFFBQXBCLEVBQThCQSxRQUE5QixFQUF3QzNDLE1BQXhDLEVBQWdEQyxNQUFoRCxFQUF3REMsU0FBeEQsRUFBbUVDLFNBQW5FLEVBQThFQyxPQUE5RSxDQUFQO0FBQ0QiLCJmaWxlIjoiY3JlYXRlLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHtkaWZmTGluZXN9IGZyb20gJy4uL2RpZmYvbGluZSc7XG5cbmV4cG9ydCBmdW5jdGlvbiBzdHJ1Y3R1cmVkUGF0Y2gob2xkRmlsZU5hbWUsIG5ld0ZpbGVOYW1lLCBvbGRTdHIsIG5ld1N0ciwgb2xkSGVhZGVyLCBuZXdIZWFkZXIsIG9wdGlvbnMpIHtcbiAgaWYgKCFvcHRpb25zKSB7XG4gICAgb3B0aW9ucyA9IHt9O1xuICB9XG4gIGlmICh0eXBlb2Ygb3B0aW9ucy5jb250ZXh0ID09PSAndW5kZWZpbmVkJykge1xuICAgIG9wdGlvbnMuY29udGV4dCA9IDQ7XG4gIH1cblxuICBjb25zdCBkaWZmID0gZGlmZkxpbmVzKG9sZFN0ciwgbmV3U3RyLCBvcHRpb25zKTtcbiAgZGlmZi5wdXNoKHt2YWx1ZTogJycsIGxpbmVzOiBbXX0pOyAgIC8vIEFwcGVuZCBhbiBlbXB0eSB2YWx1ZSB0byBtYWtlIGNsZWFudXAgZWFzaWVyXG5cbiAgZnVuY3Rpb24gY29udGV4dExpbmVzKGxpbmVzKSB7XG4gICAgcmV0dXJuIGxpbmVzLm1hcChmdW5jdGlvbihlbnRyeSkgeyByZXR1cm4gJyAnICsgZW50cnk7IH0pO1xuICB9XG5cbiAgbGV0IGh1bmtzID0gW107XG4gIGxldCBvbGRSYW5nZVN0YXJ0ID0gMCwgbmV3UmFuZ2VTdGFydCA9IDAsIGN1clJhbmdlID0gW10sXG4gICAgICBvbGRMaW5lID0gMSwgbmV3TGluZSA9IDE7XG4gIGZvciAobGV0IGkgPSAwOyBpIDwgZGlmZi5sZW5ndGg7IGkrKykge1xuICAgIGNvbnN0IGN1cnJlbnQgPSBkaWZmW2ldLFxuICAgICAgICAgIGxpbmVzID0gY3VycmVudC5saW5lcyB8fCBjdXJyZW50LnZhbHVlLnJlcGxhY2UoL1xcbiQvLCAnJykuc3BsaXQoJ1xcbicpO1xuICAgIGN1cnJlbnQubGluZXMgPSBsaW5lcztcblxuICAgIGlmIChjdXJyZW50LmFkZGVkIHx8IGN1cnJlbnQucmVtb3ZlZCkge1xuICAgICAgLy8gSWYgd2UgaGF2ZSBwcmV2aW91cyBjb250ZXh0LCBzdGFydCB3aXRoIHRoYXRcbiAgICAgIGlmICghb2xkUmFuZ2VTdGFydCkge1xuICAgICAgICBjb25zdCBwcmV2ID0gZGlmZltpIC0gMV07XG4gICAgICAgIG9sZFJhbmdlU3RhcnQgPSBvbGRMaW5lO1xuICAgICAgICBuZXdSYW5nZVN0YXJ0ID0gbmV3TGluZTtcblxuICAgICAgICBpZiAocHJldikge1xuICAgICAgICAgIGN1clJhbmdlID0gb3B0aW9ucy5jb250ZXh0ID4gMCA/IGNvbnRleHRMaW5lcyhwcmV2LmxpbmVzLnNsaWNlKC1vcHRpb25zLmNvbnRleHQpKSA6IFtdO1xuICAgICAgICAgIG9sZFJhbmdlU3RhcnQgLT0gY3VyUmFuZ2UubGVuZ3RoO1xuICAgICAgICAgIG5ld1JhbmdlU3RhcnQgLT0gY3VyUmFuZ2UubGVuZ3RoO1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIC8vIE91dHB1dCBvdXIgY2hhbmdlc1xuICAgICAgY3VyUmFuZ2UucHVzaCguLi4gbGluZXMubWFwKGZ1bmN0aW9uKGVudHJ5KSB7XG4gICAgICAgIHJldHVybiAoY3VycmVudC5hZGRlZCA/ICcrJyA6ICctJykgKyBlbnRyeTtcbiAgICAgIH0pKTtcblxuICAgICAgLy8gVHJhY2sgdGhlIHVwZGF0ZWQgZmlsZSBwb3NpdGlvblxuICAgICAgaWYgKGN1cnJlbnQuYWRkZWQpIHtcbiAgICAgICAgbmV3TGluZSArPSBsaW5lcy5sZW5ndGg7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBvbGRMaW5lICs9IGxpbmVzLmxlbmd0aDtcbiAgICAgIH1cbiAgICB9IGVsc2Uge1xuICAgICAgLy8gSWRlbnRpY2FsIGNvbnRleHQgbGluZXMuIFRyYWNrIGxpbmUgY2hhbmdlc1xuICAgICAgaWYgKG9sZFJhbmdlU3RhcnQpIHtcbiAgICAgICAgLy8gQ2xvc2Ugb3V0IGFueSBjaGFuZ2VzIHRoYXQgaGF2ZSBiZWVuIG91dHB1dCAob3Igam9pbiBvdmVybGFwcGluZylcbiAgICAgICAgaWYgKGxpbmVzLmxlbmd0aCA8PSBvcHRpb25zLmNvbnRleHQgKiAyICYmIGkgPCBkaWZmLmxlbmd0aCAtIDIpIHtcbiAgICAgICAgICAvLyBPdmVybGFwcGluZ1xuICAgICAgICAgIGN1clJhbmdlLnB1c2goLi4uIGNvbnRleHRMaW5lcyhsaW5lcykpO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIC8vIGVuZCB0aGUgcmFuZ2UgYW5kIG91dHB1dFxuICAgICAgICAgIGxldCBjb250ZXh0U2l6ZSA9IE1hdGgubWluKGxpbmVzLmxlbmd0aCwgb3B0aW9ucy5jb250ZXh0KTtcbiAgICAgICAgICBjdXJSYW5nZS5wdXNoKC4uLiBjb250ZXh0TGluZXMobGluZXMuc2xpY2UoMCwgY29udGV4dFNpemUpKSk7XG5cbiAgICAgICAgICBsZXQgaHVuayA9IHtcbiAgICAgICAgICAgIG9sZFN0YXJ0OiBvbGRSYW5nZVN0YXJ0LFxuICAgICAgICAgICAgb2xkTGluZXM6IChvbGRMaW5lIC0gb2xkUmFuZ2VTdGFydCArIGNvbnRleHRTaXplKSxcbiAgICAgICAgICAgIG5ld1N0YXJ0OiBuZXdSYW5nZVN0YXJ0LFxuICAgICAgICAgICAgbmV3TGluZXM6IChuZXdMaW5lIC0gbmV3UmFuZ2VTdGFydCArIGNvbnRleHRTaXplKSxcbiAgICAgICAgICAgIGxpbmVzOiBjdXJSYW5nZVxuICAgICAgICAgIH07XG4gICAgICAgICAgaWYgKGkgPj0gZGlmZi5sZW5ndGggLSAyICYmIGxpbmVzLmxlbmd0aCA8PSBvcHRpb25zLmNvbnRleHQpIHtcbiAgICAgICAgICAgIC8vIEVPRiBpcyBpbnNpZGUgdGhpcyBodW5rXG4gICAgICAgICAgICBsZXQgb2xkRU9GTmV3bGluZSA9ICgvXFxuJC8udGVzdChvbGRTdHIpKTtcbiAgICAgICAgICAgIGxldCBuZXdFT0ZOZXdsaW5lID0gKC9cXG4kLy50ZXN0KG5ld1N0cikpO1xuICAgICAgICAgICAgaWYgKGxpbmVzLmxlbmd0aCA9PSAwICYmICFvbGRFT0ZOZXdsaW5lKSB7XG4gICAgICAgICAgICAgIC8vIHNwZWNpYWwgY2FzZTogb2xkIGhhcyBubyBlb2wgYW5kIG5vIHRyYWlsaW5nIGNvbnRleHQ7IG5vLW5sIGNhbiBlbmQgdXAgYmVmb3JlIGFkZHNcbiAgICAgICAgICAgICAgY3VyUmFuZ2Uuc3BsaWNlKGh1bmsub2xkTGluZXMsIDAsICdcXFxcIE5vIG5ld2xpbmUgYXQgZW5kIG9mIGZpbGUnKTtcbiAgICAgICAgICAgIH0gZWxzZSBpZiAoIW9sZEVPRk5ld2xpbmUgfHwgIW5ld0VPRk5ld2xpbmUpIHtcbiAgICAgICAgICAgICAgY3VyUmFuZ2UucHVzaCgnXFxcXCBObyBuZXdsaW5lIGF0IGVuZCBvZiBmaWxlJyk7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICAgIGh1bmtzLnB1c2goaHVuayk7XG5cbiAgICAgICAgICBvbGRSYW5nZVN0YXJ0ID0gMDtcbiAgICAgICAgICBuZXdSYW5nZVN0YXJ0ID0gMDtcbiAgICAgICAgICBjdXJSYW5nZSA9IFtdO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgICBvbGRMaW5lICs9IGxpbmVzLmxlbmd0aDtcbiAgICAgIG5ld0xpbmUgKz0gbGluZXMubGVuZ3RoO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiB7XG4gICAgb2xkRmlsZU5hbWU6IG9sZEZpbGVOYW1lLCBuZXdGaWxlTmFtZTogbmV3RmlsZU5hbWUsXG4gICAgb2xkSGVhZGVyOiBvbGRIZWFkZXIsIG5ld0hlYWRlcjogbmV3SGVhZGVyLFxuICAgIGh1bmtzOiBodW5rc1xuICB9O1xufVxuXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlVHdvRmlsZXNQYXRjaChvbGRGaWxlTmFtZSwgbmV3RmlsZU5hbWUsIG9sZFN0ciwgbmV3U3RyLCBvbGRIZWFkZXIsIG5ld0hlYWRlciwgb3B0aW9ucykge1xuICBjb25zdCBkaWZmID0gc3RydWN0dXJlZFBhdGNoKG9sZEZpbGVOYW1lLCBuZXdGaWxlTmFtZSwgb2xkU3RyLCBuZXdTdHIsIG9sZEhlYWRlciwgbmV3SGVhZGVyLCBvcHRpb25zKTtcblxuICBjb25zdCByZXQgPSBbXTtcbiAgaWYgKG9sZEZpbGVOYW1lID09IG5ld0ZpbGVOYW1lKSB7XG4gICAgcmV0LnB1c2goJ0luZGV4OiAnICsgb2xkRmlsZU5hbWUpO1xuICB9XG4gIHJldC5wdXNoKCc9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09Jyk7XG4gIHJldC5wdXNoKCctLS0gJyArIGRpZmYub2xkRmlsZU5hbWUgKyAodHlwZW9mIGRpZmYub2xkSGVhZGVyID09PSAndW5kZWZpbmVkJyA/ICcnIDogJ1xcdCcgKyBkaWZmLm9sZEhlYWRlcikpO1xuICByZXQucHVzaCgnKysrICcgKyBkaWZmLm5ld0ZpbGVOYW1lICsgKHR5cGVvZiBkaWZmLm5ld0hlYWRlciA9PT0gJ3VuZGVmaW5lZCcgPyAnJyA6ICdcXHQnICsgZGlmZi5uZXdIZWFkZXIpKTtcblxuICBmb3IgKGxldCBpID0gMDsgaSA8IGRpZmYuaHVua3MubGVuZ3RoOyBpKyspIHtcbiAgICBjb25zdCBodW5rID0gZGlmZi5odW5rc1tpXTtcbiAgICByZXQucHVzaChcbiAgICAgICdAQCAtJyArIGh1bmsub2xkU3RhcnQgKyAnLCcgKyBodW5rLm9sZExpbmVzXG4gICAgICArICcgKycgKyBodW5rLm5ld1N0YXJ0ICsgJywnICsgaHVuay5uZXdMaW5lc1xuICAgICAgKyAnIEBAJ1xuICAgICk7XG4gICAgcmV0LnB1c2guYXBwbHkocmV0LCBodW5rLmxpbmVzKTtcbiAgfVxuXG4gIHJldHVybiByZXQuam9pbignXFxuJykgKyAnXFxuJztcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZVBhdGNoKGZpbGVOYW1lLCBvbGRTdHIsIG5ld1N0ciwgb2xkSGVhZGVyLCBuZXdIZWFkZXIsIG9wdGlvbnMpIHtcbiAgcmV0dXJuIGNyZWF0ZVR3b0ZpbGVzUGF0Y2goZmlsZU5hbWUsIGZpbGVOYW1lLCBvbGRTdHIsIG5ld1N0ciwgb2xkSGVhZGVyLCBuZXdIZWFkZXIsIG9wdGlvbnMpO1xufVxuIl19\n\n\n/***/ }),\n/* 15 */\n/***/ (function(module, exports) {\n\n\t/*istanbul ignore start*/\"use strict\";\n\n\texports.__esModule = true;\n\texports. /*istanbul ignore end*/arrayEqual = arrayEqual;\n\t/*istanbul ignore start*/exports. /*istanbul ignore end*/arrayStartsWith = arrayStartsWith;\n\tfunction arrayEqual(a, b) {\n\t if (a.length !== b.length) {\n\t return false;\n\t }\n\n\t return arrayStartsWith(a, b);\n\t}\n\n\tfunction arrayStartsWith(array, start) {\n\t if (start.length > array.length) {\n\t return false;\n\t }\n\n\t for (var i = 0; i < start.length; i++) {\n\t if (start[i] !== array[i]) {\n\t return false;\n\t }\n\t }\n\n\t return true;\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlsL2FycmF5LmpzIl0sIm5hbWVzIjpbImFycmF5RXF1YWwiLCJhcnJheVN0YXJ0c1dpdGgiLCJhIiwiYiIsImxlbmd0aCIsImFycmF5Iiwic3RhcnQiLCJpIl0sIm1hcHBpbmdzIjoiOzs7Z0NBQWdCQSxVLEdBQUFBLFU7eURBUUFDLGUsR0FBQUEsZTtBQVJULFNBQVNELFVBQVQsQ0FBb0JFLENBQXBCLEVBQXVCQyxDQUF2QixFQUEwQjtBQUMvQixNQUFJRCxFQUFFRSxNQUFGLEtBQWFELEVBQUVDLE1BQW5CLEVBQTJCO0FBQ3pCLFdBQU8sS0FBUDtBQUNEOztBQUVELFNBQU9ILGdCQUFnQkMsQ0FBaEIsRUFBbUJDLENBQW5CLENBQVA7QUFDRDs7QUFFTSxTQUFTRixlQUFULENBQXlCSSxLQUF6QixFQUFnQ0MsS0FBaEMsRUFBdUM7QUFDNUMsTUFBSUEsTUFBTUYsTUFBTixHQUFlQyxNQUFNRCxNQUF6QixFQUFpQztBQUMvQixXQUFPLEtBQVA7QUFDRDs7QUFFRCxPQUFLLElBQUlHLElBQUksQ0FBYixFQUFnQkEsSUFBSUQsTUFBTUYsTUFBMUIsRUFBa0NHLEdBQWxDLEVBQXVDO0FBQ3JDLFFBQUlELE1BQU1DLENBQU4sTUFBYUYsTUFBTUUsQ0FBTixDQUFqQixFQUEyQjtBQUN6QixhQUFPLEtBQVA7QUFDRDtBQUNGOztBQUVELFNBQU8sSUFBUDtBQUNEIiwiZmlsZSI6ImFycmF5LmpzIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIGFycmF5RXF1YWwoYSwgYikge1xuICBpZiAoYS5sZW5ndGggIT09IGIubGVuZ3RoKSB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG5cbiAgcmV0dXJuIGFycmF5U3RhcnRzV2l0aChhLCBiKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGFycmF5U3RhcnRzV2l0aChhcnJheSwgc3RhcnQpIHtcbiAgaWYgKHN0YXJ0Lmxlbmd0aCA+IGFycmF5Lmxlbmd0aCkge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIGZvciAobGV0IGkgPSAwOyBpIDwgc3RhcnQubGVuZ3RoOyBpKyspIHtcbiAgICBpZiAoc3RhcnRbaV0gIT09IGFycmF5W2ldKSB7XG4gICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICB9XG5cbiAgcmV0dXJuIHRydWU7XG59XG4iXX0=\n\n\n/***/ }),\n/* 16 */\n/***/ (function(module, exports) {\n\n\t/*istanbul ignore start*/\"use strict\";\n\n\texports.__esModule = true;\n\texports. /*istanbul ignore end*/convertChangesToDMP = convertChangesToDMP;\n\t// See: http://code.google.com/p/google-diff-match-patch/wiki/API\n\tfunction convertChangesToDMP(changes) {\n\t var ret = [],\n\t change = /*istanbul ignore start*/void 0 /*istanbul ignore end*/,\n\t operation = /*istanbul ignore start*/void 0 /*istanbul ignore end*/;\n\t for (var i = 0; i < changes.length; i++) {\n\t change = changes[i];\n\t if (change.added) {\n\t operation = 1;\n\t } else if (change.removed) {\n\t operation = -1;\n\t } else {\n\t operation = 0;\n\t }\n\n\t ret.push([operation, change.value]);\n\t }\n\t return ret;\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb252ZXJ0L2RtcC5qcyJdLCJuYW1lcyI6WyJjb252ZXJ0Q2hhbmdlc1RvRE1QIiwiY2hhbmdlcyIsInJldCIsImNoYW5nZSIsIm9wZXJhdGlvbiIsImkiLCJsZW5ndGgiLCJhZGRlZCIsInJlbW92ZWQiLCJwdXNoIiwidmFsdWUiXSwibWFwcGluZ3MiOiI7OztnQ0FDZ0JBLG1CLEdBQUFBLG1CO0FBRGhCO0FBQ08sU0FBU0EsbUJBQVQsQ0FBNkJDLE9BQTdCLEVBQXNDO0FBQzNDLE1BQUlDLE1BQU0sRUFBVjtBQUFBLE1BQ0lDLHdDQURKO0FBQUEsTUFFSUMsMkNBRko7QUFHQSxPQUFLLElBQUlDLElBQUksQ0FBYixFQUFnQkEsSUFBSUosUUFBUUssTUFBNUIsRUFBb0NELEdBQXBDLEVBQXlDO0FBQ3ZDRixhQUFTRixRQUFRSSxDQUFSLENBQVQ7QUFDQSxRQUFJRixPQUFPSSxLQUFYLEVBQWtCO0FBQ2hCSCxrQkFBWSxDQUFaO0FBQ0QsS0FGRCxNQUVPLElBQUlELE9BQU9LLE9BQVgsRUFBb0I7QUFDekJKLGtCQUFZLENBQUMsQ0FBYjtBQUNELEtBRk0sTUFFQTtBQUNMQSxrQkFBWSxDQUFaO0FBQ0Q7O0FBRURGLFFBQUlPLElBQUosQ0FBUyxDQUFDTCxTQUFELEVBQVlELE9BQU9PLEtBQW5CLENBQVQ7QUFDRDtBQUNELFNBQU9SLEdBQVA7QUFDRCIsImZpbGUiOiJkbXAuanMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBTZWU6IGh0dHA6Ly9jb2RlLmdvb2dsZS5jb20vcC9nb29nbGUtZGlmZi1tYXRjaC1wYXRjaC93aWtpL0FQSVxuZXhwb3J0IGZ1bmN0aW9uIGNvbnZlcnRDaGFuZ2VzVG9ETVAoY2hhbmdlcykge1xuICBsZXQgcmV0ID0gW10sXG4gICAgICBjaGFuZ2UsXG4gICAgICBvcGVyYXRpb247XG4gIGZvciAobGV0IGkgPSAwOyBpIDwgY2hhbmdlcy5sZW5ndGg7IGkrKykge1xuICAgIGNoYW5nZSA9IGNoYW5nZXNbaV07XG4gICAgaWYgKGNoYW5nZS5hZGRlZCkge1xuICAgICAgb3BlcmF0aW9uID0gMTtcbiAgICB9IGVsc2UgaWYgKGNoYW5nZS5yZW1vdmVkKSB7XG4gICAgICBvcGVyYXRpb24gPSAtMTtcbiAgICB9IGVsc2Uge1xuICAgICAgb3BlcmF0aW9uID0gMDtcbiAgICB9XG5cbiAgICByZXQucHVzaChbb3BlcmF0aW9uLCBjaGFuZ2UudmFsdWVdKTtcbiAgfVxuICByZXR1cm4gcmV0O1xufVxuIl19\n\n\n/***/ }),\n/* 17 */\n/***/ (function(module, exports) {\n\n\t/*istanbul ignore start*/'use strict';\n\n\texports.__esModule = true;\n\texports. /*istanbul ignore end*/convertChangesToXML = convertChangesToXML;\n\tfunction convertChangesToXML(changes) {\n\t var ret = [];\n\t for (var i = 0; i < changes.length; i++) {\n\t var change = changes[i];\n\t if (change.added) {\n\t ret.push('');\n\t } else if (change.removed) {\n\t ret.push('');\n\t }\n\n\t ret.push(escapeHTML(change.value));\n\n\t if (change.added) {\n\t ret.push('');\n\t } else if (change.removed) {\n\t ret.push('');\n\t }\n\t }\n\t return ret.join('');\n\t}\n\n\tfunction escapeHTML(s) {\n\t var n = s;\n\t n = n.replace(/&/g, '&');\n\t n = n.replace(//g, '>');\n\t n = n.replace(/\"/g, '"');\n\n\t return n;\n\t}\n\t//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb252ZXJ0L3htbC5qcyJdLCJuYW1lcyI6WyJjb252ZXJ0Q2hhbmdlc1RvWE1MIiwiY2hhbmdlcyIsInJldCIsImkiLCJsZW5ndGgiLCJjaGFuZ2UiLCJhZGRlZCIsInB1c2giLCJyZW1vdmVkIiwiZXNjYXBlSFRNTCIsInZhbHVlIiwiam9pbiIsInMiLCJuIiwicmVwbGFjZSJdLCJtYXBwaW5ncyI6Ijs7O2dDQUFnQkEsbUIsR0FBQUEsbUI7QUFBVCxTQUFTQSxtQkFBVCxDQUE2QkMsT0FBN0IsRUFBc0M7QUFDM0MsTUFBSUMsTUFBTSxFQUFWO0FBQ0EsT0FBSyxJQUFJQyxJQUFJLENBQWIsRUFBZ0JBLElBQUlGLFFBQVFHLE1BQTVCLEVBQW9DRCxHQUFwQyxFQUF5QztBQUN2QyxRQUFJRSxTQUFTSixRQUFRRSxDQUFSLENBQWI7QUFDQSxRQUFJRSxPQUFPQyxLQUFYLEVBQWtCO0FBQ2hCSixVQUFJSyxJQUFKLENBQVMsT0FBVDtBQUNELEtBRkQsTUFFTyxJQUFJRixPQUFPRyxPQUFYLEVBQW9CO0FBQ3pCTixVQUFJSyxJQUFKLENBQVMsT0FBVDtBQUNEOztBQUVETCxRQUFJSyxJQUFKLENBQVNFLFdBQVdKLE9BQU9LLEtBQWxCLENBQVQ7O0FBRUEsUUFBSUwsT0FBT0MsS0FBWCxFQUFrQjtBQUNoQkosVUFBSUssSUFBSixDQUFTLFFBQVQ7QUFDRCxLQUZELE1BRU8sSUFBSUYsT0FBT0csT0FBWCxFQUFvQjtBQUN6Qk4sVUFBSUssSUFBSixDQUFTLFFBQVQ7QUFDRDtBQUNGO0FBQ0QsU0FBT0wsSUFBSVMsSUFBSixDQUFTLEVBQVQsQ0FBUDtBQUNEOztBQUVELFNBQVNGLFVBQVQsQ0FBb0JHLENBQXBCLEVBQXVCO0FBQ3JCLE1BQUlDLElBQUlELENBQVI7QUFDQUMsTUFBSUEsRUFBRUMsT0FBRixDQUFVLElBQVYsRUFBZ0IsT0FBaEIsQ0FBSjtBQUNBRCxNQUFJQSxFQUFFQyxPQUFGLENBQVUsSUFBVixFQUFnQixNQUFoQixDQUFKO0FBQ0FELE1BQUlBLEVBQUVDLE9BQUYsQ0FBVSxJQUFWLEVBQWdCLE1BQWhCLENBQUo7QUFDQUQsTUFBSUEsRUFBRUMsT0FBRixDQUFVLElBQVYsRUFBZ0IsUUFBaEIsQ0FBSjs7QUFFQSxTQUFPRCxDQUFQO0FBQ0QiLCJmaWxlIjoieG1sLmpzIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIGNvbnZlcnRDaGFuZ2VzVG9YTUwoY2hhbmdlcykge1xuICBsZXQgcmV0ID0gW107XG4gIGZvciAobGV0IGkgPSAwOyBpIDwgY2hhbmdlcy5sZW5ndGg7IGkrKykge1xuICAgIGxldCBjaGFuZ2UgPSBjaGFuZ2VzW2ldO1xuICAgIGlmIChjaGFuZ2UuYWRkZWQpIHtcbiAgICAgIHJldC5wdXNoKCc8aW5zPicpO1xuICAgIH0gZWxzZSBpZiAoY2hhbmdlLnJlbW92ZWQpIHtcbiAgICAgIHJldC5wdXNoKCc8ZGVsPicpO1xuICAgIH1cblxuICAgIHJldC5wdXNoKGVzY2FwZUhUTUwoY2hhbmdlLnZhbHVlKSk7XG5cbiAgICBpZiAoY2hhbmdlLmFkZGVkKSB7XG4gICAgICByZXQucHVzaCgnPC9pbnM+Jyk7XG4gICAgfSBlbHNlIGlmIChjaGFuZ2UucmVtb3ZlZCkge1xuICAgICAgcmV0LnB1c2goJzwvZGVsPicpO1xuICAgIH1cbiAgfVxuICByZXR1cm4gcmV0LmpvaW4oJycpO1xufVxuXG5mdW5jdGlvbiBlc2NhcGVIVE1MKHMpIHtcbiAgbGV0IG4gPSBzO1xuICBuID0gbi5yZXBsYWNlKC8mL2csICcmYW1wOycpO1xuICBuID0gbi5yZXBsYWNlKC88L2csICcmbHQ7Jyk7XG4gIG4gPSBuLnJlcGxhY2UoLz4vZywgJyZndDsnKTtcbiAgbiA9IG4ucmVwbGFjZSgvXCIvZywgJyZxdW90OycpO1xuXG4gIHJldHVybiBuO1xufVxuIl19\n\n\n/***/ })\n/******/ ])\n});\n;\n\n//# sourceURL=webpack:///./node_modules/diff/dist/diff.js?"); + +/***/ }), + +/***/ "./node_modules/lodash.get/index.js": +/*!******************************************!*\ + !*** ./node_modules/lodash.get/index.js ***! + \******************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +eval("/* WEBPACK VAR INJECTION */(function(global) {/**\n * lodash (Custom Build) \n * Build: `lodash modularize exports=\"npm\" -o ./`\n * Copyright jQuery Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n */\n\n/** Used as the `TypeError` message for \"Functions\" methods. */\nvar FUNC_ERROR_TEXT = 'Expected a function';\n\n/** Used to stand-in for `undefined` hash values. */\nvar HASH_UNDEFINED = '__lodash_hash_undefined__';\n\n/** Used as references for various `Number` constants. */\nvar INFINITY = 1 / 0;\n\n/** `Object#toString` result references. */\nvar funcTag = '[object Function]',\n genTag = '[object GeneratorFunction]',\n symbolTag = '[object Symbol]';\n\n/** Used to match property names within property paths. */\nvar reIsDeepProp = /\\.|\\[(?:[^[\\]]*|([\"'])(?:(?!\\1)[^\\\\]|\\\\.)*?\\1)\\]/,\n reIsPlainProp = /^\\w*$/,\n reLeadingDot = /^\\./,\n rePropName = /[^.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))/g;\n\n/**\n * Used to match `RegExp`\n * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).\n */\nvar reRegExpChar = /[\\\\^$.*+?()[\\]{}|]/g;\n\n/** Used to match backslashes in property paths. */\nvar reEscapeChar = /\\\\(\\\\)?/g;\n\n/** Used to detect host constructors (Safari). */\nvar reIsHostCtor = /^\\[object .+?Constructor\\]$/;\n\n/** Detect free variable `global` from Node.js. */\nvar freeGlobal = typeof global == 'object' && global && global.Object === Object && global;\n\n/** Detect free variable `self`. */\nvar freeSelf = typeof self == 'object' && self && self.Object === Object && self;\n\n/** Used as a reference to the global object. */\nvar root = freeGlobal || freeSelf || Function('return this')();\n\n/**\n * Gets the value at `key` of `object`.\n *\n * @private\n * @param {Object} [object] The object to query.\n * @param {string} key The key of the property to get.\n * @returns {*} Returns the property value.\n */\nfunction getValue(object, key) {\n return object == null ? undefined : object[key];\n}\n\n/**\n * Checks if `value` is a host object in IE < 9.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a host object, else `false`.\n */\nfunction isHostObject(value) {\n // Many host objects are `Object` objects that can coerce to strings\n // despite having improperly defined `toString` methods.\n var result = false;\n if (value != null && typeof value.toString != 'function') {\n try {\n result = !!(value + '');\n } catch (e) {}\n }\n return result;\n}\n\n/** Used for built-in method references. */\nvar arrayProto = Array.prototype,\n funcProto = Function.prototype,\n objectProto = Object.prototype;\n\n/** Used to detect overreaching core-js shims. */\nvar coreJsData = root['__core-js_shared__'];\n\n/** Used to detect methods masquerading as native. */\nvar maskSrcKey = (function() {\n var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');\n return uid ? ('Symbol(src)_1.' + uid) : '';\n}());\n\n/** Used to resolve the decompiled source of functions. */\nvar funcToString = funcProto.toString;\n\n/** Used to check objects for own properties. */\nvar hasOwnProperty = objectProto.hasOwnProperty;\n\n/**\n * Used to resolve the\n * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)\n * of values.\n */\nvar objectToString = objectProto.toString;\n\n/** Used to detect if a method is native. */\nvar reIsNative = RegExp('^' +\n funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\\\$&')\n .replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g, '$1.*?') + '$'\n);\n\n/** Built-in value references. */\nvar Symbol = root.Symbol,\n splice = arrayProto.splice;\n\n/* Built-in method references that are verified to be native. */\nvar Map = getNative(root, 'Map'),\n nativeCreate = getNative(Object, 'create');\n\n/** Used to convert symbols to primitives and strings. */\nvar symbolProto = Symbol ? Symbol.prototype : undefined,\n symbolToString = symbolProto ? symbolProto.toString : undefined;\n\n/**\n * Creates a hash object.\n *\n * @private\n * @constructor\n * @param {Array} [entries] The key-value pairs to cache.\n */\nfunction Hash(entries) {\n var index = -1,\n length = entries ? entries.length : 0;\n\n this.clear();\n while (++index < length) {\n var entry = entries[index];\n this.set(entry[0], entry[1]);\n }\n}\n\n/**\n * Removes all key-value entries from the hash.\n *\n * @private\n * @name clear\n * @memberOf Hash\n */\nfunction hashClear() {\n this.__data__ = nativeCreate ? nativeCreate(null) : {};\n}\n\n/**\n * Removes `key` and its value from the hash.\n *\n * @private\n * @name delete\n * @memberOf Hash\n * @param {Object} hash The hash to modify.\n * @param {string} key The key of the value to remove.\n * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n */\nfunction hashDelete(key) {\n return this.has(key) && delete this.__data__[key];\n}\n\n/**\n * Gets the hash value for `key`.\n *\n * @private\n * @name get\n * @memberOf Hash\n * @param {string} key The key of the value to get.\n * @returns {*} Returns the entry value.\n */\nfunction hashGet(key) {\n var data = this.__data__;\n if (nativeCreate) {\n var result = data[key];\n return result === HASH_UNDEFINED ? undefined : result;\n }\n return hasOwnProperty.call(data, key) ? data[key] : undefined;\n}\n\n/**\n * Checks if a hash value for `key` exists.\n *\n * @private\n * @name has\n * @memberOf Hash\n * @param {string} key The key of the entry to check.\n * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n */\nfunction hashHas(key) {\n var data = this.__data__;\n return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key);\n}\n\n/**\n * Sets the hash `key` to `value`.\n *\n * @private\n * @name set\n * @memberOf Hash\n * @param {string} key The key of the value to set.\n * @param {*} value The value to set.\n * @returns {Object} Returns the hash instance.\n */\nfunction hashSet(key, value) {\n var data = this.__data__;\n data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;\n return this;\n}\n\n// Add methods to `Hash`.\nHash.prototype.clear = hashClear;\nHash.prototype['delete'] = hashDelete;\nHash.prototype.get = hashGet;\nHash.prototype.has = hashHas;\nHash.prototype.set = hashSet;\n\n/**\n * Creates an list cache object.\n *\n * @private\n * @constructor\n * @param {Array} [entries] The key-value pairs to cache.\n */\nfunction ListCache(entries) {\n var index = -1,\n length = entries ? entries.length : 0;\n\n this.clear();\n while (++index < length) {\n var entry = entries[index];\n this.set(entry[0], entry[1]);\n }\n}\n\n/**\n * Removes all key-value entries from the list cache.\n *\n * @private\n * @name clear\n * @memberOf ListCache\n */\nfunction listCacheClear() {\n this.__data__ = [];\n}\n\n/**\n * Removes `key` and its value from the list cache.\n *\n * @private\n * @name delete\n * @memberOf ListCache\n * @param {string} key The key of the value to remove.\n * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n */\nfunction listCacheDelete(key) {\n var data = this.__data__,\n index = assocIndexOf(data, key);\n\n if (index < 0) {\n return false;\n }\n var lastIndex = data.length - 1;\n if (index == lastIndex) {\n data.pop();\n } else {\n splice.call(data, index, 1);\n }\n return true;\n}\n\n/**\n * Gets the list cache value for `key`.\n *\n * @private\n * @name get\n * @memberOf ListCache\n * @param {string} key The key of the value to get.\n * @returns {*} Returns the entry value.\n */\nfunction listCacheGet(key) {\n var data = this.__data__,\n index = assocIndexOf(data, key);\n\n return index < 0 ? undefined : data[index][1];\n}\n\n/**\n * Checks if a list cache value for `key` exists.\n *\n * @private\n * @name has\n * @memberOf ListCache\n * @param {string} key The key of the entry to check.\n * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n */\nfunction listCacheHas(key) {\n return assocIndexOf(this.__data__, key) > -1;\n}\n\n/**\n * Sets the list cache `key` to `value`.\n *\n * @private\n * @name set\n * @memberOf ListCache\n * @param {string} key The key of the value to set.\n * @param {*} value The value to set.\n * @returns {Object} Returns the list cache instance.\n */\nfunction listCacheSet(key, value) {\n var data = this.__data__,\n index = assocIndexOf(data, key);\n\n if (index < 0) {\n data.push([key, value]);\n } else {\n data[index][1] = value;\n }\n return this;\n}\n\n// Add methods to `ListCache`.\nListCache.prototype.clear = listCacheClear;\nListCache.prototype['delete'] = listCacheDelete;\nListCache.prototype.get = listCacheGet;\nListCache.prototype.has = listCacheHas;\nListCache.prototype.set = listCacheSet;\n\n/**\n * Creates a map cache object to store key-value pairs.\n *\n * @private\n * @constructor\n * @param {Array} [entries] The key-value pairs to cache.\n */\nfunction MapCache(entries) {\n var index = -1,\n length = entries ? entries.length : 0;\n\n this.clear();\n while (++index < length) {\n var entry = entries[index];\n this.set(entry[0], entry[1]);\n }\n}\n\n/**\n * Removes all key-value entries from the map.\n *\n * @private\n * @name clear\n * @memberOf MapCache\n */\nfunction mapCacheClear() {\n this.__data__ = {\n 'hash': new Hash,\n 'map': new (Map || ListCache),\n 'string': new Hash\n };\n}\n\n/**\n * Removes `key` and its value from the map.\n *\n * @private\n * @name delete\n * @memberOf MapCache\n * @param {string} key The key of the value to remove.\n * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n */\nfunction mapCacheDelete(key) {\n return getMapData(this, key)['delete'](key);\n}\n\n/**\n * Gets the map value for `key`.\n *\n * @private\n * @name get\n * @memberOf MapCache\n * @param {string} key The key of the value to get.\n * @returns {*} Returns the entry value.\n */\nfunction mapCacheGet(key) {\n return getMapData(this, key).get(key);\n}\n\n/**\n * Checks if a map value for `key` exists.\n *\n * @private\n * @name has\n * @memberOf MapCache\n * @param {string} key The key of the entry to check.\n * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n */\nfunction mapCacheHas(key) {\n return getMapData(this, key).has(key);\n}\n\n/**\n * Sets the map `key` to `value`.\n *\n * @private\n * @name set\n * @memberOf MapCache\n * @param {string} key The key of the value to set.\n * @param {*} value The value to set.\n * @returns {Object} Returns the map cache instance.\n */\nfunction mapCacheSet(key, value) {\n getMapData(this, key).set(key, value);\n return this;\n}\n\n// Add methods to `MapCache`.\nMapCache.prototype.clear = mapCacheClear;\nMapCache.prototype['delete'] = mapCacheDelete;\nMapCache.prototype.get = mapCacheGet;\nMapCache.prototype.has = mapCacheHas;\nMapCache.prototype.set = mapCacheSet;\n\n/**\n * Gets the index at which the `key` is found in `array` of key-value pairs.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {*} key The key to search for.\n * @returns {number} Returns the index of the matched value, else `-1`.\n */\nfunction assocIndexOf(array, key) {\n var length = array.length;\n while (length--) {\n if (eq(array[length][0], key)) {\n return length;\n }\n }\n return -1;\n}\n\n/**\n * The base implementation of `_.get` without support for default values.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Array|string} path The path of the property to get.\n * @returns {*} Returns the resolved value.\n */\nfunction baseGet(object, path) {\n path = isKey(path, object) ? [path] : castPath(path);\n\n var index = 0,\n length = path.length;\n\n while (object != null && index < length) {\n object = object[toKey(path[index++])];\n }\n return (index && index == length) ? object : undefined;\n}\n\n/**\n * The base implementation of `_.isNative` without bad shim checks.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a native function,\n * else `false`.\n */\nfunction baseIsNative(value) {\n if (!isObject(value) || isMasked(value)) {\n return false;\n }\n var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor;\n return pattern.test(toSource(value));\n}\n\n/**\n * The base implementation of `_.toString` which doesn't convert nullish\n * values to empty strings.\n *\n * @private\n * @param {*} value The value to process.\n * @returns {string} Returns the string.\n */\nfunction baseToString(value) {\n // Exit early for strings to avoid a performance hit in some environments.\n if (typeof value == 'string') {\n return value;\n }\n if (isSymbol(value)) {\n return symbolToString ? symbolToString.call(value) : '';\n }\n var result = (value + '');\n return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;\n}\n\n/**\n * Casts `value` to a path array if it's not one.\n *\n * @private\n * @param {*} value The value to inspect.\n * @returns {Array} Returns the cast property path array.\n */\nfunction castPath(value) {\n return isArray(value) ? value : stringToPath(value);\n}\n\n/**\n * Gets the data for `map`.\n *\n * @private\n * @param {Object} map The map to query.\n * @param {string} key The reference key.\n * @returns {*} Returns the map data.\n */\nfunction getMapData(map, key) {\n var data = map.__data__;\n return isKeyable(key)\n ? data[typeof key == 'string' ? 'string' : 'hash']\n : data.map;\n}\n\n/**\n * Gets the native function at `key` of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {string} key The key of the method to get.\n * @returns {*} Returns the function if it's native, else `undefined`.\n */\nfunction getNative(object, key) {\n var value = getValue(object, key);\n return baseIsNative(value) ? value : undefined;\n}\n\n/**\n * Checks if `value` is a property name and not a property path.\n *\n * @private\n * @param {*} value The value to check.\n * @param {Object} [object] The object to query keys on.\n * @returns {boolean} Returns `true` if `value` is a property name, else `false`.\n */\nfunction isKey(value, object) {\n if (isArray(value)) {\n return false;\n }\n var type = typeof value;\n if (type == 'number' || type == 'symbol' || type == 'boolean' ||\n value == null || isSymbol(value)) {\n return true;\n }\n return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||\n (object != null && value in Object(object));\n}\n\n/**\n * Checks if `value` is suitable for use as unique object key.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is suitable, else `false`.\n */\nfunction isKeyable(value) {\n var type = typeof value;\n return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')\n ? (value !== '__proto__')\n : (value === null);\n}\n\n/**\n * Checks if `func` has its source masked.\n *\n * @private\n * @param {Function} func The function to check.\n * @returns {boolean} Returns `true` if `func` is masked, else `false`.\n */\nfunction isMasked(func) {\n return !!maskSrcKey && (maskSrcKey in func);\n}\n\n/**\n * Converts `string` to a property path array.\n *\n * @private\n * @param {string} string The string to convert.\n * @returns {Array} Returns the property path array.\n */\nvar stringToPath = memoize(function(string) {\n string = toString(string);\n\n var result = [];\n if (reLeadingDot.test(string)) {\n result.push('');\n }\n string.replace(rePropName, function(match, number, quote, string) {\n result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match));\n });\n return result;\n});\n\n/**\n * Converts `value` to a string key if it's not a string or symbol.\n *\n * @private\n * @param {*} value The value to inspect.\n * @returns {string|symbol} Returns the key.\n */\nfunction toKey(value) {\n if (typeof value == 'string' || isSymbol(value)) {\n return value;\n }\n var result = (value + '');\n return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;\n}\n\n/**\n * Converts `func` to its source code.\n *\n * @private\n * @param {Function} func The function to process.\n * @returns {string} Returns the source code.\n */\nfunction toSource(func) {\n if (func != null) {\n try {\n return funcToString.call(func);\n } catch (e) {}\n try {\n return (func + '');\n } catch (e) {}\n }\n return '';\n}\n\n/**\n * Creates a function that memoizes the result of `func`. If `resolver` is\n * provided, it determines the cache key for storing the result based on the\n * arguments provided to the memoized function. By default, the first argument\n * provided to the memoized function is used as the map cache key. The `func`\n * is invoked with the `this` binding of the memoized function.\n *\n * **Note:** The cache is exposed as the `cache` property on the memoized\n * function. Its creation may be customized by replacing the `_.memoize.Cache`\n * constructor with one whose instances implement the\n * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)\n * method interface of `delete`, `get`, `has`, and `set`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {Function} func The function to have its output memoized.\n * @param {Function} [resolver] The function to resolve the cache key.\n * @returns {Function} Returns the new memoized function.\n * @example\n *\n * var object = { 'a': 1, 'b': 2 };\n * var other = { 'c': 3, 'd': 4 };\n *\n * var values = _.memoize(_.values);\n * values(object);\n * // => [1, 2]\n *\n * values(other);\n * // => [3, 4]\n *\n * object.a = 2;\n * values(object);\n * // => [1, 2]\n *\n * // Modify the result cache.\n * values.cache.set(object, ['a', 'b']);\n * values(object);\n * // => ['a', 'b']\n *\n * // Replace `_.memoize.Cache`.\n * _.memoize.Cache = WeakMap;\n */\nfunction memoize(func, resolver) {\n if (typeof func != 'function' || (resolver && typeof resolver != 'function')) {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n var memoized = function() {\n var args = arguments,\n key = resolver ? resolver.apply(this, args) : args[0],\n cache = memoized.cache;\n\n if (cache.has(key)) {\n return cache.get(key);\n }\n var result = func.apply(this, args);\n memoized.cache = cache.set(key, result);\n return result;\n };\n memoized.cache = new (memoize.Cache || MapCache);\n return memoized;\n}\n\n// Assign cache to `_.memoize`.\nmemoize.Cache = MapCache;\n\n/**\n * Performs a\n * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * comparison between two values to determine if they are equivalent.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * var object = { 'a': 1 };\n * var other = { 'a': 1 };\n *\n * _.eq(object, object);\n * // => true\n *\n * _.eq(object, other);\n * // => false\n *\n * _.eq('a', 'a');\n * // => true\n *\n * _.eq('a', Object('a'));\n * // => false\n *\n * _.eq(NaN, NaN);\n * // => true\n */\nfunction eq(value, other) {\n return value === other || (value !== value && other !== other);\n}\n\n/**\n * Checks if `value` is classified as an `Array` object.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an array, else `false`.\n * @example\n *\n * _.isArray([1, 2, 3]);\n * // => true\n *\n * _.isArray(document.body.children);\n * // => false\n *\n * _.isArray('abc');\n * // => false\n *\n * _.isArray(_.noop);\n * // => false\n */\nvar isArray = Array.isArray;\n\n/**\n * Checks if `value` is classified as a `Function` object.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a function, else `false`.\n * @example\n *\n * _.isFunction(_);\n * // => true\n *\n * _.isFunction(/abc/);\n * // => false\n */\nfunction isFunction(value) {\n // The use of `Object#toString` avoids issues with the `typeof` operator\n // in Safari 8-9 which returns 'object' for typed array and other constructors.\n var tag = isObject(value) ? objectToString.call(value) : '';\n return tag == funcTag || tag == genTag;\n}\n\n/**\n * Checks if `value` is the\n * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)\n * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an object, else `false`.\n * @example\n *\n * _.isObject({});\n * // => true\n *\n * _.isObject([1, 2, 3]);\n * // => true\n *\n * _.isObject(_.noop);\n * // => true\n *\n * _.isObject(null);\n * // => false\n */\nfunction isObject(value) {\n var type = typeof value;\n return !!value && (type == 'object' || type == 'function');\n}\n\n/**\n * Checks if `value` is object-like. A value is object-like if it's not `null`\n * and has a `typeof` result of \"object\".\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is object-like, else `false`.\n * @example\n *\n * _.isObjectLike({});\n * // => true\n *\n * _.isObjectLike([1, 2, 3]);\n * // => true\n *\n * _.isObjectLike(_.noop);\n * // => false\n *\n * _.isObjectLike(null);\n * // => false\n */\nfunction isObjectLike(value) {\n return !!value && typeof value == 'object';\n}\n\n/**\n * Checks if `value` is classified as a `Symbol` primitive or object.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.\n * @example\n *\n * _.isSymbol(Symbol.iterator);\n * // => true\n *\n * _.isSymbol('abc');\n * // => false\n */\nfunction isSymbol(value) {\n return typeof value == 'symbol' ||\n (isObjectLike(value) && objectToString.call(value) == symbolTag);\n}\n\n/**\n * Converts `value` to a string. An empty string is returned for `null`\n * and `undefined` values. The sign of `-0` is preserved.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to process.\n * @returns {string} Returns the string.\n * @example\n *\n * _.toString(null);\n * // => ''\n *\n * _.toString(-0);\n * // => '-0'\n *\n * _.toString([1, 2, 3]);\n * // => '1,2,3'\n */\nfunction toString(value) {\n return value == null ? '' : baseToString(value);\n}\n\n/**\n * Gets the value at `path` of `object`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n * @static\n * @memberOf _\n * @since 3.7.0\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path of the property to get.\n * @param {*} [defaultValue] The value returned for `undefined` resolved values.\n * @returns {*} Returns the resolved value.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c': 3 } }] };\n *\n * _.get(object, 'a[0].b.c');\n * // => 3\n *\n * _.get(object, ['a', '0', 'b', 'c']);\n * // => 3\n *\n * _.get(object, 'a.b.c', 'default');\n * // => 'default'\n */\nfunction get(object, path, defaultValue) {\n var result = object == null ? undefined : baseGet(object, path);\n return result === undefined ? defaultValue : result;\n}\n\nmodule.exports = get;\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../webpack/buildin/global.js */ \"./node_modules/webpack/buildin/global.js\")))\n\n//# sourceURL=webpack:///./node_modules/lodash.get/index.js?"); + +/***/ }), + +/***/ "./node_modules/lodash/lodash.js": +/*!***************************************!*\ + !*** ./node_modules/lodash/lodash.js ***! + \***************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +eval("/* WEBPACK VAR INJECTION */(function(global, module) {var __WEBPACK_AMD_DEFINE_RESULT__;/**\n * @license\n * Lodash \n * Copyright JS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n */\n;(function() {\n\n /** Used as a safe reference for `undefined` in pre-ES5 environments. */\n var undefined;\n\n /** Used as the semantic version number. */\n var VERSION = '4.17.10';\n\n /** Used as the size to enable large array optimizations. */\n var LARGE_ARRAY_SIZE = 200;\n\n /** Error message constants. */\n var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://npms.io/search?q=ponyfill.',\n FUNC_ERROR_TEXT = 'Expected a function';\n\n /** Used to stand-in for `undefined` hash values. */\n var HASH_UNDEFINED = '__lodash_hash_undefined__';\n\n /** Used as the maximum memoize cache size. */\n var MAX_MEMOIZE_SIZE = 500;\n\n /** Used as the internal argument placeholder. */\n var PLACEHOLDER = '__lodash_placeholder__';\n\n /** Used to compose bitmasks for cloning. */\n var CLONE_DEEP_FLAG = 1,\n CLONE_FLAT_FLAG = 2,\n CLONE_SYMBOLS_FLAG = 4;\n\n /** Used to compose bitmasks for value comparisons. */\n var COMPARE_PARTIAL_FLAG = 1,\n COMPARE_UNORDERED_FLAG = 2;\n\n /** Used to compose bitmasks for function metadata. */\n var WRAP_BIND_FLAG = 1,\n WRAP_BIND_KEY_FLAG = 2,\n WRAP_CURRY_BOUND_FLAG = 4,\n WRAP_CURRY_FLAG = 8,\n WRAP_CURRY_RIGHT_FLAG = 16,\n WRAP_PARTIAL_FLAG = 32,\n WRAP_PARTIAL_RIGHT_FLAG = 64,\n WRAP_ARY_FLAG = 128,\n WRAP_REARG_FLAG = 256,\n WRAP_FLIP_FLAG = 512;\n\n /** Used as default options for `_.truncate`. */\n var DEFAULT_TRUNC_LENGTH = 30,\n DEFAULT_TRUNC_OMISSION = '...';\n\n /** Used to detect hot functions by number of calls within a span of milliseconds. */\n var HOT_COUNT = 800,\n HOT_SPAN = 16;\n\n /** Used to indicate the type of lazy iteratees. */\n var LAZY_FILTER_FLAG = 1,\n LAZY_MAP_FLAG = 2,\n LAZY_WHILE_FLAG = 3;\n\n /** Used as references for various `Number` constants. */\n var INFINITY = 1 / 0,\n MAX_SAFE_INTEGER = 9007199254740991,\n MAX_INTEGER = 1.7976931348623157e+308,\n NAN = 0 / 0;\n\n /** Used as references for the maximum length and index of an array. */\n var MAX_ARRAY_LENGTH = 4294967295,\n MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1,\n HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1;\n\n /** Used to associate wrap methods with their bit flags. */\n var wrapFlags = [\n ['ary', WRAP_ARY_FLAG],\n ['bind', WRAP_BIND_FLAG],\n ['bindKey', WRAP_BIND_KEY_FLAG],\n ['curry', WRAP_CURRY_FLAG],\n ['curryRight', WRAP_CURRY_RIGHT_FLAG],\n ['flip', WRAP_FLIP_FLAG],\n ['partial', WRAP_PARTIAL_FLAG],\n ['partialRight', WRAP_PARTIAL_RIGHT_FLAG],\n ['rearg', WRAP_REARG_FLAG]\n ];\n\n /** `Object#toString` result references. */\n var argsTag = '[object Arguments]',\n arrayTag = '[object Array]',\n asyncTag = '[object AsyncFunction]',\n boolTag = '[object Boolean]',\n dateTag = '[object Date]',\n domExcTag = '[object DOMException]',\n errorTag = '[object Error]',\n funcTag = '[object Function]',\n genTag = '[object GeneratorFunction]',\n mapTag = '[object Map]',\n numberTag = '[object Number]',\n nullTag = '[object Null]',\n objectTag = '[object Object]',\n promiseTag = '[object Promise]',\n proxyTag = '[object Proxy]',\n regexpTag = '[object RegExp]',\n setTag = '[object Set]',\n stringTag = '[object String]',\n symbolTag = '[object Symbol]',\n undefinedTag = '[object Undefined]',\n weakMapTag = '[object WeakMap]',\n weakSetTag = '[object WeakSet]';\n\n var arrayBufferTag = '[object ArrayBuffer]',\n dataViewTag = '[object DataView]',\n float32Tag = '[object Float32Array]',\n float64Tag = '[object Float64Array]',\n int8Tag = '[object Int8Array]',\n int16Tag = '[object Int16Array]',\n int32Tag = '[object Int32Array]',\n uint8Tag = '[object Uint8Array]',\n uint8ClampedTag = '[object Uint8ClampedArray]',\n uint16Tag = '[object Uint16Array]',\n uint32Tag = '[object Uint32Array]';\n\n /** Used to match empty string literals in compiled template source. */\n var reEmptyStringLeading = /\\b__p \\+= '';/g,\n reEmptyStringMiddle = /\\b(__p \\+=) '' \\+/g,\n reEmptyStringTrailing = /(__e\\(.*?\\)|\\b__t\\)) \\+\\n'';/g;\n\n /** Used to match HTML entities and HTML characters. */\n var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g,\n reUnescapedHtml = /[&<>\"']/g,\n reHasEscapedHtml = RegExp(reEscapedHtml.source),\n reHasUnescapedHtml = RegExp(reUnescapedHtml.source);\n\n /** Used to match template delimiters. */\n var reEscape = /<%-([\\s\\S]+?)%>/g,\n reEvaluate = /<%([\\s\\S]+?)%>/g,\n reInterpolate = /<%=([\\s\\S]+?)%>/g;\n\n /** Used to match property names within property paths. */\n var reIsDeepProp = /\\.|\\[(?:[^[\\]]*|([\"'])(?:(?!\\1)[^\\\\]|\\\\.)*?\\1)\\]/,\n reIsPlainProp = /^\\w*$/,\n rePropName = /[^.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))/g;\n\n /**\n * Used to match `RegExp`\n * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).\n */\n var reRegExpChar = /[\\\\^$.*+?()[\\]{}|]/g,\n reHasRegExpChar = RegExp(reRegExpChar.source);\n\n /** Used to match leading and trailing whitespace. */\n var reTrim = /^\\s+|\\s+$/g,\n reTrimStart = /^\\s+/,\n reTrimEnd = /\\s+$/;\n\n /** Used to match wrap detail comments. */\n var reWrapComment = /\\{(?:\\n\\/\\* \\[wrapped with .+\\] \\*\\/)?\\n?/,\n reWrapDetails = /\\{\\n\\/\\* \\[wrapped with (.+)\\] \\*/,\n reSplitDetails = /,? & /;\n\n /** Used to match words composed of alphanumeric characters. */\n var reAsciiWord = /[^\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7f]+/g;\n\n /** Used to match backslashes in property paths. */\n var reEscapeChar = /\\\\(\\\\)?/g;\n\n /**\n * Used to match\n * [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components).\n */\n var reEsTemplate = /\\$\\{([^\\\\}]*(?:\\\\.[^\\\\}]*)*)\\}/g;\n\n /** Used to match `RegExp` flags from their coerced string values. */\n var reFlags = /\\w*$/;\n\n /** Used to detect bad signed hexadecimal string values. */\n var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;\n\n /** Used to detect binary string values. */\n var reIsBinary = /^0b[01]+$/i;\n\n /** Used to detect host constructors (Safari). */\n var reIsHostCtor = /^\\[object .+?Constructor\\]$/;\n\n /** Used to detect octal string values. */\n var reIsOctal = /^0o[0-7]+$/i;\n\n /** Used to detect unsigned integer values. */\n var reIsUint = /^(?:0|[1-9]\\d*)$/;\n\n /** Used to match Latin Unicode letters (excluding mathematical operators). */\n var reLatin = /[\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\xff\\u0100-\\u017f]/g;\n\n /** Used to ensure capturing order of template delimiters. */\n var reNoMatch = /($^)/;\n\n /** Used to match unescaped characters in compiled string literals. */\n var reUnescapedString = /['\\n\\r\\u2028\\u2029\\\\]/g;\n\n /** Used to compose unicode character classes. */\n var rsAstralRange = '\\\\ud800-\\\\udfff',\n rsComboMarksRange = '\\\\u0300-\\\\u036f',\n reComboHalfMarksRange = '\\\\ufe20-\\\\ufe2f',\n rsComboSymbolsRange = '\\\\u20d0-\\\\u20ff',\n rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange,\n rsDingbatRange = '\\\\u2700-\\\\u27bf',\n rsLowerRange = 'a-z\\\\xdf-\\\\xf6\\\\xf8-\\\\xff',\n rsMathOpRange = '\\\\xac\\\\xb1\\\\xd7\\\\xf7',\n rsNonCharRange = '\\\\x00-\\\\x2f\\\\x3a-\\\\x40\\\\x5b-\\\\x60\\\\x7b-\\\\xbf',\n rsPunctuationRange = '\\\\u2000-\\\\u206f',\n rsSpaceRange = ' \\\\t\\\\x0b\\\\f\\\\xa0\\\\ufeff\\\\n\\\\r\\\\u2028\\\\u2029\\\\u1680\\\\u180e\\\\u2000\\\\u2001\\\\u2002\\\\u2003\\\\u2004\\\\u2005\\\\u2006\\\\u2007\\\\u2008\\\\u2009\\\\u200a\\\\u202f\\\\u205f\\\\u3000',\n rsUpperRange = 'A-Z\\\\xc0-\\\\xd6\\\\xd8-\\\\xde',\n rsVarRange = '\\\\ufe0e\\\\ufe0f',\n rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange;\n\n /** Used to compose unicode capture groups. */\n var rsApos = \"['\\u2019]\",\n rsAstral = '[' + rsAstralRange + ']',\n rsBreak = '[' + rsBreakRange + ']',\n rsCombo = '[' + rsComboRange + ']',\n rsDigits = '\\\\d+',\n rsDingbat = '[' + rsDingbatRange + ']',\n rsLower = '[' + rsLowerRange + ']',\n rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']',\n rsFitz = '\\\\ud83c[\\\\udffb-\\\\udfff]',\n rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')',\n rsNonAstral = '[^' + rsAstralRange + ']',\n rsRegional = '(?:\\\\ud83c[\\\\udde6-\\\\uddff]){2}',\n rsSurrPair = '[\\\\ud800-\\\\udbff][\\\\udc00-\\\\udfff]',\n rsUpper = '[' + rsUpperRange + ']',\n rsZWJ = '\\\\u200d';\n\n /** Used to compose unicode regexes. */\n var rsMiscLower = '(?:' + rsLower + '|' + rsMisc + ')',\n rsMiscUpper = '(?:' + rsUpper + '|' + rsMisc + ')',\n rsOptContrLower = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?',\n rsOptContrUpper = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?',\n reOptMod = rsModifier + '?',\n rsOptVar = '[' + rsVarRange + ']?',\n rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*',\n rsOrdLower = '\\\\d*(?:1st|2nd|3rd|(?![123])\\\\dth)(?=\\\\b|[A-Z_])',\n rsOrdUpper = '\\\\d*(?:1ST|2ND|3RD|(?![123])\\\\dTH)(?=\\\\b|[a-z_])',\n rsSeq = rsOptVar + reOptMod + rsOptJoin,\n rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq,\n rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';\n\n /** Used to match apostrophes. */\n var reApos = RegExp(rsApos, 'g');\n\n /**\n * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and\n * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols).\n */\n var reComboMark = RegExp(rsCombo, 'g');\n\n /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */\n var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');\n\n /** Used to match complex or compound words. */\n var reUnicodeWord = RegExp([\n rsUpper + '?' + rsLower + '+' + rsOptContrLower + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')',\n rsMiscUpper + '+' + rsOptContrUpper + '(?=' + [rsBreak, rsUpper + rsMiscLower, '$'].join('|') + ')',\n rsUpper + '?' + rsMiscLower + '+' + rsOptContrLower,\n rsUpper + '+' + rsOptContrUpper,\n rsOrdUpper,\n rsOrdLower,\n rsDigits,\n rsEmoji\n ].join('|'), 'g');\n\n /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */\n var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']');\n\n /** Used to detect strings that need a more robust regexp to match words. */\n var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;\n\n /** Used to assign default `context` object properties. */\n var contextProps = [\n 'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array',\n 'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object',\n 'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array',\n 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap',\n '_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout'\n ];\n\n /** Used to make template sourceURLs easier to identify. */\n var templateCounter = -1;\n\n /** Used to identify `toStringTag` values of typed arrays. */\n var typedArrayTags = {};\n typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =\n typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =\n typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =\n typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =\n typedArrayTags[uint32Tag] = true;\n typedArrayTags[argsTag] = typedArrayTags[arrayTag] =\n typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =\n typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =\n typedArrayTags[errorTag] = typedArrayTags[funcTag] =\n typedArrayTags[mapTag] = typedArrayTags[numberTag] =\n typedArrayTags[objectTag] = typedArrayTags[regexpTag] =\n typedArrayTags[setTag] = typedArrayTags[stringTag] =\n typedArrayTags[weakMapTag] = false;\n\n /** Used to identify `toStringTag` values supported by `_.clone`. */\n var cloneableTags = {};\n cloneableTags[argsTag] = cloneableTags[arrayTag] =\n cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =\n cloneableTags[boolTag] = cloneableTags[dateTag] =\n cloneableTags[float32Tag] = cloneableTags[float64Tag] =\n cloneableTags[int8Tag] = cloneableTags[int16Tag] =\n cloneableTags[int32Tag] = cloneableTags[mapTag] =\n cloneableTags[numberTag] = cloneableTags[objectTag] =\n cloneableTags[regexpTag] = cloneableTags[setTag] =\n cloneableTags[stringTag] = cloneableTags[symbolTag] =\n cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =\n cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true;\n cloneableTags[errorTag] = cloneableTags[funcTag] =\n cloneableTags[weakMapTag] = false;\n\n /** Used to map Latin Unicode letters to basic Latin letters. */\n var deburredLetters = {\n // Latin-1 Supplement block.\n '\\xc0': 'A', '\\xc1': 'A', '\\xc2': 'A', '\\xc3': 'A', '\\xc4': 'A', '\\xc5': 'A',\n '\\xe0': 'a', '\\xe1': 'a', '\\xe2': 'a', '\\xe3': 'a', '\\xe4': 'a', '\\xe5': 'a',\n '\\xc7': 'C', '\\xe7': 'c',\n '\\xd0': 'D', '\\xf0': 'd',\n '\\xc8': 'E', '\\xc9': 'E', '\\xca': 'E', '\\xcb': 'E',\n '\\xe8': 'e', '\\xe9': 'e', '\\xea': 'e', '\\xeb': 'e',\n '\\xcc': 'I', '\\xcd': 'I', '\\xce': 'I', '\\xcf': 'I',\n '\\xec': 'i', '\\xed': 'i', '\\xee': 'i', '\\xef': 'i',\n '\\xd1': 'N', '\\xf1': 'n',\n '\\xd2': 'O', '\\xd3': 'O', '\\xd4': 'O', '\\xd5': 'O', '\\xd6': 'O', '\\xd8': 'O',\n '\\xf2': 'o', '\\xf3': 'o', '\\xf4': 'o', '\\xf5': 'o', '\\xf6': 'o', '\\xf8': 'o',\n '\\xd9': 'U', '\\xda': 'U', '\\xdb': 'U', '\\xdc': 'U',\n '\\xf9': 'u', '\\xfa': 'u', '\\xfb': 'u', '\\xfc': 'u',\n '\\xdd': 'Y', '\\xfd': 'y', '\\xff': 'y',\n '\\xc6': 'Ae', '\\xe6': 'ae',\n '\\xde': 'Th', '\\xfe': 'th',\n '\\xdf': 'ss',\n // Latin Extended-A block.\n '\\u0100': 'A', '\\u0102': 'A', '\\u0104': 'A',\n '\\u0101': 'a', '\\u0103': 'a', '\\u0105': 'a',\n '\\u0106': 'C', '\\u0108': 'C', '\\u010a': 'C', '\\u010c': 'C',\n '\\u0107': 'c', '\\u0109': 'c', '\\u010b': 'c', '\\u010d': 'c',\n '\\u010e': 'D', '\\u0110': 'D', '\\u010f': 'd', '\\u0111': 'd',\n '\\u0112': 'E', '\\u0114': 'E', '\\u0116': 'E', '\\u0118': 'E', '\\u011a': 'E',\n '\\u0113': 'e', '\\u0115': 'e', '\\u0117': 'e', '\\u0119': 'e', '\\u011b': 'e',\n '\\u011c': 'G', '\\u011e': 'G', '\\u0120': 'G', '\\u0122': 'G',\n '\\u011d': 'g', '\\u011f': 'g', '\\u0121': 'g', '\\u0123': 'g',\n '\\u0124': 'H', '\\u0126': 'H', '\\u0125': 'h', '\\u0127': 'h',\n '\\u0128': 'I', '\\u012a': 'I', '\\u012c': 'I', '\\u012e': 'I', '\\u0130': 'I',\n '\\u0129': 'i', '\\u012b': 'i', '\\u012d': 'i', '\\u012f': 'i', '\\u0131': 'i',\n '\\u0134': 'J', '\\u0135': 'j',\n '\\u0136': 'K', '\\u0137': 'k', '\\u0138': 'k',\n '\\u0139': 'L', '\\u013b': 'L', '\\u013d': 'L', '\\u013f': 'L', '\\u0141': 'L',\n '\\u013a': 'l', '\\u013c': 'l', '\\u013e': 'l', '\\u0140': 'l', '\\u0142': 'l',\n '\\u0143': 'N', '\\u0145': 'N', '\\u0147': 'N', '\\u014a': 'N',\n '\\u0144': 'n', '\\u0146': 'n', '\\u0148': 'n', '\\u014b': 'n',\n '\\u014c': 'O', '\\u014e': 'O', '\\u0150': 'O',\n '\\u014d': 'o', '\\u014f': 'o', '\\u0151': 'o',\n '\\u0154': 'R', '\\u0156': 'R', '\\u0158': 'R',\n '\\u0155': 'r', '\\u0157': 'r', '\\u0159': 'r',\n '\\u015a': 'S', '\\u015c': 'S', '\\u015e': 'S', '\\u0160': 'S',\n '\\u015b': 's', '\\u015d': 's', '\\u015f': 's', '\\u0161': 's',\n '\\u0162': 'T', '\\u0164': 'T', '\\u0166': 'T',\n '\\u0163': 't', '\\u0165': 't', '\\u0167': 't',\n '\\u0168': 'U', '\\u016a': 'U', '\\u016c': 'U', '\\u016e': 'U', '\\u0170': 'U', '\\u0172': 'U',\n '\\u0169': 'u', '\\u016b': 'u', '\\u016d': 'u', '\\u016f': 'u', '\\u0171': 'u', '\\u0173': 'u',\n '\\u0174': 'W', '\\u0175': 'w',\n '\\u0176': 'Y', '\\u0177': 'y', '\\u0178': 'Y',\n '\\u0179': 'Z', '\\u017b': 'Z', '\\u017d': 'Z',\n '\\u017a': 'z', '\\u017c': 'z', '\\u017e': 'z',\n '\\u0132': 'IJ', '\\u0133': 'ij',\n '\\u0152': 'Oe', '\\u0153': 'oe',\n '\\u0149': \"'n\", '\\u017f': 's'\n };\n\n /** Used to map characters to HTML entities. */\n var htmlEscapes = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n\n /** Used to map HTML entities to characters. */\n var htmlUnescapes = {\n '&': '&',\n '<': '<',\n '>': '>',\n '"': '\"',\n ''': \"'\"\n };\n\n /** Used to escape characters for inclusion in compiled string literals. */\n var stringEscapes = {\n '\\\\': '\\\\',\n \"'\": \"'\",\n '\\n': 'n',\n '\\r': 'r',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n /** Built-in method references without a dependency on `root`. */\n var freeParseFloat = parseFloat,\n freeParseInt = parseInt;\n\n /** Detect free variable `global` from Node.js. */\n var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;\n\n /** Detect free variable `self`. */\n var freeSelf = typeof self == 'object' && self && self.Object === Object && self;\n\n /** Used as a reference to the global object. */\n var root = freeGlobal || freeSelf || Function('return this')();\n\n /** Detect free variable `exports`. */\n var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;\n\n /** Detect free variable `module`. */\n var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;\n\n /** Detect the popular CommonJS extension `module.exports`. */\n var moduleExports = freeModule && freeModule.exports === freeExports;\n\n /** Detect free variable `process` from Node.js. */\n var freeProcess = moduleExports && freeGlobal.process;\n\n /** Used to access faster Node.js helpers. */\n var nodeUtil = (function() {\n try {\n // Use `util.types` for Node.js 10+.\n var types = freeModule && freeModule.require && freeModule.require('util').types;\n\n if (types) {\n return types;\n }\n\n // Legacy `process.binding('util')` for Node.js < 10.\n return freeProcess && freeProcess.binding && freeProcess.binding('util');\n } catch (e) {}\n }());\n\n /* Node.js helper references. */\n var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer,\n nodeIsDate = nodeUtil && nodeUtil.isDate,\n nodeIsMap = nodeUtil && nodeUtil.isMap,\n nodeIsRegExp = nodeUtil && nodeUtil.isRegExp,\n nodeIsSet = nodeUtil && nodeUtil.isSet,\n nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;\n\n /*--------------------------------------------------------------------------*/\n\n /**\n * A faster alternative to `Function#apply`, this function invokes `func`\n * with the `this` binding of `thisArg` and the arguments of `args`.\n *\n * @private\n * @param {Function} func The function to invoke.\n * @param {*} thisArg The `this` binding of `func`.\n * @param {Array} args The arguments to invoke `func` with.\n * @returns {*} Returns the result of `func`.\n */\n function apply(func, thisArg, args) {\n switch (args.length) {\n case 0: return func.call(thisArg);\n case 1: return func.call(thisArg, args[0]);\n case 2: return func.call(thisArg, args[0], args[1]);\n case 3: return func.call(thisArg, args[0], args[1], args[2]);\n }\n return func.apply(thisArg, args);\n }\n\n /**\n * A specialized version of `baseAggregator` for arrays.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} setter The function to set `accumulator` values.\n * @param {Function} iteratee The iteratee to transform keys.\n * @param {Object} accumulator The initial aggregated object.\n * @returns {Function} Returns `accumulator`.\n */\n function arrayAggregator(array, setter, iteratee, accumulator) {\n var index = -1,\n length = array == null ? 0 : array.length;\n\n while (++index < length) {\n var value = array[index];\n setter(accumulator, value, iteratee(value), array);\n }\n return accumulator;\n }\n\n /**\n * A specialized version of `_.forEach` for arrays without support for\n * iteratee shorthands.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Array} Returns `array`.\n */\n function arrayEach(array, iteratee) {\n var index = -1,\n length = array == null ? 0 : array.length;\n\n while (++index < length) {\n if (iteratee(array[index], index, array) === false) {\n break;\n }\n }\n return array;\n }\n\n /**\n * A specialized version of `_.forEachRight` for arrays without support for\n * iteratee shorthands.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Array} Returns `array`.\n */\n function arrayEachRight(array, iteratee) {\n var length = array == null ? 0 : array.length;\n\n while (length--) {\n if (iteratee(array[length], length, array) === false) {\n break;\n }\n }\n return array;\n }\n\n /**\n * A specialized version of `_.every` for arrays without support for\n * iteratee shorthands.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} predicate The function invoked per iteration.\n * @returns {boolean} Returns `true` if all elements pass the predicate check,\n * else `false`.\n */\n function arrayEvery(array, predicate) {\n var index = -1,\n length = array == null ? 0 : array.length;\n\n while (++index < length) {\n if (!predicate(array[index], index, array)) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * A specialized version of `_.filter` for arrays without support for\n * iteratee shorthands.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} predicate The function invoked per iteration.\n * @returns {Array} Returns the new filtered array.\n */\n function arrayFilter(array, predicate) {\n var index = -1,\n length = array == null ? 0 : array.length,\n resIndex = 0,\n result = [];\n\n while (++index < length) {\n var value = array[index];\n if (predicate(value, index, array)) {\n result[resIndex++] = value;\n }\n }\n return result;\n }\n\n /**\n * A specialized version of `_.includes` for arrays without support for\n * specifying an index to search from.\n *\n * @private\n * @param {Array} [array] The array to inspect.\n * @param {*} target The value to search for.\n * @returns {boolean} Returns `true` if `target` is found, else `false`.\n */\n function arrayIncludes(array, value) {\n var length = array == null ? 0 : array.length;\n return !!length && baseIndexOf(array, value, 0) > -1;\n }\n\n /**\n * This function is like `arrayIncludes` except that it accepts a comparator.\n *\n * @private\n * @param {Array} [array] The array to inspect.\n * @param {*} target The value to search for.\n * @param {Function} comparator The comparator invoked per element.\n * @returns {boolean} Returns `true` if `target` is found, else `false`.\n */\n function arrayIncludesWith(array, value, comparator) {\n var index = -1,\n length = array == null ? 0 : array.length;\n\n while (++index < length) {\n if (comparator(value, array[index])) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * A specialized version of `_.map` for arrays without support for iteratee\n * shorthands.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Array} Returns the new mapped array.\n */\n function arrayMap(array, iteratee) {\n var index = -1,\n length = array == null ? 0 : array.length,\n result = Array(length);\n\n while (++index < length) {\n result[index] = iteratee(array[index], index, array);\n }\n return result;\n }\n\n /**\n * Appends the elements of `values` to `array`.\n *\n * @private\n * @param {Array} array The array to modify.\n * @param {Array} values The values to append.\n * @returns {Array} Returns `array`.\n */\n function arrayPush(array, values) {\n var index = -1,\n length = values.length,\n offset = array.length;\n\n while (++index < length) {\n array[offset + index] = values[index];\n }\n return array;\n }\n\n /**\n * A specialized version of `_.reduce` for arrays without support for\n * iteratee shorthands.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @param {*} [accumulator] The initial value.\n * @param {boolean} [initAccum] Specify using the first element of `array` as\n * the initial value.\n * @returns {*} Returns the accumulated value.\n */\n function arrayReduce(array, iteratee, accumulator, initAccum) {\n var index = -1,\n length = array == null ? 0 : array.length;\n\n if (initAccum && length) {\n accumulator = array[++index];\n }\n while (++index < length) {\n accumulator = iteratee(accumulator, array[index], index, array);\n }\n return accumulator;\n }\n\n /**\n * A specialized version of `_.reduceRight` for arrays without support for\n * iteratee shorthands.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @param {*} [accumulator] The initial value.\n * @param {boolean} [initAccum] Specify using the last element of `array` as\n * the initial value.\n * @returns {*} Returns the accumulated value.\n */\n function arrayReduceRight(array, iteratee, accumulator, initAccum) {\n var length = array == null ? 0 : array.length;\n if (initAccum && length) {\n accumulator = array[--length];\n }\n while (length--) {\n accumulator = iteratee(accumulator, array[length], length, array);\n }\n return accumulator;\n }\n\n /**\n * A specialized version of `_.some` for arrays without support for iteratee\n * shorthands.\n *\n * @private\n * @param {Array} [array] The array to iterate over.\n * @param {Function} predicate The function invoked per iteration.\n * @returns {boolean} Returns `true` if any element passes the predicate check,\n * else `false`.\n */\n function arraySome(array, predicate) {\n var index = -1,\n length = array == null ? 0 : array.length;\n\n while (++index < length) {\n if (predicate(array[index], index, array)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Gets the size of an ASCII `string`.\n *\n * @private\n * @param {string} string The string inspect.\n * @returns {number} Returns the string size.\n */\n var asciiSize = baseProperty('length');\n\n /**\n * Converts an ASCII `string` to an array.\n *\n * @private\n * @param {string} string The string to convert.\n * @returns {Array} Returns the converted array.\n */\n function asciiToArray(string) {\n return string.split('');\n }\n\n /**\n * Splits an ASCII `string` into an array of its words.\n *\n * @private\n * @param {string} The string to inspect.\n * @returns {Array} Returns the words of `string`.\n */\n function asciiWords(string) {\n return string.match(reAsciiWord) || [];\n }\n\n /**\n * The base implementation of methods like `_.findKey` and `_.findLastKey`,\n * without support for iteratee shorthands, which iterates over `collection`\n * using `eachFunc`.\n *\n * @private\n * @param {Array|Object} collection The collection to inspect.\n * @param {Function} predicate The function invoked per iteration.\n * @param {Function} eachFunc The function to iterate over `collection`.\n * @returns {*} Returns the found element or its key, else `undefined`.\n */\n function baseFindKey(collection, predicate, eachFunc) {\n var result;\n eachFunc(collection, function(value, key, collection) {\n if (predicate(value, key, collection)) {\n result = key;\n return false;\n }\n });\n return result;\n }\n\n /**\n * The base implementation of `_.findIndex` and `_.findLastIndex` without\n * support for iteratee shorthands.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {Function} predicate The function invoked per iteration.\n * @param {number} fromIndex The index to search from.\n * @param {boolean} [fromRight] Specify iterating from right to left.\n * @returns {number} Returns the index of the matched value, else `-1`.\n */\n function baseFindIndex(array, predicate, fromIndex, fromRight) {\n var length = array.length,\n index = fromIndex + (fromRight ? 1 : -1);\n\n while ((fromRight ? index-- : ++index < length)) {\n if (predicate(array[index], index, array)) {\n return index;\n }\n }\n return -1;\n }\n\n /**\n * The base implementation of `_.indexOf` without `fromIndex` bounds checks.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {*} value The value to search for.\n * @param {number} fromIndex The index to search from.\n * @returns {number} Returns the index of the matched value, else `-1`.\n */\n function baseIndexOf(array, value, fromIndex) {\n return value === value\n ? strictIndexOf(array, value, fromIndex)\n : baseFindIndex(array, baseIsNaN, fromIndex);\n }\n\n /**\n * This function is like `baseIndexOf` except that it accepts a comparator.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {*} value The value to search for.\n * @param {number} fromIndex The index to search from.\n * @param {Function} comparator The comparator invoked per element.\n * @returns {number} Returns the index of the matched value, else `-1`.\n */\n function baseIndexOfWith(array, value, fromIndex, comparator) {\n var index = fromIndex - 1,\n length = array.length;\n\n while (++index < length) {\n if (comparator(array[index], value)) {\n return index;\n }\n }\n return -1;\n }\n\n /**\n * The base implementation of `_.isNaN` without support for number objects.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.\n */\n function baseIsNaN(value) {\n return value !== value;\n }\n\n /**\n * The base implementation of `_.mean` and `_.meanBy` without support for\n * iteratee shorthands.\n *\n * @private\n * @param {Array} array The array to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {number} Returns the mean.\n */\n function baseMean(array, iteratee) {\n var length = array == null ? 0 : array.length;\n return length ? (baseSum(array, iteratee) / length) : NAN;\n }\n\n /**\n * The base implementation of `_.property` without support for deep paths.\n *\n * @private\n * @param {string} key The key of the property to get.\n * @returns {Function} Returns the new accessor function.\n */\n function baseProperty(key) {\n return function(object) {\n return object == null ? undefined : object[key];\n };\n }\n\n /**\n * The base implementation of `_.propertyOf` without support for deep paths.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Function} Returns the new accessor function.\n */\n function basePropertyOf(object) {\n return function(key) {\n return object == null ? undefined : object[key];\n };\n }\n\n /**\n * The base implementation of `_.reduce` and `_.reduceRight`, without support\n * for iteratee shorthands, which iterates over `collection` using `eachFunc`.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @param {*} accumulator The initial value.\n * @param {boolean} initAccum Specify using the first or last element of\n * `collection` as the initial value.\n * @param {Function} eachFunc The function to iterate over `collection`.\n * @returns {*} Returns the accumulated value.\n */\n function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) {\n eachFunc(collection, function(value, index, collection) {\n accumulator = initAccum\n ? (initAccum = false, value)\n : iteratee(accumulator, value, index, collection);\n });\n return accumulator;\n }\n\n /**\n * The base implementation of `_.sortBy` which uses `comparer` to define the\n * sort order of `array` and replaces criteria objects with their corresponding\n * values.\n *\n * @private\n * @param {Array} array The array to sort.\n * @param {Function} comparer The function to define sort order.\n * @returns {Array} Returns `array`.\n */\n function baseSortBy(array, comparer) {\n var length = array.length;\n\n array.sort(comparer);\n while (length--) {\n array[length] = array[length].value;\n }\n return array;\n }\n\n /**\n * The base implementation of `_.sum` and `_.sumBy` without support for\n * iteratee shorthands.\n *\n * @private\n * @param {Array} array The array to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {number} Returns the sum.\n */\n function baseSum(array, iteratee) {\n var result,\n index = -1,\n length = array.length;\n\n while (++index < length) {\n var current = iteratee(array[index]);\n if (current !== undefined) {\n result = result === undefined ? current : (result + current);\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `_.times` without support for iteratee shorthands\n * or max array length checks.\n *\n * @private\n * @param {number} n The number of times to invoke `iteratee`.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Array} Returns the array of results.\n */\n function baseTimes(n, iteratee) {\n var index = -1,\n result = Array(n);\n\n while (++index < n) {\n result[index] = iteratee(index);\n }\n return result;\n }\n\n /**\n * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array\n * of key-value pairs for `object` corresponding to the property names of `props`.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Array} props The property names to get values for.\n * @returns {Object} Returns the key-value pairs.\n */\n function baseToPairs(object, props) {\n return arrayMap(props, function(key) {\n return [key, object[key]];\n });\n }\n\n /**\n * The base implementation of `_.unary` without support for storing metadata.\n *\n * @private\n * @param {Function} func The function to cap arguments for.\n * @returns {Function} Returns the new capped function.\n */\n function baseUnary(func) {\n return function(value) {\n return func(value);\n };\n }\n\n /**\n * The base implementation of `_.values` and `_.valuesIn` which creates an\n * array of `object` property values corresponding to the property names\n * of `props`.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Array} props The property names to get values for.\n * @returns {Object} Returns the array of property values.\n */\n function baseValues(object, props) {\n return arrayMap(props, function(key) {\n return object[key];\n });\n }\n\n /**\n * Checks if a `cache` value for `key` exists.\n *\n * @private\n * @param {Object} cache The cache to query.\n * @param {string} key The key of the entry to check.\n * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n */\n function cacheHas(cache, key) {\n return cache.has(key);\n }\n\n /**\n * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol\n * that is not found in the character symbols.\n *\n * @private\n * @param {Array} strSymbols The string symbols to inspect.\n * @param {Array} chrSymbols The character symbols to find.\n * @returns {number} Returns the index of the first unmatched string symbol.\n */\n function charsStartIndex(strSymbols, chrSymbols) {\n var index = -1,\n length = strSymbols.length;\n\n while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}\n return index;\n }\n\n /**\n * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol\n * that is not found in the character symbols.\n *\n * @private\n * @param {Array} strSymbols The string symbols to inspect.\n * @param {Array} chrSymbols The character symbols to find.\n * @returns {number} Returns the index of the last unmatched string symbol.\n */\n function charsEndIndex(strSymbols, chrSymbols) {\n var index = strSymbols.length;\n\n while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}\n return index;\n }\n\n /**\n * Gets the number of `placeholder` occurrences in `array`.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {*} placeholder The placeholder to search for.\n * @returns {number} Returns the placeholder count.\n */\n function countHolders(array, placeholder) {\n var length = array.length,\n result = 0;\n\n while (length--) {\n if (array[length] === placeholder) {\n ++result;\n }\n }\n return result;\n }\n\n /**\n * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A\n * letters to basic Latin letters.\n *\n * @private\n * @param {string} letter The matched letter to deburr.\n * @returns {string} Returns the deburred letter.\n */\n var deburrLetter = basePropertyOf(deburredLetters);\n\n /**\n * Used by `_.escape` to convert characters to HTML entities.\n *\n * @private\n * @param {string} chr The matched character to escape.\n * @returns {string} Returns the escaped character.\n */\n var escapeHtmlChar = basePropertyOf(htmlEscapes);\n\n /**\n * Used by `_.template` to escape characters for inclusion in compiled string literals.\n *\n * @private\n * @param {string} chr The matched character to escape.\n * @returns {string} Returns the escaped character.\n */\n function escapeStringChar(chr) {\n return '\\\\' + stringEscapes[chr];\n }\n\n /**\n * Gets the value at `key` of `object`.\n *\n * @private\n * @param {Object} [object] The object to query.\n * @param {string} key The key of the property to get.\n * @returns {*} Returns the property value.\n */\n function getValue(object, key) {\n return object == null ? undefined : object[key];\n }\n\n /**\n * Checks if `string` contains Unicode symbols.\n *\n * @private\n * @param {string} string The string to inspect.\n * @returns {boolean} Returns `true` if a symbol is found, else `false`.\n */\n function hasUnicode(string) {\n return reHasUnicode.test(string);\n }\n\n /**\n * Checks if `string` contains a word composed of Unicode symbols.\n *\n * @private\n * @param {string} string The string to inspect.\n * @returns {boolean} Returns `true` if a word is found, else `false`.\n */\n function hasUnicodeWord(string) {\n return reHasUnicodeWord.test(string);\n }\n\n /**\n * Converts `iterator` to an array.\n *\n * @private\n * @param {Object} iterator The iterator to convert.\n * @returns {Array} Returns the converted array.\n */\n function iteratorToArray(iterator) {\n var data,\n result = [];\n\n while (!(data = iterator.next()).done) {\n result.push(data.value);\n }\n return result;\n }\n\n /**\n * Converts `map` to its key-value pairs.\n *\n * @private\n * @param {Object} map The map to convert.\n * @returns {Array} Returns the key-value pairs.\n */\n function mapToArray(map) {\n var index = -1,\n result = Array(map.size);\n\n map.forEach(function(value, key) {\n result[++index] = [key, value];\n });\n return result;\n }\n\n /**\n * Creates a unary function that invokes `func` with its argument transformed.\n *\n * @private\n * @param {Function} func The function to wrap.\n * @param {Function} transform The argument transform.\n * @returns {Function} Returns the new function.\n */\n function overArg(func, transform) {\n return function(arg) {\n return func(transform(arg));\n };\n }\n\n /**\n * Replaces all `placeholder` elements in `array` with an internal placeholder\n * and returns an array of their indexes.\n *\n * @private\n * @param {Array} array The array to modify.\n * @param {*} placeholder The placeholder to replace.\n * @returns {Array} Returns the new array of placeholder indexes.\n */\n function replaceHolders(array, placeholder) {\n var index = -1,\n length = array.length,\n resIndex = 0,\n result = [];\n\n while (++index < length) {\n var value = array[index];\n if (value === placeholder || value === PLACEHOLDER) {\n array[index] = PLACEHOLDER;\n result[resIndex++] = index;\n }\n }\n return result;\n }\n\n /**\n * Gets the value at `key`, unless `key` is \"__proto__\".\n *\n * @private\n * @param {Object} object The object to query.\n * @param {string} key The key of the property to get.\n * @returns {*} Returns the property value.\n */\n function safeGet(object, key) {\n return key == '__proto__'\n ? undefined\n : object[key];\n }\n\n /**\n * Converts `set` to an array of its values.\n *\n * @private\n * @param {Object} set The set to convert.\n * @returns {Array} Returns the values.\n */\n function setToArray(set) {\n var index = -1,\n result = Array(set.size);\n\n set.forEach(function(value) {\n result[++index] = value;\n });\n return result;\n }\n\n /**\n * Converts `set` to its value-value pairs.\n *\n * @private\n * @param {Object} set The set to convert.\n * @returns {Array} Returns the value-value pairs.\n */\n function setToPairs(set) {\n var index = -1,\n result = Array(set.size);\n\n set.forEach(function(value) {\n result[++index] = [value, value];\n });\n return result;\n }\n\n /**\n * A specialized version of `_.indexOf` which performs strict equality\n * comparisons of values, i.e. `===`.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {*} value The value to search for.\n * @param {number} fromIndex The index to search from.\n * @returns {number} Returns the index of the matched value, else `-1`.\n */\n function strictIndexOf(array, value, fromIndex) {\n var index = fromIndex - 1,\n length = array.length;\n\n while (++index < length) {\n if (array[index] === value) {\n return index;\n }\n }\n return -1;\n }\n\n /**\n * A specialized version of `_.lastIndexOf` which performs strict equality\n * comparisons of values, i.e. `===`.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {*} value The value to search for.\n * @param {number} fromIndex The index to search from.\n * @returns {number} Returns the index of the matched value, else `-1`.\n */\n function strictLastIndexOf(array, value, fromIndex) {\n var index = fromIndex + 1;\n while (index--) {\n if (array[index] === value) {\n return index;\n }\n }\n return index;\n }\n\n /**\n * Gets the number of symbols in `string`.\n *\n * @private\n * @param {string} string The string to inspect.\n * @returns {number} Returns the string size.\n */\n function stringSize(string) {\n return hasUnicode(string)\n ? unicodeSize(string)\n : asciiSize(string);\n }\n\n /**\n * Converts `string` to an array.\n *\n * @private\n * @param {string} string The string to convert.\n * @returns {Array} Returns the converted array.\n */\n function stringToArray(string) {\n return hasUnicode(string)\n ? unicodeToArray(string)\n : asciiToArray(string);\n }\n\n /**\n * Used by `_.unescape` to convert HTML entities to characters.\n *\n * @private\n * @param {string} chr The matched character to unescape.\n * @returns {string} Returns the unescaped character.\n */\n var unescapeHtmlChar = basePropertyOf(htmlUnescapes);\n\n /**\n * Gets the size of a Unicode `string`.\n *\n * @private\n * @param {string} string The string inspect.\n * @returns {number} Returns the string size.\n */\n function unicodeSize(string) {\n var result = reUnicode.lastIndex = 0;\n while (reUnicode.test(string)) {\n ++result;\n }\n return result;\n }\n\n /**\n * Converts a Unicode `string` to an array.\n *\n * @private\n * @param {string} string The string to convert.\n * @returns {Array} Returns the converted array.\n */\n function unicodeToArray(string) {\n return string.match(reUnicode) || [];\n }\n\n /**\n * Splits a Unicode `string` into an array of its words.\n *\n * @private\n * @param {string} The string to inspect.\n * @returns {Array} Returns the words of `string`.\n */\n function unicodeWords(string) {\n return string.match(reUnicodeWord) || [];\n }\n\n /*--------------------------------------------------------------------------*/\n\n /**\n * Create a new pristine `lodash` function using the `context` object.\n *\n * @static\n * @memberOf _\n * @since 1.1.0\n * @category Util\n * @param {Object} [context=root] The context object.\n * @returns {Function} Returns a new `lodash` function.\n * @example\n *\n * _.mixin({ 'foo': _.constant('foo') });\n *\n * var lodash = _.runInContext();\n * lodash.mixin({ 'bar': lodash.constant('bar') });\n *\n * _.isFunction(_.foo);\n * // => true\n * _.isFunction(_.bar);\n * // => false\n *\n * lodash.isFunction(lodash.foo);\n * // => false\n * lodash.isFunction(lodash.bar);\n * // => true\n *\n * // Create a suped-up `defer` in Node.js.\n * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer;\n */\n var runInContext = (function runInContext(context) {\n context = context == null ? root : _.defaults(root.Object(), context, _.pick(root, contextProps));\n\n /** Built-in constructor references. */\n var Array = context.Array,\n Date = context.Date,\n Error = context.Error,\n Function = context.Function,\n Math = context.Math,\n Object = context.Object,\n RegExp = context.RegExp,\n String = context.String,\n TypeError = context.TypeError;\n\n /** Used for built-in method references. */\n var arrayProto = Array.prototype,\n funcProto = Function.prototype,\n objectProto = Object.prototype;\n\n /** Used to detect overreaching core-js shims. */\n var coreJsData = context['__core-js_shared__'];\n\n /** Used to resolve the decompiled source of functions. */\n var funcToString = funcProto.toString;\n\n /** Used to check objects for own properties. */\n var hasOwnProperty = objectProto.hasOwnProperty;\n\n /** Used to generate unique IDs. */\n var idCounter = 0;\n\n /** Used to detect methods masquerading as native. */\n var maskSrcKey = (function() {\n var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');\n return uid ? ('Symbol(src)_1.' + uid) : '';\n }());\n\n /**\n * Used to resolve the\n * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)\n * of values.\n */\n var nativeObjectToString = objectProto.toString;\n\n /** Used to infer the `Object` constructor. */\n var objectCtorString = funcToString.call(Object);\n\n /** Used to restore the original `_` reference in `_.noConflict`. */\n var oldDash = root._;\n\n /** Used to detect if a method is native. */\n var reIsNative = RegExp('^' +\n funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\\\$&')\n .replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g, '$1.*?') + '$'\n );\n\n /** Built-in value references. */\n var Buffer = moduleExports ? context.Buffer : undefined,\n Symbol = context.Symbol,\n Uint8Array = context.Uint8Array,\n allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined,\n getPrototype = overArg(Object.getPrototypeOf, Object),\n objectCreate = Object.create,\n propertyIsEnumerable = objectProto.propertyIsEnumerable,\n splice = arrayProto.splice,\n spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined,\n symIterator = Symbol ? Symbol.iterator : undefined,\n symToStringTag = Symbol ? Symbol.toStringTag : undefined;\n\n var defineProperty = (function() {\n try {\n var func = getNative(Object, 'defineProperty');\n func({}, '', {});\n return func;\n } catch (e) {}\n }());\n\n /** Mocked built-ins. */\n var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout,\n ctxNow = Date && Date.now !== root.Date.now && Date.now,\n ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout;\n\n /* Built-in method references for those with the same name as other `lodash` methods. */\n var nativeCeil = Math.ceil,\n nativeFloor = Math.floor,\n nativeGetSymbols = Object.getOwnPropertySymbols,\n nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined,\n nativeIsFinite = context.isFinite,\n nativeJoin = arrayProto.join,\n nativeKeys = overArg(Object.keys, Object),\n nativeMax = Math.max,\n nativeMin = Math.min,\n nativeNow = Date.now,\n nativeParseInt = context.parseInt,\n nativeRandom = Math.random,\n nativeReverse = arrayProto.reverse;\n\n /* Built-in method references that are verified to be native. */\n var DataView = getNative(context, 'DataView'),\n Map = getNative(context, 'Map'),\n Promise = getNative(context, 'Promise'),\n Set = getNative(context, 'Set'),\n WeakMap = getNative(context, 'WeakMap'),\n nativeCreate = getNative(Object, 'create');\n\n /** Used to store function metadata. */\n var metaMap = WeakMap && new WeakMap;\n\n /** Used to lookup unminified function names. */\n var realNames = {};\n\n /** Used to detect maps, sets, and weakmaps. */\n var dataViewCtorString = toSource(DataView),\n mapCtorString = toSource(Map),\n promiseCtorString = toSource(Promise),\n setCtorString = toSource(Set),\n weakMapCtorString = toSource(WeakMap);\n\n /** Used to convert symbols to primitives and strings. */\n var symbolProto = Symbol ? Symbol.prototype : undefined,\n symbolValueOf = symbolProto ? symbolProto.valueOf : undefined,\n symbolToString = symbolProto ? symbolProto.toString : undefined;\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates a `lodash` object which wraps `value` to enable implicit method\n * chain sequences. Methods that operate on and return arrays, collections,\n * and functions can be chained together. Methods that retrieve a single value\n * or may return a primitive value will automatically end the chain sequence\n * and return the unwrapped value. Otherwise, the value must be unwrapped\n * with `_#value`.\n *\n * Explicit chain sequences, which must be unwrapped with `_#value`, may be\n * enabled using `_.chain`.\n *\n * The execution of chained methods is lazy, that is, it's deferred until\n * `_#value` is implicitly or explicitly called.\n *\n * Lazy evaluation allows several methods to support shortcut fusion.\n * Shortcut fusion is an optimization to merge iteratee calls; this avoids\n * the creation of intermediate arrays and can greatly reduce the number of\n * iteratee executions. Sections of a chain sequence qualify for shortcut\n * fusion if the section is applied to an array and iteratees accept only\n * one argument. The heuristic for whether a section qualifies for shortcut\n * fusion is subject to change.\n *\n * Chaining is supported in custom builds as long as the `_#value` method is\n * directly or indirectly included in the build.\n *\n * In addition to lodash methods, wrappers have `Array` and `String` methods.\n *\n * The wrapper `Array` methods are:\n * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift`\n *\n * The wrapper `String` methods are:\n * `replace` and `split`\n *\n * The wrapper methods that support shortcut fusion are:\n * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`,\n * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`,\n * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray`\n *\n * The chainable wrapper methods are:\n * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`,\n * `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`,\n * `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`,\n * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`,\n * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`,\n * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`,\n * `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`,\n * `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`,\n * `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`,\n * `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`,\n * `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`,\n * `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`,\n * `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`,\n * `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`,\n * `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`,\n * `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`,\n * `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`,\n * `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`,\n * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`,\n * `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`,\n * `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`,\n * `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`,\n * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`,\n * `zipObject`, `zipObjectDeep`, and `zipWith`\n *\n * The wrapper methods that are **not** chainable by default are:\n * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`,\n * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`,\n * `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`,\n * `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`,\n * `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`,\n * `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`,\n * `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`,\n * `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`,\n * `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`,\n * `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`,\n * `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`,\n * `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`,\n * `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`,\n * `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`,\n * `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`,\n * `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`,\n * `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`,\n * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`,\n * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`,\n * `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`,\n * `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`,\n * `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`,\n * `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`,\n * `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`,\n * `upperFirst`, `value`, and `words`\n *\n * @name _\n * @constructor\n * @category Seq\n * @param {*} value The value to wrap in a `lodash` instance.\n * @returns {Object} Returns the new `lodash` wrapper instance.\n * @example\n *\n * function square(n) {\n * return n * n;\n * }\n *\n * var wrapped = _([1, 2, 3]);\n *\n * // Returns an unwrapped value.\n * wrapped.reduce(_.add);\n * // => 6\n *\n * // Returns a wrapped value.\n * var squares = wrapped.map(square);\n *\n * _.isArray(squares);\n * // => false\n *\n * _.isArray(squares.value());\n * // => true\n */\n function lodash(value) {\n if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {\n if (value instanceof LodashWrapper) {\n return value;\n }\n if (hasOwnProperty.call(value, '__wrapped__')) {\n return wrapperClone(value);\n }\n }\n return new LodashWrapper(value);\n }\n\n /**\n * The base implementation of `_.create` without support for assigning\n * properties to the created object.\n *\n * @private\n * @param {Object} proto The object to inherit from.\n * @returns {Object} Returns the new object.\n */\n var baseCreate = (function() {\n function object() {}\n return function(proto) {\n if (!isObject(proto)) {\n return {};\n }\n if (objectCreate) {\n return objectCreate(proto);\n }\n object.prototype = proto;\n var result = new object;\n object.prototype = undefined;\n return result;\n };\n }());\n\n /**\n * The function whose prototype chain sequence wrappers inherit from.\n *\n * @private\n */\n function baseLodash() {\n // No operation performed.\n }\n\n /**\n * The base constructor for creating `lodash` wrapper objects.\n *\n * @private\n * @param {*} value The value to wrap.\n * @param {boolean} [chainAll] Enable explicit method chain sequences.\n */\n function LodashWrapper(value, chainAll) {\n this.__wrapped__ = value;\n this.__actions__ = [];\n this.__chain__ = !!chainAll;\n this.__index__ = 0;\n this.__values__ = undefined;\n }\n\n /**\n * By default, the template delimiters used by lodash are like those in\n * embedded Ruby (ERB) as well as ES2015 template strings. Change the\n * following template settings to use alternative delimiters.\n *\n * @static\n * @memberOf _\n * @type {Object}\n */\n lodash.templateSettings = {\n\n /**\n * Used to detect `data` property values to be HTML-escaped.\n *\n * @memberOf _.templateSettings\n * @type {RegExp}\n */\n 'escape': reEscape,\n\n /**\n * Used to detect code to be evaluated.\n *\n * @memberOf _.templateSettings\n * @type {RegExp}\n */\n 'evaluate': reEvaluate,\n\n /**\n * Used to detect `data` property values to inject.\n *\n * @memberOf _.templateSettings\n * @type {RegExp}\n */\n 'interpolate': reInterpolate,\n\n /**\n * Used to reference the data object in the template text.\n *\n * @memberOf _.templateSettings\n * @type {string}\n */\n 'variable': '',\n\n /**\n * Used to import variables into the compiled template.\n *\n * @memberOf _.templateSettings\n * @type {Object}\n */\n 'imports': {\n\n /**\n * A reference to the `lodash` function.\n *\n * @memberOf _.templateSettings.imports\n * @type {Function}\n */\n '_': lodash\n }\n };\n\n // Ensure wrappers are instances of `baseLodash`.\n lodash.prototype = baseLodash.prototype;\n lodash.prototype.constructor = lodash;\n\n LodashWrapper.prototype = baseCreate(baseLodash.prototype);\n LodashWrapper.prototype.constructor = LodashWrapper;\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation.\n *\n * @private\n * @constructor\n * @param {*} value The value to wrap.\n */\n function LazyWrapper(value) {\n this.__wrapped__ = value;\n this.__actions__ = [];\n this.__dir__ = 1;\n this.__filtered__ = false;\n this.__iteratees__ = [];\n this.__takeCount__ = MAX_ARRAY_LENGTH;\n this.__views__ = [];\n }\n\n /**\n * Creates a clone of the lazy wrapper object.\n *\n * @private\n * @name clone\n * @memberOf LazyWrapper\n * @returns {Object} Returns the cloned `LazyWrapper` object.\n */\n function lazyClone() {\n var result = new LazyWrapper(this.__wrapped__);\n result.__actions__ = copyArray(this.__actions__);\n result.__dir__ = this.__dir__;\n result.__filtered__ = this.__filtered__;\n result.__iteratees__ = copyArray(this.__iteratees__);\n result.__takeCount__ = this.__takeCount__;\n result.__views__ = copyArray(this.__views__);\n return result;\n }\n\n /**\n * Reverses the direction of lazy iteration.\n *\n * @private\n * @name reverse\n * @memberOf LazyWrapper\n * @returns {Object} Returns the new reversed `LazyWrapper` object.\n */\n function lazyReverse() {\n if (this.__filtered__) {\n var result = new LazyWrapper(this);\n result.__dir__ = -1;\n result.__filtered__ = true;\n } else {\n result = this.clone();\n result.__dir__ *= -1;\n }\n return result;\n }\n\n /**\n * Extracts the unwrapped value from its lazy wrapper.\n *\n * @private\n * @name value\n * @memberOf LazyWrapper\n * @returns {*} Returns the unwrapped value.\n */\n function lazyValue() {\n var array = this.__wrapped__.value(),\n dir = this.__dir__,\n isArr = isArray(array),\n isRight = dir < 0,\n arrLength = isArr ? array.length : 0,\n view = getView(0, arrLength, this.__views__),\n start = view.start,\n end = view.end,\n length = end - start,\n index = isRight ? end : (start - 1),\n iteratees = this.__iteratees__,\n iterLength = iteratees.length,\n resIndex = 0,\n takeCount = nativeMin(length, this.__takeCount__);\n\n if (!isArr || (!isRight && arrLength == length && takeCount == length)) {\n return baseWrapperValue(array, this.__actions__);\n }\n var result = [];\n\n outer:\n while (length-- && resIndex < takeCount) {\n index += dir;\n\n var iterIndex = -1,\n value = array[index];\n\n while (++iterIndex < iterLength) {\n var data = iteratees[iterIndex],\n iteratee = data.iteratee,\n type = data.type,\n computed = iteratee(value);\n\n if (type == LAZY_MAP_FLAG) {\n value = computed;\n } else if (!computed) {\n if (type == LAZY_FILTER_FLAG) {\n continue outer;\n } else {\n break outer;\n }\n }\n }\n result[resIndex++] = value;\n }\n return result;\n }\n\n // Ensure `LazyWrapper` is an instance of `baseLodash`.\n LazyWrapper.prototype = baseCreate(baseLodash.prototype);\n LazyWrapper.prototype.constructor = LazyWrapper;\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates a hash object.\n *\n * @private\n * @constructor\n * @param {Array} [entries] The key-value pairs to cache.\n */\n function Hash(entries) {\n var index = -1,\n length = entries == null ? 0 : entries.length;\n\n this.clear();\n while (++index < length) {\n var entry = entries[index];\n this.set(entry[0], entry[1]);\n }\n }\n\n /**\n * Removes all key-value entries from the hash.\n *\n * @private\n * @name clear\n * @memberOf Hash\n */\n function hashClear() {\n this.__data__ = nativeCreate ? nativeCreate(null) : {};\n this.size = 0;\n }\n\n /**\n * Removes `key` and its value from the hash.\n *\n * @private\n * @name delete\n * @memberOf Hash\n * @param {Object} hash The hash to modify.\n * @param {string} key The key of the value to remove.\n * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n */\n function hashDelete(key) {\n var result = this.has(key) && delete this.__data__[key];\n this.size -= result ? 1 : 0;\n return result;\n }\n\n /**\n * Gets the hash value for `key`.\n *\n * @private\n * @name get\n * @memberOf Hash\n * @param {string} key The key of the value to get.\n * @returns {*} Returns the entry value.\n */\n function hashGet(key) {\n var data = this.__data__;\n if (nativeCreate) {\n var result = data[key];\n return result === HASH_UNDEFINED ? undefined : result;\n }\n return hasOwnProperty.call(data, key) ? data[key] : undefined;\n }\n\n /**\n * Checks if a hash value for `key` exists.\n *\n * @private\n * @name has\n * @memberOf Hash\n * @param {string} key The key of the entry to check.\n * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n */\n function hashHas(key) {\n var data = this.__data__;\n return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key);\n }\n\n /**\n * Sets the hash `key` to `value`.\n *\n * @private\n * @name set\n * @memberOf Hash\n * @param {string} key The key of the value to set.\n * @param {*} value The value to set.\n * @returns {Object} Returns the hash instance.\n */\n function hashSet(key, value) {\n var data = this.__data__;\n this.size += this.has(key) ? 0 : 1;\n data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;\n return this;\n }\n\n // Add methods to `Hash`.\n Hash.prototype.clear = hashClear;\n Hash.prototype['delete'] = hashDelete;\n Hash.prototype.get = hashGet;\n Hash.prototype.has = hashHas;\n Hash.prototype.set = hashSet;\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates an list cache object.\n *\n * @private\n * @constructor\n * @param {Array} [entries] The key-value pairs to cache.\n */\n function ListCache(entries) {\n var index = -1,\n length = entries == null ? 0 : entries.length;\n\n this.clear();\n while (++index < length) {\n var entry = entries[index];\n this.set(entry[0], entry[1]);\n }\n }\n\n /**\n * Removes all key-value entries from the list cache.\n *\n * @private\n * @name clear\n * @memberOf ListCache\n */\n function listCacheClear() {\n this.__data__ = [];\n this.size = 0;\n }\n\n /**\n * Removes `key` and its value from the list cache.\n *\n * @private\n * @name delete\n * @memberOf ListCache\n * @param {string} key The key of the value to remove.\n * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n */\n function listCacheDelete(key) {\n var data = this.__data__,\n index = assocIndexOf(data, key);\n\n if (index < 0) {\n return false;\n }\n var lastIndex = data.length - 1;\n if (index == lastIndex) {\n data.pop();\n } else {\n splice.call(data, index, 1);\n }\n --this.size;\n return true;\n }\n\n /**\n * Gets the list cache value for `key`.\n *\n * @private\n * @name get\n * @memberOf ListCache\n * @param {string} key The key of the value to get.\n * @returns {*} Returns the entry value.\n */\n function listCacheGet(key) {\n var data = this.__data__,\n index = assocIndexOf(data, key);\n\n return index < 0 ? undefined : data[index][1];\n }\n\n /**\n * Checks if a list cache value for `key` exists.\n *\n * @private\n * @name has\n * @memberOf ListCache\n * @param {string} key The key of the entry to check.\n * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n */\n function listCacheHas(key) {\n return assocIndexOf(this.__data__, key) > -1;\n }\n\n /**\n * Sets the list cache `key` to `value`.\n *\n * @private\n * @name set\n * @memberOf ListCache\n * @param {string} key The key of the value to set.\n * @param {*} value The value to set.\n * @returns {Object} Returns the list cache instance.\n */\n function listCacheSet(key, value) {\n var data = this.__data__,\n index = assocIndexOf(data, key);\n\n if (index < 0) {\n ++this.size;\n data.push([key, value]);\n } else {\n data[index][1] = value;\n }\n return this;\n }\n\n // Add methods to `ListCache`.\n ListCache.prototype.clear = listCacheClear;\n ListCache.prototype['delete'] = listCacheDelete;\n ListCache.prototype.get = listCacheGet;\n ListCache.prototype.has = listCacheHas;\n ListCache.prototype.set = listCacheSet;\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates a map cache object to store key-value pairs.\n *\n * @private\n * @constructor\n * @param {Array} [entries] The key-value pairs to cache.\n */\n function MapCache(entries) {\n var index = -1,\n length = entries == null ? 0 : entries.length;\n\n this.clear();\n while (++index < length) {\n var entry = entries[index];\n this.set(entry[0], entry[1]);\n }\n }\n\n /**\n * Removes all key-value entries from the map.\n *\n * @private\n * @name clear\n * @memberOf MapCache\n */\n function mapCacheClear() {\n this.size = 0;\n this.__data__ = {\n 'hash': new Hash,\n 'map': new (Map || ListCache),\n 'string': new Hash\n };\n }\n\n /**\n * Removes `key` and its value from the map.\n *\n * @private\n * @name delete\n * @memberOf MapCache\n * @param {string} key The key of the value to remove.\n * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n */\n function mapCacheDelete(key) {\n var result = getMapData(this, key)['delete'](key);\n this.size -= result ? 1 : 0;\n return result;\n }\n\n /**\n * Gets the map value for `key`.\n *\n * @private\n * @name get\n * @memberOf MapCache\n * @param {string} key The key of the value to get.\n * @returns {*} Returns the entry value.\n */\n function mapCacheGet(key) {\n return getMapData(this, key).get(key);\n }\n\n /**\n * Checks if a map value for `key` exists.\n *\n * @private\n * @name has\n * @memberOf MapCache\n * @param {string} key The key of the entry to check.\n * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n */\n function mapCacheHas(key) {\n return getMapData(this, key).has(key);\n }\n\n /**\n * Sets the map `key` to `value`.\n *\n * @private\n * @name set\n * @memberOf MapCache\n * @param {string} key The key of the value to set.\n * @param {*} value The value to set.\n * @returns {Object} Returns the map cache instance.\n */\n function mapCacheSet(key, value) {\n var data = getMapData(this, key),\n size = data.size;\n\n data.set(key, value);\n this.size += data.size == size ? 0 : 1;\n return this;\n }\n\n // Add methods to `MapCache`.\n MapCache.prototype.clear = mapCacheClear;\n MapCache.prototype['delete'] = mapCacheDelete;\n MapCache.prototype.get = mapCacheGet;\n MapCache.prototype.has = mapCacheHas;\n MapCache.prototype.set = mapCacheSet;\n\n /*------------------------------------------------------------------------*/\n\n /**\n *\n * Creates an array cache object to store unique values.\n *\n * @private\n * @constructor\n * @param {Array} [values] The values to cache.\n */\n function SetCache(values) {\n var index = -1,\n length = values == null ? 0 : values.length;\n\n this.__data__ = new MapCache;\n while (++index < length) {\n this.add(values[index]);\n }\n }\n\n /**\n * Adds `value` to the array cache.\n *\n * @private\n * @name add\n * @memberOf SetCache\n * @alias push\n * @param {*} value The value to cache.\n * @returns {Object} Returns the cache instance.\n */\n function setCacheAdd(value) {\n this.__data__.set(value, HASH_UNDEFINED);\n return this;\n }\n\n /**\n * Checks if `value` is in the array cache.\n *\n * @private\n * @name has\n * @memberOf SetCache\n * @param {*} value The value to search for.\n * @returns {number} Returns `true` if `value` is found, else `false`.\n */\n function setCacheHas(value) {\n return this.__data__.has(value);\n }\n\n // Add methods to `SetCache`.\n SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;\n SetCache.prototype.has = setCacheHas;\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates a stack cache object to store key-value pairs.\n *\n * @private\n * @constructor\n * @param {Array} [entries] The key-value pairs to cache.\n */\n function Stack(entries) {\n var data = this.__data__ = new ListCache(entries);\n this.size = data.size;\n }\n\n /**\n * Removes all key-value entries from the stack.\n *\n * @private\n * @name clear\n * @memberOf Stack\n */\n function stackClear() {\n this.__data__ = new ListCache;\n this.size = 0;\n }\n\n /**\n * Removes `key` and its value from the stack.\n *\n * @private\n * @name delete\n * @memberOf Stack\n * @param {string} key The key of the value to remove.\n * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n */\n function stackDelete(key) {\n var data = this.__data__,\n result = data['delete'](key);\n\n this.size = data.size;\n return result;\n }\n\n /**\n * Gets the stack value for `key`.\n *\n * @private\n * @name get\n * @memberOf Stack\n * @param {string} key The key of the value to get.\n * @returns {*} Returns the entry value.\n */\n function stackGet(key) {\n return this.__data__.get(key);\n }\n\n /**\n * Checks if a stack value for `key` exists.\n *\n * @private\n * @name has\n * @memberOf Stack\n * @param {string} key The key of the entry to check.\n * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n */\n function stackHas(key) {\n return this.__data__.has(key);\n }\n\n /**\n * Sets the stack `key` to `value`.\n *\n * @private\n * @name set\n * @memberOf Stack\n * @param {string} key The key of the value to set.\n * @param {*} value The value to set.\n * @returns {Object} Returns the stack cache instance.\n */\n function stackSet(key, value) {\n var data = this.__data__;\n if (data instanceof ListCache) {\n var pairs = data.__data__;\n if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {\n pairs.push([key, value]);\n this.size = ++data.size;\n return this;\n }\n data = this.__data__ = new MapCache(pairs);\n }\n data.set(key, value);\n this.size = data.size;\n return this;\n }\n\n // Add methods to `Stack`.\n Stack.prototype.clear = stackClear;\n Stack.prototype['delete'] = stackDelete;\n Stack.prototype.get = stackGet;\n Stack.prototype.has = stackHas;\n Stack.prototype.set = stackSet;\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates an array of the enumerable property names of the array-like `value`.\n *\n * @private\n * @param {*} value The value to query.\n * @param {boolean} inherited Specify returning inherited property names.\n * @returns {Array} Returns the array of property names.\n */\n function arrayLikeKeys(value, inherited) {\n var isArr = isArray(value),\n isArg = !isArr && isArguments(value),\n isBuff = !isArr && !isArg && isBuffer(value),\n isType = !isArr && !isArg && !isBuff && isTypedArray(value),\n skipIndexes = isArr || isArg || isBuff || isType,\n result = skipIndexes ? baseTimes(value.length, String) : [],\n length = result.length;\n\n for (var key in value) {\n if ((inherited || hasOwnProperty.call(value, key)) &&\n !(skipIndexes && (\n // Safari 9 has enumerable `arguments.length` in strict mode.\n key == 'length' ||\n // Node.js 0.10 has enumerable non-index properties on buffers.\n (isBuff && (key == 'offset' || key == 'parent')) ||\n // PhantomJS 2 has enumerable non-index properties on typed arrays.\n (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||\n // Skip index properties.\n isIndex(key, length)\n ))) {\n result.push(key);\n }\n }\n return result;\n }\n\n /**\n * A specialized version of `_.sample` for arrays.\n *\n * @private\n * @param {Array} array The array to sample.\n * @returns {*} Returns the random element.\n */\n function arraySample(array) {\n var length = array.length;\n return length ? array[baseRandom(0, length - 1)] : undefined;\n }\n\n /**\n * A specialized version of `_.sampleSize` for arrays.\n *\n * @private\n * @param {Array} array The array to sample.\n * @param {number} n The number of elements to sample.\n * @returns {Array} Returns the random elements.\n */\n function arraySampleSize(array, n) {\n return shuffleSelf(copyArray(array), baseClamp(n, 0, array.length));\n }\n\n /**\n * A specialized version of `_.shuffle` for arrays.\n *\n * @private\n * @param {Array} array The array to shuffle.\n * @returns {Array} Returns the new shuffled array.\n */\n function arrayShuffle(array) {\n return shuffleSelf(copyArray(array));\n }\n\n /**\n * This function is like `assignValue` except that it doesn't assign\n * `undefined` values.\n *\n * @private\n * @param {Object} object The object to modify.\n * @param {string} key The key of the property to assign.\n * @param {*} value The value to assign.\n */\n function assignMergeValue(object, key, value) {\n if ((value !== undefined && !eq(object[key], value)) ||\n (value === undefined && !(key in object))) {\n baseAssignValue(object, key, value);\n }\n }\n\n /**\n * Assigns `value` to `key` of `object` if the existing value is not equivalent\n * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons.\n *\n * @private\n * @param {Object} object The object to modify.\n * @param {string} key The key of the property to assign.\n * @param {*} value The value to assign.\n */\n function assignValue(object, key, value) {\n var objValue = object[key];\n if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||\n (value === undefined && !(key in object))) {\n baseAssignValue(object, key, value);\n }\n }\n\n /**\n * Gets the index at which the `key` is found in `array` of key-value pairs.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {*} key The key to search for.\n * @returns {number} Returns the index of the matched value, else `-1`.\n */\n function assocIndexOf(array, key) {\n var length = array.length;\n while (length--) {\n if (eq(array[length][0], key)) {\n return length;\n }\n }\n return -1;\n }\n\n /**\n * Aggregates elements of `collection` on `accumulator` with keys transformed\n * by `iteratee` and values set by `setter`.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} setter The function to set `accumulator` values.\n * @param {Function} iteratee The iteratee to transform keys.\n * @param {Object} accumulator The initial aggregated object.\n * @returns {Function} Returns `accumulator`.\n */\n function baseAggregator(collection, setter, iteratee, accumulator) {\n baseEach(collection, function(value, key, collection) {\n setter(accumulator, value, iteratee(value), collection);\n });\n return accumulator;\n }\n\n /**\n * The base implementation of `_.assign` without support for multiple sources\n * or `customizer` functions.\n *\n * @private\n * @param {Object} object The destination object.\n * @param {Object} source The source object.\n * @returns {Object} Returns `object`.\n */\n function baseAssign(object, source) {\n return object && copyObject(source, keys(source), object);\n }\n\n /**\n * The base implementation of `_.assignIn` without support for multiple sources\n * or `customizer` functions.\n *\n * @private\n * @param {Object} object The destination object.\n * @param {Object} source The source object.\n * @returns {Object} Returns `object`.\n */\n function baseAssignIn(object, source) {\n return object && copyObject(source, keysIn(source), object);\n }\n\n /**\n * The base implementation of `assignValue` and `assignMergeValue` without\n * value checks.\n *\n * @private\n * @param {Object} object The object to modify.\n * @param {string} key The key of the property to assign.\n * @param {*} value The value to assign.\n */\n function baseAssignValue(object, key, value) {\n if (key == '__proto__' && defineProperty) {\n defineProperty(object, key, {\n 'configurable': true,\n 'enumerable': true,\n 'value': value,\n 'writable': true\n });\n } else {\n object[key] = value;\n }\n }\n\n /**\n * The base implementation of `_.at` without support for individual paths.\n *\n * @private\n * @param {Object} object The object to iterate over.\n * @param {string[]} paths The property paths to pick.\n * @returns {Array} Returns the picked elements.\n */\n function baseAt(object, paths) {\n var index = -1,\n length = paths.length,\n result = Array(length),\n skip = object == null;\n\n while (++index < length) {\n result[index] = skip ? undefined : get(object, paths[index]);\n }\n return result;\n }\n\n /**\n * The base implementation of `_.clamp` which doesn't coerce arguments.\n *\n * @private\n * @param {number} number The number to clamp.\n * @param {number} [lower] The lower bound.\n * @param {number} upper The upper bound.\n * @returns {number} Returns the clamped number.\n */\n function baseClamp(number, lower, upper) {\n if (number === number) {\n if (upper !== undefined) {\n number = number <= upper ? number : upper;\n }\n if (lower !== undefined) {\n number = number >= lower ? number : lower;\n }\n }\n return number;\n }\n\n /**\n * The base implementation of `_.clone` and `_.cloneDeep` which tracks\n * traversed objects.\n *\n * @private\n * @param {*} value The value to clone.\n * @param {boolean} bitmask The bitmask flags.\n * 1 - Deep clone\n * 2 - Flatten inherited properties\n * 4 - Clone symbols\n * @param {Function} [customizer] The function to customize cloning.\n * @param {string} [key] The key of `value`.\n * @param {Object} [object] The parent object of `value`.\n * @param {Object} [stack] Tracks traversed objects and their clone counterparts.\n * @returns {*} Returns the cloned value.\n */\n function baseClone(value, bitmask, customizer, key, object, stack) {\n var result,\n isDeep = bitmask & CLONE_DEEP_FLAG,\n isFlat = bitmask & CLONE_FLAT_FLAG,\n isFull = bitmask & CLONE_SYMBOLS_FLAG;\n\n if (customizer) {\n result = object ? customizer(value, key, object, stack) : customizer(value);\n }\n if (result !== undefined) {\n return result;\n }\n if (!isObject(value)) {\n return value;\n }\n var isArr = isArray(value);\n if (isArr) {\n result = initCloneArray(value);\n if (!isDeep) {\n return copyArray(value, result);\n }\n } else {\n var tag = getTag(value),\n isFunc = tag == funcTag || tag == genTag;\n\n if (isBuffer(value)) {\n return cloneBuffer(value, isDeep);\n }\n if (tag == objectTag || tag == argsTag || (isFunc && !object)) {\n result = (isFlat || isFunc) ? {} : initCloneObject(value);\n if (!isDeep) {\n return isFlat\n ? copySymbolsIn(value, baseAssignIn(result, value))\n : copySymbols(value, baseAssign(result, value));\n }\n } else {\n if (!cloneableTags[tag]) {\n return object ? value : {};\n }\n result = initCloneByTag(value, tag, isDeep);\n }\n }\n // Check for circular references and return its corresponding clone.\n stack || (stack = new Stack);\n var stacked = stack.get(value);\n if (stacked) {\n return stacked;\n }\n stack.set(value, result);\n\n if (isSet(value)) {\n value.forEach(function(subValue) {\n result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));\n });\n\n return result;\n }\n\n if (isMap(value)) {\n value.forEach(function(subValue, key) {\n result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));\n });\n\n return result;\n }\n\n var keysFunc = isFull\n ? (isFlat ? getAllKeysIn : getAllKeys)\n : (isFlat ? keysIn : keys);\n\n var props = isArr ? undefined : keysFunc(value);\n arrayEach(props || value, function(subValue, key) {\n if (props) {\n key = subValue;\n subValue = value[key];\n }\n // Recursively populate clone (susceptible to call stack limits).\n assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));\n });\n return result;\n }\n\n /**\n * The base implementation of `_.conforms` which doesn't clone `source`.\n *\n * @private\n * @param {Object} source The object of property predicates to conform to.\n * @returns {Function} Returns the new spec function.\n */\n function baseConforms(source) {\n var props = keys(source);\n return function(object) {\n return baseConformsTo(object, source, props);\n };\n }\n\n /**\n * The base implementation of `_.conformsTo` which accepts `props` to check.\n *\n * @private\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property predicates to conform to.\n * @returns {boolean} Returns `true` if `object` conforms, else `false`.\n */\n function baseConformsTo(object, source, props) {\n var length = props.length;\n if (object == null) {\n return !length;\n }\n object = Object(object);\n while (length--) {\n var key = props[length],\n predicate = source[key],\n value = object[key];\n\n if ((value === undefined && !(key in object)) || !predicate(value)) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * The base implementation of `_.delay` and `_.defer` which accepts `args`\n * to provide to `func`.\n *\n * @private\n * @param {Function} func The function to delay.\n * @param {number} wait The number of milliseconds to delay invocation.\n * @param {Array} args The arguments to provide to `func`.\n * @returns {number|Object} Returns the timer id or timeout object.\n */\n function baseDelay(func, wait, args) {\n if (typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n return setTimeout(function() { func.apply(undefined, args); }, wait);\n }\n\n /**\n * The base implementation of methods like `_.difference` without support\n * for excluding multiple arrays or iteratee shorthands.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {Array} values The values to exclude.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of filtered values.\n */\n function baseDifference(array, values, iteratee, comparator) {\n var index = -1,\n includes = arrayIncludes,\n isCommon = true,\n length = array.length,\n result = [],\n valuesLength = values.length;\n\n if (!length) {\n return result;\n }\n if (iteratee) {\n values = arrayMap(values, baseUnary(iteratee));\n }\n if (comparator) {\n includes = arrayIncludesWith;\n isCommon = false;\n }\n else if (values.length >= LARGE_ARRAY_SIZE) {\n includes = cacheHas;\n isCommon = false;\n values = new SetCache(values);\n }\n outer:\n while (++index < length) {\n var value = array[index],\n computed = iteratee == null ? value : iteratee(value);\n\n value = (comparator || value !== 0) ? value : 0;\n if (isCommon && computed === computed) {\n var valuesIndex = valuesLength;\n while (valuesIndex--) {\n if (values[valuesIndex] === computed) {\n continue outer;\n }\n }\n result.push(value);\n }\n else if (!includes(values, computed, comparator)) {\n result.push(value);\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `_.forEach` without support for iteratee shorthands.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Array|Object} Returns `collection`.\n */\n var baseEach = createBaseEach(baseForOwn);\n\n /**\n * The base implementation of `_.forEachRight` without support for iteratee shorthands.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Array|Object} Returns `collection`.\n */\n var baseEachRight = createBaseEach(baseForOwnRight, true);\n\n /**\n * The base implementation of `_.every` without support for iteratee shorthands.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} predicate The function invoked per iteration.\n * @returns {boolean} Returns `true` if all elements pass the predicate check,\n * else `false`\n */\n function baseEvery(collection, predicate) {\n var result = true;\n baseEach(collection, function(value, index, collection) {\n result = !!predicate(value, index, collection);\n return result;\n });\n return result;\n }\n\n /**\n * The base implementation of methods like `_.max` and `_.min` which accepts a\n * `comparator` to determine the extremum value.\n *\n * @private\n * @param {Array} array The array to iterate over.\n * @param {Function} iteratee The iteratee invoked per iteration.\n * @param {Function} comparator The comparator used to compare values.\n * @returns {*} Returns the extremum value.\n */\n function baseExtremum(array, iteratee, comparator) {\n var index = -1,\n length = array.length;\n\n while (++index < length) {\n var value = array[index],\n current = iteratee(value);\n\n if (current != null && (computed === undefined\n ? (current === current && !isSymbol(current))\n : comparator(current, computed)\n )) {\n var computed = current,\n result = value;\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `_.fill` without an iteratee call guard.\n *\n * @private\n * @param {Array} array The array to fill.\n * @param {*} value The value to fill `array` with.\n * @param {number} [start=0] The start position.\n * @param {number} [end=array.length] The end position.\n * @returns {Array} Returns `array`.\n */\n function baseFill(array, value, start, end) {\n var length = array.length;\n\n start = toInteger(start);\n if (start < 0) {\n start = -start > length ? 0 : (length + start);\n }\n end = (end === undefined || end > length) ? length : toInteger(end);\n if (end < 0) {\n end += length;\n }\n end = start > end ? 0 : toLength(end);\n while (start < end) {\n array[start++] = value;\n }\n return array;\n }\n\n /**\n * The base implementation of `_.filter` without support for iteratee shorthands.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} predicate The function invoked per iteration.\n * @returns {Array} Returns the new filtered array.\n */\n function baseFilter(collection, predicate) {\n var result = [];\n baseEach(collection, function(value, index, collection) {\n if (predicate(value, index, collection)) {\n result.push(value);\n }\n });\n return result;\n }\n\n /**\n * The base implementation of `_.flatten` with support for restricting flattening.\n *\n * @private\n * @param {Array} array The array to flatten.\n * @param {number} depth The maximum recursion depth.\n * @param {boolean} [predicate=isFlattenable] The function invoked per iteration.\n * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.\n * @param {Array} [result=[]] The initial result value.\n * @returns {Array} Returns the new flattened array.\n */\n function baseFlatten(array, depth, predicate, isStrict, result) {\n var index = -1,\n length = array.length;\n\n predicate || (predicate = isFlattenable);\n result || (result = []);\n\n while (++index < length) {\n var value = array[index];\n if (depth > 0 && predicate(value)) {\n if (depth > 1) {\n // Recursively flatten arrays (susceptible to call stack limits).\n baseFlatten(value, depth - 1, predicate, isStrict, result);\n } else {\n arrayPush(result, value);\n }\n } else if (!isStrict) {\n result[result.length] = value;\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `baseForOwn` which iterates over `object`\n * properties returned by `keysFunc` and invokes `iteratee` for each property.\n * Iteratee functions may exit iteration early by explicitly returning `false`.\n *\n * @private\n * @param {Object} object The object to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @param {Function} keysFunc The function to get the keys of `object`.\n * @returns {Object} Returns `object`.\n */\n var baseFor = createBaseFor();\n\n /**\n * This function is like `baseFor` except that it iterates over properties\n * in the opposite order.\n *\n * @private\n * @param {Object} object The object to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @param {Function} keysFunc The function to get the keys of `object`.\n * @returns {Object} Returns `object`.\n */\n var baseForRight = createBaseFor(true);\n\n /**\n * The base implementation of `_.forOwn` without support for iteratee shorthands.\n *\n * @private\n * @param {Object} object The object to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Object} Returns `object`.\n */\n function baseForOwn(object, iteratee) {\n return object && baseFor(object, iteratee, keys);\n }\n\n /**\n * The base implementation of `_.forOwnRight` without support for iteratee shorthands.\n *\n * @private\n * @param {Object} object The object to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Object} Returns `object`.\n */\n function baseForOwnRight(object, iteratee) {\n return object && baseForRight(object, iteratee, keys);\n }\n\n /**\n * The base implementation of `_.functions` which creates an array of\n * `object` function property names filtered from `props`.\n *\n * @private\n * @param {Object} object The object to inspect.\n * @param {Array} props The property names to filter.\n * @returns {Array} Returns the function names.\n */\n function baseFunctions(object, props) {\n return arrayFilter(props, function(key) {\n return isFunction(object[key]);\n });\n }\n\n /**\n * The base implementation of `_.get` without support for default values.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Array|string} path The path of the property to get.\n * @returns {*} Returns the resolved value.\n */\n function baseGet(object, path) {\n path = castPath(path, object);\n\n var index = 0,\n length = path.length;\n\n while (object != null && index < length) {\n object = object[toKey(path[index++])];\n }\n return (index && index == length) ? object : undefined;\n }\n\n /**\n * The base implementation of `getAllKeys` and `getAllKeysIn` which uses\n * `keysFunc` and `symbolsFunc` to get the enumerable property names and\n * symbols of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Function} keysFunc The function to get the keys of `object`.\n * @param {Function} symbolsFunc The function to get the symbols of `object`.\n * @returns {Array} Returns the array of property names and symbols.\n */\n function baseGetAllKeys(object, keysFunc, symbolsFunc) {\n var result = keysFunc(object);\n return isArray(object) ? result : arrayPush(result, symbolsFunc(object));\n }\n\n /**\n * The base implementation of `getTag` without fallbacks for buggy environments.\n *\n * @private\n * @param {*} value The value to query.\n * @returns {string} Returns the `toStringTag`.\n */\n function baseGetTag(value) {\n if (value == null) {\n return value === undefined ? undefinedTag : nullTag;\n }\n return (symToStringTag && symToStringTag in Object(value))\n ? getRawTag(value)\n : objectToString(value);\n }\n\n /**\n * The base implementation of `_.gt` which doesn't coerce arguments.\n *\n * @private\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if `value` is greater than `other`,\n * else `false`.\n */\n function baseGt(value, other) {\n return value > other;\n }\n\n /**\n * The base implementation of `_.has` without support for deep paths.\n *\n * @private\n * @param {Object} [object] The object to query.\n * @param {Array|string} key The key to check.\n * @returns {boolean} Returns `true` if `key` exists, else `false`.\n */\n function baseHas(object, key) {\n return object != null && hasOwnProperty.call(object, key);\n }\n\n /**\n * The base implementation of `_.hasIn` without support for deep paths.\n *\n * @private\n * @param {Object} [object] The object to query.\n * @param {Array|string} key The key to check.\n * @returns {boolean} Returns `true` if `key` exists, else `false`.\n */\n function baseHasIn(object, key) {\n return object != null && key in Object(object);\n }\n\n /**\n * The base implementation of `_.inRange` which doesn't coerce arguments.\n *\n * @private\n * @param {number} number The number to check.\n * @param {number} start The start of the range.\n * @param {number} end The end of the range.\n * @returns {boolean} Returns `true` if `number` is in the range, else `false`.\n */\n function baseInRange(number, start, end) {\n return number >= nativeMin(start, end) && number < nativeMax(start, end);\n }\n\n /**\n * The base implementation of methods like `_.intersection`, without support\n * for iteratee shorthands, that accepts an array of arrays to inspect.\n *\n * @private\n * @param {Array} arrays The arrays to inspect.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of shared values.\n */\n function baseIntersection(arrays, iteratee, comparator) {\n var includes = comparator ? arrayIncludesWith : arrayIncludes,\n length = arrays[0].length,\n othLength = arrays.length,\n othIndex = othLength,\n caches = Array(othLength),\n maxLength = Infinity,\n result = [];\n\n while (othIndex--) {\n var array = arrays[othIndex];\n if (othIndex && iteratee) {\n array = arrayMap(array, baseUnary(iteratee));\n }\n maxLength = nativeMin(array.length, maxLength);\n caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120))\n ? new SetCache(othIndex && array)\n : undefined;\n }\n array = arrays[0];\n\n var index = -1,\n seen = caches[0];\n\n outer:\n while (++index < length && result.length < maxLength) {\n var value = array[index],\n computed = iteratee ? iteratee(value) : value;\n\n value = (comparator || value !== 0) ? value : 0;\n if (!(seen\n ? cacheHas(seen, computed)\n : includes(result, computed, comparator)\n )) {\n othIndex = othLength;\n while (--othIndex) {\n var cache = caches[othIndex];\n if (!(cache\n ? cacheHas(cache, computed)\n : includes(arrays[othIndex], computed, comparator))\n ) {\n continue outer;\n }\n }\n if (seen) {\n seen.push(computed);\n }\n result.push(value);\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `_.invert` and `_.invertBy` which inverts\n * `object` with values transformed by `iteratee` and set by `setter`.\n *\n * @private\n * @param {Object} object The object to iterate over.\n * @param {Function} setter The function to set `accumulator` values.\n * @param {Function} iteratee The iteratee to transform values.\n * @param {Object} accumulator The initial inverted object.\n * @returns {Function} Returns `accumulator`.\n */\n function baseInverter(object, setter, iteratee, accumulator) {\n baseForOwn(object, function(value, key, object) {\n setter(accumulator, iteratee(value), key, object);\n });\n return accumulator;\n }\n\n /**\n * The base implementation of `_.invoke` without support for individual\n * method arguments.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Array|string} path The path of the method to invoke.\n * @param {Array} args The arguments to invoke the method with.\n * @returns {*} Returns the result of the invoked method.\n */\n function baseInvoke(object, path, args) {\n path = castPath(path, object);\n object = parent(object, path);\n var func = object == null ? object : object[toKey(last(path))];\n return func == null ? undefined : apply(func, object, args);\n }\n\n /**\n * The base implementation of `_.isArguments`.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an `arguments` object,\n */\n function baseIsArguments(value) {\n return isObjectLike(value) && baseGetTag(value) == argsTag;\n }\n\n /**\n * The base implementation of `_.isArrayBuffer` without Node.js optimizations.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`.\n */\n function baseIsArrayBuffer(value) {\n return isObjectLike(value) && baseGetTag(value) == arrayBufferTag;\n }\n\n /**\n * The base implementation of `_.isDate` without Node.js optimizations.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a date object, else `false`.\n */\n function baseIsDate(value) {\n return isObjectLike(value) && baseGetTag(value) == dateTag;\n }\n\n /**\n * The base implementation of `_.isEqual` which supports partial comparisons\n * and tracks traversed objects.\n *\n * @private\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @param {boolean} bitmask The bitmask flags.\n * 1 - Unordered comparison\n * 2 - Partial comparison\n * @param {Function} [customizer] The function to customize comparisons.\n * @param {Object} [stack] Tracks traversed `value` and `other` objects.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n */\n function baseIsEqual(value, other, bitmask, customizer, stack) {\n if (value === other) {\n return true;\n }\n if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {\n return value !== value && other !== other;\n }\n return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);\n }\n\n /**\n * A specialized version of `baseIsEqual` for arrays and objects which performs\n * deep comparisons and tracks traversed objects enabling objects with circular\n * references to be compared.\n *\n * @private\n * @param {Object} object The object to compare.\n * @param {Object} other The other object to compare.\n * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.\n * @param {Function} customizer The function to customize comparisons.\n * @param {Function} equalFunc The function to determine equivalents of values.\n * @param {Object} [stack] Tracks traversed `object` and `other` objects.\n * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.\n */\n function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) {\n var objIsArr = isArray(object),\n othIsArr = isArray(other),\n objTag = objIsArr ? arrayTag : getTag(object),\n othTag = othIsArr ? arrayTag : getTag(other);\n\n objTag = objTag == argsTag ? objectTag : objTag;\n othTag = othTag == argsTag ? objectTag : othTag;\n\n var objIsObj = objTag == objectTag,\n othIsObj = othTag == objectTag,\n isSameTag = objTag == othTag;\n\n if (isSameTag && isBuffer(object)) {\n if (!isBuffer(other)) {\n return false;\n }\n objIsArr = true;\n objIsObj = false;\n }\n if (isSameTag && !objIsObj) {\n stack || (stack = new Stack);\n return (objIsArr || isTypedArray(object))\n ? equalArrays(object, other, bitmask, customizer, equalFunc, stack)\n : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);\n }\n if (!(bitmask & COMPARE_PARTIAL_FLAG)) {\n var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),\n othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');\n\n if (objIsWrapped || othIsWrapped) {\n var objUnwrapped = objIsWrapped ? object.value() : object,\n othUnwrapped = othIsWrapped ? other.value() : other;\n\n stack || (stack = new Stack);\n return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);\n }\n }\n if (!isSameTag) {\n return false;\n }\n stack || (stack = new Stack);\n return equalObjects(object, other, bitmask, customizer, equalFunc, stack);\n }\n\n /**\n * The base implementation of `_.isMap` without Node.js optimizations.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a map, else `false`.\n */\n function baseIsMap(value) {\n return isObjectLike(value) && getTag(value) == mapTag;\n }\n\n /**\n * The base implementation of `_.isMatch` without support for iteratee shorthands.\n *\n * @private\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property values to match.\n * @param {Array} matchData The property names, values, and compare flags to match.\n * @param {Function} [customizer] The function to customize comparisons.\n * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n */\n function baseIsMatch(object, source, matchData, customizer) {\n var index = matchData.length,\n length = index,\n noCustomizer = !customizer;\n\n if (object == null) {\n return !length;\n }\n object = Object(object);\n while (index--) {\n var data = matchData[index];\n if ((noCustomizer && data[2])\n ? data[1] !== object[data[0]]\n : !(data[0] in object)\n ) {\n return false;\n }\n }\n while (++index < length) {\n data = matchData[index];\n var key = data[0],\n objValue = object[key],\n srcValue = data[1];\n\n if (noCustomizer && data[2]) {\n if (objValue === undefined && !(key in object)) {\n return false;\n }\n } else {\n var stack = new Stack;\n if (customizer) {\n var result = customizer(objValue, srcValue, key, object, source, stack);\n }\n if (!(result === undefined\n ? baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG, customizer, stack)\n : result\n )) {\n return false;\n }\n }\n }\n return true;\n }\n\n /**\n * The base implementation of `_.isNative` without bad shim checks.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a native function,\n * else `false`.\n */\n function baseIsNative(value) {\n if (!isObject(value) || isMasked(value)) {\n return false;\n }\n var pattern = isFunction(value) ? reIsNative : reIsHostCtor;\n return pattern.test(toSource(value));\n }\n\n /**\n * The base implementation of `_.isRegExp` without Node.js optimizations.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a regexp, else `false`.\n */\n function baseIsRegExp(value) {\n return isObjectLike(value) && baseGetTag(value) == regexpTag;\n }\n\n /**\n * The base implementation of `_.isSet` without Node.js optimizations.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a set, else `false`.\n */\n function baseIsSet(value) {\n return isObjectLike(value) && getTag(value) == setTag;\n }\n\n /**\n * The base implementation of `_.isTypedArray` without Node.js optimizations.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.\n */\n function baseIsTypedArray(value) {\n return isObjectLike(value) &&\n isLength(value.length) && !!typedArrayTags[baseGetTag(value)];\n }\n\n /**\n * The base implementation of `_.iteratee`.\n *\n * @private\n * @param {*} [value=_.identity] The value to convert to an iteratee.\n * @returns {Function} Returns the iteratee.\n */\n function baseIteratee(value) {\n // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9.\n // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details.\n if (typeof value == 'function') {\n return value;\n }\n if (value == null) {\n return identity;\n }\n if (typeof value == 'object') {\n return isArray(value)\n ? baseMatchesProperty(value[0], value[1])\n : baseMatches(value);\n }\n return property(value);\n }\n\n /**\n * The base implementation of `_.keys` which doesn't treat sparse arrays as dense.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property names.\n */\n function baseKeys(object) {\n if (!isPrototype(object)) {\n return nativeKeys(object);\n }\n var result = [];\n for (var key in Object(object)) {\n if (hasOwnProperty.call(object, key) && key != 'constructor') {\n result.push(key);\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property names.\n */\n function baseKeysIn(object) {\n if (!isObject(object)) {\n return nativeKeysIn(object);\n }\n var isProto = isPrototype(object),\n result = [];\n\n for (var key in object) {\n if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {\n result.push(key);\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `_.lt` which doesn't coerce arguments.\n *\n * @private\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if `value` is less than `other`,\n * else `false`.\n */\n function baseLt(value, other) {\n return value < other;\n }\n\n /**\n * The base implementation of `_.map` without support for iteratee shorthands.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} iteratee The function invoked per iteration.\n * @returns {Array} Returns the new mapped array.\n */\n function baseMap(collection, iteratee) {\n var index = -1,\n result = isArrayLike(collection) ? Array(collection.length) : [];\n\n baseEach(collection, function(value, key, collection) {\n result[++index] = iteratee(value, key, collection);\n });\n return result;\n }\n\n /**\n * The base implementation of `_.matches` which doesn't clone `source`.\n *\n * @private\n * @param {Object} source The object of property values to match.\n * @returns {Function} Returns the new spec function.\n */\n function baseMatches(source) {\n var matchData = getMatchData(source);\n if (matchData.length == 1 && matchData[0][2]) {\n return matchesStrictComparable(matchData[0][0], matchData[0][1]);\n }\n return function(object) {\n return object === source || baseIsMatch(object, source, matchData);\n };\n }\n\n /**\n * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`.\n *\n * @private\n * @param {string} path The path of the property to get.\n * @param {*} srcValue The value to match.\n * @returns {Function} Returns the new spec function.\n */\n function baseMatchesProperty(path, srcValue) {\n if (isKey(path) && isStrictComparable(srcValue)) {\n return matchesStrictComparable(toKey(path), srcValue);\n }\n return function(object) {\n var objValue = get(object, path);\n return (objValue === undefined && objValue === srcValue)\n ? hasIn(object, path)\n : baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG);\n };\n }\n\n /**\n * The base implementation of `_.merge` without support for multiple sources.\n *\n * @private\n * @param {Object} object The destination object.\n * @param {Object} source The source object.\n * @param {number} srcIndex The index of `source`.\n * @param {Function} [customizer] The function to customize merged values.\n * @param {Object} [stack] Tracks traversed source values and their merged\n * counterparts.\n */\n function baseMerge(object, source, srcIndex, customizer, stack) {\n if (object === source) {\n return;\n }\n baseFor(source, function(srcValue, key) {\n if (isObject(srcValue)) {\n stack || (stack = new Stack);\n baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);\n }\n else {\n var newValue = customizer\n ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack)\n : undefined;\n\n if (newValue === undefined) {\n newValue = srcValue;\n }\n assignMergeValue(object, key, newValue);\n }\n }, keysIn);\n }\n\n /**\n * A specialized version of `baseMerge` for arrays and objects which performs\n * deep merges and tracks traversed objects enabling objects with circular\n * references to be merged.\n *\n * @private\n * @param {Object} object The destination object.\n * @param {Object} source The source object.\n * @param {string} key The key of the value to merge.\n * @param {number} srcIndex The index of `source`.\n * @param {Function} mergeFunc The function to merge values.\n * @param {Function} [customizer] The function to customize assigned values.\n * @param {Object} [stack] Tracks traversed source values and their merged\n * counterparts.\n */\n function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {\n var objValue = safeGet(object, key),\n srcValue = safeGet(source, key),\n stacked = stack.get(srcValue);\n\n if (stacked) {\n assignMergeValue(object, key, stacked);\n return;\n }\n var newValue = customizer\n ? customizer(objValue, srcValue, (key + ''), object, source, stack)\n : undefined;\n\n var isCommon = newValue === undefined;\n\n if (isCommon) {\n var isArr = isArray(srcValue),\n isBuff = !isArr && isBuffer(srcValue),\n isTyped = !isArr && !isBuff && isTypedArray(srcValue);\n\n newValue = srcValue;\n if (isArr || isBuff || isTyped) {\n if (isArray(objValue)) {\n newValue = objValue;\n }\n else if (isArrayLikeObject(objValue)) {\n newValue = copyArray(objValue);\n }\n else if (isBuff) {\n isCommon = false;\n newValue = cloneBuffer(srcValue, true);\n }\n else if (isTyped) {\n isCommon = false;\n newValue = cloneTypedArray(srcValue, true);\n }\n else {\n newValue = [];\n }\n }\n else if (isPlainObject(srcValue) || isArguments(srcValue)) {\n newValue = objValue;\n if (isArguments(objValue)) {\n newValue = toPlainObject(objValue);\n }\n else if (!isObject(objValue) || (srcIndex && isFunction(objValue))) {\n newValue = initCloneObject(srcValue);\n }\n }\n else {\n isCommon = false;\n }\n }\n if (isCommon) {\n // Recursively merge objects and arrays (susceptible to call stack limits).\n stack.set(srcValue, newValue);\n mergeFunc(newValue, srcValue, srcIndex, customizer, stack);\n stack['delete'](srcValue);\n }\n assignMergeValue(object, key, newValue);\n }\n\n /**\n * The base implementation of `_.nth` which doesn't coerce arguments.\n *\n * @private\n * @param {Array} array The array to query.\n * @param {number} n The index of the element to return.\n * @returns {*} Returns the nth element of `array`.\n */\n function baseNth(array, n) {\n var length = array.length;\n if (!length) {\n return;\n }\n n += n < 0 ? length : 0;\n return isIndex(n, length) ? array[n] : undefined;\n }\n\n /**\n * The base implementation of `_.orderBy` without param guards.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by.\n * @param {string[]} orders The sort orders of `iteratees`.\n * @returns {Array} Returns the new sorted array.\n */\n function baseOrderBy(collection, iteratees, orders) {\n var index = -1;\n iteratees = arrayMap(iteratees.length ? iteratees : [identity], baseUnary(getIteratee()));\n\n var result = baseMap(collection, function(value, key, collection) {\n var criteria = arrayMap(iteratees, function(iteratee) {\n return iteratee(value);\n });\n return { 'criteria': criteria, 'index': ++index, 'value': value };\n });\n\n return baseSortBy(result, function(object, other) {\n return compareMultiple(object, other, orders);\n });\n }\n\n /**\n * The base implementation of `_.pick` without support for individual\n * property identifiers.\n *\n * @private\n * @param {Object} object The source object.\n * @param {string[]} paths The property paths to pick.\n * @returns {Object} Returns the new object.\n */\n function basePick(object, paths) {\n return basePickBy(object, paths, function(value, path) {\n return hasIn(object, path);\n });\n }\n\n /**\n * The base implementation of `_.pickBy` without support for iteratee shorthands.\n *\n * @private\n * @param {Object} object The source object.\n * @param {string[]} paths The property paths to pick.\n * @param {Function} predicate The function invoked per property.\n * @returns {Object} Returns the new object.\n */\n function basePickBy(object, paths, predicate) {\n var index = -1,\n length = paths.length,\n result = {};\n\n while (++index < length) {\n var path = paths[index],\n value = baseGet(object, path);\n\n if (predicate(value, path)) {\n baseSet(result, castPath(path, object), value);\n }\n }\n return result;\n }\n\n /**\n * A specialized version of `baseProperty` which supports deep paths.\n *\n * @private\n * @param {Array|string} path The path of the property to get.\n * @returns {Function} Returns the new accessor function.\n */\n function basePropertyDeep(path) {\n return function(object) {\n return baseGet(object, path);\n };\n }\n\n /**\n * The base implementation of `_.pullAllBy` without support for iteratee\n * shorthands.\n *\n * @private\n * @param {Array} array The array to modify.\n * @param {Array} values The values to remove.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns `array`.\n */\n function basePullAll(array, values, iteratee, comparator) {\n var indexOf = comparator ? baseIndexOfWith : baseIndexOf,\n index = -1,\n length = values.length,\n seen = array;\n\n if (array === values) {\n values = copyArray(values);\n }\n if (iteratee) {\n seen = arrayMap(array, baseUnary(iteratee));\n }\n while (++index < length) {\n var fromIndex = 0,\n value = values[index],\n computed = iteratee ? iteratee(value) : value;\n\n while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) {\n if (seen !== array) {\n splice.call(seen, fromIndex, 1);\n }\n splice.call(array, fromIndex, 1);\n }\n }\n return array;\n }\n\n /**\n * The base implementation of `_.pullAt` without support for individual\n * indexes or capturing the removed elements.\n *\n * @private\n * @param {Array} array The array to modify.\n * @param {number[]} indexes The indexes of elements to remove.\n * @returns {Array} Returns `array`.\n */\n function basePullAt(array, indexes) {\n var length = array ? indexes.length : 0,\n lastIndex = length - 1;\n\n while (length--) {\n var index = indexes[length];\n if (length == lastIndex || index !== previous) {\n var previous = index;\n if (isIndex(index)) {\n splice.call(array, index, 1);\n } else {\n baseUnset(array, index);\n }\n }\n }\n return array;\n }\n\n /**\n * The base implementation of `_.random` without support for returning\n * floating-point numbers.\n *\n * @private\n * @param {number} lower The lower bound.\n * @param {number} upper The upper bound.\n * @returns {number} Returns the random number.\n */\n function baseRandom(lower, upper) {\n return lower + nativeFloor(nativeRandom() * (upper - lower + 1));\n }\n\n /**\n * The base implementation of `_.range` and `_.rangeRight` which doesn't\n * coerce arguments.\n *\n * @private\n * @param {number} start The start of the range.\n * @param {number} end The end of the range.\n * @param {number} step The value to increment or decrement by.\n * @param {boolean} [fromRight] Specify iterating from right to left.\n * @returns {Array} Returns the range of numbers.\n */\n function baseRange(start, end, step, fromRight) {\n var index = -1,\n length = nativeMax(nativeCeil((end - start) / (step || 1)), 0),\n result = Array(length);\n\n while (length--) {\n result[fromRight ? length : ++index] = start;\n start += step;\n }\n return result;\n }\n\n /**\n * The base implementation of `_.repeat` which doesn't coerce arguments.\n *\n * @private\n * @param {string} string The string to repeat.\n * @param {number} n The number of times to repeat the string.\n * @returns {string} Returns the repeated string.\n */\n function baseRepeat(string, n) {\n var result = '';\n if (!string || n < 1 || n > MAX_SAFE_INTEGER) {\n return result;\n }\n // Leverage the exponentiation by squaring algorithm for a faster repeat.\n // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details.\n do {\n if (n % 2) {\n result += string;\n }\n n = nativeFloor(n / 2);\n if (n) {\n string += string;\n }\n } while (n);\n\n return result;\n }\n\n /**\n * The base implementation of `_.rest` which doesn't validate or coerce arguments.\n *\n * @private\n * @param {Function} func The function to apply a rest parameter to.\n * @param {number} [start=func.length-1] The start position of the rest parameter.\n * @returns {Function} Returns the new function.\n */\n function baseRest(func, start) {\n return setToString(overRest(func, start, identity), func + '');\n }\n\n /**\n * The base implementation of `_.sample`.\n *\n * @private\n * @param {Array|Object} collection The collection to sample.\n * @returns {*} Returns the random element.\n */\n function baseSample(collection) {\n return arraySample(values(collection));\n }\n\n /**\n * The base implementation of `_.sampleSize` without param guards.\n *\n * @private\n * @param {Array|Object} collection The collection to sample.\n * @param {number} n The number of elements to sample.\n * @returns {Array} Returns the random elements.\n */\n function baseSampleSize(collection, n) {\n var array = values(collection);\n return shuffleSelf(array, baseClamp(n, 0, array.length));\n }\n\n /**\n * The base implementation of `_.set`.\n *\n * @private\n * @param {Object} object The object to modify.\n * @param {Array|string} path The path of the property to set.\n * @param {*} value The value to set.\n * @param {Function} [customizer] The function to customize path creation.\n * @returns {Object} Returns `object`.\n */\n function baseSet(object, path, value, customizer) {\n if (!isObject(object)) {\n return object;\n }\n path = castPath(path, object);\n\n var index = -1,\n length = path.length,\n lastIndex = length - 1,\n nested = object;\n\n while (nested != null && ++index < length) {\n var key = toKey(path[index]),\n newValue = value;\n\n if (index != lastIndex) {\n var objValue = nested[key];\n newValue = customizer ? customizer(objValue, key, nested) : undefined;\n if (newValue === undefined) {\n newValue = isObject(objValue)\n ? objValue\n : (isIndex(path[index + 1]) ? [] : {});\n }\n }\n assignValue(nested, key, newValue);\n nested = nested[key];\n }\n return object;\n }\n\n /**\n * The base implementation of `setData` without support for hot loop shorting.\n *\n * @private\n * @param {Function} func The function to associate metadata with.\n * @param {*} data The metadata.\n * @returns {Function} Returns `func`.\n */\n var baseSetData = !metaMap ? identity : function(func, data) {\n metaMap.set(func, data);\n return func;\n };\n\n /**\n * The base implementation of `setToString` without support for hot loop shorting.\n *\n * @private\n * @param {Function} func The function to modify.\n * @param {Function} string The `toString` result.\n * @returns {Function} Returns `func`.\n */\n var baseSetToString = !defineProperty ? identity : function(func, string) {\n return defineProperty(func, 'toString', {\n 'configurable': true,\n 'enumerable': false,\n 'value': constant(string),\n 'writable': true\n });\n };\n\n /**\n * The base implementation of `_.shuffle`.\n *\n * @private\n * @param {Array|Object} collection The collection to shuffle.\n * @returns {Array} Returns the new shuffled array.\n */\n function baseShuffle(collection) {\n return shuffleSelf(values(collection));\n }\n\n /**\n * The base implementation of `_.slice` without an iteratee call guard.\n *\n * @private\n * @param {Array} array The array to slice.\n * @param {number} [start=0] The start position.\n * @param {number} [end=array.length] The end position.\n * @returns {Array} Returns the slice of `array`.\n */\n function baseSlice(array, start, end) {\n var index = -1,\n length = array.length;\n\n if (start < 0) {\n start = -start > length ? 0 : (length + start);\n }\n end = end > length ? length : end;\n if (end < 0) {\n end += length;\n }\n length = start > end ? 0 : ((end - start) >>> 0);\n start >>>= 0;\n\n var result = Array(length);\n while (++index < length) {\n result[index] = array[index + start];\n }\n return result;\n }\n\n /**\n * The base implementation of `_.some` without support for iteratee shorthands.\n *\n * @private\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} predicate The function invoked per iteration.\n * @returns {boolean} Returns `true` if any element passes the predicate check,\n * else `false`.\n */\n function baseSome(collection, predicate) {\n var result;\n\n baseEach(collection, function(value, index, collection) {\n result = predicate(value, index, collection);\n return !result;\n });\n return !!result;\n }\n\n /**\n * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which\n * performs a binary search of `array` to determine the index at which `value`\n * should be inserted into `array` in order to maintain its sort order.\n *\n * @private\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @param {boolean} [retHighest] Specify returning the highest qualified index.\n * @returns {number} Returns the index at which `value` should be inserted\n * into `array`.\n */\n function baseSortedIndex(array, value, retHighest) {\n var low = 0,\n high = array == null ? low : array.length;\n\n if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) {\n while (low < high) {\n var mid = (low + high) >>> 1,\n computed = array[mid];\n\n if (computed !== null && !isSymbol(computed) &&\n (retHighest ? (computed <= value) : (computed < value))) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return high;\n }\n return baseSortedIndexBy(array, value, identity, retHighest);\n }\n\n /**\n * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy`\n * which invokes `iteratee` for `value` and each element of `array` to compute\n * their sort ranking. The iteratee is invoked with one argument; (value).\n *\n * @private\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @param {Function} iteratee The iteratee invoked per element.\n * @param {boolean} [retHighest] Specify returning the highest qualified index.\n * @returns {number} Returns the index at which `value` should be inserted\n * into `array`.\n */\n function baseSortedIndexBy(array, value, iteratee, retHighest) {\n value = iteratee(value);\n\n var low = 0,\n high = array == null ? 0 : array.length,\n valIsNaN = value !== value,\n valIsNull = value === null,\n valIsSymbol = isSymbol(value),\n valIsUndefined = value === undefined;\n\n while (low < high) {\n var mid = nativeFloor((low + high) / 2),\n computed = iteratee(array[mid]),\n othIsDefined = computed !== undefined,\n othIsNull = computed === null,\n othIsReflexive = computed === computed,\n othIsSymbol = isSymbol(computed);\n\n if (valIsNaN) {\n var setLow = retHighest || othIsReflexive;\n } else if (valIsUndefined) {\n setLow = othIsReflexive && (retHighest || othIsDefined);\n } else if (valIsNull) {\n setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);\n } else if (valIsSymbol) {\n setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol);\n } else if (othIsNull || othIsSymbol) {\n setLow = false;\n } else {\n setLow = retHighest ? (computed <= value) : (computed < value);\n }\n if (setLow) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return nativeMin(high, MAX_ARRAY_INDEX);\n }\n\n /**\n * The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without\n * support for iteratee shorthands.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n */\n function baseSortedUniq(array, iteratee) {\n var index = -1,\n length = array.length,\n resIndex = 0,\n result = [];\n\n while (++index < length) {\n var value = array[index],\n computed = iteratee ? iteratee(value) : value;\n\n if (!index || !eq(computed, seen)) {\n var seen = computed;\n result[resIndex++] = value === 0 ? 0 : value;\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `_.toNumber` which doesn't ensure correct\n * conversions of binary, hexadecimal, or octal string values.\n *\n * @private\n * @param {*} value The value to process.\n * @returns {number} Returns the number.\n */\n function baseToNumber(value) {\n if (typeof value == 'number') {\n return value;\n }\n if (isSymbol(value)) {\n return NAN;\n }\n return +value;\n }\n\n /**\n * The base implementation of `_.toString` which doesn't convert nullish\n * values to empty strings.\n *\n * @private\n * @param {*} value The value to process.\n * @returns {string} Returns the string.\n */\n function baseToString(value) {\n // Exit early for strings to avoid a performance hit in some environments.\n if (typeof value == 'string') {\n return value;\n }\n if (isArray(value)) {\n // Recursively convert values (susceptible to call stack limits).\n return arrayMap(value, baseToString) + '';\n }\n if (isSymbol(value)) {\n return symbolToString ? symbolToString.call(value) : '';\n }\n var result = (value + '');\n return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;\n }\n\n /**\n * The base implementation of `_.uniqBy` without support for iteratee shorthands.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n */\n function baseUniq(array, iteratee, comparator) {\n var index = -1,\n includes = arrayIncludes,\n length = array.length,\n isCommon = true,\n result = [],\n seen = result;\n\n if (comparator) {\n isCommon = false;\n includes = arrayIncludesWith;\n }\n else if (length >= LARGE_ARRAY_SIZE) {\n var set = iteratee ? null : createSet(array);\n if (set) {\n return setToArray(set);\n }\n isCommon = false;\n includes = cacheHas;\n seen = new SetCache;\n }\n else {\n seen = iteratee ? [] : result;\n }\n outer:\n while (++index < length) {\n var value = array[index],\n computed = iteratee ? iteratee(value) : value;\n\n value = (comparator || value !== 0) ? value : 0;\n if (isCommon && computed === computed) {\n var seenIndex = seen.length;\n while (seenIndex--) {\n if (seen[seenIndex] === computed) {\n continue outer;\n }\n }\n if (iteratee) {\n seen.push(computed);\n }\n result.push(value);\n }\n else if (!includes(seen, computed, comparator)) {\n if (seen !== result) {\n seen.push(computed);\n }\n result.push(value);\n }\n }\n return result;\n }\n\n /**\n * The base implementation of `_.unset`.\n *\n * @private\n * @param {Object} object The object to modify.\n * @param {Array|string} path The property path to unset.\n * @returns {boolean} Returns `true` if the property is deleted, else `false`.\n */\n function baseUnset(object, path) {\n path = castPath(path, object);\n object = parent(object, path);\n return object == null || delete object[toKey(last(path))];\n }\n\n /**\n * The base implementation of `_.update`.\n *\n * @private\n * @param {Object} object The object to modify.\n * @param {Array|string} path The path of the property to update.\n * @param {Function} updater The function to produce the updated value.\n * @param {Function} [customizer] The function to customize path creation.\n * @returns {Object} Returns `object`.\n */\n function baseUpdate(object, path, updater, customizer) {\n return baseSet(object, path, updater(baseGet(object, path)), customizer);\n }\n\n /**\n * The base implementation of methods like `_.dropWhile` and `_.takeWhile`\n * without support for iteratee shorthands.\n *\n * @private\n * @param {Array} array The array to query.\n * @param {Function} predicate The function invoked per iteration.\n * @param {boolean} [isDrop] Specify dropping elements instead of taking them.\n * @param {boolean} [fromRight] Specify iterating from right to left.\n * @returns {Array} Returns the slice of `array`.\n */\n function baseWhile(array, predicate, isDrop, fromRight) {\n var length = array.length,\n index = fromRight ? length : -1;\n\n while ((fromRight ? index-- : ++index < length) &&\n predicate(array[index], index, array)) {}\n\n return isDrop\n ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length))\n : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index));\n }\n\n /**\n * The base implementation of `wrapperValue` which returns the result of\n * performing a sequence of actions on the unwrapped `value`, where each\n * successive action is supplied the return value of the previous.\n *\n * @private\n * @param {*} value The unwrapped value.\n * @param {Array} actions Actions to perform to resolve the unwrapped value.\n * @returns {*} Returns the resolved value.\n */\n function baseWrapperValue(value, actions) {\n var result = value;\n if (result instanceof LazyWrapper) {\n result = result.value();\n }\n return arrayReduce(actions, function(result, action) {\n return action.func.apply(action.thisArg, arrayPush([result], action.args));\n }, result);\n }\n\n /**\n * The base implementation of methods like `_.xor`, without support for\n * iteratee shorthands, that accepts an array of arrays to inspect.\n *\n * @private\n * @param {Array} arrays The arrays to inspect.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of values.\n */\n function baseXor(arrays, iteratee, comparator) {\n var length = arrays.length;\n if (length < 2) {\n return length ? baseUniq(arrays[0]) : [];\n }\n var index = -1,\n result = Array(length);\n\n while (++index < length) {\n var array = arrays[index],\n othIndex = -1;\n\n while (++othIndex < length) {\n if (othIndex != index) {\n result[index] = baseDifference(result[index] || array, arrays[othIndex], iteratee, comparator);\n }\n }\n }\n return baseUniq(baseFlatten(result, 1), iteratee, comparator);\n }\n\n /**\n * This base implementation of `_.zipObject` which assigns values using `assignFunc`.\n *\n * @private\n * @param {Array} props The property identifiers.\n * @param {Array} values The property values.\n * @param {Function} assignFunc The function to assign values.\n * @returns {Object} Returns the new object.\n */\n function baseZipObject(props, values, assignFunc) {\n var index = -1,\n length = props.length,\n valsLength = values.length,\n result = {};\n\n while (++index < length) {\n var value = index < valsLength ? values[index] : undefined;\n assignFunc(result, props[index], value);\n }\n return result;\n }\n\n /**\n * Casts `value` to an empty array if it's not an array like object.\n *\n * @private\n * @param {*} value The value to inspect.\n * @returns {Array|Object} Returns the cast array-like object.\n */\n function castArrayLikeObject(value) {\n return isArrayLikeObject(value) ? value : [];\n }\n\n /**\n * Casts `value` to `identity` if it's not a function.\n *\n * @private\n * @param {*} value The value to inspect.\n * @returns {Function} Returns cast function.\n */\n function castFunction(value) {\n return typeof value == 'function' ? value : identity;\n }\n\n /**\n * Casts `value` to a path array if it's not one.\n *\n * @private\n * @param {*} value The value to inspect.\n * @param {Object} [object] The object to query keys on.\n * @returns {Array} Returns the cast property path array.\n */\n function castPath(value, object) {\n if (isArray(value)) {\n return value;\n }\n return isKey(value, object) ? [value] : stringToPath(toString(value));\n }\n\n /**\n * A `baseRest` alias which can be replaced with `identity` by module\n * replacement plugins.\n *\n * @private\n * @type {Function}\n * @param {Function} func The function to apply a rest parameter to.\n * @returns {Function} Returns the new function.\n */\n var castRest = baseRest;\n\n /**\n * Casts `array` to a slice if it's needed.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {number} start The start position.\n * @param {number} [end=array.length] The end position.\n * @returns {Array} Returns the cast slice.\n */\n function castSlice(array, start, end) {\n var length = array.length;\n end = end === undefined ? length : end;\n return (!start && end >= length) ? array : baseSlice(array, start, end);\n }\n\n /**\n * A simple wrapper around the global [`clearTimeout`](https://mdn.io/clearTimeout).\n *\n * @private\n * @param {number|Object} id The timer id or timeout object of the timer to clear.\n */\n var clearTimeout = ctxClearTimeout || function(id) {\n return root.clearTimeout(id);\n };\n\n /**\n * Creates a clone of `buffer`.\n *\n * @private\n * @param {Buffer} buffer The buffer to clone.\n * @param {boolean} [isDeep] Specify a deep clone.\n * @returns {Buffer} Returns the cloned buffer.\n */\n function cloneBuffer(buffer, isDeep) {\n if (isDeep) {\n return buffer.slice();\n }\n var length = buffer.length,\n result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);\n\n buffer.copy(result);\n return result;\n }\n\n /**\n * Creates a clone of `arrayBuffer`.\n *\n * @private\n * @param {ArrayBuffer} arrayBuffer The array buffer to clone.\n * @returns {ArrayBuffer} Returns the cloned array buffer.\n */\n function cloneArrayBuffer(arrayBuffer) {\n var result = new arrayBuffer.constructor(arrayBuffer.byteLength);\n new Uint8Array(result).set(new Uint8Array(arrayBuffer));\n return result;\n }\n\n /**\n * Creates a clone of `dataView`.\n *\n * @private\n * @param {Object} dataView The data view to clone.\n * @param {boolean} [isDeep] Specify a deep clone.\n * @returns {Object} Returns the cloned data view.\n */\n function cloneDataView(dataView, isDeep) {\n var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;\n return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);\n }\n\n /**\n * Creates a clone of `regexp`.\n *\n * @private\n * @param {Object} regexp The regexp to clone.\n * @returns {Object} Returns the cloned regexp.\n */\n function cloneRegExp(regexp) {\n var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));\n result.lastIndex = regexp.lastIndex;\n return result;\n }\n\n /**\n * Creates a clone of the `symbol` object.\n *\n * @private\n * @param {Object} symbol The symbol object to clone.\n * @returns {Object} Returns the cloned symbol object.\n */\n function cloneSymbol(symbol) {\n return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};\n }\n\n /**\n * Creates a clone of `typedArray`.\n *\n * @private\n * @param {Object} typedArray The typed array to clone.\n * @param {boolean} [isDeep] Specify a deep clone.\n * @returns {Object} Returns the cloned typed array.\n */\n function cloneTypedArray(typedArray, isDeep) {\n var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;\n return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);\n }\n\n /**\n * Compares values to sort them in ascending order.\n *\n * @private\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {number} Returns the sort order indicator for `value`.\n */\n function compareAscending(value, other) {\n if (value !== other) {\n var valIsDefined = value !== undefined,\n valIsNull = value === null,\n valIsReflexive = value === value,\n valIsSymbol = isSymbol(value);\n\n var othIsDefined = other !== undefined,\n othIsNull = other === null,\n othIsReflexive = other === other,\n othIsSymbol = isSymbol(other);\n\n if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) ||\n (valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) ||\n (valIsNull && othIsDefined && othIsReflexive) ||\n (!valIsDefined && othIsReflexive) ||\n !valIsReflexive) {\n return 1;\n }\n if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) ||\n (othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) ||\n (othIsNull && valIsDefined && valIsReflexive) ||\n (!othIsDefined && valIsReflexive) ||\n !othIsReflexive) {\n return -1;\n }\n }\n return 0;\n }\n\n /**\n * Used by `_.orderBy` to compare multiple properties of a value to another\n * and stable sort them.\n *\n * If `orders` is unspecified, all values are sorted in ascending order. Otherwise,\n * specify an order of \"desc\" for descending or \"asc\" for ascending sort order\n * of corresponding values.\n *\n * @private\n * @param {Object} object The object to compare.\n * @param {Object} other The other object to compare.\n * @param {boolean[]|string[]} orders The order to sort by for each property.\n * @returns {number} Returns the sort order indicator for `object`.\n */\n function compareMultiple(object, other, orders) {\n var index = -1,\n objCriteria = object.criteria,\n othCriteria = other.criteria,\n length = objCriteria.length,\n ordersLength = orders.length;\n\n while (++index < length) {\n var result = compareAscending(objCriteria[index], othCriteria[index]);\n if (result) {\n if (index >= ordersLength) {\n return result;\n }\n var order = orders[index];\n return result * (order == 'desc' ? -1 : 1);\n }\n }\n // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications\n // that causes it, under certain circumstances, to provide the same value for\n // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247\n // for more details.\n //\n // This also ensures a stable sort in V8 and other engines.\n // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details.\n return object.index - other.index;\n }\n\n /**\n * Creates an array that is the composition of partially applied arguments,\n * placeholders, and provided arguments into a single array of arguments.\n *\n * @private\n * @param {Array} args The provided arguments.\n * @param {Array} partials The arguments to prepend to those provided.\n * @param {Array} holders The `partials` placeholder indexes.\n * @params {boolean} [isCurried] Specify composing for a curried function.\n * @returns {Array} Returns the new array of composed arguments.\n */\n function composeArgs(args, partials, holders, isCurried) {\n var argsIndex = -1,\n argsLength = args.length,\n holdersLength = holders.length,\n leftIndex = -1,\n leftLength = partials.length,\n rangeLength = nativeMax(argsLength - holdersLength, 0),\n result = Array(leftLength + rangeLength),\n isUncurried = !isCurried;\n\n while (++leftIndex < leftLength) {\n result[leftIndex] = partials[leftIndex];\n }\n while (++argsIndex < holdersLength) {\n if (isUncurried || argsIndex < argsLength) {\n result[holders[argsIndex]] = args[argsIndex];\n }\n }\n while (rangeLength--) {\n result[leftIndex++] = args[argsIndex++];\n }\n return result;\n }\n\n /**\n * This function is like `composeArgs` except that the arguments composition\n * is tailored for `_.partialRight`.\n *\n * @private\n * @param {Array} args The provided arguments.\n * @param {Array} partials The arguments to append to those provided.\n * @param {Array} holders The `partials` placeholder indexes.\n * @params {boolean} [isCurried] Specify composing for a curried function.\n * @returns {Array} Returns the new array of composed arguments.\n */\n function composeArgsRight(args, partials, holders, isCurried) {\n var argsIndex = -1,\n argsLength = args.length,\n holdersIndex = -1,\n holdersLength = holders.length,\n rightIndex = -1,\n rightLength = partials.length,\n rangeLength = nativeMax(argsLength - holdersLength, 0),\n result = Array(rangeLength + rightLength),\n isUncurried = !isCurried;\n\n while (++argsIndex < rangeLength) {\n result[argsIndex] = args[argsIndex];\n }\n var offset = argsIndex;\n while (++rightIndex < rightLength) {\n result[offset + rightIndex] = partials[rightIndex];\n }\n while (++holdersIndex < holdersLength) {\n if (isUncurried || argsIndex < argsLength) {\n result[offset + holders[holdersIndex]] = args[argsIndex++];\n }\n }\n return result;\n }\n\n /**\n * Copies the values of `source` to `array`.\n *\n * @private\n * @param {Array} source The array to copy values from.\n * @param {Array} [array=[]] The array to copy values to.\n * @returns {Array} Returns `array`.\n */\n function copyArray(source, array) {\n var index = -1,\n length = source.length;\n\n array || (array = Array(length));\n while (++index < length) {\n array[index] = source[index];\n }\n return array;\n }\n\n /**\n * Copies properties of `source` to `object`.\n *\n * @private\n * @param {Object} source The object to copy properties from.\n * @param {Array} props The property identifiers to copy.\n * @param {Object} [object={}] The object to copy properties to.\n * @param {Function} [customizer] The function to customize copied values.\n * @returns {Object} Returns `object`.\n */\n function copyObject(source, props, object, customizer) {\n var isNew = !object;\n object || (object = {});\n\n var index = -1,\n length = props.length;\n\n while (++index < length) {\n var key = props[index];\n\n var newValue = customizer\n ? customizer(object[key], source[key], key, object, source)\n : undefined;\n\n if (newValue === undefined) {\n newValue = source[key];\n }\n if (isNew) {\n baseAssignValue(object, key, newValue);\n } else {\n assignValue(object, key, newValue);\n }\n }\n return object;\n }\n\n /**\n * Copies own symbols of `source` to `object`.\n *\n * @private\n * @param {Object} source The object to copy symbols from.\n * @param {Object} [object={}] The object to copy symbols to.\n * @returns {Object} Returns `object`.\n */\n function copySymbols(source, object) {\n return copyObject(source, getSymbols(source), object);\n }\n\n /**\n * Copies own and inherited symbols of `source` to `object`.\n *\n * @private\n * @param {Object} source The object to copy symbols from.\n * @param {Object} [object={}] The object to copy symbols to.\n * @returns {Object} Returns `object`.\n */\n function copySymbolsIn(source, object) {\n return copyObject(source, getSymbolsIn(source), object);\n }\n\n /**\n * Creates a function like `_.groupBy`.\n *\n * @private\n * @param {Function} setter The function to set accumulator values.\n * @param {Function} [initializer] The accumulator object initializer.\n * @returns {Function} Returns the new aggregator function.\n */\n function createAggregator(setter, initializer) {\n return function(collection, iteratee) {\n var func = isArray(collection) ? arrayAggregator : baseAggregator,\n accumulator = initializer ? initializer() : {};\n\n return func(collection, setter, getIteratee(iteratee, 2), accumulator);\n };\n }\n\n /**\n * Creates a function like `_.assign`.\n *\n * @private\n * @param {Function} assigner The function to assign values.\n * @returns {Function} Returns the new assigner function.\n */\n function createAssigner(assigner) {\n return baseRest(function(object, sources) {\n var index = -1,\n length = sources.length,\n customizer = length > 1 ? sources[length - 1] : undefined,\n guard = length > 2 ? sources[2] : undefined;\n\n customizer = (assigner.length > 3 && typeof customizer == 'function')\n ? (length--, customizer)\n : undefined;\n\n if (guard && isIterateeCall(sources[0], sources[1], guard)) {\n customizer = length < 3 ? undefined : customizer;\n length = 1;\n }\n object = Object(object);\n while (++index < length) {\n var source = sources[index];\n if (source) {\n assigner(object, source, index, customizer);\n }\n }\n return object;\n });\n }\n\n /**\n * Creates a `baseEach` or `baseEachRight` function.\n *\n * @private\n * @param {Function} eachFunc The function to iterate over a collection.\n * @param {boolean} [fromRight] Specify iterating from right to left.\n * @returns {Function} Returns the new base function.\n */\n function createBaseEach(eachFunc, fromRight) {\n return function(collection, iteratee) {\n if (collection == null) {\n return collection;\n }\n if (!isArrayLike(collection)) {\n return eachFunc(collection, iteratee);\n }\n var length = collection.length,\n index = fromRight ? length : -1,\n iterable = Object(collection);\n\n while ((fromRight ? index-- : ++index < length)) {\n if (iteratee(iterable[index], index, iterable) === false) {\n break;\n }\n }\n return collection;\n };\n }\n\n /**\n * Creates a base function for methods like `_.forIn` and `_.forOwn`.\n *\n * @private\n * @param {boolean} [fromRight] Specify iterating from right to left.\n * @returns {Function} Returns the new base function.\n */\n function createBaseFor(fromRight) {\n return function(object, iteratee, keysFunc) {\n var index = -1,\n iterable = Object(object),\n props = keysFunc(object),\n length = props.length;\n\n while (length--) {\n var key = props[fromRight ? length : ++index];\n if (iteratee(iterable[key], key, iterable) === false) {\n break;\n }\n }\n return object;\n };\n }\n\n /**\n * Creates a function that wraps `func` to invoke it with the optional `this`\n * binding of `thisArg`.\n *\n * @private\n * @param {Function} func The function to wrap.\n * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n * @param {*} [thisArg] The `this` binding of `func`.\n * @returns {Function} Returns the new wrapped function.\n */\n function createBind(func, bitmask, thisArg) {\n var isBind = bitmask & WRAP_BIND_FLAG,\n Ctor = createCtor(func);\n\n function wrapper() {\n var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;\n return fn.apply(isBind ? thisArg : this, arguments);\n }\n return wrapper;\n }\n\n /**\n * Creates a function like `_.lowerFirst`.\n *\n * @private\n * @param {string} methodName The name of the `String` case method to use.\n * @returns {Function} Returns the new case function.\n */\n function createCaseFirst(methodName) {\n return function(string) {\n string = toString(string);\n\n var strSymbols = hasUnicode(string)\n ? stringToArray(string)\n : undefined;\n\n var chr = strSymbols\n ? strSymbols[0]\n : string.charAt(0);\n\n var trailing = strSymbols\n ? castSlice(strSymbols, 1).join('')\n : string.slice(1);\n\n return chr[methodName]() + trailing;\n };\n }\n\n /**\n * Creates a function like `_.camelCase`.\n *\n * @private\n * @param {Function} callback The function to combine each word.\n * @returns {Function} Returns the new compounder function.\n */\n function createCompounder(callback) {\n return function(string) {\n return arrayReduce(words(deburr(string).replace(reApos, '')), callback, '');\n };\n }\n\n /**\n * Creates a function that produces an instance of `Ctor` regardless of\n * whether it was invoked as part of a `new` expression or by `call` or `apply`.\n *\n * @private\n * @param {Function} Ctor The constructor to wrap.\n * @returns {Function} Returns the new wrapped function.\n */\n function createCtor(Ctor) {\n return function() {\n // Use a `switch` statement to work with class constructors. See\n // http://ecma-international.org/ecma-262/7.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist\n // for more details.\n var args = arguments;\n switch (args.length) {\n case 0: return new Ctor;\n case 1: return new Ctor(args[0]);\n case 2: return new Ctor(args[0], args[1]);\n case 3: return new Ctor(args[0], args[1], args[2]);\n case 4: return new Ctor(args[0], args[1], args[2], args[3]);\n case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]);\n case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]);\n case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);\n }\n var thisBinding = baseCreate(Ctor.prototype),\n result = Ctor.apply(thisBinding, args);\n\n // Mimic the constructor's `return` behavior.\n // See https://es5.github.io/#x13.2.2 for more details.\n return isObject(result) ? result : thisBinding;\n };\n }\n\n /**\n * Creates a function that wraps `func` to enable currying.\n *\n * @private\n * @param {Function} func The function to wrap.\n * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n * @param {number} arity The arity of `func`.\n * @returns {Function} Returns the new wrapped function.\n */\n function createCurry(func, bitmask, arity) {\n var Ctor = createCtor(func);\n\n function wrapper() {\n var length = arguments.length,\n args = Array(length),\n index = length,\n placeholder = getHolder(wrapper);\n\n while (index--) {\n args[index] = arguments[index];\n }\n var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder)\n ? []\n : replaceHolders(args, placeholder);\n\n length -= holders.length;\n if (length < arity) {\n return createRecurry(\n func, bitmask, createHybrid, wrapper.placeholder, undefined,\n args, holders, undefined, undefined, arity - length);\n }\n var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;\n return apply(fn, this, args);\n }\n return wrapper;\n }\n\n /**\n * Creates a `_.find` or `_.findLast` function.\n *\n * @private\n * @param {Function} findIndexFunc The function to find the collection index.\n * @returns {Function} Returns the new find function.\n */\n function createFind(findIndexFunc) {\n return function(collection, predicate, fromIndex) {\n var iterable = Object(collection);\n if (!isArrayLike(collection)) {\n var iteratee = getIteratee(predicate, 3);\n collection = keys(collection);\n predicate = function(key) { return iteratee(iterable[key], key, iterable); };\n }\n var index = findIndexFunc(collection, predicate, fromIndex);\n return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined;\n };\n }\n\n /**\n * Creates a `_.flow` or `_.flowRight` function.\n *\n * @private\n * @param {boolean} [fromRight] Specify iterating from right to left.\n * @returns {Function} Returns the new flow function.\n */\n function createFlow(fromRight) {\n return flatRest(function(funcs) {\n var length = funcs.length,\n index = length,\n prereq = LodashWrapper.prototype.thru;\n\n if (fromRight) {\n funcs.reverse();\n }\n while (index--) {\n var func = funcs[index];\n if (typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n if (prereq && !wrapper && getFuncName(func) == 'wrapper') {\n var wrapper = new LodashWrapper([], true);\n }\n }\n index = wrapper ? index : length;\n while (++index < length) {\n func = funcs[index];\n\n var funcName = getFuncName(func),\n data = funcName == 'wrapper' ? getData(func) : undefined;\n\n if (data && isLaziable(data[0]) &&\n data[1] == (WRAP_ARY_FLAG | WRAP_CURRY_FLAG | WRAP_PARTIAL_FLAG | WRAP_REARG_FLAG) &&\n !data[4].length && data[9] == 1\n ) {\n wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]);\n } else {\n wrapper = (func.length == 1 && isLaziable(func))\n ? wrapper[funcName]()\n : wrapper.thru(func);\n }\n }\n return function() {\n var args = arguments,\n value = args[0];\n\n if (wrapper && args.length == 1 && isArray(value)) {\n return wrapper.plant(value).value();\n }\n var index = 0,\n result = length ? funcs[index].apply(this, args) : value;\n\n while (++index < length) {\n result = funcs[index].call(this, result);\n }\n return result;\n };\n });\n }\n\n /**\n * Creates a function that wraps `func` to invoke it with optional `this`\n * binding of `thisArg`, partial application, and currying.\n *\n * @private\n * @param {Function|string} func The function or method name to wrap.\n * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n * @param {*} [thisArg] The `this` binding of `func`.\n * @param {Array} [partials] The arguments to prepend to those provided to\n * the new function.\n * @param {Array} [holders] The `partials` placeholder indexes.\n * @param {Array} [partialsRight] The arguments to append to those provided\n * to the new function.\n * @param {Array} [holdersRight] The `partialsRight` placeholder indexes.\n * @param {Array} [argPos] The argument positions of the new function.\n * @param {number} [ary] The arity cap of `func`.\n * @param {number} [arity] The arity of `func`.\n * @returns {Function} Returns the new wrapped function.\n */\n function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {\n var isAry = bitmask & WRAP_ARY_FLAG,\n isBind = bitmask & WRAP_BIND_FLAG,\n isBindKey = bitmask & WRAP_BIND_KEY_FLAG,\n isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG),\n isFlip = bitmask & WRAP_FLIP_FLAG,\n Ctor = isBindKey ? undefined : createCtor(func);\n\n function wrapper() {\n var length = arguments.length,\n args = Array(length),\n index = length;\n\n while (index--) {\n args[index] = arguments[index];\n }\n if (isCurried) {\n var placeholder = getHolder(wrapper),\n holdersCount = countHolders(args, placeholder);\n }\n if (partials) {\n args = composeArgs(args, partials, holders, isCurried);\n }\n if (partialsRight) {\n args = composeArgsRight(args, partialsRight, holdersRight, isCurried);\n }\n length -= holdersCount;\n if (isCurried && length < arity) {\n var newHolders = replaceHolders(args, placeholder);\n return createRecurry(\n func, bitmask, createHybrid, wrapper.placeholder, thisArg,\n args, newHolders, argPos, ary, arity - length\n );\n }\n var thisBinding = isBind ? thisArg : this,\n fn = isBindKey ? thisBinding[func] : func;\n\n length = args.length;\n if (argPos) {\n args = reorder(args, argPos);\n } else if (isFlip && length > 1) {\n args.reverse();\n }\n if (isAry && ary < length) {\n args.length = ary;\n }\n if (this && this !== root && this instanceof wrapper) {\n fn = Ctor || createCtor(fn);\n }\n return fn.apply(thisBinding, args);\n }\n return wrapper;\n }\n\n /**\n * Creates a function like `_.invertBy`.\n *\n * @private\n * @param {Function} setter The function to set accumulator values.\n * @param {Function} toIteratee The function to resolve iteratees.\n * @returns {Function} Returns the new inverter function.\n */\n function createInverter(setter, toIteratee) {\n return function(object, iteratee) {\n return baseInverter(object, setter, toIteratee(iteratee), {});\n };\n }\n\n /**\n * Creates a function that performs a mathematical operation on two values.\n *\n * @private\n * @param {Function} operator The function to perform the operation.\n * @param {number} [defaultValue] The value used for `undefined` arguments.\n * @returns {Function} Returns the new mathematical operation function.\n */\n function createMathOperation(operator, defaultValue) {\n return function(value, other) {\n var result;\n if (value === undefined && other === undefined) {\n return defaultValue;\n }\n if (value !== undefined) {\n result = value;\n }\n if (other !== undefined) {\n if (result === undefined) {\n return other;\n }\n if (typeof value == 'string' || typeof other == 'string') {\n value = baseToString(value);\n other = baseToString(other);\n } else {\n value = baseToNumber(value);\n other = baseToNumber(other);\n }\n result = operator(value, other);\n }\n return result;\n };\n }\n\n /**\n * Creates a function like `_.over`.\n *\n * @private\n * @param {Function} arrayFunc The function to iterate over iteratees.\n * @returns {Function} Returns the new over function.\n */\n function createOver(arrayFunc) {\n return flatRest(function(iteratees) {\n iteratees = arrayMap(iteratees, baseUnary(getIteratee()));\n return baseRest(function(args) {\n var thisArg = this;\n return arrayFunc(iteratees, function(iteratee) {\n return apply(iteratee, thisArg, args);\n });\n });\n });\n }\n\n /**\n * Creates the padding for `string` based on `length`. The `chars` string\n * is truncated if the number of characters exceeds `length`.\n *\n * @private\n * @param {number} length The padding length.\n * @param {string} [chars=' '] The string used as padding.\n * @returns {string} Returns the padding for `string`.\n */\n function createPadding(length, chars) {\n chars = chars === undefined ? ' ' : baseToString(chars);\n\n var charsLength = chars.length;\n if (charsLength < 2) {\n return charsLength ? baseRepeat(chars, length) : chars;\n }\n var result = baseRepeat(chars, nativeCeil(length / stringSize(chars)));\n return hasUnicode(chars)\n ? castSlice(stringToArray(result), 0, length).join('')\n : result.slice(0, length);\n }\n\n /**\n * Creates a function that wraps `func` to invoke it with the `this` binding\n * of `thisArg` and `partials` prepended to the arguments it receives.\n *\n * @private\n * @param {Function} func The function to wrap.\n * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n * @param {*} thisArg The `this` binding of `func`.\n * @param {Array} partials The arguments to prepend to those provided to\n * the new function.\n * @returns {Function} Returns the new wrapped function.\n */\n function createPartial(func, bitmask, thisArg, partials) {\n var isBind = bitmask & WRAP_BIND_FLAG,\n Ctor = createCtor(func);\n\n function wrapper() {\n var argsIndex = -1,\n argsLength = arguments.length,\n leftIndex = -1,\n leftLength = partials.length,\n args = Array(leftLength + argsLength),\n fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;\n\n while (++leftIndex < leftLength) {\n args[leftIndex] = partials[leftIndex];\n }\n while (argsLength--) {\n args[leftIndex++] = arguments[++argsIndex];\n }\n return apply(fn, isBind ? thisArg : this, args);\n }\n return wrapper;\n }\n\n /**\n * Creates a `_.range` or `_.rangeRight` function.\n *\n * @private\n * @param {boolean} [fromRight] Specify iterating from right to left.\n * @returns {Function} Returns the new range function.\n */\n function createRange(fromRight) {\n return function(start, end, step) {\n if (step && typeof step != 'number' && isIterateeCall(start, end, step)) {\n end = step = undefined;\n }\n // Ensure the sign of `-0` is preserved.\n start = toFinite(start);\n if (end === undefined) {\n end = start;\n start = 0;\n } else {\n end = toFinite(end);\n }\n step = step === undefined ? (start < end ? 1 : -1) : toFinite(step);\n return baseRange(start, end, step, fromRight);\n };\n }\n\n /**\n * Creates a function that performs a relational operation on two values.\n *\n * @private\n * @param {Function} operator The function to perform the operation.\n * @returns {Function} Returns the new relational operation function.\n */\n function createRelationalOperation(operator) {\n return function(value, other) {\n if (!(typeof value == 'string' && typeof other == 'string')) {\n value = toNumber(value);\n other = toNumber(other);\n }\n return operator(value, other);\n };\n }\n\n /**\n * Creates a function that wraps `func` to continue currying.\n *\n * @private\n * @param {Function} func The function to wrap.\n * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n * @param {Function} wrapFunc The function to create the `func` wrapper.\n * @param {*} placeholder The placeholder value.\n * @param {*} [thisArg] The `this` binding of `func`.\n * @param {Array} [partials] The arguments to prepend to those provided to\n * the new function.\n * @param {Array} [holders] The `partials` placeholder indexes.\n * @param {Array} [argPos] The argument positions of the new function.\n * @param {number} [ary] The arity cap of `func`.\n * @param {number} [arity] The arity of `func`.\n * @returns {Function} Returns the new wrapped function.\n */\n function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {\n var isCurry = bitmask & WRAP_CURRY_FLAG,\n newHolders = isCurry ? holders : undefined,\n newHoldersRight = isCurry ? undefined : holders,\n newPartials = isCurry ? partials : undefined,\n newPartialsRight = isCurry ? undefined : partials;\n\n bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG);\n bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG);\n\n if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) {\n bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG);\n }\n var newData = [\n func, bitmask, thisArg, newPartials, newHolders, newPartialsRight,\n newHoldersRight, argPos, ary, arity\n ];\n\n var result = wrapFunc.apply(undefined, newData);\n if (isLaziable(func)) {\n setData(result, newData);\n }\n result.placeholder = placeholder;\n return setWrapToString(result, func, bitmask);\n }\n\n /**\n * Creates a function like `_.round`.\n *\n * @private\n * @param {string} methodName The name of the `Math` method to use when rounding.\n * @returns {Function} Returns the new round function.\n */\n function createRound(methodName) {\n var func = Math[methodName];\n return function(number, precision) {\n number = toNumber(number);\n precision = precision == null ? 0 : nativeMin(toInteger(precision), 292);\n if (precision) {\n // Shift with exponential notation to avoid floating-point issues.\n // See [MDN](https://mdn.io/round#Examples) for more details.\n var pair = (toString(number) + 'e').split('e'),\n value = func(pair[0] + 'e' + (+pair[1] + precision));\n\n pair = (toString(value) + 'e').split('e');\n return +(pair[0] + 'e' + (+pair[1] - precision));\n }\n return func(number);\n };\n }\n\n /**\n * Creates a set object of `values`.\n *\n * @private\n * @param {Array} values The values to add to the set.\n * @returns {Object} Returns the new set.\n */\n var createSet = !(Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY) ? noop : function(values) {\n return new Set(values);\n };\n\n /**\n * Creates a `_.toPairs` or `_.toPairsIn` function.\n *\n * @private\n * @param {Function} keysFunc The function to get the keys of a given object.\n * @returns {Function} Returns the new pairs function.\n */\n function createToPairs(keysFunc) {\n return function(object) {\n var tag = getTag(object);\n if (tag == mapTag) {\n return mapToArray(object);\n }\n if (tag == setTag) {\n return setToPairs(object);\n }\n return baseToPairs(object, keysFunc(object));\n };\n }\n\n /**\n * Creates a function that either curries or invokes `func` with optional\n * `this` binding and partially applied arguments.\n *\n * @private\n * @param {Function|string} func The function or method name to wrap.\n * @param {number} bitmask The bitmask flags.\n * 1 - `_.bind`\n * 2 - `_.bindKey`\n * 4 - `_.curry` or `_.curryRight` of a bound function\n * 8 - `_.curry`\n * 16 - `_.curryRight`\n * 32 - `_.partial`\n * 64 - `_.partialRight`\n * 128 - `_.rearg`\n * 256 - `_.ary`\n * 512 - `_.flip`\n * @param {*} [thisArg] The `this` binding of `func`.\n * @param {Array} [partials] The arguments to be partially applied.\n * @param {Array} [holders] The `partials` placeholder indexes.\n * @param {Array} [argPos] The argument positions of the new function.\n * @param {number} [ary] The arity cap of `func`.\n * @param {number} [arity] The arity of `func`.\n * @returns {Function} Returns the new wrapped function.\n */\n function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {\n var isBindKey = bitmask & WRAP_BIND_KEY_FLAG;\n if (!isBindKey && typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n var length = partials ? partials.length : 0;\n if (!length) {\n bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG);\n partials = holders = undefined;\n }\n ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0);\n arity = arity === undefined ? arity : toInteger(arity);\n length -= holders ? holders.length : 0;\n\n if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) {\n var partialsRight = partials,\n holdersRight = holders;\n\n partials = holders = undefined;\n }\n var data = isBindKey ? undefined : getData(func);\n\n var newData = [\n func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,\n argPos, ary, arity\n ];\n\n if (data) {\n mergeData(newData, data);\n }\n func = newData[0];\n bitmask = newData[1];\n thisArg = newData[2];\n partials = newData[3];\n holders = newData[4];\n arity = newData[9] = newData[9] === undefined\n ? (isBindKey ? 0 : func.length)\n : nativeMax(newData[9] - length, 0);\n\n if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) {\n bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG);\n }\n if (!bitmask || bitmask == WRAP_BIND_FLAG) {\n var result = createBind(func, bitmask, thisArg);\n } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) {\n result = createCurry(func, bitmask, arity);\n } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) {\n result = createPartial(func, bitmask, thisArg, partials);\n } else {\n result = createHybrid.apply(undefined, newData);\n }\n var setter = data ? baseSetData : setData;\n return setWrapToString(setter(result, newData), func, bitmask);\n }\n\n /**\n * Used by `_.defaults` to customize its `_.assignIn` use to assign properties\n * of source objects to the destination object for all destination properties\n * that resolve to `undefined`.\n *\n * @private\n * @param {*} objValue The destination value.\n * @param {*} srcValue The source value.\n * @param {string} key The key of the property to assign.\n * @param {Object} object The parent object of `objValue`.\n * @returns {*} Returns the value to assign.\n */\n function customDefaultsAssignIn(objValue, srcValue, key, object) {\n if (objValue === undefined ||\n (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) {\n return srcValue;\n }\n return objValue;\n }\n\n /**\n * Used by `_.defaultsDeep` to customize its `_.merge` use to merge source\n * objects into destination objects that are passed thru.\n *\n * @private\n * @param {*} objValue The destination value.\n * @param {*} srcValue The source value.\n * @param {string} key The key of the property to merge.\n * @param {Object} object The parent object of `objValue`.\n * @param {Object} source The parent object of `srcValue`.\n * @param {Object} [stack] Tracks traversed source values and their merged\n * counterparts.\n * @returns {*} Returns the value to assign.\n */\n function customDefaultsMerge(objValue, srcValue, key, object, source, stack) {\n if (isObject(objValue) && isObject(srcValue)) {\n // Recursively merge objects and arrays (susceptible to call stack limits).\n stack.set(srcValue, objValue);\n baseMerge(objValue, srcValue, undefined, customDefaultsMerge, stack);\n stack['delete'](srcValue);\n }\n return objValue;\n }\n\n /**\n * Used by `_.omit` to customize its `_.cloneDeep` use to only clone plain\n * objects.\n *\n * @private\n * @param {*} value The value to inspect.\n * @param {string} key The key of the property to inspect.\n * @returns {*} Returns the uncloned value or `undefined` to defer cloning to `_.cloneDeep`.\n */\n function customOmitClone(value) {\n return isPlainObject(value) ? undefined : value;\n }\n\n /**\n * A specialized version of `baseIsEqualDeep` for arrays with support for\n * partial deep comparisons.\n *\n * @private\n * @param {Array} array The array to compare.\n * @param {Array} other The other array to compare.\n * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.\n * @param {Function} customizer The function to customize comparisons.\n * @param {Function} equalFunc The function to determine equivalents of values.\n * @param {Object} stack Tracks traversed `array` and `other` objects.\n * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.\n */\n function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {\n var isPartial = bitmask & COMPARE_PARTIAL_FLAG,\n arrLength = array.length,\n othLength = other.length;\n\n if (arrLength != othLength && !(isPartial && othLength > arrLength)) {\n return false;\n }\n // Assume cyclic values are equal.\n var stacked = stack.get(array);\n if (stacked && stack.get(other)) {\n return stacked == other;\n }\n var index = -1,\n result = true,\n seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined;\n\n stack.set(array, other);\n stack.set(other, array);\n\n // Ignore non-index properties.\n while (++index < arrLength) {\n var arrValue = array[index],\n othValue = other[index];\n\n if (customizer) {\n var compared = isPartial\n ? customizer(othValue, arrValue, index, other, array, stack)\n : customizer(arrValue, othValue, index, array, other, stack);\n }\n if (compared !== undefined) {\n if (compared) {\n continue;\n }\n result = false;\n break;\n }\n // Recursively compare arrays (susceptible to call stack limits).\n if (seen) {\n if (!arraySome(other, function(othValue, othIndex) {\n if (!cacheHas(seen, othIndex) &&\n (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {\n return seen.push(othIndex);\n }\n })) {\n result = false;\n break;\n }\n } else if (!(\n arrValue === othValue ||\n equalFunc(arrValue, othValue, bitmask, customizer, stack)\n )) {\n result = false;\n break;\n }\n }\n stack['delete'](array);\n stack['delete'](other);\n return result;\n }\n\n /**\n * A specialized version of `baseIsEqualDeep` for comparing objects of\n * the same `toStringTag`.\n *\n * **Note:** This function only supports comparing values with tags of\n * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.\n *\n * @private\n * @param {Object} object The object to compare.\n * @param {Object} other The other object to compare.\n * @param {string} tag The `toStringTag` of the objects to compare.\n * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.\n * @param {Function} customizer The function to customize comparisons.\n * @param {Function} equalFunc The function to determine equivalents of values.\n * @param {Object} stack Tracks traversed `object` and `other` objects.\n * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.\n */\n function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {\n switch (tag) {\n case dataViewTag:\n if ((object.byteLength != other.byteLength) ||\n (object.byteOffset != other.byteOffset)) {\n return false;\n }\n object = object.buffer;\n other = other.buffer;\n\n case arrayBufferTag:\n if ((object.byteLength != other.byteLength) ||\n !equalFunc(new Uint8Array(object), new Uint8Array(other))) {\n return false;\n }\n return true;\n\n case boolTag:\n case dateTag:\n case numberTag:\n // Coerce booleans to `1` or `0` and dates to milliseconds.\n // Invalid dates are coerced to `NaN`.\n return eq(+object, +other);\n\n case errorTag:\n return object.name == other.name && object.message == other.message;\n\n case regexpTag:\n case stringTag:\n // Coerce regexes to strings and treat strings, primitives and objects,\n // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring\n // for more details.\n return object == (other + '');\n\n case mapTag:\n var convert = mapToArray;\n\n case setTag:\n var isPartial = bitmask & COMPARE_PARTIAL_FLAG;\n convert || (convert = setToArray);\n\n if (object.size != other.size && !isPartial) {\n return false;\n }\n // Assume cyclic values are equal.\n var stacked = stack.get(object);\n if (stacked) {\n return stacked == other;\n }\n bitmask |= COMPARE_UNORDERED_FLAG;\n\n // Recursively compare objects (susceptible to call stack limits).\n stack.set(object, other);\n var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);\n stack['delete'](object);\n return result;\n\n case symbolTag:\n if (symbolValueOf) {\n return symbolValueOf.call(object) == symbolValueOf.call(other);\n }\n }\n return false;\n }\n\n /**\n * A specialized version of `baseIsEqualDeep` for objects with support for\n * partial deep comparisons.\n *\n * @private\n * @param {Object} object The object to compare.\n * @param {Object} other The other object to compare.\n * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.\n * @param {Function} customizer The function to customize comparisons.\n * @param {Function} equalFunc The function to determine equivalents of values.\n * @param {Object} stack Tracks traversed `object` and `other` objects.\n * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.\n */\n function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {\n var isPartial = bitmask & COMPARE_PARTIAL_FLAG,\n objProps = getAllKeys(object),\n objLength = objProps.length,\n othProps = getAllKeys(other),\n othLength = othProps.length;\n\n if (objLength != othLength && !isPartial) {\n return false;\n }\n var index = objLength;\n while (index--) {\n var key = objProps[index];\n if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {\n return false;\n }\n }\n // Assume cyclic values are equal.\n var stacked = stack.get(object);\n if (stacked && stack.get(other)) {\n return stacked == other;\n }\n var result = true;\n stack.set(object, other);\n stack.set(other, object);\n\n var skipCtor = isPartial;\n while (++index < objLength) {\n key = objProps[index];\n var objValue = object[key],\n othValue = other[key];\n\n if (customizer) {\n var compared = isPartial\n ? customizer(othValue, objValue, key, other, object, stack)\n : customizer(objValue, othValue, key, object, other, stack);\n }\n // Recursively compare objects (susceptible to call stack limits).\n if (!(compared === undefined\n ? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack))\n : compared\n )) {\n result = false;\n break;\n }\n skipCtor || (skipCtor = key == 'constructor');\n }\n if (result && !skipCtor) {\n var objCtor = object.constructor,\n othCtor = other.constructor;\n\n // Non `Object` object instances with different constructors are not equal.\n if (objCtor != othCtor &&\n ('constructor' in object && 'constructor' in other) &&\n !(typeof objCtor == 'function' && objCtor instanceof objCtor &&\n typeof othCtor == 'function' && othCtor instanceof othCtor)) {\n result = false;\n }\n }\n stack['delete'](object);\n stack['delete'](other);\n return result;\n }\n\n /**\n * A specialized version of `baseRest` which flattens the rest array.\n *\n * @private\n * @param {Function} func The function to apply a rest parameter to.\n * @returns {Function} Returns the new function.\n */\n function flatRest(func) {\n return setToString(overRest(func, undefined, flatten), func + '');\n }\n\n /**\n * Creates an array of own enumerable property names and symbols of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property names and symbols.\n */\n function getAllKeys(object) {\n return baseGetAllKeys(object, keys, getSymbols);\n }\n\n /**\n * Creates an array of own and inherited enumerable property names and\n * symbols of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property names and symbols.\n */\n function getAllKeysIn(object) {\n return baseGetAllKeys(object, keysIn, getSymbolsIn);\n }\n\n /**\n * Gets metadata for `func`.\n *\n * @private\n * @param {Function} func The function to query.\n * @returns {*} Returns the metadata for `func`.\n */\n var getData = !metaMap ? noop : function(func) {\n return metaMap.get(func);\n };\n\n /**\n * Gets the name of `func`.\n *\n * @private\n * @param {Function} func The function to query.\n * @returns {string} Returns the function name.\n */\n function getFuncName(func) {\n var result = (func.name + ''),\n array = realNames[result],\n length = hasOwnProperty.call(realNames, result) ? array.length : 0;\n\n while (length--) {\n var data = array[length],\n otherFunc = data.func;\n if (otherFunc == null || otherFunc == func) {\n return data.name;\n }\n }\n return result;\n }\n\n /**\n * Gets the argument placeholder value for `func`.\n *\n * @private\n * @param {Function} func The function to inspect.\n * @returns {*} Returns the placeholder value.\n */\n function getHolder(func) {\n var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func;\n return object.placeholder;\n }\n\n /**\n * Gets the appropriate \"iteratee\" function. If `_.iteratee` is customized,\n * this function returns the custom method, otherwise it returns `baseIteratee`.\n * If arguments are provided, the chosen function is invoked with them and\n * its result is returned.\n *\n * @private\n * @param {*} [value] The value to convert to an iteratee.\n * @param {number} [arity] The arity of the created iteratee.\n * @returns {Function} Returns the chosen function or its result.\n */\n function getIteratee() {\n var result = lodash.iteratee || iteratee;\n result = result === iteratee ? baseIteratee : result;\n return arguments.length ? result(arguments[0], arguments[1]) : result;\n }\n\n /**\n * Gets the data for `map`.\n *\n * @private\n * @param {Object} map The map to query.\n * @param {string} key The reference key.\n * @returns {*} Returns the map data.\n */\n function getMapData(map, key) {\n var data = map.__data__;\n return isKeyable(key)\n ? data[typeof key == 'string' ? 'string' : 'hash']\n : data.map;\n }\n\n /**\n * Gets the property names, values, and compare flags of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the match data of `object`.\n */\n function getMatchData(object) {\n var result = keys(object),\n length = result.length;\n\n while (length--) {\n var key = result[length],\n value = object[key];\n\n result[length] = [key, value, isStrictComparable(value)];\n }\n return result;\n }\n\n /**\n * Gets the native function at `key` of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {string} key The key of the method to get.\n * @returns {*} Returns the function if it's native, else `undefined`.\n */\n function getNative(object, key) {\n var value = getValue(object, key);\n return baseIsNative(value) ? value : undefined;\n }\n\n /**\n * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.\n *\n * @private\n * @param {*} value The value to query.\n * @returns {string} Returns the raw `toStringTag`.\n */\n function getRawTag(value) {\n var isOwn = hasOwnProperty.call(value, symToStringTag),\n tag = value[symToStringTag];\n\n try {\n value[symToStringTag] = undefined;\n var unmasked = true;\n } catch (e) {}\n\n var result = nativeObjectToString.call(value);\n if (unmasked) {\n if (isOwn) {\n value[symToStringTag] = tag;\n } else {\n delete value[symToStringTag];\n }\n }\n return result;\n }\n\n /**\n * Creates an array of the own enumerable symbols of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of symbols.\n */\n var getSymbols = !nativeGetSymbols ? stubArray : function(object) {\n if (object == null) {\n return [];\n }\n object = Object(object);\n return arrayFilter(nativeGetSymbols(object), function(symbol) {\n return propertyIsEnumerable.call(object, symbol);\n });\n };\n\n /**\n * Creates an array of the own and inherited enumerable symbols of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of symbols.\n */\n var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) {\n var result = [];\n while (object) {\n arrayPush(result, getSymbols(object));\n object = getPrototype(object);\n }\n return result;\n };\n\n /**\n * Gets the `toStringTag` of `value`.\n *\n * @private\n * @param {*} value The value to query.\n * @returns {string} Returns the `toStringTag`.\n */\n var getTag = baseGetTag;\n\n // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.\n if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||\n (Map && getTag(new Map) != mapTag) ||\n (Promise && getTag(Promise.resolve()) != promiseTag) ||\n (Set && getTag(new Set) != setTag) ||\n (WeakMap && getTag(new WeakMap) != weakMapTag)) {\n getTag = function(value) {\n var result = baseGetTag(value),\n Ctor = result == objectTag ? value.constructor : undefined,\n ctorString = Ctor ? toSource(Ctor) : '';\n\n if (ctorString) {\n switch (ctorString) {\n case dataViewCtorString: return dataViewTag;\n case mapCtorString: return mapTag;\n case promiseCtorString: return promiseTag;\n case setCtorString: return setTag;\n case weakMapCtorString: return weakMapTag;\n }\n }\n return result;\n };\n }\n\n /**\n * Gets the view, applying any `transforms` to the `start` and `end` positions.\n *\n * @private\n * @param {number} start The start of the view.\n * @param {number} end The end of the view.\n * @param {Array} transforms The transformations to apply to the view.\n * @returns {Object} Returns an object containing the `start` and `end`\n * positions of the view.\n */\n function getView(start, end, transforms) {\n var index = -1,\n length = transforms.length;\n\n while (++index < length) {\n var data = transforms[index],\n size = data.size;\n\n switch (data.type) {\n case 'drop': start += size; break;\n case 'dropRight': end -= size; break;\n case 'take': end = nativeMin(end, start + size); break;\n case 'takeRight': start = nativeMax(start, end - size); break;\n }\n }\n return { 'start': start, 'end': end };\n }\n\n /**\n * Extracts wrapper details from the `source` body comment.\n *\n * @private\n * @param {string} source The source to inspect.\n * @returns {Array} Returns the wrapper details.\n */\n function getWrapDetails(source) {\n var match = source.match(reWrapDetails);\n return match ? match[1].split(reSplitDetails) : [];\n }\n\n /**\n * Checks if `path` exists on `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Array|string} path The path to check.\n * @param {Function} hasFunc The function to check properties.\n * @returns {boolean} Returns `true` if `path` exists, else `false`.\n */\n function hasPath(object, path, hasFunc) {\n path = castPath(path, object);\n\n var index = -1,\n length = path.length,\n result = false;\n\n while (++index < length) {\n var key = toKey(path[index]);\n if (!(result = object != null && hasFunc(object, key))) {\n break;\n }\n object = object[key];\n }\n if (result || ++index != length) {\n return result;\n }\n length = object == null ? 0 : object.length;\n return !!length && isLength(length) && isIndex(key, length) &&\n (isArray(object) || isArguments(object));\n }\n\n /**\n * Initializes an array clone.\n *\n * @private\n * @param {Array} array The array to clone.\n * @returns {Array} Returns the initialized clone.\n */\n function initCloneArray(array) {\n var length = array.length,\n result = new array.constructor(length);\n\n // Add properties assigned by `RegExp#exec`.\n if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {\n result.index = array.index;\n result.input = array.input;\n }\n return result;\n }\n\n /**\n * Initializes an object clone.\n *\n * @private\n * @param {Object} object The object to clone.\n * @returns {Object} Returns the initialized clone.\n */\n function initCloneObject(object) {\n return (typeof object.constructor == 'function' && !isPrototype(object))\n ? baseCreate(getPrototype(object))\n : {};\n }\n\n /**\n * Initializes an object clone based on its `toStringTag`.\n *\n * **Note:** This function only supports cloning values with tags of\n * `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`.\n *\n * @private\n * @param {Object} object The object to clone.\n * @param {string} tag The `toStringTag` of the object to clone.\n * @param {boolean} [isDeep] Specify a deep clone.\n * @returns {Object} Returns the initialized clone.\n */\n function initCloneByTag(object, tag, isDeep) {\n var Ctor = object.constructor;\n switch (tag) {\n case arrayBufferTag:\n return cloneArrayBuffer(object);\n\n case boolTag:\n case dateTag:\n return new Ctor(+object);\n\n case dataViewTag:\n return cloneDataView(object, isDeep);\n\n case float32Tag: case float64Tag:\n case int8Tag: case int16Tag: case int32Tag:\n case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:\n return cloneTypedArray(object, isDeep);\n\n case mapTag:\n return new Ctor;\n\n case numberTag:\n case stringTag:\n return new Ctor(object);\n\n case regexpTag:\n return cloneRegExp(object);\n\n case setTag:\n return new Ctor;\n\n case symbolTag:\n return cloneSymbol(object);\n }\n }\n\n /**\n * Inserts wrapper `details` in a comment at the top of the `source` body.\n *\n * @private\n * @param {string} source The source to modify.\n * @returns {Array} details The details to insert.\n * @returns {string} Returns the modified source.\n */\n function insertWrapDetails(source, details) {\n var length = details.length;\n if (!length) {\n return source;\n }\n var lastIndex = length - 1;\n details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex];\n details = details.join(length > 2 ? ', ' : ' ');\n return source.replace(reWrapComment, '{\\n/* [wrapped with ' + details + '] */\\n');\n }\n\n /**\n * Checks if `value` is a flattenable `arguments` object or array.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is flattenable, else `false`.\n */\n function isFlattenable(value) {\n return isArray(value) || isArguments(value) ||\n !!(spreadableSymbol && value && value[spreadableSymbol]);\n }\n\n /**\n * Checks if `value` is a valid array-like index.\n *\n * @private\n * @param {*} value The value to check.\n * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.\n * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.\n */\n function isIndex(value, length) {\n var type = typeof value;\n length = length == null ? MAX_SAFE_INTEGER : length;\n\n return !!length &&\n (type == 'number' ||\n (type != 'symbol' && reIsUint.test(value))) &&\n (value > -1 && value % 1 == 0 && value < length);\n }\n\n /**\n * Checks if the given arguments are from an iteratee call.\n *\n * @private\n * @param {*} value The potential iteratee value argument.\n * @param {*} index The potential iteratee index or key argument.\n * @param {*} object The potential iteratee object argument.\n * @returns {boolean} Returns `true` if the arguments are from an iteratee call,\n * else `false`.\n */\n function isIterateeCall(value, index, object) {\n if (!isObject(object)) {\n return false;\n }\n var type = typeof index;\n if (type == 'number'\n ? (isArrayLike(object) && isIndex(index, object.length))\n : (type == 'string' && index in object)\n ) {\n return eq(object[index], value);\n }\n return false;\n }\n\n /**\n * Checks if `value` is a property name and not a property path.\n *\n * @private\n * @param {*} value The value to check.\n * @param {Object} [object] The object to query keys on.\n * @returns {boolean} Returns `true` if `value` is a property name, else `false`.\n */\n function isKey(value, object) {\n if (isArray(value)) {\n return false;\n }\n var type = typeof value;\n if (type == 'number' || type == 'symbol' || type == 'boolean' ||\n value == null || isSymbol(value)) {\n return true;\n }\n return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||\n (object != null && value in Object(object));\n }\n\n /**\n * Checks if `value` is suitable for use as unique object key.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is suitable, else `false`.\n */\n function isKeyable(value) {\n var type = typeof value;\n return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')\n ? (value !== '__proto__')\n : (value === null);\n }\n\n /**\n * Checks if `func` has a lazy counterpart.\n *\n * @private\n * @param {Function} func The function to check.\n * @returns {boolean} Returns `true` if `func` has a lazy counterpart,\n * else `false`.\n */\n function isLaziable(func) {\n var funcName = getFuncName(func),\n other = lodash[funcName];\n\n if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) {\n return false;\n }\n if (func === other) {\n return true;\n }\n var data = getData(other);\n return !!data && func === data[0];\n }\n\n /**\n * Checks if `func` has its source masked.\n *\n * @private\n * @param {Function} func The function to check.\n * @returns {boolean} Returns `true` if `func` is masked, else `false`.\n */\n function isMasked(func) {\n return !!maskSrcKey && (maskSrcKey in func);\n }\n\n /**\n * Checks if `func` is capable of being masked.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `func` is maskable, else `false`.\n */\n var isMaskable = coreJsData ? isFunction : stubFalse;\n\n /**\n * Checks if `value` is likely a prototype object.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a prototype, else `false`.\n */\n function isPrototype(value) {\n var Ctor = value && value.constructor,\n proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;\n\n return value === proto;\n }\n\n /**\n * Checks if `value` is suitable for strict equality comparisons, i.e. `===`.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` if suitable for strict\n * equality comparisons, else `false`.\n */\n function isStrictComparable(value) {\n return value === value && !isObject(value);\n }\n\n /**\n * A specialized version of `matchesProperty` for source values suitable\n * for strict equality comparisons, i.e. `===`.\n *\n * @private\n * @param {string} key The key of the property to get.\n * @param {*} srcValue The value to match.\n * @returns {Function} Returns the new spec function.\n */\n function matchesStrictComparable(key, srcValue) {\n return function(object) {\n if (object == null) {\n return false;\n }\n return object[key] === srcValue &&\n (srcValue !== undefined || (key in Object(object)));\n };\n }\n\n /**\n * A specialized version of `_.memoize` which clears the memoized function's\n * cache when it exceeds `MAX_MEMOIZE_SIZE`.\n *\n * @private\n * @param {Function} func The function to have its output memoized.\n * @returns {Function} Returns the new memoized function.\n */\n function memoizeCapped(func) {\n var result = memoize(func, function(key) {\n if (cache.size === MAX_MEMOIZE_SIZE) {\n cache.clear();\n }\n return key;\n });\n\n var cache = result.cache;\n return result;\n }\n\n /**\n * Merges the function metadata of `source` into `data`.\n *\n * Merging metadata reduces the number of wrappers used to invoke a function.\n * This is possible because methods like `_.bind`, `_.curry`, and `_.partial`\n * may be applied regardless of execution order. Methods like `_.ary` and\n * `_.rearg` modify function arguments, making the order in which they are\n * executed important, preventing the merging of metadata. However, we make\n * an exception for a safe combined case where curried functions have `_.ary`\n * and or `_.rearg` applied.\n *\n * @private\n * @param {Array} data The destination metadata.\n * @param {Array} source The source metadata.\n * @returns {Array} Returns `data`.\n */\n function mergeData(data, source) {\n var bitmask = data[1],\n srcBitmask = source[1],\n newBitmask = bitmask | srcBitmask,\n isCommon = newBitmask < (WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG | WRAP_ARY_FLAG);\n\n var isCombo =\n ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_CURRY_FLAG)) ||\n ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_REARG_FLAG) && (data[7].length <= source[8])) ||\n ((srcBitmask == (WRAP_ARY_FLAG | WRAP_REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == WRAP_CURRY_FLAG));\n\n // Exit early if metadata can't be merged.\n if (!(isCommon || isCombo)) {\n return data;\n }\n // Use source `thisArg` if available.\n if (srcBitmask & WRAP_BIND_FLAG) {\n data[2] = source[2];\n // Set when currying a bound function.\n newBitmask |= bitmask & WRAP_BIND_FLAG ? 0 : WRAP_CURRY_BOUND_FLAG;\n }\n // Compose partial arguments.\n var value = source[3];\n if (value) {\n var partials = data[3];\n data[3] = partials ? composeArgs(partials, value, source[4]) : value;\n data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : source[4];\n }\n // Compose partial right arguments.\n value = source[5];\n if (value) {\n partials = data[5];\n data[5] = partials ? composeArgsRight(partials, value, source[6]) : value;\n data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : source[6];\n }\n // Use source `argPos` if available.\n value = source[7];\n if (value) {\n data[7] = value;\n }\n // Use source `ary` if it's smaller.\n if (srcBitmask & WRAP_ARY_FLAG) {\n data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]);\n }\n // Use source `arity` if one is not provided.\n if (data[9] == null) {\n data[9] = source[9];\n }\n // Use source `func` and merge bitmasks.\n data[0] = source[0];\n data[1] = newBitmask;\n\n return data;\n }\n\n /**\n * This function is like\n * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)\n * except that it includes inherited enumerable properties.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property names.\n */\n function nativeKeysIn(object) {\n var result = [];\n if (object != null) {\n for (var key in Object(object)) {\n result.push(key);\n }\n }\n return result;\n }\n\n /**\n * Converts `value` to a string using `Object.prototype.toString`.\n *\n * @private\n * @param {*} value The value to convert.\n * @returns {string} Returns the converted string.\n */\n function objectToString(value) {\n return nativeObjectToString.call(value);\n }\n\n /**\n * A specialized version of `baseRest` which transforms the rest array.\n *\n * @private\n * @param {Function} func The function to apply a rest parameter to.\n * @param {number} [start=func.length-1] The start position of the rest parameter.\n * @param {Function} transform The rest array transform.\n * @returns {Function} Returns the new function.\n */\n function overRest(func, start, transform) {\n start = nativeMax(start === undefined ? (func.length - 1) : start, 0);\n return function() {\n var args = arguments,\n index = -1,\n length = nativeMax(args.length - start, 0),\n array = Array(length);\n\n while (++index < length) {\n array[index] = args[start + index];\n }\n index = -1;\n var otherArgs = Array(start + 1);\n while (++index < start) {\n otherArgs[index] = args[index];\n }\n otherArgs[start] = transform(array);\n return apply(func, this, otherArgs);\n };\n }\n\n /**\n * Gets the parent value at `path` of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Array} path The path to get the parent value of.\n * @returns {*} Returns the parent value.\n */\n function parent(object, path) {\n return path.length < 2 ? object : baseGet(object, baseSlice(path, 0, -1));\n }\n\n /**\n * Reorder `array` according to the specified indexes where the element at\n * the first index is assigned as the first element, the element at\n * the second index is assigned as the second element, and so on.\n *\n * @private\n * @param {Array} array The array to reorder.\n * @param {Array} indexes The arranged array indexes.\n * @returns {Array} Returns `array`.\n */\n function reorder(array, indexes) {\n var arrLength = array.length,\n length = nativeMin(indexes.length, arrLength),\n oldArray = copyArray(array);\n\n while (length--) {\n var index = indexes[length];\n array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined;\n }\n return array;\n }\n\n /**\n * Sets metadata for `func`.\n *\n * **Note:** If this function becomes hot, i.e. is invoked a lot in a short\n * period of time, it will trip its breaker and transition to an identity\n * function to avoid garbage collection pauses in V8. See\n * [V8 issue 2070](https://bugs.chromium.org/p/v8/issues/detail?id=2070)\n * for more details.\n *\n * @private\n * @param {Function} func The function to associate metadata with.\n * @param {*} data The metadata.\n * @returns {Function} Returns `func`.\n */\n var setData = shortOut(baseSetData);\n\n /**\n * A simple wrapper around the global [`setTimeout`](https://mdn.io/setTimeout).\n *\n * @private\n * @param {Function} func The function to delay.\n * @param {number} wait The number of milliseconds to delay invocation.\n * @returns {number|Object} Returns the timer id or timeout object.\n */\n var setTimeout = ctxSetTimeout || function(func, wait) {\n return root.setTimeout(func, wait);\n };\n\n /**\n * Sets the `toString` method of `func` to return `string`.\n *\n * @private\n * @param {Function} func The function to modify.\n * @param {Function} string The `toString` result.\n * @returns {Function} Returns `func`.\n */\n var setToString = shortOut(baseSetToString);\n\n /**\n * Sets the `toString` method of `wrapper` to mimic the source of `reference`\n * with wrapper details in a comment at the top of the source body.\n *\n * @private\n * @param {Function} wrapper The function to modify.\n * @param {Function} reference The reference function.\n * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n * @returns {Function} Returns `wrapper`.\n */\n function setWrapToString(wrapper, reference, bitmask) {\n var source = (reference + '');\n return setToString(wrapper, insertWrapDetails(source, updateWrapDetails(getWrapDetails(source), bitmask)));\n }\n\n /**\n * Creates a function that'll short out and invoke `identity` instead\n * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN`\n * milliseconds.\n *\n * @private\n * @param {Function} func The function to restrict.\n * @returns {Function} Returns the new shortable function.\n */\n function shortOut(func) {\n var count = 0,\n lastCalled = 0;\n\n return function() {\n var stamp = nativeNow(),\n remaining = HOT_SPAN - (stamp - lastCalled);\n\n lastCalled = stamp;\n if (remaining > 0) {\n if (++count >= HOT_COUNT) {\n return arguments[0];\n }\n } else {\n count = 0;\n }\n return func.apply(undefined, arguments);\n };\n }\n\n /**\n * A specialized version of `_.shuffle` which mutates and sets the size of `array`.\n *\n * @private\n * @param {Array} array The array to shuffle.\n * @param {number} [size=array.length] The size of `array`.\n * @returns {Array} Returns `array`.\n */\n function shuffleSelf(array, size) {\n var index = -1,\n length = array.length,\n lastIndex = length - 1;\n\n size = size === undefined ? length : size;\n while (++index < size) {\n var rand = baseRandom(index, lastIndex),\n value = array[rand];\n\n array[rand] = array[index];\n array[index] = value;\n }\n array.length = size;\n return array;\n }\n\n /**\n * Converts `string` to a property path array.\n *\n * @private\n * @param {string} string The string to convert.\n * @returns {Array} Returns the property path array.\n */\n var stringToPath = memoizeCapped(function(string) {\n var result = [];\n if (string.charCodeAt(0) === 46 /* . */) {\n result.push('');\n }\n string.replace(rePropName, function(match, number, quote, subString) {\n result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match));\n });\n return result;\n });\n\n /**\n * Converts `value` to a string key if it's not a string or symbol.\n *\n * @private\n * @param {*} value The value to inspect.\n * @returns {string|symbol} Returns the key.\n */\n function toKey(value) {\n if (typeof value == 'string' || isSymbol(value)) {\n return value;\n }\n var result = (value + '');\n return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;\n }\n\n /**\n * Converts `func` to its source code.\n *\n * @private\n * @param {Function} func The function to convert.\n * @returns {string} Returns the source code.\n */\n function toSource(func) {\n if (func != null) {\n try {\n return funcToString.call(func);\n } catch (e) {}\n try {\n return (func + '');\n } catch (e) {}\n }\n return '';\n }\n\n /**\n * Updates wrapper `details` based on `bitmask` flags.\n *\n * @private\n * @returns {Array} details The details to modify.\n * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n * @returns {Array} Returns `details`.\n */\n function updateWrapDetails(details, bitmask) {\n arrayEach(wrapFlags, function(pair) {\n var value = '_.' + pair[0];\n if ((bitmask & pair[1]) && !arrayIncludes(details, value)) {\n details.push(value);\n }\n });\n return details.sort();\n }\n\n /**\n * Creates a clone of `wrapper`.\n *\n * @private\n * @param {Object} wrapper The wrapper to clone.\n * @returns {Object} Returns the cloned wrapper.\n */\n function wrapperClone(wrapper) {\n if (wrapper instanceof LazyWrapper) {\n return wrapper.clone();\n }\n var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__);\n result.__actions__ = copyArray(wrapper.__actions__);\n result.__index__ = wrapper.__index__;\n result.__values__ = wrapper.__values__;\n return result;\n }\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates an array of elements split into groups the length of `size`.\n * If `array` can't be split evenly, the final chunk will be the remaining\n * elements.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to process.\n * @param {number} [size=1] The length of each chunk\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Array} Returns the new array of chunks.\n * @example\n *\n * _.chunk(['a', 'b', 'c', 'd'], 2);\n * // => [['a', 'b'], ['c', 'd']]\n *\n * _.chunk(['a', 'b', 'c', 'd'], 3);\n * // => [['a', 'b', 'c'], ['d']]\n */\n function chunk(array, size, guard) {\n if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) {\n size = 1;\n } else {\n size = nativeMax(toInteger(size), 0);\n }\n var length = array == null ? 0 : array.length;\n if (!length || size < 1) {\n return [];\n }\n var index = 0,\n resIndex = 0,\n result = Array(nativeCeil(length / size));\n\n while (index < length) {\n result[resIndex++] = baseSlice(array, index, (index += size));\n }\n return result;\n }\n\n /**\n * Creates an array with all falsey values removed. The values `false`, `null`,\n * `0`, `\"\"`, `undefined`, and `NaN` are falsey.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to compact.\n * @returns {Array} Returns the new array of filtered values.\n * @example\n *\n * _.compact([0, 1, false, 2, '', 3]);\n * // => [1, 2, 3]\n */\n function compact(array) {\n var index = -1,\n length = array == null ? 0 : array.length,\n resIndex = 0,\n result = [];\n\n while (++index < length) {\n var value = array[index];\n if (value) {\n result[resIndex++] = value;\n }\n }\n return result;\n }\n\n /**\n * Creates a new array concatenating `array` with any additional arrays\n * and/or values.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to concatenate.\n * @param {...*} [values] The values to concatenate.\n * @returns {Array} Returns the new concatenated array.\n * @example\n *\n * var array = [1];\n * var other = _.concat(array, 2, [3], [[4]]);\n *\n * console.log(other);\n * // => [1, 2, 3, [4]]\n *\n * console.log(array);\n * // => [1]\n */\n function concat() {\n var length = arguments.length;\n if (!length) {\n return [];\n }\n var args = Array(length - 1),\n array = arguments[0],\n index = length;\n\n while (index--) {\n args[index - 1] = arguments[index];\n }\n return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));\n }\n\n /**\n * Creates an array of `array` values not included in the other given arrays\n * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons. The order and references of result values are\n * determined by the first array.\n *\n * **Note:** Unlike `_.pullAll`, this method returns a new array.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {...Array} [values] The values to exclude.\n * @returns {Array} Returns the new array of filtered values.\n * @see _.without, _.xor\n * @example\n *\n * _.difference([2, 1], [2, 3]);\n * // => [1]\n */\n var difference = baseRest(function(array, values) {\n return isArrayLikeObject(array)\n ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true))\n : [];\n });\n\n /**\n * This method is like `_.difference` except that it accepts `iteratee` which\n * is invoked for each element of `array` and `values` to generate the criterion\n * by which they're compared. The order and references of result values are\n * determined by the first array. The iteratee is invoked with one argument:\n * (value).\n *\n * **Note:** Unlike `_.pullAllBy`, this method returns a new array.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {...Array} [values] The values to exclude.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new array of filtered values.\n * @example\n *\n * _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);\n * // => [1.2]\n *\n * // The `_.property` iteratee shorthand.\n * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');\n * // => [{ 'x': 2 }]\n */\n var differenceBy = baseRest(function(array, values) {\n var iteratee = last(values);\n if (isArrayLikeObject(iteratee)) {\n iteratee = undefined;\n }\n return isArrayLikeObject(array)\n ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2))\n : [];\n });\n\n /**\n * This method is like `_.difference` except that it accepts `comparator`\n * which is invoked to compare elements of `array` to `values`. The order and\n * references of result values are determined by the first array. The comparator\n * is invoked with two arguments: (arrVal, othVal).\n *\n * **Note:** Unlike `_.pullAllWith`, this method returns a new array.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {...Array} [values] The values to exclude.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of filtered values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n *\n * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual);\n * // => [{ 'x': 2, 'y': 1 }]\n */\n var differenceWith = baseRest(function(array, values) {\n var comparator = last(values);\n if (isArrayLikeObject(comparator)) {\n comparator = undefined;\n }\n return isArrayLikeObject(array)\n ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator)\n : [];\n });\n\n /**\n * Creates a slice of `array` with `n` elements dropped from the beginning.\n *\n * @static\n * @memberOf _\n * @since 0.5.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {number} [n=1] The number of elements to drop.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * _.drop([1, 2, 3]);\n * // => [2, 3]\n *\n * _.drop([1, 2, 3], 2);\n * // => [3]\n *\n * _.drop([1, 2, 3], 5);\n * // => []\n *\n * _.drop([1, 2, 3], 0);\n * // => [1, 2, 3]\n */\n function drop(array, n, guard) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return [];\n }\n n = (guard || n === undefined) ? 1 : toInteger(n);\n return baseSlice(array, n < 0 ? 0 : n, length);\n }\n\n /**\n * Creates a slice of `array` with `n` elements dropped from the end.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {number} [n=1] The number of elements to drop.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * _.dropRight([1, 2, 3]);\n * // => [1, 2]\n *\n * _.dropRight([1, 2, 3], 2);\n * // => [1]\n *\n * _.dropRight([1, 2, 3], 5);\n * // => []\n *\n * _.dropRight([1, 2, 3], 0);\n * // => [1, 2, 3]\n */\n function dropRight(array, n, guard) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return [];\n }\n n = (guard || n === undefined) ? 1 : toInteger(n);\n n = length - n;\n return baseSlice(array, 0, n < 0 ? 0 : n);\n }\n\n /**\n * Creates a slice of `array` excluding elements dropped from the end.\n * Elements are dropped until `predicate` returns falsey. The predicate is\n * invoked with three arguments: (value, index, array).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'active': true },\n * { 'user': 'fred', 'active': false },\n * { 'user': 'pebbles', 'active': false }\n * ];\n *\n * _.dropRightWhile(users, function(o) { return !o.active; });\n * // => objects for ['barney']\n *\n * // The `_.matches` iteratee shorthand.\n * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false });\n * // => objects for ['barney', 'fred']\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.dropRightWhile(users, ['active', false]);\n * // => objects for ['barney']\n *\n * // The `_.property` iteratee shorthand.\n * _.dropRightWhile(users, 'active');\n * // => objects for ['barney', 'fred', 'pebbles']\n */\n function dropRightWhile(array, predicate) {\n return (array && array.length)\n ? baseWhile(array, getIteratee(predicate, 3), true, true)\n : [];\n }\n\n /**\n * Creates a slice of `array` excluding elements dropped from the beginning.\n * Elements are dropped until `predicate` returns falsey. The predicate is\n * invoked with three arguments: (value, index, array).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'active': false },\n * { 'user': 'fred', 'active': false },\n * { 'user': 'pebbles', 'active': true }\n * ];\n *\n * _.dropWhile(users, function(o) { return !o.active; });\n * // => objects for ['pebbles']\n *\n * // The `_.matches` iteratee shorthand.\n * _.dropWhile(users, { 'user': 'barney', 'active': false });\n * // => objects for ['fred', 'pebbles']\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.dropWhile(users, ['active', false]);\n * // => objects for ['pebbles']\n *\n * // The `_.property` iteratee shorthand.\n * _.dropWhile(users, 'active');\n * // => objects for ['barney', 'fred', 'pebbles']\n */\n function dropWhile(array, predicate) {\n return (array && array.length)\n ? baseWhile(array, getIteratee(predicate, 3), true)\n : [];\n }\n\n /**\n * Fills elements of `array` with `value` from `start` up to, but not\n * including, `end`.\n *\n * **Note:** This method mutates `array`.\n *\n * @static\n * @memberOf _\n * @since 3.2.0\n * @category Array\n * @param {Array} array The array to fill.\n * @param {*} value The value to fill `array` with.\n * @param {number} [start=0] The start position.\n * @param {number} [end=array.length] The end position.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [1, 2, 3];\n *\n * _.fill(array, 'a');\n * console.log(array);\n * // => ['a', 'a', 'a']\n *\n * _.fill(Array(3), 2);\n * // => [2, 2, 2]\n *\n * _.fill([4, 6, 8, 10], '*', 1, 3);\n * // => [4, '*', '*', 10]\n */\n function fill(array, value, start, end) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return [];\n }\n if (start && typeof start != 'number' && isIterateeCall(array, value, start)) {\n start = 0;\n end = length;\n }\n return baseFill(array, value, start, end);\n }\n\n /**\n * This method is like `_.find` except that it returns the index of the first\n * element `predicate` returns truthy for instead of the element itself.\n *\n * @static\n * @memberOf _\n * @since 1.1.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @param {number} [fromIndex=0] The index to search from.\n * @returns {number} Returns the index of the found element, else `-1`.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'active': false },\n * { 'user': 'fred', 'active': false },\n * { 'user': 'pebbles', 'active': true }\n * ];\n *\n * _.findIndex(users, function(o) { return o.user == 'barney'; });\n * // => 0\n *\n * // The `_.matches` iteratee shorthand.\n * _.findIndex(users, { 'user': 'fred', 'active': false });\n * // => 1\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.findIndex(users, ['active', false]);\n * // => 0\n *\n * // The `_.property` iteratee shorthand.\n * _.findIndex(users, 'active');\n * // => 2\n */\n function findIndex(array, predicate, fromIndex) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return -1;\n }\n var index = fromIndex == null ? 0 : toInteger(fromIndex);\n if (index < 0) {\n index = nativeMax(length + index, 0);\n }\n return baseFindIndex(array, getIteratee(predicate, 3), index);\n }\n\n /**\n * This method is like `_.findIndex` except that it iterates over elements\n * of `collection` from right to left.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @param {number} [fromIndex=array.length-1] The index to search from.\n * @returns {number} Returns the index of the found element, else `-1`.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'active': true },\n * { 'user': 'fred', 'active': false },\n * { 'user': 'pebbles', 'active': false }\n * ];\n *\n * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; });\n * // => 2\n *\n * // The `_.matches` iteratee shorthand.\n * _.findLastIndex(users, { 'user': 'barney', 'active': true });\n * // => 0\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.findLastIndex(users, ['active', false]);\n * // => 2\n *\n * // The `_.property` iteratee shorthand.\n * _.findLastIndex(users, 'active');\n * // => 0\n */\n function findLastIndex(array, predicate, fromIndex) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return -1;\n }\n var index = length - 1;\n if (fromIndex !== undefined) {\n index = toInteger(fromIndex);\n index = fromIndex < 0\n ? nativeMax(length + index, 0)\n : nativeMin(index, length - 1);\n }\n return baseFindIndex(array, getIteratee(predicate, 3), index, true);\n }\n\n /**\n * Flattens `array` a single level deep.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to flatten.\n * @returns {Array} Returns the new flattened array.\n * @example\n *\n * _.flatten([1, [2, [3, [4]], 5]]);\n * // => [1, 2, [3, [4]], 5]\n */\n function flatten(array) {\n var length = array == null ? 0 : array.length;\n return length ? baseFlatten(array, 1) : [];\n }\n\n /**\n * Recursively flattens `array`.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to flatten.\n * @returns {Array} Returns the new flattened array.\n * @example\n *\n * _.flattenDeep([1, [2, [3, [4]], 5]]);\n * // => [1, 2, 3, 4, 5]\n */\n function flattenDeep(array) {\n var length = array == null ? 0 : array.length;\n return length ? baseFlatten(array, INFINITY) : [];\n }\n\n /**\n * Recursively flatten `array` up to `depth` times.\n *\n * @static\n * @memberOf _\n * @since 4.4.0\n * @category Array\n * @param {Array} array The array to flatten.\n * @param {number} [depth=1] The maximum recursion depth.\n * @returns {Array} Returns the new flattened array.\n * @example\n *\n * var array = [1, [2, [3, [4]], 5]];\n *\n * _.flattenDepth(array, 1);\n * // => [1, 2, [3, [4]], 5]\n *\n * _.flattenDepth(array, 2);\n * // => [1, 2, 3, [4], 5]\n */\n function flattenDepth(array, depth) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return [];\n }\n depth = depth === undefined ? 1 : toInteger(depth);\n return baseFlatten(array, depth);\n }\n\n /**\n * The inverse of `_.toPairs`; this method returns an object composed\n * from key-value `pairs`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} pairs The key-value pairs.\n * @returns {Object} Returns the new object.\n * @example\n *\n * _.fromPairs([['a', 1], ['b', 2]]);\n * // => { 'a': 1, 'b': 2 }\n */\n function fromPairs(pairs) {\n var index = -1,\n length = pairs == null ? 0 : pairs.length,\n result = {};\n\n while (++index < length) {\n var pair = pairs[index];\n result[pair[0]] = pair[1];\n }\n return result;\n }\n\n /**\n * Gets the first element of `array`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @alias first\n * @category Array\n * @param {Array} array The array to query.\n * @returns {*} Returns the first element of `array`.\n * @example\n *\n * _.head([1, 2, 3]);\n * // => 1\n *\n * _.head([]);\n * // => undefined\n */\n function head(array) {\n return (array && array.length) ? array[0] : undefined;\n }\n\n /**\n * Gets the index at which the first occurrence of `value` is found in `array`\n * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons. If `fromIndex` is negative, it's used as the\n * offset from the end of `array`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {*} value The value to search for.\n * @param {number} [fromIndex=0] The index to search from.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.indexOf([1, 2, 1, 2], 2);\n * // => 1\n *\n * // Search from the `fromIndex`.\n * _.indexOf([1, 2, 1, 2], 2, 2);\n * // => 3\n */\n function indexOf(array, value, fromIndex) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return -1;\n }\n var index = fromIndex == null ? 0 : toInteger(fromIndex);\n if (index < 0) {\n index = nativeMax(length + index, 0);\n }\n return baseIndexOf(array, value, index);\n }\n\n /**\n * Gets all but the last element of `array`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to query.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * _.initial([1, 2, 3]);\n * // => [1, 2]\n */\n function initial(array) {\n var length = array == null ? 0 : array.length;\n return length ? baseSlice(array, 0, -1) : [];\n }\n\n /**\n * Creates an array of unique values that are included in all given arrays\n * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons. The order and references of result values are\n * determined by the first array.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @returns {Array} Returns the new array of intersecting values.\n * @example\n *\n * _.intersection([2, 1], [2, 3]);\n * // => [2]\n */\n var intersection = baseRest(function(arrays) {\n var mapped = arrayMap(arrays, castArrayLikeObject);\n return (mapped.length && mapped[0] === arrays[0])\n ? baseIntersection(mapped)\n : [];\n });\n\n /**\n * This method is like `_.intersection` except that it accepts `iteratee`\n * which is invoked for each element of each `arrays` to generate the criterion\n * by which they're compared. The order and references of result values are\n * determined by the first array. The iteratee is invoked with one argument:\n * (value).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new array of intersecting values.\n * @example\n *\n * _.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor);\n * // => [2.1]\n *\n * // The `_.property` iteratee shorthand.\n * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 1 }]\n */\n var intersectionBy = baseRest(function(arrays) {\n var iteratee = last(arrays),\n mapped = arrayMap(arrays, castArrayLikeObject);\n\n if (iteratee === last(mapped)) {\n iteratee = undefined;\n } else {\n mapped.pop();\n }\n return (mapped.length && mapped[0] === arrays[0])\n ? baseIntersection(mapped, getIteratee(iteratee, 2))\n : [];\n });\n\n /**\n * This method is like `_.intersection` except that it accepts `comparator`\n * which is invoked to compare elements of `arrays`. The order and references\n * of result values are determined by the first array. The comparator is\n * invoked with two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of intersecting values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.intersectionWith(objects, others, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }]\n */\n var intersectionWith = baseRest(function(arrays) {\n var comparator = last(arrays),\n mapped = arrayMap(arrays, castArrayLikeObject);\n\n comparator = typeof comparator == 'function' ? comparator : undefined;\n if (comparator) {\n mapped.pop();\n }\n return (mapped.length && mapped[0] === arrays[0])\n ? baseIntersection(mapped, undefined, comparator)\n : [];\n });\n\n /**\n * Converts all elements in `array` into a string separated by `separator`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to convert.\n * @param {string} [separator=','] The element separator.\n * @returns {string} Returns the joined string.\n * @example\n *\n * _.join(['a', 'b', 'c'], '~');\n * // => 'a~b~c'\n */\n function join(array, separator) {\n return array == null ? '' : nativeJoin.call(array, separator);\n }\n\n /**\n * Gets the last element of `array`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to query.\n * @returns {*} Returns the last element of `array`.\n * @example\n *\n * _.last([1, 2, 3]);\n * // => 3\n */\n function last(array) {\n var length = array == null ? 0 : array.length;\n return length ? array[length - 1] : undefined;\n }\n\n /**\n * This method is like `_.indexOf` except that it iterates over elements of\n * `array` from right to left.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {*} value The value to search for.\n * @param {number} [fromIndex=array.length-1] The index to search from.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.lastIndexOf([1, 2, 1, 2], 2);\n * // => 3\n *\n * // Search from the `fromIndex`.\n * _.lastIndexOf([1, 2, 1, 2], 2, 2);\n * // => 1\n */\n function lastIndexOf(array, value, fromIndex) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return -1;\n }\n var index = length;\n if (fromIndex !== undefined) {\n index = toInteger(fromIndex);\n index = index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1);\n }\n return value === value\n ? strictLastIndexOf(array, value, index)\n : baseFindIndex(array, baseIsNaN, index, true);\n }\n\n /**\n * Gets the element at index `n` of `array`. If `n` is negative, the nth\n * element from the end is returned.\n *\n * @static\n * @memberOf _\n * @since 4.11.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {number} [n=0] The index of the element to return.\n * @returns {*} Returns the nth element of `array`.\n * @example\n *\n * var array = ['a', 'b', 'c', 'd'];\n *\n * _.nth(array, 1);\n * // => 'b'\n *\n * _.nth(array, -2);\n * // => 'c';\n */\n function nth(array, n) {\n return (array && array.length) ? baseNth(array, toInteger(n)) : undefined;\n }\n\n /**\n * Removes all given values from `array` using\n * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons.\n *\n * **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove`\n * to remove elements from an array by predicate.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Array\n * @param {Array} array The array to modify.\n * @param {...*} [values] The values to remove.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = ['a', 'b', 'c', 'a', 'b', 'c'];\n *\n * _.pull(array, 'a', 'c');\n * console.log(array);\n * // => ['b', 'b']\n */\n var pull = baseRest(pullAll);\n\n /**\n * This method is like `_.pull` except that it accepts an array of values to remove.\n *\n * **Note:** Unlike `_.difference`, this method mutates `array`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to modify.\n * @param {Array} values The values to remove.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = ['a', 'b', 'c', 'a', 'b', 'c'];\n *\n * _.pullAll(array, ['a', 'c']);\n * console.log(array);\n * // => ['b', 'b']\n */\n function pullAll(array, values) {\n return (array && array.length && values && values.length)\n ? basePullAll(array, values)\n : array;\n }\n\n /**\n * This method is like `_.pullAll` except that it accepts `iteratee` which is\n * invoked for each element of `array` and `values` to generate the criterion\n * by which they're compared. The iteratee is invoked with one argument: (value).\n *\n * **Note:** Unlike `_.differenceBy`, this method mutates `array`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to modify.\n * @param {Array} values The values to remove.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }];\n *\n * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x');\n * console.log(array);\n * // => [{ 'x': 2 }]\n */\n function pullAllBy(array, values, iteratee) {\n return (array && array.length && values && values.length)\n ? basePullAll(array, values, getIteratee(iteratee, 2))\n : array;\n }\n\n /**\n * This method is like `_.pullAll` except that it accepts `comparator` which\n * is invoked to compare elements of `array` to `values`. The comparator is\n * invoked with two arguments: (arrVal, othVal).\n *\n * **Note:** Unlike `_.differenceWith`, this method mutates `array`.\n *\n * @static\n * @memberOf _\n * @since 4.6.0\n * @category Array\n * @param {Array} array The array to modify.\n * @param {Array} values The values to remove.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }];\n *\n * _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual);\n * console.log(array);\n * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }]\n */\n function pullAllWith(array, values, comparator) {\n return (array && array.length && values && values.length)\n ? basePullAll(array, values, undefined, comparator)\n : array;\n }\n\n /**\n * Removes elements from `array` corresponding to `indexes` and returns an\n * array of removed elements.\n *\n * **Note:** Unlike `_.at`, this method mutates `array`.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to modify.\n * @param {...(number|number[])} [indexes] The indexes of elements to remove.\n * @returns {Array} Returns the new array of removed elements.\n * @example\n *\n * var array = ['a', 'b', 'c', 'd'];\n * var pulled = _.pullAt(array, [1, 3]);\n *\n * console.log(array);\n * // => ['a', 'c']\n *\n * console.log(pulled);\n * // => ['b', 'd']\n */\n var pullAt = flatRest(function(array, indexes) {\n var length = array == null ? 0 : array.length,\n result = baseAt(array, indexes);\n\n basePullAt(array, arrayMap(indexes, function(index) {\n return isIndex(index, length) ? +index : index;\n }).sort(compareAscending));\n\n return result;\n });\n\n /**\n * Removes all elements from `array` that `predicate` returns truthy for\n * and returns an array of the removed elements. The predicate is invoked\n * with three arguments: (value, index, array).\n *\n * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull`\n * to pull elements from an array by value.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Array\n * @param {Array} array The array to modify.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the new array of removed elements.\n * @example\n *\n * var array = [1, 2, 3, 4];\n * var evens = _.remove(array, function(n) {\n * return n % 2 == 0;\n * });\n *\n * console.log(array);\n * // => [1, 3]\n *\n * console.log(evens);\n * // => [2, 4]\n */\n function remove(array, predicate) {\n var result = [];\n if (!(array && array.length)) {\n return result;\n }\n var index = -1,\n indexes = [],\n length = array.length;\n\n predicate = getIteratee(predicate, 3);\n while (++index < length) {\n var value = array[index];\n if (predicate(value, index, array)) {\n result.push(value);\n indexes.push(index);\n }\n }\n basePullAt(array, indexes);\n return result;\n }\n\n /**\n * Reverses `array` so that the first element becomes the last, the second\n * element becomes the second to last, and so on.\n *\n * **Note:** This method mutates `array` and is based on\n * [`Array#reverse`](https://mdn.io/Array/reverse).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to modify.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [1, 2, 3];\n *\n * _.reverse(array);\n * // => [3, 2, 1]\n *\n * console.log(array);\n * // => [3, 2, 1]\n */\n function reverse(array) {\n return array == null ? array : nativeReverse.call(array);\n }\n\n /**\n * Creates a slice of `array` from `start` up to, but not including, `end`.\n *\n * **Note:** This method is used instead of\n * [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are\n * returned.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to slice.\n * @param {number} [start=0] The start position.\n * @param {number} [end=array.length] The end position.\n * @returns {Array} Returns the slice of `array`.\n */\n function slice(array, start, end) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return [];\n }\n if (end && typeof end != 'number' && isIterateeCall(array, start, end)) {\n start = 0;\n end = length;\n }\n else {\n start = start == null ? 0 : toInteger(start);\n end = end === undefined ? length : toInteger(end);\n }\n return baseSlice(array, start, end);\n }\n\n /**\n * Uses a binary search to determine the lowest index at which `value`\n * should be inserted into `array` in order to maintain its sort order.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @returns {number} Returns the index at which `value` should be inserted\n * into `array`.\n * @example\n *\n * _.sortedIndex([30, 50], 40);\n * // => 1\n */\n function sortedIndex(array, value) {\n return baseSortedIndex(array, value);\n }\n\n /**\n * This method is like `_.sortedIndex` except that it accepts `iteratee`\n * which is invoked for `value` and each element of `array` to compute their\n * sort ranking. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the index at which `value` should be inserted\n * into `array`.\n * @example\n *\n * var objects = [{ 'x': 4 }, { 'x': 5 }];\n *\n * _.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });\n * // => 0\n *\n * // The `_.property` iteratee shorthand.\n * _.sortedIndexBy(objects, { 'x': 4 }, 'x');\n * // => 0\n */\n function sortedIndexBy(array, value, iteratee) {\n return baseSortedIndexBy(array, value, getIteratee(iteratee, 2));\n }\n\n /**\n * This method is like `_.indexOf` except that it performs a binary\n * search on a sorted `array`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {*} value The value to search for.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.sortedIndexOf([4, 5, 5, 5, 6], 5);\n * // => 1\n */\n function sortedIndexOf(array, value) {\n var length = array == null ? 0 : array.length;\n if (length) {\n var index = baseSortedIndex(array, value);\n if (index < length && eq(array[index], value)) {\n return index;\n }\n }\n return -1;\n }\n\n /**\n * This method is like `_.sortedIndex` except that it returns the highest\n * index at which `value` should be inserted into `array` in order to\n * maintain its sort order.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @returns {number} Returns the index at which `value` should be inserted\n * into `array`.\n * @example\n *\n * _.sortedLastIndex([4, 5, 5, 5, 6], 5);\n * // => 4\n */\n function sortedLastIndex(array, value) {\n return baseSortedIndex(array, value, true);\n }\n\n /**\n * This method is like `_.sortedLastIndex` except that it accepts `iteratee`\n * which is invoked for `value` and each element of `array` to compute their\n * sort ranking. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the index at which `value` should be inserted\n * into `array`.\n * @example\n *\n * var objects = [{ 'x': 4 }, { 'x': 5 }];\n *\n * _.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });\n * // => 1\n *\n * // The `_.property` iteratee shorthand.\n * _.sortedLastIndexBy(objects, { 'x': 4 }, 'x');\n * // => 1\n */\n function sortedLastIndexBy(array, value, iteratee) {\n return baseSortedIndexBy(array, value, getIteratee(iteratee, 2), true);\n }\n\n /**\n * This method is like `_.lastIndexOf` except that it performs a binary\n * search on a sorted `array`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {*} value The value to search for.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.sortedLastIndexOf([4, 5, 5, 5, 6], 5);\n * // => 3\n */\n function sortedLastIndexOf(array, value) {\n var length = array == null ? 0 : array.length;\n if (length) {\n var index = baseSortedIndex(array, value, true) - 1;\n if (eq(array[index], value)) {\n return index;\n }\n }\n return -1;\n }\n\n /**\n * This method is like `_.uniq` except that it's designed and optimized\n * for sorted arrays.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.sortedUniq([1, 1, 2]);\n * // => [1, 2]\n */\n function sortedUniq(array) {\n return (array && array.length)\n ? baseSortedUniq(array)\n : [];\n }\n\n /**\n * This method is like `_.uniqBy` except that it's designed and optimized\n * for sorted arrays.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor);\n * // => [1.1, 2.3]\n */\n function sortedUniqBy(array, iteratee) {\n return (array && array.length)\n ? baseSortedUniq(array, getIteratee(iteratee, 2))\n : [];\n }\n\n /**\n * Gets all but the first element of `array`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to query.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * _.tail([1, 2, 3]);\n * // => [2, 3]\n */\n function tail(array) {\n var length = array == null ? 0 : array.length;\n return length ? baseSlice(array, 1, length) : [];\n }\n\n /**\n * Creates a slice of `array` with `n` elements taken from the beginning.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {number} [n=1] The number of elements to take.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * _.take([1, 2, 3]);\n * // => [1]\n *\n * _.take([1, 2, 3], 2);\n * // => [1, 2]\n *\n * _.take([1, 2, 3], 5);\n * // => [1, 2, 3]\n *\n * _.take([1, 2, 3], 0);\n * // => []\n */\n function take(array, n, guard) {\n if (!(array && array.length)) {\n return [];\n }\n n = (guard || n === undefined) ? 1 : toInteger(n);\n return baseSlice(array, 0, n < 0 ? 0 : n);\n }\n\n /**\n * Creates a slice of `array` with `n` elements taken from the end.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {number} [n=1] The number of elements to take.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * _.takeRight([1, 2, 3]);\n * // => [3]\n *\n * _.takeRight([1, 2, 3], 2);\n * // => [2, 3]\n *\n * _.takeRight([1, 2, 3], 5);\n * // => [1, 2, 3]\n *\n * _.takeRight([1, 2, 3], 0);\n * // => []\n */\n function takeRight(array, n, guard) {\n var length = array == null ? 0 : array.length;\n if (!length) {\n return [];\n }\n n = (guard || n === undefined) ? 1 : toInteger(n);\n n = length - n;\n return baseSlice(array, n < 0 ? 0 : n, length);\n }\n\n /**\n * Creates a slice of `array` with elements taken from the end. Elements are\n * taken until `predicate` returns falsey. The predicate is invoked with\n * three arguments: (value, index, array).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'active': true },\n * { 'user': 'fred', 'active': false },\n * { 'user': 'pebbles', 'active': false }\n * ];\n *\n * _.takeRightWhile(users, function(o) { return !o.active; });\n * // => objects for ['fred', 'pebbles']\n *\n * // The `_.matches` iteratee shorthand.\n * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false });\n * // => objects for ['pebbles']\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.takeRightWhile(users, ['active', false]);\n * // => objects for ['fred', 'pebbles']\n *\n * // The `_.property` iteratee shorthand.\n * _.takeRightWhile(users, 'active');\n * // => []\n */\n function takeRightWhile(array, predicate) {\n return (array && array.length)\n ? baseWhile(array, getIteratee(predicate, 3), false, true)\n : [];\n }\n\n /**\n * Creates a slice of `array` with elements taken from the beginning. Elements\n * are taken until `predicate` returns falsey. The predicate is invoked with\n * three arguments: (value, index, array).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Array\n * @param {Array} array The array to query.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the slice of `array`.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'active': false },\n * { 'user': 'fred', 'active': false },\n * { 'user': 'pebbles', 'active': true }\n * ];\n *\n * _.takeWhile(users, function(o) { return !o.active; });\n * // => objects for ['barney', 'fred']\n *\n * // The `_.matches` iteratee shorthand.\n * _.takeWhile(users, { 'user': 'barney', 'active': false });\n * // => objects for ['barney']\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.takeWhile(users, ['active', false]);\n * // => objects for ['barney', 'fred']\n *\n * // The `_.property` iteratee shorthand.\n * _.takeWhile(users, 'active');\n * // => []\n */\n function takeWhile(array, predicate) {\n return (array && array.length)\n ? baseWhile(array, getIteratee(predicate, 3))\n : [];\n }\n\n /**\n * Creates an array of unique values, in order, from all given arrays using\n * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @returns {Array} Returns the new array of combined values.\n * @example\n *\n * _.union([2], [1, 2]);\n * // => [2, 1]\n */\n var union = baseRest(function(arrays) {\n return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true));\n });\n\n /**\n * This method is like `_.union` except that it accepts `iteratee` which is\n * invoked for each element of each `arrays` to generate the criterion by\n * which uniqueness is computed. Result values are chosen from the first\n * array in which the value occurs. The iteratee is invoked with one argument:\n * (value).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new array of combined values.\n * @example\n *\n * _.unionBy([2.1], [1.2, 2.3], Math.floor);\n * // => [2.1, 1.2]\n *\n * // The `_.property` iteratee shorthand.\n * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 1 }, { 'x': 2 }]\n */\n var unionBy = baseRest(function(arrays) {\n var iteratee = last(arrays);\n if (isArrayLikeObject(iteratee)) {\n iteratee = undefined;\n }\n return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), getIteratee(iteratee, 2));\n });\n\n /**\n * This method is like `_.union` except that it accepts `comparator` which\n * is invoked to compare elements of `arrays`. Result values are chosen from\n * the first array in which the value occurs. The comparator is invoked\n * with two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of combined values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.unionWith(objects, others, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]\n */\n var unionWith = baseRest(function(arrays) {\n var comparator = last(arrays);\n comparator = typeof comparator == 'function' ? comparator : undefined;\n return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator);\n });\n\n /**\n * Creates a duplicate-free version of an array, using\n * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons, in which only the first occurrence of each element\n * is kept. The order of result values is determined by the order they occur\n * in the array.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.uniq([2, 1, 2]);\n * // => [2, 1]\n */\n function uniq(array) {\n return (array && array.length) ? baseUniq(array) : [];\n }\n\n /**\n * This method is like `_.uniq` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the criterion by which\n * uniqueness is computed. The order of result values is determined by the\n * order they occur in the array. The iteratee is invoked with one argument:\n * (value).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.uniqBy([2.1, 1.2, 2.3], Math.floor);\n * // => [2.1, 1.2]\n *\n * // The `_.property` iteratee shorthand.\n * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 1 }, { 'x': 2 }]\n */\n function uniqBy(array, iteratee) {\n return (array && array.length) ? baseUniq(array, getIteratee(iteratee, 2)) : [];\n }\n\n /**\n * This method is like `_.uniq` except that it accepts `comparator` which\n * is invoked to compare elements of `array`. The order of result values is\n * determined by the order they occur in the array.The comparator is invoked\n * with two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.uniqWith(objects, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]\n */\n function uniqWith(array, comparator) {\n comparator = typeof comparator == 'function' ? comparator : undefined;\n return (array && array.length) ? baseUniq(array, undefined, comparator) : [];\n }\n\n /**\n * This method is like `_.zip` except that it accepts an array of grouped\n * elements and creates an array regrouping the elements to their pre-zip\n * configuration.\n *\n * @static\n * @memberOf _\n * @since 1.2.0\n * @category Array\n * @param {Array} array The array of grouped elements to process.\n * @returns {Array} Returns the new array of regrouped elements.\n * @example\n *\n * var zipped = _.zip(['a', 'b'], [1, 2], [true, false]);\n * // => [['a', 1, true], ['b', 2, false]]\n *\n * _.unzip(zipped);\n * // => [['a', 'b'], [1, 2], [true, false]]\n */\n function unzip(array) {\n if (!(array && array.length)) {\n return [];\n }\n var length = 0;\n array = arrayFilter(array, function(group) {\n if (isArrayLikeObject(group)) {\n length = nativeMax(group.length, length);\n return true;\n }\n });\n return baseTimes(length, function(index) {\n return arrayMap(array, baseProperty(index));\n });\n }\n\n /**\n * This method is like `_.unzip` except that it accepts `iteratee` to specify\n * how regrouped values should be combined. The iteratee is invoked with the\n * elements of each group: (...group).\n *\n * @static\n * @memberOf _\n * @since 3.8.0\n * @category Array\n * @param {Array} array The array of grouped elements to process.\n * @param {Function} [iteratee=_.identity] The function to combine\n * regrouped values.\n * @returns {Array} Returns the new array of regrouped elements.\n * @example\n *\n * var zipped = _.zip([1, 2], [10, 20], [100, 200]);\n * // => [[1, 10, 100], [2, 20, 200]]\n *\n * _.unzipWith(zipped, _.add);\n * // => [3, 30, 300]\n */\n function unzipWith(array, iteratee) {\n if (!(array && array.length)) {\n return [];\n }\n var result = unzip(array);\n if (iteratee == null) {\n return result;\n }\n return arrayMap(result, function(group) {\n return apply(iteratee, undefined, group);\n });\n }\n\n /**\n * Creates an array excluding all given values using\n * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons.\n *\n * **Note:** Unlike `_.pull`, this method returns a new array.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {...*} [values] The values to exclude.\n * @returns {Array} Returns the new array of filtered values.\n * @see _.difference, _.xor\n * @example\n *\n * _.without([2, 1, 2, 3], 1, 2);\n * // => [3]\n */\n var without = baseRest(function(array, values) {\n return isArrayLikeObject(array)\n ? baseDifference(array, values)\n : [];\n });\n\n /**\n * Creates an array of unique values that is the\n * [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)\n * of the given arrays. The order of result values is determined by the order\n * they occur in the arrays.\n *\n * @static\n * @memberOf _\n * @since 2.4.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @returns {Array} Returns the new array of filtered values.\n * @see _.difference, _.without\n * @example\n *\n * _.xor([2, 1], [2, 3]);\n * // => [1, 3]\n */\n var xor = baseRest(function(arrays) {\n return baseXor(arrayFilter(arrays, isArrayLikeObject));\n });\n\n /**\n * This method is like `_.xor` except that it accepts `iteratee` which is\n * invoked for each element of each `arrays` to generate the criterion by\n * which by which they're compared. The order of result values is determined\n * by the order they occur in the arrays. The iteratee is invoked with one\n * argument: (value).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new array of filtered values.\n * @example\n *\n * _.xorBy([2.1, 1.2], [2.3, 3.4], Math.floor);\n * // => [1.2, 3.4]\n *\n * // The `_.property` iteratee shorthand.\n * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 2 }]\n */\n var xorBy = baseRest(function(arrays) {\n var iteratee = last(arrays);\n if (isArrayLikeObject(iteratee)) {\n iteratee = undefined;\n }\n return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee, 2));\n });\n\n /**\n * This method is like `_.xor` except that it accepts `comparator` which is\n * invoked to compare elements of `arrays`. The order of result values is\n * determined by the order they occur in the arrays. The comparator is invoked\n * with two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of filtered values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.xorWith(objects, others, _.isEqual);\n * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]\n */\n var xorWith = baseRest(function(arrays) {\n var comparator = last(arrays);\n comparator = typeof comparator == 'function' ? comparator : undefined;\n return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator);\n });\n\n /**\n * Creates an array of grouped elements, the first of which contains the\n * first elements of the given arrays, the second of which contains the\n * second elements of the given arrays, and so on.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {...Array} [arrays] The arrays to process.\n * @returns {Array} Returns the new array of grouped elements.\n * @example\n *\n * _.zip(['a', 'b'], [1, 2], [true, false]);\n * // => [['a', 1, true], ['b', 2, false]]\n */\n var zip = baseRest(unzip);\n\n /**\n * This method is like `_.fromPairs` except that it accepts two arrays,\n * one of property identifiers and one of corresponding values.\n *\n * @static\n * @memberOf _\n * @since 0.4.0\n * @category Array\n * @param {Array} [props=[]] The property identifiers.\n * @param {Array} [values=[]] The property values.\n * @returns {Object} Returns the new object.\n * @example\n *\n * _.zipObject(['a', 'b'], [1, 2]);\n * // => { 'a': 1, 'b': 2 }\n */\n function zipObject(props, values) {\n return baseZipObject(props || [], values || [], assignValue);\n }\n\n /**\n * This method is like `_.zipObject` except that it supports property paths.\n *\n * @static\n * @memberOf _\n * @since 4.1.0\n * @category Array\n * @param {Array} [props=[]] The property identifiers.\n * @param {Array} [values=[]] The property values.\n * @returns {Object} Returns the new object.\n * @example\n *\n * _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]);\n * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } }\n */\n function zipObjectDeep(props, values) {\n return baseZipObject(props || [], values || [], baseSet);\n }\n\n /**\n * This method is like `_.zip` except that it accepts `iteratee` to specify\n * how grouped values should be combined. The iteratee is invoked with the\n * elements of each group: (...group).\n *\n * @static\n * @memberOf _\n * @since 3.8.0\n * @category Array\n * @param {...Array} [arrays] The arrays to process.\n * @param {Function} [iteratee=_.identity] The function to combine\n * grouped values.\n * @returns {Array} Returns the new array of grouped elements.\n * @example\n *\n * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) {\n * return a + b + c;\n * });\n * // => [111, 222]\n */\n var zipWith = baseRest(function(arrays) {\n var length = arrays.length,\n iteratee = length > 1 ? arrays[length - 1] : undefined;\n\n iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined;\n return unzipWith(arrays, iteratee);\n });\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates a `lodash` wrapper instance that wraps `value` with explicit method\n * chain sequences enabled. The result of such sequences must be unwrapped\n * with `_#value`.\n *\n * @static\n * @memberOf _\n * @since 1.3.0\n * @category Seq\n * @param {*} value The value to wrap.\n * @returns {Object} Returns the new `lodash` wrapper instance.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'age': 36 },\n * { 'user': 'fred', 'age': 40 },\n * { 'user': 'pebbles', 'age': 1 }\n * ];\n *\n * var youngest = _\n * .chain(users)\n * .sortBy('age')\n * .map(function(o) {\n * return o.user + ' is ' + o.age;\n * })\n * .head()\n * .value();\n * // => 'pebbles is 1'\n */\n function chain(value) {\n var result = lodash(value);\n result.__chain__ = true;\n return result;\n }\n\n /**\n * This method invokes `interceptor` and returns `value`. The interceptor\n * is invoked with one argument; (value). The purpose of this method is to\n * \"tap into\" a method chain sequence in order to modify intermediate results.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Seq\n * @param {*} value The value to provide to `interceptor`.\n * @param {Function} interceptor The function to invoke.\n * @returns {*} Returns `value`.\n * @example\n *\n * _([1, 2, 3])\n * .tap(function(array) {\n * // Mutate input array.\n * array.pop();\n * })\n * .reverse()\n * .value();\n * // => [2, 1]\n */\n function tap(value, interceptor) {\n interceptor(value);\n return value;\n }\n\n /**\n * This method is like `_.tap` except that it returns the result of `interceptor`.\n * The purpose of this method is to \"pass thru\" values replacing intermediate\n * results in a method chain sequence.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Seq\n * @param {*} value The value to provide to `interceptor`.\n * @param {Function} interceptor The function to invoke.\n * @returns {*} Returns the result of `interceptor`.\n * @example\n *\n * _(' abc ')\n * .chain()\n * .trim()\n * .thru(function(value) {\n * return [value];\n * })\n * .value();\n * // => ['abc']\n */\n function thru(value, interceptor) {\n return interceptor(value);\n }\n\n /**\n * This method is the wrapper version of `_.at`.\n *\n * @name at\n * @memberOf _\n * @since 1.0.0\n * @category Seq\n * @param {...(string|string[])} [paths] The property paths to pick.\n * @returns {Object} Returns the new `lodash` wrapper instance.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };\n *\n * _(object).at(['a[0].b.c', 'a[1]']).value();\n * // => [3, 4]\n */\n var wrapperAt = flatRest(function(paths) {\n var length = paths.length,\n start = length ? paths[0] : 0,\n value = this.__wrapped__,\n interceptor = function(object) { return baseAt(object, paths); };\n\n if (length > 1 || this.__actions__.length ||\n !(value instanceof LazyWrapper) || !isIndex(start)) {\n return this.thru(interceptor);\n }\n value = value.slice(start, +start + (length ? 1 : 0));\n value.__actions__.push({\n 'func': thru,\n 'args': [interceptor],\n 'thisArg': undefined\n });\n return new LodashWrapper(value, this.__chain__).thru(function(array) {\n if (length && !array.length) {\n array.push(undefined);\n }\n return array;\n });\n });\n\n /**\n * Creates a `lodash` wrapper instance with explicit method chain sequences enabled.\n *\n * @name chain\n * @memberOf _\n * @since 0.1.0\n * @category Seq\n * @returns {Object} Returns the new `lodash` wrapper instance.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'age': 36 },\n * { 'user': 'fred', 'age': 40 }\n * ];\n *\n * // A sequence without explicit chaining.\n * _(users).head();\n * // => { 'user': 'barney', 'age': 36 }\n *\n * // A sequence with explicit chaining.\n * _(users)\n * .chain()\n * .head()\n * .pick('user')\n * .value();\n * // => { 'user': 'barney' }\n */\n function wrapperChain() {\n return chain(this);\n }\n\n /**\n * Executes the chain sequence and returns the wrapped result.\n *\n * @name commit\n * @memberOf _\n * @since 3.2.0\n * @category Seq\n * @returns {Object} Returns the new `lodash` wrapper instance.\n * @example\n *\n * var array = [1, 2];\n * var wrapped = _(array).push(3);\n *\n * console.log(array);\n * // => [1, 2]\n *\n * wrapped = wrapped.commit();\n * console.log(array);\n * // => [1, 2, 3]\n *\n * wrapped.last();\n * // => 3\n *\n * console.log(array);\n * // => [1, 2, 3]\n */\n function wrapperCommit() {\n return new LodashWrapper(this.value(), this.__chain__);\n }\n\n /**\n * Gets the next value on a wrapped object following the\n * [iterator protocol](https://mdn.io/iteration_protocols#iterator).\n *\n * @name next\n * @memberOf _\n * @since 4.0.0\n * @category Seq\n * @returns {Object} Returns the next iterator value.\n * @example\n *\n * var wrapped = _([1, 2]);\n *\n * wrapped.next();\n * // => { 'done': false, 'value': 1 }\n *\n * wrapped.next();\n * // => { 'done': false, 'value': 2 }\n *\n * wrapped.next();\n * // => { 'done': true, 'value': undefined }\n */\n function wrapperNext() {\n if (this.__values__ === undefined) {\n this.__values__ = toArray(this.value());\n }\n var done = this.__index__ >= this.__values__.length,\n value = done ? undefined : this.__values__[this.__index__++];\n\n return { 'done': done, 'value': value };\n }\n\n /**\n * Enables the wrapper to be iterable.\n *\n * @name Symbol.iterator\n * @memberOf _\n * @since 4.0.0\n * @category Seq\n * @returns {Object} Returns the wrapper object.\n * @example\n *\n * var wrapped = _([1, 2]);\n *\n * wrapped[Symbol.iterator]() === wrapped;\n * // => true\n *\n * Array.from(wrapped);\n * // => [1, 2]\n */\n function wrapperToIterator() {\n return this;\n }\n\n /**\n * Creates a clone of the chain sequence planting `value` as the wrapped value.\n *\n * @name plant\n * @memberOf _\n * @since 3.2.0\n * @category Seq\n * @param {*} value The value to plant.\n * @returns {Object} Returns the new `lodash` wrapper instance.\n * @example\n *\n * function square(n) {\n * return n * n;\n * }\n *\n * var wrapped = _([1, 2]).map(square);\n * var other = wrapped.plant([3, 4]);\n *\n * other.value();\n * // => [9, 16]\n *\n * wrapped.value();\n * // => [1, 4]\n */\n function wrapperPlant(value) {\n var result,\n parent = this;\n\n while (parent instanceof baseLodash) {\n var clone = wrapperClone(parent);\n clone.__index__ = 0;\n clone.__values__ = undefined;\n if (result) {\n previous.__wrapped__ = clone;\n } else {\n result = clone;\n }\n var previous = clone;\n parent = parent.__wrapped__;\n }\n previous.__wrapped__ = value;\n return result;\n }\n\n /**\n * This method is the wrapper version of `_.reverse`.\n *\n * **Note:** This method mutates the wrapped array.\n *\n * @name reverse\n * @memberOf _\n * @since 0.1.0\n * @category Seq\n * @returns {Object} Returns the new `lodash` wrapper instance.\n * @example\n *\n * var array = [1, 2, 3];\n *\n * _(array).reverse().value()\n * // => [3, 2, 1]\n *\n * console.log(array);\n * // => [3, 2, 1]\n */\n function wrapperReverse() {\n var value = this.__wrapped__;\n if (value instanceof LazyWrapper) {\n var wrapped = value;\n if (this.__actions__.length) {\n wrapped = new LazyWrapper(this);\n }\n wrapped = wrapped.reverse();\n wrapped.__actions__.push({\n 'func': thru,\n 'args': [reverse],\n 'thisArg': undefined\n });\n return new LodashWrapper(wrapped, this.__chain__);\n }\n return this.thru(reverse);\n }\n\n /**\n * Executes the chain sequence to resolve the unwrapped value.\n *\n * @name value\n * @memberOf _\n * @since 0.1.0\n * @alias toJSON, valueOf\n * @category Seq\n * @returns {*} Returns the resolved unwrapped value.\n * @example\n *\n * _([1, 2, 3]).value();\n * // => [1, 2, 3]\n */\n function wrapperValue() {\n return baseWrapperValue(this.__wrapped__, this.__actions__);\n }\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Creates an object composed of keys generated from the results of running\n * each element of `collection` thru `iteratee`. The corresponding value of\n * each key is the number of times the key was returned by `iteratee`. The\n * iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @since 0.5.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The iteratee to transform keys.\n * @returns {Object} Returns the composed aggregate object.\n * @example\n *\n * _.countBy([6.1, 4.2, 6.3], Math.floor);\n * // => { '4': 1, '6': 2 }\n *\n * // The `_.property` iteratee shorthand.\n * _.countBy(['one', 'two', 'three'], 'length');\n * // => { '3': 2, '5': 1 }\n */\n var countBy = createAggregator(function(result, value, key) {\n if (hasOwnProperty.call(result, key)) {\n ++result[key];\n } else {\n baseAssignValue(result, key, 1);\n }\n });\n\n /**\n * Checks if `predicate` returns truthy for **all** elements of `collection`.\n * Iteration is stopped once `predicate` returns falsey. The predicate is\n * invoked with three arguments: (value, index|key, collection).\n *\n * **Note:** This method returns `true` for\n * [empty collections](https://en.wikipedia.org/wiki/Empty_set) because\n * [everything is true](https://en.wikipedia.org/wiki/Vacuous_truth) of\n * elements of empty collections.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {boolean} Returns `true` if all elements pass the predicate check,\n * else `false`.\n * @example\n *\n * _.every([true, 1, null, 'yes'], Boolean);\n * // => false\n *\n * var users = [\n * { 'user': 'barney', 'age': 36, 'active': false },\n * { 'user': 'fred', 'age': 40, 'active': false }\n * ];\n *\n * // The `_.matches` iteratee shorthand.\n * _.every(users, { 'user': 'barney', 'active': false });\n * // => false\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.every(users, ['active', false]);\n * // => true\n *\n * // The `_.property` iteratee shorthand.\n * _.every(users, 'active');\n * // => false\n */\n function every(collection, predicate, guard) {\n var func = isArray(collection) ? arrayEvery : baseEvery;\n if (guard && isIterateeCall(collection, predicate, guard)) {\n predicate = undefined;\n }\n return func(collection, getIteratee(predicate, 3));\n }\n\n /**\n * Iterates over elements of `collection`, returning an array of all elements\n * `predicate` returns truthy for. The predicate is invoked with three\n * arguments: (value, index|key, collection).\n *\n * **Note:** Unlike `_.remove`, this method returns a new array.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the new filtered array.\n * @see _.reject\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'age': 36, 'active': true },\n * { 'user': 'fred', 'age': 40, 'active': false }\n * ];\n *\n * _.filter(users, function(o) { return !o.active; });\n * // => objects for ['fred']\n *\n * // The `_.matches` iteratee shorthand.\n * _.filter(users, { 'age': 36, 'active': true });\n * // => objects for ['barney']\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.filter(users, ['active', false]);\n * // => objects for ['fred']\n *\n * // The `_.property` iteratee shorthand.\n * _.filter(users, 'active');\n * // => objects for ['barney']\n */\n function filter(collection, predicate) {\n var func = isArray(collection) ? arrayFilter : baseFilter;\n return func(collection, getIteratee(predicate, 3));\n }\n\n /**\n * Iterates over elements of `collection`, returning the first element\n * `predicate` returns truthy for. The predicate is invoked with three\n * arguments: (value, index|key, collection).\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to inspect.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @param {number} [fromIndex=0] The index to search from.\n * @returns {*} Returns the matched element, else `undefined`.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'age': 36, 'active': true },\n * { 'user': 'fred', 'age': 40, 'active': false },\n * { 'user': 'pebbles', 'age': 1, 'active': true }\n * ];\n *\n * _.find(users, function(o) { return o.age < 40; });\n * // => object for 'barney'\n *\n * // The `_.matches` iteratee shorthand.\n * _.find(users, { 'age': 1, 'active': true });\n * // => object for 'pebbles'\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.find(users, ['active', false]);\n * // => object for 'fred'\n *\n * // The `_.property` iteratee shorthand.\n * _.find(users, 'active');\n * // => object for 'barney'\n */\n var find = createFind(findIndex);\n\n /**\n * This method is like `_.find` except that it iterates over elements of\n * `collection` from right to left.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Collection\n * @param {Array|Object} collection The collection to inspect.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @param {number} [fromIndex=collection.length-1] The index to search from.\n * @returns {*} Returns the matched element, else `undefined`.\n * @example\n *\n * _.findLast([1, 2, 3, 4], function(n) {\n * return n % 2 == 1;\n * });\n * // => 3\n */\n var findLast = createFind(findLastIndex);\n\n /**\n * Creates a flattened array of values by running each element in `collection`\n * thru `iteratee` and flattening the mapped results. The iteratee is invoked\n * with three arguments: (value, index|key, collection).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the new flattened array.\n * @example\n *\n * function duplicate(n) {\n * return [n, n];\n * }\n *\n * _.flatMap([1, 2], duplicate);\n * // => [1, 1, 2, 2]\n */\n function flatMap(collection, iteratee) {\n return baseFlatten(map(collection, iteratee), 1);\n }\n\n /**\n * This method is like `_.flatMap` except that it recursively flattens the\n * mapped results.\n *\n * @static\n * @memberOf _\n * @since 4.7.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the new flattened array.\n * @example\n *\n * function duplicate(n) {\n * return [[[n, n]]];\n * }\n *\n * _.flatMapDeep([1, 2], duplicate);\n * // => [1, 1, 2, 2]\n */\n function flatMapDeep(collection, iteratee) {\n return baseFlatten(map(collection, iteratee), INFINITY);\n }\n\n /**\n * This method is like `_.flatMap` except that it recursively flattens the\n * mapped results up to `depth` times.\n *\n * @static\n * @memberOf _\n * @since 4.7.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @param {number} [depth=1] The maximum recursion depth.\n * @returns {Array} Returns the new flattened array.\n * @example\n *\n * function duplicate(n) {\n * return [[[n, n]]];\n * }\n *\n * _.flatMapDepth([1, 2], duplicate, 2);\n * // => [[1, 1], [2, 2]]\n */\n function flatMapDepth(collection, iteratee, depth) {\n depth = depth === undefined ? 1 : toInteger(depth);\n return baseFlatten(map(collection, iteratee), depth);\n }\n\n /**\n * Iterates over elements of `collection` and invokes `iteratee` for each element.\n * The iteratee is invoked with three arguments: (value, index|key, collection).\n * Iteratee functions may exit iteration early by explicitly returning `false`.\n *\n * **Note:** As with other \"Collections\" methods, objects with a \"length\"\n * property are iterated like arrays. To avoid this behavior use `_.forIn`\n * or `_.forOwn` for object iteration.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @alias each\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Array|Object} Returns `collection`.\n * @see _.forEachRight\n * @example\n *\n * _.forEach([1, 2], function(value) {\n * console.log(value);\n * });\n * // => Logs `1` then `2`.\n *\n * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) {\n * console.log(key);\n * });\n * // => Logs 'a' then 'b' (iteration order is not guaranteed).\n */\n function forEach(collection, iteratee) {\n var func = isArray(collection) ? arrayEach : baseEach;\n return func(collection, getIteratee(iteratee, 3));\n }\n\n /**\n * This method is like `_.forEach` except that it iterates over elements of\n * `collection` from right to left.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @alias eachRight\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Array|Object} Returns `collection`.\n * @see _.forEach\n * @example\n *\n * _.forEachRight([1, 2], function(value) {\n * console.log(value);\n * });\n * // => Logs `2` then `1`.\n */\n function forEachRight(collection, iteratee) {\n var func = isArray(collection) ? arrayEachRight : baseEachRight;\n return func(collection, getIteratee(iteratee, 3));\n }\n\n /**\n * Creates an object composed of keys generated from the results of running\n * each element of `collection` thru `iteratee`. The order of grouped values\n * is determined by the order they occur in `collection`. The corresponding\n * value of each key is an array of elements responsible for generating the\n * key. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The iteratee to transform keys.\n * @returns {Object} Returns the composed aggregate object.\n * @example\n *\n * _.groupBy([6.1, 4.2, 6.3], Math.floor);\n * // => { '4': [4.2], '6': [6.1, 6.3] }\n *\n * // The `_.property` iteratee shorthand.\n * _.groupBy(['one', 'two', 'three'], 'length');\n * // => { '3': ['one', 'two'], '5': ['three'] }\n */\n var groupBy = createAggregator(function(result, value, key) {\n if (hasOwnProperty.call(result, key)) {\n result[key].push(value);\n } else {\n baseAssignValue(result, key, [value]);\n }\n });\n\n /**\n * Checks if `value` is in `collection`. If `collection` is a string, it's\n * checked for a substring of `value`, otherwise\n * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * is used for equality comparisons. If `fromIndex` is negative, it's used as\n * the offset from the end of `collection`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object|string} collection The collection to inspect.\n * @param {*} value The value to search for.\n * @param {number} [fromIndex=0] The index to search from.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`.\n * @returns {boolean} Returns `true` if `value` is found, else `false`.\n * @example\n *\n * _.includes([1, 2, 3], 1);\n * // => true\n *\n * _.includes([1, 2, 3], 1, 2);\n * // => false\n *\n * _.includes({ 'a': 1, 'b': 2 }, 1);\n * // => true\n *\n * _.includes('abcd', 'bc');\n * // => true\n */\n function includes(collection, value, fromIndex, guard) {\n collection = isArrayLike(collection) ? collection : values(collection);\n fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0;\n\n var length = collection.length;\n if (fromIndex < 0) {\n fromIndex = nativeMax(length + fromIndex, 0);\n }\n return isString(collection)\n ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1)\n : (!!length && baseIndexOf(collection, value, fromIndex) > -1);\n }\n\n /**\n * Invokes the method at `path` of each element in `collection`, returning\n * an array of the results of each invoked method. Any additional arguments\n * are provided to each invoked method. If `path` is a function, it's invoked\n * for, and `this` bound to, each element in `collection`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Array|Function|string} path The path of the method to invoke or\n * the function invoked per iteration.\n * @param {...*} [args] The arguments to invoke each method with.\n * @returns {Array} Returns the array of results.\n * @example\n *\n * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort');\n * // => [[1, 5, 7], [1, 2, 3]]\n *\n * _.invokeMap([123, 456], String.prototype.split, '');\n * // => [['1', '2', '3'], ['4', '5', '6']]\n */\n var invokeMap = baseRest(function(collection, path, args) {\n var index = -1,\n isFunc = typeof path == 'function',\n result = isArrayLike(collection) ? Array(collection.length) : [];\n\n baseEach(collection, function(value) {\n result[++index] = isFunc ? apply(path, value, args) : baseInvoke(value, path, args);\n });\n return result;\n });\n\n /**\n * Creates an object composed of keys generated from the results of running\n * each element of `collection` thru `iteratee`. The corresponding value of\n * each key is the last element responsible for generating the key. The\n * iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The iteratee to transform keys.\n * @returns {Object} Returns the composed aggregate object.\n * @example\n *\n * var array = [\n * { 'dir': 'left', 'code': 97 },\n * { 'dir': 'right', 'code': 100 }\n * ];\n *\n * _.keyBy(array, function(o) {\n * return String.fromCharCode(o.code);\n * });\n * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } }\n *\n * _.keyBy(array, 'dir');\n * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } }\n */\n var keyBy = createAggregator(function(result, value, key) {\n baseAssignValue(result, key, value);\n });\n\n /**\n * Creates an array of values by running each element in `collection` thru\n * `iteratee`. The iteratee is invoked with three arguments:\n * (value, index|key, collection).\n *\n * Many lodash methods are guarded to work as iteratees for methods like\n * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`.\n *\n * The guarded methods are:\n * `ary`, `chunk`, `curry`, `curryRight`, `drop`, `dropRight`, `every`,\n * `fill`, `invert`, `parseInt`, `random`, `range`, `rangeRight`, `repeat`,\n * `sampleSize`, `slice`, `some`, `sortBy`, `split`, `take`, `takeRight`,\n * `template`, `trim`, `trimEnd`, `trimStart`, and `words`\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the new mapped array.\n * @example\n *\n * function square(n) {\n * return n * n;\n * }\n *\n * _.map([4, 8], square);\n * // => [16, 64]\n *\n * _.map({ 'a': 4, 'b': 8 }, square);\n * // => [16, 64] (iteration order is not guaranteed)\n *\n * var users = [\n * { 'user': 'barney' },\n * { 'user': 'fred' }\n * ];\n *\n * // The `_.property` iteratee shorthand.\n * _.map(users, 'user');\n * // => ['barney', 'fred']\n */\n function map(collection, iteratee) {\n var func = isArray(collection) ? arrayMap : baseMap;\n return func(collection, getIteratee(iteratee, 3));\n }\n\n /**\n * This method is like `_.sortBy` except that it allows specifying the sort\n * orders of the iteratees to sort by. If `orders` is unspecified, all values\n * are sorted in ascending order. Otherwise, specify an order of \"desc\" for\n * descending or \"asc\" for ascending sort order of corresponding values.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Array[]|Function[]|Object[]|string[]} [iteratees=[_.identity]]\n * The iteratees to sort by.\n * @param {string[]} [orders] The sort orders of `iteratees`.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`.\n * @returns {Array} Returns the new sorted array.\n * @example\n *\n * var users = [\n * { 'user': 'fred', 'age': 48 },\n * { 'user': 'barney', 'age': 34 },\n * { 'user': 'fred', 'age': 40 },\n * { 'user': 'barney', 'age': 36 }\n * ];\n *\n * // Sort by `user` in ascending order and by `age` in descending order.\n * _.orderBy(users, ['user', 'age'], ['asc', 'desc']);\n * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]\n */\n function orderBy(collection, iteratees, orders, guard) {\n if (collection == null) {\n return [];\n }\n if (!isArray(iteratees)) {\n iteratees = iteratees == null ? [] : [iteratees];\n }\n orders = guard ? undefined : orders;\n if (!isArray(orders)) {\n orders = orders == null ? [] : [orders];\n }\n return baseOrderBy(collection, iteratees, orders);\n }\n\n /**\n * Creates an array of elements split into two groups, the first of which\n * contains elements `predicate` returns truthy for, the second of which\n * contains elements `predicate` returns falsey for. The predicate is\n * invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the array of grouped elements.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'age': 36, 'active': false },\n * { 'user': 'fred', 'age': 40, 'active': true },\n * { 'user': 'pebbles', 'age': 1, 'active': false }\n * ];\n *\n * _.partition(users, function(o) { return o.active; });\n * // => objects for [['fred'], ['barney', 'pebbles']]\n *\n * // The `_.matches` iteratee shorthand.\n * _.partition(users, { 'age': 1, 'active': false });\n * // => objects for [['pebbles'], ['barney', 'fred']]\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.partition(users, ['active', false]);\n * // => objects for [['barney', 'pebbles'], ['fred']]\n *\n * // The `_.property` iteratee shorthand.\n * _.partition(users, 'active');\n * // => objects for [['fred'], ['barney', 'pebbles']]\n */\n var partition = createAggregator(function(result, value, key) {\n result[key ? 0 : 1].push(value);\n }, function() { return [[], []]; });\n\n /**\n * Reduces `collection` to a value which is the accumulated result of running\n * each element in `collection` thru `iteratee`, where each successive\n * invocation is supplied the return value of the previous. If `accumulator`\n * is not given, the first element of `collection` is used as the initial\n * value. The iteratee is invoked with four arguments:\n * (accumulator, value, index|key, collection).\n *\n * Many lodash methods are guarded to work as iteratees for methods like\n * `_.reduce`, `_.reduceRight`, and `_.transform`.\n *\n * The guarded methods are:\n * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`,\n * and `sortBy`\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @param {*} [accumulator] The initial value.\n * @returns {*} Returns the accumulated value.\n * @see _.reduceRight\n * @example\n *\n * _.reduce([1, 2], function(sum, n) {\n * return sum + n;\n * }, 0);\n * // => 3\n *\n * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {\n * (result[value] || (result[value] = [])).push(key);\n * return result;\n * }, {});\n * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed)\n */\n function reduce(collection, iteratee, accumulator) {\n var func = isArray(collection) ? arrayReduce : baseReduce,\n initAccum = arguments.length < 3;\n\n return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach);\n }\n\n /**\n * This method is like `_.reduce` except that it iterates over elements of\n * `collection` from right to left.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @param {*} [accumulator] The initial value.\n * @returns {*} Returns the accumulated value.\n * @see _.reduce\n * @example\n *\n * var array = [[0, 1], [2, 3], [4, 5]];\n *\n * _.reduceRight(array, function(flattened, other) {\n * return flattened.concat(other);\n * }, []);\n * // => [4, 5, 2, 3, 0, 1]\n */\n function reduceRight(collection, iteratee, accumulator) {\n var func = isArray(collection) ? arrayReduceRight : baseReduce,\n initAccum = arguments.length < 3;\n\n return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight);\n }\n\n /**\n * The opposite of `_.filter`; this method returns the elements of `collection`\n * that `predicate` does **not** return truthy for.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {Array} Returns the new filtered array.\n * @see _.filter\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'age': 36, 'active': false },\n * { 'user': 'fred', 'age': 40, 'active': true }\n * ];\n *\n * _.reject(users, function(o) { return !o.active; });\n * // => objects for ['fred']\n *\n * // The `_.matches` iteratee shorthand.\n * _.reject(users, { 'age': 40, 'active': true });\n * // => objects for ['barney']\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.reject(users, ['active', false]);\n * // => objects for ['fred']\n *\n * // The `_.property` iteratee shorthand.\n * _.reject(users, 'active');\n * // => objects for ['barney']\n */\n function reject(collection, predicate) {\n var func = isArray(collection) ? arrayFilter : baseFilter;\n return func(collection, negate(getIteratee(predicate, 3)));\n }\n\n /**\n * Gets a random element from `collection`.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Collection\n * @param {Array|Object} collection The collection to sample.\n * @returns {*} Returns the random element.\n * @example\n *\n * _.sample([1, 2, 3, 4]);\n * // => 2\n */\n function sample(collection) {\n var func = isArray(collection) ? arraySample : baseSample;\n return func(collection);\n }\n\n /**\n * Gets `n` random elements at unique keys from `collection` up to the\n * size of `collection`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Collection\n * @param {Array|Object} collection The collection to sample.\n * @param {number} [n=1] The number of elements to sample.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Array} Returns the random elements.\n * @example\n *\n * _.sampleSize([1, 2, 3], 2);\n * // => [3, 1]\n *\n * _.sampleSize([1, 2, 3], 4);\n * // => [2, 3, 1]\n */\n function sampleSize(collection, n, guard) {\n if ((guard ? isIterateeCall(collection, n, guard) : n === undefined)) {\n n = 1;\n } else {\n n = toInteger(n);\n }\n var func = isArray(collection) ? arraySampleSize : baseSampleSize;\n return func(collection, n);\n }\n\n /**\n * Creates an array of shuffled values, using a version of the\n * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle).\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to shuffle.\n * @returns {Array} Returns the new shuffled array.\n * @example\n *\n * _.shuffle([1, 2, 3, 4]);\n * // => [4, 1, 3, 2]\n */\n function shuffle(collection) {\n var func = isArray(collection) ? arrayShuffle : baseShuffle;\n return func(collection);\n }\n\n /**\n * Gets the size of `collection` by returning its length for array-like\n * values or the number of own enumerable string keyed properties for objects.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object|string} collection The collection to inspect.\n * @returns {number} Returns the collection size.\n * @example\n *\n * _.size([1, 2, 3]);\n * // => 3\n *\n * _.size({ 'a': 1, 'b': 2 });\n * // => 2\n *\n * _.size('pebbles');\n * // => 7\n */\n function size(collection) {\n if (collection == null) {\n return 0;\n }\n if (isArrayLike(collection)) {\n return isString(collection) ? stringSize(collection) : collection.length;\n }\n var tag = getTag(collection);\n if (tag == mapTag || tag == setTag) {\n return collection.size;\n }\n return baseKeys(collection).length;\n }\n\n /**\n * Checks if `predicate` returns truthy for **any** element of `collection`.\n * Iteration is stopped once `predicate` returns truthy. The predicate is\n * invoked with three arguments: (value, index|key, collection).\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {boolean} Returns `true` if any element passes the predicate check,\n * else `false`.\n * @example\n *\n * _.some([null, 0, 'yes', false], Boolean);\n * // => true\n *\n * var users = [\n * { 'user': 'barney', 'active': true },\n * { 'user': 'fred', 'active': false }\n * ];\n *\n * // The `_.matches` iteratee shorthand.\n * _.some(users, { 'user': 'barney', 'active': false });\n * // => false\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.some(users, ['active', false]);\n * // => true\n *\n * // The `_.property` iteratee shorthand.\n * _.some(users, 'active');\n * // => true\n */\n function some(collection, predicate, guard) {\n var func = isArray(collection) ? arraySome : baseSome;\n if (guard && isIterateeCall(collection, predicate, guard)) {\n predicate = undefined;\n }\n return func(collection, getIteratee(predicate, 3));\n }\n\n /**\n * Creates an array of elements, sorted in ascending order by the results of\n * running each element in a collection thru each iteratee. This method\n * performs a stable sort, that is, it preserves the original sort order of\n * equal elements. The iteratees are invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {...(Function|Function[])} [iteratees=[_.identity]]\n * The iteratees to sort by.\n * @returns {Array} Returns the new sorted array.\n * @example\n *\n * var users = [\n * { 'user': 'fred', 'age': 48 },\n * { 'user': 'barney', 'age': 36 },\n * { 'user': 'fred', 'age': 40 },\n * { 'user': 'barney', 'age': 34 }\n * ];\n *\n * _.sortBy(users, [function(o) { return o.user; }]);\n * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]\n *\n * _.sortBy(users, ['user', 'age']);\n * // => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]\n */\n var sortBy = baseRest(function(collection, iteratees) {\n if (collection == null) {\n return [];\n }\n var length = iteratees.length;\n if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) {\n iteratees = [];\n } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) {\n iteratees = [iteratees[0]];\n }\n return baseOrderBy(collection, baseFlatten(iteratees, 1), []);\n });\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Gets the timestamp of the number of milliseconds that have elapsed since\n * the Unix epoch (1 January 1970 00:00:00 UTC).\n *\n * @static\n * @memberOf _\n * @since 2.4.0\n * @category Date\n * @returns {number} Returns the timestamp.\n * @example\n *\n * _.defer(function(stamp) {\n * console.log(_.now() - stamp);\n * }, _.now());\n * // => Logs the number of milliseconds it took for the deferred invocation.\n */\n var now = ctxNow || function() {\n return root.Date.now();\n };\n\n /*------------------------------------------------------------------------*/\n\n /**\n * The opposite of `_.before`; this method creates a function that invokes\n * `func` once it's called `n` or more times.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {number} n The number of calls before `func` is invoked.\n * @param {Function} func The function to restrict.\n * @returns {Function} Returns the new restricted function.\n * @example\n *\n * var saves = ['profile', 'settings'];\n *\n * var done = _.after(saves.length, function() {\n * console.log('done saving!');\n * });\n *\n * _.forEach(saves, function(type) {\n * asyncSave({ 'type': type, 'complete': done });\n * });\n * // => Logs 'done saving!' after the two async saves have completed.\n */\n function after(n, func) {\n if (typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n n = toInteger(n);\n return function() {\n if (--n < 1) {\n return func.apply(this, arguments);\n }\n };\n }\n\n /**\n * Creates a function that invokes `func`, with up to `n` arguments,\n * ignoring any additional arguments.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Function\n * @param {Function} func The function to cap arguments for.\n * @param {number} [n=func.length] The arity cap.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Function} Returns the new capped function.\n * @example\n *\n * _.map(['6', '8', '10'], _.ary(parseInt, 1));\n * // => [6, 8, 10]\n */\n function ary(func, n, guard) {\n n = guard ? undefined : n;\n n = (func && n == null) ? func.length : n;\n return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n);\n }\n\n /**\n * Creates a function that invokes `func`, with the `this` binding and arguments\n * of the created function, while it's called less than `n` times. Subsequent\n * calls to the created function return the result of the last `func` invocation.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Function\n * @param {number} n The number of calls at which `func` is no longer invoked.\n * @param {Function} func The function to restrict.\n * @returns {Function} Returns the new restricted function.\n * @example\n *\n * jQuery(element).on('click', _.before(5, addContactToList));\n * // => Allows adding up to 4 contacts to the list.\n */\n function before(n, func) {\n var result;\n if (typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n n = toInteger(n);\n return function() {\n if (--n > 0) {\n result = func.apply(this, arguments);\n }\n if (n <= 1) {\n func = undefined;\n }\n return result;\n };\n }\n\n /**\n * Creates a function that invokes `func` with the `this` binding of `thisArg`\n * and `partials` prepended to the arguments it receives.\n *\n * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds,\n * may be used as a placeholder for partially applied arguments.\n *\n * **Note:** Unlike native `Function#bind`, this method doesn't set the \"length\"\n * property of bound functions.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {Function} func The function to bind.\n * @param {*} thisArg The `this` binding of `func`.\n * @param {...*} [partials] The arguments to be partially applied.\n * @returns {Function} Returns the new bound function.\n * @example\n *\n * function greet(greeting, punctuation) {\n * return greeting + ' ' + this.user + punctuation;\n * }\n *\n * var object = { 'user': 'fred' };\n *\n * var bound = _.bind(greet, object, 'hi');\n * bound('!');\n * // => 'hi fred!'\n *\n * // Bound with placeholders.\n * var bound = _.bind(greet, object, _, '!');\n * bound('hi');\n * // => 'hi fred!'\n */\n var bind = baseRest(function(func, thisArg, partials) {\n var bitmask = WRAP_BIND_FLAG;\n if (partials.length) {\n var holders = replaceHolders(partials, getHolder(bind));\n bitmask |= WRAP_PARTIAL_FLAG;\n }\n return createWrap(func, bitmask, thisArg, partials, holders);\n });\n\n /**\n * Creates a function that invokes the method at `object[key]` with `partials`\n * prepended to the arguments it receives.\n *\n * This method differs from `_.bind` by allowing bound functions to reference\n * methods that may be redefined or don't yet exist. See\n * [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern)\n * for more details.\n *\n * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic\n * builds, may be used as a placeholder for partially applied arguments.\n *\n * @static\n * @memberOf _\n * @since 0.10.0\n * @category Function\n * @param {Object} object The object to invoke the method on.\n * @param {string} key The key of the method.\n * @param {...*} [partials] The arguments to be partially applied.\n * @returns {Function} Returns the new bound function.\n * @example\n *\n * var object = {\n * 'user': 'fred',\n * 'greet': function(greeting, punctuation) {\n * return greeting + ' ' + this.user + punctuation;\n * }\n * };\n *\n * var bound = _.bindKey(object, 'greet', 'hi');\n * bound('!');\n * // => 'hi fred!'\n *\n * object.greet = function(greeting, punctuation) {\n * return greeting + 'ya ' + this.user + punctuation;\n * };\n *\n * bound('!');\n * // => 'hiya fred!'\n *\n * // Bound with placeholders.\n * var bound = _.bindKey(object, 'greet', _, '!');\n * bound('hi');\n * // => 'hiya fred!'\n */\n var bindKey = baseRest(function(object, key, partials) {\n var bitmask = WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG;\n if (partials.length) {\n var holders = replaceHolders(partials, getHolder(bindKey));\n bitmask |= WRAP_PARTIAL_FLAG;\n }\n return createWrap(key, bitmask, object, partials, holders);\n });\n\n /**\n * Creates a function that accepts arguments of `func` and either invokes\n * `func` returning its result, if at least `arity` number of arguments have\n * been provided, or returns a function that accepts the remaining `func`\n * arguments, and so on. The arity of `func` may be specified if `func.length`\n * is not sufficient.\n *\n * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds,\n * may be used as a placeholder for provided arguments.\n *\n * **Note:** This method doesn't set the \"length\" property of curried functions.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Function\n * @param {Function} func The function to curry.\n * @param {number} [arity=func.length] The arity of `func`.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Function} Returns the new curried function.\n * @example\n *\n * var abc = function(a, b, c) {\n * return [a, b, c];\n * };\n *\n * var curried = _.curry(abc);\n *\n * curried(1)(2)(3);\n * // => [1, 2, 3]\n *\n * curried(1, 2)(3);\n * // => [1, 2, 3]\n *\n * curried(1, 2, 3);\n * // => [1, 2, 3]\n *\n * // Curried with placeholders.\n * curried(1)(_, 3)(2);\n * // => [1, 2, 3]\n */\n function curry(func, arity, guard) {\n arity = guard ? undefined : arity;\n var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity);\n result.placeholder = curry.placeholder;\n return result;\n }\n\n /**\n * This method is like `_.curry` except that arguments are applied to `func`\n * in the manner of `_.partialRight` instead of `_.partial`.\n *\n * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic\n * builds, may be used as a placeholder for provided arguments.\n *\n * **Note:** This method doesn't set the \"length\" property of curried functions.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Function\n * @param {Function} func The function to curry.\n * @param {number} [arity=func.length] The arity of `func`.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Function} Returns the new curried function.\n * @example\n *\n * var abc = function(a, b, c) {\n * return [a, b, c];\n * };\n *\n * var curried = _.curryRight(abc);\n *\n * curried(3)(2)(1);\n * // => [1, 2, 3]\n *\n * curried(2, 3)(1);\n * // => [1, 2, 3]\n *\n * curried(1, 2, 3);\n * // => [1, 2, 3]\n *\n * // Curried with placeholders.\n * curried(3)(1, _)(2);\n * // => [1, 2, 3]\n */\n function curryRight(func, arity, guard) {\n arity = guard ? undefined : arity;\n var result = createWrap(func, WRAP_CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity);\n result.placeholder = curryRight.placeholder;\n return result;\n }\n\n /**\n * Creates a debounced function that delays invoking `func` until after `wait`\n * milliseconds have elapsed since the last time the debounced function was\n * invoked. The debounced function comes with a `cancel` method to cancel\n * delayed `func` invocations and a `flush` method to immediately invoke them.\n * Provide `options` to indicate whether `func` should be invoked on the\n * leading and/or trailing edge of the `wait` timeout. The `func` is invoked\n * with the last arguments provided to the debounced function. Subsequent\n * calls to the debounced function return the result of the last `func`\n * invocation.\n *\n * **Note:** If `leading` and `trailing` options are `true`, `func` is\n * invoked on the trailing edge of the timeout only if the debounced function\n * is invoked more than once during the `wait` timeout.\n *\n * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred\n * until to the next tick, similar to `setTimeout` with a timeout of `0`.\n *\n * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)\n * for details over the differences between `_.debounce` and `_.throttle`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {Function} func The function to debounce.\n * @param {number} [wait=0] The number of milliseconds to delay.\n * @param {Object} [options={}] The options object.\n * @param {boolean} [options.leading=false]\n * Specify invoking on the leading edge of the timeout.\n * @param {number} [options.maxWait]\n * The maximum time `func` is allowed to be delayed before it's invoked.\n * @param {boolean} [options.trailing=true]\n * Specify invoking on the trailing edge of the timeout.\n * @returns {Function} Returns the new debounced function.\n * @example\n *\n * // Avoid costly calculations while the window size is in flux.\n * jQuery(window).on('resize', _.debounce(calculateLayout, 150));\n *\n * // Invoke `sendMail` when clicked, debouncing subsequent calls.\n * jQuery(element).on('click', _.debounce(sendMail, 300, {\n * 'leading': true,\n * 'trailing': false\n * }));\n *\n * // Ensure `batchLog` is invoked once after 1 second of debounced calls.\n * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });\n * var source = new EventSource('/stream');\n * jQuery(source).on('message', debounced);\n *\n * // Cancel the trailing debounced invocation.\n * jQuery(window).on('popstate', debounced.cancel);\n */\n function debounce(func, wait, options) {\n var lastArgs,\n lastThis,\n maxWait,\n result,\n timerId,\n lastCallTime,\n lastInvokeTime = 0,\n leading = false,\n maxing = false,\n trailing = true;\n\n if (typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n wait = toNumber(wait) || 0;\n if (isObject(options)) {\n leading = !!options.leading;\n maxing = 'maxWait' in options;\n maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;\n trailing = 'trailing' in options ? !!options.trailing : trailing;\n }\n\n function invokeFunc(time) {\n var args = lastArgs,\n thisArg = lastThis;\n\n lastArgs = lastThis = undefined;\n lastInvokeTime = time;\n result = func.apply(thisArg, args);\n return result;\n }\n\n function leadingEdge(time) {\n // Reset any `maxWait` timer.\n lastInvokeTime = time;\n // Start the timer for the trailing edge.\n timerId = setTimeout(timerExpired, wait);\n // Invoke the leading edge.\n return leading ? invokeFunc(time) : result;\n }\n\n function remainingWait(time) {\n var timeSinceLastCall = time - lastCallTime,\n timeSinceLastInvoke = time - lastInvokeTime,\n timeWaiting = wait - timeSinceLastCall;\n\n return maxing\n ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)\n : timeWaiting;\n }\n\n function shouldInvoke(time) {\n var timeSinceLastCall = time - lastCallTime,\n timeSinceLastInvoke = time - lastInvokeTime;\n\n // Either this is the first call, activity has stopped and we're at the\n // trailing edge, the system time has gone backwards and we're treating\n // it as the trailing edge, or we've hit the `maxWait` limit.\n return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||\n (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));\n }\n\n function timerExpired() {\n var time = now();\n if (shouldInvoke(time)) {\n return trailingEdge(time);\n }\n // Restart the timer.\n timerId = setTimeout(timerExpired, remainingWait(time));\n }\n\n function trailingEdge(time) {\n timerId = undefined;\n\n // Only invoke if we have `lastArgs` which means `func` has been\n // debounced at least once.\n if (trailing && lastArgs) {\n return invokeFunc(time);\n }\n lastArgs = lastThis = undefined;\n return result;\n }\n\n function cancel() {\n if (timerId !== undefined) {\n clearTimeout(timerId);\n }\n lastInvokeTime = 0;\n lastArgs = lastCallTime = lastThis = timerId = undefined;\n }\n\n function flush() {\n return timerId === undefined ? result : trailingEdge(now());\n }\n\n function debounced() {\n var time = now(),\n isInvoking = shouldInvoke(time);\n\n lastArgs = arguments;\n lastThis = this;\n lastCallTime = time;\n\n if (isInvoking) {\n if (timerId === undefined) {\n return leadingEdge(lastCallTime);\n }\n if (maxing) {\n // Handle invocations in a tight loop.\n timerId = setTimeout(timerExpired, wait);\n return invokeFunc(lastCallTime);\n }\n }\n if (timerId === undefined) {\n timerId = setTimeout(timerExpired, wait);\n }\n return result;\n }\n debounced.cancel = cancel;\n debounced.flush = flush;\n return debounced;\n }\n\n /**\n * Defers invoking the `func` until the current call stack has cleared. Any\n * additional arguments are provided to `func` when it's invoked.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {Function} func The function to defer.\n * @param {...*} [args] The arguments to invoke `func` with.\n * @returns {number} Returns the timer id.\n * @example\n *\n * _.defer(function(text) {\n * console.log(text);\n * }, 'deferred');\n * // => Logs 'deferred' after one millisecond.\n */\n var defer = baseRest(function(func, args) {\n return baseDelay(func, 1, args);\n });\n\n /**\n * Invokes `func` after `wait` milliseconds. Any additional arguments are\n * provided to `func` when it's invoked.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {Function} func The function to delay.\n * @param {number} wait The number of milliseconds to delay invocation.\n * @param {...*} [args] The arguments to invoke `func` with.\n * @returns {number} Returns the timer id.\n * @example\n *\n * _.delay(function(text) {\n * console.log(text);\n * }, 1000, 'later');\n * // => Logs 'later' after one second.\n */\n var delay = baseRest(function(func, wait, args) {\n return baseDelay(func, toNumber(wait) || 0, args);\n });\n\n /**\n * Creates a function that invokes `func` with arguments reversed.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Function\n * @param {Function} func The function to flip arguments for.\n * @returns {Function} Returns the new flipped function.\n * @example\n *\n * var flipped = _.flip(function() {\n * return _.toArray(arguments);\n * });\n *\n * flipped('a', 'b', 'c', 'd');\n * // => ['d', 'c', 'b', 'a']\n */\n function flip(func) {\n return createWrap(func, WRAP_FLIP_FLAG);\n }\n\n /**\n * Creates a function that memoizes the result of `func`. If `resolver` is\n * provided, it determines the cache key for storing the result based on the\n * arguments provided to the memoized function. By default, the first argument\n * provided to the memoized function is used as the map cache key. The `func`\n * is invoked with the `this` binding of the memoized function.\n *\n * **Note:** The cache is exposed as the `cache` property on the memoized\n * function. Its creation may be customized by replacing the `_.memoize.Cache`\n * constructor with one whose instances implement the\n * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)\n * method interface of `clear`, `delete`, `get`, `has`, and `set`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {Function} func The function to have its output memoized.\n * @param {Function} [resolver] The function to resolve the cache key.\n * @returns {Function} Returns the new memoized function.\n * @example\n *\n * var object = { 'a': 1, 'b': 2 };\n * var other = { 'c': 3, 'd': 4 };\n *\n * var values = _.memoize(_.values);\n * values(object);\n * // => [1, 2]\n *\n * values(other);\n * // => [3, 4]\n *\n * object.a = 2;\n * values(object);\n * // => [1, 2]\n *\n * // Modify the result cache.\n * values.cache.set(object, ['a', 'b']);\n * values(object);\n * // => ['a', 'b']\n *\n * // Replace `_.memoize.Cache`.\n * _.memoize.Cache = WeakMap;\n */\n function memoize(func, resolver) {\n if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n var memoized = function() {\n var args = arguments,\n key = resolver ? resolver.apply(this, args) : args[0],\n cache = memoized.cache;\n\n if (cache.has(key)) {\n return cache.get(key);\n }\n var result = func.apply(this, args);\n memoized.cache = cache.set(key, result) || cache;\n return result;\n };\n memoized.cache = new (memoize.Cache || MapCache);\n return memoized;\n }\n\n // Expose `MapCache`.\n memoize.Cache = MapCache;\n\n /**\n * Creates a function that negates the result of the predicate `func`. The\n * `func` predicate is invoked with the `this` binding and arguments of the\n * created function.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Function\n * @param {Function} predicate The predicate to negate.\n * @returns {Function} Returns the new negated function.\n * @example\n *\n * function isEven(n) {\n * return n % 2 == 0;\n * }\n *\n * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven));\n * // => [1, 3, 5]\n */\n function negate(predicate) {\n if (typeof predicate != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n return function() {\n var args = arguments;\n switch (args.length) {\n case 0: return !predicate.call(this);\n case 1: return !predicate.call(this, args[0]);\n case 2: return !predicate.call(this, args[0], args[1]);\n case 3: return !predicate.call(this, args[0], args[1], args[2]);\n }\n return !predicate.apply(this, args);\n };\n }\n\n /**\n * Creates a function that is restricted to invoking `func` once. Repeat calls\n * to the function return the value of the first invocation. The `func` is\n * invoked with the `this` binding and arguments of the created function.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {Function} func The function to restrict.\n * @returns {Function} Returns the new restricted function.\n * @example\n *\n * var initialize = _.once(createApplication);\n * initialize();\n * initialize();\n * // => `createApplication` is invoked once\n */\n function once(func) {\n return before(2, func);\n }\n\n /**\n * Creates a function that invokes `func` with its arguments transformed.\n *\n * @static\n * @since 4.0.0\n * @memberOf _\n * @category Function\n * @param {Function} func The function to wrap.\n * @param {...(Function|Function[])} [transforms=[_.identity]]\n * The argument transforms.\n * @returns {Function} Returns the new function.\n * @example\n *\n * function doubled(n) {\n * return n * 2;\n * }\n *\n * function square(n) {\n * return n * n;\n * }\n *\n * var func = _.overArgs(function(x, y) {\n * return [x, y];\n * }, [square, doubled]);\n *\n * func(9, 3);\n * // => [81, 6]\n *\n * func(10, 5);\n * // => [100, 10]\n */\n var overArgs = castRest(function(func, transforms) {\n transforms = (transforms.length == 1 && isArray(transforms[0]))\n ? arrayMap(transforms[0], baseUnary(getIteratee()))\n : arrayMap(baseFlatten(transforms, 1), baseUnary(getIteratee()));\n\n var funcsLength = transforms.length;\n return baseRest(function(args) {\n var index = -1,\n length = nativeMin(args.length, funcsLength);\n\n while (++index < length) {\n args[index] = transforms[index].call(this, args[index]);\n }\n return apply(func, this, args);\n });\n });\n\n /**\n * Creates a function that invokes `func` with `partials` prepended to the\n * arguments it receives. This method is like `_.bind` except it does **not**\n * alter the `this` binding.\n *\n * The `_.partial.placeholder` value, which defaults to `_` in monolithic\n * builds, may be used as a placeholder for partially applied arguments.\n *\n * **Note:** This method doesn't set the \"length\" property of partially\n * applied functions.\n *\n * @static\n * @memberOf _\n * @since 0.2.0\n * @category Function\n * @param {Function} func The function to partially apply arguments to.\n * @param {...*} [partials] The arguments to be partially applied.\n * @returns {Function} Returns the new partially applied function.\n * @example\n *\n * function greet(greeting, name) {\n * return greeting + ' ' + name;\n * }\n *\n * var sayHelloTo = _.partial(greet, 'hello');\n * sayHelloTo('fred');\n * // => 'hello fred'\n *\n * // Partially applied with placeholders.\n * var greetFred = _.partial(greet, _, 'fred');\n * greetFred('hi');\n * // => 'hi fred'\n */\n var partial = baseRest(function(func, partials) {\n var holders = replaceHolders(partials, getHolder(partial));\n return createWrap(func, WRAP_PARTIAL_FLAG, undefined, partials, holders);\n });\n\n /**\n * This method is like `_.partial` except that partially applied arguments\n * are appended to the arguments it receives.\n *\n * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic\n * builds, may be used as a placeholder for partially applied arguments.\n *\n * **Note:** This method doesn't set the \"length\" property of partially\n * applied functions.\n *\n * @static\n * @memberOf _\n * @since 1.0.0\n * @category Function\n * @param {Function} func The function to partially apply arguments to.\n * @param {...*} [partials] The arguments to be partially applied.\n * @returns {Function} Returns the new partially applied function.\n * @example\n *\n * function greet(greeting, name) {\n * return greeting + ' ' + name;\n * }\n *\n * var greetFred = _.partialRight(greet, 'fred');\n * greetFred('hi');\n * // => 'hi fred'\n *\n * // Partially applied with placeholders.\n * var sayHelloTo = _.partialRight(greet, 'hello', _);\n * sayHelloTo('fred');\n * // => 'hello fred'\n */\n var partialRight = baseRest(function(func, partials) {\n var holders = replaceHolders(partials, getHolder(partialRight));\n return createWrap(func, WRAP_PARTIAL_RIGHT_FLAG, undefined, partials, holders);\n });\n\n /**\n * Creates a function that invokes `func` with arguments arranged according\n * to the specified `indexes` where the argument value at the first index is\n * provided as the first argument, the argument value at the second index is\n * provided as the second argument, and so on.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Function\n * @param {Function} func The function to rearrange arguments for.\n * @param {...(number|number[])} indexes The arranged argument indexes.\n * @returns {Function} Returns the new function.\n * @example\n *\n * var rearged = _.rearg(function(a, b, c) {\n * return [a, b, c];\n * }, [2, 0, 1]);\n *\n * rearged('b', 'c', 'a')\n * // => ['a', 'b', 'c']\n */\n var rearg = flatRest(function(func, indexes) {\n return createWrap(func, WRAP_REARG_FLAG, undefined, undefined, undefined, indexes);\n });\n\n /**\n * Creates a function that invokes `func` with the `this` binding of the\n * created function and arguments from `start` and beyond provided as\n * an array.\n *\n * **Note:** This method is based on the\n * [rest parameter](https://mdn.io/rest_parameters).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Function\n * @param {Function} func The function to apply a rest parameter to.\n * @param {number} [start=func.length-1] The start position of the rest parameter.\n * @returns {Function} Returns the new function.\n * @example\n *\n * var say = _.rest(function(what, names) {\n * return what + ' ' + _.initial(names).join(', ') +\n * (_.size(names) > 1 ? ', & ' : '') + _.last(names);\n * });\n *\n * say('hello', 'fred', 'barney', 'pebbles');\n * // => 'hello fred, barney, & pebbles'\n */\n function rest(func, start) {\n if (typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n start = start === undefined ? start : toInteger(start);\n return baseRest(func, start);\n }\n\n /**\n * Creates a function that invokes `func` with the `this` binding of the\n * create function and an array of arguments much like\n * [`Function#apply`](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply).\n *\n * **Note:** This method is based on the\n * [spread operator](https://mdn.io/spread_operator).\n *\n * @static\n * @memberOf _\n * @since 3.2.0\n * @category Function\n * @param {Function} func The function to spread arguments over.\n * @param {number} [start=0] The start position of the spread.\n * @returns {Function} Returns the new function.\n * @example\n *\n * var say = _.spread(function(who, what) {\n * return who + ' says ' + what;\n * });\n *\n * say(['fred', 'hello']);\n * // => 'fred says hello'\n *\n * var numbers = Promise.all([\n * Promise.resolve(40),\n * Promise.resolve(36)\n * ]);\n *\n * numbers.then(_.spread(function(x, y) {\n * return x + y;\n * }));\n * // => a Promise of 76\n */\n function spread(func, start) {\n if (typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n start = start == null ? 0 : nativeMax(toInteger(start), 0);\n return baseRest(function(args) {\n var array = args[start],\n otherArgs = castSlice(args, 0, start);\n\n if (array) {\n arrayPush(otherArgs, array);\n }\n return apply(func, this, otherArgs);\n });\n }\n\n /**\n * Creates a throttled function that only invokes `func` at most once per\n * every `wait` milliseconds. The throttled function comes with a `cancel`\n * method to cancel delayed `func` invocations and a `flush` method to\n * immediately invoke them. Provide `options` to indicate whether `func`\n * should be invoked on the leading and/or trailing edge of the `wait`\n * timeout. The `func` is invoked with the last arguments provided to the\n * throttled function. Subsequent calls to the throttled function return the\n * result of the last `func` invocation.\n *\n * **Note:** If `leading` and `trailing` options are `true`, `func` is\n * invoked on the trailing edge of the timeout only if the throttled function\n * is invoked more than once during the `wait` timeout.\n *\n * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred\n * until to the next tick, similar to `setTimeout` with a timeout of `0`.\n *\n * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)\n * for details over the differences between `_.throttle` and `_.debounce`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {Function} func The function to throttle.\n * @param {number} [wait=0] The number of milliseconds to throttle invocations to.\n * @param {Object} [options={}] The options object.\n * @param {boolean} [options.leading=true]\n * Specify invoking on the leading edge of the timeout.\n * @param {boolean} [options.trailing=true]\n * Specify invoking on the trailing edge of the timeout.\n * @returns {Function} Returns the new throttled function.\n * @example\n *\n * // Avoid excessively updating the position while scrolling.\n * jQuery(window).on('scroll', _.throttle(updatePosition, 100));\n *\n * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.\n * var throttled = _.throttle(renewToken, 300000, { 'trailing': false });\n * jQuery(element).on('click', throttled);\n *\n * // Cancel the trailing throttled invocation.\n * jQuery(window).on('popstate', throttled.cancel);\n */\n function throttle(func, wait, options) {\n var leading = true,\n trailing = true;\n\n if (typeof func != 'function') {\n throw new TypeError(FUNC_ERROR_TEXT);\n }\n if (isObject(options)) {\n leading = 'leading' in options ? !!options.leading : leading;\n trailing = 'trailing' in options ? !!options.trailing : trailing;\n }\n return debounce(func, wait, {\n 'leading': leading,\n 'maxWait': wait,\n 'trailing': trailing\n });\n }\n\n /**\n * Creates a function that accepts up to one argument, ignoring any\n * additional arguments.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Function\n * @param {Function} func The function to cap arguments for.\n * @returns {Function} Returns the new capped function.\n * @example\n *\n * _.map(['6', '8', '10'], _.unary(parseInt));\n * // => [6, 8, 10]\n */\n function unary(func) {\n return ary(func, 1);\n }\n\n /**\n * Creates a function that provides `value` to `wrapper` as its first\n * argument. Any additional arguments provided to the function are appended\n * to those provided to the `wrapper`. The wrapper is invoked with the `this`\n * binding of the created function.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Function\n * @param {*} value The value to wrap.\n * @param {Function} [wrapper=identity] The wrapper function.\n * @returns {Function} Returns the new function.\n * @example\n *\n * var p = _.wrap(_.escape, function(func, text) {\n * return '

' + func(text) + '

';\n * });\n *\n * p('fred, barney, & pebbles');\n * // => '

fred, barney, & pebbles

'\n */\n function wrap(value, wrapper) {\n return partial(castFunction(wrapper), value);\n }\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Casts `value` as an array if it's not one.\n *\n * @static\n * @memberOf _\n * @since 4.4.0\n * @category Lang\n * @param {*} value The value to inspect.\n * @returns {Array} Returns the cast array.\n * @example\n *\n * _.castArray(1);\n * // => [1]\n *\n * _.castArray({ 'a': 1 });\n * // => [{ 'a': 1 }]\n *\n * _.castArray('abc');\n * // => ['abc']\n *\n * _.castArray(null);\n * // => [null]\n *\n * _.castArray(undefined);\n * // => [undefined]\n *\n * _.castArray();\n * // => []\n *\n * var array = [1, 2, 3];\n * console.log(_.castArray(array) === array);\n * // => true\n */\n function castArray() {\n if (!arguments.length) {\n return [];\n }\n var value = arguments[0];\n return isArray(value) ? value : [value];\n }\n\n /**\n * Creates a shallow clone of `value`.\n *\n * **Note:** This method is loosely based on the\n * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm)\n * and supports cloning arrays, array buffers, booleans, date objects, maps,\n * numbers, `Object` objects, regexes, sets, strings, symbols, and typed\n * arrays. The own enumerable properties of `arguments` objects are cloned\n * as plain objects. An empty object is returned for uncloneable values such\n * as error objects, functions, DOM nodes, and WeakMaps.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to clone.\n * @returns {*} Returns the cloned value.\n * @see _.cloneDeep\n * @example\n *\n * var objects = [{ 'a': 1 }, { 'b': 2 }];\n *\n * var shallow = _.clone(objects);\n * console.log(shallow[0] === objects[0]);\n * // => true\n */\n function clone(value) {\n return baseClone(value, CLONE_SYMBOLS_FLAG);\n }\n\n /**\n * This method is like `_.clone` except that it accepts `customizer` which\n * is invoked to produce the cloned value. If `customizer` returns `undefined`,\n * cloning is handled by the method instead. The `customizer` is invoked with\n * up to four arguments; (value [, index|key, object, stack]).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to clone.\n * @param {Function} [customizer] The function to customize cloning.\n * @returns {*} Returns the cloned value.\n * @see _.cloneDeepWith\n * @example\n *\n * function customizer(value) {\n * if (_.isElement(value)) {\n * return value.cloneNode(false);\n * }\n * }\n *\n * var el = _.cloneWith(document.body, customizer);\n *\n * console.log(el === document.body);\n * // => false\n * console.log(el.nodeName);\n * // => 'BODY'\n * console.log(el.childNodes.length);\n * // => 0\n */\n function cloneWith(value, customizer) {\n customizer = typeof customizer == 'function' ? customizer : undefined;\n return baseClone(value, CLONE_SYMBOLS_FLAG, customizer);\n }\n\n /**\n * This method is like `_.clone` except that it recursively clones `value`.\n *\n * @static\n * @memberOf _\n * @since 1.0.0\n * @category Lang\n * @param {*} value The value to recursively clone.\n * @returns {*} Returns the deep cloned value.\n * @see _.clone\n * @example\n *\n * var objects = [{ 'a': 1 }, { 'b': 2 }];\n *\n * var deep = _.cloneDeep(objects);\n * console.log(deep[0] === objects[0]);\n * // => false\n */\n function cloneDeep(value) {\n return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);\n }\n\n /**\n * This method is like `_.cloneWith` except that it recursively clones `value`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to recursively clone.\n * @param {Function} [customizer] The function to customize cloning.\n * @returns {*} Returns the deep cloned value.\n * @see _.cloneWith\n * @example\n *\n * function customizer(value) {\n * if (_.isElement(value)) {\n * return value.cloneNode(true);\n * }\n * }\n *\n * var el = _.cloneDeepWith(document.body, customizer);\n *\n * console.log(el === document.body);\n * // => false\n * console.log(el.nodeName);\n * // => 'BODY'\n * console.log(el.childNodes.length);\n * // => 20\n */\n function cloneDeepWith(value, customizer) {\n customizer = typeof customizer == 'function' ? customizer : undefined;\n return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer);\n }\n\n /**\n * Checks if `object` conforms to `source` by invoking the predicate\n * properties of `source` with the corresponding property values of `object`.\n *\n * **Note:** This method is equivalent to `_.conforms` when `source` is\n * partially applied.\n *\n * @static\n * @memberOf _\n * @since 4.14.0\n * @category Lang\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property predicates to conform to.\n * @returns {boolean} Returns `true` if `object` conforms, else `false`.\n * @example\n *\n * var object = { 'a': 1, 'b': 2 };\n *\n * _.conformsTo(object, { 'b': function(n) { return n > 1; } });\n * // => true\n *\n * _.conformsTo(object, { 'b': function(n) { return n > 2; } });\n * // => false\n */\n function conformsTo(object, source) {\n return source == null || baseConformsTo(object, source, keys(source));\n }\n\n /**\n * Performs a\n * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * comparison between two values to determine if they are equivalent.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * var object = { 'a': 1 };\n * var other = { 'a': 1 };\n *\n * _.eq(object, object);\n * // => true\n *\n * _.eq(object, other);\n * // => false\n *\n * _.eq('a', 'a');\n * // => true\n *\n * _.eq('a', Object('a'));\n * // => false\n *\n * _.eq(NaN, NaN);\n * // => true\n */\n function eq(value, other) {\n return value === other || (value !== value && other !== other);\n }\n\n /**\n * Checks if `value` is greater than `other`.\n *\n * @static\n * @memberOf _\n * @since 3.9.0\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if `value` is greater than `other`,\n * else `false`.\n * @see _.lt\n * @example\n *\n * _.gt(3, 1);\n * // => true\n *\n * _.gt(3, 3);\n * // => false\n *\n * _.gt(1, 3);\n * // => false\n */\n var gt = createRelationalOperation(baseGt);\n\n /**\n * Checks if `value` is greater than or equal to `other`.\n *\n * @static\n * @memberOf _\n * @since 3.9.0\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if `value` is greater than or equal to\n * `other`, else `false`.\n * @see _.lte\n * @example\n *\n * _.gte(3, 1);\n * // => true\n *\n * _.gte(3, 3);\n * // => true\n *\n * _.gte(1, 3);\n * // => false\n */\n var gte = createRelationalOperation(function(value, other) {\n return value >= other;\n });\n\n /**\n * Checks if `value` is likely an `arguments` object.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an `arguments` object,\n * else `false`.\n * @example\n *\n * _.isArguments(function() { return arguments; }());\n * // => true\n *\n * _.isArguments([1, 2, 3]);\n * // => false\n */\n var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {\n return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&\n !propertyIsEnumerable.call(value, 'callee');\n };\n\n /**\n * Checks if `value` is classified as an `Array` object.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an array, else `false`.\n * @example\n *\n * _.isArray([1, 2, 3]);\n * // => true\n *\n * _.isArray(document.body.children);\n * // => false\n *\n * _.isArray('abc');\n * // => false\n *\n * _.isArray(_.noop);\n * // => false\n */\n var isArray = Array.isArray;\n\n /**\n * Checks if `value` is classified as an `ArrayBuffer` object.\n *\n * @static\n * @memberOf _\n * @since 4.3.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`.\n * @example\n *\n * _.isArrayBuffer(new ArrayBuffer(2));\n * // => true\n *\n * _.isArrayBuffer(new Array(2));\n * // => false\n */\n var isArrayBuffer = nodeIsArrayBuffer ? baseUnary(nodeIsArrayBuffer) : baseIsArrayBuffer;\n\n /**\n * Checks if `value` is array-like. A value is considered array-like if it's\n * not a function and has a `value.length` that's an integer greater than or\n * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is array-like, else `false`.\n * @example\n *\n * _.isArrayLike([1, 2, 3]);\n * // => true\n *\n * _.isArrayLike(document.body.children);\n * // => true\n *\n * _.isArrayLike('abc');\n * // => true\n *\n * _.isArrayLike(_.noop);\n * // => false\n */\n function isArrayLike(value) {\n return value != null && isLength(value.length) && !isFunction(value);\n }\n\n /**\n * This method is like `_.isArrayLike` except that it also checks if `value`\n * is an object.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an array-like object,\n * else `false`.\n * @example\n *\n * _.isArrayLikeObject([1, 2, 3]);\n * // => true\n *\n * _.isArrayLikeObject(document.body.children);\n * // => true\n *\n * _.isArrayLikeObject('abc');\n * // => false\n *\n * _.isArrayLikeObject(_.noop);\n * // => false\n */\n function isArrayLikeObject(value) {\n return isObjectLike(value) && isArrayLike(value);\n }\n\n /**\n * Checks if `value` is classified as a boolean primitive or object.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a boolean, else `false`.\n * @example\n *\n * _.isBoolean(false);\n * // => true\n *\n * _.isBoolean(null);\n * // => false\n */\n function isBoolean(value) {\n return value === true || value === false ||\n (isObjectLike(value) && baseGetTag(value) == boolTag);\n }\n\n /**\n * Checks if `value` is a buffer.\n *\n * @static\n * @memberOf _\n * @since 4.3.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a buffer, else `false`.\n * @example\n *\n * _.isBuffer(new Buffer(2));\n * // => true\n *\n * _.isBuffer(new Uint8Array(2));\n * // => false\n */\n var isBuffer = nativeIsBuffer || stubFalse;\n\n /**\n * Checks if `value` is classified as a `Date` object.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a date object, else `false`.\n * @example\n *\n * _.isDate(new Date);\n * // => true\n *\n * _.isDate('Mon April 23 2012');\n * // => false\n */\n var isDate = nodeIsDate ? baseUnary(nodeIsDate) : baseIsDate;\n\n /**\n * Checks if `value` is likely a DOM element.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`.\n * @example\n *\n * _.isElement(document.body);\n * // => true\n *\n * _.isElement('');\n * // => false\n */\n function isElement(value) {\n return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value);\n }\n\n /**\n * Checks if `value` is an empty object, collection, map, or set.\n *\n * Objects are considered empty if they have no own enumerable string keyed\n * properties.\n *\n * Array-like values such as `arguments` objects, arrays, buffers, strings, or\n * jQuery-like collections are considered empty if they have a `length` of `0`.\n * Similarly, maps and sets are considered empty if they have a `size` of `0`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is empty, else `false`.\n * @example\n *\n * _.isEmpty(null);\n * // => true\n *\n * _.isEmpty(true);\n * // => true\n *\n * _.isEmpty(1);\n * // => true\n *\n * _.isEmpty([1, 2, 3]);\n * // => false\n *\n * _.isEmpty({ 'a': 1 });\n * // => false\n */\n function isEmpty(value) {\n if (value == null) {\n return true;\n }\n if (isArrayLike(value) &&\n (isArray(value) || typeof value == 'string' || typeof value.splice == 'function' ||\n isBuffer(value) || isTypedArray(value) || isArguments(value))) {\n return !value.length;\n }\n var tag = getTag(value);\n if (tag == mapTag || tag == setTag) {\n return !value.size;\n }\n if (isPrototype(value)) {\n return !baseKeys(value).length;\n }\n for (var key in value) {\n if (hasOwnProperty.call(value, key)) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Performs a deep comparison between two values to determine if they are\n * equivalent.\n *\n * **Note:** This method supports comparing arrays, array buffers, booleans,\n * date objects, error objects, maps, numbers, `Object` objects, regexes,\n * sets, strings, symbols, and typed arrays. `Object` objects are compared\n * by their own, not inherited, enumerable properties. Functions and DOM\n * nodes are compared by strict equality, i.e. `===`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * var object = { 'a': 1 };\n * var other = { 'a': 1 };\n *\n * _.isEqual(object, other);\n * // => true\n *\n * object === other;\n * // => false\n */\n function isEqual(value, other) {\n return baseIsEqual(value, other);\n }\n\n /**\n * This method is like `_.isEqual` except that it accepts `customizer` which\n * is invoked to compare values. If `customizer` returns `undefined`, comparisons\n * are handled by the method instead. The `customizer` is invoked with up to\n * six arguments: (objValue, othValue [, index|key, object, other, stack]).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @param {Function} [customizer] The function to customize comparisons.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * function isGreeting(value) {\n * return /^h(?:i|ello)$/.test(value);\n * }\n *\n * function customizer(objValue, othValue) {\n * if (isGreeting(objValue) && isGreeting(othValue)) {\n * return true;\n * }\n * }\n *\n * var array = ['hello', 'goodbye'];\n * var other = ['hi', 'goodbye'];\n *\n * _.isEqualWith(array, other, customizer);\n * // => true\n */\n function isEqualWith(value, other, customizer) {\n customizer = typeof customizer == 'function' ? customizer : undefined;\n var result = customizer ? customizer(value, other) : undefined;\n return result === undefined ? baseIsEqual(value, other, undefined, customizer) : !!result;\n }\n\n /**\n * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`,\n * `SyntaxError`, `TypeError`, or `URIError` object.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an error object, else `false`.\n * @example\n *\n * _.isError(new Error);\n * // => true\n *\n * _.isError(Error);\n * // => false\n */\n function isError(value) {\n if (!isObjectLike(value)) {\n return false;\n }\n var tag = baseGetTag(value);\n return tag == errorTag || tag == domExcTag ||\n (typeof value.message == 'string' && typeof value.name == 'string' && !isPlainObject(value));\n }\n\n /**\n * Checks if `value` is a finite primitive number.\n *\n * **Note:** This method is based on\n * [`Number.isFinite`](https://mdn.io/Number/isFinite).\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a finite number, else `false`.\n * @example\n *\n * _.isFinite(3);\n * // => true\n *\n * _.isFinite(Number.MIN_VALUE);\n * // => true\n *\n * _.isFinite(Infinity);\n * // => false\n *\n * _.isFinite('3');\n * // => false\n */\n function isFinite(value) {\n return typeof value == 'number' && nativeIsFinite(value);\n }\n\n /**\n * Checks if `value` is classified as a `Function` object.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a function, else `false`.\n * @example\n *\n * _.isFunction(_);\n * // => true\n *\n * _.isFunction(/abc/);\n * // => false\n */\n function isFunction(value) {\n if (!isObject(value)) {\n return false;\n }\n // The use of `Object#toString` avoids issues with the `typeof` operator\n // in Safari 9 which returns 'object' for typed arrays and other constructors.\n var tag = baseGetTag(value);\n return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;\n }\n\n /**\n * Checks if `value` is an integer.\n *\n * **Note:** This method is based on\n * [`Number.isInteger`](https://mdn.io/Number/isInteger).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an integer, else `false`.\n * @example\n *\n * _.isInteger(3);\n * // => true\n *\n * _.isInteger(Number.MIN_VALUE);\n * // => false\n *\n * _.isInteger(Infinity);\n * // => false\n *\n * _.isInteger('3');\n * // => false\n */\n function isInteger(value) {\n return typeof value == 'number' && value == toInteger(value);\n }\n\n /**\n * Checks if `value` is a valid array-like length.\n *\n * **Note:** This method is loosely based on\n * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.\n * @example\n *\n * _.isLength(3);\n * // => true\n *\n * _.isLength(Number.MIN_VALUE);\n * // => false\n *\n * _.isLength(Infinity);\n * // => false\n *\n * _.isLength('3');\n * // => false\n */\n function isLength(value) {\n return typeof value == 'number' &&\n value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;\n }\n\n /**\n * Checks if `value` is the\n * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)\n * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an object, else `false`.\n * @example\n *\n * _.isObject({});\n * // => true\n *\n * _.isObject([1, 2, 3]);\n * // => true\n *\n * _.isObject(_.noop);\n * // => true\n *\n * _.isObject(null);\n * // => false\n */\n function isObject(value) {\n var type = typeof value;\n return value != null && (type == 'object' || type == 'function');\n }\n\n /**\n * Checks if `value` is object-like. A value is object-like if it's not `null`\n * and has a `typeof` result of \"object\".\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is object-like, else `false`.\n * @example\n *\n * _.isObjectLike({});\n * // => true\n *\n * _.isObjectLike([1, 2, 3]);\n * // => true\n *\n * _.isObjectLike(_.noop);\n * // => false\n *\n * _.isObjectLike(null);\n * // => false\n */\n function isObjectLike(value) {\n return value != null && typeof value == 'object';\n }\n\n /**\n * Checks if `value` is classified as a `Map` object.\n *\n * @static\n * @memberOf _\n * @since 4.3.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a map, else `false`.\n * @example\n *\n * _.isMap(new Map);\n * // => true\n *\n * _.isMap(new WeakMap);\n * // => false\n */\n var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap;\n\n /**\n * Performs a partial deep comparison between `object` and `source` to\n * determine if `object` contains equivalent property values.\n *\n * **Note:** This method is equivalent to `_.matches` when `source` is\n * partially applied.\n *\n * Partial comparisons will match empty array and empty object `source`\n * values against any array or object value, respectively. See `_.isEqual`\n * for a list of supported value comparisons.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Lang\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property values to match.\n * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n * @example\n *\n * var object = { 'a': 1, 'b': 2 };\n *\n * _.isMatch(object, { 'b': 2 });\n * // => true\n *\n * _.isMatch(object, { 'b': 1 });\n * // => false\n */\n function isMatch(object, source) {\n return object === source || baseIsMatch(object, source, getMatchData(source));\n }\n\n /**\n * This method is like `_.isMatch` except that it accepts `customizer` which\n * is invoked to compare values. If `customizer` returns `undefined`, comparisons\n * are handled by the method instead. The `customizer` is invoked with five\n * arguments: (objValue, srcValue, index|key, object, source).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property values to match.\n * @param {Function} [customizer] The function to customize comparisons.\n * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n * @example\n *\n * function isGreeting(value) {\n * return /^h(?:i|ello)$/.test(value);\n * }\n *\n * function customizer(objValue, srcValue) {\n * if (isGreeting(objValue) && isGreeting(srcValue)) {\n * return true;\n * }\n * }\n *\n * var object = { 'greeting': 'hello' };\n * var source = { 'greeting': 'hi' };\n *\n * _.isMatchWith(object, source, customizer);\n * // => true\n */\n function isMatchWith(object, source, customizer) {\n customizer = typeof customizer == 'function' ? customizer : undefined;\n return baseIsMatch(object, source, getMatchData(source), customizer);\n }\n\n /**\n * Checks if `value` is `NaN`.\n *\n * **Note:** This method is based on\n * [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as\n * global [`isNaN`](https://mdn.io/isNaN) which returns `true` for\n * `undefined` and other non-number values.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.\n * @example\n *\n * _.isNaN(NaN);\n * // => true\n *\n * _.isNaN(new Number(NaN));\n * // => true\n *\n * isNaN(undefined);\n * // => true\n *\n * _.isNaN(undefined);\n * // => false\n */\n function isNaN(value) {\n // An `NaN` primitive is the only value that is not equal to itself.\n // Perform the `toStringTag` check first to avoid errors with some\n // ActiveX objects in IE.\n return isNumber(value) && value != +value;\n }\n\n /**\n * Checks if `value` is a pristine native function.\n *\n * **Note:** This method can't reliably detect native functions in the presence\n * of the core-js package because core-js circumvents this kind of detection.\n * Despite multiple requests, the core-js maintainer has made it clear: any\n * attempt to fix the detection will be obstructed. As a result, we're left\n * with little choice but to throw an error. Unfortunately, this also affects\n * packages, like [babel-polyfill](https://www.npmjs.com/package/babel-polyfill),\n * which rely on core-js.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a native function,\n * else `false`.\n * @example\n *\n * _.isNative(Array.prototype.push);\n * // => true\n *\n * _.isNative(_);\n * // => false\n */\n function isNative(value) {\n if (isMaskable(value)) {\n throw new Error(CORE_ERROR_TEXT);\n }\n return baseIsNative(value);\n }\n\n /**\n * Checks if `value` is `null`.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is `null`, else `false`.\n * @example\n *\n * _.isNull(null);\n * // => true\n *\n * _.isNull(void 0);\n * // => false\n */\n function isNull(value) {\n return value === null;\n }\n\n /**\n * Checks if `value` is `null` or `undefined`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is nullish, else `false`.\n * @example\n *\n * _.isNil(null);\n * // => true\n *\n * _.isNil(void 0);\n * // => true\n *\n * _.isNil(NaN);\n * // => false\n */\n function isNil(value) {\n return value == null;\n }\n\n /**\n * Checks if `value` is classified as a `Number` primitive or object.\n *\n * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are\n * classified as numbers, use the `_.isFinite` method.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a number, else `false`.\n * @example\n *\n * _.isNumber(3);\n * // => true\n *\n * _.isNumber(Number.MIN_VALUE);\n * // => true\n *\n * _.isNumber(Infinity);\n * // => true\n *\n * _.isNumber('3');\n * // => false\n */\n function isNumber(value) {\n return typeof value == 'number' ||\n (isObjectLike(value) && baseGetTag(value) == numberTag);\n }\n\n /**\n * Checks if `value` is a plain object, that is, an object created by the\n * `Object` constructor or one with a `[[Prototype]]` of `null`.\n *\n * @static\n * @memberOf _\n * @since 0.8.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * }\n *\n * _.isPlainObject(new Foo);\n * // => false\n *\n * _.isPlainObject([1, 2, 3]);\n * // => false\n *\n * _.isPlainObject({ 'x': 0, 'y': 0 });\n * // => true\n *\n * _.isPlainObject(Object.create(null));\n * // => true\n */\n function isPlainObject(value) {\n if (!isObjectLike(value) || baseGetTag(value) != objectTag) {\n return false;\n }\n var proto = getPrototype(value);\n if (proto === null) {\n return true;\n }\n var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;\n return typeof Ctor == 'function' && Ctor instanceof Ctor &&\n funcToString.call(Ctor) == objectCtorString;\n }\n\n /**\n * Checks if `value` is classified as a `RegExp` object.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a regexp, else `false`.\n * @example\n *\n * _.isRegExp(/abc/);\n * // => true\n *\n * _.isRegExp('/abc/');\n * // => false\n */\n var isRegExp = nodeIsRegExp ? baseUnary(nodeIsRegExp) : baseIsRegExp;\n\n /**\n * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754\n * double precision number which isn't the result of a rounded unsafe integer.\n *\n * **Note:** This method is based on\n * [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`.\n * @example\n *\n * _.isSafeInteger(3);\n * // => true\n *\n * _.isSafeInteger(Number.MIN_VALUE);\n * // => false\n *\n * _.isSafeInteger(Infinity);\n * // => false\n *\n * _.isSafeInteger('3');\n * // => false\n */\n function isSafeInteger(value) {\n return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER;\n }\n\n /**\n * Checks if `value` is classified as a `Set` object.\n *\n * @static\n * @memberOf _\n * @since 4.3.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a set, else `false`.\n * @example\n *\n * _.isSet(new Set);\n * // => true\n *\n * _.isSet(new WeakSet);\n * // => false\n */\n var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet;\n\n /**\n * Checks if `value` is classified as a `String` primitive or object.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a string, else `false`.\n * @example\n *\n * _.isString('abc');\n * // => true\n *\n * _.isString(1);\n * // => false\n */\n function isString(value) {\n return typeof value == 'string' ||\n (!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag);\n }\n\n /**\n * Checks if `value` is classified as a `Symbol` primitive or object.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.\n * @example\n *\n * _.isSymbol(Symbol.iterator);\n * // => true\n *\n * _.isSymbol('abc');\n * // => false\n */\n function isSymbol(value) {\n return typeof value == 'symbol' ||\n (isObjectLike(value) && baseGetTag(value) == symbolTag);\n }\n\n /**\n * Checks if `value` is classified as a typed array.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.\n * @example\n *\n * _.isTypedArray(new Uint8Array);\n * // => true\n *\n * _.isTypedArray([]);\n * // => false\n */\n var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;\n\n /**\n * Checks if `value` is `undefined`.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`.\n * @example\n *\n * _.isUndefined(void 0);\n * // => true\n *\n * _.isUndefined(null);\n * // => false\n */\n function isUndefined(value) {\n return value === undefined;\n }\n\n /**\n * Checks if `value` is classified as a `WeakMap` object.\n *\n * @static\n * @memberOf _\n * @since 4.3.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a weak map, else `false`.\n * @example\n *\n * _.isWeakMap(new WeakMap);\n * // => true\n *\n * _.isWeakMap(new Map);\n * // => false\n */\n function isWeakMap(value) {\n return isObjectLike(value) && getTag(value) == weakMapTag;\n }\n\n /**\n * Checks if `value` is classified as a `WeakSet` object.\n *\n * @static\n * @memberOf _\n * @since 4.3.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a weak set, else `false`.\n * @example\n *\n * _.isWeakSet(new WeakSet);\n * // => true\n *\n * _.isWeakSet(new Set);\n * // => false\n */\n function isWeakSet(value) {\n return isObjectLike(value) && baseGetTag(value) == weakSetTag;\n }\n\n /**\n * Checks if `value` is less than `other`.\n *\n * @static\n * @memberOf _\n * @since 3.9.0\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if `value` is less than `other`,\n * else `false`.\n * @see _.gt\n * @example\n *\n * _.lt(1, 3);\n * // => true\n *\n * _.lt(3, 3);\n * // => false\n *\n * _.lt(3, 1);\n * // => false\n */\n var lt = createRelationalOperation(baseLt);\n\n /**\n * Checks if `value` is less than or equal to `other`.\n *\n * @static\n * @memberOf _\n * @since 3.9.0\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if `value` is less than or equal to\n * `other`, else `false`.\n * @see _.gte\n * @example\n *\n * _.lte(1, 3);\n * // => true\n *\n * _.lte(3, 3);\n * // => true\n *\n * _.lte(3, 1);\n * // => false\n */\n var lte = createRelationalOperation(function(value, other) {\n return value <= other;\n });\n\n /**\n * Converts `value` to an array.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {Array} Returns the converted array.\n * @example\n *\n * _.toArray({ 'a': 1, 'b': 2 });\n * // => [1, 2]\n *\n * _.toArray('abc');\n * // => ['a', 'b', 'c']\n *\n * _.toArray(1);\n * // => []\n *\n * _.toArray(null);\n * // => []\n */\n function toArray(value) {\n if (!value) {\n return [];\n }\n if (isArrayLike(value)) {\n return isString(value) ? stringToArray(value) : copyArray(value);\n }\n if (symIterator && value[symIterator]) {\n return iteratorToArray(value[symIterator]());\n }\n var tag = getTag(value),\n func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values);\n\n return func(value);\n }\n\n /**\n * Converts `value` to a finite number.\n *\n * @static\n * @memberOf _\n * @since 4.12.0\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted number.\n * @example\n *\n * _.toFinite(3.2);\n * // => 3.2\n *\n * _.toFinite(Number.MIN_VALUE);\n * // => 5e-324\n *\n * _.toFinite(Infinity);\n * // => 1.7976931348623157e+308\n *\n * _.toFinite('3.2');\n * // => 3.2\n */\n function toFinite(value) {\n if (!value) {\n return value === 0 ? value : 0;\n }\n value = toNumber(value);\n if (value === INFINITY || value === -INFINITY) {\n var sign = (value < 0 ? -1 : 1);\n return sign * MAX_INTEGER;\n }\n return value === value ? value : 0;\n }\n\n /**\n * Converts `value` to an integer.\n *\n * **Note:** This method is loosely based on\n * [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted integer.\n * @example\n *\n * _.toInteger(3.2);\n * // => 3\n *\n * _.toInteger(Number.MIN_VALUE);\n * // => 0\n *\n * _.toInteger(Infinity);\n * // => 1.7976931348623157e+308\n *\n * _.toInteger('3.2');\n * // => 3\n */\n function toInteger(value) {\n var result = toFinite(value),\n remainder = result % 1;\n\n return result === result ? (remainder ? result - remainder : result) : 0;\n }\n\n /**\n * Converts `value` to an integer suitable for use as the length of an\n * array-like object.\n *\n * **Note:** This method is based on\n * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted integer.\n * @example\n *\n * _.toLength(3.2);\n * // => 3\n *\n * _.toLength(Number.MIN_VALUE);\n * // => 0\n *\n * _.toLength(Infinity);\n * // => 4294967295\n *\n * _.toLength('3.2');\n * // => 3\n */\n function toLength(value) {\n return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0;\n }\n\n /**\n * Converts `value` to a number.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to process.\n * @returns {number} Returns the number.\n * @example\n *\n * _.toNumber(3.2);\n * // => 3.2\n *\n * _.toNumber(Number.MIN_VALUE);\n * // => 5e-324\n *\n * _.toNumber(Infinity);\n * // => Infinity\n *\n * _.toNumber('3.2');\n * // => 3.2\n */\n function toNumber(value) {\n if (typeof value == 'number') {\n return value;\n }\n if (isSymbol(value)) {\n return NAN;\n }\n if (isObject(value)) {\n var other = typeof value.valueOf == 'function' ? value.valueOf() : value;\n value = isObject(other) ? (other + '') : other;\n }\n if (typeof value != 'string') {\n return value === 0 ? value : +value;\n }\n value = value.replace(reTrim, '');\n var isBinary = reIsBinary.test(value);\n return (isBinary || reIsOctal.test(value))\n ? freeParseInt(value.slice(2), isBinary ? 2 : 8)\n : (reIsBadHex.test(value) ? NAN : +value);\n }\n\n /**\n * Converts `value` to a plain object flattening inherited enumerable string\n * keyed properties of `value` to own properties of the plain object.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {Object} Returns the converted plain object.\n * @example\n *\n * function Foo() {\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.assign({ 'a': 1 }, new Foo);\n * // => { 'a': 1, 'b': 2 }\n *\n * _.assign({ 'a': 1 }, _.toPlainObject(new Foo));\n * // => { 'a': 1, 'b': 2, 'c': 3 }\n */\n function toPlainObject(value) {\n return copyObject(value, keysIn(value));\n }\n\n /**\n * Converts `value` to a safe integer. A safe integer can be compared and\n * represented correctly.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted integer.\n * @example\n *\n * _.toSafeInteger(3.2);\n * // => 3\n *\n * _.toSafeInteger(Number.MIN_VALUE);\n * // => 0\n *\n * _.toSafeInteger(Infinity);\n * // => 9007199254740991\n *\n * _.toSafeInteger('3.2');\n * // => 3\n */\n function toSafeInteger(value) {\n return value\n ? baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER)\n : (value === 0 ? value : 0);\n }\n\n /**\n * Converts `value` to a string. An empty string is returned for `null`\n * and `undefined` values. The sign of `-0` is preserved.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {string} Returns the converted string.\n * @example\n *\n * _.toString(null);\n * // => ''\n *\n * _.toString(-0);\n * // => '-0'\n *\n * _.toString([1, 2, 3]);\n * // => '1,2,3'\n */\n function toString(value) {\n return value == null ? '' : baseToString(value);\n }\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Assigns own enumerable string keyed properties of source objects to the\n * destination object. Source objects are applied from left to right.\n * Subsequent sources overwrite property assignments of previous sources.\n *\n * **Note:** This method mutates `object` and is loosely based on\n * [`Object.assign`](https://mdn.io/Object/assign).\n *\n * @static\n * @memberOf _\n * @since 0.10.0\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @see _.assignIn\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * }\n *\n * function Bar() {\n * this.c = 3;\n * }\n *\n * Foo.prototype.b = 2;\n * Bar.prototype.d = 4;\n *\n * _.assign({ 'a': 0 }, new Foo, new Bar);\n * // => { 'a': 1, 'c': 3 }\n */\n var assign = createAssigner(function(object, source) {\n if (isPrototype(source) || isArrayLike(source)) {\n copyObject(source, keys(source), object);\n return;\n }\n for (var key in source) {\n if (hasOwnProperty.call(source, key)) {\n assignValue(object, key, source[key]);\n }\n }\n });\n\n /**\n * This method is like `_.assign` except that it iterates over own and\n * inherited source properties.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @alias extend\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @see _.assign\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * }\n *\n * function Bar() {\n * this.c = 3;\n * }\n *\n * Foo.prototype.b = 2;\n * Bar.prototype.d = 4;\n *\n * _.assignIn({ 'a': 0 }, new Foo, new Bar);\n * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 }\n */\n var assignIn = createAssigner(function(object, source) {\n copyObject(source, keysIn(source), object);\n });\n\n /**\n * This method is like `_.assignIn` except that it accepts `customizer`\n * which is invoked to produce the assigned values. If `customizer` returns\n * `undefined`, assignment is handled by the method instead. The `customizer`\n * is invoked with five arguments: (objValue, srcValue, key, object, source).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @alias extendWith\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} [customizer] The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @see _.assignWith\n * @example\n *\n * function customizer(objValue, srcValue) {\n * return _.isUndefined(objValue) ? srcValue : objValue;\n * }\n *\n * var defaults = _.partialRight(_.assignInWith, customizer);\n *\n * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n * // => { 'a': 1, 'b': 2 }\n */\n var assignInWith = createAssigner(function(object, source, srcIndex, customizer) {\n copyObject(source, keysIn(source), object, customizer);\n });\n\n /**\n * This method is like `_.assign` except that it accepts `customizer`\n * which is invoked to produce the assigned values. If `customizer` returns\n * `undefined`, assignment is handled by the method instead. The `customizer`\n * is invoked with five arguments: (objValue, srcValue, key, object, source).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} [customizer] The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @see _.assignInWith\n * @example\n *\n * function customizer(objValue, srcValue) {\n * return _.isUndefined(objValue) ? srcValue : objValue;\n * }\n *\n * var defaults = _.partialRight(_.assignWith, customizer);\n *\n * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n * // => { 'a': 1, 'b': 2 }\n */\n var assignWith = createAssigner(function(object, source, srcIndex, customizer) {\n copyObject(source, keys(source), object, customizer);\n });\n\n /**\n * Creates an array of values corresponding to `paths` of `object`.\n *\n * @static\n * @memberOf _\n * @since 1.0.0\n * @category Object\n * @param {Object} object The object to iterate over.\n * @param {...(string|string[])} [paths] The property paths to pick.\n * @returns {Array} Returns the picked values.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };\n *\n * _.at(object, ['a[0].b.c', 'a[1]']);\n * // => [3, 4]\n */\n var at = flatRest(baseAt);\n\n /**\n * Creates an object that inherits from the `prototype` object. If a\n * `properties` object is given, its own enumerable string keyed properties\n * are assigned to the created object.\n *\n * @static\n * @memberOf _\n * @since 2.3.0\n * @category Object\n * @param {Object} prototype The object to inherit from.\n * @param {Object} [properties] The properties to assign to the object.\n * @returns {Object} Returns the new object.\n * @example\n *\n * function Shape() {\n * this.x = 0;\n * this.y = 0;\n * }\n *\n * function Circle() {\n * Shape.call(this);\n * }\n *\n * Circle.prototype = _.create(Shape.prototype, {\n * 'constructor': Circle\n * });\n *\n * var circle = new Circle;\n * circle instanceof Circle;\n * // => true\n *\n * circle instanceof Shape;\n * // => true\n */\n function create(prototype, properties) {\n var result = baseCreate(prototype);\n return properties == null ? result : baseAssign(result, properties);\n }\n\n /**\n * Assigns own and inherited enumerable string keyed properties of source\n * objects to the destination object for all destination properties that\n * resolve to `undefined`. Source objects are applied from left to right.\n * Once a property is set, additional values of the same property are ignored.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @see _.defaultsDeep\n * @example\n *\n * _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n * // => { 'a': 1, 'b': 2 }\n */\n var defaults = baseRest(function(object, sources) {\n object = Object(object);\n\n var index = -1;\n var length = sources.length;\n var guard = length > 2 ? sources[2] : undefined;\n\n if (guard && isIterateeCall(sources[0], sources[1], guard)) {\n length = 1;\n }\n\n while (++index < length) {\n var source = sources[index];\n var props = keysIn(source);\n var propsIndex = -1;\n var propsLength = props.length;\n\n while (++propsIndex < propsLength) {\n var key = props[propsIndex];\n var value = object[key];\n\n if (value === undefined ||\n (eq(value, objectProto[key]) && !hasOwnProperty.call(object, key))) {\n object[key] = source[key];\n }\n }\n }\n\n return object;\n });\n\n /**\n * This method is like `_.defaults` except that it recursively assigns\n * default properties.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 3.10.0\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @see _.defaults\n * @example\n *\n * _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } });\n * // => { 'a': { 'b': 2, 'c': 3 } }\n */\n var defaultsDeep = baseRest(function(args) {\n args.push(undefined, customDefaultsMerge);\n return apply(mergeWith, undefined, args);\n });\n\n /**\n * This method is like `_.find` except that it returns the key of the first\n * element `predicate` returns truthy for instead of the element itself.\n *\n * @static\n * @memberOf _\n * @since 1.1.0\n * @category Object\n * @param {Object} object The object to inspect.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {string|undefined} Returns the key of the matched element,\n * else `undefined`.\n * @example\n *\n * var users = {\n * 'barney': { 'age': 36, 'active': true },\n * 'fred': { 'age': 40, 'active': false },\n * 'pebbles': { 'age': 1, 'active': true }\n * };\n *\n * _.findKey(users, function(o) { return o.age < 40; });\n * // => 'barney' (iteration order is not guaranteed)\n *\n * // The `_.matches` iteratee shorthand.\n * _.findKey(users, { 'age': 1, 'active': true });\n * // => 'pebbles'\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.findKey(users, ['active', false]);\n * // => 'fred'\n *\n * // The `_.property` iteratee shorthand.\n * _.findKey(users, 'active');\n * // => 'barney'\n */\n function findKey(object, predicate) {\n return baseFindKey(object, getIteratee(predicate, 3), baseForOwn);\n }\n\n /**\n * This method is like `_.findKey` except that it iterates over elements of\n * a collection in the opposite order.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Object\n * @param {Object} object The object to inspect.\n * @param {Function} [predicate=_.identity] The function invoked per iteration.\n * @returns {string|undefined} Returns the key of the matched element,\n * else `undefined`.\n * @example\n *\n * var users = {\n * 'barney': { 'age': 36, 'active': true },\n * 'fred': { 'age': 40, 'active': false },\n * 'pebbles': { 'age': 1, 'active': true }\n * };\n *\n * _.findLastKey(users, function(o) { return o.age < 40; });\n * // => returns 'pebbles' assuming `_.findKey` returns 'barney'\n *\n * // The `_.matches` iteratee shorthand.\n * _.findLastKey(users, { 'age': 36, 'active': true });\n * // => 'barney'\n *\n * // The `_.matchesProperty` iteratee shorthand.\n * _.findLastKey(users, ['active', false]);\n * // => 'fred'\n *\n * // The `_.property` iteratee shorthand.\n * _.findLastKey(users, 'active');\n * // => 'pebbles'\n */\n function findLastKey(object, predicate) {\n return baseFindKey(object, getIteratee(predicate, 3), baseForOwnRight);\n }\n\n /**\n * Iterates over own and inherited enumerable string keyed properties of an\n * object and invokes `iteratee` for each property. The iteratee is invoked\n * with three arguments: (value, key, object). Iteratee functions may exit\n * iteration early by explicitly returning `false`.\n *\n * @static\n * @memberOf _\n * @since 0.3.0\n * @category Object\n * @param {Object} object The object to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Object} Returns `object`.\n * @see _.forInRight\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.forIn(new Foo, function(value, key) {\n * console.log(key);\n * });\n * // => Logs 'a', 'b', then 'c' (iteration order is not guaranteed).\n */\n function forIn(object, iteratee) {\n return object == null\n ? object\n : baseFor(object, getIteratee(iteratee, 3), keysIn);\n }\n\n /**\n * This method is like `_.forIn` except that it iterates over properties of\n * `object` in the opposite order.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Object\n * @param {Object} object The object to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Object} Returns `object`.\n * @see _.forIn\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.forInRight(new Foo, function(value, key) {\n * console.log(key);\n * });\n * // => Logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'.\n */\n function forInRight(object, iteratee) {\n return object == null\n ? object\n : baseForRight(object, getIteratee(iteratee, 3), keysIn);\n }\n\n /**\n * Iterates over own enumerable string keyed properties of an object and\n * invokes `iteratee` for each property. The iteratee is invoked with three\n * arguments: (value, key, object). Iteratee functions may exit iteration\n * early by explicitly returning `false`.\n *\n * @static\n * @memberOf _\n * @since 0.3.0\n * @category Object\n * @param {Object} object The object to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Object} Returns `object`.\n * @see _.forOwnRight\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.forOwn(new Foo, function(value, key) {\n * console.log(key);\n * });\n * // => Logs 'a' then 'b' (iteration order is not guaranteed).\n */\n function forOwn(object, iteratee) {\n return object && baseForOwn(object, getIteratee(iteratee, 3));\n }\n\n /**\n * This method is like `_.forOwn` except that it iterates over properties of\n * `object` in the opposite order.\n *\n * @static\n * @memberOf _\n * @since 2.0.0\n * @category Object\n * @param {Object} object The object to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Object} Returns `object`.\n * @see _.forOwn\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.forOwnRight(new Foo, function(value, key) {\n * console.log(key);\n * });\n * // => Logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'.\n */\n function forOwnRight(object, iteratee) {\n return object && baseForOwnRight(object, getIteratee(iteratee, 3));\n }\n\n /**\n * Creates an array of function property names from own enumerable properties\n * of `object`.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Object\n * @param {Object} object The object to inspect.\n * @returns {Array} Returns the function names.\n * @see _.functionsIn\n * @example\n *\n * function Foo() {\n * this.a = _.constant('a');\n * this.b = _.constant('b');\n * }\n *\n * Foo.prototype.c = _.constant('c');\n *\n * _.functions(new Foo);\n * // => ['a', 'b']\n */\n function functions(object) {\n return object == null ? [] : baseFunctions(object, keys(object));\n }\n\n /**\n * Creates an array of function property names from own and inherited\n * enumerable properties of `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The object to inspect.\n * @returns {Array} Returns the function names.\n * @see _.functions\n * @example\n *\n * function Foo() {\n * this.a = _.constant('a');\n * this.b = _.constant('b');\n * }\n *\n * Foo.prototype.c = _.constant('c');\n *\n * _.functionsIn(new Foo);\n * // => ['a', 'b', 'c']\n */\n function functionsIn(object) {\n return object == null ? [] : baseFunctions(object, keysIn(object));\n }\n\n /**\n * Gets the value at `path` of `object`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n * @static\n * @memberOf _\n * @since 3.7.0\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path of the property to get.\n * @param {*} [defaultValue] The value returned for `undefined` resolved values.\n * @returns {*} Returns the resolved value.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c': 3 } }] };\n *\n * _.get(object, 'a[0].b.c');\n * // => 3\n *\n * _.get(object, ['a', '0', 'b', 'c']);\n * // => 3\n *\n * _.get(object, 'a.b.c', 'default');\n * // => 'default'\n */\n function get(object, path, defaultValue) {\n var result = object == null ? undefined : baseGet(object, path);\n return result === undefined ? defaultValue : result;\n }\n\n /**\n * Checks if `path` is a direct property of `object`.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path to check.\n * @returns {boolean} Returns `true` if `path` exists, else `false`.\n * @example\n *\n * var object = { 'a': { 'b': 2 } };\n * var other = _.create({ 'a': _.create({ 'b': 2 }) });\n *\n * _.has(object, 'a');\n * // => true\n *\n * _.has(object, 'a.b');\n * // => true\n *\n * _.has(object, ['a', 'b']);\n * // => true\n *\n * _.has(other, 'a');\n * // => false\n */\n function has(object, path) {\n return object != null && hasPath(object, path, baseHas);\n }\n\n /**\n * Checks if `path` is a direct or inherited property of `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path to check.\n * @returns {boolean} Returns `true` if `path` exists, else `false`.\n * @example\n *\n * var object = _.create({ 'a': _.create({ 'b': 2 }) });\n *\n * _.hasIn(object, 'a');\n * // => true\n *\n * _.hasIn(object, 'a.b');\n * // => true\n *\n * _.hasIn(object, ['a', 'b']);\n * // => true\n *\n * _.hasIn(object, 'b');\n * // => false\n */\n function hasIn(object, path) {\n return object != null && hasPath(object, path, baseHasIn);\n }\n\n /**\n * Creates an object composed of the inverted keys and values of `object`.\n * If `object` contains duplicate values, subsequent values overwrite\n * property assignments of previous values.\n *\n * @static\n * @memberOf _\n * @since 0.7.0\n * @category Object\n * @param {Object} object The object to invert.\n * @returns {Object} Returns the new inverted object.\n * @example\n *\n * var object = { 'a': 1, 'b': 2, 'c': 1 };\n *\n * _.invert(object);\n * // => { '1': 'c', '2': 'b' }\n */\n var invert = createInverter(function(result, value, key) {\n if (value != null &&\n typeof value.toString != 'function') {\n value = nativeObjectToString.call(value);\n }\n\n result[value] = key;\n }, constant(identity));\n\n /**\n * This method is like `_.invert` except that the inverted object is generated\n * from the results of running each element of `object` thru `iteratee`. The\n * corresponding inverted value of each inverted key is an array of keys\n * responsible for generating the inverted value. The iteratee is invoked\n * with one argument: (value).\n *\n * @static\n * @memberOf _\n * @since 4.1.0\n * @category Object\n * @param {Object} object The object to invert.\n * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Object} Returns the new inverted object.\n * @example\n *\n * var object = { 'a': 1, 'b': 2, 'c': 1 };\n *\n * _.invertBy(object);\n * // => { '1': ['a', 'c'], '2': ['b'] }\n *\n * _.invertBy(object, function(value) {\n * return 'group' + value;\n * });\n * // => { 'group1': ['a', 'c'], 'group2': ['b'] }\n */\n var invertBy = createInverter(function(result, value, key) {\n if (value != null &&\n typeof value.toString != 'function') {\n value = nativeObjectToString.call(value);\n }\n\n if (hasOwnProperty.call(result, value)) {\n result[value].push(key);\n } else {\n result[value] = [key];\n }\n }, getIteratee);\n\n /**\n * Invokes the method at `path` of `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path of the method to invoke.\n * @param {...*} [args] The arguments to invoke the method with.\n * @returns {*} Returns the result of the invoked method.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] };\n *\n * _.invoke(object, 'a[0].b.c.slice', 1, 3);\n * // => [2, 3]\n */\n var invoke = baseRest(baseInvoke);\n\n /**\n * Creates an array of the own enumerable property names of `object`.\n *\n * **Note:** Non-object values are coerced to objects. See the\n * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)\n * for more details.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Object\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property names.\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.keys(new Foo);\n * // => ['a', 'b'] (iteration order is not guaranteed)\n *\n * _.keys('hi');\n * // => ['0', '1']\n */\n function keys(object) {\n return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);\n }\n\n /**\n * Creates an array of the own and inherited enumerable property names of `object`.\n *\n * **Note:** Non-object values are coerced to objects.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Object\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property names.\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.keysIn(new Foo);\n * // => ['a', 'b', 'c'] (iteration order is not guaranteed)\n */\n function keysIn(object) {\n return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);\n }\n\n /**\n * The opposite of `_.mapValues`; this method creates an object with the\n * same values as `object` and keys generated by running each own enumerable\n * string keyed property of `object` thru `iteratee`. The iteratee is invoked\n * with three arguments: (value, key, object).\n *\n * @static\n * @memberOf _\n * @since 3.8.0\n * @category Object\n * @param {Object} object The object to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Object} Returns the new mapped object.\n * @see _.mapValues\n * @example\n *\n * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) {\n * return key + value;\n * });\n * // => { 'a1': 1, 'b2': 2 }\n */\n function mapKeys(object, iteratee) {\n var result = {};\n iteratee = getIteratee(iteratee, 3);\n\n baseForOwn(object, function(value, key, object) {\n baseAssignValue(result, iteratee(value, key, object), value);\n });\n return result;\n }\n\n /**\n * Creates an object with the same keys as `object` and values generated\n * by running each own enumerable string keyed property of `object` thru\n * `iteratee`. The iteratee is invoked with three arguments:\n * (value, key, object).\n *\n * @static\n * @memberOf _\n * @since 2.4.0\n * @category Object\n * @param {Object} object The object to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @returns {Object} Returns the new mapped object.\n * @see _.mapKeys\n * @example\n *\n * var users = {\n * 'fred': { 'user': 'fred', 'age': 40 },\n * 'pebbles': { 'user': 'pebbles', 'age': 1 }\n * };\n *\n * _.mapValues(users, function(o) { return o.age; });\n * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)\n *\n * // The `_.property` iteratee shorthand.\n * _.mapValues(users, 'age');\n * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)\n */\n function mapValues(object, iteratee) {\n var result = {};\n iteratee = getIteratee(iteratee, 3);\n\n baseForOwn(object, function(value, key, object) {\n baseAssignValue(result, key, iteratee(value, key, object));\n });\n return result;\n }\n\n /**\n * This method is like `_.assign` except that it recursively merges own and\n * inherited enumerable string keyed properties of source objects into the\n * destination object. Source properties that resolve to `undefined` are\n * skipped if a destination value exists. Array and plain object properties\n * are merged recursively. Other objects and value types are overridden by\n * assignment. Source objects are applied from left to right. Subsequent\n * sources overwrite property assignments of previous sources.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 0.5.0\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @example\n *\n * var object = {\n * 'a': [{ 'b': 2 }, { 'd': 4 }]\n * };\n *\n * var other = {\n * 'a': [{ 'c': 3 }, { 'e': 5 }]\n * };\n *\n * _.merge(object, other);\n * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }\n */\n var merge = createAssigner(function(object, source, srcIndex) {\n baseMerge(object, source, srcIndex);\n });\n\n /**\n * This method is like `_.merge` except that it accepts `customizer` which\n * is invoked to produce the merged values of the destination and source\n * properties. If `customizer` returns `undefined`, merging is handled by the\n * method instead. The `customizer` is invoked with six arguments:\n * (objValue, srcValue, key, object, source, stack).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} customizer The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function customizer(objValue, srcValue) {\n * if (_.isArray(objValue)) {\n * return objValue.concat(srcValue);\n * }\n * }\n *\n * var object = { 'a': [1], 'b': [2] };\n * var other = { 'a': [3], 'b': [4] };\n *\n * _.mergeWith(object, other, customizer);\n * // => { 'a': [1, 3], 'b': [2, 4] }\n */\n var mergeWith = createAssigner(function(object, source, srcIndex, customizer) {\n baseMerge(object, source, srcIndex, customizer);\n });\n\n /**\n * The opposite of `_.pick`; this method creates an object composed of the\n * own and inherited enumerable property paths of `object` that are not omitted.\n *\n * **Note:** This method is considerably slower than `_.pick`.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {...(string|string[])} [paths] The property paths to omit.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.omit(object, ['a', 'c']);\n * // => { 'b': '2' }\n */\n var omit = flatRest(function(object, paths) {\n var result = {};\n if (object == null) {\n return result;\n }\n var isDeep = false;\n paths = arrayMap(paths, function(path) {\n path = castPath(path, object);\n isDeep || (isDeep = path.length > 1);\n return path;\n });\n copyObject(object, getAllKeysIn(object), result);\n if (isDeep) {\n result = baseClone(result, CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAG, customOmitClone);\n }\n var length = paths.length;\n while (length--) {\n baseUnset(result, paths[length]);\n }\n return result;\n });\n\n /**\n * The opposite of `_.pickBy`; this method creates an object composed of\n * the own and inherited enumerable string keyed properties of `object` that\n * `predicate` doesn't return truthy for. The predicate is invoked with two\n * arguments: (value, key).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The source object.\n * @param {Function} [predicate=_.identity] The function invoked per property.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.omitBy(object, _.isNumber);\n * // => { 'b': '2' }\n */\n function omitBy(object, predicate) {\n return pickBy(object, negate(getIteratee(predicate)));\n }\n\n /**\n * Creates an object composed of the picked `object` properties.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {...(string|string[])} [paths] The property paths to pick.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.pick(object, ['a', 'c']);\n * // => { 'a': 1, 'c': 3 }\n */\n var pick = flatRest(function(object, paths) {\n return object == null ? {} : basePick(object, paths);\n });\n\n /**\n * Creates an object composed of the `object` properties `predicate` returns\n * truthy for. The predicate is invoked with two arguments: (value, key).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The source object.\n * @param {Function} [predicate=_.identity] The function invoked per property.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.pickBy(object, _.isNumber);\n * // => { 'a': 1, 'c': 3 }\n */\n function pickBy(object, predicate) {\n if (object == null) {\n return {};\n }\n var props = arrayMap(getAllKeysIn(object), function(prop) {\n return [prop];\n });\n predicate = getIteratee(predicate);\n return basePickBy(object, props, function(value, path) {\n return predicate(value, path[0]);\n });\n }\n\n /**\n * This method is like `_.get` except that if the resolved value is a\n * function it's invoked with the `this` binding of its parent object and\n * its result is returned.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path of the property to resolve.\n * @param {*} [defaultValue] The value returned for `undefined` resolved values.\n * @returns {*} Returns the resolved value.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] };\n *\n * _.result(object, 'a[0].b.c1');\n * // => 3\n *\n * _.result(object, 'a[0].b.c2');\n * // => 4\n *\n * _.result(object, 'a[0].b.c3', 'default');\n * // => 'default'\n *\n * _.result(object, 'a[0].b.c3', _.constant('default'));\n * // => 'default'\n */\n function result(object, path, defaultValue) {\n path = castPath(path, object);\n\n var index = -1,\n length = path.length;\n\n // Ensure the loop is entered when path is empty.\n if (!length) {\n length = 1;\n object = undefined;\n }\n while (++index < length) {\n var value = object == null ? undefined : object[toKey(path[index])];\n if (value === undefined) {\n index = length;\n value = defaultValue;\n }\n object = isFunction(value) ? value.call(object) : value;\n }\n return object;\n }\n\n /**\n * Sets the value at `path` of `object`. If a portion of `path` doesn't exist,\n * it's created. Arrays are created for missing index properties while objects\n * are created for all other missing properties. Use `_.setWith` to customize\n * `path` creation.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 3.7.0\n * @category Object\n * @param {Object} object The object to modify.\n * @param {Array|string} path The path of the property to set.\n * @param {*} value The value to set.\n * @returns {Object} Returns `object`.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c': 3 } }] };\n *\n * _.set(object, 'a[0].b.c', 4);\n * console.log(object.a[0].b.c);\n * // => 4\n *\n * _.set(object, ['x', '0', 'y', 'z'], 5);\n * console.log(object.x[0].y.z);\n * // => 5\n */\n function set(object, path, value) {\n return object == null ? object : baseSet(object, path, value);\n }\n\n /**\n * This method is like `_.set` except that it accepts `customizer` which is\n * invoked to produce the objects of `path`. If `customizer` returns `undefined`\n * path creation is handled by the method instead. The `customizer` is invoked\n * with three arguments: (nsValue, key, nsObject).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The object to modify.\n * @param {Array|string} path The path of the property to set.\n * @param {*} value The value to set.\n * @param {Function} [customizer] The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * var object = {};\n *\n * _.setWith(object, '[0][1]', 'a', Object);\n * // => { '0': { '1': 'a' } }\n */\n function setWith(object, path, value, customizer) {\n customizer = typeof customizer == 'function' ? customizer : undefined;\n return object == null ? object : baseSet(object, path, value, customizer);\n }\n\n /**\n * Creates an array of own enumerable string keyed-value pairs for `object`\n * which can be consumed by `_.fromPairs`. If `object` is a map or set, its\n * entries are returned.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @alias entries\n * @category Object\n * @param {Object} object The object to query.\n * @returns {Array} Returns the key-value pairs.\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.toPairs(new Foo);\n * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed)\n */\n var toPairs = createToPairs(keys);\n\n /**\n * Creates an array of own and inherited enumerable string keyed-value pairs\n * for `object` which can be consumed by `_.fromPairs`. If `object` is a map\n * or set, its entries are returned.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @alias entriesIn\n * @category Object\n * @param {Object} object The object to query.\n * @returns {Array} Returns the key-value pairs.\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.toPairsIn(new Foo);\n * // => [['a', 1], ['b', 2], ['c', 3]] (iteration order is not guaranteed)\n */\n var toPairsIn = createToPairs(keysIn);\n\n /**\n * An alternative to `_.reduce`; this method transforms `object` to a new\n * `accumulator` object which is the result of running each of its own\n * enumerable string keyed properties thru `iteratee`, with each invocation\n * potentially mutating the `accumulator` object. If `accumulator` is not\n * provided, a new object with the same `[[Prototype]]` will be used. The\n * iteratee is invoked with four arguments: (accumulator, value, key, object).\n * Iteratee functions may exit iteration early by explicitly returning `false`.\n *\n * @static\n * @memberOf _\n * @since 1.3.0\n * @category Object\n * @param {Object} object The object to iterate over.\n * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n * @param {*} [accumulator] The custom accumulator value.\n * @returns {*} Returns the accumulated value.\n * @example\n *\n * _.transform([2, 3, 4], function(result, n) {\n * result.push(n *= n);\n * return n % 2 == 0;\n * }, []);\n * // => [4, 9]\n *\n * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {\n * (result[value] || (result[value] = [])).push(key);\n * }, {});\n * // => { '1': ['a', 'c'], '2': ['b'] }\n */\n function transform(object, iteratee, accumulator) {\n var isArr = isArray(object),\n isArrLike = isArr || isBuffer(object) || isTypedArray(object);\n\n iteratee = getIteratee(iteratee, 4);\n if (accumulator == null) {\n var Ctor = object && object.constructor;\n if (isArrLike) {\n accumulator = isArr ? new Ctor : [];\n }\n else if (isObject(object)) {\n accumulator = isFunction(Ctor) ? baseCreate(getPrototype(object)) : {};\n }\n else {\n accumulator = {};\n }\n }\n (isArrLike ? arrayEach : baseForOwn)(object, function(value, index, object) {\n return iteratee(accumulator, value, index, object);\n });\n return accumulator;\n }\n\n /**\n * Removes the property at `path` of `object`.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Object\n * @param {Object} object The object to modify.\n * @param {Array|string} path The path of the property to unset.\n * @returns {boolean} Returns `true` if the property is deleted, else `false`.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c': 7 } }] };\n * _.unset(object, 'a[0].b.c');\n * // => true\n *\n * console.log(object);\n * // => { 'a': [{ 'b': {} }] };\n *\n * _.unset(object, ['a', '0', 'b', 'c']);\n * // => true\n *\n * console.log(object);\n * // => { 'a': [{ 'b': {} }] };\n */\n function unset(object, path) {\n return object == null ? true : baseUnset(object, path);\n }\n\n /**\n * This method is like `_.set` except that accepts `updater` to produce the\n * value to set. Use `_.updateWith` to customize `path` creation. The `updater`\n * is invoked with one argument: (value).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 4.6.0\n * @category Object\n * @param {Object} object The object to modify.\n * @param {Array|string} path The path of the property to set.\n * @param {Function} updater The function to produce the updated value.\n * @returns {Object} Returns `object`.\n * @example\n *\n * var object = { 'a': [{ 'b': { 'c': 3 } }] };\n *\n * _.update(object, 'a[0].b.c', function(n) { return n * n; });\n * console.log(object.a[0].b.c);\n * // => 9\n *\n * _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; });\n * console.log(object.x[0].y.z);\n * // => 0\n */\n function update(object, path, updater) {\n return object == null ? object : baseUpdate(object, path, castFunction(updater));\n }\n\n /**\n * This method is like `_.update` except that it accepts `customizer` which is\n * invoked to produce the objects of `path`. If `customizer` returns `undefined`\n * path creation is handled by the method instead. The `customizer` is invoked\n * with three arguments: (nsValue, key, nsObject).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @since 4.6.0\n * @category Object\n * @param {Object} object The object to modify.\n * @param {Array|string} path The path of the property to set.\n * @param {Function} updater The function to produce the updated value.\n * @param {Function} [customizer] The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * var object = {};\n *\n * _.updateWith(object, '[0][1]', _.constant('a'), Object);\n * // => { '0': { '1': 'a' } }\n */\n function updateWith(object, path, updater, customizer) {\n customizer = typeof customizer == 'function' ? customizer : undefined;\n return object == null ? object : baseUpdate(object, path, castFunction(updater), customizer);\n }\n\n /**\n * Creates an array of the own enumerable string keyed property values of `object`.\n *\n * **Note:** Non-object values are coerced to objects.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category Object\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property values.\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.values(new Foo);\n * // => [1, 2] (iteration order is not guaranteed)\n *\n * _.values('hi');\n * // => ['h', 'i']\n */\n function values(object) {\n return object == null ? [] : baseValues(object, keys(object));\n }\n\n /**\n * Creates an array of the own and inherited enumerable string keyed property\n * values of `object`.\n *\n * **Note:** Non-object values are coerced to objects.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category Object\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of property values.\n * @example\n *\n * function Foo() {\n * this.a = 1;\n * this.b = 2;\n * }\n *\n * Foo.prototype.c = 3;\n *\n * _.valuesIn(new Foo);\n * // => [1, 2, 3] (iteration order is not guaranteed)\n */\n function valuesIn(object) {\n return object == null ? [] : baseValues(object, keysIn(object));\n }\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Clamps `number` within the inclusive `lower` and `upper` bounds.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Number\n * @param {number} number The number to clamp.\n * @param {number} [lower] The lower bound.\n * @param {number} upper The upper bound.\n * @returns {number} Returns the clamped number.\n * @example\n *\n * _.clamp(-10, -5, 5);\n * // => -5\n *\n * _.clamp(10, -5, 5);\n * // => 5\n */\n function clamp(number, lower, upper) {\n if (upper === undefined) {\n upper = lower;\n lower = undefined;\n }\n if (upper !== undefined) {\n upper = toNumber(upper);\n upper = upper === upper ? upper : 0;\n }\n if (lower !== undefined) {\n lower = toNumber(lower);\n lower = lower === lower ? lower : 0;\n }\n return baseClamp(toNumber(number), lower, upper);\n }\n\n /**\n * Checks if `n` is between `start` and up to, but not including, `end`. If\n * `end` is not specified, it's set to `start` with `start` then set to `0`.\n * If `start` is greater than `end` the params are swapped to support\n * negative ranges.\n *\n * @static\n * @memberOf _\n * @since 3.3.0\n * @category Number\n * @param {number} number The number to check.\n * @param {number} [start=0] The start of the range.\n * @param {number} end The end of the range.\n * @returns {boolean} Returns `true` if `number` is in the range, else `false`.\n * @see _.range, _.rangeRight\n * @example\n *\n * _.inRange(3, 2, 4);\n * // => true\n *\n * _.inRange(4, 8);\n * // => true\n *\n * _.inRange(4, 2);\n * // => false\n *\n * _.inRange(2, 2);\n * // => false\n *\n * _.inRange(1.2, 2);\n * // => true\n *\n * _.inRange(5.2, 4);\n * // => false\n *\n * _.inRange(-3, -2, -6);\n * // => true\n */\n function inRange(number, start, end) {\n start = toFinite(start);\n if (end === undefined) {\n end = start;\n start = 0;\n } else {\n end = toFinite(end);\n }\n number = toNumber(number);\n return baseInRange(number, start, end);\n }\n\n /**\n * Produces a random number between the inclusive `lower` and `upper` bounds.\n * If only one argument is provided a number between `0` and the given number\n * is returned. If `floating` is `true`, or either `lower` or `upper` are\n * floats, a floating-point number is returned instead of an integer.\n *\n * **Note:** JavaScript follows the IEEE-754 standard for resolving\n * floating-point values which can produce unexpected results.\n *\n * @static\n * @memberOf _\n * @since 0.7.0\n * @category Number\n * @param {number} [lower=0] The lower bound.\n * @param {number} [upper=1] The upper bound.\n * @param {boolean} [floating] Specify returning a floating-point number.\n * @returns {number} Returns the random number.\n * @example\n *\n * _.random(0, 5);\n * // => an integer between 0 and 5\n *\n * _.random(5);\n * // => also an integer between 0 and 5\n *\n * _.random(5, true);\n * // => a floating-point number between 0 and 5\n *\n * _.random(1.2, 5.2);\n * // => a floating-point number between 1.2 and 5.2\n */\n function random(lower, upper, floating) {\n if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) {\n upper = floating = undefined;\n }\n if (floating === undefined) {\n if (typeof upper == 'boolean') {\n floating = upper;\n upper = undefined;\n }\n else if (typeof lower == 'boolean') {\n floating = lower;\n lower = undefined;\n }\n }\n if (lower === undefined && upper === undefined) {\n lower = 0;\n upper = 1;\n }\n else {\n lower = toFinite(lower);\n if (upper === undefined) {\n upper = lower;\n lower = 0;\n } else {\n upper = toFinite(upper);\n }\n }\n if (lower > upper) {\n var temp = lower;\n lower = upper;\n upper = temp;\n }\n if (floating || lower % 1 || upper % 1) {\n var rand = nativeRandom();\n return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper);\n }\n return baseRandom(lower, upper);\n }\n\n /*------------------------------------------------------------------------*/\n\n /**\n * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to convert.\n * @returns {string} Returns the camel cased string.\n * @example\n *\n * _.camelCase('Foo Bar');\n * // => 'fooBar'\n *\n * _.camelCase('--foo-bar--');\n * // => 'fooBar'\n *\n * _.camelCase('__FOO_BAR__');\n * // => 'fooBar'\n */\n var camelCase = createCompounder(function(result, word, index) {\n word = word.toLowerCase();\n return result + (index ? capitalize(word) : word);\n });\n\n /**\n * Converts the first character of `string` to upper case and the remaining\n * to lower case.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to capitalize.\n * @returns {string} Returns the capitalized string.\n * @example\n *\n * _.capitalize('FRED');\n * // => 'Fred'\n */\n function capitalize(string) {\n return upperFirst(toString(string).toLowerCase());\n }\n\n /**\n * Deburrs `string` by converting\n * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table)\n * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A)\n * letters to basic Latin letters and removing\n * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to deburr.\n * @returns {string} Returns the deburred string.\n * @example\n *\n * _.deburr('déjà vu');\n * // => 'deja vu'\n */\n function deburr(string) {\n string = toString(string);\n return string && string.replace(reLatin, deburrLetter).replace(reComboMark, '');\n }\n\n /**\n * Checks if `string` ends with the given target string.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to inspect.\n * @param {string} [target] The string to search for.\n * @param {number} [position=string.length] The position to search up to.\n * @returns {boolean} Returns `true` if `string` ends with `target`,\n * else `false`.\n * @example\n *\n * _.endsWith('abc', 'c');\n * // => true\n *\n * _.endsWith('abc', 'b');\n * // => false\n *\n * _.endsWith('abc', 'b', 2);\n * // => true\n */\n function endsWith(string, target, position) {\n string = toString(string);\n target = baseToString(target);\n\n var length = string.length;\n position = position === undefined\n ? length\n : baseClamp(toInteger(position), 0, length);\n\n var end = position;\n position -= target.length;\n return position >= 0 && string.slice(position, end) == target;\n }\n\n /**\n * Converts the characters \"&\", \"<\", \">\", '\"', and \"'\" in `string` to their\n * corresponding HTML entities.\n *\n * **Note:** No other characters are escaped. To escape additional\n * characters use a third-party library like [_he_](https://mths.be/he).\n *\n * Though the \">\" character is escaped for symmetry, characters like\n * \">\" and \"/\" don't need escaping in HTML and have no special meaning\n * unless they're part of a tag or unquoted attribute value. See\n * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands)\n * (under \"semi-related fun fact\") for more details.\n *\n * When working with HTML you should always\n * [quote attribute values](http://wonko.com/post/html-escaping) to reduce\n * XSS vectors.\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category String\n * @param {string} [string=''] The string to escape.\n * @returns {string} Returns the escaped string.\n * @example\n *\n * _.escape('fred, barney, & pebbles');\n * // => 'fred, barney, & pebbles'\n */\n function escape(string) {\n string = toString(string);\n return (string && reHasUnescapedHtml.test(string))\n ? string.replace(reUnescapedHtml, escapeHtmlChar)\n : string;\n }\n\n /**\n * Escapes the `RegExp` special characters \"^\", \"$\", \"\\\", \".\", \"*\", \"+\",\n * \"?\", \"(\", \")\", \"[\", \"]\", \"{\", \"}\", and \"|\" in `string`.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to escape.\n * @returns {string} Returns the escaped string.\n * @example\n *\n * _.escapeRegExp('[lodash](https://lodash.com/)');\n * // => '\\[lodash\\]\\(https://lodash\\.com/\\)'\n */\n function escapeRegExp(string) {\n string = toString(string);\n return (string && reHasRegExpChar.test(string))\n ? string.replace(reRegExpChar, '\\\\$&')\n : string;\n }\n\n /**\n * Converts `string` to\n * [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to convert.\n * @returns {string} Returns the kebab cased string.\n * @example\n *\n * _.kebabCase('Foo Bar');\n * // => 'foo-bar'\n *\n * _.kebabCase('fooBar');\n * // => 'foo-bar'\n *\n * _.kebabCase('__FOO_BAR__');\n * // => 'foo-bar'\n */\n var kebabCase = createCompounder(function(result, word, index) {\n return result + (index ? '-' : '') + word.toLowerCase();\n });\n\n /**\n * Converts `string`, as space separated words, to lower case.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category String\n * @param {string} [string=''] The string to convert.\n * @returns {string} Returns the lower cased string.\n * @example\n *\n * _.lowerCase('--Foo-Bar--');\n * // => 'foo bar'\n *\n * _.lowerCase('fooBar');\n * // => 'foo bar'\n *\n * _.lowerCase('__FOO_BAR__');\n * // => 'foo bar'\n */\n var lowerCase = createCompounder(function(result, word, index) {\n return result + (index ? ' ' : '') + word.toLowerCase();\n });\n\n /**\n * Converts the first character of `string` to lower case.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category String\n * @param {string} [string=''] The string to convert.\n * @returns {string} Returns the converted string.\n * @example\n *\n * _.lowerFirst('Fred');\n * // => 'fred'\n *\n * _.lowerFirst('FRED');\n * // => 'fRED'\n */\n var lowerFirst = createCaseFirst('toLowerCase');\n\n /**\n * Pads `string` on the left and right sides if it's shorter than `length`.\n * Padding characters are truncated if they can't be evenly divided by `length`.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to pad.\n * @param {number} [length=0] The padding length.\n * @param {string} [chars=' '] The string used as padding.\n * @returns {string} Returns the padded string.\n * @example\n *\n * _.pad('abc', 8);\n * // => ' abc '\n *\n * _.pad('abc', 8, '_-');\n * // => '_-abc_-_'\n *\n * _.pad('abc', 3);\n * // => 'abc'\n */\n function pad(string, length, chars) {\n string = toString(string);\n length = toInteger(length);\n\n var strLength = length ? stringSize(string) : 0;\n if (!length || strLength >= length) {\n return string;\n }\n var mid = (length - strLength) / 2;\n return (\n createPadding(nativeFloor(mid), chars) +\n string +\n createPadding(nativeCeil(mid), chars)\n );\n }\n\n /**\n * Pads `string` on the right side if it's shorter than `length`. Padding\n * characters are truncated if they exceed `length`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category String\n * @param {string} [string=''] The string to pad.\n * @param {number} [length=0] The padding length.\n * @param {string} [chars=' '] The string used as padding.\n * @returns {string} Returns the padded string.\n * @example\n *\n * _.padEnd('abc', 6);\n * // => 'abc '\n *\n * _.padEnd('abc', 6, '_-');\n * // => 'abc_-_'\n *\n * _.padEnd('abc', 3);\n * // => 'abc'\n */\n function padEnd(string, length, chars) {\n string = toString(string);\n length = toInteger(length);\n\n var strLength = length ? stringSize(string) : 0;\n return (length && strLength < length)\n ? (string + createPadding(length - strLength, chars))\n : string;\n }\n\n /**\n * Pads `string` on the left side if it's shorter than `length`. Padding\n * characters are truncated if they exceed `length`.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category String\n * @param {string} [string=''] The string to pad.\n * @param {number} [length=0] The padding length.\n * @param {string} [chars=' '] The string used as padding.\n * @returns {string} Returns the padded string.\n * @example\n *\n * _.padStart('abc', 6);\n * // => ' abc'\n *\n * _.padStart('abc', 6, '_-');\n * // => '_-_abc'\n *\n * _.padStart('abc', 3);\n * // => 'abc'\n */\n function padStart(string, length, chars) {\n string = toString(string);\n length = toInteger(length);\n\n var strLength = length ? stringSize(string) : 0;\n return (length && strLength < length)\n ? (createPadding(length - strLength, chars) + string)\n : string;\n }\n\n /**\n * Converts `string` to an integer of the specified radix. If `radix` is\n * `undefined` or `0`, a `radix` of `10` is used unless `value` is a\n * hexadecimal, in which case a `radix` of `16` is used.\n *\n * **Note:** This method aligns with the\n * [ES5 implementation](https://es5.github.io/#x15.1.2.2) of `parseInt`.\n *\n * @static\n * @memberOf _\n * @since 1.1.0\n * @category String\n * @param {string} string The string to convert.\n * @param {number} [radix=10] The radix to interpret `value` by.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {number} Returns the converted integer.\n * @example\n *\n * _.parseInt('08');\n * // => 8\n *\n * _.map(['6', '08', '10'], _.parseInt);\n * // => [6, 8, 10]\n */\n function parseInt(string, radix, guard) {\n if (guard || radix == null) {\n radix = 0;\n } else if (radix) {\n radix = +radix;\n }\n return nativeParseInt(toString(string).replace(reTrimStart, ''), radix || 0);\n }\n\n /**\n * Repeats the given string `n` times.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to repeat.\n * @param {number} [n=1] The number of times to repeat the string.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {string} Returns the repeated string.\n * @example\n *\n * _.repeat('*', 3);\n * // => '***'\n *\n * _.repeat('abc', 2);\n * // => 'abcabc'\n *\n * _.repeat('abc', 0);\n * // => ''\n */\n function repeat(string, n, guard) {\n if ((guard ? isIterateeCall(string, n, guard) : n === undefined)) {\n n = 1;\n } else {\n n = toInteger(n);\n }\n return baseRepeat(toString(string), n);\n }\n\n /**\n * Replaces matches for `pattern` in `string` with `replacement`.\n *\n * **Note:** This method is based on\n * [`String#replace`](https://mdn.io/String/replace).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category String\n * @param {string} [string=''] The string to modify.\n * @param {RegExp|string} pattern The pattern to replace.\n * @param {Function|string} replacement The match replacement.\n * @returns {string} Returns the modified string.\n * @example\n *\n * _.replace('Hi Fred', 'Fred', 'Barney');\n * // => 'Hi Barney'\n */\n function replace() {\n var args = arguments,\n string = toString(args[0]);\n\n return args.length < 3 ? string : string.replace(args[1], args[2]);\n }\n\n /**\n * Converts `string` to\n * [snake case](https://en.wikipedia.org/wiki/Snake_case).\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to convert.\n * @returns {string} Returns the snake cased string.\n * @example\n *\n * _.snakeCase('Foo Bar');\n * // => 'foo_bar'\n *\n * _.snakeCase('fooBar');\n * // => 'foo_bar'\n *\n * _.snakeCase('--FOO-BAR--');\n * // => 'foo_bar'\n */\n var snakeCase = createCompounder(function(result, word, index) {\n return result + (index ? '_' : '') + word.toLowerCase();\n });\n\n /**\n * Splits `string` by `separator`.\n *\n * **Note:** This method is based on\n * [`String#split`](https://mdn.io/String/split).\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category String\n * @param {string} [string=''] The string to split.\n * @param {RegExp|string} separator The separator pattern to split by.\n * @param {number} [limit] The length to truncate results to.\n * @returns {Array} Returns the string segments.\n * @example\n *\n * _.split('a-b-c', '-', 2);\n * // => ['a', 'b']\n */\n function split(string, separator, limit) {\n if (limit && typeof limit != 'number' && isIterateeCall(string, separator, limit)) {\n separator = limit = undefined;\n }\n limit = limit === undefined ? MAX_ARRAY_LENGTH : limit >>> 0;\n if (!limit) {\n return [];\n }\n string = toString(string);\n if (string && (\n typeof separator == 'string' ||\n (separator != null && !isRegExp(separator))\n )) {\n separator = baseToString(separator);\n if (!separator && hasUnicode(string)) {\n return castSlice(stringToArray(string), 0, limit);\n }\n }\n return string.split(separator, limit);\n }\n\n /**\n * Converts `string` to\n * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage).\n *\n * @static\n * @memberOf _\n * @since 3.1.0\n * @category String\n * @param {string} [string=''] The string to convert.\n * @returns {string} Returns the start cased string.\n * @example\n *\n * _.startCase('--foo-bar--');\n * // => 'Foo Bar'\n *\n * _.startCase('fooBar');\n * // => 'Foo Bar'\n *\n * _.startCase('__FOO_BAR__');\n * // => 'FOO BAR'\n */\n var startCase = createCompounder(function(result, word, index) {\n return result + (index ? ' ' : '') + upperFirst(word);\n });\n\n /**\n * Checks if `string` starts with the given target string.\n *\n * @static\n * @memberOf _\n * @since 3.0.0\n * @category String\n * @param {string} [string=''] The string to inspect.\n * @param {string} [target] The string to search for.\n * @param {number} [position=0] The position to search from.\n * @returns {boolean} Returns `true` if `string` starts with `target`,\n * else `false`.\n * @example\n *\n * _.startsWith('abc', 'a');\n * // => true\n *\n * _.startsWith('abc', 'b');\n * // => false\n *\n * _.startsWith('abc', 'b', 1);\n * // => true\n */\n function startsWith(string, target, position) {\n string = toString(string);\n position = position == null\n ? 0\n : baseClamp(toInteger(position), 0, string.length);\n\n target = baseToString(target);\n return string.slice(position, position + target.length) == target;\n }\n\n /**\n * Creates a compiled template function that can interpolate data properties\n * in \"interpolate\" delimiters, HTML-escape interpolated data properties in\n * \"escape\" delimiters, and execute JavaScript in \"evaluate\" delimiters. Data\n * properties may be accessed as free variables in the template. If a setting\n * object is given, it takes precedence over `_.templateSettings` values.\n *\n * **Note:** In the development build `_.template` utilizes\n * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl)\n * for easier debugging.\n *\n * For more information on precompiling templates see\n * [lodash's custom builds documentation](https://lodash.com/custom-builds).\n *\n * For more information on Chrome extension sandboxes see\n * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval).\n *\n * @static\n * @since 0.1.0\n * @memberOf _\n * @category String\n * @param {string} [string=''] The template string.\n * @param {Object} [options={}] The options object.\n * @param {RegExp} [options.escape=_.templateSettings.escape]\n * The HTML \"escape\" delimiter.\n * @param {RegExp} [options.evaluate=_.templateSettings.evaluate]\n * The \"evaluate\" delimiter.\n * @param {Object} [options.imports=_.templateSettings.imports]\n * An object to import into the template as free variables.\n * @param {RegExp} [options.interpolate=_.templateSettings.interpolate]\n * The \"interpolate\" delimiter.\n * @param {string} [options.sourceURL='lodash.templateSources[n]']\n * The sourceURL of the compiled template.\n * @param {string} [options.variable='obj']\n * The data object variable name.\n * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n * @returns {Function} Returns the compiled template function.\n * @example\n *\n * // Use the \"interpolate\" delimiter to create a compiled template.\n * var compiled = _.template('hello <%= user %>!');\n * compiled({ 'user': 'fred' });\n * // => 'hello fred!'\n *\n * // Use the HTML \"escape\" delimiter to escape data property values.\n * var compiled = _.template('<%- value %>');\n * compiled({ 'value': '