From fb3801f84b4b9800187c8b9b0a5f0be682f87a0b Mon Sep 17 00:00:00 2001 From: Brian Canini <canini.16@osu.edu> Date: Fri, 24 Apr 2020 11:21:23 -0400 Subject: [PATCH] Updating drupal/simple_sitemap (3.0.0 => 3.6.0) --- composer.json | 2 +- composer.lock | 23 +- vendor/composer/installed.json | 23 +- web/modules/simple_sitemap/README.md | 77 ++- web/modules/simple_sitemap/composer.json | 3 +- .../install/simple_sitemap.settings.yml | 1 + .../config/schema/simple_sitemap.schema.yml | 6 + .../css/simple_sitemap.sitemaps.css | 3 + web/modules/simple_sitemap/drupalci.yml | 23 + .../js/simple_sitemap.fieldsetSummaries.js | 22 +- .../simple_sitemap/js/simple_sitemap.form.js | 26 - .../js/simple_sitemap.sitemapEntities.js | 51 +- .../simple_sitemap_engines.settings.yml | 2 + ...map_engines.simple_sitemap_engine.bing.yml | 4 + ...p_engines.simple_sitemap_engine.google.yml | 4 + .../schema/simple_sitemap_engines.schema.yml | 33 ++ .../simple_sitemap_engines.info.yml | 14 + .../simple_sitemap_engines.install | 13 + .../simple_sitemap_engines.links.menu.yml | 5 + .../simple_sitemap_engines.links.task.yml | 17 + .../simple_sitemap_engines.module | 42 ++ .../simple_sitemap_engines.routing.yml | 15 + .../Controller/SearchEngineListBuilder.php | 89 +++ .../src/Entity/SearchEngine.php | 83 +++ .../src/Form/SimplesitemapEnginesForm.php | 170 ++++++ .../Plugin/QueueWorker/SitemapSubmitter.php | 128 +++++ .../src/Kernel/SubmitSitemapTest.php.bak | 144 +++++ .../simple_sitemap_views.views.schema.yml | 23 + .../js/simple_sitemap.viewsUi.js | 58 ++ .../simple_sitemap_views.info.yml | 15 + .../simple_sitemap_views.install | 73 +++ .../simple_sitemap_views.libraries.yml | 8 + .../simple_sitemap_views.links.task.yml | 5 + .../simple_sitemap_views.module | 24 + .../simple_sitemap_views.routing.yml | 7 + .../simple_sitemap_views.services.yml | 9 + .../SimpleSitemapViewsController.php | 94 ++++ .../src/EventSubscriber/ArgumentCollector.php | 114 ++++ .../Plugin/QueueWorker/GarbageCollector.php | 137 +++++ .../UrlGenerator/ViewsUrlGenerator.php | 265 +++++++++ .../SimpleSitemapDisplayExtender.php | 358 +++++++++++++ .../src/SimpleSitemapViews.php | 506 ++++++++++++++++++ ...ws.view.simple_sitemap_views_test_view.yml | 282 ++++++++++ .../simple_sitemap_views_test.info.yml | 13 + .../src/Functional/SimpleSitemapViewsTest.php | 157 ++++++ .../Functional/SimpleSitemapViewsTestBase.php | 93 ++++ .../simple_sitemap/simple_sitemap.api.php | 12 +- .../simple_sitemap/simple_sitemap.drush.inc | 8 +- .../simple_sitemap/simple_sitemap.info.yml | 14 +- .../simple_sitemap/simple_sitemap.install | 52 +- .../simple_sitemap.libraries.yml | 11 +- .../simple_sitemap.links.menu.yml | 18 +- .../simple_sitemap.links.task.yml | 50 +- .../simple_sitemap/simple_sitemap.module | 115 ++-- .../simple_sitemap.permissions.yml | 2 +- .../simple_sitemap/simple_sitemap.routing.yml | 33 +- .../simple_sitemap.services.yml | 12 +- .../src/Commands/SimplesitemapCommands.php | 37 +- .../Controller/SimplesitemapController.php | 54 +- .../simple_sitemap/src/EntityHelper.php | 114 ++-- .../simple_sitemap/src/Form/FormHelper.php | 373 +++++++------ .../src/Form/SimplesitemapCustomLinksForm.php | 10 +- .../src/Form/SimplesitemapEntitiesForm.php | 95 ++-- .../src/Form/SimplesitemapFormBase.php | 7 - .../src/Form/SimplesitemapSettingsForm.php | 265 ++------- .../src/Form/SimplesitemapSitemapsForm.php | 252 +++++++++ .../src/Form/SimplesitemapVariantsForm.php | 12 +- web/modules/simple_sitemap/src/Logger.php | 10 +- ....php => PathProcessorSitemapVariantIn.php} | 4 +- .../PathProcessorSitemapVariantOut.php | 27 + .../DefaultSitemapGenerator.php | 201 ++++--- .../SitemapGenerator/SitemapGeneratorBase.php | 82 ++- .../SitemapGeneratorInterface.php | 14 +- .../SitemapGenerator/SitemapWriter.php | 24 +- .../UrlGenerator/ArbitraryUrlGenerator.php | 2 + .../UrlGenerator/CustomUrlGenerator.php | 8 +- .../EntityMenuLinkContentUrlGenerator.php | 24 +- .../UrlGenerator/EntityUrlGenerator.php | 9 +- .../UrlGenerator/EntityUrlGeneratorBase.php | 51 +- .../UrlGenerator/UrlGeneratorBase.php | 3 + .../UrlGenerator/UrlGeneratorInterface.php | 8 +- .../simple_sitemap/src/Queue/BatchTrait.php | 9 +- .../simple_sitemap/src/Queue/QueueWorker.php | 60 ++- .../simple_sitemap/src/Simplesitemap.php | 176 +++--- .../src/SimplesitemapManager.php | 15 +- .../src/SimplesitemapSettings.php | 2 + .../src/Functional/SimplesitemapTest.php | 134 +++-- .../src/Functional/SimplesitemapTestBase.php | 14 + .../xsl/jquery.tablesorter.min.js | 1 + .../xsl/parser-date-iso8601.min.js | 4 + .../simple_sitemap/xsl/simple_sitemap.xsl | 192 +++++++ .../simple_sitemap/xsl/simple_sitemap.xsl.css | 91 ++++ .../simple_sitemap/xsl/simple_sitemap.xsl.js | 69 +++ 93 files changed, 4985 insertions(+), 1059 deletions(-) create mode 100644 web/modules/simple_sitemap/css/simple_sitemap.sitemaps.css create mode 100644 web/modules/simple_sitemap/drupalci.yml delete mode 100644 web/modules/simple_sitemap/js/simple_sitemap.form.js create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.settings.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.simple_sitemap_engine.bing.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.simple_sitemap_engine.google.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/config/schema/simple_sitemap_engines.schema.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.info.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.install create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.links.menu.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.links.task.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.module create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.routing.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Controller/SearchEngineListBuilder.php create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Entity/SearchEngine.php create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Form/SimplesitemapEnginesForm.php create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Plugin/QueueWorker/SitemapSubmitter.php create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_engines/tests/src/Kernel/SubmitSitemapTest.php.bak create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_views/config/schema/simple_sitemap_views.views.schema.yml create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/js/simple_sitemap.viewsUi.js create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.info.yml create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.install create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.libraries.yml create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.links.task.yml create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.module create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.routing.yml create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.services.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_views/src/Controller/SimpleSitemapViewsController.php create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/src/EventSubscriber/ArgumentCollector.php create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/QueueWorker/GarbageCollector.php create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/simple_sitemap/UrlGenerator/ViewsUrlGenerator.php create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/views/display_extender/SimpleSitemapDisplayExtender.php create mode 100755 web/modules/simple_sitemap/modules/simple_sitemap_views/src/SimpleSitemapViews.php create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/config/install/views.view.simple_sitemap_views_test_view.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/simple_sitemap_views_test.info.yml create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTest.php create mode 100644 web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTestBase.php create mode 100644 web/modules/simple_sitemap/src/Form/SimplesitemapSitemapsForm.php rename web/modules/simple_sitemap/src/PathProcessor/{PathProcessorSitemapVariant.php => PathProcessorSitemapVariantIn.php} (80%) create mode 100644 web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariantOut.php mode change 100644 => 100755 web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php mode change 100644 => 100755 web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php mode change 100644 => 100755 web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php mode change 100644 => 100755 web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php mode change 100644 => 100755 web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php create mode 100644 web/modules/simple_sitemap/xsl/jquery.tablesorter.min.js create mode 100644 web/modules/simple_sitemap/xsl/parser-date-iso8601.min.js create mode 100644 web/modules/simple_sitemap/xsl/simple_sitemap.xsl create mode 100644 web/modules/simple_sitemap/xsl/simple_sitemap.xsl.css create mode 100644 web/modules/simple_sitemap/xsl/simple_sitemap.xsl.js diff --git a/composer.json b/composer.json index 7d8e057341..eb2b8f4ccc 100644 --- a/composer.json +++ b/composer.json @@ -161,7 +161,7 @@ "drupal/search_api_db": "1.1", "drupal/simple_gmap": "2.0", "drupal/simple_megamenu": "1.0-beta3", - "drupal/simple_sitemap": "3.0", + "drupal/simple_sitemap": "3.6", "drupal/simplesamlphp_auth": "3.1", "drupal/smtp": "1.x-dev#84f789cbba894290cf82ed2558c8a4e7e24f3c89", "drupal/social_media": "1.8", diff --git a/composer.lock b/composer.lock index f0f2dc23f8..e3b40ec0fc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "39e5fe443bda96701a296238e7c184a7", + "content-hash": "58715021b0129ecbef2bc04e2432d3f1", "packages": [ { "name": "alchemy/zippy", @@ -7239,30 +7239,27 @@ }, { "name": "drupal/simple_sitemap", - "version": "3.0.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/simple_sitemap.git", - "reference": "8.x-3.0" + "reference": "8.x-3.6" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/simple_sitemap-8.x-3.0.zip", - "reference": "8.x-3.0", - "shasum": "9c3d1fb78d4693dd4f3258e8870b173650c828cd" + "url": "https://ftp.drupal.org/files/projects/simple_sitemap-8.x-3.6.zip", + "reference": "8.x-3.6", + "shasum": "37fc0ae98a4ccb23316cb089ae4839913e80cbf6" }, "require": { - "drupal/core": "~8.0", + "drupal/core": "^8 || ^9", "ext-xmlwriter": "*" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-3.x": "3.x-dev" - }, "drupal": { - "version": "8.x-3.0", - "datestamp": "1544876325", + "version": "8.x-3.6", + "datestamp": "1586469908", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7270,7 +7267,7 @@ }, "drush": { "services": { - "drush.services.yml": "^9" + "drush.services.yml": "^9 || ^10" } } }, diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 4aa7afa252..13ca36e6c0 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -7461,31 +7461,28 @@ }, { "name": "drupal/simple_sitemap", - "version": "3.0.0", - "version_normalized": "3.0.0.0", + "version": "3.6.0", + "version_normalized": "3.6.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/simple_sitemap.git", - "reference": "8.x-3.0" + "reference": "8.x-3.6" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/simple_sitemap-8.x-3.0.zip", - "reference": "8.x-3.0", - "shasum": "9c3d1fb78d4693dd4f3258e8870b173650c828cd" + "url": "https://ftp.drupal.org/files/projects/simple_sitemap-8.x-3.6.zip", + "reference": "8.x-3.6", + "shasum": "37fc0ae98a4ccb23316cb089ae4839913e80cbf6" }, "require": { - "drupal/core": "~8.0", + "drupal/core": "^8 || ^9", "ext-xmlwriter": "*" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-3.x": "3.x-dev" - }, "drupal": { - "version": "8.x-3.0", - "datestamp": "1544876325", + "version": "8.x-3.6", + "datestamp": "1586469908", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7493,7 +7490,7 @@ }, "drush": { "services": { - "drush.services.yml": "^9" + "drush.services.yml": "^9 || ^10" } } }, diff --git a/web/modules/simple_sitemap/README.md b/web/modules/simple_sitemap/README.md index 5d10a86440..8cc7b1e812 100644 --- a/web/modules/simple_sitemap/README.md +++ b/web/modules/simple_sitemap/README.md @@ -25,7 +25,7 @@ content entity types including: * ... Contributed entity types like commerce products or media entities can be indexed -as well. On top of that custom links can be added to the sitemap. +as well. On top of that custom links and view pages can be added to sitemaps. To learn about XML sitemaps, see https://en.wikipedia.org/wiki/Sitemaps. @@ -46,17 +46,18 @@ The module permission 'administer sitemap settings' can be configured under ### ENTITIES ### -Initially only the home page is indexed in the sitemap. To include content into -the sitemap, visit /admin/config/search/simplesitemap/entities to enable support -for entity types of your choosing. Entity types which feature bundles can then -be configured on a per-bundle basis, e.g. +Initially only the home page is indexed in the default sitemap variant. To +include content into a sitemap, visit +/admin/config/search/simplesitemap/entities to enable support for entity types +of your choosing. Bundleless entity types can be configured right on that page, +for bundles of entity types visit the bundle's configuration pages, e.g. * /admin/structure/types/manage/[content type] for nodes * /admin/structure/taxonomy/manage/[taxonomy vocabulary] for taxonomy terms * /admin/structure/menu/manage/[menu] for menu items * ... -When including an entity type or bundle into the sitemap, the priority setting +When including an entity type or bundle into a sitemap, the priority setting can be set which will set the 'priority' parameter for all entities of that type. Same goes for the 'changefreq' setting. All Images referenced by the entities can be indexed as well. See https://en.wikipedia.org/wiki/Sitemaps to @@ -70,13 +71,24 @@ If you wish for the sitemap to reflect the new configuration instantly, check 'Regenerate sitemaps after clicking save'. This setting only appears if a change in the settings has been detected. -As the sitemap is accessible to anonymous users, bear in mind that only links +Once variants are set up in admin/config/search/simplesitemap/variants, all the +above settings can be configured and overwritten on a per variant basis right +from the UI. + +As the sitemaps are accessible to anonymous users, bear in mind that only links will be included which are accessible to anonymous users. There are no access checks for links added through the module's hooks (see below). +### VIEWS ### + +To index views, enable the included, optional module Simple XML Sitemap (Views) +(simple_sitemap_views). Simple views as well as views with arguments can be +indexed on the view edit page. For views with arguments, links to all view +variants will be included in the sitemap. + ### CUSTOM LINKS ### -To include custom links into the sitemap, visit +To include custom links into a sitemap, visit /admin/config/search/simplesitemap/custom. ### SETTINGS ### @@ -90,10 +102,19 @@ It is possible to have several sitemap instances of different sitemap types with specific links accessible under certain URLs. These sitemap variants can be configured under admin/config/search/simplesitemap/variants. +#### AUTOMATIC SUBMISSION #### + +It is possible to have the module automatically submit specific sitemap +variants to search engines. Google and Bing are preconfigured. This +functionality is available through the included simple_sitemap_engines +submodule. After enabling this module, go to +admin/config/search/simplesitemap/engines/settings to set it up. + ## USAGE ## The sitemaps are accessible to the whole world under [variant name]/sitemap.xml. -In addition to that, the default sitemap is accessible under /sitemap.xml. +In addition to that, the default sitemap is accessible under /sitemap.xml. To +view the XML source, press ctrl+u. If the cron generation is turned on, the sitemaps will be regenerated according to the 'Sitemap generation interval' setting. @@ -101,9 +122,15 @@ to the 'Sitemap generation interval' setting. A manual generation is possible on admin/config/search/simplesitemap. This is also the place that shows the overall and variant specific generation status. -The sitemap can be also generated via drush: Use the command -'drush simple-sitemap:generate' ('ssg'), or 'drush simple-sitemap:rebuild-queue' -('ssr'). +The sitemap can be also generated via drush: + * `simple-sitemap:generate` or `ssg`: Generates the sitemap (continues + generating from queue, or rebuilds queue for all variants beforehand if + nothing is queued). + + * `simple-sitemap:rebuild-queue` or `ssr`: Deletes queue and queues elements + for all or specific sitemap variants. Add `--variants` flag and specify a + comma separated list of variants if wanting to queue only specific variants + for the upcoming generation. Generation of hundreds of thousands of links can take time. Each variant gets published as soon as all of its links have been generated. The previous version @@ -121,14 +148,14 @@ programmatic sitemap generation. These include: * setVariants * getSitemap * removeSitemap - * generateSitemap + * queue * rebuildQueue + * generateSitemap * enableEntityType * disableEntityType * setBundleSettings * getBundleSettings * removeBundleSettings - * supplementDefaultSettings * setEntityInstanceSettings * getEntityInstanceSettings * removeEntityInstanceSettings @@ -142,11 +169,7 @@ programmatic sitemap generation. These include: * addSitemapVariant * removeSitemapVariants * getQueueWorker - * deleteQueue - * rebuildQueue * getInitialElementCount - * getQueuedElementCount - * getStashedResultCount * getProcessedElementCount * generationInProgress @@ -164,9 +187,9 @@ $generator ->saveSetting('remove_duplicates', TRUE) ->enableEntityType('node') ->setVariants(['default', 'test']) - ->setBundleSettings('node', 'page', ['index' => TRUE, 'priority' = 0.5]) + ->setBundleSettings('node', 'page', ['index' => TRUE, 'priority' => 0.5]) ->removeCustomLinks() - ->addCustomLink('/some/view/page', ['priority' = 0.5]) + ->addCustomLink('/some/view/page', ['priority' => 0.5]) ->generateSitemap(); ``` @@ -176,26 +199,26 @@ Drupal\simple_sitemap\Simplesitemap for further details. ### API HOOKS ### It is possible to hook into link generation by implementing -`hook_simple_sitemap_links_alter(&$links){}` in a custom module and altering the +`hook_simple_sitemap_links_alter(&$links, $sitemap_variant){}` in a custom module and altering the link array shortly before it is transformed to XML. Adding arbitrary links is possible through the use of -`hook_simple_sitemap_arbitrary_links_alter(&$arbitrary_links){}`. There are no +`hook_simple_sitemap_arbitrary_links_alter(&$arbitrary_links, $sitemap_variant){}`. There are no checks performed on these links (i.e. if they are internal/valid/accessible) and parameters like priority/lastmod/changefreq have to be added manually. Altering sitemap attributes and sitemap index attributes is possible through the -use of `hook_simple_sitemap_attributes_alter(&$attributes){}` and -`hook_simple_sitemap_index_attributes_alter(&$index_attributes){}`. +use of `hook_simple_sitemap_attributes_alter(&$attributes, $sitemap_variant){}` and +`hook_simple_sitemap_index_attributes_alter(&$index_attributes, $sitemap_variant){}`. Altering URL generators is possible through -the use of `hook_simple_sitemap_url_generators_alter(&$generators){}`. +the use of `hook_simple_sitemap_url_generators_alter(&$url_generators){}`. Altering sitemap generators is possible through -the use of `hook_simple_sitemap_sitemap_generators_alter(&$generators){}`. +the use of `hook_simple_sitemap_sitemap_generators_alter(&$sitemap_generators){}`. Altering sitemap types is possible through -the use of `hook_simple_sitemap_sitemap_types_alter(&$generators){}`. +the use of `hook_simple_sitemap_sitemap_types_alter(&$sitemap_types){}`. ### WRITING PLUGINS ### diff --git a/web/modules/simple_sitemap/composer.json b/web/modules/simple_sitemap/composer.json index f8c19b9344..2f92ef5cb0 100644 --- a/web/modules/simple_sitemap/composer.json +++ b/web/modules/simple_sitemap/composer.json @@ -24,9 +24,8 @@ "extra": { "drush": { "services": { - "drush.services.yml": "^9" + "drush.services.yml": "^9 || ^10" } } } } - diff --git a/web/modules/simple_sitemap/config/install/simple_sitemap.settings.yml b/web/modules/simple_sitemap/config/install/simple_sitemap.settings.yml index 1d4c8e0bb4..bf7fac13cc 100644 --- a/web/modules/simple_sitemap/config/install/simple_sitemap.settings.yml +++ b/web/modules/simple_sitemap/config/install/simple_sitemap.settings.yml @@ -4,6 +4,7 @@ cron_generate_interval: 0 generate_duration: 10000 remove_duplicates: true skip_untranslated: true +xsl: true base_url: '' default_variant: 'default' custom_links_include_images: false diff --git a/web/modules/simple_sitemap/config/schema/simple_sitemap.schema.yml b/web/modules/simple_sitemap/config/schema/simple_sitemap.schema.yml index 9fd7712459..aea1d49f1e 100644 --- a/web/modules/simple_sitemap/config/schema/simple_sitemap.schema.yml +++ b/web/modules/simple_sitemap/config/schema/simple_sitemap.schema.yml @@ -19,6 +19,12 @@ simple_sitemap.settings: skip_untranslated: label: 'Skip untranslated' type: boolean + disable_language_hreflang: + label: 'Disable language hreflang' + type: boolean + xsl: + label: 'Include a stylesheet in the sitemaps for humans' + type: boolean base_url: label: 'Base URL' type: string diff --git a/web/modules/simple_sitemap/css/simple_sitemap.sitemaps.css b/web/modules/simple_sitemap/css/simple_sitemap.sitemaps.css new file mode 100644 index 0000000000..7d98ac4842 --- /dev/null +++ b/web/modules/simple_sitemap/css/simple_sitemap.sitemaps.css @@ -0,0 +1,3 @@ +#simple-sitemap-sitemaps-form .progress__bar { + background-image: none; +} diff --git a/web/modules/simple_sitemap/drupalci.yml b/web/modules/simple_sitemap/drupalci.yml new file mode 100644 index 0000000000..089478535f --- /dev/null +++ b/web/modules/simple_sitemap/drupalci.yml @@ -0,0 +1,23 @@ +build: + assessment: + validate_codebase: + phplint: + container_composer: + phpcs: + # phpcs will use core's specified version of Coder. + sniff-all-files: true + halt-on-fail: false + testing: + # run_tests task is executed several times in order of performance speeds. + # halt-on-fail can be set on the run_tests tasks in order to fail fast. + # suppress-deprecations is false in order to be alerted to usages of + # deprecated code. + run_tests.standard: + types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional' + testgroups: '--all' + suppress-deprecations: false + run_tests.js: + types: 'PHPUnit-FunctionalJavascript' + testgroups: '--all' + suppress-deprecations: false + nightwatchjs: { } diff --git a/web/modules/simple_sitemap/js/simple_sitemap.fieldsetSummaries.js b/web/modules/simple_sitemap/js/simple_sitemap.fieldsetSummaries.js index 1963294aac..7de0e47622 100644 --- a/web/modules/simple_sitemap/js/simple_sitemap.fieldsetSummaries.js +++ b/web/modules/simple_sitemap/js/simple_sitemap.fieldsetSummaries.js @@ -7,20 +7,22 @@ "use strict"; Drupal.behaviors.simple_sitemapFieldsetSummaries = { - attach: function(context) { + attach: function(context, settings) { $(context).find('#edit-simple-sitemap').drupalSetSummary(function(context) { - var vals = []; - if ($(context).find('#edit-simple-sitemap-index-content-1').is(':checked')) { - vals.push(Drupal.t('Included in sitemap')); - vals.push(Drupal.t('Variant') + ': ' + $('#edit-simple-sitemap-variant option:selected', context).text()); - vals.push(Drupal.t('Priority') + ': ' + $('#edit-simple-sitemap-priority option:selected', context).text()); - vals.push(Drupal.t('Change frequency') + ': ' + $('#edit-simple-sitemap-changefreq option:selected', context).text()); - vals.push(Drupal.t('Include images') + ': ' + $('#edit-simple-sitemap-include-images option:selected', context).text()); + var enabledVariants = []; + $('input:radio.enabled-for-variant').each(function() { + if ($(this).is(':checked') && $(this).val() == 1) { + enabledVariants.push($(this).attr('class').split(' ')[1]) + } + }); + + if (enabledVariants.length > 0) { + return Drupal.t('Included in sitemap variants: ') + enabledVariants.join(', '); } else { - vals.push(Drupal.t('Excluded from sitemap')); + return Drupal.t('Excluded from all sitemap variants'); } - return vals.join('<br />'); + }); } }; diff --git a/web/modules/simple_sitemap/js/simple_sitemap.form.js b/web/modules/simple_sitemap/js/simple_sitemap.form.js deleted file mode 100644 index d4f24fa4bd..0000000000 --- a/web/modules/simple_sitemap/js/simple_sitemap.form.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @file - * Attaches simple_sitemap behaviors to the entity form. - */ -(function($) { - - "use strict"; - - Drupal.behaviors.simple_sitemapForm = { - attach: function(context) { - - // On load: Hide the 'Regenerate sitemap' field to only display it if settings have changed. - $('.form-item-simple-sitemap-regenerate-now').hide(); - - // Show 'Regenerate sitemap' field if settings have changed. - $("#edit-simple-sitemap-index-content" - + ", #edit-simple-sitemap-variant" - + ", #edit-simple-sitemap-priority" - + ", #edit-simple-sitemap-changefreq" - + ", #edit-simple-sitemap-include-images" - ).change(function() { - $('.form-item-simple-sitemap-regenerate-now').show(); - }); - } - }; -})(jQuery); diff --git a/web/modules/simple_sitemap/js/simple_sitemap.sitemapEntities.js b/web/modules/simple_sitemap/js/simple_sitemap.sitemapEntities.js index abfe5fb1f0..0c4984ed80 100644 --- a/web/modules/simple_sitemap/js/simple_sitemap.sitemapEntities.js +++ b/web/modules/simple_sitemap/js/simple_sitemap.sitemapEntities.js @@ -8,46 +8,25 @@ Drupal.behaviors.simple_sitemapSitemapEntities = { attach: function(context, settings) { - var allEntities = settings.simple_sitemap.all_entities; - var atomicEntities = settings.simple_sitemap.atomic_entities; + $.each(settings.simple_sitemap.all_entities, function(index, entityId) { + var target = '#edit-' + entityId + '-enabled'; + triggerVisibility(target, entityId); - // Hide the 'Regenerate sitemap' field to only display it if settings have changed. - $('.form-item-simple-sitemap-regenerate-now').hide(); - - $.each(allEntities, function(index, value) { - - // On load: hide all warning messages. - $('#warning-' + value).hide(); - - // On change: Show or hide warning message dependent on 'enabled' checkbox. - var enabledId = '#edit-' + value + '-enabled'; - $(enabledId).change(function() { - if ($(enabledId).is(':checked')) { - $('#warning-' + value).hide(); - $('#indexed-bundles-' + value).show(); - } - else { - $('#warning-' + value).show(); - $('#indexed-bundles-' + value).hide(); - } - - // Show 'Regenerate sitemap' field if 'enabled' setting has changed. - $('.form-item-simple-sitemap-regenerate-now').show(); + $(target).change(function() { + triggerVisibility(target, entityId); }); }); - // todo - // Show 'Regenerate sitemap' field if settings have changed. - // $.each(atomicEntities, function(index, value) { - // var variant = '.form-item-' + value + '-simple-sitemap-variant'; - // var priorityId = '.form-item-' + value + '-simple-sitemap-priority'; - // var changefreqId = '.form-item-' + value + '-simple-sitemap-changefreq'; - // var includeImagesId = '.form-item-' + value + '-simple-sitemap-include-images'; - // - // $(variant, priorityId, changefreqId, includeImagesId).change(function() { - // $('.form-item-simple-sitemap-regenerate-now').show(); - // }); - // }); + function triggerVisibility(target, entityId) { + if ($(target).is(':checked')) { + $('#warning-' + entityId).hide(); + $('#indexed-bundles-' + entityId).show(); + } + else { + $('#warning-' + entityId).show(); + $('#indexed-bundles-' + entityId).hide(); + } + } } }; })(jQuery); diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.settings.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.settings.yml new file mode 100644 index 0000000000..4c544c1d14 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.settings.yml @@ -0,0 +1,2 @@ +enabled: true +submission_interval: 86400 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.simple_sitemap_engine.bing.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.simple_sitemap_engine.bing.yml new file mode 100644 index 0000000000..3c03ba1f95 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.simple_sitemap_engine.bing.yml @@ -0,0 +1,4 @@ +id: bing +label: 'Bing' +url: https://www.bing.com/ping?sitemap=[sitemap] +sitemap_variants: { } diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.simple_sitemap_engine.google.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.simple_sitemap_engine.google.yml new file mode 100644 index 0000000000..30abc518e1 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/install/simple_sitemap_engines.simple_sitemap_engine.google.yml @@ -0,0 +1,4 @@ +id: google +label: 'Google' +url: https://www.google.com/ping?sitemap=[sitemap] +sitemap_variants: { } diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/schema/simple_sitemap_engines.schema.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/schema/simple_sitemap_engines.schema.yml new file mode 100644 index 0000000000..9b70101fd6 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/config/schema/simple_sitemap_engines.schema.yml @@ -0,0 +1,33 @@ +simple_sitemap_engines.simple_sitemap_engine.*: + type: config_entity + label: 'Search engine' + mapping: + id: + type: string + label: 'Search engine ID' + label: + type: label + label: 'Label' + url: + type: string + label: 'Submission URL' + sitemap_variants: + type: sequence + label: 'Sitemap variants' + sequence: + type: string + label: 'Sitemap variant' + last_submitted: + type: integer + label: 'Last submitted' + +simple_sitemap_engines.settings: + type: config_object + label: 'Sitemap search engine submission settings' + mapping: + enabled: + type: boolean + label: 'Sitemap submission enabled' + submission_interval: + type: integer + label: 'Sitemap submission frequency' diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.info.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.info.yml new file mode 100644 index 0000000000..b8ff3f6eb8 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.info.yml @@ -0,0 +1,14 @@ +name: 'Simple XML Sitemap (Search engines)' +type: module +description: 'Submits sitemaps to search engines.' +configure: simple_sitemap_engines.settings +package: SEO +core: 8.x +core_version_requirement: ^8 || ^9 +dependencies: + - simple_sitemap:simple_sitemap + +# Information added by Drupal.org packaging script on 2020-04-09 +version: '8.x-3.6' +project: 'simple_sitemap' +datestamp: 1586468195 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.install b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.install new file mode 100644 index 0000000000..9c17504708 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.install @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Module install and update procedures. + */ + +/** + * Implements hook_uninstall(). + */ +function simple_sitemap_engines_uninstall() { + \Drupal::service('state')->delete('simple_sitemap_engines_last_submitted'); +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.links.menu.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.links.menu.yml new file mode 100644 index 0000000000..00bfdcfa3b --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.links.menu.yml @@ -0,0 +1,5 @@ +simple_sitemap.engines: + title: 'Search engines' + parent: simple_sitemap.sitemaps + route_name: simple_sitemap.engines.status + weight: 2 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.links.task.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.links.task.yml new file mode 100644 index 0000000000..7653fa5842 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.links.task.yml @@ -0,0 +1,17 @@ +simple_sitemap.engines: + route_name: simple_sitemap.engines.status + title: 'Search engines' + base_route: simple_sitemap.sitemaps + weight: 2 + +simple_sitemap.engines.status: + route_name: simple_sitemap.engines.status + title: 'Status' + parent_id: simple_sitemap.engines + weight: 0 + +simple_sitemap.engines.settings: + route_name: simple_sitemap.engines.settings + title: 'Settings' + parent_id: simple_sitemap.engines + weight: 1 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.module b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.module new file mode 100644 index 0000000000..855d3211f5 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.module @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Submits sitemaps to search engines. + */ + +/** + * Implements hook_cron(). + * + * If the sitemap submission interval has elapsed, adds each search engine to + * the submission queue to be processed. + * + * @see Drupal\simple_sitemap_engines\Plugin\QueueWorker\SitemapSubmitter + */ +function simple_sitemap_engines_cron() { + + /** @var \Drupal\Core\Config\Config $config */ + $config = \Drupal::config('simple_sitemap_engines.settings'); + + if ($config->get('enabled')) { + $interval = (int) $config->get('submission_interval') * 60 * 60; + $request_time = \Drupal::service('datetime.time')->getRequestTime(); + $state = \Drupal::state(); + + if ($interval === 0 + || $state->get('simple_sitemap_engines_last_submitted', 0) + $interval <= $request_time) { + + /** @var \Drupal\Core\Queue\QueueInterface $queue */ + $queue = \Drupal::queue('simple_sitemap_engine_submit'); + + $state->set('simple_sitemap_engines_last_submitted', $request_time); + foreach (\Drupal::entityTypeManager() + ->getStorage('simple_sitemap_engine') + ->loadMultiple() as $id => $engine) { + if (!empty($engine->sitemap_variants)) { + $queue->createItem($id); + } + } + } + } +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.routing.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.routing.yml new file mode 100644 index 0000000000..d5ca0dcb1b --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.routing.yml @@ -0,0 +1,15 @@ +simple_sitemap.engines.status: + path: '/admin/config/search/simplesitemap/engines' + defaults: + _entity_list: 'simple_sitemap_engine' + _title: 'Simple XML Sitemap' + requirements: + _permission: 'administer sitemap settings' + +simple_sitemap.engines.settings: + path: '/admin/config/search/simplesitemap/engines/settings' + defaults: + _form: '\Drupal\simple_sitemap_engines\Form\SimplesitemapEnginesForm' + _title: 'Simple XML Sitemap' + requirements: + _permission: 'administer sitemap settings' diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Controller/SearchEngineListBuilder.php b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Controller/SearchEngineListBuilder.php new file mode 100644 index 0000000000..8ce2b2fc10 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Controller/SearchEngineListBuilder.php @@ -0,0 +1,89 @@ +<?php + +namespace Drupal\simple_sitemap_engines\Controller; + +use Drupal\Core\Config\Entity\ConfigEntityListBuilder; +use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\simple_sitemap\Form\FormHelper; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Search engine entity list builder. + */ +class SearchEngineListBuilder extends ConfigEntityListBuilder { + + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + + /** + * SearchEngineListBuilder constructor. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The entity storage class. + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter service. + */ + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter) { + parent::__construct($entity_type, $storage); + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity_type.manager')->getStorage($entity_type->id()), + $container->get('date.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = $this->t('Name'); + $header['url'] = $this->t('Submission URL'); + $header['variants'] = $this->t('Sitemap variants'); + $header['last_submitted'] = $this->t('Last submitted'); + + return $header; + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + + /** @var \Drupal\simple_sitemap_engines\Entity\SearchEngine $entity */ + $row['label'] = $entity->label(); + $row['url'] = $entity->url; + $row['variants'] = implode(', ', $entity->sitemap_variants); + $row['last_submitted'] = $entity->last_submitted + ? $this->dateFormatter->format($entity->last_submitted, 'short') + : $this->t('Never'); + + return $row; + } + + public function render() { + return ['simple_sitemap_engines' => [ + '#prefix' => FormHelper::getDonationText(), + '#title' => $this->t('Submission status'), + '#type' => 'fieldset', + 'table' => parent::render(), + '#description' => $this->t('Submission settings can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/engines/settings']), + ]]; + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Entity/SearchEngine.php b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Entity/SearchEngine.php new file mode 100644 index 0000000000..0b1c9a7871 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Entity/SearchEngine.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\simple_sitemap_engines\Entity; + +use Drupal\Core\Config\Entity\ConfigEntityBase; + +/** + * Defines the the search engine entity class. + * + * @ConfigEntityType( + * id = "simple_sitemap_engine", + * label = @Translation("Search engine"), + * admin_permission = "administer sitemap settings", + * entity_keys = { + * "id" = "id", + * "label" = "label", + * }, + * handlers = { + * "list_builder" = "Drupal\simple_sitemap_engines\Controller\SearchEngineListBuilder", + * }, + * links = { + * "collection" = "/admin/config/search/simplesitemap/engines/list", + * }, + * config_export = { + * "id", + * "label", + * "url", + * "sitemap_variants", + * "last_submitted", + * } + * ) + */ +class SearchEngine extends ConfigEntityBase { + + /** + * The search engine ID. + * + * @var string + */ + public $id; + + /** + * The search engine label. + * + * @var string + */ + public $label; + + /** + * The search engine submission URL. + * + * When submitting to search engines, '[sitemap]' will be replaced with the + * full URL to the sitemap.xml. + * + * @var string + */ + public $url; + + /** + * List of sitemap variants to be submitted to this search engine. + * + * @var array + */ + public $sitemap_variants; + + /** + * Timestamp when the sitemap was last submitted to this search engine. + * + * @var int + */ + public $last_submitted; + + /** + * Implements magic __toString() to simplify checkbox list building. + * + * @return string + * The search engine label. + */ + public function __toString() { + return $this->label(); + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Form/SimplesitemapEnginesForm.php b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Form/SimplesitemapEnginesForm.php new file mode 100644 index 0000000000..5241ad40a0 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Form/SimplesitemapEnginesForm.php @@ -0,0 +1,170 @@ +<?php + +namespace Drupal\simple_sitemap_engines\Form; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Datetime\DateFormatter; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\simple_sitemap\Form\FormHelper; +use Drupal\simple_sitemap\SimplesitemapManager; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Form for managing search engine submission settings. + */ +class SimplesitemapEnginesForm extends ConfigFormBase { + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatter + */ + protected $dateFormatter; + + /** + * The sitemap manager service. + * + * @var \Drupal\simple_sitemap\SimplesitemapManager + */ + protected $sitemapManager; + + /** + * SimplesitemapEnginesForm constructor. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + * @param \Drupal\Core\Datetime\DateFormatter $date_formatter + * The date formatter service. + * @param \Drupal\simple_sitemap\SimplesitemapManager $sitemap_manager + * The sitemap manager service. + */ + public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, DateFormatter $date_formatter, SimplesitemapManager $sitemap_manager) { + parent::__construct($config_factory); + $this->entityTypeManager = $entity_type_manager; + $this->dateFormatter = $date_formatter; + $this->sitemapManager = $sitemap_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('entity_type.manager'), + $container->get('date.formatter'), + $container->get('simple_sitemap.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'simple_sitemap_engines_settings_form'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['simple_sitemap_engines.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('simple_sitemap_engines.settings'); + + $form['#tree'] = TRUE; + + $form['settings'] = [ + '#type' => 'fieldset', + '#title' => $this->t('General submission settings'), + '#prefix' => FormHelper::getDonationText(), + ]; + + $form['settings']['enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Submit the sitemap to search engines'), + '#description' => $this->t('This enables/disables sitemap submitting; don\'t forget to choose variants below.'), + '#default_value' => $config->get('enabled'), + ]; + + $form['settings']['submission_interval'] = [ + '#type' => 'select', + '#title' => $this->t('Submission interval'), + '#options' => FormHelper::getCronIntervalOptions(), + '#default_value' => $config->get('submission_interval'), + '#states' => [ + 'visible' => [':input[name="settings[enabled]"]' => ['checked' => TRUE]], + ], + ]; + + $form['engines'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Variant specific settings'), + '#markup' => '<div class="description">' . $this->t('Choose which sitemap variants are to be submitted to which search engines.<br>Variants can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/variants']) . '</div>', + ]; + + $engines = $this->entityTypeManager->getStorage('simple_sitemap_engine')->loadMultiple(); + foreach ($engines as $engine_id => $engine) { + $form['engines'][$engine_id] = [ + '#type' => 'details', + '#title' => $engine->label(), + '#open' => !empty($engine->sitemap_variants) || count($engines) === 1, + ]; + $form['engines'][$engine_id]['variants'] = [ + '#type' => 'select', + '#title' => $this->t('Sitemap variants'), + '#options' => array_map( + function ($variant) { return $this->t($variant['label']); }, + $this->sitemapManager->getSitemapVariants(NULL, FALSE) + ), + '#default_value' => $engine->sitemap_variants, + '#multiple' => TRUE, + ]; + } + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + foreach ($this->entityTypeManager->getStorage('simple_sitemap_engine')->loadMultiple() as $engine_id => $engine) { + if (!empty($values = $form_state->getValue(['engines', $engine_id, 'variants']))) { + $submit = TRUE; + } + $engine->sitemap_variants = $values; + $engine->save(); + } + + $config = $this->config('simple_sitemap_engines.settings'); + + $enabled = (bool) $form_state->getValue(['settings', 'enabled']); + $config->set('enabled', $enabled); + $config->set('submission_interval', $form_state->getValue(['settings', 'submission_interval'])); + $config->save(); + + if ($enabled && empty($submit)) { + $this->messenger()->addWarning($this->t('No sitemap variants have been selected for submission.')); + } + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Plugin/QueueWorker/SitemapSubmitter.php b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Plugin/QueueWorker/SitemapSubmitter.php new file mode 100644 index 0000000000..5160b2cec3 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Plugin/QueueWorker/SitemapSubmitter.php @@ -0,0 +1,128 @@ +<?php + +namespace Drupal\simple_sitemap_engines\Plugin\QueueWorker; + +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Queue\QueueWorkerBase; +use Drupal\simple_sitemap\Simplesitemap; +use Drupal\simple_sitemap\Logger; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\RequestException; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Process a queue of search engines to submit sitemaps. + * + * @QueueWorker( + * id = "simple_sitemap_engine_submit", + * title = @Translation("Sitemap search engine submission"), + * cron = {"time" = 30} + * ) + * + * @see simple_sitemap_engines_cron() + */ +class SitemapSubmitter extends QueueWorkerBase implements ContainerFactoryPluginInterface { + + /** + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $engineStorage; + + /** + * @var \GuzzleHttp\ClientInterface + */ + protected $httpClient; + + /** + * @var \Drupal\simple_sitemap\Simplesitemap + */ + protected $generator; + + /** + * @var \Drupal\simple_sitemap\Logger + */ + protected $logger; + + /** + * SitemapSubmitter constructor. + * @param array $configuration + * @param $plugin_id + * @param $plugin_definition + * @param \Drupal\Core\Entity\EntityStorageInterface $engine_storage + * @param \GuzzleHttp\ClientInterface $http_client + * @param \Drupal\simple_sitemap\Simplesitemap $generator + * @param \Drupal\simple_sitemap\Logger $logger + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $engine_storage, ClientInterface $http_client, Simplesitemap $generator, Logger $logger) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->engineStorage = $engine_storage; + $this->httpClient = $http_client; + $this->generator = $generator; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager')->getStorage('simple_sitemap_engine'), + $container->get('http_client'), + $container->get('simple_sitemap.generator'), + $container->get('simple_sitemap.logger') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($engine_id) { + /** @var \Drupal\simple_sitemap_engines\Entity\SearchEngine $engine */ + if ($engine = $this->engineStorage->load($engine_id)) { + + $sitemap_urls = []; + $manager = $this->generator->getSitemapManager(); + + foreach ($manager->getSitemapTypes() as $type_name => $type_definition) { + $sitemap_generator = $manager->getSitemapGenerator($type_definition['sitemapGenerator']); + + // Submit all variants that are enabled for this search engine. + foreach ($manager->getSitemapVariants($type_name, FALSE) as $variant_id => $variant_definition) { + if (in_array($variant_id, $engine->sitemap_variants) + && FALSE !== $this->generator->setVariants($variant_id)->getSitemap()) { + $sitemap_urls[$variant_definition['label']] = $sitemap_generator->setSitemapVariant($variant_id)->getSitemapUrl(); + } + } + } + + // Submit all URLs. + foreach ($sitemap_urls as $variant => $sitemap_url) { + $submit_url = str_replace('[sitemap]', $sitemap_url, $engine->url); + try { + $this->httpClient->request('GET', $submit_url); + // Log if submission was successful. + $this->logger->m('Sitemap @variant submitted to @url', ['@variant' => $variant, '@url' => $submit_url])->log(); + // Record last submission time. This is purely informational; the + // variable that determines when the next submission should be run is + // stored in the global state. + $engine->last_submitted = time(); + } + catch (RequestException $e) { + // Catch and log exceptions so this submission gets removed from the + // queue whether or not it succeeded. + // If the error was caused by network failure, it's fine to just wait + // until next time the submission is queued to try again. + // If the error was caused by a malformed URL, keeping the submission + // in the queue to retry is pointless since it will always fail. + watchdog_exception('simple_sitemap', $e); + } + } + $engine->save(); + } + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/tests/src/Kernel/SubmitSitemapTest.php.bak b/web/modules/simple_sitemap/modules/simple_sitemap_engines/tests/src/Kernel/SubmitSitemapTest.php.bak new file mode 100644 index 0000000000..e189d32bb2 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/tests/src/Kernel/SubmitSitemapTest.php.bak @@ -0,0 +1,144 @@ +<?php + +namespace Drupal\Tests\simple_sitemap_engines\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\RequestException; +use Prophecy\Argument; + +// phpcs:disable Drupal.Arrays.Array.LongLineDeclaration + +/** + * Tests search engine sitemap submission. + * + * @group simple_sitemap_engines + */ +class SubmitSitemapTest extends KernelTestBase { + + /** + * The modules to enable. + * + * @var array + */ + public static $modules = ['system', 'simple_sitemap', 'simple_sitemap_engines']; + + /** + * The cron service. + * + * @var \Drupal\Core\Cron + */ + protected $cron; + + /** + * The search engine entity storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $engineStorage; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('simple_sitemap_engine'); + $this->installConfig('simple_sitemap'); + $this->installConfig('simple_sitemap_engines'); + + $this->cron = \Drupal::service('cron'); + $this->engineStorage = \Drupal::entityTypeManager()->getStorage('simple_sitemap_engine'); + $this->queue = \Drupal::queue('simple_sitemap_engine_submit'); + + // Set Google to submit the default sitemap variant. Other search engines + // will not submit anything. + $google = $this->engineStorage->load('google'); + $google->sitemap_variants = ['default']; + $google->save(); + } + + /** + * Tests sitemap submission URLs and last submission status. + */ + public function testSubmission() { + // Create a mock HTTP client. + $http_client = $this->prophesize(ClientInterface::class); + // Make mock HTTP requests always succeed. + $http_client->request('GET', Argument::any())->willReturn(TRUE); + // Replace the default HTTP client service with the mock. + $this->container->set('http_client', $http_client->reveal()); + + // Run cron to trigger submission. + $this->cron->run(); + + $google = $this->engineStorage->load('google'); + $bing = $this->engineStorage->load('bing'); + + // Check that Google was marked as submitted and Bing was not. + $this->assertNotEmpty($google->last_submitted); + $this->assertEmpty($bing->last_submitted); + + // Check that exactly 1 HTTP request was sent to the correct URL. + $http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalled(); + $http_client->request('GET', Argument::any())->shouldBeCalledTimes(1); + } + + /** + * Tests that sitemaps are not submitted every time cron runs. + */ + public function testNoDoubleSubmission() { + // Create a mock HTTP client. + $http_client = $this->prophesize(ClientInterface::class); + // Make mock HTTP requests always succeed. + $http_client->request('GET', Argument::any())->willReturn(TRUE); + // Replace the default HTTP client service with the mock. + $this->container->set('http_client', $http_client->reveal()); + + // Run cron to trigger submission. + $this->cron->run(); + + // Check that Google was submitted and store its last submitted time. + $google = $this->engineStorage->load('google'); + $http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalledTimes(1); + $this->assertNotEmpty($google->last_submitted); + $google_last_submitted = $google->last_submitted; + + // Make sure enough time passes between cron runs to guarantee that they + // do not run within the same second, since timestamps are compared below. + sleep(2); + $this->cron->run(); + $google = $this->engineStorage->load('google'); + + // Check that the last submitted time was not updated on the second cron + // run. + $this->assertEquals($google->last_submitted, $google_last_submitted); + // Check that no duplicate request was sent. + $http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalledTimes(1); + } + + /** + * Tests that failed sitemap submissions are handled properly. + */ + public function testFailedSubmission() { + // Create a mock HTTP client. + $http_client = $this->prophesize(ClientInterface::class); + // Make mock HTTP requests always fail. + $http_client->request('GET', Argument::any())->willThrow(RequestException::class); + // Replace the default HTTP client service with the mock. + $this->container->set('http_client', $http_client->reveal()); + + // Run cron to trigger submission. + $this->cron->run(); + + $google = $this->engineStorage->load('google'); + + // Check that one request was attempted. + $http_client->request('GET', Argument::any())->shouldBeCalledTimes(1); + // Check the last submission time is still empty. + $this->assertEmpty($google->last_submitted); + // Check that the submission was removed from the queue despite failure. + $this->assertEquals(0, $this->queue->numberOfItems()); + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/config/schema/simple_sitemap_views.views.schema.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/config/schema/simple_sitemap_views.views.schema.yml new file mode 100644 index 0000000000..99a96c90fe --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/config/schema/simple_sitemap_views.views.schema.yml @@ -0,0 +1,23 @@ +views.display_extender.simple_sitemap_display_extender: + type: views_display_extender + mapping: + index: + label: 'Index' + type: boolean + variant: + label: 'Sitemap variant' + type: string + priority: + label: 'Priority' + type: string + changefreq: + label: 'Change frequency' + type: string + arguments: + label: 'Indexed arguments' + type: sequence + sequence: + type: string + max_links: + label: 'Max links' + type: integer diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/js/simple_sitemap.viewsUi.js b/web/modules/simple_sitemap/modules/simple_sitemap_views/js/simple_sitemap.viewsUi.js new file mode 100755 index 0000000000..503eee1fdb --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/js/simple_sitemap.viewsUi.js @@ -0,0 +1,58 @@ +/** + * @file + * Views UI helpers for Simple XML Sitemap display extender. + */ + +(function ($, Drupal) { + Drupal.simpleSitemapViewsUi = {}; + + Drupal.behaviors.simpleSitemapViewsUiCheckboxify = { + attach: function attach() { + var $button = $('[data-drupal-selector="edit-index-button"]').once('simple-sitemap-views-ui-checkboxify'); + if ($button.length) { + new Drupal.simpleSitemapViewsUi.Checkboxifier($button); + } + } + }; + + Drupal.behaviors.simpleSitemapViewsUiArguments = { + attach: function attach() { + var $arguments = $('.indexed-arguments').once('simple-sitemap-views-ui-arguments'); + var $checkboxes = $arguments.find('input[type="checkbox"]'); + if ($checkboxes.length) { + new Drupal.simpleSitemapViewsUi.Arguments($checkboxes); + } + } + }; + + Drupal.simpleSitemapViewsUi.Checkboxifier = function ($button) { + this.$button = $button; + this.$parent = this.$button.parent('div.simple-sitemap-views-index'); + this.$input = this.$parent.find('input:checkbox'); + this.$button.hide(); + this.$input.on('click', $.proxy(this, 'clickHandler')); + }; + + Drupal.simpleSitemapViewsUi.Checkboxifier.prototype.clickHandler = function () { + this.$button.trigger('click').trigger('submit'); + }; + + Drupal.simpleSitemapViewsUi.Arguments = function ($checkboxes) { + this.$checkboxes = $checkboxes; + this.$checkboxes.on('change', $.proxy(this, 'changeHandler')); + }; + + Drupal.simpleSitemapViewsUi.Arguments.prototype.changeHandler = function (e) { + var $checkbox = $(e.target), index = this.$checkboxes.index($checkbox); + $checkbox.prop('checked') ? this.check(index) : this.uncheck(index); + }; + + Drupal.simpleSitemapViewsUi.Arguments.prototype.check = function (index) { + this.$checkboxes.slice(0, index).prop('checked', true); + }; + + Drupal.simpleSitemapViewsUi.Arguments.prototype.uncheck = function (index) { + this.$checkboxes.slice(index).prop('checked', false); + }; + +})(jQuery, Drupal); \ No newline at end of file diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.info.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.info.yml new file mode 100644 index 0000000000..a569a3365d --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.info.yml @@ -0,0 +1,15 @@ +name: 'Simple XML Sitemap (Views)' +type: module +description: 'Provides integration of the Simple XML Sitemap module with the Views module.' +configure: simple_sitemap.settings_views +package: SEO +core: 8.x +core_version_requirement: ^8 || ^9 +dependencies: + - simple_sitemap:simple_sitemap + - drupal:views + +# Information added by Drupal.org packaging script on 2020-04-09 +version: '8.x-3.6' +project: 'simple_sitemap' +datestamp: 1586468195 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.install b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.install new file mode 100755 index 0000000000..44b34e1bfd --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.install @@ -0,0 +1,73 @@ +<?php + +/** + * @file + * Install and uninstall hooks for the simple_sitemap_views module. + */ + +/** + * Implements hook_install(). + */ +function simple_sitemap_views_install() { + // Enable views display extender plugin. + \Drupal::service('simple_sitemap.views')->enable(); +} + +/** + * Implements hook_uninstall(). + */ +function simple_sitemap_views_uninstall() { + // Disable views display extender plugin. + \Drupal::service('simple_sitemap.views')->disable(); +} + +/** + * Implements hook_schema(). + */ +function simple_sitemap_views_schema() { + $schema['simple_sitemap_views'] = [ + 'description' => 'Index of argument values for view pages.', + 'fields' => [ + 'id' => [ + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique ID for argument values.', + ], + 'view_id' => [ + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'default' => '', + 'length' => 128, + 'description' => 'The ID of the view.', + ], + 'display_id' => [ + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'default' => '', + 'length' => 128, + 'description' => 'The ID of the view display.', + ], + 'arguments_ids' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'default' => '', + 'length' => 1024, + 'description' => 'A string representation of the set of argument identifiers.', + ], + 'arguments_values' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'default' => '', + 'length' => 1024, + 'description' => 'A string representation of the set of argument values.', + ], + ], + 'primary key' => ['id'], + 'indexes' => [ + 'view' => ['view_id'], + 'display' => ['view_id', 'display_id'], + 'arguments_ids' => ['view_id', 'display_id', 'arguments_ids'], + ], + ]; + return $schema; +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.libraries.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.libraries.yml new file mode 100755 index 0000000000..1e9d81cee5 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.libraries.yml @@ -0,0 +1,8 @@ +viewsUi: + version: VERSION + js: + js/simple_sitemap.viewsUi.js: {} + dependencies: + - core/jquery + - core/drupal + - core/jquery.once diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.links.task.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.links.task.yml new file mode 100755 index 0000000000..3ed5f37cb3 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.links.task.yml @@ -0,0 +1,5 @@ +simple_sitemap.views: + route_name: simple_sitemap.views + title: 'Views' + parent_id: simple_sitemap.inclusion + weight: 2 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.module b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.module new file mode 100755 index 0000000000..cdd757d89e --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.module @@ -0,0 +1,24 @@ +<?php + +/** + * @file + * Contains simple_sitemap_views.module. + */ + +/** + * Implements hook_cron(). + */ +function simple_sitemap_views_cron() { + // Create tasks in the garbage collection queue. + \Drupal::service('simple_sitemap.views')->executeGarbageCollection(); +} + +/** + * Implements hook_simple_sitemap_sitemap_types_alter(). + */ +function simple_sitemap_views_simple_sitemap_sitemap_types_alter(array &$sitemap_types) { + // Add a 'views' UrlGenerator plugin to the default hreflang sitemap type. + if (isset($sitemap_types['default_hreflang'])) { + $sitemap_types['default_hreflang']['urlGenerators'][] = 'views'; + } +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.routing.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.routing.yml new file mode 100755 index 0000000000..650cf71b41 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.routing.yml @@ -0,0 +1,7 @@ +simple_sitemap.views: + path: '/admin/config/search/simplesitemap/views' + defaults: + _controller: '\Drupal\simple_sitemap_views\Controller\SimpleSitemapViewsController::content' + _title: 'Simple XML Sitemap' + requirements: + _permission: 'administer sitemap settings' diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.services.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.services.yml new file mode 100755 index 0000000000..f01cceb37e --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.services.yml @@ -0,0 +1,9 @@ +services: + simple_sitemap.views: + class: Drupal\simple_sitemap_views\SimpleSitemapViews + arguments: ['@entity_type.manager', '@config.factory', '@queue', '@database'] + simple_sitemap.views.argument_collector: + class: Drupal\simple_sitemap_views\EventSubscriber\ArgumentCollector + arguments: ['@entity_type.manager', '@simple_sitemap.views', '@current_route_match'] + tags: + - {name: 'event_subscriber'} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Controller/SimpleSitemapViewsController.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Controller/SimpleSitemapViewsController.php new file mode 100644 index 0000000000..cb6e8728a9 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Controller/SimpleSitemapViewsController.php @@ -0,0 +1,94 @@ +<?php + +namespace Drupal\simple_sitemap_views\Controller; + +use Drupal\simple_sitemap\Form\FormHelper; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\simple_sitemap_views\SimpleSitemapViews; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Url; + +/** + * Controller for Simple XML Sitemap Views admin page. + */ +class SimpleSitemapViewsController extends ControllerBase { + + /** + * Views sitemap data. + * + * @var \Drupal\simple_sitemap_views\SimpleSitemapViews + */ + protected $sitemapViews; + + /** + * SimpleSitemapViewsController constructor. + * + * @param \Drupal\simple_sitemap_views\SimpleSitemapViews $sitemap_views + * Views sitemap data. + */ + public function __construct(SimpleSitemapViews $sitemap_views) { + $this->sitemapViews = $sitemap_views; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('simple_sitemap.views') + ); + } + + /** + * Builds a listing of indexed views displays. + * + * @return array + * A render array. + */ + public function content() { + $table = [ + '#type' => 'table', + '#header' => [ + $this->t('View'), + $this->t('Display'), + $this->t('Arguments'), + $this->t('Operations'), + ], + '#empty' => $this->t('No view displays are set to be indexed yet. <a href="@url">Edit a view.</a>', ['@url' => $GLOBALS['base_url'] . '/admin/structure/views']), + ]; + + foreach ($this->sitemapViews->getIndexableViews() as $index => $view) { + $table[$index]['view'] = ['#markup' => $view->storage->label()]; + $table[$index]['display'] = ['#markup' => $view->display_handler->display['display_title']]; + // Determine whether view display arguments are indexed. + $arguments_status = $this->sitemapViews->getIndexableArguments($view) ? $this->t('Yes') : $this->t('No'); + $table[$index]['arguments'] = ['#markup' => $arguments_status]; + + // Link to view display edit form. + $display_edit_url = Url::fromRoute('entity.view.edit_display_form', [ + 'view' => $view->id(), + 'display_id' => $view->current_display, + ]); + $table[$index]['operations'] = [ + '#type' => 'operations', + '#links' => [ + 'display_edit' => [ + 'title' => $this->t('Edit'), + 'url' => $display_edit_url, + ], + ], + ]; + } + + // Show information about indexed displays. + $build['simple_sitemap_views'] = [ + '#prefix' => FormHelper::getDonationText(), + '#title' => $this->t('Indexed view displays'), + '#type' => 'fieldset', + 'table' => $table, + ]; + + return $build; + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/EventSubscriber/ArgumentCollector.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/EventSubscriber/ArgumentCollector.php new file mode 100755 index 0000000000..f33ef222ea --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/EventSubscriber/ArgumentCollector.php @@ -0,0 +1,114 @@ +<?php + +namespace Drupal\simple_sitemap_views\EventSubscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\PostResponseEvent; +use Drupal\simple_sitemap_views\SimpleSitemapViews; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Drupal\Core\Routing\RouteMatchInterface; + +/** + * Collect information about views arguments. + */ +class ArgumentCollector implements EventSubscriberInterface { + + /** + * View entities storage. + * + * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface + */ + protected $viewStorage; + + /** + * Views sitemap data. + * + * @var \Drupal\simple_sitemap_views\SimpleSitemapViews + */ + protected $sitemapViews; + + /** + * The currently active route match object. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + + /** + * ArgumentCollector constructor. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * Entity type manager. + * @param \Drupal\simple_sitemap_views\SimpleSitemapViews $sitemap_views + * Views sitemap data. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The currently active route match object. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, SimpleSitemapViews $sitemap_views, RouteMatchInterface $route_match) { + $this->viewStorage = $entity_type_manager->getStorage('view'); + $this->sitemapViews = $sitemap_views; + $this->routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::TERMINATE] = 'onTerminate'; + + return $events; + } + + /** + * Collect information about views arguments. + * + * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event + * Object of event after a response was sent. + */ + public function onTerminate(PostResponseEvent $event) { + // Only successful requests are interesting. + // Collect information about arguments only if views support is enabled. + if (!$event->getResponse()->isSuccessful() || !$this->sitemapViews->isEnabled()) { + return; + } + + // Get view ID from route. + $view_id = $this->routeMatch->getParameter('view_id'); + /** @var \Drupal\views\ViewEntityInterface $view_entity */ + if ($view_id && $view_entity = $this->viewStorage->load($view_id)) { + // Get display ID from route. + $display_id = $this->routeMatch->getParameter('display_id'); + // Get a set of view arguments and try to add them to the index. + $view = $view_entity->getExecutable(); + $args = $this->getViewArgumentsFromRoute(); + $this->sitemapViews->addArgumentsToIndex($view, $args, $display_id); + // Destroy a view instance. + $view->destroy(); + } + } + + /** + * Get view arguments from current route. + * + * @return array + * View arguments array. + */ + protected function getViewArgumentsFromRoute() { + // The code of this function is taken in part from the view page controller + // method (Drupal\views\Routing\ViewPageController::handle()). + $route = $this->routeMatch->getRouteObject(); + $map = $route->hasOption('_view_argument_map') ? $route->getOption('_view_argument_map') : []; + + $args = []; + foreach ($map as $attribute => $parameter_name) { + $parameter_name = isset($parameter_name) ? $parameter_name : $attribute; + if (($arg = $this->routeMatch->getRawParameter($parameter_name)) !== NULL) { + $args[] = $arg; + } + } + + return $args; + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/QueueWorker/GarbageCollector.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/QueueWorker/GarbageCollector.php new file mode 100755 index 0000000000..91a6b284f9 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/QueueWorker/GarbageCollector.php @@ -0,0 +1,137 @@ +<?php + +namespace Drupal\simple_sitemap_views\Plugin\QueueWorker; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\simple_sitemap_views\SimpleSitemapViews; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Queue\QueueWorkerBase; + +/** + * Executes garbage collection in the simple_sitemap_views table. + * + * @QueueWorker( + * id = "simple_sitemap.views.garbage_collector", + * title = @Translation("Garbage collection in the simple_sitemap_views table"), + * cron = {"time" = 30} + * ) + */ +class GarbageCollector extends QueueWorkerBase implements ContainerFactoryPluginInterface { + + /** + * View entities storage. + * + * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface + */ + protected $viewStorage; + + /** + * Views sitemap data. + * + * @var \Drupal\simple_sitemap_views\SimpleSitemapViews + */ + protected $sitemapViews; + + /** + * GarbageCollector 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\EntityTypeManagerInterface $entity_type_manager + * Entity type manager. + * @param \Drupal\simple_sitemap_views\SimpleSitemapViews $sitemap_views + * Views sitemap data. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, SimpleSitemapViews $sitemap_views) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->viewStorage = $entity_type_manager->getStorage('view'); + $this->sitemapViews = $sitemap_views; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('simple_sitemap.views') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + $view_id = $data['view_id']; + /** @var \Drupal\views\ViewEntityInterface $view_entity */ + $view_entity = $this->viewStorage->load($view_id); + $display_ids = []; + + // Check that the view exists and it is enabled. + if ($view_entity && $view_entity->status()) { + $view = $view_entity->getExecutable(); + foreach ($this->sitemapViews->getRouterDisplayIds($view_entity) as $display_id) { + // Ensure the display was correctly set. + // Check that the display is enabled. + if (!$view->setDisplay($display_id) || !$view->display_handler->isEnabled()) { + continue; + } + // Check that the display has indexable arguments. + $args_ids = $this->sitemapViews->getIndexableArguments($view); + if (empty($args_ids)) { + continue; + } + + $display_ids[] = $display_id; + // Delete records about sets of arguments that are no longer indexed. + $args_ids = $this->sitemapViews->getArgumentsStringVariations($args_ids); + $condition = new Condition('AND'); + $condition->condition('view_id', $view_id); + $condition->condition('display_id', $display_id); + $condition->condition('arguments_ids', $args_ids, 'NOT IN'); + $this->sitemapViews->removeArgumentsFromIndex($condition); + + // Check if the records limit for display is exceeded. + $settings = $this->sitemapViews->getSitemapSettings($view); + $max_links = is_numeric($settings['max_links']) ? $settings['max_links'] : 0; + if ($max_links > 0) { + $condition = new Condition('AND'); + $condition->condition('view_id', $view_id); + $condition->condition('display_id', $display_id); + // Delete records that exceed the limit. + if ($index_id = $this->sitemapViews->getIndexIdByPosition($max_links, $condition)) { + $condition->condition('id', $index_id, '>'); + $this->sitemapViews->removeArgumentsFromIndex($condition); + } + } + } + // Delete records about view displays that do not exist or are disabled. + if (!empty($display_ids)) { + $condition = new Condition('AND'); + $condition->condition('view_id', $view_id); + $condition->condition('display_id', $display_ids, 'NOT IN'); + $this->sitemapViews->removeArgumentsFromIndex($condition); + } + // Destroy a view instance. + $view->destroy(); + } + + // Delete records about the view, if it does not exist, is disabled or it + // does not have a display whose arguments are indexed. + if (empty($display_ids)) { + $condition = new Condition('AND'); + $condition->condition('view_id', $view_id); + $this->sitemapViews->removeArgumentsFromIndex($condition); + } + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/simple_sitemap/UrlGenerator/ViewsUrlGenerator.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/simple_sitemap/UrlGenerator/ViewsUrlGenerator.php new file mode 100755 index 0000000000..76ee0461e2 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/simple_sitemap/UrlGenerator/ViewsUrlGenerator.php @@ -0,0 +1,265 @@ +<?php + +namespace Drupal\simple_sitemap_views\Plugin\simple_sitemap\UrlGenerator; + +use Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\EntityUrlGeneratorBase; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\simple_sitemap_views\SimpleSitemapViews; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Routing\RouteProviderInterface; +use Drupal\Core\Database\Query\Condition; +use Drupal\simple_sitemap\Simplesitemap; +use Drupal\simple_sitemap\EntityHelper; +use Drupal\simple_sitemap\Logger; +use Drupal\views\Views; +use Drupal\Core\Url; + +/** + * Views URL generator plugin. + * + * @UrlGenerator( + * id = "views", + * label = @Translation("Views URL generator"), + * description = @Translation("Generates URLs for views."), + * ) + */ +class ViewsUrlGenerator extends EntityUrlGeneratorBase { + + /** + * Views sitemap data. + * + * @var \Drupal\simple_sitemap_views\SimpleSitemapViews + */ + protected $sitemapViews; + + /** + * The route provider. + * + * @var \Drupal\Core\Routing\RouteProviderInterface + */ + protected $routeProvider; + + /** + * ViewsUrlGenerator 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\simple_sitemap\Simplesitemap $generator + * The simple_sitemap.generator service. + * @param \Drupal\simple_sitemap\Logger $logger + * The simple_sitemap.logger service. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\simple_sitemap\EntityHelper $entity_helper + * The simple_sitemap.entity_helper service. + * @param \Drupal\simple_sitemap_views\SimpleSitemapViews $sitemap_views + * Views sitemap data. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider. + */ + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + Simplesitemap $generator, + Logger $logger, + LanguageManagerInterface $language_manager, + EntityTypeManagerInterface $entity_type_manager, + EntityHelper $entity_helper, + SimpleSitemapViews $sitemap_views, + RouteProviderInterface $route_provider + ) { + parent::__construct( + $configuration, + $plugin_id, + $plugin_definition, + $generator, + $logger, + $language_manager, + $entity_type_manager, + $entity_helper + ); + $this->sitemapViews = $sitemap_views; + $this->routeProvider = $route_provider; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('simple_sitemap.generator'), + $container->get('simple_sitemap.logger'), + $container->get('language_manager'), + $container->get('entity_type.manager'), + $container->get('simple_sitemap.entity_helper'), + $container->get('simple_sitemap.views'), + $container->get('router.route_provider') + ); + } + + /** + * {@inheritdoc} + */ + public function getDataSets() { + $data_sets = []; + + // Get data sets. + foreach ($this->sitemapViews->getIndexableViews() as $view) { + $settings = $this->sitemapViews->getSitemapSettings($view); + if ($settings['variant'] != $this->sitemapVariant) { + // Destroy a view instance. + $view->destroy(); + continue; + } + + $base_data_set = [ + 'view_id' => $view->id(), + 'display_id' => $view->current_display, + ]; + // View path without arguments. + $data_sets[] = $base_data_set + ['arguments' => NULL]; + + // Process indexed arguments. + if ($args_ids = $this->sitemapViews->getIndexableArguments($view)) { + // Form the condition according to the variants of the + // indexable arguments. + $args_ids = $this->sitemapViews->getArgumentsStringVariations($args_ids); + $condition = new Condition('AND'); + $condition->condition('view_id', $view->id()); + $condition->condition('display_id', $view->current_display); + $condition->condition('arguments_ids', $args_ids, 'IN'); + // Get the arguments values from the index. + $max_links = is_numeric($settings['max_links']) ? $settings['max_links'] : NULL; + $indexed_arguments = $this->sitemapViews->getArgumentsFromIndex($condition, $max_links, TRUE); + // Add the arguments values for processing. + foreach ($indexed_arguments as $index_id => $arguments_info) { + $data_sets[] = $base_data_set + [ + 'index_id' => $index_id, + 'arguments' => $arguments_info['arguments'], + ]; + } + } + // Destroy a view instance. + $view->destroy(); + } + return $data_sets; + } + + /** + * {@inheritdoc} + */ + protected function processDataSet($data_set) { + // Get information from data set. + $view_id = $data_set['view_id']; + $display_id = $data_set['display_id']; + $args = $data_set['arguments']; + + try { + // Trying to get an instance of the view. + $view = Views::getView($view_id); + if (empty($view)) { + throw new \UnexpectedValueException('Failed to get an instance of the view.'); + } + + // Trying to set the view display. + $view->initDisplay(); + if (!$view->displayHandlers->has($display_id) || !$view->setDisplay($display_id)) { + throw new \UnexpectedValueException('Failed to set the view display.'); + } + + // Trying to get the sitemap settings. + $settings = $this->sitemapViews->getSitemapSettings($view); + if (empty($settings)) { + throw new \UnexpectedValueException('Failed to get the sitemap settings.'); + } + + // Trying to get the view URL. + $url = $view->getUrl($args); + $url->setAbsolute(); + + if (is_array($args)) { + $params = array_merge([$view_id, $display_id], $args); + $view_result = call_user_func_array('views_get_view_result', $params); + // Do not include paths on which the view returns an empty result. + if (empty($view_result)) { + throw new \UnexpectedValueException('The view returned an empty result.'); + } + // Remove empty arguments from URL. + $this->cleanRouteParameters($url, $args); + } + $path = $url->getInternalPath(); + // Destroy a view instance. + $view->destroy(); + } + catch (\Exception $e) { + // Delete records about arguments that are not added to the sitemap. + if (!empty($data_set['index_id'])) { + $condition = new Condition('AND'); + $condition->condition('id', $data_set['index_id']); + $this->sitemapViews->removeArgumentsFromIndex($condition); + } + return FALSE; + } + + return [ + 'url' => $url, + 'lastmod' => NULL, + 'priority' => isset($settings['priority']) ? $settings['priority'] : NULL, + 'changefreq' => !empty($settings['changefreq']) ? $settings['changefreq'] : NULL, + 'images' => [], + // Additional info useful in hooks. + 'meta' => [ + 'path' => $path, + 'view_info' => [ + 'view_id' => $view_id, + 'display_id' => $display_id, + 'arguments' => $args, + ], + ], + ]; + } + + /** + * Clears the URL from parameters that are not present in the arguments. + * + * @param \Drupal\Core\Url $url + * The URL object. + * @param array $args + * Array of arguments. + * + * @throws \UnexpectedValueException. + * If this is a URI with no corresponding route. + */ + protected function cleanRouteParameters(Url $url, array $args) { + $parameters = $url->getRouteParameters(); + // Check that the number of params does not match the number of arguments. + if (count($parameters) != count($args)) { + $route_name = $url->getRouteName(); + $route = $this->routeProvider->getRouteByName($route_name); + $variables = $route->compile()->getVariables(); + // Remove params that are not present in the arguments. + foreach ($variables as $variable_name) { + if (empty($args)) { + unset($parameters[$variable_name]); + } + else { + array_shift($args); + } + } + // Set new route params. + $url->setRouteParameters($parameters); + } + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/views/display_extender/SimpleSitemapDisplayExtender.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/views/display_extender/SimpleSitemapDisplayExtender.php new file mode 100755 index 0000000000..0ffa6db3f4 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/views/display_extender/SimpleSitemapDisplayExtender.php @@ -0,0 +1,358 @@ +<?php + +namespace Drupal\simple_sitemap_views\Plugin\views\display_extender; + +use Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\views\Plugin\views\display\DisplayRouterInterface; +use Drupal\views\Plugin\views\display\DisplayPluginBase; +use Drupal\simple_sitemap\SimplesitemapManager; +use Drupal\simple_sitemap\Form\FormHelper; +use Drupal\Core\Form\FormStateInterface; +use Drupal\views\ViewExecutable; + +/** + * Simple XML Sitemap display extender plugin. + * + * @ingroup views_display_extender_plugins + * + * @ViewsDisplayExtender( + * id = "simple_sitemap_display_extender", + * title = @Translation("Simple XML Sitemap"), + * help = @Translation("Simple XML Sitemap settings for this view."), + * no_ui = FALSE + * ) + */ +class SimpleSitemapDisplayExtender extends DisplayExtenderPluginBase { + + /** + * Simple XML Sitemap form helper. + * + * @var \Drupal\simple_sitemap\Form\FormHelper + */ + protected $formHelper; + + /** + * Simple XML Sitemap manager. + * + * @var \Drupal\simple_sitemap\SimplesitemapManager + */ + protected $sitemapManager; + + /** + * Constructs the 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 \Drupal\simple_sitemap\Form\FormHelper $form_helper + * Simple XML Sitemap form helper. + * @param \Drupal\simple_sitemap\SimplesitemapManager $sitemap_manager + * Simple XML Sitemap manager. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, FormHelper $form_helper, SimplesitemapManager $sitemap_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->formHelper = $form_helper; + $this->sitemapManager = $sitemap_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('simple_sitemap.form_helper'), + $container->get('simple_sitemap.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { + parent::init($view, $display, $options); + if (!$this->hasSitemapSettings()) { + $this->options = []; + } + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['index'] = ['default' => 0]; + $options['variant'] = ['default' => NULL]; + $options['priority'] = ['default' => 0.5]; + $options['changefreq'] = ['default' => '']; + $options['arguments'] = ['default' => []]; + $options['max_links'] = ['default' => 100]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + if ($this->hasSitemapSettings() && $form_state->get('section') == 'simple_sitemap') { + $form['#title'] .= $this->t('Simple XML Sitemap settings for this display'); + $settings = $this->getSitemapSettings(); + + // The index section. + $form['index'] = [ + '#prefix' => '<div class="simple-sitemap-views-index">', + '#suffix' => '</div>', + ]; + // Add a checkbox for JS users, which will have behavior attached to it + // so it can replace the button. + $form['index']['index'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Index this display'), + '#default_value' => $settings['index'], + ]; + // Then add the button itself. + $form['index']['index_button'] = [ + '#limit_validation_errors' => [], + '#type' => 'submit', + '#value' => $this->t('Index this display'), + '#submit' => [[$this, 'displaySitemapSettingsForm']], + ]; + + // Show the whole form only if indexing is checked. + if ($this->options['index']) { + // Main settings fieldset. + $form['main'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Main settings'), + ]; + // The sitemap variant. + $form['main']['variant'] = [ + '#type' => 'select', + '#title' => $this->t('Sitemap variant'), + '#description' => $this->t('The sitemap variant this display is to be indexed in.'), + '#options' => $this->formHelper->getVariantSelectValues(), + '#default_value' => $this->formHelper->getVariantSelectValuesDefault($settings['variant']), + '#required' => TRUE, + ]; + // The sitemap priority. + $form['main']['priority'] = [ + '#type' => 'select', + '#title' => $this->t('Priority'), + '#description' => $this->t('The priority this display will have in the eyes of search engine bots.'), + '#default_value' => $settings['priority'], + '#options' => $this->formHelper->getPrioritySelectValues(), + ]; + // The sitemap change frequency. + $form['main']['changefreq'] = [ + '#type' => 'select', + '#title' => $this->t('Change frequency'), + '#description' => $this->t('The frequency with which this display changes. Search engine bots may take this as an indication of how often to index it.'), + '#default_value' => $settings['changefreq'], + '#options' => $this->formHelper->getChangefreqSelectValues(), + ]; + + // Argument settings fieldset. + $form['arguments'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Argument settings'), + ]; + // Get view arguments options. + if ($arguments_options = $this->getArgumentsOptions()) { + // Indexed arguments element. + $form['arguments']['arguments'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Indexed arguments'), + '#options' => $arguments_options, + '#default_value' => $settings['arguments'], + '#attributes' => ['class' => ['indexed-arguments']], + ]; + // Max links with arguments. + $form['arguments']['max_links'] = [ + '#type' => 'number', + '#title' => $this->t('Maximum display variations'), + '#description' => $this->t('The maximum number of link variations to be indexed for this display. If left blank, each argument will create link variations for this display. Use with caution, as a large number of argument valuescan significantly increase the number of sitemap links.'), + '#default_value' => $settings['max_links'], + '#min' => 1, + ]; + } + else { + $form['arguments']['#description'] = $this->t('This display has no arguments.'); + } + } + + // Attaching script to form. + $form['#attached']['library'][] = 'simple_sitemap_views/viewsUi'; + } + } + + /** + * {@inheritdoc} + */ + public function validateOptionsForm(&$form, FormStateInterface $form_state) { + if ($this->hasSitemapSettings() && $form_state->get('section') == 'simple_sitemap') { + // Validate indexed arguments. + $arguments = $form_state->getValue('arguments', []); + $errors = $this->validateIndexedArguments($arguments); + foreach ($errors as $message) { + $form_state->setError($form['arguments']['arguments'], $message); + } + } + } + + /** + * {@inheritdoc} + */ + public function submitOptionsForm(&$form, FormStateInterface $form_state) { + if ($this->hasSitemapSettings() && $form_state->get('section') == 'simple_sitemap') { + $values = $form_state->cleanValues()->getValues(); + $values['arguments'] = isset($values['arguments']) ? array_filter($values['arguments']) : []; + // Save sitemap settings. + foreach ($values as $key => $value) { + if (array_key_exists($key, $this->options)) { + $this->options[$key] = $value; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function validate() { + $errors = parent::validate(); + + // Validate the argument options relative to the + // current state of the view argument handlers. + if ($this->hasSitemapSettings()) { + $settings = $this->getSitemapSettings(); + $result = $this->validateIndexedArguments($settings['arguments']); + $errors = array_merge($errors, $result); + } + return $errors; + } + + /** + * {@inheritdoc} + */ + public function optionsSummary(&$categories, &$options) { + if ($this->hasSitemapSettings()) { + $categories['simple_sitemap'] = [ + 'title' => $this->t('Simple XML Sitemap'), + 'column' => 'second', + ]; + $options['simple_sitemap'] = [ + 'category' => 'simple_sitemap', + 'title' => $this->t('Status'), + 'value' => $this->isIndexingEnabled() ? $this->t('Included in sitemap') : $this->t('Excluded from sitemap'), + ]; + } + } + + /** + * Displays the sitemap settings form. + * + * @param array $form + * The form structure. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Current form state. + */ + public function displaySitemapSettingsForm(array $form, FormStateInterface $form_state) { + // Update index option. + $this->options['index'] = empty($this->options['index']); + + // Rebuild settings form. + /** @var \Drupal\views_ui\ViewUI $view */ + $view = $form_state->get('view'); + $display_handler = $view->getExecutable()->display_handler; + $extender_options = $display_handler->getOption('display_extenders'); + if (isset($extender_options[$this->pluginId])) { + $extender_options[$this->pluginId] = $this->options; + $display_handler->setOption('display_extenders', $extender_options); + } + $view->cacheSet(); + $form_state->set('rerender', TRUE); + $form_state->setRebuild(); + } + + /** + * Get sitemap settings configuration for this display. + * + * @return array + * The sitemap settings. + */ + public function getSitemapSettings() { + return $this->options; + } + + /** + * Identify whether or not the current display has sitemap settings. + * + * @return bool + * Has sitemap settings (TRUE) or not (FALSE). + */ + public function hasSitemapSettings() { + return $this->displayHandler instanceof DisplayRouterInterface; + } + + /** + * Identify whether or not the current display indexing is enabled. + * + * @return bool + * Indexing is enabled (TRUE) or not (FALSE). + */ + public function isIndexingEnabled() { + $settings = $this->getSitemapSettings(); + return !empty($settings['index']); + } + + /** + * Returns available view arguments options. + * + * @return array + * View arguments labels keyed by argument ID. + */ + protected function getArgumentsOptions() { + $arguments_options = []; + // Get view argument handlers. + $arguments = $this->displayHandler->getHandlers('argument'); + /** @var \Drupal\views\Plugin\views\argument\ArgumentPluginBase $argument */ + foreach ($arguments as $id => $argument) { + $arguments_options[$id] = $argument->adminLabel(); + } + return $arguments_options; + } + + /** + * Validate indexed arguments. + * + * @param array $indexed_arguments + * Indexed arguments array. + * + * @return array + * An array of error strings. This will be empty if there are no validation + * errors. + */ + protected function validateIndexedArguments(array $indexed_arguments) { + $arguments = $this->displayHandler->getHandlers('argument'); + $arguments = array_fill_keys(array_keys($arguments), 0); + $arguments = array_merge($arguments, $indexed_arguments); + reset($arguments); + + $errors = []; + while (($argument = current($arguments)) !== FALSE) { + $next_argument = next($arguments); + if (empty($argument) && !empty($next_argument)) { + $errors[] = $this->t('To enable indexing of an argument, you must enable indexing of all previous arguments.'); + break; + } + } + return $errors; + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/SimpleSitemapViews.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/SimpleSitemapViews.php new file mode 100755 index 0000000000..d7cf0db54d --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/SimpleSitemapViews.php @@ -0,0 +1,506 @@ +<?php + +namespace Drupal\simple_sitemap_views; + +use Drupal\simple_sitemap_views\Plugin\views\display_extender\SimpleSitemapDisplayExtender; +use Drupal\Core\Database\Query\ConditionInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Database\Query\Condition; +use Drupal\views\ViewEntityInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Queue\QueueFactory; +use Drupal\Core\Database\Database; +use Drupal\views\ViewExecutable; +use Drupal\views\Views; + +/** + * Class to manage sitemap data for views. + */ +class SimpleSitemapViews { + + /** + * Separator between arguments. + */ + const ARGUMENT_SEPARATOR = '/'; + + /** + * Views display extender plugin ID. + */ + const PLUGIN_ID = 'simple_sitemap_display_extender'; + + /** + * View entities storage. + * + * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface + */ + protected $viewStorage; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The queue factory. + * + * @var \Drupal\Core\Queue\QueueFactory + */ + protected $queueFactory; + + /** + * The current active database's master connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * SimpleSitemapViews constructor. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Queue\QueueFactory $queue_factory + * The queue factory. + * @param \Drupal\Core\Database\Connection $database + * The current active database's master connection. + */ + public function __construct( + EntityTypeManagerInterface $entity_type_manager, + ConfigFactoryInterface $config_factory, + QueueFactory $queue_factory, + Connection $database + ) { + $this->viewStorage = $entity_type_manager->getStorage('view'); + $this->configFactory = $config_factory; + $this->queueFactory = $queue_factory; + $this->database = $database; + } + + /** + * Checks that views support is enabled. + * + * @return bool + * Returns TRUE if support is enabled, and FALSE otherwise. + */ + public function isEnabled() { + // Support enabled when views display extender is enabled. + $enabled = Views::getEnabledDisplayExtenders(); + + return isset($enabled[self::PLUGIN_ID]); + } + + /** + * Enables sitemap support for views. + */ + public function enable() { + $config = $this->configFactory->getEditable('views.settings'); + $display_extenders = $config->get('display_extenders') ?: []; + + // Enable views display extender plugin. + $display_extenders[self::PLUGIN_ID] = self::PLUGIN_ID; + $config->set('display_extenders', $display_extenders); + $config->save(); + } + + /** + * Disables sitemap support for views. + */ + public function disable() { + $config = $this->configFactory->getEditable('views.settings'); + $display_extenders = $config->get('display_extenders') ?: []; + + // Disable views display extender plugin. + unset($display_extenders[self::PLUGIN_ID]); + $config->set('display_extenders', $display_extenders); + $config->save(); + + // Clear the table with indexed arguments. + // Clear the garbage collection queue. + $this->removeArgumentsFromIndex(); + $queue = $this->queueFactory->get('simple_sitemap.views.garbage_collector'); + $queue->deleteQueue(); + } + + /** + * Get sitemap settings for view display. + * + * @param \Drupal\views\ViewExecutable $view + * A view executable instance. + * @param string|null $display_id + * The display id. If empty uses the preselected display. + * + * @return array|null + * The sitemap settings if the display is indexed, NULL otherwise. + */ + public function getSitemapSettings(ViewExecutable $view, $display_id = NULL) { + // Ensure the display was correctly set. + if (!$view->setDisplay($display_id)) { + return NULL; + } + // Get the list of extenders. + $extenders = $view->display_handler->getExtenders(); + $extender = isset($extenders[self::PLUGIN_ID]) ? $extenders[self::PLUGIN_ID] : NULL; + // Retrieve the sitemap settings from the extender. + if ($extender instanceof SimpleSitemapDisplayExtender && $extender->hasSitemapSettings() && $extender->isIndexingEnabled()) { + return $extender->getSitemapSettings(); + } + + return NULL; + } + + /** + * Get indexable arguments for view display. + * + * @param \Drupal\views\ViewExecutable $view + * A view executable instance. + * @param string|null $display_id + * The display id. If empty uses the preselected display. + * + * @return array + * Indexable arguments identifiers. + */ + public function getIndexableArguments(ViewExecutable $view, $display_id = NULL) { + $indexable_arguments = []; + $settings = $this->getSitemapSettings($view, $display_id); + if ($settings && !empty($settings['arguments']) && is_array($settings['arguments'])) { + // Find indexable arguments. + $arguments = array_keys($view->display_handler->getHandlers('argument')); + foreach ($arguments as $argument_id) { + if (empty($settings['arguments'][$argument_id])) { + break; + } + $indexable_arguments[] = $argument_id; + } + } + + return $indexable_arguments; + } + + /** + * Adds view arguments to the index. + * + * @param \Drupal\views\ViewExecutable $view + * A view executable instance. + * @param array $args + * Array of arguments to add to the index. + * @param string|null $display_id + * The display id. If empty uses the preselected display. + * + * @return bool + * TRUE if the arguments are added to the index, FALSE otherwise. + */ + public function addArgumentsToIndex(ViewExecutable $view, array $args, $display_id = NULL) { + // An array of arguments to be added to the index can not be empty. + // Also ensure the display was correctly set. + if (empty($args) || !$view->setDisplay($display_id)) { + return FALSE; + } + // Check that indexing of at least one argument is enabled. + $indexable_arguments = $this->getIndexableArguments($view); + if (empty($indexable_arguments)) { + return FALSE; + } + // Check that the number of identifiers is equal to the number of values. + $args_ids = array_slice($indexable_arguments, 0, count($args)); + if (count($args_ids) != count($args)) { + return FALSE; + } + // Check that the current number of rows in the index does not + // exceed the specified number. + $condition = new Condition('AND'); + $condition->condition('view_id', $view->id()); + $condition->condition('display_id', $view->current_display); + $settings = $this->getSitemapSettings($view); + $max_links = is_numeric($settings['max_links']) ? $settings['max_links'] : 0; + if ($max_links > 0 && $this->getArgumentsFromIndexCount($condition) >= $max_links) { + return FALSE; + } + // Convert the set of identifiers and a set of values to string. + $args_ids = $this->convertArgumentsArrayToString($args_ids); + $args_values = $this->convertArgumentsArrayToString($args); + $condition->condition('arguments_ids', $args_ids); + $condition->condition('arguments_values', $args_values); + // Check that this set of arguments has not yet been indexed. + if ($this->getArgumentsFromIndexCount($condition)) { + return FALSE; + } + // Check that the view result is not empty for this set of arguments. + $params = array_merge([$view->id(), $view->current_display], $args); + $view_result = call_user_func_array('views_get_view_result', $params); + if (empty($view_result)) { + return FALSE; + } + // Add a set of arguments to the index. + $options = ['return' => Database::RETURN_AFFECTED]; + $query = $this->database->insert('simple_sitemap_views', $options); + $query->fields([ + 'view_id' => $view->id(), + 'display_id' => $view->current_display, + 'arguments_ids' => $args_ids, + 'arguments_values' => $args_values, + ]); + + return (bool) $query->execute(); + } + + /** + * Get arguments from index. + * + * @param \Drupal\Core\Database\Query\ConditionInterface|null $condition + * The query conditions. + * @param int|null $limit + * The number of records to return from the result set. If NULL, returns + * all records. + * @param bool $convert + * Defaults to FALSE. If TRUE, the argument string will be converted + * to an array. + * + * @return array + * An array with information about the indexed arguments. + */ + public function getArgumentsFromIndex(ConditionInterface $condition = NULL, $limit = NULL, $convert = FALSE) { + // Select the rows from the index table. + $query = $this->database->select('simple_sitemap_views', 'ssv'); + $query->addField('ssv', 'id'); + $query->addField('ssv', 'view_id'); + $query->addField('ssv', 'display_id'); + $query->addField('ssv', 'arguments_values', 'arguments'); + // Add conditions if necessary. + if (!empty($condition)) { + $query->condition($condition); + } + // Limit results if necessary. + if (!empty($limit)) { + $query->range(0, $limit); + } + $rows = $query->execute()->fetchAll(); + // Form the result. + $arguments = []; + foreach ($rows as $row) { + $arguments[$row->id] = [ + 'view_id' => $row->view_id, + 'display_id' => $row->display_id, + 'arguments' => $convert ? $this->convertArgumentsStringToArray($row->arguments) : $row->arguments, + ]; + } + + return $arguments; + } + + /** + * Get the number of rows in the index. + * + * @param \Drupal\Core\Database\Query\ConditionInterface|null $condition + * The query conditions. + * + * @return int + * The number of rows. + */ + public function getArgumentsFromIndexCount(ConditionInterface $condition = NULL) { + $query = $this->database->select('simple_sitemap_views', 'ssv'); + // Add conditions if necessary. + if (!empty($condition)) { + $query->condition($condition); + } + + // Get the number of rows from the index table. + return $query->countQuery()->execute()->fetchField(); + } + + /** + * Returns the ID of the record in the index for the specified position. + * + * @param int $position + * Position of the record. + * @param \Drupal\Core\Database\Query\ConditionInterface|null $condition + * The query conditions. + * + * @return int|bool + * The ID of the record, or FALSE if there is no specified position. + */ + public function getIndexIdByPosition($position, ConditionInterface $condition = NULL) { + $query = $this->database->select('simple_sitemap_views', 'ssv'); + $query->addField('ssv', 'id'); + // Add conditions if necessary. + if (!empty($condition)) { + $query->condition($condition); + } + $query->orderBy('id', 'ASC'); + $query->range($position - 1, 1); + + return $query->execute()->fetchField(); + } + + /** + * Remove arguments from index. + * + * @param \Drupal\Core\Database\Query\ConditionInterface|null $condition + * The query conditions. + */ + public function removeArgumentsFromIndex(ConditionInterface $condition = NULL) { + if (empty($condition)) { + // If there are no conditions, use the TRUNCATE query. + $query = $this->database->truncate('simple_sitemap_views'); + } + else { + // Otherwise, use the DELETE query. + $query = $this->database->delete('simple_sitemap_views'); + $query->condition($condition); + } + $query->execute(); + } + + /** + * Returns an array of view displays that use the route. + * + * @param \Drupal\views\ViewEntityInterface $view_entity + * The config entity in which the view is stored. + * + * @return array + * Array of display identifiers. + */ + public function getRouterDisplayIds(ViewEntityInterface $view_entity) { + $display_plugins = $this->getRouterDisplayPluginIds(); + $filter_callback = function (array $display) use ($display_plugins) { + return !empty($display['display_plugin']) && in_array($display['display_plugin'], $display_plugins); + }; + $displays = array_filter($view_entity->get('display'), $filter_callback); + + return array_keys($displays); + } + + /** + * Returns an array of executable views whose current display is indexable. + * + * @return \Drupal\views\ViewExecutable[] + * An array of ViewExecutable instances. + */ + public function getIndexableViews() { + // Check that views support is enabled. + if (!$this->isEnabled()) { + return []; + } + // Load views with display plugins that use the route. + $query = $this->viewStorage->getQuery(); + $query->condition('status', TRUE); + $query->condition("display.*.display_plugin", $this->getRouterDisplayPluginIds(), 'IN'); + $view_ids = $query->execute(); + // If there are no such views, then return an empty array. + if (empty($view_ids)) { + return []; + } + + $indexable_views = []; + /** @var \Drupal\views\ViewEntityInterface $view_entity */ + foreach ($this->viewStorage->loadMultiple($view_ids) as $view_entity) { + foreach ($this->getRouterDisplayIds($view_entity) as $display_id) { + $view = Views::executableFactory()->get($view_entity); + // Ensure the display was correctly set. + if (!$view->setDisplay($display_id)) { + $view->destroy(); + continue; + } + + // Check that the display is enabled and indexed. + if ($view->display_handler->isEnabled() && $this->getSitemapSettings($view)) { + $indexable_views[] = $view; + } + } + } + + return $indexable_views; + } + + /** + * Creates tasks in the garbage collection queue. + */ + public function executeGarbageCollection() { + // The task queue of garbage collection in the arguments index. + $queue = $this->queueFactory->get('simple_sitemap.views.garbage_collector'); + // Check that the queue is empty. + if ($queue->numberOfItems()) { + return; + } + // Get identifiers of indexed views. + $query = $this->database->select('simple_sitemap_views', 'ssv'); + $query->addField('ssv', 'view_id'); + $query->distinct(); + $result = $query->execute()->fetchCol(); + // Create a garbage collection tasks. + foreach ($result as $view_id) { + $queue->createItem(['view_id' => $view_id]); + } + } + + /** + * Get variations for string representation of arguments. + * + * @param array $args + * Array of arguments. + * + * @return array + * Array of variations of the string representation of arguments. + */ + public function getArgumentsStringVariations(array $args) { + $variations = []; + for ($length = 1; $length <= count($args); $length++) { + $args_slice = array_slice($args, 0, $length); + $variations[] = $this->convertArgumentsArrayToString($args_slice); + } + + return $variations; + } + + /** + * Converts an array of arguments to a string. + * + * @param array $args + * Array of arguments to convert. + * + * @return string + * A string representation of the arguments. + */ + protected function convertArgumentsArrayToString(array $args) { + return implode(self::ARGUMENT_SEPARATOR, $args); + } + + /** + * Converts a string with arguments to an array. + * + * @param string $args + * A string representation of the arguments to convert. + * + * @return array + * Array of arguments. + */ + protected function convertArgumentsStringToArray($args) { + return explode(self::ARGUMENT_SEPARATOR, $args); + } + + /** + * Get all display plugins that use the route. + * + * @return array + * An array with plugin identifiers. + */ + protected function getRouterDisplayPluginIds() { + static $plugin_ids = []; + if (empty($plugin_ids)) { + // Get all display plugins that use the route. + $display_plugins = Views::pluginManager('display')->getDefinitions(); + foreach ($display_plugins as $plugin_id => $definition) { + if (!empty($definition['uses_route'])) { + $plugin_ids[$plugin_id] = $plugin_id; + } + } + } + + return $plugin_ids; + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/config/install/views.view.simple_sitemap_views_test_view.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/config/install/views.view.simple_sitemap_views_test_view.yml new file mode 100644 index 0000000000..d4dfae2f2f --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/config/install/views.view.simple_sitemap_views_test_view.yml @@ -0,0 +1,282 @@ +langcode: en +status: true +dependencies: + module: + - node + - user +id: simple_sitemap_views_test_view +label: 'Test view' +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: none + options: + items_per_page: null + offset: 0 + 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 + 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: { } + title: 'Test view' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + type: + id: type + table: node_field_data + field: type + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + glossary: false + limit: 0 + case: none + path_case: none + transform_dash: false + break_phrase: false + entity_type: node + entity_field: type + plugin_id: node_type + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + glossary: false + limit: 0 + case: none + path_case: none + transform_dash: false + break_phrase: false + entity_type: node + entity_field: title + plugin_id: string + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: false + not: false + entity_type: node + entity_field: nid + plugin_id: node_nid + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: + simple_sitemap_display_extender: + index: true + variant: default + priority: '0.5' + changefreq: '' + arguments: + type: type + title: title + max_links: 2 + path: simple-sitemap-views-test-view + rendering_language: en + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/simple_sitemap_views_test.info.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/simple_sitemap_views_test.info.yml new file mode 100644 index 0000000000..f7885e48a5 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/simple_sitemap_views_test.info.yml @@ -0,0 +1,13 @@ +name: 'Simple XML Sitemap (Views) Test' +type: module +description: 'Test module for Simple XML Sitemap (Views).' +package: Testing +core: 8.x +core_version_requirement: ^8 || ^9 +dependencies: + - simple_sitemap:simple_sitemap_views + +# Information added by Drupal.org packaging script on 2020-04-09 +version: '8.x-3.6' +project: 'simple_sitemap' +datestamp: 1586468195 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTest.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTest.php new file mode 100644 index 0000000000..e3ffc43181 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTest.php @@ -0,0 +1,157 @@ +<?php + +namespace Drupal\Tests\simple_sitemap_views\Functional; + +/** + * Tests Simple XML Sitemap (Views) functional integration. + * + * @group simple_sitemap_views + */ +class SimpleSitemapViewsTest extends SimpleSitemapViewsTestBase { + + /** + * Tests status of sitemap support for views. + */ + public function testSitemapSupportForViews() { + // Views support must be enabled after module installation. + $this->assertTrue($this->sitemapViews->isEnabled()); + + $this->sitemapViews->disable(); + $this->assertFalse($this->sitemapViews->isEnabled()); + + $this->sitemapViews->enable(); + $this->assertTrue($this->sitemapViews->isEnabled()); + } + + /** + * Tests indexable views. + */ + public function testIndexableViews() { + // Ensure that at least one indexable view exists. + $indexable_views = $this->sitemapViews->getIndexableViews(); + $this->assertNotEmpty($indexable_views); + + $test_view_exists = FALSE; + foreach ($indexable_views as &$view) { + if ($view->id() == $this->testView->id() && $view->current_display == $this->testView->current_display) { + $test_view_exists = TRUE; + break; + } + } + // The test view should be in the list. + $this->assertTrue($test_view_exists); + + // Check the indexing status of the arguments. + $indexable_arguments = $this->sitemapViews->getIndexableArguments($this->testView); + $this->assertContains('type', $indexable_arguments); + $this->assertContains('title', $indexable_arguments); + $this->assertNotContains('nid', $indexable_arguments); + } + + /** + * Tests the process of adding arguments to the index. + */ + public function testAddArgumentsToIndex() { + // Arguments with the wrong value should not be indexed. + $this->sitemapViews->addArgumentsToIndex($this->testView, ['page2']); + $this->assertIndexSize(0); + + // Non-indexable arguments should not be indexed. + $args = ['page', $this->node->getTitle(), $this->node->id()]; + $this->sitemapViews->addArgumentsToIndex($this->testView, $args); + $this->assertIndexSize(0); + + // The argument set should not be indexed more than once. + for ($i = 0; $i < 2; $i++) { + $this->sitemapViews->addArgumentsToIndex($this->testView, ['page']); + $this->assertIndexSize(1); + } + + // A new set of arguments must be indexed. + $args = ['page', $this->node->getTitle()]; + $this->sitemapViews->addArgumentsToIndex($this->testView, $args); + $this->assertIndexSize(2); + + // The number of argument sets in the index for one view display should not + // exceed the maximum number of link variations. + $args = ['page', $this->node2->getTitle()]; + $this->sitemapViews->addArgumentsToIndex($this->testView, $args); + $this->assertIndexSize(2); + } + + /** + * Tests the process of generating view display URLs. + */ + public function testViewsUrlGenerator() { + $sitemap_types = $this->generator->getSitemapManager()->getSitemapTypes(); + $this->assertContains('views', $sitemap_types['default_hreflang']['urlGenerators']); + + $title = $this->node->getTitle(); + $this->sitemapViews->addArgumentsToIndex($this->testView, ['page']); + $this->sitemapViews->addArgumentsToIndex($this->testView, ['page', $title]); + $this->generator->generateSitemap('backend'); + + // Check that the sitemap contains view display URLs. + $this->drupalGet($this->defaultSitemapUrl); + $test_view_url = $this->testView->getUrl()->toString(); + $this->assertSession()->responseContains($test_view_url); + $this->assertSession()->responseContains("$test_view_url/page"); + $this->assertSession()->responseContains("$test_view_url/page/$title"); + } + + /** + * Tests the garbage collection process. + */ + public function testGarbageCollector() { + // Disable cron generation, since data can be removed + // from the index during generation. + $this->generator->saveSetting('cron_generate', FALSE); + + // Record with the wrong set of indexed arguments must be removed. + $this->addRecordToIndex( + $this->testView->id(), + $this->testView->current_display, + ['type', 'title', 'nid'], + ['page', $this->node->getTitle(), $this->node->id()] + ); + $this->cron->run(); + $this->assertIndexSize(0); + + // Record of a non-existent view must be removed. + $this->addRecordToIndex( + 'simple_sitemap_fake_view', + $this->testView->current_display, + ['type', 'title'], + ['page', $this->node->getTitle()] + ); + $this->cron->run(); + $this->assertIndexSize(0); + + // Record of a non-existent display must be removed. + $this->addRecordToIndex( + $this->testView->id(), + 'simple_sitemap_fake_display', + ['type', 'title'], + ['page', $this->node->getTitle()] + ); + $this->cron->run(); + $this->assertIndexSize(0); + + // The number of records should not exceed the specified limit. + for ($i = 0; $i < 3; $i++) { + $this->addRecordToIndex( + $this->testView->id(), + $this->testView->current_display, + ['type', 'title'], + ['page2', "Node$i"] + ); + } + $this->cron->run(); + $this->assertIndexSize(2); + + // Records about pages with empty result must be removed during generation. + $this->generator->generateSitemap('backend'); + $this->assertIndexSize(0); + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTestBase.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTestBase.php new file mode 100644 index 0000000000..3dae919d91 --- /dev/null +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTestBase.php @@ -0,0 +1,93 @@ +<?php + +namespace Drupal\Tests\simple_sitemap_views\Functional; + +use Drupal\Tests\simple_sitemap\Functional\SimplesitemapTestBase; +use Drupal\simple_sitemap_views\SimpleSitemapViews; +use Drupal\views\Views; + +/** + * Defines a base class for Simple XML Sitemap (Views) functional testing. + */ +abstract class SimpleSitemapViewsTestBase extends SimplesitemapTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'simple_sitemap_views', + 'simple_sitemap_views_test', + ]; + + /** + * Views sitemap data. + * + * @var \Drupal\simple_sitemap_views\SimpleSitemapViews + */ + protected $sitemapViews; + + /** + * The cron service. + * + * @var \Drupal\Core\CronInterface + */ + protected $cron; + + /** + * Test view. + * + * @var \Drupal\views\ViewExecutable + */ + protected $testView; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->sitemapViews = $this->container->get('simple_sitemap.views'); + $this->cron = $this->container->get('cron'); + + $this->testView = Views::getView('simple_sitemap_views_test_view'); + $this->testView->setDisplay('page_1'); + } + + /** + * Asserts the size of the arguments index. + * + * @param int $size + * The expected size. + */ + protected function assertIndexSize($size) { + $this->assertEquals($size, $this->sitemapViews->getArgumentsFromIndexCount()); + } + + /** + * Adds a record to the arguments index. + * + * @param string $view_id + * The view ID. + * @param string $display_id + * The view display ID. + * @param array $args_ids + * A set of argument IDs. + * @param array $args_values + * A set of argument values. + */ + protected function addRecordToIndex($view_id, $display_id, array $args_ids, array $args_values) { + $args_ids = implode(SimpleSitemapViews::ARGUMENT_SEPARATOR, $args_ids); + $args_values = implode(SimpleSitemapViews::ARGUMENT_SEPARATOR, $args_values); + + // Insert a record into the index table. + $query = $this->database->insert('simple_sitemap_views'); + $query->fields([ + 'view_id' => $view_id, + 'display_id' => $display_id, + 'arguments_ids' => $args_ids, + 'arguments_values' => $args_values, + ]); + $query->execute(); + } + +} diff --git a/web/modules/simple_sitemap/simple_sitemap.api.php b/web/modules/simple_sitemap/simple_sitemap.api.php index 55936bf834..15593722d1 100644 --- a/web/modules/simple_sitemap/simple_sitemap.api.php +++ b/web/modules/simple_sitemap/simple_sitemap.api.php @@ -2,7 +2,7 @@ /** * @file - * Hooks provided by the Simple XML sitemap module. + * Hooks provided by the Simple XML Sitemap module. */ /** @@ -17,13 +17,13 @@ * @param array &$links * Array containing multilingual links generated for each path to be indexed * - * @param string|null $sitemap_variant + * @param string $sitemap_variant */ function hook_simple_sitemap_links_alter(array &$links, $sitemap_variant) { // Remove German URL for a certain path in the hreflang sitemap. foreach ($links as $key => $link) { - if ($link['path'] === 'node/1') { + if ($link['meta']['path'] === 'node/1') { // Remove 'loc' URL if it points to a german site. if ($link['langcode'] === 'de') { @@ -45,7 +45,7 @@ function hook_simple_sitemap_links_alter(array &$links, $sitemap_variant) { * Add arbitrary links to the sitemap. * * @param array &$arbitrary_links - * @param string|null $sitemap_variant + * @param string $sitemap_variant */ function hook_simple_sitemap_arbitrary_links_alter(array &$arbitrary_links, $sitemap_variant) { @@ -85,7 +85,7 @@ function hook_simple_sitemap_arbitrary_links_alter(array &$arbitrary_links, $sit * Attributes can be added, changed and removed. * * @param array &$attributes - * @param string|null $sitemap_variant + * @param string $sitemap_variant */ function hook_simple_sitemap_attributes_alter(array &$attributes, $sitemap_variant) { @@ -98,7 +98,7 @@ function hook_simple_sitemap_attributes_alter(array &$attributes, $sitemap_varia * Attributes can be added, changed and removed. * * @param array &$index_attributes - * @param string|null $sitemap_variant + * @param string $sitemap_variant */ function hook_simple_sitemap_index_attributes_alter(array &$index_attributes, $sitemap_variant) { diff --git a/web/modules/simple_sitemap/simple_sitemap.drush.inc b/web/modules/simple_sitemap/simple_sitemap.drush.inc index 72a6d3bf55..709f3ff30c 100644 --- a/web/modules/simple_sitemap/simple_sitemap.drush.inc +++ b/web/modules/simple_sitemap/simple_sitemap.drush.inc @@ -5,19 +5,21 @@ * Drush (< 9) integration. */ +use Drupal\simple_sitemap\Queue\QueueWorker; + /** * Implements hook_drush_command(). */ function simple_sitemap_drush_command() { $items['simple-sitemap-generate'] = [ - 'description' => 'Regenerate the XML sitemaps according to the module settings.', + 'description' => 'Regenerate all XML sitemap variants or continue generation.', 'callback' => 'drush_simple_sitemap_generate', 'drupal dependencies' => ['simple_sitemap'], 'aliases' => ['ssg'], ]; $items['simple-sitemap-rebuild-queue'] = [ - 'description' => 'Rebuild the sitemap queue for all sitemap variants.', + 'description' => 'Queue all sitemap variants for regeneration.', 'callback' => 'drush_simple_sitemap_rebuild_queue', 'drupal dependencies' => ['simple_sitemap'], 'aliases' => ['ssr'], @@ -32,7 +34,7 @@ function simple_sitemap_drush_command() { * Regenerate the XML sitemaps according to the module settings. */ function drush_simple_sitemap_generate() { - \Drupal::service('simple_sitemap.generator')->generateSitemap('drush'); + \Drupal::service('simple_sitemap.generator')->generateSitemap(QueueWorker::GENERATE_TYPE_DRUSH); } /** diff --git a/web/modules/simple_sitemap/simple_sitemap.info.yml b/web/modules/simple_sitemap/simple_sitemap.info.yml index 262e0bcfb7..48227d551b 100644 --- a/web/modules/simple_sitemap/simple_sitemap.info.yml +++ b/web/modules/simple_sitemap/simple_sitemap.info.yml @@ -1,12 +1,12 @@ name: 'Simple XML Sitemap' type: module -description: 'Creates a standard conform hreflang XML sitemap of the site content and provides a framework for developing other sitemap types.' -configure: simple_sitemap.settings +description: 'Generates standard conform hreflang XML sitemaps of the site content and provides a framework for developing other sitemap types.' +configure: simple_sitemap.sitemaps package: SEO -# core: 8.x +core: 8.x +core_version_requirement: ^8 || ^9 -# Information added by Drupal.org packaging script on 2018-12-07 -version: '8.x-3.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-09 +version: '8.x-3.6' project: 'simple_sitemap' -datestamp: 1544174900 +datestamp: 1586468195 diff --git a/web/modules/simple_sitemap/simple_sitemap.install b/web/modules/simple_sitemap/simple_sitemap.install index 33e1c1393a..7569b242ff 100644 --- a/web/modules/simple_sitemap/simple_sitemap.install +++ b/web/modules/simple_sitemap/simple_sitemap.install @@ -16,9 +16,9 @@ function simple_sitemap_requirements($phase) { if (!extension_loaded('xmlwriter')) { $requirements['simple_sitemap_php_extensions'] = [ - 'title' => t('Simple XML sitemap PHP extensions'), + 'title' => t('Simple XML Sitemap PHP extensions'), 'value' => t('Missing PHP xmlwriter extension'), - 'description' => t('In order to be able to generate sitemaps, the Simple XML sitemap module requires the <em>xmlwriter</em> PHP extension to be enabled.'), + 'description' => t('In order to be able to generate sitemaps, the Simple XML Sitemap module requires the <em>xmlwriter</em> PHP extension to be enabled.'), 'severity' => REQUIREMENT_ERROR, ]; } @@ -57,7 +57,7 @@ function simple_sitemap_requirements($phase) { // } // // $requirements['simple_sitemap_generated'] = [ -// 'title' => 'Simple XML sitemap', +// 'title' => 'Simple XML Sitemap', // 'value' => $value, // 'description' => $description, // 'severity' => $severity, @@ -92,7 +92,6 @@ function simple_sitemap_schema() { 'id' => [ 'description' => 'Sitemap chunk unique identifier.', 'type' => 'int', - 'size' => 'small', 'not null' => TRUE, 'unsigned' => TRUE, ], @@ -106,7 +105,6 @@ function simple_sitemap_schema() { 'delta' => [ 'description' => 'Delta of the chunk within the type scope.', 'type' => 'int', - 'size' => 'small', 'not null' => TRUE, 'unsigned' => TRUE, ], @@ -169,6 +167,7 @@ function simple_sitemap_schema() { ], 'primary key' => ['id'], ]; + return $schema; } @@ -676,3 +675,46 @@ function simple_sitemap_update_8217() { return t('The XML sitemaps need to be regenerated.'); } + +/** + * Changing id and delta fields of simple_sitemap table from smallint to int. + */ +function simple_sitemap_update_8301() { + $schema = \Drupal::database()->schema(); + + $schema->changeField( + 'simple_sitemap', + 'id', + 'id', [ + 'description' => 'Sitemap chunk unique identifier.', + 'type' => 'int', + 'not null' => TRUE, + 'unsigned' => TRUE, + ] + ); + + $schema->changeField( + 'simple_sitemap', + 'delta', + 'delta', [ + 'description' => 'Delta of the chunk within the variant scope.', + 'type' => 'int', + 'not null' => TRUE, + 'unsigned' => TRUE, + ] + ); +} + +/** + * Removing unused batch_process_limit key from simple_sitemap.settings + * configuration. + */ +function simple_sitemap_update_8302() { + $settings = \Drupal::service('config.factory') + ->getEditable('simple_sitemap.settings'); + + if (NULL !== $settings->get('batch_process_limit')) { + $settings->clear('batch_process_limit'); + $settings->save(); + } +} diff --git a/web/modules/simple_sitemap/simple_sitemap.libraries.yml b/web/modules/simple_sitemap/simple_sitemap.libraries.yml index 72b955aeb7..272381459d 100644 --- a/web/modules/simple_sitemap/simple_sitemap.libraries.yml +++ b/web/modules/simple_sitemap/simple_sitemap.libraries.yml @@ -4,12 +4,6 @@ fieldsetSummaries: js/simple_sitemap.fieldsetSummaries.js: {} dependencies: - core/jquery -form: - version: VERSION - js: - js/simple_sitemap.form.js: {} - dependencies: - - core/jquery sitemapEntities: version: VERSION js: @@ -17,3 +11,8 @@ sitemapEntities: dependencies: - core/jquery - core/drupalSettings +sitemaps: + version: VERSION + css: + theme: + css/simple_sitemap.sitemaps.css: {} diff --git a/web/modules/simple_sitemap/simple_sitemap.links.menu.yml b/web/modules/simple_sitemap/simple_sitemap.links.menu.yml index 02ea6faf76..0a6eb0a74c 100644 --- a/web/modules/simple_sitemap/simple_sitemap.links.menu.yml +++ b/web/modules/simple_sitemap/simple_sitemap.links.menu.yml @@ -1,5 +1,17 @@ -simple_sitemap.settings: - title: 'Simple XML sitemap' - description: 'Configure and generate the XML sitemap, add custom links to it.' +simple_sitemap.sitemaps: + title: 'Simple XML Sitemap' + description: 'Configure, add content to and generate XML sitemaps.' parent: system.admin_config_search + route_name: simple_sitemap.sitemaps + +simple_sitemap.settings: + title: 'Settings' + parent: simple_sitemap.sitemaps route_name: simple_sitemap.settings + weight: 0 + +simple_sitemap.inclusion: + title: 'Inclusion' + parent: simple_sitemap.sitemaps + route_name: simple_sitemap.entities + weight: 1 diff --git a/web/modules/simple_sitemap/simple_sitemap.links.task.yml b/web/modules/simple_sitemap/simple_sitemap.links.task.yml index c0e163e9a0..01548962e5 100644 --- a/web/modules/simple_sitemap/simple_sitemap.links.task.yml +++ b/web/modules/simple_sitemap/simple_sitemap.links.task.yml @@ -1,25 +1,41 @@ +simple_sitemap.sitemaps: + route_name: simple_sitemap.sitemaps + title: 'Sitemaps' + base_route: simple_sitemap.sitemaps + weight: -1 + +simple_sitemap.status: + route_name: simple_sitemap.sitemaps + title: 'Status' + parent_id: simple_sitemap.sitemaps + weight: -1 + +simple_sitemap.variants: + route_name: simple_sitemap.variants + title: 'Variants' + parent_id: simple_sitemap.sitemaps + weight: 0 + simple_sitemap.settings: route_name: simple_sitemap.settings title: 'Settings' - base_route: simple_sitemap.settings - weight: -1 - -simple_sitemap.settings_entities: - route_name: simple_sitemap.settings_entities - title: 'Sitemap entities' - base_route: simple_sitemap.settings + base_route: simple_sitemap.sitemaps weight: 0 -simple_sitemap.variants: - route_name: simple_sitemap.settings_variants - title: 'Sitemap variants' - base_route: simple_sitemap.settings +simple_sitemap.inclusion: + route_name: simple_sitemap.entities + title: 'Inclusion' + base_route: simple_sitemap.sitemaps weight: 1 -simple_sitemap.settings_custom: - route_name: simple_sitemap.settings_custom - title: 'Custom links' - base_route: simple_sitemap.settings - weight: 2 - +simple_sitemap.entities: + route_name: simple_sitemap.entities + title: 'Entities' + parent_id: simple_sitemap.inclusion + weight: 0 +simple_sitemap.custom: + route_name: simple_sitemap.custom + title: 'Custom links' + parent_id: simple_sitemap.inclusion + weight: 1 diff --git a/web/modules/simple_sitemap/simple_sitemap.module b/web/modules/simple_sitemap/simple_sitemap.module index 504f8352dc..6ac0a6cbfa 100644 --- a/web/modules/simple_sitemap/simple_sitemap.module +++ b/web/modules/simple_sitemap/simple_sitemap.module @@ -8,6 +8,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\simple_sitemap\Queue\QueueWorker; use Drupal\system\MenuInterface; use Drupal\language\ConfigurableLanguageInterface; @@ -43,16 +44,11 @@ function simple_sitemap_form_alter(&$form, FormStateInterface $form_state, $form $form['simple_sitemap'] = [ '#type' => 'details', '#group' => isset($form['additional_settings']) ? 'additional_settings' : 'advanced', - '#title' => t('Simple XML sitemap'), + '#title' => t('Simple XML Sitemap'), '#description' => $f->getEntityCategory() === 'instance' ? t('Settings for this entity can be overridden here.') : '', '#weight' => 10, ]; - // Attach some js magic to forms. - if ($f->getEntityCategory() !== 'instance') { - $form['#attached']['library'][] = 'simple_sitemap/form'; - } - // Only attach fieldset summary js to 'additional settings' vertical tabs. if (isset($form['additional_settings'])) { $form['#attached']['library'][] = 'simple_sitemap/fieldsetSummaries'; @@ -103,45 +99,41 @@ function simple_sitemap_entity_form_submit($form, FormStateInterface &$form_stat /** @var \Drupal\simple_sitemap\Simplesitemap $generator */ $generator = \Drupal::service('simple_sitemap.generator'); - $settings = [ - 'index' => (bool) $values['simple_sitemap_index_content'], - 'priority' => $values['simple_sitemap_priority'], - 'changefreq' => $values['simple_sitemap_changefreq'], - 'include_images' => (bool) $values['simple_sitemap_include_images'], - ]; - - // Deleting bundle settings for old bundle. - // See SimplesitemapEntitiesForm::submitForm(). - // todo: This will not be necessary if "multiple variants pro bundle" is implemented. - if (isset($form['simple_sitemap']['simple_sitemap_variant']['#default_value'])) { - $old_variant = $form['simple_sitemap']['simple_sitemap_variant']['#default_value']; - if ($old_variant !== $values['simple_sitemap_variant']) { - $generator->setVariants($old_variant)->removeBundleSettings($f->getEntityTypeId(), $f->getBundleName()); + foreach ($generator->getSitemapManager()->getSitemapVariants(NULL, FALSE) as $variant => $definition) { + + if (isset($values['index_' . $variant . '_' . $f->getEntityTypeId() . '_settings'])) { // Variants may have changed since form load. + $settings = [ + 'index' => (bool) $values['index_' . $variant . '_' . $f->getEntityTypeId() . '_settings'], + 'priority' => $values['priority_' . $variant . '_' . $f->getEntityTypeId() . '_settings'], + 'changefreq' => $values['changefreq_' . $variant . '_' . $f->getEntityTypeId() . '_settings'], + 'include_images' => (bool) $values['include_images_' . $variant . '_' . $f->getEntityTypeId() . '_settings'], + ]; + + $generator->setVariants($variant); + + switch ($f->getEntityCategory()) { + + case 'bundle': + $generator->setBundleSettings($f->getEntityTypeId(), $f->getBundleName(), $settings); + if (empty($settings['index'])) { + $generator->removeEntityInstanceSettings($f->getEntityTypeId(), $f->getInstanceId()); + } + break; + + case 'instance': + if (!$f->entityIsNew()) { + $generator->setEntityInstanceSettings($f->getEntityTypeId(), $f->getInstanceId(), $settings); + } + break; + } } } - switch ($f->getEntityCategory()) { - - case 'bundle': - $generator->setVariants($values['simple_sitemap_variant']) - ->setBundleSettings($f->getEntityTypeId(), - !empty($f->getBundleName()) ? $f->getBundleName() : $f->getFormEntityId(), - $settings - ); - break; - - case 'instance': - $generator->setVariants($values['simple_sitemap_variant']) - ->setEntityInstanceSettings($f->getEntityTypeId(), - !empty($f->getInstanceId()) ? $f->getInstanceId() : $f->getFormEntityId(), - $settings - ); - break; - } - // Regenerate sitemaps according to user setting. if ($values['simple_sitemap_regenerate_now']) { - $generator->rebuildQueue()->generateSitemap(); + $generator->setVariants(TRUE) + ->rebuildQueue() + ->generateSitemap(); } } } @@ -168,7 +160,7 @@ function simple_sitemap_cron() { $state->set('simple_sitemap.last_cron_generate', $request_time); } - $generator->generateSitemap('cron'); + $generator->generateSitemap(QueueWorker::GENERATE_TYPE_CRON); } } } @@ -199,11 +191,16 @@ function simple_sitemap_configurable_language_delete(ConfigurableLanguageInterfa */ function simple_sitemap_entity_delete(EntityInterface $entity) { - /** @var \Drupal\simple_sitemap\Simplesitemap $generator */ - $generator = \Drupal::service('simple_sitemap.generator'); - $generator->setVariants(TRUE)->removeEntityInstanceSettings( - $entity->getEntityTypeId(), $entity->id() - ); + /** @var \Drupal\simple_sitemap\EntityHelper $entity_helper */ + $entity_helper = \Drupal::service('simple_sitemap.entity_helper'); + if ($entity_helper->supports($entity->getEntityType())) { + + /** @var \Drupal\simple_sitemap\Simplesitemap $generator */ + $generator = \Drupal::service('simple_sitemap.generator'); + $generator->setVariants(TRUE)->removeEntityInstanceSettings( + $entity->getEntityTypeId(), $entity->id() + ); + } } /** @@ -234,3 +231,29 @@ function simple_sitemap_menu_delete(MenuInterface $menu) { $generator = \Drupal::service('simple_sitemap.generator'); $generator->setVariants(TRUE)->removeBundleSettings('menu_link_content', $menu->id()); } + +/** + * Implements hook_page_attachments_alter(). + */ +function simple_sitemap_page_attachments_alter(array &$attachments) { + if (!empty($attachments['#attached']['html_head_link'])) { + + /** @var \Drupal\simple_sitemap\Simplesitemap $generator */ + $generator = \Drupal::service('simple_sitemap.generator'); + + if ($generator->getSetting('disable_language_hreflang')) { + // @fixme https://www.drupal.org/project/drupal/issues/1255092 + // Content Translation module normally adds identical hreflang tags, so + // executing its hook_page_attachments() implementation would be harmless, + // but if an entity page is configured as the front page, it attaches + // extraneous hreflang tags using the entity URL. + foreach ($attachments['#attached']['html_head_link'] as $key => $list) { + foreach ($list as $k => $element) { + if (!empty($element['hreflang']) && !empty($element['rel'])) { + unset($attachments['#attached']['html_head_link'][$key]); + } + } + } + } + } +} diff --git a/web/modules/simple_sitemap/simple_sitemap.permissions.yml b/web/modules/simple_sitemap/simple_sitemap.permissions.yml index 035e3ce214..88aaa232a5 100644 --- a/web/modules/simple_sitemap/simple_sitemap.permissions.yml +++ b/web/modules/simple_sitemap/simple_sitemap.permissions.yml @@ -1,4 +1,4 @@ administer sitemap settings: title: 'Administer sitemap settings' - description: 'Administer Simple XML sitemap settings, alter inclusion settings of content and generate the sitemap on demand.' + description: 'Administer Simple XML Sitemap settings, alter inclusion settings of content and generate sitemaps on demand.' restrict access: false diff --git a/web/modules/simple_sitemap/simple_sitemap.routing.yml b/web/modules/simple_sitemap/simple_sitemap.routing.yml index eaa9fbd260..89d5ec6ab4 100644 --- a/web/modules/simple_sitemap/simple_sitemap.routing.yml +++ b/web/modules/simple_sitemap/simple_sitemap.routing.yml @@ -18,34 +18,51 @@ simple_sitemap.sitemap_variant: requirements: _access: 'TRUE' -simple_sitemap.settings: +simple_sitemap.sitemap_xsl: + path: '/sitemap.xsl' + defaults: + _controller: '\Drupal\simple_sitemap\Controller\SimplesitemapController::getSitemapXsl' + _title: 'Sitemap XSL' + _disable_route_normalizer: 'TRUE' + requirements: + _access: 'TRUE' + +simple_sitemap.sitemaps: path: '/admin/config/search/simplesitemap' + defaults: + _form: '\Drupal\simple_sitemap\Form\SimplesitemapSitemapsForm' + _title: 'Simple XML Sitemap' + requirements: + _permission: 'administer sitemap settings' + +simple_sitemap.settings: + path: '/admin/config/search/simplesitemap/settings' defaults: _form: '\Drupal\simple_sitemap\Form\SimplesitemapSettingsForm' - _title: 'Simple XML Sitemap Settings' + _title: 'Simple XML Sitemap' requirements: _permission: 'administer sitemap settings' -simple_sitemap.settings_entities: +simple_sitemap.entities: path: '/admin/config/search/simplesitemap/entities' defaults: _form: '\Drupal\simple_sitemap\Form\SimplesitemapEntitiesForm' - _title: 'Simple XML Sitemap Settings' + _title: 'Simple XML Sitemap' requirements: _permission: 'administer sitemap settings' -simple_sitemap.settings_custom: +simple_sitemap.custom: path: '/admin/config/search/simplesitemap/custom' defaults: _form: '\Drupal\simple_sitemap\Form\SimplesitemapCustomLinksForm' - _title: 'Simple XML Sitemap Settings' + _title: 'Simple XML Sitemap' requirements: _permission: 'administer sitemap settings' -simple_sitemap.settings_variants: +simple_sitemap.variants: path: '/admin/config/search/simplesitemap/variants' defaults: _form: '\Drupal\simple_sitemap\Form\SimplesitemapVariantsForm' - _title: 'Simple XML Sitemap Settings' + _title: 'Simple XML Sitemap' requirements: _permission: 'administer sitemap settings' diff --git a/web/modules/simple_sitemap/simple_sitemap.services.yml b/web/modules/simple_sitemap/simple_sitemap.services.yml index 3a784281d1..8d89aa6351 100644 --- a/web/modules/simple_sitemap/simple_sitemap.services.yml +++ b/web/modules/simple_sitemap/simple_sitemap.services.yml @@ -9,7 +9,6 @@ services: - '@config.factory' - '@database' - '@entity_type.manager' - - '@entity_type.bundle.info' - '@path.validator' - '@date.formatter' - '@datetime.time' @@ -58,7 +57,7 @@ services: public: true arguments: - '@entity_type.manager' - - '@database' + - '@entity_type.bundle.info' simple_sitemap.form_helper: class: Drupal\simple_sitemap\Form\FormHelper @@ -76,11 +75,16 @@ services: - '@messenger' - '@current_user' - simple_sitemap.path_processor_variant: - class: Drupal\simple_sitemap\PathProcessor\PathProcessorSitemapVariant + simple_sitemap.path_processor.variant.in: + class: Drupal\simple_sitemap\PathProcessor\PathProcessorSitemapVariantIn tags: - { name: path_processor_inbound, priority: 300 } + simple_sitemap.path_processor.variant.out: + class: Drupal\simple_sitemap\PathProcessor\PathProcessorSitemapVariantOut + tags: + - { name: path_processor_outbound, priority: 300 } + logger.channel.simple_sitemap: parent: logger.channel_base public: false diff --git a/web/modules/simple_sitemap/src/Commands/SimplesitemapCommands.php b/web/modules/simple_sitemap/src/Commands/SimplesitemapCommands.php index 1ee2366d41..db5b9ee347 100644 --- a/web/modules/simple_sitemap/src/Commands/SimplesitemapCommands.php +++ b/web/modules/simple_sitemap/src/Commands/SimplesitemapCommands.php @@ -2,6 +2,7 @@ namespace Drupal\simple_sitemap\Commands; +use Drupal\simple_sitemap\Queue\QueueWorker; use Drupal\simple_sitemap\Simplesitemap; use Drush\Commands\DrushCommands; @@ -25,35 +26,57 @@ public function __construct(Simplesitemap $generator) { } /** - * Regenerate the XML sitemaps according to the module settings. + * Regenerate all XML sitemap variants or continue generation. * * @command simple-sitemap:generate * * @usage drush simple-sitemap:generate - * Regenerate the XML sitemaps according to the module settings. + * Regenerate all XML sitemap variants or continue generation. * * @validate-module-enabled simple_sitemap * * @aliases ssg, simple-sitemap-generate */ public function generate() { - $this->generator->generateSitemap('drush'); + $this->generator->generateSitemap(QueueWorker::GENERATE_TYPE_DRUSH); } /** - * Rebuild the sitemap queue for all sitemap variants. + * Queue all or specific sitemap variants for regeneration. * * @command simple-sitemap:rebuild-queue * + * @option variants + * Queue all or specific sitemap variants for regeneration. + * * @usage drush simple-sitemap:rebuild-queue * Rebuild the sitemap queue for all sitemap variants. + * @usage drush simple-sitemap:rebuild-queue --variants=default,test + * Rebuild the sitemap queue queuing only variants 'default' and 'test'. * * @validate-module-enabled simple_sitemap * * @aliases ssr, simple-sitemap-rebuild-queue + * + * @param array $options + * + * @throws \Drupal\Component\Plugin\Exception\PluginException */ - public function rebuildQueue() { - $this->generator->rebuildQueue(); - } + public function rebuildQueue(array $options = ['variants' => '']) { + $variants = array_keys($this->generator->getSitemapManager()->getSitemapVariants(NULL, FALSE)); + if (strlen($options['variants']) > 0) { + $chosen_variants = array_map('trim', array_filter(explode(',', $options['variants']))); + if (!empty($erroneous_variants = array_diff($chosen_variants, $variants))) { + $message = 'The following variants do not exist: ' . implode(', ', $erroneous_variants) + . '. Available variants are: ' . implode(', ', $variants) . '.'; + $this->logger()->log('error', $message); + return; + } + $variants = $chosen_variants; + } + + $this->generator->setVariants($variants)->rebuildQueue(); + $this->logger()->log('notice', 'The following variants have been queued for regeneration: ' . implode(', ', $variants) . '.'); + } } diff --git a/web/modules/simple_sitemap/src/Controller/SimplesitemapController.php b/web/modules/simple_sitemap/src/Controller/SimplesitemapController.php index b8c637e85b..524287f7c7 100644 --- a/web/modules/simple_sitemap/src/Controller/SimplesitemapController.php +++ b/web/modules/simple_sitemap/src/Controller/SimplesitemapController.php @@ -43,17 +43,16 @@ public static function create(ContainerInterface $container) { * or its sitemap index file. * Caches the response in case of expected output, prevents caching otherwise. * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * * @param string $variant * Optional name of sitemap variant. - * @see \hook_simple_sitemap_variants_alter() * @see SimplesitemapManager::getSitemapVariants() * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. - * * @throws NotFoundHttpException * - * @return object + * @return \Symfony\Component\HttpFoundation\Response|false * Returns an XML response. */ public function getSitemap(Request $request, $variant = NULL) { @@ -63,8 +62,49 @@ public function getSitemap(Request $request, $variant = NULL) { } return new Response($output, Response::HTTP_OK, [ - 'content-type' => 'application/xml', - 'X-Robots-Tag' => 'noindex', // Tell search engines not to index the sitemap itself. + 'Content-type' => 'application/xml; charset=utf-8', + 'X-Robots-Tag' => 'noindex, follow', + ]); + } + + /** + * Returns the XML stylesheet for the sitemap. + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function getSitemapXsl() { + + // Read the XSL content from the file. + $module_path = drupal_get_path('module', 'simple_sitemap'); + $xsl_content = file_get_contents($module_path . '/xsl/simple_sitemap.xsl'); + + // Replace custom tokens in the XSL content with appropriate values. + $replacements = [ + '[title]' => $this->t('Sitemap file'), + '[generated-by]' => $this->t('Generated by the <a href="@link">@module_name</a> Drupal module.', ['@link' => 'https://www.drupal.org/project/simple_sitemap', '@module_name' => 'Simple XML Sitemap']), + '[number-of-sitemaps]' => $this->t('Number of sitemaps in this index'), + '[sitemap-url]' => $this->t('Sitemap URL'), + '[number-of-urls]' => $this->t('Number of URLs in this sitemap'), + '[url-location]' => $this->t('URL location'), + '[lastmod]' => $this->t('Last modification date'), + '[changefreq]' => $this->t('Change frequency'), + '[priority]' => $this->t('Priority'), + '[translation-set]' => $this->t('Translation set'), + '[images]' => $this->t('Images'), + '[image-title]' => $this->t('Title'), + '[image-caption]' => $this->t('Caption'), + '[jquery]' => base_path() . 'core/assets/vendor/jquery/jquery.min.js', + '[jquery-tablesorter]' => base_path() . $module_path . '/xsl/jquery.tablesorter.min.js', + '[parser-date-iso8601]' => base_path() . $module_path . '/xsl/parser-date-iso8601.min.js', + '[xsl-js]' => base_path() . $module_path . '/xsl/simple_sitemap.xsl.js', + '[xsl-css]' => base_path() . $module_path . '/xsl/simple_sitemap.xsl.css', + ]; + + // Output the XSL content. + return new Response(strtr($xsl_content, $replacements), Response::HTTP_OK, [ + 'Content-type' => 'application/xml; charset=utf-8', + 'X-Robots-Tag' => 'noindex, nofollow', ]); } + } diff --git a/web/modules/simple_sitemap/src/EntityHelper.php b/web/modules/simple_sitemap/src/EntityHelper.php index d49bdb9bdc..906c7f7cfc 100644 --- a/web/modules/simple_sitemap/src/EntityHelper.php +++ b/web/modules/simple_sitemap/src/EntityHelper.php @@ -4,41 +4,69 @@ use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Database\Connection; use Drupal\Core\Url; /** - * Class EntityHelper + * Helper class for working with entities. + * * @package Drupal\simple_sitemap */ class EntityHelper { /** + * The entity type manager. + * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** - * @var \Drupal\Core\Database\Connection + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface */ - protected $db; + protected $entityTypeBundleInfo; /** * EntityHelper constructor. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager - * @param \Drupal\Core\Database\Connection $database + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) { + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * @param string $entity_type_id + * @return array + */ + public function getBundleInfo($entity_type_id) { + return $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + } + + /** + * @param string $entity_type_id + * @param string $bundle_name + * @return mixed */ - public function __construct(EntityTypeManagerInterface $entityTypeManager, Connection $database) { - $this->entityTypeManager = $entityTypeManager; - $this->db = $database; + public function getBundleLabel($entity_type_id, $bundle_name) { + $entity_info = $this->getBundleInfo($entity_type_id); + + return isset($entity_info[$bundle_name]['label']) + ? $entity_info[$bundle_name]['label'] + : $bundle_name; // Menu fix. } /** * Gets an entity's bundle name. * * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to get the bundle name for. + * * @return string + * The bundle of the entity. */ public function getEntityInstanceBundleName(EntityInterface $entity) { return $entity->getEntityTypeId() === 'menu_link_content' @@ -50,7 +78,10 @@ public function getEntityInstanceBundleName(EntityInterface $entity) { * Gets the entity type id for a bundle. * * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to get an entity type id for a bundle. + * * @return null|string + * The entity type for a bundle or NULL on failure. */ public function getBundleEntityTypeId(EntityInterface $entity) { return $entity->getEntityTypeId() === 'menu' @@ -65,24 +96,34 @@ public function getBundleEntityTypeId(EntityInterface $entity) { * Objects of entity types that can be indexed by the sitemap. */ public function getSupportedEntityTypes() { + return array_filter($this->entityTypeManager->getDefinitions(), [$this, 'supports']); + } - /** @var \Drupal\Core\Entity\ContentEntityTypeInterface[] $entity_types */ - $entity_types = $this->entityTypeManager->getDefinitions(); - foreach ($entity_types as $entity_type_id => $entity_type) { - if (!$entity_type instanceof ContentEntityTypeInterface - || !method_exists($entity_type, 'getBundleEntityType') - || !$entity_type->hasLinkTemplate('canonical')) { - unset($entity_types[$entity_type_id]); - } + /** + * Determines if an entity type is supported or not. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * @return bool + * TRUE if entity type supported by Simple Sitemap, FALSE if not. + */ + public function supports(EntityTypeInterface $entity_type) { + if (!$entity_type instanceof ContentEntityTypeInterface + || !method_exists($entity_type, 'getBundleEntityType') + || !$entity_type->hasLinkTemplate('canonical')) { + return FALSE; } - return $entity_types; - } + + return TRUE; + } /** * Checks whether an entity type does not provide bundles. * * @param string $entity_type_id + * The entity type ID. + * * @return bool + * TRUE if the entity type is atomic and FALSE otherwise. */ public function entityTypeIsAtomic($entity_type_id) { @@ -101,8 +142,14 @@ public function entityTypeIsAtomic($entity_type_id) { } /** + * Gets the entity from URL object. + * * @param \Drupal\Core\Url $url_object + * The URL object. + * * @return \Drupal\Core\Entity\EntityInterface|null + * An entity object. NULL if no matching entity is found. + * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ @@ -116,9 +163,16 @@ public function getEntityFromUrlObject(Url $url_object) { } /** + * Gets the entity IDs by entity type and bundle. + * * @param string $entity_type_id + * The entity type ID. * @param string|null $bundle_name - * @return array|int + * The bundle name. + * + * @return array + * An array of entity IDs + * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ @@ -141,24 +195,4 @@ public function getEntityInstanceIds($entity_type_id, $bundle_name = NULL) { return $entity_query->execute(); } - /** - * @param string $entity_type_name - * @param string $entity_id - * @return array - */ - public function getEntityImageUrls($entity_type_name, $entity_id) { - $query = $this->db->select('file_managed', 'fm'); - $query->fields('fm', ['uri']); - $query->join('file_usage', 'fu', 'fu.fid = fm.fid'); - $query->condition('fm.filemime', 'image/%', 'LIKE'); - $query->condition('fu.type', $entity_type_name); - $query->condition('fu.id', $entity_id); - - foreach ($query->execute() as $row) { - $imageUris[] = file_create_url($row->uri); - } - - return !empty($imageUris) ? $imageUris : []; - } - } diff --git a/web/modules/simple_sitemap/src/Form/FormHelper.php b/web/modules/simple_sitemap/src/Form/FormHelper.php index d11b7f810f..c0d3d56ec6 100644 --- a/web/modules/simple_sitemap/src/Form/FormHelper.php +++ b/web/modules/simple_sitemap/src/Form/FormHelper.php @@ -15,7 +15,6 @@ class FormHelper { use StringTranslationTrait; - const PRIORITY_DEFAULT = 0.5; const PRIORITY_HIGHEST = 10; const PRIORITY_DIVIDER = 10; @@ -60,15 +59,13 @@ class FormHelper { protected $instanceId; /** - * @var string + * @var array */ - protected $variant; + protected $settings; /** * @var array */ - protected $bundleSettings; - protected static $allowedFormOperations = [ 'default', 'edit', @@ -76,6 +73,9 @@ class FormHelper { 'register', ]; + /** + * @var array + */ protected static $changefreqValues = [ 'always', 'hourly', @@ -86,13 +86,18 @@ class FormHelper { 'never', ]; - protected static $valuesToCheck = [ - 'simple_sitemap_variant', - 'simple_sitemap_index_content', - 'simple_sitemap_priority', - 'simple_sitemap_changefreq', - 'simple_sitemap_include_images', - 'simple_sitemap_regenerate_now', + protected static $cronIntervals = [ + 1, + 3, + 6, + 12, + 24, + 48, + 72, + 96, + 120, + 144, + 168, ]; /** @@ -114,12 +119,16 @@ public function __construct( /** * @param \Drupal\Core\Form\FormStateInterface $form_state * @return bool + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * */ public function processForm(FormStateInterface $form_state) { $this->formState = $form_state; $this->cleanUpFormInfo(); $this->getEntityDataFromFormEntity(); - $this->negotiateVariant(); + $this->negotiateSettings(); + return $this->supports(); } @@ -145,6 +154,7 @@ public function getEntityCategory() { */ public function setEntityTypeId($entity_type_id) { $this->entityTypeId = $entity_type_id; + return $this; } @@ -161,6 +171,7 @@ public function getEntityTypeId() { */ public function setBundleName($bundle_name) { $this->bundleName = $bundle_name; + return $this; } @@ -177,6 +188,7 @@ public function getBundleName() { */ public function setInstanceId($instance_id) { $this->instanceId = $instance_id; + return $this; } @@ -207,157 +219,155 @@ protected function supports() { return FALSE; } - // Do not alter the form, if sitemap is disabled for the entity type of this - // entity instance. - elseif ($this->getEntityCategory() === 'instance') { - if (NULL === $this->variant || !$this->generator - ->setVariants($this->variant) - ->bundleIsIndexed($this->getEntityTypeId(), $this->getBundleName())) { - return FALSE; - } - } - return TRUE; } + /** + * @return bool + */ + public function entityIsNew() { + return !empty($entity = $this->getFormEntity()) ? $entity->isNew() : TRUE; + } + /** * @param array $form_fragment + * @return $this */ public function displayRegenerateNow(&$form_fragment) { $form_fragment['simple_sitemap_regenerate_now'] = [ '#type' => 'checkbox', - '#title' => $this->t('Regenerate sitemap after hitting <em>Save</em>'), + '#title' => $this->t('Regenerate all sitemaps after hitting <em>Save</em>'), '#description' => $this->t('This setting will regenerate all sitemaps including the above changes.'), '#default_value' => FALSE, ]; if ($this->generator->getSetting('cron_generate')) { - $form_fragment['simple_sitemap_regenerate_now']['#description'] .= '</br>' . $this->t('Otherwise the sitemap will be regenerated during a future cron run.'); + $form_fragment['simple_sitemap_regenerate_now']['#description'] .= '<br>' . $this->t('Otherwise the sitemaps will be regenerated during a future cron run.'); } - } - protected function negotiateVariant() { - $all_bundle_settings = $this->generator->setVariants(TRUE) - ->getBundleSettings($this->getEntityTypeId(), $this->getBundleName(), FALSE, TRUE); - $this->bundleSettings = NULL !== ($variant = key($all_bundle_settings)) - ? $all_bundle_settings[$variant] - : []; - $this->variant = $variant; + return $this; } /** - * @param array $form_fragment - * @param bool $multiple * @return $this + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ - public function displayEntitySettings(&$form_fragment, $multiple = FALSE) { - $prefix = $multiple ? $this->getEntityTypeId() . '_' : ''; + public function negotiateSettings() { + + $this->settings = $this->generator->setVariants(TRUE) + ->getBundleSettings($this->getEntityTypeId(), $this->getBundleName(), TRUE, TRUE); + if ($this->getEntityCategory() === 'instance') { + + //todo Should spit out variant => settings and not just settings; to do this, alter getEntityInstanceSettings() to include 'multiple variants' option. + foreach ($this->settings as $variant_name => $settings) { + if (NULL !== $instance_id = $this->getInstanceId()) { + $this->settings[$variant_name] = $this->generator + ->setVariants($variant_name) + ->getEntityInstanceSettings($this->getEntityTypeId(), $instance_id); + } + $this->settings[$variant_name]['bundle_settings'] = $settings; + } + } - $settings = $this->getEntityCategory() === 'instance' && NULL !== $this->variant && NULL !== $this->getInstanceId() - ? $this->generator->setVariants($this->variant)->getEntityInstanceSettings($this->getEntityTypeId(), $this->getInstanceId()) - : $this->bundleSettings; - Simplesitemap::supplementDefaultSettings('entity', $settings); + return $this; + } - $bundle_name = !empty($this->getBundleName()) ? $this->getBundleName() : $this->t('undefined'); + /** + * @param $form_fragment + * @return $this + */ + public function displayEntitySettings(&$form_fragment) { + $bundle_name = !empty($this->getBundleName()) + ? $this->entityHelper->getBundleLabel($this->getEntityTypeId(), $this->getBundleName()) + : $this->t('undefined'); + + $variants = $this->generator->getSitemapManager()->getSitemapVariants(NULL, FALSE); + $form_fragment['settings']['#markup'] = empty($variants) + ? $this->t('At least one sitemap variants needs to be defined for a bundle to be indexable.<br>Variants can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/variants']) + : '<strong>' . $this->t('Sitemap variants') . '</strong>'; + + foreach ($variants as $variant => $definition) { + $form_fragment['settings'][$variant] = [ + '#type' => 'details', + '#title' => '<em>' . $this->t($definition['label']) . '</em>', + '#open' => !empty($this->settings[$variant]['index']), + ]; - // Index - if (!$multiple) { - $form_fragment[$prefix . 'simple_sitemap_index_content'] = [ + // Disable fields of entity instance whose bundle is not indexed. + $form_fragment['settings'][$variant]['#disabled'] = $this->getEntityCategory() === 'instance' && empty($this->settings[$variant]['bundle_settings']['index']); + + // Index + $form_fragment['settings'][$variant]['index_' . $variant . '_' . $this->getEntityTypeId() . '_settings'] = [ '#type' => 'radios', - '#default_value' => (int) $settings['index'], + '#default_value' => (int) $this->settings[$variant]['index'], '#options' => [ - 0 => $this->getEntityCategory() === 'instance' - ? $this->t('Do not index this @bundle entity', ['@bundle' => $bundle_name]) - : $this->t('Do not index entities of this type'), - 1 => $this->getEntityCategory() === 'instance' - ? $this->t('Index this @bundle entity', ['@bundle' => $bundle_name]) - : $this->t('Index entities of this type'), + $this->getEntityCategory() === 'instance' + ? $this->t('Do not index this <em>@bundle</em> entity in variant <em>@variant_label</em>', ['@bundle' => $bundle_name, '@variant_label' => $this->t($variants[$variant]['label'])]) + : $this->t('Do not index entities of type <em>@bundle</em> in variant <em>@variant_label</em>', ['@bundle' => $bundle_name, '@variant_label' => $this->t($variants[$variant]['label'])]), + $this->getEntityCategory() === 'instance' + ? $this->t('Index this <em>@bundle entity</em> in variant <em>@variant_label</em>', ['@bundle' => $bundle_name, '@variant_label' => $this->t($variants[$variant]['label'])]) + : $this->t('Index entities of type <em>@bundle</em> in variant <em>@variant_label</em>', ['@bundle' => $bundle_name, '@variant_label' => $this->t($variants[$variant]['label'])]), ], + '#attributes' => ['class' => ['enabled-for-variant', $variant]], ]; - if ($this->getEntityCategory() === 'instance' && isset($this->bundleSettings['index'])) { - $form_fragment[$prefix . 'simple_sitemap_index_content']['#options'][(int) $this->bundleSettings['index']] .= ' <em>(' . $this->t('default') . ')</em>'; + if ($this->getEntityCategory() === 'instance' && isset($this->settings[$variant]['bundle_settings']['index'])) { + $form_fragment['settings'][$variant]['index_' . $variant . '_' . $this->getEntityTypeId() . '_settings']['#options'][(int) $this->settings[$variant]['bundle_settings']['index']] .= ' <em>(' . $this->t('default') . ')</em>'; } - } - - // Variant - $form_fragment[$prefix . 'simple_sitemap_variant'] = [ - '#type' => 'select', - '#title' => $this->t('Sitemap variant'), - '#description' => $this->t('The sitemap variant entities of this type are to be indexed in.'), - '#options' => array_map( - function($variant) { return $this->t($variant['label']); }, - $this->generator->getSitemapManager()->getSitemapVariants(NULL, FALSE) - ), - '#default_value' => $this->variant, - '#states' => [ - 'visible' => !$multiple - ? [':input[name="' . $prefix . 'simple_sitemap_index_content"]' => ['value' => 1]] - : [':input[name="' . $prefix . 'enabled"]' => ['checked' => TRUE]], - 'required' => !$multiple // todo Should implement server side validation on top of this. - ? [':input[name="' . $prefix . 'simple_sitemap_index_content"]' => ['value' => 1]] - : [':input[name="' . $prefix . 'enabled"]' => ['checked' => TRUE]], - ], - '#disabled' => $this->getEntityCategory() === 'instance' - ]; - // Priority - $form_fragment[$prefix . 'simple_sitemap_priority'] = [ - '#type' => 'select', - '#title' => $this->t('Priority'), - '#description' => $this->getEntityCategory() === 'instance' - ? $this->t('The priority this @bundle entity will have in the eyes of search engine bots.', ['@bundle' => $bundle_name]) - : $this->t('The priority entities of this type will have in the eyes of search engine bots.'), - '#default_value' => $settings['priority'], - '#options' => $this->getPrioritySelectValues(), - '#states' => [ - 'visible' => !$multiple - ? [':input[name="' . $prefix . 'simple_sitemap_index_content"]' => ['value' => 1]] - : [':input[name="' . $prefix . 'enabled"]' => ['checked' => TRUE]], - ], - ]; + // Priority + $form_fragment['settings'][$variant]['priority_' . $variant . '_' . $this->getEntityTypeId() . '_settings'] = [ + '#type' => 'select', + '#title' => $this->t('Priority'), + '#description' => $this->getEntityCategory() === 'instance' + ? $this->t('The priority this <em>@bundle</em> entity will have in the eyes of search engine bots.', ['@bundle' => $bundle_name]) + : $this->t('The priority entities of this type will have in the eyes of search engine bots.'), + '#default_value' => $this->settings[$variant]['priority'], + '#options' => $this->getPrioritySelectValues(), + '#states' => [ + 'visible' => [':input[name="index_' . $variant . '_' . $this->getEntityTypeId() . '_settings"]' => ['value' => 1]], + ], + ]; - if ($this->getEntityCategory() === 'instance' && isset($this->bundleSettings['priority'])) { - $form_fragment[$prefix . 'simple_sitemap_priority']['#options'][$this->formatPriority($this->bundleSettings['priority'])] .= ' (' . $this->t('default') . ')'; - } + if ($this->getEntityCategory() === 'instance' && isset($this->settings[$variant]['bundle_settings']['priority'])) { + $form_fragment['settings'][$variant]['priority_' . $variant . '_' . $this->getEntityTypeId() . '_settings']['#options'][$this->formatPriority($this->settings[$variant]['bundle_settings']['priority'])] .= ' (' . $this->t('default') . ')'; + } - // Changefreq - $form_fragment[$prefix . 'simple_sitemap_changefreq'] = [ - '#type' => 'select', - '#title' => $this->t('Change frequency'), - '#description' => $this->getEntityCategory() === 'instance' - ? $this->t('The frequency with which this @bundle entity changes. Search engine bots may take this as an indication of how often to index it.', ['@bundle' => $bundle_name]) - : $this->t('The frequency with which entities of this type change. Search engine bots may take this as an indication of how often to index them.'), - '#default_value' => $settings['changefreq'], - '#options' => $this->getChangefreqSelectValues(), - '#states' => [ - 'visible' => !$multiple - ? [':input[name="' . $prefix . 'simple_sitemap_index_content"]' => ['value' => 1]] - : [':input[name="' . $prefix . 'enabled"]' => ['checked' => TRUE]], - ], - ]; + // Changefreq + $form_fragment['settings'][$variant]['changefreq_' . $variant . '_' . $this->getEntityTypeId() . '_settings'] = [ + '#type' => 'select', + '#title' => $this->t('Change frequency'), + '#description' => $this->getEntityCategory() === 'instance' + ? $this->t('The frequency with which this <em>@bundle</em> entity changes. Search engine bots may take this as an indication of how often to index it.', ['@bundle' => $bundle_name]) + : $this->t('The frequency with which entities of this type change. Search engine bots may take this as an indication of how often to index them.'), + '#default_value' => $this->settings[$variant]['changefreq'], + '#options' => $this->getChangefreqSelectValues(), + '#states' => [ + 'visible' => [':input[name="index_' . $variant . '_' . $this->getEntityTypeId() . '_settings"]' => ['value' => 1]], + ], + ]; - if ($this->getEntityCategory() === 'instance' && isset($this->bundleSettings['changefreq'])) { - $form_fragment[$prefix . 'simple_sitemap_changefreq']['#options'][$this->bundleSettings['changefreq']] .= ' (' . $this->t('default') . ')'; - } + if ($this->getEntityCategory() === 'instance' && isset($this->settings[$variant]['bundle_settings']['changefreq'])) { + $form_fragment['settings'][$variant]['changefreq_' . $variant . '_' . $this->getEntityTypeId() . '_settings']['#options'][$this->settings[$variant]['bundle_settings']['changefreq']] .= ' (' . $this->t('default') . ')'; + } - // Images - $form_fragment[$prefix . 'simple_sitemap_include_images'] = [ - '#type' => 'select', - '#title' => $this->t('Include images'), - '#description' => $this->getEntityCategory() === 'instance' - ? $this->t('Determines if images referenced by this @bundle entity should be included in the sitemap.', ['@bundle' => $bundle_name]) - : $this->t('Determines if images referenced by entities of this type should be included in the sitemap.'), - '#default_value' => (int) $settings['include_images'], - '#options' => [0 => $this->t('No'), 1 => $this->t('Yes')], - '#states' => [ - 'visible' => !$multiple - ? [':input[name="' . $prefix . 'simple_sitemap_index_content"]' => ['value' => 1]] - : [':input[name="' . $prefix . 'enabled"]' => ['checked' => TRUE]], - ], - ]; + // Images + $form_fragment['settings'][$variant]['include_images_' . $variant . '_' . $this->getEntityTypeId() . '_settings'] = [ + '#type' => 'select', + '#title' => $this->t('Include images'), + '#description' => $this->getEntityCategory() === 'instance' + ? $this->t('Determines if images referenced by this <em>@bundle</em> entity should be included in the sitemap.', ['@bundle' => $bundle_name]) + : $this->t('Determines if images referenced by entities of this type should be included in the sitemap.'), + '#default_value' => (int) $this->settings[$variant]['include_images'], + '#options' => [$this->t('No'), $this->t('Yes')], + '#states' => [ + 'visible' => [':input[name="index_' . $variant . '_' . $this->getEntityTypeId() . '_settings"]' => ['value' => 1]], + ], + ]; - if ($this->getEntityCategory() === 'instance' && isset($this->bundleSettings['include_images'])) { - $form_fragment[$prefix . 'simple_sitemap_include_images']['#options'][(int) $this->bundleSettings['include_images']] .= ' (' . $this->t('default') . ')'; + if ($this->getEntityCategory() === 'instance' && isset($this->settings[$variant]['bundle_settings']['include_images'])) { + $form_fragment['settings'][$variant]['include_images_' . $variant . '_' . $this->getEntityTypeId() . '_settings']['#options'][(int) $this->settings[$variant]['bundle_settings']['include_images']] .= ' (' . $this->t('default') . ')'; + } } return $this; @@ -404,7 +414,7 @@ protected function getEntityDataFromFormEntity() { $this->setEntityTypeId($entity_type_id); $this->setBundleName($this->entityHelper->getEntityInstanceBundleName($form_entity)); // New menu link's id is '' instead of NULL, hence checking for empty. - $this->setInstanceId(!empty($form_entity->id()) ? $form_entity->id() : NULL); + $this->setInstanceId(!$this->entityIsNew() ? $form_entity->id() : NULL); break; default: @@ -416,7 +426,7 @@ protected function getEntityDataFromFormEntity() { /** * Gets the object entity of the form if available. * - * @return \Drupal\Core\Entity\Entity|false + * @return \Drupal\Core\Entity\EntityBase|false * Entity or FALSE if non-existent or if form operation is * 'delete'. */ @@ -428,6 +438,7 @@ protected function getFormEntity() { && in_array($form_object->getOperation(), self::$allowedFormOperations)) { return $form_object->getEntity(); } + return FALSE; } @@ -436,25 +447,17 @@ protected function getFormEntity() { * * Needed because this service may contain form info from the previous * operation when revived from the container. + * + * @return $this */ - protected function cleanUpFormInfo() { + public function cleanUpFormInfo() { $this->entityCategory = NULL; $this->entityTypeId = NULL; $this->bundleName = NULL; $this->instanceId = NULL; - $this->variant = NULL; - $this->bundleSettings = NULL; - } + $this->settings = NULL; - /** - * Gets new entity Id after entity creation. - * To be used in an entity form submit. - * - * @return int - * Entity ID. - */ - public function getFormEntityId() { - return $this->formState->getFormObject()->getEntity()->id(); + return $this; } /** @@ -468,20 +471,60 @@ public function getFormEntityId() { * TRUE if simple_sitemap form values have been altered by the user. */ public function valuesChanged($form, array $values) { - foreach (self::$valuesToCheck as $field_name) { - if (!isset($form['simple_sitemap'][$field_name]['#default_value']) - || (isset($values[$field_name]) && $values[$field_name] != $form['simple_sitemap'][$field_name]['#default_value'])) { - return TRUE; - } - } - return FALSE; +// foreach (self::$valuesToCheck as $field_name) { +// if (!isset($form['simple_sitemap'][$field_name]['#default_value']) +// || (isset($values[$field_name]) && $values[$field_name] != $form['simple_sitemap'][$field_name]['#default_value'])) { +// return TRUE; +// } +// } +// +// return FALSE; + + //todo + return TRUE; + } + + /** + * Gets the values needed to display the variant dropdown setting. + * + * @return array + */ + public function getVariantSelectValues() { + return array_map( + function($variant) { return $this->t($variant['label']); }, + $this->generator->getSitemapManager()->getSitemapVariants(NULL, FALSE) + ); + } + + /** + * Returns correct default value for variant select list. + * + * If only one variant is available, return it, otherwise check if a default + * variant is provided and return it. + * + * @param string|null $default_value + * Actual default value from the database. + * + * @return string|null + * Value to be set on form. + */ + public function getVariantSelectValuesDefault($default_value) { + $options = $this->getVariantSelectValues(); + return NULL === $default_value + ? (1 === count($options) + ? array_keys($options)[0] + : (!empty($default = $this->generator->getSetting('default_variant')) + ? $default + : $default_value + ) + ) + : $default_value; } /** * Gets the values needed to display the priority dropdown setting. * * @return array - * Select options. */ public function getPrioritySelectValues() { $options = []; @@ -489,6 +532,7 @@ public function getPrioritySelectValues() { $value = $this->formatPriority($value / self::PRIORITY_DIVIDER); $options[$value] = $value; } + return $options; } @@ -496,13 +540,13 @@ public function getPrioritySelectValues() { * Gets the values needed to display the changefreq dropdown setting. * * @return array - * Select options. */ public function getChangefreqSelectValues() { $options = ['' => $this->t('- Not specified -')]; - foreach (self::$changefreqValues as $setting) { + foreach (self::getChangefreqOptions() as $setting) { $options[$setting] = $this->t($setting); } + return $options; } @@ -536,4 +580,25 @@ public static function isValidPriority($priority) { public static function isValidChangefreq($changefreq) { return in_array($changefreq, self::$changefreqValues); } + + /** + * @return array + */ + public static function getCronIntervalOptions() { + /** @var \Drupal\Core\Datetime\DateFormatter $formatter */ + $formatter = \Drupal::service('date.formatter'); + $intervals = array_flip(self::$cronIntervals); + foreach ($intervals as $interval => &$label) { + $label = $formatter->formatInterval($interval * 60 * 60); + } + + return [0 => t('On every cron run')] + $intervals; + } + + /** + * @return string + */ + public static function getDonationText() { + return '<div class="description">' . t('If you would like to say thanks and support the development of this module, a <a target="_blank" href="@url">donation</a> will be much appreciated.', ['@url' => 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5AFYRSBLGSC3W']) . '</div>'; + } } diff --git a/web/modules/simple_sitemap/src/Form/SimplesitemapCustomLinksForm.php b/web/modules/simple_sitemap/src/Form/SimplesitemapCustomLinksForm.php index 1b2e409388..5af84391dc 100644 --- a/web/modules/simple_sitemap/src/Form/SimplesitemapCustomLinksForm.php +++ b/web/modules/simple_sitemap/src/Form/SimplesitemapCustomLinksForm.php @@ -63,21 +63,21 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => $this->t('Custom links'), '#type' => 'fieldset', '#markup' => '<div class="description">' . $this->t('Add custom internal drupal paths to the XML sitemap.') . '</div>', - '#prefix' => $this->getDonationText(), + '#prefix' => FormHelper::getDonationText(), ]; $form['simple_sitemap_custom']['custom_links'] = [ '#type' => 'textarea', '#title' => $this->t('Relative Drupal paths'), '#default_value' => $this->customLinksToString($this->generator->setVariants(TRUE)->getCustomLinks(NULL, FALSE)), - '#description' => $this->t("Please specify drupal internal (relative) paths, one per line. Do not forget to prepend the paths with a '/'.<br/>Optionally link priority <em>(0.0 - 1.0)</em> can be added by appending it after a space.<br/> Optionally link change frequency <em>(always / hourly / daily / weekly / monthly / yearly / never)</em> can be added by appending it after a space.<br/><br/><strong>Examples:</strong><br/><em>/ 1.0 daily</em> -> home page with the highest priority and daily change frequency<br/><em>/contact</em> -> contact page with the default priority and no change frequency information"), + '#description' => $this->t("Please specify drupal internal (relative) paths, one per line. Do not forget to prepend the paths with a '/'.<br>Optionally link priority <em>(0.0 - 1.0)</em> can be added by appending it after a space.<br> Optionally link change frequency <em>(always / hourly / daily / weekly / monthly / yearly / never)</em> can be added by appending it after a space.<br/<br><strong>Examples:</strong><br><em>/ 1.0 daily</em> -> home page with the highest priority and daily change frequency<br><em>/contact</em> -> contact page with the default priority and no change frequency information"), ]; $form['simple_sitemap_custom']['variants'] = [ '#type' => 'select', '#multiple' => TRUE, '#title' => $this->t('Sitemap variants'), - '#description' => $this->t('The sitemap variants to include the above links in.<br/>Variants can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/variants']), + '#description' => $this->t('The sitemap variants to include the above links in.<br>Variants can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/variants']), '#options' => array_map( function($variant) { return $this->t($variant['label']); }, $this->generator->getSitemapManager()->getSitemapVariants(NULL, FALSE) @@ -163,7 +163,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Regenerate sitemaps according to user setting. if ($form_state->getValue('simple_sitemap_regenerate_now')) { - $this->generator->rebuildQueue()->generateSitemap(); + $this->generator->setVariants(TRUE) + ->rebuildQueue() + ->generateSitemap(); } } diff --git a/web/modules/simple_sitemap/src/Form/SimplesitemapEntitiesForm.php b/web/modules/simple_sitemap/src/Form/SimplesitemapEntitiesForm.php index 190503c342..5bd498eae3 100644 --- a/web/modules/simple_sitemap/src/Form/SimplesitemapEntitiesForm.php +++ b/web/modules/simple_sitemap/src/Form/SimplesitemapEntitiesForm.php @@ -59,29 +59,38 @@ public function getFormId() { */ public function buildForm(array $form, FormStateInterface $form_state) { - $form['simple_sitemap_entities']['#prefix'] = $this->getDonationText(); + $form['simple_sitemap_entities']['#prefix'] = FormHelper::getDonationText(); $form['simple_sitemap_entities']['entities'] = [ '#title' => $this->t('Sitemap entities'), '#type' => 'fieldset', - '#markup' => '<div class="description">' . $this->t('Simple XML sitemap settings will be added only to entity forms of entity types enabled here. For all entity types featuring bundles (e.g. <em>node</em>) sitemap settings have to be set on their bundle pages (e.g. <em>page</em>).') . '</div>', + '#markup' => '<div class="description">' . $this->t('Simple XML Sitemap settings will be added only to entity forms of entity types enabled here. For all entity types featuring bundles (e.g. <em>node</em>) sitemap settings have to be set on their bundle pages (e.g. <em>page</em>).') . '</div>', ]; $form['#attached']['library'][] = 'simple_sitemap/sitemapEntities'; $form['#attached']['drupalSettings']['simple_sitemap'] = ['all_entities' => [], 'atomic_entities' => []]; + $variants = $this->generator->getSitemapManager()->getSitemapVariants(NULL, FALSE); + $all_bundle_settings = $this->generator->setVariants(TRUE)->getBundleSettings(NULL, NULL, TRUE, TRUE); + $indexed_bundles = []; + foreach ($all_bundle_settings as $variant => $entity_types) { + foreach ($entity_types as $entity_type_name => $bundles) { + foreach ($bundles as $bundle_name => $bundle_settings) { + if (!empty($bundle_settings['index'])) { + $indexed_bundles[$entity_type_name][$bundle_name]['variants'][] = $this->t($variants[$variant]['label']); + $indexed_bundles[$entity_type_name][$bundle_name]['bundle_label'] = $this->entityHelper->getBundleLabel($entity_type_name, $bundle_name); + } + } + } + } + $entity_type_labels = []; foreach ($this->entityHelper->getSupportedEntityTypes() as $entity_type_id => $entity_type) { $entity_type_labels[$entity_type_id] = $entity_type->getLabel() ? : $entity_type_id; } asort($entity_type_labels); - $this->formHelper->processForm($form_state); - - $bundle_settings = $this->generator->getBundleSettings(); - foreach ($entity_type_labels as $entity_type_id => $entity_type_label) { -; $enabled_entity_type = $this->generator->entityTypeIsEnabled($entity_type_id); $atomic_entity_type = $this->entityHelper->entityTypeIsAtomic($entity_type_id); $css_entity_type_id = str_replace('_', '-', $entity_type_id); @@ -94,34 +103,36 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['simple_sitemap_entities']['entities'][$entity_type_id][$entity_type_id . '_enabled'] = [ '#type' => 'checkbox', - '#title' => $this->t('Enable @entity_type_label <em>(@entity_type_id)</em> support', ['@entity_type_label' => strtolower($entity_type_label), '@entity_type_id' => $entity_type_id]), + '#title' => $this->t('Enable @entity_type_label <em>(@entity_type_id)</em> support', ['@entity_type_label' => $entity_type_label, '@entity_type_id' => $entity_type_id]), '#description' => $atomic_entity_type - ? $this->t('Sitemap settings for the entity type <em>@entity_type_label</em> can be set below and overridden on its entity pages.', ['@entity_type_label' => strtolower($entity_type_label)]) - : $this->t('Sitemap settings for the entity type <em>@entity_type_label</em> can be set on its bundle pages and overridden on its entity pages.', ['@entity_type_label' => strtolower($entity_type_label)]), + ? $this->t('Sitemap settings for the entity type <em>@entity_type_label</em> can be set below and overridden on its entity pages.', ['@entity_type_label' => $entity_type_label]) + : $this->t('Sitemap settings for the entity type <em>@entity_type_label</em> can be set on its bundle pages and overridden on its entity pages.', ['@entity_type_label' => $entity_type_label]), '#default_value' => $enabled_entity_type, ]; if ($form['simple_sitemap_entities']['entities'][$entity_type_id][$entity_type_id . '_enabled']['#default_value']) { - $bundle_info = ''; - $indexed_bundles = isset($bundle_settings[$entity_type_id]) - ? implode(array_keys(array_filter($bundle_settings[$entity_type_id], function ($val) {return $val['index'];})), ', ') - : ''; + $indexed_bundles_string = ''; + if (isset($indexed_bundles[$entity_type_id])) { + foreach ($indexed_bundles[$entity_type_id] as $bundle => $bundle_data) { + $indexed_bundles_string .= '<br><em>' . $bundle_data['bundle_label'] . '</em> <span class="description">(' . $this->t('sitemap variants') . ': <em>' . implode(', ', $bundle_data['variants']) . '</em>)</span>'; + } + } + $bundle_info = ''; if (!$atomic_entity_type) { $bundle_info .= '<div id="indexed-bundles-' . $css_entity_type_id . '">' - . (!empty($indexed_bundles) - ? $this->t("<em>@entity_type_label</em> bundles set to be indexed:", ['@entity_type_label' => ucfirst(strtolower($entity_type_label))]) . ' ' . '<em>' . $indexed_bundles . '</em>' - : $this->t('No <em>@entity_type_label</em> bundles are set to be indexed yet.', ['@entity_type_label' => strtolower($entity_type_label)])) + . (!empty($indexed_bundles_string) + ? $this->t("<em>@entity_type_label</em> bundles set to be indexed:", ['@entity_type_label' => $entity_type_label]) . ' ' . $indexed_bundles_string + : $this->t('No <em>@entity_type_label</em> bundles are set to be indexed yet.', ['@entity_type_label' => $entity_type_label])) . '</div>'; } - if (!empty($indexed_bundles)) { + if (!empty($indexed_bundles_string)) { $bundle_info .= '<div id="warning-' . $css_entity_type_id . '">' . ($atomic_entity_type ? $this->t("<strong>Warning:</strong> This entity type's sitemap settings including per-entity overrides will be deleted after hitting <em>Save</em>.") - : $this->t("<strong>Warning:</strong> The sitemap settings for <em>@bundles</em> and any per-entity overrides will be deleted after hitting <em>Save</em>.", - ['@bundles' => $indexed_bundles])) + : $this->t("<strong>Warning:</strong> The sitemap settings and any per-entity overrides will be deleted for the following bundles:" . $indexed_bundles_string)) . '</div>'; } @@ -131,11 +142,18 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['#attached']['drupalSettings']['simple_sitemap']['all_entities'][] = $css_entity_type_id; if ($atomic_entity_type) { - $this->formHelper->setEntityCategory('bundle') + $form['simple_sitemap_entities']['entities'][$entity_type_id][$entity_type_id . '_settings']['#prefix'] = '<div id="indexed-bundles-' . $css_entity_type_id . '">'; + $form['simple_sitemap_entities']['entities'][$entity_type_id][$entity_type_id . '_settings']['#suffix'] = '</div>'; + + $this->formHelper + ->cleanUpFormInfo() + ->setEntityCategory('bundle') ->setEntityTypeId($entity_type_id) ->setBundleName($entity_type_id) - ->displayEntitySettings($form['simple_sitemap_entities']['entities'][$entity_type_id][$entity_type_id . '_settings'], TRUE); - $form['#attached']['drupalSettings']['simple_sitemap']['atomic_entities'][] = $css_entity_type_id; + ->negotiateSettings() + ->displayEntitySettings( + $form['simple_sitemap_entities']['entities'][$entity_type_id][$entity_type_id . '_settings'] + ); } } @@ -155,25 +173,18 @@ public function submitForm(array &$form, FormStateInterface $form_state) { if ($value) { $this->generator->enableEntityType($entity_type_id); if ($this->entityHelper->entityTypeIsAtomic($entity_type_id)) { - - // Deleting bundle settings for old bundle. - // See simple_sitemap.module::simple_sitemap_entity_form_submit(). - // todo: This will not be necessary if "multiple variants pro bundle" is implemented. - if (isset($form['simple_sitemap_entities']['entities'][$entity_type_id][$entity_type_id . '_settings'][$entity_type_id . '_simple_sitemap_variant']['#default_value'])) { - $old_variant = $form['simple_sitemap_entities']['entities'][$entity_type_id][$entity_type_id . '_settings'][$entity_type_id . '_simple_sitemap_variant']['#default_value']; - if ($old_variant !== $values[$entity_type_id . '_simple_sitemap_variant']) { - $this->generator->setVariants($old_variant)->removeBundleSettings($entity_type_id); + foreach ($this->generator->getSitemapManager()->getSitemapVariants(NULL, FALSE) as $variant => $definition) { + if (isset($values['index_' . $variant . '_' . $entity_type_id . '_settings'])) { + $this->generator + ->setVariants($variant) + ->setBundleSettings($entity_type_id, $entity_type_id, [ + 'index' => (bool) $values['index_' . $variant . '_' . $entity_type_id . '_settings'], + 'priority' => $values['priority_' . $variant . '_' . $entity_type_id . '_settings'], + 'changefreq' => $values['changefreq_' . $variant . '_' . $entity_type_id . '_settings'], + 'include_images' => (bool) $values['include_images_' . $variant . '_' . $entity_type_id . '_settings'], + ]); } } - - $this->generator - ->setVariants($values[$entity_type_id . '_simple_sitemap_variant']) - ->setBundleSettings($entity_type_id, $entity_type_id, [ - 'index' => TRUE, - 'priority' => $values[$entity_type_id . '_simple_sitemap_priority'], - 'changefreq' => $values[$entity_type_id . '_simple_sitemap_changefreq'], - 'include_images' => (bool) $values[$entity_type_id . '_simple_sitemap_include_images'], - ]); } } else { @@ -185,7 +196,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Regenerate sitemaps according to user setting. if ($form_state->getValue('simple_sitemap_regenerate_now')) { - $this->generator->generateSitemap(); + $this->generator->setVariants(TRUE) + ->rebuildQueue() + ->generateSitemap(); } } diff --git a/web/modules/simple_sitemap/src/Form/SimplesitemapFormBase.php b/web/modules/simple_sitemap/src/Form/SimplesitemapFormBase.php index e796a72307..ac5118d411 100644 --- a/web/modules/simple_sitemap/src/Form/SimplesitemapFormBase.php +++ b/web/modules/simple_sitemap/src/Form/SimplesitemapFormBase.php @@ -52,11 +52,4 @@ protected function getEditableConfigNames() { return ['simple_sitemap.settings']; } - /** - * - */ - protected function getDonationText() { - return '<div class="description">' . $this->t('If you would like to say thanks and support the development of this module, a <a target="_blank" href="@url">donation</a> will be much appreciated.', ['@url' => 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5AFYRSBLGSC3W']) . '</div>'; - } - } diff --git a/web/modules/simple_sitemap/src/Form/SimplesitemapSettingsForm.php b/web/modules/simple_sitemap/src/Form/SimplesitemapSettingsForm.php index 92835b3e51..eb9fdd1397 100644 --- a/web/modules/simple_sitemap/src/Form/SimplesitemapSettingsForm.php +++ b/web/modules/simple_sitemap/src/Form/SimplesitemapSettingsForm.php @@ -6,8 +6,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\simple_sitemap\Simplesitemap; use Drupal\Component\Utility\UrlHelper; -use Drupal\Core\Language\LanguageManager; -use Drupal\Core\Database\Connection; +use Drupal\Core\Language\LanguageManagerInterface; /** * Class SimplesitemapSettingsForm @@ -20,30 +19,22 @@ class SimplesitemapSettingsForm extends SimplesitemapFormBase { */ protected $languageManager; - /** - * @var \Drupal\Core\Database\Connection - */ - protected $db; - /** * SimplesitemapSettingsForm constructor. * @param \Drupal\simple_sitemap\Simplesitemap $generator * @param \Drupal\simple_sitemap\Form\FormHelper $form_helper - * @param \Drupal\Core\Language\LanguageManager $language_manager - * @param \Drupal\Core\Database\Connection $database + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager */ public function __construct( Simplesitemap $generator, FormHelper $form_helper, - LanguageManager $language_manager, - Connection $database + LanguageManagerInterface $language_manager ) { parent::__construct( $generator, $form_helper ); $this->languageManager = $language_manager; - $this->db = $database; } /** @@ -53,8 +44,7 @@ public static function create(ContainerInterface $container) { return new static( $container->get('simple_sitemap.generator'), $container->get('simple_sitemap.form_helper'), - $container->get('language_manager'), - $container->get('database') + $container->get('language_manager') ); } @@ -70,134 +60,7 @@ public function getFormId() { */ public function buildForm(array $form, FormStateInterface $form_state) { - $form['simple_sitemap_settings']['#prefix'] = $this->getDonationText(); - - $form['simple_sitemap_settings']['status'] = [ - '#type' => 'fieldset', - '#title' => $this->t('Sitemap status'), - '#markup' => '<div class="description">' . $this->t('Sitemaps can be regenerated on demand here.') . '</div>', - '#description' => $this->t('Variants can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/variants']), - ]; - - $form['simple_sitemap_settings']['status']['actions'] = [ - '#prefix' => '<div class="clearfix"><div class="form-item">', - '#suffix' => '</div></div>', - ]; - - $form['simple_sitemap_settings']['status']['actions']['regenerate_submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Generate from queue'), - '#submit' => ['::generateSitemap'], - '#validate' => [], - ]; - -// $form['simple_sitemap_settings']['status']['actions']['regenerate_backend_submit'] = [ -// '#type' => 'submit', -// '#value' => $this->t('Generate from queue (background)'), -// '#submit' => ['::generateSitemapBackend'], -// '#validate' => [], -// ]; - - $form['simple_sitemap_settings']['status']['actions']['rebuild_queue_submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Rebuild queue'), - '#submit' => ['::rebuildQueue'], - '#validate' => [], - ]; - - $form['simple_sitemap_settings']['status']['progress'] = [ - '#prefix' => '<div class="clearfix">', - '#suffix' => '</div>', - ]; - - $form['simple_sitemap_settings']['status']['progress']['title']['#markup'] = $this->t('Progress of sitemap regeneration'); - - $queue_worker = $this->generator->getQueueWorker(); - $total_count = $queue_worker->getInitialElementCount(); - if (!empty($total_count)) { - $indexed_count = $queue_worker->getProcessedElementCount(); - $percent = round(100 * $indexed_count / $total_count); - - // With all results processed, there still may be some stashed results to be indexed. - $percent = $percent === 100 && $queue_worker->generationInProgress() ? 99 : $percent; - - $index_progress = [ - '#theme' => 'progress_bar', - '#percent' => $percent, - '#message' => t('@indexed out of @total items have been processed.', ['@indexed' => $indexed_count, '@total' => $total_count]), - ]; - $form['simple_sitemap_settings']['status']['progress']['bar']['#markup'] = render($index_progress); - } - else { - $form['simple_sitemap_settings']['status']['progress']['bar']['#markup'] = '<div class="description">' . $this->t('There are no items to be indexed.') . '</div>'; - } - - $sitemap_manager = $this->generator->getSitemapManager(); - $sitemap_statuses = $this->fetchSitemapInstanceStatuses(); - - foreach ($sitemap_manager->getSitemapTypes() as $type_name => $type_definition) { - if (!empty($variants = $sitemap_manager->getSitemapVariants($type_name, FALSE))) { - $form['simple_sitemap_settings']['status']['types'][$type_name] = [ - '#type' => 'details', - '#title' => '<em>' . $type_definition['label'] . '</em> ' . $this->t('sitemaps'), - '#open' => !empty($variants) && count($variants) <= 5, - '#description' => !empty($type_definition['description']) ? '<div class="description">' . $type_definition['description'] . '</div>' : '', - ]; - $form['simple_sitemap_settings']['status']['types'][$type_name]['table'] = [ - '#type' => 'table', - '#header' => [$this->t('Variant'), $this->t('Status'), /*$this->t('Actions')*/], - '#attributes' => ['class' => ['form-item', 'clearfix']], - ]; - foreach ($variants as $variant_name => $variant_definition) { - $row = []; - $row['name']['data']['#markup'] = '<span title="' . $variant_name . '">' . $variant_definition['label'] . '</span>'; - if (!isset($sitemap_statuses[$variant_name])) { - $row['status'] = $this->t('pending'); - } - else { - $url = $GLOBALS['base_url'] . '/' . $variant_name . '/sitemap.xml'; - switch ($sitemap_statuses[$variant_name]) { - case 0: - $row['status'] = $this->t('generating'); - break; - case 1: - $row['status']['data']['#markup'] = $this->t('<a href="@url" target="_blank">published</a>', ['@url' => $url]); - break; - case 2: - $row['status'] = $this->t('<a href="@url" target="_blank">published</a>, regenerating', ['@url' => $url]); - break; - } - } - -// $row['actions'] = ''; - $form['simple_sitemap_settings']['status']['types'][$type_name]['table']['#rows'][$variant_name] = $row; - unset($sitemap_statuses[$variant_name]); - } - } - } - if (empty($form['simple_sitemap_settings']['status']['types'])) { - $form['simple_sitemap_settings']['status']['types']['#markup'] = $this->t('No variants have been defined'); - } - -/* if (!empty($sitemap_statuses)) { - $form['simple_sitemap_settings']['status']['types']['&orphans'] = [ - '#type' => 'details', - '#title' => $this->t('Orphans'), - '#open' => TRUE, - ]; - - $form['simple_sitemap_settings']['status']['types']['&orphans']['table'] = [ - '#type' => 'table', - '#header' => [$this->t('Variant'), $this->t('Status'), $this->t('Actions')], - ]; - foreach ($sitemap_statuses as $orphan_name => $orphan_info) { - $form['simple_sitemap_settings']['status']['types']['&orphans']['table']['#rows'][$orphan_name] = [ - 'name' => $orphan_name, - 'status' => $this->t('orphaned'), - 'actions' => '', - ]; - } - }*/ + $form['simple_sitemap_settings']['#prefix'] = FormHelper::getDonationText(); $form['simple_sitemap_settings']['settings'] = [ '#type' => 'fieldset', @@ -216,47 +79,45 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => $this->t('Sitemap generation interval'), '#description' => $this->t('The sitemap will be generated according to this interval.'), '#default_value' => $this->generator->getSetting('cron_generate_interval', 0), - '#options' => [ - 0 => $this->t('On every cron run'), - 1 => $this->t('Once an hour'), - 3 => $this->t('Once every @hours hours', ['@hours' => 3]), - 6 => $this->t('Once every @hours hours', ['@hours' => 6]), - 12 => $this->t('Once every @hours hours', ['@hours' => 12]), - 24 => $this->t('Once a day'), - 48 => $this->t('Once every @days days', ['@days' => 48/24]), - 72 => $this->t('Once every @days days', ['@days' => 72/24]), - 96 => $this->t('Once every @days days', ['@days' => 96/24]), - 120 => $this->t('Once every @days days', ['@days' => 120/24]), - 144 => $this->t('Once every @days days', ['@days' => 144/24]), - 168 => $this->t('Once a week'), - ], + '#options' => FormHelper::getCronIntervalOptions(), '#states' => [ - 'visible' => [ - ':input[name="cron_generate"]' => ['checked' => TRUE], - ], + 'visible' => [':input[name="cron_generate"]' => ['checked' => TRUE]], ], ]; + $form['simple_sitemap_settings']['settings']['xsl'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Add styling and sorting to sitemaps'), + '#description' => $this->t('If checked, sitemaps will be displayed as tables with sortable entries and thus become much friendlier towards human visitors. Search engines will not care.'), + '#default_value' => $this->generator->getSetting('xsl', TRUE), + ]; + $form['simple_sitemap_settings']['settings']['languages'] = [ '#type' => 'details', '#title' => $this->t('Language settings'), '#open' => FALSE, ]; - $language_options = []; - foreach ($this->languageManager->getLanguages() as $language) { - if (!$language->isDefault()) { - $language_options[$language->getId()] = $language->getName(); - } - } + $form['simple_sitemap_settings']['settings']['languages']['disable_language_hreflang'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Remove hreflang markup in HTML'), + '#description' => $this->t('Google recommends displaying hreflang definitions either in the HTML markup or in the sitemap, but not in both places.<br>If checked, hreflang definitions created by the language module will be removed from the markup reducing its size.'), + '#default_value' => $this->generator->getSetting('disable_language_hreflang', FALSE), + ]; $form['simple_sitemap_settings']['settings']['languages']['skip_untranslated'] = [ '#type' => 'checkbox', '#title' => $this->t('Skip non-existent translations'), - '#description' => $this->t('If checked, entity links are generated exclusively for languages the entity has been translated to as long as the language is not excluded below.<br/>Otherwise entity links are generated for every language installed on the site apart from languages excluded below.<br/>Bear in mind that non-entity paths like homepage will always be generated for every non-excluded language.'), + '#description' => $this->t('If checked, entity links are generated exclusively for languages the entity has been translated to as long as the language is not excluded below.<br>Otherwise entity links are generated for every language installed on the site apart from languages excluded below.<br>Bear in mind that non-entity paths like homepage will always be generated for every non-excluded language.'), '#default_value' => $this->generator->getSetting('skip_untranslated', FALSE), ]; + $language_options = []; + foreach ($this->languageManager->getLanguages() as $language) { + if (!$language->isDefault()) { + $language_options[$language->getId()] = $language->getName(); + } + } $form['simple_sitemap_settings']['settings']['languages']['excluded_languages'] = [ '#title' => $this->t('Exclude languages'), '#type' => 'checkboxes', @@ -278,7 +139,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['simple_sitemap_settings']['advanced']['default_variant'] = [ '#type' => 'select', '#title' => $this->t('Default sitemap variant'), - '#description' => $this->t('This sitemap variant will be available under <em>/sitemap.xml</em> in addition to its default path <em>/variant-name/sitemap.xml</em>.<br/>Variants can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/variants']), + '#description' => $this->t('This sitemap variant will be available under <em>/sitemap.xml</em> in addition to its default path <em>/variant-name/sitemap.xml</em>.<br>Variants can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/variants']), '#default_value' => isset($variants[$default_variant]) ? $default_variant : '', '#options' => ['' => $this->t('- None -')] + array_map(function($variant) { return $this->t($variant['label']); }, $variants), ]; @@ -288,13 +149,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => $this->t('Default base URL'), '#default_value' => $this->generator->getSetting('base_url', ''), '#size' => 30, - '#description' => $this->t('On some hosting providers it is impossible to pass parameters to cron to tell Drupal which URL to bootstrap with. In this case the base URL of sitemap links can be overridden here.<br/>Example: <em>@url</em>', ['@url' => $GLOBALS['base_url']]), + '#description' => $this->t('On some hosting providers it is impossible to pass parameters to cron to tell Drupal which URL to bootstrap with. In this case the base URL of sitemap links can be overridden here.<br>Example: <em>@url</em>', ['@url' => $GLOBALS['base_url']]), ]; $form['simple_sitemap_settings']['advanced']['remove_duplicates'] = [ '#type' => 'checkbox', '#title' => $this->t('Exclude duplicate links'), - '#description' => $this->t('Prevent per-sitemap variant duplicate links.<br/>Uncheck this to significantly speed up the sitemap generation process on a huge site (more than 20 000 indexed entities).'), + '#description' => $this->t('Prevent per-sitemap variant duplicate links.<br>Unchecking this may help avoiding PHP memory errors on huge sites.'), '#default_value' => $this->generator->getSetting('remove_duplicates', TRUE), ]; @@ -302,7 +163,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'number', '#title' => $this->t('Maximum links in a sitemap'), '#min' => 1, - '#description' => $this->t('The maximum number of links one sitemap can hold. If more links are generated than set here, a sitemap index will be created and the links split into several sub-sitemaps.<br/>50 000 links is the maximum Google will parse per sitemap, but an equally important consideration is generation performance: Splitting sitemaps into chunks <em>greatly</em> increases it.<br/>If left blank, all links will be shown on a single sitemap.'), + '#description' => $this->t('The maximum number of links one sitemap can hold. If more links are generated than set here, a sitemap index will be created and the links split into several sub-sitemaps.<br>50 000 links is the maximum Google will parse per sitemap, but choosing a lower value may be needed to avoid PHP memory errors on huge sites.<br>If left blank, all links will be shown on a single sitemap.'), '#default_value' => $this->generator->getSetting('max_links'), ]; @@ -310,7 +171,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'number', '#title' => $this->t('Sitemap generation max duration'), '#min' => 1, - '#description' => $this->t('The maximum duration <strong>in seconds</strong> the generation task can run during a single cron run or during one batch process iteration.<br/>The higher the number, the quicker the generation process, but higher the risk of PHP timeout errors.'), + '#description' => $this->t('The maximum duration <strong>in seconds</strong> the generation task can run during a single cron run or during one batch process iteration.<br>The higher the number, the quicker the generation process, but higher the risk of PHP timeout errors.'), '#default_value' => $this->generator->getSetting('generate_duration', 10000) / 1000, '#required' => TRUE, ]; @@ -320,29 +181,6 @@ public function buildForm(array $form, FormStateInterface $form_state) { return parent::buildForm($form, $form_state); } - /** - * @return array - * Array of sitemap statuses keyed by variant name. - * Status values: - * 0: Instance is unpublished - * 1: Instance is published - * 2: Instance is published but is being regenerated - */ - protected function fetchSitemapInstanceStatuses() { - $results = $this->db - ->query('SELECT type, status FROM {simple_sitemap} GROUP BY type, status') - ->fetchAll(); - - $instances = []; - foreach ($results as $i => $result) { - $instances[$result->type] = isset($instances[$result->type]) - ? $result->status + 1 - : (int) $result->status; - } - - return $instances; - } - /** * {@inheritdoc} */ @@ -350,7 +188,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { $base_url = $form_state->getValue('base_url'); $form_state->setValue('base_url', rtrim($base_url, '/')); if ($base_url !== '' && !UrlHelper::isValid($base_url, TRUE)) { - $form_state->setErrorByName('base_url', t('The base URL is invalid.')); + $form_state->setErrorByName('base_url', $this->t('The base URL is invalid.')); } } @@ -363,8 +201,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { 'cron_generate_interval', 'remove_duplicates', 'skip_untranslated', + 'xsl', 'base_url', - 'default_variant'] as $setting_name) { + 'default_variant', + 'disable_language_hreflang'] as $setting_name) { $this->generator->saveSetting($setting_name, $form_state->getValue($setting_name)); } $this->generator->saveSetting('excluded_languages', array_filter($form_state->getValue('excluded_languages'))); @@ -374,36 +214,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Regenerate sitemaps according to user setting. if ($form_state->getValue('simple_sitemap_regenerate_now')) { - $this->generator->rebuildQueue()->generateSitemap(); + $this->generator->setVariants(TRUE) + ->rebuildQueue() + ->generateSitemap(); } } - - /** - * @param array $form - * @param \Drupal\Core\Form\FormStateInterface $form_state - * @throws \Drupal\Component\Plugin\Exception\PluginException - */ - public function generateSitemap(array &$form, FormStateInterface $form_state) { - $this->generator->generateSitemap(); - } - - /** - * @param array $form - * @param \Drupal\Core\Form\FormStateInterface $form_state - * @throws \Drupal\Component\Plugin\Exception\PluginException - */ - public function generateSitemapBackend (array &$form, FormStateInterface $form_state) { - $this->generator->generateSitemap('backend'); - } - - - /** - * @param array $form - * @param \Drupal\Core\Form\FormStateInterface $form_state - * @throws \Drupal\Component\Plugin\Exception\PluginException - */ - public function rebuildQueue(array &$form, FormStateInterface $form_state) { - $this->generator->rebuildQueue(); - } - } diff --git a/web/modules/simple_sitemap/src/Form/SimplesitemapSitemapsForm.php b/web/modules/simple_sitemap/src/Form/SimplesitemapSitemapsForm.php new file mode 100644 index 0000000000..b637a3ab2e --- /dev/null +++ b/web/modules/simple_sitemap/src/Form/SimplesitemapSitemapsForm.php @@ -0,0 +1,252 @@ +<?php + +namespace Drupal\simple_sitemap\Form; + +use Drupal\Core\Datetime\DateFormatter; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\simple_sitemap\Simplesitemap; +use Drupal\Core\Database\Connection; + +/** + * Class SimplesitemapSitemapsForm + * @package Drupal\simple_sitemap\Form + */ +class SimplesitemapSitemapsForm extends SimplesitemapFormBase { + + /** + * @var \Drupal\Core\Database\Connection + */ + protected $db; + + /** + * @var \Drupal\Core\Datetime\DateFormatter + */ + protected $dateFormatter; + + /** + * SimplesitemapSitemapsForm constructor. + * @param \Drupal\simple_sitemap\Simplesitemap $generator + * @param \Drupal\simple_sitemap\Form\FormHelper $form_helper + * @param \Drupal\Core\Database\Connection $database + * @param \Drupal\Core\Datetime\DateFormatter $date_formatter + */ + public function __construct( + Simplesitemap $generator, + FormHelper $form_helper, + Connection $database, + DateFormatter $date_formatter + ) { + parent::__construct( + $generator, + $form_helper + ); + $this->db = $database; + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('simple_sitemap.generator'), + $container->get('simple_sitemap.form_helper'), + $container->get('database'), + $container->get('date.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'simple_sitemap_sitemaps_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $form['simple_sitemap_settings']['#prefix'] = FormHelper::getDonationText(); + $form['simple_sitemap_settings']['#attached']['library'][] = 'simple_sitemap/sitemaps'; + $queue_worker = $this->generator->getQueueWorker(); + + $form['simple_sitemap_settings']['status'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Sitemap status'), + '#markup' => '<div class="description">' . $this->t('Sitemaps can be regenerated on demand here.') . '</div>', + '#description' => $this->t('Variants can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/variants']), + ]; + + $form['simple_sitemap_settings']['status']['actions'] = [ + '#prefix' => '<div class="clearfix"><div class="form-item">', + '#suffix' => '</div></div>', + ]; + + $form['simple_sitemap_settings']['status']['actions']['rebuild_queue_submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Rebuild queue'), + '#submit' => ['::rebuildQueue'], + '#validate' => [], + ]; + + $form['simple_sitemap_settings']['status']['actions']['regenerate_submit'] = [ + '#type' => 'submit', + '#value' => $queue_worker->generationInProgress() + ? $this->t('Resume generation') + : $this->t('Rebuild queue & generate'), + '#submit' => ['::generateSitemap'], + '#validate' => [], + ]; + + $form['simple_sitemap_settings']['status']['progress'] = [ + '#prefix' => '<div class="clearfix">', + '#suffix' => '</div>', + ]; + + $form['simple_sitemap_settings']['status']['progress']['title']['#markup'] = $this->t('Progress of sitemap regeneration'); + + $total_count = $queue_worker->getInitialElementCount(); + if (!empty($total_count)) { + $indexed_count = $queue_worker->getProcessedElementCount(); + $percent = round(100 * $indexed_count / $total_count); + + // With all results processed, there still may be some stashed results to be indexed. + $percent = $percent === 100 && $queue_worker->generationInProgress() ? 99 : $percent; + + $index_progress = [ + '#theme' => 'progress_bar', + '#percent' => $percent, + '#message' => $this->t('@indexed out of @total items have been processed.<br>Each sitemap variant is published after all of its items have been processed.', ['@indexed' => $indexed_count, '@total' => $total_count]), + ]; + $form['simple_sitemap_settings']['status']['progress']['bar']['#markup'] = render($index_progress); + } + else { + $form['simple_sitemap_settings']['status']['progress']['bar']['#markup'] = '<div class="description">' . $this->t('There are no items to be indexed.') . '</div>'; + } + + $sitemap_manager = $this->generator->getSitemapManager(); + $sitemap_settings = [ + 'base_url' => $this->generator->getSetting('base_url', ''), + 'default_variant' => $this->generator->getSetting('default_variant', NULL), + ]; + $sitemap_statuses = $this->fetchSitemapInstanceStatuses(); + $published_timestamps = $this->fetchSitemapInstancePublishedTimestamps(); + foreach ($sitemap_manager->getSitemapTypes() as $type_name => $type_definition) { + if (!empty($variants = $sitemap_manager->getSitemapVariants($type_name, FALSE))) { + $sitemap_generator = $sitemap_manager + ->getSitemapGenerator($type_definition['sitemapGenerator']) + ->setSettings($sitemap_settings); + + $form['simple_sitemap_settings']['status']['types'][$type_name] = [ + '#type' => 'details', + '#title' => '<em>' . $type_definition['label'] . '</em> ' . $this->t('sitemaps'), + '#open' => !empty($variants) && count($variants) <= 5, + '#description' => !empty($type_definition['description']) ? '<div class="description">' . $type_definition['description'] . '</div>' : '', + ]; + $form['simple_sitemap_settings']['status']['types'][$type_name]['table'] = [ + '#type' => 'table', + '#header' => [$this->t('Variant'), $this->t('Status')], + '#attributes' => ['class' => ['form-item', 'clearfix']], + ]; + foreach ($variants as $variant_name => $variant_definition) { + $row = []; + switch ($sitemap_statuses[$variant_name]) { + case 0: + $row['name']['data']['#markup'] = '<span title="' . $variant_name . '">' . $this->t($variant_definition['label']) . '</span>'; + break; + case 1: + case 2: + $row['name']['data']['#markup'] = $this->t('<a href="@url" target="_blank">@variant</a>', + ['@url' => $sitemap_generator->setSitemapVariant($variant_name)->getSitemapUrl(), '@variant' => $this->t($variant_definition['label'])] + ); + break; + } + if (!isset($sitemap_statuses[$variant_name])) { + $row['status'] = $this->t('pending'); + } + else { + switch ($sitemap_statuses[$variant_name]) { + case 0: + $row['status'] = $this->t('generating'); + break; + case 1: + $row['status'] = $this->t('published on @time', ['@time' => $this->dateFormatter->format($published_timestamps[$variant_name])] + ); + break; + case 2: + $row['status'] = $this->t('published on @time, regenerating', + ['@url' => $sitemap_generator->setSitemapVariant($variant_name)->getSitemapUrl(), '@time' => $this->dateFormatter->format($published_timestamps[$variant_name])] + ); + break; + } + } + $form['simple_sitemap_settings']['status']['types'][$type_name]['table']['#rows'][$variant_name] = $row; + unset($sitemap_statuses[$variant_name]); + } + } + } + if (empty($form['simple_sitemap_settings']['status']['types'])) { + $form['simple_sitemap_settings']['status']['types']['#markup'] = $this->t('No variants have been defined'); + } + + return $form; + } + + /** + * @return array + * Array of sitemap statuses keyed by variant name. + * Status values: + * 0: Instance is unpublished + * 1: Instance is published + * 2: Instance is published but is being regenerated + * + * @todo Move to SitemapGeneratorBase or DefaultSitemapGenerator so it can be overwritten by sitemap types with custom storages. + */ + protected function fetchSitemapInstanceStatuses() { + $results = $this->db + ->query('SELECT type, status FROM {simple_sitemap} GROUP BY type, status') + ->fetchAll(); + + $instances = []; + foreach ($results as $i => $result) { + $instances[$result->type] = isset($instances[$result->type]) + ? $result->status + 1 + : (int) $result->status; + } + + return $instances; + } + + /** + * @return array + * + * @todo Move to SitemapGeneratorBase or DefaultSitemapGenerator so it can be overwritten by sitemap types with custom storages. + */ + protected function fetchSitemapInstancePublishedTimestamps() { + return $this->db + ->query('SELECT type, MAX(sitemap_created) FROM (SELECT sitemap_created, type FROM {simple_sitemap} WHERE status = :status) AS timestamps GROUP BY type', [':status' => 1]) + ->fetchAllKeyed(0, 1); + } + + /** + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function generateSitemap(array &$form, FormStateInterface $form_state) { + $this->generator->generateSitemap(); + } + + /** + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function rebuildQueue(array &$form, FormStateInterface $form_state) { + $this->generator->rebuildQueue(); + } + +} diff --git a/web/modules/simple_sitemap/src/Form/SimplesitemapVariantsForm.php b/web/modules/simple_sitemap/src/Form/SimplesitemapVariantsForm.php index d1e1ac533c..0bba1066db 100644 --- a/web/modules/simple_sitemap/src/Form/SimplesitemapVariantsForm.php +++ b/web/modules/simple_sitemap/src/Form/SimplesitemapVariantsForm.php @@ -26,19 +26,19 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['simple_sitemap_variants'] = [ '#title' => $this->t('Sitemap variants'), '#type' => 'fieldset', - '#markup' => '<div class="description">' . $this->t('Define sitemap variants. A sitemap variant is a sitemap instance of a certain type (specific sitemap generator and URL generators) accessible under a certain URL.<br/>Each variant can have its own entity bundle settings (to be defined on bundle edit pages).') . '</div>', - '#prefix' => $this->getDonationText(), + '#markup' => '<div class="description">' . $this->t('Define sitemap variants. A sitemap variant is a sitemap instance of a certain type (specific sitemap generator and URL generators) accessible under a certain URL.<br>Each variant can have its own entity bundle settings (to be defined on bundle edit pages).') . '</div>', + '#prefix' => FormHelper::getDonationText(), ]; $form['simple_sitemap_variants']['variants'] = [ '#type' => 'textarea', '#title' => $this->t('Variants'), '#default_value' => $this->variantsToString($this->generator->getSitemapManager()->getSitemapVariants(NULL, TRUE)), - '#description' => $this->t("Please specify sitemap variants, one per line. <strong>Caution: </strong>Removing variants here will delete their bundle settings, custom links and corresponding sitemap instances.<br/><br/>A variant definition consists of the variant name (used as the variant's path), the sitemap type it belongs to (optional) and the variant label (optional). These three values have to be separated by the | pipe | symbol.<br/><br/><strong>Examples:</strong><br/><em>default | default_hreflang | Default</em> -> variant of the <em>default_hreflang</em> sitemap type and <em>Default</em> as label; accessible under <em>/default/sitemap.xml</em><br/><em>test</em> -> variant of the <em>@default_sitemap_type</em> sitemap type and <em>test</em> as label; accessible under <em>/test/sitemap.xml</em><br/><br/><strong>Available sitemap types:</strong>", ['@default_sitemap_type' => SimplesitemapManager::DEFAULT_SITEMAP_TYPE]), + '#description' => $this->t("Please specify sitemap variants, one per line. <strong>Caution: </strong>Removing variants here will delete their bundle settings, custom links and corresponding sitemap instances.<br><br>A variant definition consists of the variant name (used as the variant's path), the sitemap type it belongs to (optional) and the variant label (optional). These three values have to be separated by the | pipe | symbol.<br><br><strong>Examples:</strong><br><em>default | default_hreflang | Default</em> -> variant of the <em>default_hreflang</em> sitemap type and <em>Default</em> as label; accessible under <em>/default/sitemap.xml</em><br><em>test</em> -> variant of the <em>@default_sitemap_type</em> sitemap type and <em>test</em> as label; accessible under <em>/test/sitemap.xml</em><br><br><strong>Available sitemap types:</strong>", ['@default_sitemap_type' => SimplesitemapManager::DEFAULT_SITEMAP_TYPE]), ]; foreach ($this->generator->getSitemapManager()->getSitemapTypes() as $sitemap_type => $definition) { - $form['simple_sitemap_variants']['variants']['#description'] .= '<br/>' . '<em>' . $sitemap_type . '</em>' . (!empty($definition['description']) ? (': ' . $definition['description']) : ''); + $form['simple_sitemap_variants']['variants']['#description'] .= '<br>' . '<em>' . $sitemap_type . '</em>' . (!empty($definition['description']) ? (': ' . $definition['description']) : ''); } $this->formHelper->displayRegenerateNow($form['simple_sitemap_custom']); @@ -67,7 +67,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { $form_state->setErrorByName('', $this->t("<strong>Line @line</strong>: The variant name cannot be empty.", $placeholders)); } - if (!preg_match('/^[\w-_]+$/', $variant_name)) { + if (!preg_match('/^[\w\-_]+$/', $variant_name)) { $form_state->setErrorByName('', $this->t("<strong>Line @line</strong>: The variant name <em>@name</em> can only include alphanumeric characters, dashes and underscores.", $placeholders)); } @@ -127,6 +127,7 @@ protected function stringToVariants($variant_string) { $variants[$name]['type'] = !empty($variant_settings[1]) ? trim($variant_settings[1]) : SimplesitemapManager::DEFAULT_SITEMAP_TYPE; $variants[$name]['label'] = !empty($variant_settings[2]) ? trim($variant_settings[2]) : $name; } + return $variants; } @@ -142,6 +143,7 @@ protected function variantsToString(array $variants) { . ' | ' . $variant_definition['label'] . "\r\n"; } + return $variants_string; } } diff --git a/web/modules/simple_sitemap/src/Logger.php b/web/modules/simple_sitemap/src/Logger.php index 42c6daa9f2..081c8ee876 100644 --- a/web/modules/simple_sitemap/src/Logger.php +++ b/web/modules/simple_sitemap/src/Logger.php @@ -2,7 +2,7 @@ namespace Drupal\simple_sitemap; -use Drupal\Core\Messenger\Messenger; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Session\AccountProxyInterface; use Psr\Log\LoggerInterface; @@ -31,7 +31,7 @@ class Logger { protected $logger; /** - * @var \Drupal\Core\Messenger\Messenger + * @var \Drupal\Core\Messenger\MessengerInterface */ protected $messenger; @@ -53,12 +53,12 @@ class Logger { /** * Logger constructor. * @param \Psr\Log\LoggerInterface $logger - * @param \Drupal\Core\Messenger\Messenger $messenger + * @param \Drupal\Core\Messenger\MessengerInterface $messenger * @param \Drupal\Core\Session\AccountProxyInterface $current_user */ public function __construct( LoggerInterface $logger, - Messenger $messenger, + MessengerInterface $messenger, AccountProxyInterface $current_user ) { $this->logger = $logger; @@ -83,6 +83,7 @@ public function m($message, $substitutions = []) { */ public function log($logSeverityLevel = self::LOG_SEVERITY_LEVEL_DEFAULT) { $this->logger->$logSeverityLevel(strtr($this->message, $this->substitutions)); + return $this; } @@ -95,6 +96,7 @@ public function display($displayMessageType = self::DISPLAY_MESSAGE_TYPE_DEFAULT if (empty($permission) || $this->currentUser->hasPermission($permission)) { $this->messenger->addMessage($this->t($this->message, $this->substitutions), $displayMessageType); } + return $this; } } diff --git a/web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariant.php b/web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariantIn.php similarity index 80% rename from web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariant.php rename to web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariantIn.php index f0999cccc9..265c91d9d0 100644 --- a/web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariant.php +++ b/web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariantIn.php @@ -6,10 +6,10 @@ use Symfony\Component\HttpFoundation\Request; /** - * Class PathProcessorSitemapVariant + * Class PathProcessorSitemapVariantIn * @package Drupal\simple_sitemap\PathProcessor */ -class PathProcessorSitemapVariant implements InboundPathProcessorInterface { +class PathProcessorSitemapVariantIn implements InboundPathProcessorInterface { /** * {@inheritdoc} diff --git a/web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariantOut.php b/web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariantOut.php new file mode 100644 index 0000000000..1af3da2ef0 --- /dev/null +++ b/web/modules/simple_sitemap/src/PathProcessor/PathProcessorSitemapVariantOut.php @@ -0,0 +1,27 @@ +<?php + +namespace Drupal\simple_sitemap\PathProcessor; + +use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; +use Symfony\Component\HttpFoundation\Request; +use Drupal\Core\Render\BubbleableMetadata; + +/** + * Class PathProcessorSitemapVariantOut + * @package Drupal\simple_sitemap\PathProcessor + */ +class PathProcessorSitemapVariantOut implements OutboundPathProcessorInterface { + + /** + * {@inheritdoc} + */ + public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) { + $args = explode('/', $path); + if (count($args) === 4 && $args[3] === 'sitemap.xml') { + $path = '/' . $args[2] . '/sitemap.xml'; + } + + return $path; + } + +} diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php old mode 100644 new mode 100755 index 71f40ef594..1c95134a43 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php @@ -3,10 +3,6 @@ namespace Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Database\Connection; -use Drupal\Core\Extension\ModuleHandler; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Component\Datetime\Time; /** * Class DefaultSitemapGenerator @@ -37,56 +33,6 @@ class DefaultSitemapGenerator extends SitemapGeneratorBase { 'xmlns:image' => self::XMLNS_IMAGE, ]; - /** - * DefaultSitemapGenerator constructor. - * @param array $configuration - * @param string $plugin_id - * @param mixed $plugin_definition - * @param \Drupal\Core\Database\Connection $database - * @param \Drupal\Core\Extension\ModuleHandler $module_handler - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * @param \Drupal\Component\Datetime\Time $time - * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapWriter $sitemapWriter - */ - public function __construct( - array $configuration, - $plugin_id, - $plugin_definition, - Connection $database, - ModuleHandler $module_handler, - LanguageManagerInterface $language_manager, - Time $time, - SitemapWriter $sitemapWriter - ) { - parent::__construct( - $configuration, - $plugin_id, - $plugin_definition, - $database, - $module_handler, - $language_manager, - $time, - $sitemapWriter - ); - } - - public static function create( - ContainerInterface $container, - array $configuration, - $plugin_id, - $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('database'), - $container->get('module_handler'), - $container->get('language_manager'), - $container->get('datetime.time'), - $container->get('simple_sitemap.sitemap_writer') - ); - } - /** * Generates and returns a sitemap chunk. * @@ -99,11 +45,27 @@ public static function create( protected function getXml(array $links) { $this->writer->openMemory(); $this->writer->setIndent(TRUE); - $this->writer->startDocument(self::XML_VERSION, self::ENCODING); - $this->writer->writeComment(self::GENERATED_BY); + $this->writer->startSitemapDocument(); + + // Add the XML stylesheet to document if enabled. + if ($this->settings['xsl']) { + $this->writer->writeXsl(); + } + + $this->writer->writeGeneratedBy(); $this->writer->startElement('urlset'); + $this->addSitemapAttributes(); + $this->addLinks($links); + $this->writer->endElement(); + $this->writer->endDocument(); + + return $this->writer->outputMemory(); + } - // Add attributes to document. + /** + * Adds attributes to the sitemap. + */ + protected function addSitemapAttributes() { $attributes = self::$attributes; if (!$this->isHreflangSitemap()) { unset($attributes['xmlns:xhtml']); @@ -113,72 +75,103 @@ protected function getXml(array $links) { foreach ($attributes as $name => $value) { $this->writer->writeAttribute($name, $value); } + } - // Add URLs to document. + /** + * Adds URL elements to the sitemap. + * + * @param array $links + */ + protected function addLinks(array $links) { $sitemap_variant = $this->sitemapVariant; $this->moduleHandler->alter('simple_sitemap_links', $links, $sitemap_variant); - foreach ($links as $link) { - - // Add each translation variant URL as location to the sitemap. + foreach ($links as $url_data) { $this->writer->startElement('url'); - $this->writer->writeElement('loc', $link['url']); - - // If more than one language is enabled, add all translation variant URLs - // as alternate links to this location turning the sitemap into a hreflang - // sitemap. - if (isset($link['alternate_urls']) && $this->isHreflangSitemap()) { - foreach ($link['alternate_urls'] as $language_id => $alternate_url) { - $this->writer->startElement('xhtml:link'); - $this->writer->writeAttribute('rel', 'alternate'); - $this->writer->writeAttribute('hreflang', $language_id); - $this->writer->writeAttribute('href', $alternate_url); - $this->writer->endElement(); - } - } + $this->addUrl($url_data); + $this->writer->endElement(); + } + } - // Add lastmod if any. - if (isset($link['lastmod'])) { - $this->writer->writeElement('lastmod', $link['lastmod']); - } + /** + * Adds a URL element to the sitemap. + * + * @param array $url_data + * The array of properties for this URL. + */ + protected function addUrl(array $url_data) { + $this->writer->writeElement('loc', $url_data['url']); + + // If more than one language is enabled, add all translation variant URLs + // as alternate links to this link turning the sitemap into a hreflang + // sitemap. + if (isset($url_data['alternate_urls']) && $this->isHreflangSitemap()) { + $this->addAlternateUrls($url_data['alternate_urls']); + } - // Add changefreq if any. - if (isset($link['changefreq'])) { - $this->writer->writeElement('changefreq', $link['changefreq']); - } + // Add lastmod if any. + if (isset($url_data['lastmod'])) { + $this->writer->writeElement('lastmod', $url_data['lastmod']); + } - // Add priority if any. - if (isset($link['priority'])) { - $this->writer->writeElement('priority', $link['priority']); - } + // Add changefreq if any. + if (isset($url_data['changefreq'])) { + $this->writer->writeElement('changefreq', $url_data['changefreq']); + } + + // Add priority if any. + if (isset($url_data['priority'])) { + $this->writer->writeElement('priority', $url_data['priority']); + } - // Add images if any. - if (!empty($link['images'])) { - foreach ($link['images'] as $image) { - $this->writer->startElement('image:image'); - $this->writer->writeElement('image:loc', $image['path']); - $this->writer->endElement(); + // Add images if any. + if (!empty($url_data['images'])) { + foreach ($url_data['images'] as $image) { + $this->writer->startElement('image:image'); + $this->writer->writeElement('image:loc', $image['path']); + if (strlen($image['title']) > 0) { + $this->writer->writeElement('image:title', $image['title']); } + if (strlen($image['alt']) > 0) { + $this->writer->writeElement('image:caption', $image['alt']); + } + $this->writer->endElement(); } + } + } + /** + * Adds all translation variant URLs as alternate URLs to a URL. + * + * @param array $alternate_urls + */ + protected function addAlternateUrls(array $alternate_urls) { + foreach ($alternate_urls as $language_id => $alternate_url) { + $this->writer->startElement('xhtml:link'); + $this->addAlternateUrl($language_id, $alternate_url); $this->writer->endElement(); } - $this->writer->endElement(); - $this->writer->endDocument(); + } - return $this->writer->outputMemory(); + /** + * Adds a translation variant URL as alternate URL to a URL. + * + * @param $language_id + * @param $alternate_url + */ + protected function addAlternateUrl($language_id, $alternate_url) { + $this->writer->writeAttribute('rel', 'alternate'); + $this->writer->writeAttribute('hreflang', $language_id); + $this->writer->writeAttribute('href', $alternate_url); } /** + * Checks if sitemap is hreflang compliant. + * * @return bool */ protected function isHreflangSitemap() { - if (NULL === $this->isHreflangSitemap) { - $this->isHreflangSitemap = count( - array_diff_key($this->languageManager->getLanguages(), - $this->settings['excluded_languages']) - ) > 1; - } - return $this->isHreflangSitemap; + return NULL !== $this->isHreflangSitemap + ? $this->isHreflangSitemap + : self::isMultilingualSitemap(); } - } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorBase.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorBase.php index 9524534654..937ed0160e 100644 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorBase.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorBase.php @@ -2,12 +2,15 @@ namespace Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator; +use Drupal\Core\Url; +use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl; use Drupal\simple_sitemap\Plugin\simple_sitemap\SimplesitemapPluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ModuleHandler; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Component\Datetime\Time; +use Drupal\Core\Language\LanguageInterface; /** * Class SitemapGeneratorBase @@ -17,10 +20,6 @@ abstract class SitemapGeneratorBase extends SimplesitemapPluginBase implements S const FIRST_CHUNK_DELTA = 1; const INDEX_DELTA = 0; - - const GENERATED_BY = 'Generated by the Simple XML sitemap Drupal module: https://drupal.org/project/simple_sitemap.'; - const XML_VERSION = '1.0'; - const ENCODING = 'UTF-8'; const XMLNS = 'http://www.sitemaps.org/schemas/sitemap/0.9'; /** @@ -58,6 +57,11 @@ abstract class SitemapGeneratorBase extends SimplesitemapPluginBase implements S */ protected $sitemapVariant; + /** + * @var array + */ + protected $sitemapUrlSettings; + /** * @var array */ @@ -92,7 +96,6 @@ public function __construct( $this->languageManager = $language_manager; $this->time = $time; $this->writer = $sitemap_writer; - $this->sitemapVariant = $this->settings['default_variant']; } public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { @@ -114,6 +117,7 @@ public static function create(ContainerInterface $container, array $configuratio */ public function setSitemapVariant($sitemap_variant) { $this->sitemapVariant = $sitemap_variant; + return $this; } @@ -147,8 +151,14 @@ protected function getChunkInfo() { protected function getIndexXml(array $chunk_info) { $this->writer->openMemory(); $this->writer->setIndent(TRUE); - $this->writer->startDocument(self::XML_VERSION, self::ENCODING); - $this->writer->writeComment(self::GENERATED_BY); + $this->writer->startSitemapDocument(); + + // Add the XML stylesheet to document if enabled. + if ($this->settings['xsl']) { + $this->writer->writeXsl(); + } + + $this->writer->writeGeneratedBy(); $this->writer->startElement('sitemapindex'); // Add attributes to document. @@ -162,9 +172,8 @@ protected function getIndexXml(array $chunk_info) { // Add sitemap chunk locations to document. foreach ($chunk_info as $chunk_data) { $this->writer->startElement('sitemap'); - $this->writer->writeElement('loc', $this->getCustomBaseUrl() - . '/' . (!$this->isDefaultVariant() ? ($chunk_data->type . '/') : '') . 'sitemap.xml?page=' . $chunk_data->delta); - $this->writer->writeElement('lastmod', date_iso8601($chunk_data->sitemap_created)); + $this->writer->writeElement('loc', $this->getSitemapUrl($chunk_data->delta)); + $this->writer->writeElement('lastmod', date('c', $chunk_data->sitemap_created)); $this->writer->endElement(); } @@ -270,8 +279,9 @@ public function generateIndex() { * @return $this */ public function publish() { - $unpublished_chunk = $this->db->query('SELECT MAX(id) FROM {simple_sitemap} WHERE type = :type AND status = :status', [':type' => $this->sitemapVariant, ':status' => 0]) - ->fetchField(); + $unpublished_chunk = $this->db->query('SELECT MAX(id) FROM {simple_sitemap} WHERE type = :type AND status = :status', [ + ':type' => $this->sitemapVariant, ':status' => 0 + ])->fetchField(); // Only allow publishing a sitemap variant if there is an unpublished // sitemap variant, as publishing involves deleting the currently published @@ -290,6 +300,7 @@ public function publish() { */ public function setSettings(array $settings) { $this->settings = $settings; + return $this; } @@ -298,6 +309,53 @@ public function setSettings(array $settings) { */ protected function getCustomBaseUrl() { $customBaseUrl = $this->settings['base_url']; + return !empty($customBaseUrl) ? $customBaseUrl : $GLOBALS['base_url']; } + + protected function getSitemapUrlSettings() { + if (NULL === $this->sitemapUrlSettings) { + $this->sitemapUrlSettings = [ + 'absolute' => TRUE, + 'base_url' => $this->getCustomBaseUrl(), + 'language' => $this->languageManager->getLanguage(LanguageInterface::LANGCODE_NOT_APPLICABLE), + ]; + } + + return $this->sitemapUrlSettings; + } + + /** + * @param null $delta + * @return \Drupal\Core\GeneratedUrl|string + */ + public function getSitemapUrl($delta = NULL) { + $parameters = NULL !== $delta ? ['page' => $delta] : []; + $url = $this->isDefaultVariant() + ? Url::fromRoute( + 'simple_sitemap.sitemap_default', + $parameters, + $this->getSitemapUrlSettings()) + : Url::fromRoute( + 'simple_sitemap.sitemap_variant', + $parameters + ['variant' => $this->sitemapVariant], + $this->getSitemapUrlSettings() + ); + + return $url->toString(); + } + + /** + * @return bool + */ + public static function isMultilingualSitemap() { + $has_multiple_indexable_languages = count( + array_diff_key(\Drupal::languageManager()->getLanguages(), + \Drupal::service('simple_sitemap.generator')->getSetting('excluded_languages')) + ) > 1; + + return $has_multiple_indexable_languages + && \Drupal::service('language_negotiator')->isNegotiationMethodEnabled(LanguageNegotiationUrl::METHOD_ID); + } + } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorInterface.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorInterface.php index a44095a334..db9bfdd304 100644 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorInterface.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorInterface.php @@ -8,15 +8,17 @@ */ interface SitemapGeneratorInterface { - function setSitemapVariant($sitemap_variant); + public function setSitemapVariant($sitemap_variant); - function setSettings(array $settings); + public function setSettings(array $settings); - function generate(array $links); + public function generate(array $links); - function generateIndex(); + public function generateIndex(); - function publish(); + public function publish(); - function remove(); + public function remove(); + + public function getSitemapUrl($delta = NULL); } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapWriter.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapWriter.php index 7085162b6e..f9a764a05c 100644 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapWriter.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapWriter.php @@ -2,12 +2,32 @@ namespace Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator; -use XMLWriter; +use Drupal\Core\Url; /** * Class SitemapWriter * @package Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator */ -class SitemapWriter extends XMLWriter { +class SitemapWriter extends \XMLWriter { + + const GENERATED_BY = 'Generated by the Simple XML Sitemap Drupal module: https://drupal.org/project/simple_sitemap.'; + const XML_VERSION = '1.0'; + const ENCODING = 'UTF-8'; + + /** + * Adds the XML stylesheet to the XML page. + */ + public function writeXsl() { + $xsl_url = Url::fromRoute('simple_sitemap.sitemap_xsl')->toString(); + $this->writePI('xml-stylesheet', 'type="text/xsl" href="' . $xsl_url . '"'); + } + + public function writeGeneratedBy() { + $this->writeComment(self::GENERATED_BY); + } + + public function startSitemapDocument() { + $this->startDocument(self::XML_VERSION, self::ENCODING); + } } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/ArbitraryUrlGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/ArbitraryUrlGenerator.php index 5e30fde3e2..ef20b73e67 100644 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/ArbitraryUrlGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/ArbitraryUrlGenerator.php @@ -53,6 +53,7 @@ public static function create( array $configuration, $plugin_id, $plugin_definition) { + return new static( $configuration, $plugin_id, @@ -70,6 +71,7 @@ public function getDataSets() { $arbitrary_links = []; $sitemap_variant = $this->sitemapVariant; $this->moduleHandler->alter('simple_sitemap_arbitrary_links', $arbitrary_links, $sitemap_variant); + return array_values($arbitrary_links); } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php old mode 100644 new mode 100755 index 98d90a737c..2bb641f80b --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php @@ -96,6 +96,7 @@ public static function create( */ public function getDataSets() { $this->includeImages = $this->generator->getSetting('custom_links_include_images', FALSE); + return array_values($this->generator->setVariants($this->sitemapVariant)->getCustomLinks()); } @@ -108,6 +109,7 @@ protected function processDataSet($data_set) { ['@path' => $data_set['path'], '@custom_paths_url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/custom']) ->display('warning', 'administer sitemap settings') ->log('warning'); + return FALSE; } @@ -119,11 +121,11 @@ protected function processDataSet($data_set) { $path_data = [ 'url' => $url_object, 'lastmod' => method_exists($entity, 'getChangedTime') - ? date_iso8601($entity->getChangedTime()) : NULL, + ? date('c', $entity->getChangedTime()) : NULL, 'priority' => isset($data_set['priority']) ? $data_set['priority'] : NULL, 'changefreq' => !empty($data_set['changefreq']) ? $data_set['changefreq'] : NULL, - 'images' => $this->includeImages && method_exists($entity, 'getEntityTypeId') - ? $this->getImages($entity->getEntityTypeId(), $entity->id()) + 'images' => $this->includeImages && !empty($entity) + ? $this->getEntityImageData($entity) : [], 'meta' => [ 'path' => $path, diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php old mode 100644 new mode 100755 index ab3ddd3d15..921bfba004 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php @@ -9,7 +9,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Menu\MenuTreeParameters; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Menu\MenuLinkTree; +use Drupal\Core\Menu\MenuLinkTreeInterface; use Drupal\Core\Menu\MenuLinkBase; /** @@ -44,7 +44,7 @@ class EntityMenuLinkContentUrlGenerator extends EntityUrlGeneratorBase { * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * @param \Drupal\simple_sitemap\EntityHelper $entityHelper - * @param \Drupal\Core\Menu\MenuLinkTree $menu_link_tree + * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree */ public function __construct( array $configuration, @@ -55,7 +55,7 @@ public function __construct( LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager, EntityHelper $entityHelper, - MenuLinkTree $menu_link_tree + MenuLinkTreeInterface $menu_link_tree ) { parent::__construct( $configuration, @@ -120,7 +120,7 @@ public function getDataSets() { /** * @inheritdoc * - * @todo Check if menu link has not been deleted after the queue has been built. + * @todo Find a way to be able to check if a menu link still exists. This is difficult as we don't operate on MenuLinkContent entities, but on Link entities directly (as some menu links are not MenuLinkContent entities). */ protected function processDataSet($data_set) { @@ -146,17 +146,25 @@ protected function processDataSet($data_set) { // If menu link is of entity type menu_link_content, take under account its entity override. else { - $entity_settings = $this->generator->getEntityInstanceSettings('menu_link_content', $meta_data['entity_id']); + $entity_settings = $this->generator + ->setVariants($this->sitemapVariant) + ->getEntityInstanceSettings('menu_link_content', $meta_data['entity_id']); if (empty($entity_settings['index'])) { return FALSE; } } - // There can be internal paths that are not rooted, like 'base:/path'. if ($url_object->isRouted()) { + + // Do not include paths that have no URL. + if (in_array($url_object->getRouteName(), ['<nolink>', '<none>'])) { + return FALSE; + } + $path = $url_object->getInternalPath(); } + // There can be internal paths that are not rooted, like 'base:/path'. else { // Handle base scheme. if (strpos($uri = $url_object->toUriString(), 'base:/') === 0 ) { $path = $uri[6] === '/' ? substr($uri, 7) : substr($uri, 6); @@ -173,12 +181,12 @@ protected function processDataSet($data_set) { $path_data = [ 'url' => $url_object, 'lastmod' => !empty($entity) && method_exists($entity, 'getChangedTime') - ? date_iso8601($entity->getChangedTime()) + ? date('c', $entity->getChangedTime()) : NULL, 'priority' => isset($entity_settings['priority']) ? $entity_settings['priority'] : NULL, 'changefreq' => !empty($entity_settings['changefreq']) ? $entity_settings['changefreq'] : NULL, 'images' => !empty($entity_settings['include_images']) && !empty($entity) - ? $this->getImages($entity->getEntityTypeId(), $entity->id()) + ? $this->getEntityImageData($entity) : [], // Additional info useful in hooks. diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php old mode 100644 new mode 100755 index 7f3d528384..efa2abfe6c --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php @@ -115,6 +115,11 @@ public function getDataSets() { $query->condition($keys['status'], 1); } + // Shift access check to EntityUrlGeneratorBase for language + // specific access. + // See https://www.drupal.org/project/simple_sitemap/issues/3102450. + $query->accessCheck(FALSE); + foreach ($query->execute() as $entity_id) { $data_sets[] = [ 'entity_type' => $entity_type_name, @@ -161,11 +166,11 @@ protected function processDataSet($data_set) { return [ 'url' => $url_object, - 'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL, + 'lastmod' => method_exists($entity, 'getChangedTime') ? date('c', $entity->getChangedTime()) : NULL, 'priority' => isset($entity_settings['priority']) ? $entity_settings['priority'] : NULL, 'changefreq' => !empty($entity_settings['changefreq']) ? $entity_settings['changefreq'] : NULL, 'images' => !empty($entity_settings['include_images']) - ? $this->getImages($entity_type_name, $entity_id) + ? $this->getEntityImageData($entity) : [], // Additional info useful in hooks. diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php old mode 100644 new mode 100755 index 693cbfbf66..7ef4e9e2e3 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php @@ -2,9 +2,11 @@ namespace Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator; +use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorBase; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Url; +use Drupal\file\Entity\File; use Drupal\simple_sitemap\EntityHelper; use Drupal\simple_sitemap\Logger; use Drupal\simple_sitemap\Simplesitemap; @@ -44,6 +46,11 @@ abstract class EntityUrlGeneratorBase extends UrlGeneratorBase { */ protected $entityHelper; + /** + * @var bool + */ + protected $isMultilingualSitemap; + /** * UrlGeneratorBase constructor. * @param array $configuration @@ -71,6 +78,7 @@ public function __construct( $this->entityTypeManager = $entity_type_manager; $this->anonUser = new AnonymousUserSession(); $this->entityHelper = $entityHelper; + $this->isMultilingualSitemap = SitemapGeneratorBase::isMultilingualSitemap(); } public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { @@ -96,8 +104,9 @@ public static function create(ContainerInterface $container, array $configuratio protected function getUrlVariants(array $path_data, Url $url_object) { $url_variants = []; - if (!$url_object->isRouted()) { - // Not a routed URL, including only default variant. + if (!$this->isMultilingualSitemap || !$url_object->isRouted()) { + + // Not a routed URL or URL language negotiation disabled: Including only default variant. $alternate_urls = $this->getAlternateUrlsForDefaultLanguage($url_object); } elseif ($this->settings['skip_untranslated'] @@ -108,7 +117,7 @@ protected function getUrlVariants(array $path_data, Url $url_object) { if (isset($translation_languages[Language::LANGCODE_NOT_SPECIFIED]) || isset($translation_languages[Language::LANGCODE_NOT_APPLICABLE])) { - // Content entity's language is unknown, including only default variant. + // Content entity's language is unknown: Including only default variant. $alternate_urls = $this->getAlternateUrlsForDefaultLanguage($url_object); } else { @@ -143,6 +152,7 @@ protected function getAlternateUrlsForDefaultLanguage(Url $url_object) { ->setOption('language', $this->languages[$this->defaultLanguageId])->toString() ); } + return $alternate_urls; } @@ -164,6 +174,7 @@ protected function getAlternateUrlsForTranslatedLanguages(ContentEntityBase $ent } } } + return $alternate_urls; } @@ -182,6 +193,7 @@ protected function getAlternateUrlsForAllLanguages(Url $url_object) { } } } + return $alternate_urls; } @@ -198,21 +210,34 @@ public function generate($data_set) { unset($path_data['url']); return $this->getUrlVariants($path_data, $url_object); } - else { - return FALSE !== $path_data ? [$path_data] : []; - } + + return FALSE !== $path_data ? [$path_data] : []; } /** - * @param string $entity_type_name - * @param string $entity_id + * @param \Drupal\Core\Entity\ContentEntityBase $entity + * * @return array */ - protected function getImages($entity_type_name, $entity_id) { - $images = []; - foreach ($this->entityHelper->getEntityImageUrls($entity_type_name, $entity_id) as $url) { - $images[]['path'] = $this->replaceBaseUrlWithCustom($url); + protected function getEntityImageData(ContentEntityBase $entity) { + $image_data = []; + foreach ($entity->getFieldDefinitions() as $field) { + if ($field->getType() === 'image') { + foreach ($entity->get($field->getName())->getValue() as $value) { + if (!empty($file = File::load($value['target_id']))) { + $image_data[] = [ + 'path' => $this->replaceBaseUrlWithCustom( + file_create_url($file->getFileUri()) + ), + 'alt' => $value['alt'], + 'title' => $value['title'], + ]; + } + } + } } - return $images; + + return $image_data; } + } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/UrlGeneratorBase.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/UrlGeneratorBase.php index 16255ecc85..297696e241 100644 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/UrlGeneratorBase.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/UrlGeneratorBase.php @@ -69,6 +69,7 @@ public static function create(ContainerInterface $container, array $configuratio */ public function setSettings(array $settings) { $this->settings = $settings; + return $this; } @@ -78,6 +79,7 @@ public function setSettings(array $settings) { */ public function setSitemapVariant($sitemap_variant) { $this->sitemapVariant = $sitemap_variant; + return $this; } @@ -108,6 +110,7 @@ abstract protected function processDataSet($data_set); */ public function generate($data_set) { $path_data = $this->processDataSet($data_set); + return FALSE !== $path_data ? [$path_data] : []; } } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/UrlGeneratorInterface.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/UrlGeneratorInterface.php index 97a6127199..dac87e08cd 100644 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/UrlGeneratorInterface.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/UrlGeneratorInterface.php @@ -8,11 +8,11 @@ */ interface UrlGeneratorInterface { - function setSettings(array $settings); + public function setSettings(array $settings); - function setSitemapVariant($sitemap_variant); + public function setSitemapVariant($sitemap_variant); - function getDataSets(); + public function getDataSets(); - function generate($data_set); + public function generate($data_set); } diff --git a/web/modules/simple_sitemap/src/Queue/BatchTrait.php b/web/modules/simple_sitemap/src/Queue/BatchTrait.php index 4bb5dad5f4..b104addfc3 100644 --- a/web/modules/simple_sitemap/src/Queue/BatchTrait.php +++ b/web/modules/simple_sitemap/src/Queue/BatchTrait.php @@ -20,24 +20,24 @@ trait BatchTrait { * @param array|null $variants * @return bool */ - public function batchGenerateSitemap($from = 'form', $variants = NULL) { + public function batchGenerateSitemap($from = self::GENERATE_TYPE_FORM, $variants = NULL) { $this->batch = [ 'title' => $this->t('Generating XML sitemaps'), 'init_message' => $this->t('Initializing...'), 'error_message' => $this->t(self::$batchErrorMessage), - 'progress_message' => $this->t('Processing items from queue. Each sitemap variant is published as soon as its items have been processed.'), + 'progress_message' => $this->t('Processing items from the queue.<br>Each sitemap variant is published after all of its items have been processed.'), 'operations' => [[ __CLASS__ . '::' . 'doBatchGenerateSitemap', []]], 'finished' => [__CLASS__, 'finishGeneration'], ]; switch ($from) { - case 'form': + case self::GENERATE_TYPE_FORM: // Start batch process. batch_set($this->batch); return TRUE; - case 'drush': + case self::GENERATE_TYPE_DRUSH: // Start drush batch process. batch_set($this->batch); @@ -55,7 +55,6 @@ public function batchGenerateSitemap($from = 'form', $variants = NULL) { * @param $context * @throws \Drupal\Component\Plugin\Exception\PluginException * - * @todo Make sure batch does not run at the same time as cron. * @todo Variants into generateSitemap(). */ public static function doBatchGenerateSitemap(&$context) { diff --git a/web/modules/simple_sitemap/src/Queue/QueueWorker.php b/web/modules/simple_sitemap/src/Queue/QueueWorker.php index ee7c59cf94..0f01394a80 100644 --- a/web/modules/simple_sitemap/src/Queue/QueueWorker.php +++ b/web/modules/simple_sitemap/src/Queue/QueueWorker.php @@ -16,6 +16,11 @@ class QueueWorker { const REBUILD_QUEUE_CHUNK_ITEM_SIZE = 5000; + const GENERATE_TYPE_FORM = 'form'; + const GENERATE_TYPE_DRUSH = 'drush'; + const GENERATE_TYPE_CRON = 'cron'; + const GENERATE_TYPE_BACKEND = 'backend'; + /** * @var \Drupal\simple_sitemap\SimplesitemapSettings */ @@ -120,24 +125,24 @@ public function deleteQueue() { } /** - * @param array|null $variants + * @param string[]|string|null $variants * @return $this * @throws \Drupal\Component\Plugin\Exception\PluginException */ - public function rebuildQueue($variants = NULL) { - $all_data_sets = []; - $sitemap_variants = $this->manager->getSitemapVariants(); - + public function queue($variants = NULL) { $type_definitions = $this->manager->getSitemapTypes(); - $this->deleteQueue(); - - foreach ($sitemap_variants as $variant_name => $variant_definition) { + $all_data_sets = []; - // Skipping unwanted sitemap variants. - if (NULL !== $variants && !in_array($variant_name, (array) $variants)) { - continue; - } + // Gather variant data of variants chosen for this rebuild. + $queue_variants = NULL === $variants + ? $this->manager->getSitemapVariants() + : array_filter( + $this->manager->getSitemapVariants(), + static function($name) use ($variants) { return in_array($name, (array) $variants); }, + ARRAY_FILTER_USE_KEY + ); + foreach ($queue_variants as $variant_name => $variant_definition) { $type = $variant_definition['type']; // Adding generate_sitemap operations for all data sets. @@ -148,7 +153,7 @@ public function rebuildQueue($variants = NULL) { ->getDataSets(); if (!empty($data_sets)) { - $sitemap_variants[$variant_name]['data'] = TRUE; + $queue_variants[$variant_name]['data'] = TRUE; foreach ($data_sets as $data_set) { $all_data_sets[] = [ 'data' => $data_set, @@ -171,10 +176,20 @@ public function rebuildQueue($variants = NULL) { } $this->getQueuedElementCount(TRUE); - // todo: May not be clean to remove sitemap variants data when queuing elements. - // todo: Add test. - // Remove all sitemap variant instances where no results have been queued. - $this->manager->removeSitemap(array_keys(array_filter($sitemap_variants, function($e) { return empty($e['data']); }))); + // Remove all sitemap instances of variants which did not yield any queue elements. + $this->manager->removeSitemap(array_keys(array_filter($queue_variants, static function($e) { return empty($e['data']); }))); + + return $this; + } + + /** + * @param string[]|string|null $variants + * @return $this + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function rebuildQueue($variants = NULL) { + $this->deleteQueue(); + $this->queue($variants); return $this; } @@ -189,10 +204,11 @@ protected function queueElements($elements) { * @return $this * @throws \Drupal\Component\Plugin\Exception\PluginException */ - public function generateSitemap($from = 'form') { + public function generateSitemap($from = self::GENERATE_TYPE_FORM) { $this->generatorSettings = [ 'base_url' => $this->settings->getSetting('base_url', ''), + 'xsl' => $this->settings->getSetting('xsl', TRUE), 'default_variant' => $this->settings->getSetting('default_variant', NULL), 'skip_untranslated' => $this->settings->getSetting('skip_untranslated', FALSE), 'remove_duplicates' => $this->settings->getSetting('remove_duplicates', TRUE), @@ -272,12 +288,12 @@ protected function generateResultsFromElement($element) { protected function removeDuplicates(&$results) { if ($this->generatorSettings['remove_duplicates'] && !empty($results)) { $result = $results[key($results)]; - if (!empty($result['meta']['path'])) { - if (in_array($result['meta']['path'], $this->processedPaths)) { + if (isset($result['meta']['path'])) { + if (isset($this->processedPaths[$result['meta']['path']])) { $results = []; } else { - $this->processedPaths[] = $result['meta']['path']; + $this->processedPaths[$result['meta']['path']] = TRUE; } } } @@ -305,7 +321,7 @@ protected function generateVariantChunksFromResults($complete = FALSE) { } } } - }; + } } protected function publishCurrentVariant() { diff --git a/web/modules/simple_sitemap/src/Simplesitemap.php b/web/modules/simple_sitemap/src/Simplesitemap.php index b96c0f0f9d..eed94bffd9 100644 --- a/web/modules/simple_sitemap/src/Simplesitemap.php +++ b/web/modules/simple_sitemap/src/Simplesitemap.php @@ -3,7 +3,6 @@ namespace Drupal\simple_sitemap; use Drupal\Core\Database\Connection; -use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\simple_sitemap\Queue\QueueWorker; use Drupal\Core\Path\PathValidator; @@ -17,6 +16,7 @@ * @package Drupal\simple_sitemap */ class Simplesitemap { + /** * @var \Drupal\simple_sitemap\EntityHelper */ @@ -47,11 +47,6 @@ class Simplesitemap { */ protected $entityTypeManager; - /** - * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface - */ - protected $entityTypeBundleInfo; - /** * @var \Drupal\Core\Path\PathValidator */ @@ -103,7 +98,6 @@ class Simplesitemap { * @param \Drupal\Core\Config\ConfigFactory $config_factory * @param \Drupal\Core\Database\Connection $database * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info * @param \Drupal\Core\Path\PathValidator $path_validator * @param \Drupal\Core\Datetime\DateFormatter $date_formatter * @param \Drupal\Component\Datetime\Time $time @@ -116,7 +110,6 @@ public function __construct( ConfigFactory $config_factory, Connection $database, EntityTypeManagerInterface $entity_type_manager, - EntityTypeBundleInfoInterface $entity_type_bundle_info, PathValidator $path_validator, DateFormatter $date_formatter, Time $time, @@ -128,7 +121,6 @@ public function __construct( $this->configFactory = $config_factory; $this->db = $database; $this->entityTypeManager = $entity_type_manager; - $this->entityTypeBundleInfo = $entity_type_bundle_info; $this->pathValidator = $path_validator; $this->dateFormatter = $date_formatter; $this->time = $time; @@ -165,6 +157,7 @@ public function getSetting($name, $default = FALSE) { */ public function saveSetting($name, $setting) { $this->settings->saveSetting($name, $setting); + return $this; } @@ -190,10 +183,14 @@ public function getSitemapManager() { * true: All existing variants will be set. * * @return $this + * + * @todo Check if variants exist and throw exception. */ public function setVariants($variants = NULL) { if (NULL === $variants) { - $this->variants = FALSE !== ($default_variant = $this->getSetting('default_variant')) ? [$default_variant] : []; + $this->variants = !empty($default_variant = $this->getSetting('default_variant', '')) + ? [$default_variant] + : []; } elseif ($variants === TRUE) { $this->variants = array_keys( @@ -224,16 +221,16 @@ protected function getVariants($default_get_all = TRUE) { } /** - * Returns the whole sitemap, a requested sitemap chunk, - * or the sitemap index file. + * Returns a sitemap variant, its index, or its requested chunk. * - * @param int $delta + * @param int|null $delta + * Optional delta of the chunk. * * @return string|false - * If no sitemap delta is provided, either a sitemap index is returned, or the - * whole sitemap variant, if the amount of links does not exceed the max - * links setting. If a sitemap delta is provided, a sitemap chunk is returned. - * Returns false if the sitemap is not retrievable from the database. + * If no chunk delta is provided, either the sitemap variant is returned, + * or its index in case of a chunked sitemap. + * If a chunk delta is provided, the relevant chunk is returned. + * Returns false if the sitemap variant is not retrievable from the database. */ public function getSitemap($delta = NULL) { $chunk_info = $this->fetchSitemapVariantInfo(); @@ -245,18 +242,16 @@ public function getSitemap($delta = NULL) { return $this->fetchSitemapChunk($chunk_info[SitemapGeneratorBase::INDEX_DELTA]->id) ->sitemap_string; } - else { - // Return sitemap chunk if there is only one chunk. - return isset($chunk_info[SitemapGeneratorBase::FIRST_CHUNK_DELTA]) - ? $this->fetchSitemapChunk($chunk_info[SitemapGeneratorBase::FIRST_CHUNK_DELTA]->id) - ->sitemap_string - : FALSE; - } - } - else { - // Return specific sitemap chunk. - return $this->fetchSitemapChunk($chunk_info[$delta]->id)->sitemap_string; + + // Return sitemap chunk if there is only one chunk. + return isset($chunk_info[SitemapGeneratorBase::FIRST_CHUNK_DELTA]) + ? $this->fetchSitemapChunk($chunk_info[SitemapGeneratorBase::FIRST_CHUNK_DELTA]->id) + ->sitemap_string + : FALSE; } + + // Return specific sitemap chunk. + return $this->fetchSitemapChunk($chunk_info[$delta]->id)->sitemap_string; } /** @@ -268,15 +263,19 @@ public function getSitemap($delta = NULL) { * variant set the above keyed by sitemap delta. */ protected function fetchSitemapVariantInfo() { - $result = $this->db->select('simple_sitemap', 's') - ->fields('s', ['id', 'delta', 'sitemap_created', 'type']) - ->condition('s.status', 1) - ->condition('s.type', $this->getVariants(), 'IN') - ->execute(); - - return count($this->getVariants()) > 1 - ? $result->fetchAllAssoc('type') - : $result->fetchAllAssoc('delta'); + if (!empty($this->getVariants())) { + $result = $this->db->select('simple_sitemap', 's') + ->fields('s', ['id', 'delta', 'sitemap_created', 'type']) + ->condition('s.status', 1) + ->condition('s.type', $this->getVariants(), 'IN') + ->execute(); + + return count($this->getVariants()) > 1 + ? $result->fetchAllAssoc('type') + : $result->fetchAllAssoc('delta'); + } + + return []; } /** @@ -315,18 +314,17 @@ public function removeSitemap() { * * @throws \Drupal\Component\Plugin\Exception\PluginException * - * @todo Respect $this->variants and generate for specific variants. * @todo Implement lock functionality. */ - public function generateSitemap($from = 'form') { + public function generateSitemap($from = QueueWorker::GENERATE_TYPE_FORM) { switch($from) { - case 'form': - case 'drush': + case QueueWorker::GENERATE_TYPE_FORM: + case QueueWorker::GENERATE_TYPE_DRUSH; $this->queueWorker->batchGenerateSitemap($from); break; - case 'cron': - case 'backend': + case QueueWorker::GENERATE_TYPE_CRON: + case QueueWorker::GENERATE_TYPE_BACKEND: $this->queueWorker->generateSitemap($from); break; } @@ -335,7 +333,19 @@ public function generateSitemap($from = 'form') { } /** - * Rebuilds the queue for the currently set variants. + * Queues links from currently set variants. + * + * @return $this + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function queue() { + $this->queueWorker->queue($this->getVariants()); + + return $this; + } + + /** + * Deletes the queue and queues links from currently set variants. * * @return $this * @throws \Drupal\Component\Plugin\Exception\PluginException @@ -380,6 +390,7 @@ public function enableEntityType($entity_type_id) { $enabled_entity_types[] = $entity_type_id; $this->saveSetting('enabled_entity_types', $enabled_entity_types); } + return $this; } @@ -419,6 +430,8 @@ public function disableEntityType($entity_type_id) { /** * Sets settings for bundle or non-bundle entity types. This is done for the * currently set variant. + * Please note, this method takes only the first set + * variant into account. See todo. * * @param $entity_type_id * @param null $bundle_name @@ -429,7 +442,6 @@ public function disableEntityType($entity_type_id) { * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException * - * @todo: enableEntityType automatically * @todo multiple variants */ public function setBundleSettings($entity_type_id, $bundle_name = NULL, $settings = ['index' => TRUE]) { @@ -499,7 +511,7 @@ public function setBundleSettings($entity_type_id, $bundle_name = NULL, $setting * Limit the result set to a specific bundle name. * * @param bool $supplement_defaults - * Supplements the result set with default custom link settings. + * Supplements the result set with default bundle settings. * * @param bool $multiple_variants * If true, returns an array of results keyed by variant name, otherwise it @@ -511,25 +523,17 @@ public function setBundleSettings($entity_type_id, $bundle_name = NULL, $setting */ public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL, $supplement_defaults = TRUE, $multiple_variants = FALSE) { + $bundle_name = NULL !== $bundle_name ? $bundle_name : $entity_type_id; $all_bundle_settings = []; foreach ($variants = $this->getVariants(FALSE) as $variant) { if (NULL !== $entity_type_id) { - $bundle_name = NULL !== $bundle_name ? $bundle_name : $entity_type_id; - $bundle_settings = $this->configFactory ->get("simple_sitemap.bundle_settings.$variant.$entity_type_id.$bundle_name") ->get(); - // If not found and entity type is enabled, return default bundle settings. if (empty($bundle_settings) && $supplement_defaults) { - if ($this->entityTypeIsEnabled($entity_type_id) - && isset($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)[$bundle_name])) { - self::supplementDefaultSettings('entity', $bundle_settings); - } - else { - $bundle_settings = NULL; - } + self::supplementDefaultSettings('entity', $bundle_settings); } } else { @@ -543,20 +547,17 @@ public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL, $ // Supplement default bundle settings for all bundles not found in simple_sitemap.bundle_settings.*.* configuration. if ($supplement_defaults) { foreach ($this->entityHelper->getSupportedEntityTypes() as $type_id => $type_definition) { - if ($this->entityTypeIsEnabled($type_id)) { - foreach($this->entityTypeBundleInfo->getBundleInfo($type_id) as $bundle => $bundle_definition) { - if (!isset($bundle_settings[$type_id][$bundle])) { - self::supplementDefaultSettings('entity', $bundle_settings[$type_id][$bundle]); - } + foreach($this->entityHelper->getBundleInfo($type_id) as $bundle => $bundle_definition) { + if (!isset($bundle_settings[$type_id][$bundle])) { + self::supplementDefaultSettings('entity', $bundle_settings[$type_id][$bundle]); } } } } } + if ($multiple_variants) { - if (!empty($bundle_settings)) { - $all_bundle_settings[$variant] = $bundle_settings; - } + $all_bundle_settings[$variant] = $bundle_settings; } else { return $bundle_settings; @@ -594,11 +595,8 @@ public function removeBundleSettings($entity_type_id = NULL, $bundle_name = NULL ->getEditable("simple_sitemap.bundle_settings.$variant.$entity_type_id.$bundle_name")->delete(); } - $this->removeEntityInstanceSettings($entity_type_id, ( - empty($ids) - ? NULL - : $this->entityHelper->getEntityInstanceIds($entity_type_id, $bundle_name) - )); + $entity_ids = $this->entityHelper->getEntityInstanceIds($entity_type_id, $bundle_name); + $this->removeEntityInstanceSettings($entity_type_id, (empty($entity_ids) ? NULL : $entity_ids)); } else { foreach ($variants as $variant) { @@ -606,8 +604,8 @@ public function removeBundleSettings($entity_type_id = NULL, $bundle_name = NULL foreach ($config_names as $config_name) { $this->configFactory->getEditable($config_name)->delete(); } - $this->removeEntityInstanceSettings(); } + $this->removeEntityInstanceSettings(); } return $this; @@ -651,7 +649,10 @@ public function setEntityInstanceSettings($entity_type_id, $id, $settings) { return $this; } - $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id); + if (empty($entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id))) { + // todo exception + return $this; + } $all_bundle_settings = $this->getBundleSettings( $entity_type_id, $this->entityHelper->getEntityInstanceBundleName($entity), TRUE, TRUE @@ -697,18 +698,21 @@ public function setEntityInstanceSettings($entity_type_id, $id, $settings) { * Gets sitemap settings for an entity instance which overrides bundle * settings, or gets bundle settings, if they are not overridden. This is * done for the currently set variant. + * Please note, this method takes only the first set + * variant into account. See todo. * * @param string $entity_type_id * @param string $id * * @return array|false * Array of entity instance settings or the settings of its bundle. False if - * entity type does not exist. + * entity type or variant does not exist. * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException * * @todo multiple variants + * @todo: May want to use Simplesitemap::supplementDefaultSettings('entity', $settings) inside here instead of calling it everywhere this method is called. */ public function getEntityInstanceSettings($entity_type_id, $id) { if (empty($variants = $this->getVariants(FALSE))) { @@ -726,14 +730,15 @@ public function getEntityInstanceSettings($entity_type_id, $id) { if (!empty($results)) { return unserialize($results); } - else { - $entity = $this->entityTypeManager->getStorage($entity_type_id) - ->load($id); - return $this->getBundleSettings( - $entity_type_id, - $this->entityHelper->getEntityInstanceBundleName($entity) - ); + + if (empty($entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id))) { + return FALSE; } + + return $this->getBundleSettings( + $entity_type_id, + $this->entityHelper->getEntityInstanceBundleName($entity) + ); } /** @@ -771,18 +776,21 @@ public function removeEntityInstanceSettings($entity_type_id = NULL, $entity_ids /** * Checks if an entity bundle (or a non-bundle entity type) is set to be - * indexed for the currently set variant. + * indexed for any of the currently set variants. * * @param string $entity_type_id * @param string|null $bundle_name * * @return bool - * - * @todo multiple variants? */ public function bundleIsIndexed($entity_type_id, $bundle_name = NULL) { - $settings = $this->getBundleSettings($entity_type_id, $bundle_name); - return !empty($settings['index']); + foreach ($this->getBundleSettings($entity_type_id, $bundle_name, FALSE, TRUE) as $settings) { + if (!empty($settings['index'])) { + return TRUE; + } + } + + return FALSE; } /** diff --git a/web/modules/simple_sitemap/src/SimplesitemapManager.php b/web/modules/simple_sitemap/src/SimplesitemapManager.php index 6acc482003..6a48d5cd71 100644 --- a/web/modules/simple_sitemap/src/SimplesitemapManager.php +++ b/web/modules/simple_sitemap/src/SimplesitemapManager.php @@ -18,7 +18,6 @@ class SimplesitemapManager { const DEFAULT_SITEMAP_TYPE = 'default_hreflang'; - const DEFAULT_SITEMAP_GENERATOR = 'default'; /** * @var \Drupal\Core\Config\ConfigFactory @@ -150,6 +149,7 @@ public function getSitemapVariants($sitemap_type = NULL, $attach_type_info = TRU $variants = $attach_type_info ? $this->attachSitemapTypeToVariants($variants, $sitemap_type) : $variants; } array_multisort(array_column($variants, "weight"), SORT_ASC, $variants); + return $variants; } @@ -159,15 +159,7 @@ public function getSitemapVariants($sitemap_type = NULL, $attach_type_info = TRU * @return array */ protected function attachSitemapTypeToVariants(array $variants, $type) { - return array_map(function($variant) use ($type) { return $variant + ['type' => $type]; }, $variants); - } - - /** - * @param array $variants - * @return array - */ - protected function detachSitemapTypeFromVariants(array $variants) { - return array_map(function($variant) { unset($variant['type']); return $variant; }, $variants); + return array_map(static function($variant) use ($type) { return $variant + ['type' => $type]; }, $variants); } /** @@ -205,7 +197,7 @@ public function addSitemapVariant($name, $definition = []) { } if (isset($old_variant)) { - $definition = $definition + $old_variant; + $definition += $old_variant; } $variants = array_merge($this->getSitemapVariants($definition['type'], FALSE), [$name => ['label' => $definition['label'], 'weight' => $definition['weight']]]); @@ -303,7 +295,6 @@ public function removeSitemapVariants($variant_names = NULL) { } $query->execute(); - // Remove default variant setting. if (NULL === $variant_names || in_array($this->settings->getSetting('default_variant', ''), (array) $variant_names)) { diff --git a/web/modules/simple_sitemap/src/SimplesitemapSettings.php b/web/modules/simple_sitemap/src/SimplesitemapSettings.php index 929f703502..1654604c4b 100644 --- a/web/modules/simple_sitemap/src/SimplesitemapSettings.php +++ b/web/modules/simple_sitemap/src/SimplesitemapSettings.php @@ -41,6 +41,7 @@ public function getSetting($name, $default = FALSE) { $setting = $this->configFactory ->get('simple_sitemap.settings') ->get($name); + return NULL !== $setting ? $setting : $default; } @@ -63,6 +64,7 @@ public function getSettings() { public function saveSetting($name, $setting) { $this->configFactory->getEditable('simple_sitemap.settings') ->set($name, $setting)->save(); + return $this; } } diff --git a/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTest.php b/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTest.php index 819c903f91..a29bca6743 100644 --- a/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTest.php +++ b/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTest.php @@ -2,10 +2,13 @@ namespace Drupal\Tests\simple_sitemap\Functional; +use Drupal\Core\Cache\Cache; use Drupal\Core\Url; +use Drupal\node\Entity\Node; +use Drupal\simple_sitemap\Queue\QueueWorker; /** - * Tests Simple XML sitemap functional integration. + * Tests Simple XML Sitemap functional integration. * * @group simple_sitemap */ @@ -18,7 +21,7 @@ class SimplesitemapTest extends SimplesitemapTestBase { * @throws \Behat\Mink\Exception\ExpectationException */ public function testInitialGeneration() { - $this->generator->generateSitemap('backend'); + $this->generator->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('urlset'); $this->assertSession()->responseContains( @@ -38,7 +41,7 @@ public function testAddCustomLink() { $this->generator->addCustomLink( '/node/' . $this->node->id(), ['priority' => 0.2, 'changefreq' => 'monthly'] - )->generateSitemap('backend'); + )->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); @@ -55,7 +58,7 @@ public function testAddCustomLink() { $this->generator->addCustomLink( '/node/' . $this->node->id(), ['changefreq' => 'yearly'] - )->generateSitemap('backend'); + )->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet('admin/config/search/simplesitemap/custom'); $this->assertSession()->pageTextContains( @@ -72,7 +75,7 @@ public function testAddCustomLink() { public function testAddCustomLinkDefaults() { $this->generator->removeCustomLinks() ->addCustomLink('/node/' . $this->node->id()) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); @@ -91,20 +94,19 @@ public function testRemoveCustomLinks() { // Test removing one custom path from the sitemap. $this->generator->addCustomLink('/node/' . $this->node->id()) ->removeCustomLinks('/node/' . $this->node->id()) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseNotContains('node/' . $this->node->id()); - //todo Not working -// // Test removing all custom paths from the sitemap. -// $this->generator->removeCustomLinks() -// ->generateSitemap('backend'); -// -// $this->drupalGet($this->defaultSitemapUrl); -// $this->assertSession()->responseNotContains( -// Url::fromRoute('<front>')->setAbsolute()->toString() -// ); + // Test removing all custom paths from the sitemap. + $this->generator->removeCustomLinks() + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); + + $this->drupalGet($this->defaultSitemapUrl); + $this->assertSession()->responseNotContains( + Url::fromRoute('<front>')->setAbsolute()->toString() + ); } /** @@ -125,7 +127,7 @@ public function testSetBundleSettings() { 'priority' => 0.5, 'changefreq' => 'hourly', ]) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); @@ -136,7 +138,7 @@ public function testSetBundleSettings() { // Only change bundle priority. $this->generator->setBundleSettings('node', 'page', ['priority' => 0.9]) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); @@ -148,7 +150,7 @@ public function testSetBundleSettings() { 'node', 'page', ['changefreq' => 'daily'] - )->generateSitemap('backend'); + )->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); @@ -157,7 +159,7 @@ public function testSetBundleSettings() { // Remove changefreq setting. $this->generator->setBundleSettings('node', 'page', ['changefreq' => '']) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); @@ -170,23 +172,22 @@ public function testSetBundleSettings() { $node3 = $this->createNode(['title' => 'Node3', 'type' => 'blog']); $this->generator->setBundleSettings('node', 'page', ['index' => TRUE]) ->setBundleSettings('node', 'blog', ['index' => TRUE]) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); $this->assertSession()->responseContains('node/' . $node3->id()); - // todo Now working -// // Set bundle 'index' setting to false. -// $this->generator -// ->setBundleSettings('node', 'page', ['index' => FALSE]) -// ->setBundleSettings('node', 'blog', ['index' => FALSE]) -// ->generateSitemap('backend'); -// -// $this->drupalGet($this->defaultSitemapUrl); -// -// $this->assertSession()->responseNotContains('node/' . $this->node->id()); -// $this->assertSession()->responseNotContains('node/' . $node3->id()); + // Set bundle 'index' setting to false. + $this->generator + ->setBundleSettings('node', 'page', ['index' => FALSE]) + ->setBundleSettings('node', 'blog', ['index' => FALSE]) + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); + + $this->drupalGet($this->defaultSitemapUrl); + + $this->assertSession()->responseNotContains('node/' . $this->node->id()); + $this->assertSession()->responseNotContains('node/' . $node3->id()); } /** @@ -198,7 +199,7 @@ public function testSetBundleSettings() { public function testSetBundleSettingsDefaults() { $this->generator->setBundleSettings('node', 'page') ->removeCustomLinks() - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); @@ -216,7 +217,7 @@ public function testLastmod() { // Entity links should have 'lastmod'. $this->generator->setBundleSettings('node', 'page') ->removeCustomLinks() - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('lastmod'); @@ -224,7 +225,7 @@ public function testLastmod() { // Entity custom links should have 'lastmod'. $this->generator->setBundleSettings('node', 'page', ['index' => FALSE]) ->addCustomLink('/node/' . $this->node->id()) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('lastmod'); @@ -232,7 +233,7 @@ public function testLastmod() { // Non-entity custom links should not have 'lastmod'. $this->generator->removeCustomLinks() ->addCustomLink('/') - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseNotContains('lastmod'); @@ -247,13 +248,13 @@ public function testRemoveDuplicatesSetting() { $this->generator->setBundleSettings('node', 'page') ->addCustomLink('/node/1') ->saveSetting('remove_duplicates', TRUE) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertUniqueTextWorkaround('node/' . $this->node->id()); $this->generator->saveSetting('remove_duplicates', FALSE) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertNoUniqueTextWorkaround('node/' . $this->node->id()); @@ -269,7 +270,7 @@ public function testMaxLinksSetting() { $this->generator->setBundleSettings('node', 'page') ->saveSetting('max_links', 1) ->removeCustomLinks() - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('sitemap.xml?page=1'); @@ -299,14 +300,14 @@ public function testMaxLinksSetting() { public function testBaseUrlSetting() { $this->generator->setBundleSettings('node', 'page') ->saveSetting('base_url', 'http://base_url_test') - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('http://base_url_test'); // Set base URL in the sitemap index. $this->generator->saveSetting('max_links', 1) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('http://base_url_test/sitemap.xml?page=1'); @@ -325,7 +326,7 @@ public function testSetEntityInstanceSettings() { ->removeCustomLinks() ->setEntityInstanceSettings('node', $this->node->id(), ['priority' => 0.1, 'changefreq' => 'never']) ->setEntityInstanceSettings('node', $this->node2->id(), ['index' => FALSE]) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); // Test sitemap result. $this->drupalGet($this->defaultSitemapUrl); @@ -352,7 +353,7 @@ public function testSetEntityInstanceSettings() { $this->assertFalse(empty($result)); $this->generator->setBundleSettings('node', 'page', ['priority' => 0.1, 'changefreq' => 'never']) - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); // Test sitemap result. $this->drupalGet($this->defaultSitemapUrl); @@ -378,13 +379,27 @@ public function testSetEntityInstanceSettings() { $this->assertTrue(empty($result)); } + /** + * Tests that a page does not break if an entity has its id set. + */ + public function testNewEntityWithIdSet() { + $new_node = Node::create([ + 'nid' => rand(5, 10), + 'type' => 'page', + ]); + // Assert that the form does not break if an entity has an id but is not + // saved. + // @see https://www.drupal.org/project/simple_sitemap/issues/3079897 + \Drupal::service('entity.form_builder')->getForm($new_node); + } + /** * Test indexing an atomic entity (here: a user) */ public function testAtomicEntityIndexation() { $user_id = $this->privilegedUser->id(); $this->generator->setBundleSettings('user') - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseNotContains('user/' . $user_id); @@ -392,7 +407,7 @@ public function testAtomicEntityIndexation() { user_role_grant_permissions('anonymous', ['access user profiles']); drupal_flush_all_caches(); //todo Not pretty. - $this->generator->generateSitemap('backend'); + $this->generator->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('user/' . $user_id); @@ -418,9 +433,9 @@ public function testDisableEntityType() { $this->drupalLogin($this->privilegedUser); $this->drupalGet('admin/structure/types/manage/page'); - $this->assertSession()->pageTextNotContains('Simple XML sitemap'); + $this->assertSession()->pageTextNotContains('Simple XML Sitemap'); - $this->generator->generateSitemap('backend'); + $this->generator->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseNotContains('node/' . $this->node->id()); @@ -443,9 +458,9 @@ public function testEnableEntityType() { $this->drupalLogin($this->privilegedUser); $this->drupalGet('admin/structure/types/manage/page'); - $this->assertSession()->pageTextContains('Simple XML sitemap'); + $this->assertSession()->pageTextContains('Simple XML Sitemap'); - $this->generator->generateSitemap('backend'); + $this->generator->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $this->drupalGet($this->defaultSitemapUrl); $this->assertSession()->responseContains('node/' . $this->node->id()); @@ -470,7 +485,7 @@ public function testSitemapVariants() { $this->generator ->setBundleSettings('node', 'page') - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); $variants = $this->generator->getSitemapManager()->getSitemapVariants(); $this->assertTrue(isset($variants['test'])); @@ -485,7 +500,7 @@ public function testSitemapVariants() { $this->generator ->setVariants('test') ->setBundleSettings('node', 'page') - ->generateSitemap('backend'); + ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); // Test if bundle settings have been set for correct variant. $this->drupalGet($this->defaultSitemapUrl); @@ -544,7 +559,7 @@ public function testGenerationResume($element_count, $generate_duration, $max_li $generate_count = 0; while ($queue->generationInProgress()) { $generate_count++; - $this->generator->generateSitemap('backend'); + $this->generator->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); } // Test if sitemap generation has been resumed when time limit is very low. @@ -562,5 +577,22 @@ public function testGenerationResume($element_count, $generate_duration, $max_li $this->assertTrue($chunk_count > 1 ? (FALSE !== $index) : !$index); } + /** + * Test the removal of hreflang tags in HTML. + */ + public function testHrefLangRemoval() { + // Test the nodes markup contains hreflang with default settings. + $this->generator->saveSetting('disable_language_hreflang', FALSE); + $this->drupalGet('node/' . $this->node->id()); + $this->assertNotEmpty($this->xpath("//link[@hreflang]")); + + Cache::invalidateTags($this->node->getCacheTags()); + + // Test the hreflang markup gets removed. + $this->generator->saveSetting('disable_language_hreflang', TRUE); + $this->drupalGet('node/' . $this->node->id()); + $this->assertEmpty($this->xpath("//link[@hreflang]")); + } + } diff --git a/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTestBase.php b/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTestBase.php index 451eda83df..a1def2e672 100644 --- a/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTestBase.php +++ b/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTestBase.php @@ -61,6 +61,20 @@ abstract class SimplesitemapTestBase extends BrowserTestBase { protected $defaultSitemapUrl = 'sitemap.xml'; + /** + * Use the testing profile. + * + * @var string + */ + protected $profile = 'testing'; + + /** + * Use the classy theme. + * + * @var string + */ + protected $defaultTheme = 'classy'; + /** * {@inheritdoc} */ diff --git a/web/modules/simple_sitemap/xsl/jquery.tablesorter.min.js b/web/modules/simple_sitemap/xsl/jquery.tablesorter.min.js new file mode 100644 index 0000000000..27a2b39788 --- /dev/null +++ b/web/modules/simple_sitemap/xsl/jquery.tablesorter.min.js @@ -0,0 +1 @@ +!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof module&&"object"==typeof module.exports?module.exports=e(require("jquery")):e(jQuery)}(function(e){return function(A){"use strict";var L=A.tablesorter={version:"2.31.1",parsers:[],widgets:[],defaults:{theme:"default",widthFixed:!1,showProcessing:!1,headerTemplate:"{content}",onRenderTemplate:null,onRenderHeader:null,cancelSelection:!0,tabIndex:!0,dateFormat:"mmddyyyy",sortMultiSortKey:"shiftKey",sortResetKey:"ctrlKey",usNumberFormat:!0,delayInit:!1,serverSideSorting:!1,resort:!0,headers:{},ignoreCase:!0,sortForce:null,sortList:[],sortAppend:null,sortStable:!1,sortInitialOrder:"asc",sortLocaleCompare:!1,sortReset:!1,sortRestart:!1,emptyTo:"bottom",stringTo:"max",duplicateSpan:!0,textExtraction:"basic",textAttribute:"data-text",textSorter:null,numberSorter:null,initWidgets:!0,widgetClass:"widget-{name}",widgets:[],widgetOptions:{zebra:["even","odd"]},initialized:null,tableClass:"",cssAsc:"",cssDesc:"",cssNone:"",cssHeader:"",cssHeaderRow:"",cssProcessing:"",cssChildRow:"tablesorter-childRow",cssInfoBlock:"tablesorter-infoOnly",cssNoSort:"tablesorter-noSort",cssIgnoreRow:"tablesorter-ignoreRow",cssIcon:"tablesorter-icon",cssIconNone:"",cssIconAsc:"",cssIconDesc:"",cssIconDisabled:"",pointerClick:"click",pointerDown:"mousedown",pointerUp:"mouseup",selectorHeaders:"> thead th, > thead td",selectorSort:"th, td",selectorRemove:".remove-me",debug:!1,headerList:[],empties:{},strings:{},parsers:[],globalize:0,imgAttr:0},css:{table:"tablesorter",cssHasChild:"tablesorter-hasChildRow",childRow:"tablesorter-childRow",colgroup:"tablesorter-colgroup",header:"tablesorter-header",headerRow:"tablesorter-headerRow",headerIn:"tablesorter-header-inner",icon:"tablesorter-icon",processing:"tablesorter-processing",sortAsc:"tablesorter-headerAsc",sortDesc:"tablesorter-headerDesc",sortNone:"tablesorter-headerUnSorted"},language:{sortAsc:"Ascending sort applied, ",sortDesc:"Descending sort applied, ",sortNone:"No sort applied, ",sortDisabled:"sorting is disabled",nextAsc:"activate to apply an ascending sort",nextDesc:"activate to apply a descending sort",nextNone:"activate to remove the sort"},regex:{templateContent:/\{content\}/g,templateIcon:/\{icon\}/g,templateName:/\{name\}/i,spaces:/\s+/g,nonWord:/\W/g,formElements:/(input|select|button|textarea)/i,chunk:/(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi,chunks:/(^\\0|\\0$)/,hex:/^0x[0-9a-f]+$/i,comma:/,/g,digitNonUS:/[\s|\.]/g,digitNegativeTest:/^\s*\([.\d]+\)/,digitNegativeReplace:/^\s*\(([.\d]+)\)/,digitTest:/^[\-+(]?\d+[)]?$/,digitReplace:/[,.'"\s]/g},string:{max:1,min:-1,emptymin:1,emptymax:-1,zero:0,none:0,"null":0,top:!0,bottom:!1},keyCodes:{enter:13},dates:{},instanceMethods:{},setup:function(t,r){if(t&&t.tHead&&0!==t.tBodies.length&&!0!==t.hasInitialized){var e,o="",s=A(t),a=A.metadata;t.hasInitialized=!1,t.isProcessing=!0,t.config=r,A.data(t,"tablesorter",r),L.debug(r,"core")&&(console[console.group?"group":"log"]("Initializing tablesorter v"+L.version),A.data(t,"startoveralltimer",new Date)),r.supportsDataObject=((e=A.fn.jquery.split("."))[0]=parseInt(e[0],10),1<e[0]||1===e[0]&&4<=parseInt(e[1],10)),r.emptyTo=r.emptyTo.toLowerCase(),r.stringTo=r.stringTo.toLowerCase(),r.last={sortList:[],clickedIndex:-1},/tablesorter\-/.test(s.attr("class"))||(o=""!==r.theme?" tablesorter-"+r.theme:""),r.namespace?r.namespace="."+r.namespace.replace(L.regex.nonWord,""):r.namespace=".tablesorter"+Math.random().toString(16).slice(2),r.table=t,r.$table=s.addClass(L.css.table+" "+r.tableClass+o+" "+r.namespace.slice(1)).attr("role","grid"),r.$headers=s.find(r.selectorHeaders),r.$table.children().children("tr").attr("role","row"),r.$tbodies=s.children("tbody:not(."+r.cssInfoBlock+")").attr({"aria-live":"polite","aria-relevant":"all"}),r.$table.children("caption").length&&((o=r.$table.children("caption")[0]).id||(o.id=r.namespace.slice(1)+"caption"),r.$table.attr("aria-labelledby",o.id)),r.widgetInit={},r.textExtraction=r.$table.attr("data-text-extraction")||r.textExtraction||"basic",L.buildHeaders(r),L.fixColumnWidth(t),L.addWidgetFromClass(t),L.applyWidgetOptions(t),L.setupParsers(r),r.totalRows=0,r.debug&&L.validateOptions(r),r.delayInit||L.buildCache(r),L.bindEvents(t,r.$headers,!0),L.bindMethods(r),r.supportsDataObject&&void 0!==s.data().sortlist?r.sortList=s.data().sortlist:a&&s.metadata()&&s.metadata().sortlist&&(r.sortList=s.metadata().sortlist),L.applyWidget(t,!0),0<r.sortList.length?(r.last.sortList=r.sortList,L.sortOn(r,r.sortList,{},!r.initWidgets)):(L.setHeadersCss(r),r.initWidgets&&L.applyWidget(t,!1)),r.showProcessing&&s.unbind("sortBegin"+r.namespace+" sortEnd"+r.namespace).bind("sortBegin"+r.namespace+" sortEnd"+r.namespace,function(e){clearTimeout(r.timerProcessing),L.isProcessing(t),"sortBegin"===e.type&&(r.timerProcessing=setTimeout(function(){L.isProcessing(t,!0)},500))}),t.hasInitialized=!0,t.isProcessing=!1,L.debug(r,"core")&&(console.log("Overall initialization time:"+L.benchmark(A.data(t,"startoveralltimer"))),L.debug(r,"core")&&console.groupEnd&&console.groupEnd()),s.triggerHandler("tablesorter-initialized",t),"function"==typeof r.initialized&&r.initialized(t)}else L.debug(r,"core")&&(t.hasInitialized?console.warn("Stopping initialization. Tablesorter has already been initialized"):console.error("Stopping initialization! No table, thead or tbody",t))},bindMethods:function(r){var e=r.$table,t=r.namespace,o="sortReset update updateRows updateAll updateHeaders addRows updateCell updateComplete sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave ".split(" ").join(t+" ");e.unbind(o.replace(L.regex.spaces," ")).bind("sortReset"+t,function(e,t){e.stopPropagation(),L.sortReset(this.config,function(e){e.isApplyingWidgets?setTimeout(function(){L.applyWidget(e,"",t)},100):L.applyWidget(e,"",t)})}).bind("updateAll"+t,function(e,t,r){e.stopPropagation(),L.updateAll(this.config,t,r)}).bind("update"+t+" updateRows"+t,function(e,t,r){e.stopPropagation(),L.update(this.config,t,r)}).bind("updateHeaders"+t,function(e,t){e.stopPropagation(),L.updateHeaders(this.config,t)}).bind("updateCell"+t,function(e,t,r,o){e.stopPropagation(),L.updateCell(this.config,t,r,o)}).bind("addRows"+t,function(e,t,r,o){e.stopPropagation(),L.addRows(this.config,t,r,o)}).bind("updateComplete"+t,function(){this.isUpdating=!1}).bind("sorton"+t,function(e,t,r,o){e.stopPropagation(),L.sortOn(this.config,t,r,o)}).bind("appendCache"+t,function(e,t,r){e.stopPropagation(),L.appendCache(this.config,r),A.isFunction(t)&&t(this)}).bind("updateCache"+t,function(e,t,r){e.stopPropagation(),L.updateCache(this.config,t,r)}).bind("applyWidgetId"+t,function(e,t){e.stopPropagation(),L.applyWidgetId(this,t)}).bind("applyWidgets"+t,function(e,t){e.stopPropagation(),L.applyWidget(this,!1,t)}).bind("refreshWidgets"+t,function(e,t,r){e.stopPropagation(),L.refreshWidgets(this,t,r)}).bind("removeWidget"+t,function(e,t,r){e.stopPropagation(),L.removeWidget(this,t,r)}).bind("destroy"+t,function(e,t,r){e.stopPropagation(),L.destroy(this,t,r)}).bind("resetToLoadState"+t,function(e){e.stopPropagation(),L.removeWidget(this,!0,!1);var t=A.extend(!0,{},r.originalSettings);(r=A.extend(!0,{},L.defaults,t)).originalSettings=t,this.hasInitialized=!1,L.setup(this,r)})},bindEvents:function(e,t,r){var o,i=(e=A(e)[0]).config,s=i.namespace,d=null;!0!==r&&(t.addClass(s.slice(1)+"_extra_headers"),(o=L.getClosest(t,"table")).length&&"TABLE"===o[0].nodeName&&o[0]!==e&&A(o[0]).addClass(s.slice(1)+"_extra_table")),o=(i.pointerDown+" "+i.pointerUp+" "+i.pointerClick+" sort keyup ").replace(L.regex.spaces," ").split(" ").join(s+" "),t.find(i.selectorSort).add(t.filter(i.selectorSort)).unbind(o).bind(o,function(e,t){var r,o,s,a=A(e.target),n=" "+e.type+" ";if(!(1!==(e.which||e.button)&&!n.match(" "+i.pointerClick+" | sort | keyup ")||" keyup "===n&&e.which!==L.keyCodes.enter||n.match(" "+i.pointerClick+" ")&&void 0!==e.which||n.match(" "+i.pointerUp+" ")&&d!==e.target&&!0!==t)){if(n.match(" "+i.pointerDown+" "))return d=e.target,void("1"===(s=a.jquery.split("."))[0]&&s[1]<4&&e.preventDefault());if(d=null,r=L.getClosest(A(this),"."+L.css.header),L.regex.formElements.test(e.target.nodeName)||a.hasClass(i.cssNoSort)||0<a.parents("."+i.cssNoSort).length||r.hasClass("sorter-false")||0<a.parents("button").length)return!i.cancelSelection;i.delayInit&&L.isEmptyObject(i.cache)&&L.buildCache(i),i.last.clickedIndex=r.attr("data-column")||r.index(),(o=i.$headerIndexed[i.last.clickedIndex][0])&&!o.sortDisabled&&L.initSort(i,o,e)}}),i.cancelSelection&&t.attr("unselectable","on").bind("selectstart",!1).css({"user-select":"none",MozUserSelect:"none"})},buildHeaders:function(d){var e,l,t,r;for(d.headerList=[],d.headerContent=[],d.sortVars=[],L.debug(d,"core")&&(t=new Date),d.columns=L.computeColumnIndex(d.$table.children("thead, tfoot").children("tr")),l=d.cssIcon?'<i class="'+(d.cssIcon===L.css.icon?L.css.icon:d.cssIcon+" "+L.css.icon)+'"></i>':"",d.$headers=A(A.map(d.$table.find(d.selectorHeaders),function(e,t){var r,o,s,a,n,i=A(e);if(!L.getClosest(i,"tr").hasClass(d.cssIgnoreRow))return/(th|td)/i.test(e.nodeName)||(n=L.getClosest(i,"th, td"),i.attr("data-column",n.attr("data-column"))),r=L.getColumnData(d.table,d.headers,t,!0),d.headerContent[t]=i.html(),""===d.headerTemplate||i.find("."+L.css.headerIn).length||(a=d.headerTemplate.replace(L.regex.templateContent,i.html()).replace(L.regex.templateIcon,i.find("."+L.css.icon).length?"":l),d.onRenderTemplate&&(o=d.onRenderTemplate.apply(i,[t,a]))&&"string"==typeof o&&(a=o),i.html('<div class="'+L.css.headerIn+'">'+a+"</div>")),d.onRenderHeader&&d.onRenderHeader.apply(i,[t,d,d.$table]),s=parseInt(i.attr("data-column"),10),e.column=s,n=L.getOrder(L.getData(i,r,"sortInitialOrder")||d.sortInitialOrder),d.sortVars[s]={count:-1,order:n?d.sortReset?[1,0,2]:[1,0]:d.sortReset?[0,1,2]:[0,1],lockedOrder:!1,sortedBy:""},void 0!==(n=L.getData(i,r,"lockedOrder")||!1)&&!1!==n&&(d.sortVars[s].lockedOrder=!0,d.sortVars[s].order=L.getOrder(n)?[1,1]:[0,0]),d.headerList[t]=e,i.addClass(L.css.header+" "+d.cssHeader),L.getClosest(i,"tr").addClass(L.css.headerRow+" "+d.cssHeaderRow).attr("role","row"),d.tabIndex&&i.attr("tabindex",0),e})),d.$headerIndexed=[],r=0;r<d.columns;r++)L.isEmptyObject(d.sortVars[r])&&(d.sortVars[r]={}),e=d.$headers.filter('[data-column="'+r+'"]'),d.$headerIndexed[r]=e.length?e.not(".sorter-false").length?e.not(".sorter-false").filter(":last"):e.filter(":last"):A();d.$table.find(d.selectorHeaders).attr({scope:"col",role:"columnheader"}),L.updateHeader(d),L.debug(d,"core")&&(console.log("Built headers:"+L.benchmark(t)),console.log(d.$headers))},addInstanceMethods:function(e){A.extend(L.instanceMethods,e)},setupParsers:function(e,t){var r,o,s,a,n,i,d,l,c,g,p,u,f,h,m=e.table,b=0,y=L.debug(e,"core"),w={};if(e.$tbodies=e.$table.children("tbody:not(."+e.cssInfoBlock+")"),0===(h=(f=void 0===t?e.$tbodies:t).length))return y?console.warn("Warning: *Empty table!* Not building a parser cache"):"";for(y&&(u=new Date,console[console.group?"group":"log"]("Detecting parsers for each column")),o={extractors:[],parsers:[]};b<h;){if((r=f[b].rows).length)for(n=0,a=e.columns,i=0;i<a;i++){if((d=e.$headerIndexed[n])&&d.length&&(l=L.getColumnData(m,e.headers,n),p=L.getParserById(L.getData(d,l,"extractor")),g=L.getParserById(L.getData(d,l,"sorter")),c="false"===L.getData(d,l,"parser"),e.empties[n]=(L.getData(d,l,"empty")||e.emptyTo||(e.emptyToBottom?"bottom":"top")).toLowerCase(),e.strings[n]=(L.getData(d,l,"string")||e.stringTo||"max").toLowerCase(),c&&(g=L.getParserById("no-parser")),p||(p=!1),g||(g=L.detectParserForColumn(e,r,-1,n)),y&&(w["("+n+") "+d.text()]={parser:g.id,extractor:p?p.id:"none",string:e.strings[n],empty:e.empties[n]}),o.parsers[n]=g,o.extractors[n]=p,0<(s=d[0].colSpan-1)))for(n+=s,a+=s;0<s+1;)o.parsers[n-s]=g,o.extractors[n-s]=p,s--;n++}b+=o.parsers.length?h:1}y&&(L.isEmptyObject(w)?console.warn(" No parsers detected!"):console[console.table?"table":"log"](w),console.log("Completed detecting parsers"+L.benchmark(u)),console.groupEnd&&console.groupEnd()),e.parsers=o.parsers,e.extractors=o.extractors},addParser:function(e){var t,r=L.parsers.length,o=!0;for(t=0;t<r;t++)L.parsers[t].id.toLowerCase()===e.id.toLowerCase()&&(o=!1);o&&(L.parsers[L.parsers.length]=e)},getParserById:function(e){if("false"==e)return!1;var t,r=L.parsers.length;for(t=0;t<r;t++)if(L.parsers[t].id.toLowerCase()===e.toString().toLowerCase())return L.parsers[t];return!1},detectParserForColumn:function(e,t,r,o){for(var s,a,n,i=L.parsers.length,d=!1,l="",c=L.debug(e,"core"),g=!0;""===l&&g;)(n=t[++r])&&r<50?n.className.indexOf(L.cssIgnoreRow)<0&&(d=t[r].cells[o],l=L.getElementText(e,d,o),a=A(d),c&&console.log("Checking if value was empty on row "+r+", column: "+o+': "'+l+'"')):g=!1;for(;0<=--i;)if((s=L.parsers[i])&&"text"!==s.id&&s.is&&s.is(l,e.table,d,a))return s;return L.getParserById("text")},getElementText:function(e,t,r){if(!t)return"";var o,s=e.textExtraction||"",a=t.jquery?t:A(t);return"string"==typeof s?"basic"===s&&void 0!==(o=a.attr(e.textAttribute))?A.trim(o):A.trim(t.textContent||a.text()):"function"==typeof s?A.trim(s(a[0],e.table,r)):"function"==typeof(o=L.getColumnData(e.table,s,r))?A.trim(o(a[0],e.table,r)):A.trim(a[0].textContent||a.text())},getParsedText:function(e,t,r,o){void 0===o&&(o=L.getElementText(e,t,r));var s=""+o,a=e.parsers[r],n=e.extractors[r];return a&&(n&&"function"==typeof n.format&&(o=n.format(o,e.table,t,r)),s="no-parser"===a.id?"":a.format(""+o,e.table,t,r),e.ignoreCase&&"string"==typeof s&&(s=s.toLowerCase())),s},buildCache:function(e,t,r){var o,s,a,n,i,d,l,c,g,p,u,f,h,m,b,y,w,x,v,C,$,I,D=e.table,R=e.parsers,T=L.debug(e,"core");if(e.$tbodies=e.$table.children("tbody:not(."+e.cssInfoBlock+")"),l=void 0===r?e.$tbodies:r,e.cache={},e.totalRows=0,!R)return T?console.warn("Warning: *Empty table!* Not building a cache"):"";for(T&&(f=new Date),e.showProcessing&&L.isProcessing(D,!0),d=0;d<l.length;d++){for(y=[],o=e.cache[d]={normalized:[]},h=l[d]&&l[d].rows.length||0,n=0;n<h;++n)if(m={child:[],raw:[]},g=[],!(c=A(l[d].rows[n])).hasClass(e.selectorRemove.slice(1)))if(c.hasClass(e.cssChildRow)&&0!==n)for($=o.normalized.length-1,(b=o.normalized[$][e.columns]).$row=b.$row.add(c),c.prev().hasClass(e.cssChildRow)||c.prev().addClass(L.css.cssHasChild),p=c.children("th, td"),$=b.child.length,b.child[$]=[],x=0,C=e.columns,i=0;i<C;i++)(u=p[i])&&(b.child[$][i]=L.getParsedText(e,u,i),0<(w=p[i].colSpan-1)&&(x+=w,C+=w)),x++;else{for(m.$row=c,m.order=n,x=0,C=e.columns,i=0;i<C;++i){if((u=c[0].cells[i])&&x<e.columns&&(!(v=void 0!==R[x])&&T&&console.warn("No parser found for row: "+n+", column: "+i+'; cell containing: "'+A(u).text()+'"; does it have a header?'),s=L.getElementText(e,u,x),m.raw[x]=s,a=L.getParsedText(e,u,x,s),g[x]=a,v&&"numeric"===(R[x].type||"").toLowerCase()&&(y[x]=Math.max(Math.abs(a)||0,y[x]||0)),0<(w=u.colSpan-1))){for(I=0;I<=w;)a=e.duplicateSpan||0===I?s:"string"!=typeof e.textExtraction&&L.getElementText(e,u,x+I)||"",m.raw[x+I]=a,g[x+I]=a,I++;x+=w,C+=w}x++}g[e.columns]=m,o.normalized[o.normalized.length]=g}o.colMax=y,e.totalRows+=o.normalized.length}if(e.showProcessing&&L.isProcessing(D),T){for($=Math.min(5,e.cache[0].normalized.length),console[console.group?"group":"log"]("Building cache for "+e.totalRows+" rows (showing "+$+" rows in log) and "+e.columns+" columns"+L.benchmark(f)),s={},i=0;i<e.columns;i++)for(x=0;x<$;x++)s["row: "+x]||(s["row: "+x]={}),s["row: "+x][e.$headerIndexed[i].text()]=e.cache[0].normalized[x][i];console[console.table?"table":"log"](s),console.groupEnd&&console.groupEnd()}A.isFunction(t)&&t(D)},getColumnText:function(e,t,r,o){var s,a,n,i,d,l,c,g,p,u,f="function"==typeof r,h="all"===t,m={raw:[],parsed:[],$cell:[]},b=(e=A(e)[0]).config;if(!L.isEmptyObject(b)){for(d=b.$tbodies.length,s=0;s<d;s++)for(l=(n=b.cache[s].normalized).length,a=0;a<l;a++)i=n[a],o&&!i[b.columns].$row.is(o)||(u=!0,g=h?i.slice(0,b.columns):i[t],i=i[b.columns],c=h?i.raw:i.raw[t],p=h?i.$row.children():i.$row.children().eq(t),f&&(u=r({tbodyIndex:s,rowIndex:a,parsed:g,raw:c,$row:i.$row,$cell:p})),!1!==u&&(m.parsed[m.parsed.length]=g,m.raw[m.raw.length]=c,m.$cell[m.$cell.length]=p));return m}L.debug(b,"core")&&console.warn("No cache found - aborting getColumnText function!")},setHeadersCss:function(a){var e,t,r=a.sortList,o=r.length,s=L.css.sortNone+" "+a.cssNone,n=[L.css.sortAsc+" "+a.cssAsc,L.css.sortDesc+" "+a.cssDesc],i=[a.cssIconAsc,a.cssIconDesc,a.cssIconNone],d=["ascending","descending"],l=function(e,t){e.removeClass(s).addClass(n[t]).attr("aria-sort",d[t]).find("."+L.css.icon).removeClass(i[2]).addClass(i[t])},c=a.$table.find("tfoot tr").children("td, th").add(A(a.namespace+"_extra_headers")).removeClass(n.join(" ")),g=a.$headers.add(A("thead "+a.namespace+"_extra_headers")).removeClass(n.join(" ")).addClass(s).attr("aria-sort","none").find("."+L.css.icon).removeClass(i.join(" ")).end();for(g.not(".sorter-false").find("."+L.css.icon).addClass(i[2]),a.cssIconDisabled&&g.filter(".sorter-false").find("."+L.css.icon).addClass(a.cssIconDisabled),e=0;e<o;e++)if(2!==r[e][1]){if((g=(g=a.$headers.filter(function(e){for(var t=!0,r=a.$headers.eq(e),o=parseInt(r.attr("data-column"),10),s=o+L.getClosest(r,"th, td")[0].colSpan;o<s;o++)t=!!t&&(t||-1<L.isValueInArray(o,a.sortList));return t})).not(".sorter-false").filter('[data-column="'+r[e][0]+'"]'+(1===o?":last":""))).length)for(t=0;t<g.length;t++)g[t].sortDisabled||l(g.eq(t),r[e][1]);c.length&&l(c.filter('[data-column="'+r[e][0]+'"]'),r[e][1])}for(o=a.$headers.length,e=0;e<o;e++)L.setColumnAriaLabel(a,a.$headers.eq(e))},getClosest:function(e,t){return A.fn.closest?e.closest(t):e.is(t)?e:e.parents(t).filter(":first")},setColumnAriaLabel:function(e,t,r){if(t.length){var o=parseInt(t.attr("data-column"),10),s=e.sortVars[o],a=t.hasClass(L.css.sortAsc)?"sortAsc":t.hasClass(L.css.sortDesc)?"sortDesc":"sortNone",n=A.trim(t.text())+": "+L.language[a];t.hasClass("sorter-false")||!1===r?n+=L.language.sortDisabled:(a=(s.count+1)%s.order.length,r=s.order[a],n+=L.language[0===r?"nextAsc":1===r?"nextDesc":"nextNone"]),t.attr("aria-label",n),s.sortedBy?t.attr("data-sortedBy",s.sortedBy):t.removeAttr("data-sortedBy")}},updateHeader:function(e){var t,r,o,s,a=e.table,n=e.$headers.length;for(t=0;t<n;t++)o=e.$headers.eq(t),s=L.getColumnData(a,e.headers,t,!0),r="false"===L.getData(o,s,"sorter")||"false"===L.getData(o,s,"parser"),L.setColumnSort(e,o,r)},setColumnSort:function(e,t,r){var o=e.table.id;t[0].sortDisabled=r,t[r?"addClass":"removeClass"]("sorter-false").attr("aria-disabled",""+r),e.tabIndex&&(r?t.removeAttr("tabindex"):t.attr("tabindex","0")),o&&(r?t.removeAttr("aria-controls"):t.attr("aria-controls",o))},updateHeaderSortCount:function(e,t){var r,o,s,a,n,i,d,l,c=t||e.sortList,g=c.length;for(e.sortList=[],a=0;a<g;a++)if(d=c[a],(r=parseInt(d[0],10))<e.columns){switch(e.sortVars[r].order||(l=L.getOrder(e.sortInitialOrder)?e.sortReset?[1,0,2]:[1,0]:e.sortReset?[0,1,2]:[0,1],e.sortVars[r].order=l,e.sortVars[r].count=0),l=e.sortVars[r].order,o=(o=(""+d[1]).match(/^(1|d|s|o|n)/))?o[0]:""){case"1":case"d":o=1;break;case"s":o=n||0;break;case"o":o=0===(i=l[(n||0)%l.length])?1:1===i?0:2;break;case"n":o=l[++e.sortVars[r].count%l.length];break;default:o=0}n=0===a?o:n,s=[r,parseInt(o,10)||0],e.sortList[e.sortList.length]=s,o=A.inArray(s[1],l),e.sortVars[r].count=0<=o?o:s[1]%l.length}},updateAll:function(e,t,r){var o=e.table;o.isUpdating=!0,L.refreshWidgets(o,!0,!0),L.buildHeaders(e),L.bindEvents(o,e.$headers,!0),L.bindMethods(e),L.commonUpdate(e,t,r)},update:function(e,t,r){e.table.isUpdating=!0,L.updateHeader(e),L.commonUpdate(e,t,r)},updateHeaders:function(e,t){e.table.isUpdating=!0,L.buildHeaders(e),L.bindEvents(e.table,e.$headers,!0),L.resortComplete(e,t)},updateCell:function(e,t,r,o){if(A(t).closest("tr").hasClass(e.cssChildRow))console.warn('Tablesorter Warning! "updateCell" for child row content has been disabled, use "update" instead');else{if(L.isEmptyObject(e.cache))return L.updateHeader(e),void L.commonUpdate(e,r,o);e.table.isUpdating=!0,e.$table.find(e.selectorRemove).remove();var s,a,n,i,d,l,c=e.$tbodies,g=A(t),p=c.index(L.getClosest(g,"tbody")),u=e.cache[p],f=L.getClosest(g,"tr");if(t=g[0],c.length&&0<=p){if(n=c.eq(p).find("tr").not("."+e.cssChildRow).index(f),d=u.normalized[n],(l=f[0].cells.length)!==e.columns)for(s=!1,a=i=0;a<l;a++)s||f[0].cells[a]===t?s=!0:i+=f[0].cells[a].colSpan;else i=g.index();s=L.getElementText(e,t,i),d[e.columns].raw[i]=s,s=L.getParsedText(e,t,i,s),d[i]=s,"numeric"===(e.parsers[i].type||"").toLowerCase()&&(u.colMax[i]=Math.max(Math.abs(s)||0,u.colMax[i]||0)),!1!==(s="undefined"!==r?r:e.resort)?L.checkResort(e,s,o):L.resortComplete(e,o)}else L.debug(e,"core")&&console.error("updateCell aborted, tbody missing or not within the indicated table"),e.table.isUpdating=!1}},addRows:function(e,t,r,o){var s,a,n,i,d,l,c,g,p,u,f,h,m,b="string"==typeof t&&1===e.$tbodies.length&&/<tr/.test(t||""),y=e.table;if(b)t=A(t),e.$tbodies.append(t);else if(!(t&&t instanceof A&&L.getClosest(t,"table")[0]===e.table))return L.debug(e,"core")&&console.error("addRows method requires (1) a jQuery selector reference to rows that have already been added to the table, or (2) row HTML string to be added to a table with only one tbody"),!1;if(y.isUpdating=!0,L.isEmptyObject(e.cache))L.updateHeader(e),L.commonUpdate(e,r,o);else{for(d=t.filter("tr").attr("role","row").length,n=e.$tbodies.index(t.parents("tbody").filter(":first")),e.parsers&&e.parsers.length||L.setupParsers(e),i=0;i<d;i++){for(p=0,c=t[i].cells.length,g=e.cache[n].normalized.length,f=[],u={child:[],raw:[],$row:t.eq(i),order:g},l=0;l<c;l++)h=t[i].cells[l],s=L.getElementText(e,h,p),u.raw[p]=s,a=L.getParsedText(e,h,p,s),f[p]=a,"numeric"===(e.parsers[p].type||"").toLowerCase()&&(e.cache[n].colMax[p]=Math.max(Math.abs(a)||0,e.cache[n].colMax[p]||0)),0<(m=h.colSpan-1)&&(p+=m),p++;f[e.columns]=u,e.cache[n].normalized[g]=f}L.checkResort(e,r,o)}},updateCache:function(e,t,r){e.parsers&&e.parsers.length||L.setupParsers(e,r),L.buildCache(e,t,r)},appendCache:function(e,t){var r,o,s,a,n,i,d,l=e.table,c=e.$tbodies,g=[],p=e.cache;if(L.isEmptyObject(p))return e.appender?e.appender(l,g):l.isUpdating?e.$table.triggerHandler("updateComplete",l):"";for(L.debug(e,"core")&&(d=new Date),i=0;i<c.length;i++)if((s=c.eq(i)).length){for(a=L.processTbody(l,s,!0),o=(r=p[i].normalized).length,n=0;n<o;n++)g[g.length]=r[n][e.columns].$row,e.appender&&(!e.pager||e.pager.removeRows||e.pager.ajax)||a.append(r[n][e.columns].$row);L.processTbody(l,a,!1)}e.appender&&e.appender(l,g),L.debug(e,"core")&&console.log("Rebuilt table"+L.benchmark(d)),t||e.appender||L.applyWidget(l),l.isUpdating&&e.$table.triggerHandler("updateComplete",l)},commonUpdate:function(e,t,r){e.$table.find(e.selectorRemove).remove(),L.setupParsers(e),L.buildCache(e),L.checkResort(e,t,r)},initSort:function(t,e,r){if(t.table.isUpdating)return setTimeout(function(){L.initSort(t,e,r)},50);var o,s,a,n,i,d,l,c=!r[t.sortMultiSortKey],g=t.table,p=t.$headers.length,u=L.getClosest(A(e),"th, td"),f=parseInt(u.attr("data-column"),10),h="mouseup"===r.type?"user":r.type,m=t.sortVars[f].order;if(u=u[0],t.$table.triggerHandler("sortStart",g),d=(t.sortVars[f].count+1)%m.length,t.sortVars[f].count=r[t.sortResetKey]?2:d,t.sortRestart)for(a=0;a<p;a++)l=t.$headers.eq(a),f!==(d=parseInt(l.attr("data-column"),10))&&(c||l.hasClass(L.css.sortNone))&&(t.sortVars[d].count=-1);if(c){if(A.each(t.sortVars,function(e){t.sortVars[e].sortedBy=""}),t.sortList=[],t.last.sortList=[],null!==t.sortForce)for(o=t.sortForce,s=0;s<o.length;s++)o[s][0]!==f&&(t.sortList[t.sortList.length]=o[s],t.sortVars[o[s][0]].sortedBy="sortForce");if((n=m[t.sortVars[f].count])<2&&(t.sortList[t.sortList.length]=[f,n],t.sortVars[f].sortedBy=h,1<u.colSpan))for(s=1;s<u.colSpan;s++)t.sortList[t.sortList.length]=[f+s,n],t.sortVars[f+s].count=A.inArray(n,m),t.sortVars[f+s].sortedBy=h}else if(t.sortList=A.extend([],t.last.sortList),0<=L.isValueInArray(f,t.sortList))for(t.sortVars[f].sortedBy=h,s=0;s<t.sortList.length;s++)(d=t.sortList[s])[0]===f&&(d[1]=m[t.sortVars[f].count],2===d[1]&&(t.sortList.splice(s,1),t.sortVars[f].count=-1));else if(n=m[t.sortVars[f].count],t.sortVars[f].sortedBy=h,n<2&&(t.sortList[t.sortList.length]=[f,n],1<u.colSpan))for(s=1;s<u.colSpan;s++)t.sortList[t.sortList.length]=[f+s,n],t.sortVars[f+s].count=A.inArray(n,m),t.sortVars[f+s].sortedBy=h;if(t.last.sortList=A.extend([],t.sortList),t.sortList.length&&t.sortAppend&&(o=A.isArray(t.sortAppend)?t.sortAppend:t.sortAppend[t.sortList[0][0]],!L.isEmptyObject(o)))for(s=0;s<o.length;s++)if(o[s][0]!==f&&L.isValueInArray(o[s][0],t.sortList)<0){if(i=(""+(n=o[s][1])).match(/^(a|d|s|o|n)/))switch(d=t.sortList[0][1],i[0]){case"d":n=1;break;case"s":n=d;break;case"o":n=0===d?1:0;break;case"n":n=(d+1)%m.length;break;default:n=0}t.sortList[t.sortList.length]=[o[s][0],n],t.sortVars[o[s][0]].sortedBy="sortAppend"}t.$table.triggerHandler("sortBegin",g),setTimeout(function(){L.setHeadersCss(t),L.multisort(t),L.appendCache(t),t.$table.triggerHandler("sortBeforeEnd",g),t.$table.triggerHandler("sortEnd",g)},1)},multisort:function(l){var e,t,c,r,g=l.table,p=[],u=0,f=l.textSorter||"",h=l.sortList,m=h.length,o=l.$tbodies.length;if(!l.serverSideSorting&&!L.isEmptyObject(l.cache)){if(L.debug(l,"core")&&(t=new Date),"object"==typeof f)for(c=l.columns;c--;)"function"==typeof(r=L.getColumnData(g,f,c))&&(p[c]=r);for(e=0;e<o;e++)c=l.cache[e].colMax,l.cache[e].normalized.sort(function(e,t){var r,o,s,a,n,i,d;for(r=0;r<m;r++){if(s=h[r][0],a=h[r][1],u=0===a,l.sortStable&&e[s]===t[s]&&1===m)return e[l.columns].order-t[l.columns].order;if(n=(o=/n/i.test(L.getSortType(l.parsers,s)))&&l.strings[s]?(o="boolean"==typeof L.string[l.strings[s]]?(u?1:-1)*(L.string[l.strings[s]]?-1:1):l.strings[s]&&L.string[l.strings[s]]||0,l.numberSorter?l.numberSorter(e[s],t[s],u,c[s],g):L["sortNumeric"+(u?"Asc":"Desc")](e[s],t[s],o,c[s],s,l)):(i=u?e:t,d=u?t:e,"function"==typeof f?f(i[s],d[s],u,s,g):"function"==typeof p[s]?p[s](i[s],d[s],u,s,g):L["sortNatural"+(u?"Asc":"Desc")](e[s]||"",t[s]||"",s,l)))return n}return e[l.columns].order-t[l.columns].order});L.debug(l,"core")&&console.log("Applying sort "+h.toString()+L.benchmark(t))}},resortComplete:function(e,t){e.table.isUpdating&&e.$table.triggerHandler("updateComplete",e.table),A.isFunction(t)&&t(e.table)},checkResort:function(e,t,r){var o=A.isArray(t)?t:e.sortList;!1===(void 0===t?e.resort:t)||e.serverSideSorting||e.table.isProcessing?(L.resortComplete(e,r),L.applyWidget(e.table,!1)):o.length?L.sortOn(e,o,function(){L.resortComplete(e,r)},!0):L.sortReset(e,function(){L.resortComplete(e,r),L.applyWidget(e.table,!1)})},sortOn:function(e,t,r,o){var s,a=e.table;for(e.$table.triggerHandler("sortStart",a),s=0;s<e.columns;s++)e.sortVars[s].sortedBy=-1<L.isValueInArray(s,t)?"sorton":"";L.updateHeaderSortCount(e,t),L.setHeadersCss(e),e.delayInit&&L.isEmptyObject(e.cache)&&L.buildCache(e),e.$table.triggerHandler("sortBegin",a),L.multisort(e),L.appendCache(e,o),e.$table.triggerHandler("sortBeforeEnd",a),e.$table.triggerHandler("sortEnd",a),L.applyWidget(a),A.isFunction(r)&&r(a)},sortReset:function(e,t){var r;for(e.sortList=[],r=0;r<e.columns;r++)e.sortVars[r].count=-1,e.sortVars[r].sortedBy="";L.setHeadersCss(e),L.multisort(e),L.appendCache(e),A.isFunction(t)&&t(e.table)},getSortType:function(e,t){return e&&e[t]&&e[t].type||""},getOrder:function(e){return/^d/i.test(e)||1===e},sortNatural:function(e,t){if(e===t)return 0;e=(e||"").toString(),t=(t||"").toString();var r,o,s,a,n,i,d=L.regex;if(d.hex.test(t)){if((r=parseInt(e.match(d.hex),16))<(o=parseInt(t.match(d.hex),16)))return-1;if(o<r)return 1}for(r=e.replace(d.chunk,"\\0$1\\0").replace(d.chunks,"").split("\\0"),o=t.replace(d.chunk,"\\0$1\\0").replace(d.chunks,"").split("\\0"),i=Math.max(r.length,o.length),n=0;n<i;n++){if(s=isNaN(r[n])?r[n]||0:parseFloat(r[n])||0,a=isNaN(o[n])?o[n]||0:parseFloat(o[n])||0,isNaN(s)!==isNaN(a))return isNaN(s)?1:-1;if(typeof s!=typeof a&&(s+="",a+=""),s<a)return-1;if(a<s)return 1}return 0},sortNaturalAsc:function(e,t,r,o){if(e===t)return 0;var s=L.string[o.empties[r]||o.emptyTo];return""===e&&0!==s?"boolean"==typeof s?s?-1:1:-s||-1:""===t&&0!==s?"boolean"==typeof s?s?1:-1:s||1:L.sortNatural(e,t)},sortNaturalDesc:function(e,t,r,o){if(e===t)return 0;var s=L.string[o.empties[r]||o.emptyTo];return""===e&&0!==s?"boolean"==typeof s?s?-1:1:s||1:""===t&&0!==s?"boolean"==typeof s?s?1:-1:-s||-1:L.sortNatural(t,e)},sortText:function(e,t){return t<e?1:e<t?-1:0},getTextValue:function(e,t,r){if(r){var o,s=e?e.length:0,a=r+t;for(o=0;o<s;o++)a+=e.charCodeAt(o);return t*a}return 0},sortNumericAsc:function(e,t,r,o,s,a){if(e===t)return 0;var n=L.string[a.empties[s]||a.emptyTo];return""===e&&0!==n?"boolean"==typeof n?n?-1:1:-n||-1:""===t&&0!==n?"boolean"==typeof n?n?1:-1:n||1:(isNaN(e)&&(e=L.getTextValue(e,r,o)),isNaN(t)&&(t=L.getTextValue(t,r,o)),e-t)},sortNumericDesc:function(e,t,r,o,s,a){if(e===t)return 0;var n=L.string[a.empties[s]||a.emptyTo];return""===e&&0!==n?"boolean"==typeof n?n?-1:1:n||1:""===t&&0!==n?"boolean"==typeof n?n?1:-1:-n||-1:(isNaN(e)&&(e=L.getTextValue(e,r,o)),isNaN(t)&&(t=L.getTextValue(t,r,o)),t-e)},sortNumeric:function(e,t){return e-t},addWidget:function(e){e.id&&!L.isEmptyObject(L.getWidgetById(e.id))&&console.warn('"'+e.id+'" widget was loaded more than once!'),L.widgets[L.widgets.length]=e},hasWidget:function(e,t){return(e=A(e)).length&&e[0].config&&e[0].config.widgetInit[t]||!1},getWidgetById:function(e){var t,r,o=L.widgets.length;for(t=0;t<o;t++)if((r=L.widgets[t])&&r.id&&r.id.toLowerCase()===e.toLowerCase())return r},applyWidgetOptions:function(e){var t,r,o,s=e.config,a=s.widgets.length;if(a)for(t=0;t<a;t++)(r=L.getWidgetById(s.widgets[t]))&&r.options&&(o=A.extend(!0,{},r.options),s.widgetOptions=A.extend(!0,o,s.widgetOptions),A.extend(!0,L.defaults.widgetOptions,r.options))},addWidgetFromClass:function(e){var t,r,o=e.config,s="^"+o.widgetClass.replace(L.regex.templateName,"(\\S+)+")+"$",a=new RegExp(s,"g"),n=(e.className||"").split(L.regex.spaces);if(n.length)for(t=n.length,r=0;r<t;r++)n[r].match(a)&&(o.widgets[o.widgets.length]=n[r].replace(a,"$1"))},applyWidgetId:function(e,t,r){var o,s,a,n=(e=A(e)[0]).config,i=n.widgetOptions,d=L.debug(n,"core"),l=L.getWidgetById(t);l&&(a=l.id,o=!1,A.inArray(a,n.widgets)<0&&(n.widgets[n.widgets.length]=a),d&&(s=new Date),!r&&n.widgetInit[a]||(n.widgetInit[a]=!0,e.hasInitialized&&L.applyWidgetOptions(e),"function"==typeof l.init&&(o=!0,d&&console[console.group?"group":"log"]("Initializing "+a+" widget"),l.init(e,l,n,i))),r||"function"!=typeof l.format||(o=!0,d&&console[console.group?"group":"log"]("Updating "+a+" widget"),l.format(e,n,i,!1)),d&&o&&(console.log("Completed "+(r?"initializing ":"applying ")+a+" widget"+L.benchmark(s)),console.groupEnd&&console.groupEnd()))},applyWidget:function(e,t,r){var o,s,a,n,i,d=(e=A(e)[0]).config,l=L.debug(d,"core"),c=[];if(!1===t||!e.hasInitialized||!e.isApplyingWidgets&&!e.isUpdating){if(l&&(i=new Date),L.addWidgetFromClass(e),clearTimeout(d.timerReady),d.widgets.length){for(e.isApplyingWidgets=!0,d.widgets=A.grep(d.widgets,function(e,t){return A.inArray(e,d.widgets)===t}),s=(a=d.widgets||[]).length,o=0;o<s;o++)(n=L.getWidgetById(a[o]))&&n.id?(n.priority||(n.priority=10),c[o]=n):l&&console.warn('"'+a[o]+'" was enabled, but the widget code has not been loaded!');for(c.sort(function(e,t){return e.priority<t.priority?-1:e.priority===t.priority?0:1}),s=c.length,l&&console[console.group?"group":"log"]("Start "+(t?"initializing":"applying")+" widgets"),o=0;o<s;o++)(n=c[o])&&n.id&&L.applyWidgetId(e,n.id,t);l&&console.groupEnd&&console.groupEnd()}d.timerReady=setTimeout(function(){e.isApplyingWidgets=!1,A.data(e,"lastWidgetApplication",new Date),d.$table.triggerHandler("tablesorter-ready"),t||"function"!=typeof r||r(e),l&&(n=d.widgets.length,console.log("Completed "+(!0===t?"initializing ":"applying ")+n+" widget"+(1!==n?"s":"")+L.benchmark(i)))},10)}},removeWidget:function(e,t,r){var o,s,a,n,i=(e=A(e)[0]).config;if(!0===t)for(t=[],n=L.widgets.length,a=0;a<n;a++)(s=L.widgets[a])&&s.id&&(t[t.length]=s.id);else t=(A.isArray(t)?t.join(","):t||"").toLowerCase().split(/[\s,]+/);for(n=t.length,o=0;o<n;o++)s=L.getWidgetById(t[o]),0<=(a=A.inArray(t[o],i.widgets))&&!0!==r&&i.widgets.splice(a,1),s&&s.remove&&(L.debug(i,"core")&&console.log((r?"Refreshing":"Removing")+' "'+t[o]+'" widget'),s.remove(e,i,i.widgetOptions,r),i.widgetInit[t[o]]=!1);i.$table.triggerHandler("widgetRemoveEnd",e)},refreshWidgets:function(e,t,r){var o,s,a=(e=A(e)[0]).config.widgets,n=L.widgets,i=n.length,d=[],l=function(e){A(e).triggerHandler("refreshComplete")};for(o=0;o<i;o++)(s=n[o])&&s.id&&(t||A.inArray(s.id,a)<0)&&(d[d.length]=s.id);L.removeWidget(e,d.join(","),!0),!0!==r?(L.applyWidget(e,t||!1,l),t&&L.applyWidget(e,!1,l)):l(e)},benchmark:function(e){return" ("+((new Date).getTime()-e.getTime())+" ms)"},log:function(){console.log(arguments)},debug:function(e,t){return e&&(!0===e.debug||"string"==typeof e.debug&&-1<e.debug.indexOf(t))},isEmptyObject:function(e){for(var t in e)return!1;return!0},isValueInArray:function(e,t){var r,o=t&&t.length||0;for(r=0;r<o;r++)if(t[r][0]===e)return r;return-1},formatFloat:function(e,t){return"string"!=typeof e||""===e?e:(e=(t&&t.config?!1!==t.config.usNumberFormat:void 0===t||t)?e.replace(L.regex.comma,""):e.replace(L.regex.digitNonUS,"").replace(L.regex.comma,"."),L.regex.digitNegativeTest.test(e)&&(e=e.replace(L.regex.digitNegativeReplace,"-$1")),r=parseFloat(e),isNaN(r)?A.trim(e):r);var r},isDigit:function(e){return isNaN(e)?L.regex.digitTest.test(e.toString().replace(L.regex.digitReplace,"")):""!==e},computeColumnIndex:function(e,t){var r,o,s,a,n,i,d,l,c,g,p=t&&t.columns||0,u=[],f=new Array(p);for(r=0;r<e.length;r++)for(i=e[r].cells,o=0;o<i.length;o++){for(d=r,l=(n=i[o]).rowSpan||1,c=n.colSpan||1,void 0===u[d]&&(u[d]=[]),s=0;s<u[d].length+1;s++)if(void 0===u[d][s]){g=s;break}for(p&&n.cellIndex===g||(n.setAttribute?n.setAttribute("data-column",g):A(n).attr("data-column",g)),s=d;s<d+l;s++)for(void 0===u[s]&&(u[s]=[]),f=u[s],a=g;a<g+c;a++)f[a]="x"}return L.checkColumnCount(e,u,f.length),f.length},checkColumnCount:function(e,t,r){var o,s,a=!0,n=[];for(o=0;o<t.length;o++)if(t[o]&&(s=t[o].length,t[o].length!==r)){a=!1;break}a||(e.each(function(e,t){var r=t.parentElement.nodeName;n.indexOf(r)<0&&n.push(r)}),console.error("Invalid or incorrect number of columns in the "+n.join(" or ")+"; expected "+r+", but found "+s+" columns"))},fixColumnWidth:function(e){var t,r,o,s,a,n=(e=A(e)[0]).config,i=n.$table.children("colgroup");if(i.length&&i.hasClass(L.css.colgroup)&&i.remove(),n.widthFixed&&0===n.$table.children("colgroup").length){for(i=A('<colgroup class="'+L.css.colgroup+'">'),t=n.$table.width(),s=(o=n.$tbodies.find("tr:first").children(":visible")).length,a=0;a<s;a++)r=parseInt(o.eq(a).width()/t*1e3,10)/10+"%",i.append(A("<col>").css("width",r));n.$table.prepend(i)}},getData:function(e,t,r){var o,s,a="",n=A(e);return n.length?(o=!!A.metadata&&n.metadata(),s=" "+(n.attr("class")||""),void 0!==n.data(r)||void 0!==n.data(r.toLowerCase())?a+=n.data(r)||n.data(r.toLowerCase()):o&&void 0!==o[r]?a+=o[r]:t&&void 0!==t[r]?a+=t[r]:" "!==s&&s.match(" "+r+"-")&&(a=s.match(new RegExp("\\s"+r+"-([\\w-]+)"))[1]||""),A.trim(a)):""},getColumnData:function(e,t,r,o,s){if("object"!=typeof t||null===t)return t;var a,n=(e=A(e)[0]).config,i=s||n.$headers,d=n.$headerIndexed&&n.$headerIndexed[r]||i.find('[data-column="'+r+'"]:last');if(void 0!==t[r])return o?t[r]:t[i.index(d)];for(a in t)if("string"==typeof a&&d.filter(a).add(d.find(a)).length)return t[a]},isProcessing:function(e,t,r){var o=(e=A(e))[0].config,s=r||e.find("."+L.css.header);t?(void 0!==r&&0<o.sortList.length&&(s=s.filter(function(){return!this.sortDisabled&&0<=L.isValueInArray(parseFloat(A(this).attr("data-column")),o.sortList)})),e.add(s).addClass(L.css.processing+" "+o.cssProcessing)):e.add(s).removeClass(L.css.processing+" "+o.cssProcessing)},processTbody:function(e,t,r){if(e=A(e)[0],r)return e.isProcessing=!0,t.before('<colgroup class="tablesorter-savemyplace"/>'),A.fn.detach?t.detach():t.remove();var o=A(e).find("colgroup.tablesorter-savemyplace");t.insertAfter(o),o.remove(),e.isProcessing=!1},clearTableBody:function(e){A(e)[0].config.$tbodies.children().detach()},characterEquivalents:{a:"áàâãäąå",A:"ÁÀÂÃÄĄÅ",c:"çćč",C:"ÇĆČ",e:"éèêëěę",E:"ÉÈÊËĚĘ",i:"íìİîïı",I:"ÍÌİÎÏ",o:"óòôõöō",O:"ÓÒÔÕÖŌ",ss:"ß",SS:"ẞ",u:"úùûüů",U:"ÚÙÛÜŮ"},replaceAccents:function(e){var t,r="[",o=L.characterEquivalents;if(!L.characterRegex){for(t in L.characterRegexArray={},o)"string"==typeof t&&(r+=o[t],L.characterRegexArray[t]=new RegExp("["+o[t]+"]","g"));L.characterRegex=new RegExp(r+"]")}if(L.characterRegex.test(e))for(t in o)"string"==typeof t&&(e=e.replace(L.characterRegexArray[t],t));return e},validateOptions:function(e){var t,r,o,s,a="headers sortForce sortList sortAppend widgets".split(" "),n=e.originalSettings;if(n){for(t in L.debug(e,"core")&&(s=new Date),n)if("undefined"===(o=typeof L.defaults[t]))console.warn('Tablesorter Warning! "table.config.'+t+'" option not recognized');else if("object"===o)for(r in n[t])o=L.defaults[t]&&typeof L.defaults[t][r],A.inArray(t,a)<0&&"undefined"===o&&console.warn('Tablesorter Warning! "table.config.'+t+"."+r+'" option not recognized');L.debug(e,"core")&&console.log("validate options time:"+L.benchmark(s))}},restoreHeaders:function(e){var t,r,o=A(e)[0].config,s=o.$table.find(o.selectorHeaders),a=s.length;for(t=0;t<a;t++)(r=s.eq(t)).find("."+L.css.headerIn).length&&r.html(o.headerContent[t])},destroy:function(e,t,r){if((e=A(e)[0]).hasInitialized){L.removeWidget(e,!0,!1);var o,s=A(e),a=e.config,n=s.find("thead:first"),i=n.find("tr."+L.css.headerRow).removeClass(L.css.headerRow+" "+a.cssHeaderRow),d=s.find("tfoot:first > tr").children("th, td");!1===t&&0<=A.inArray("uitheme",a.widgets)&&(s.triggerHandler("applyWidgetId",["uitheme"]),s.triggerHandler("applyWidgetId",["zebra"])),n.find("tr").not(i).remove(),o="sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets removeWidget destroy mouseup mouseleave "+"keypress sortBegin sortEnd resetToLoadState ".split(" ").join(a.namespace+" "),s.removeData("tablesorter").unbind(o.replace(L.regex.spaces," ")),a.$headers.add(d).removeClass([L.css.header,a.cssHeader,a.cssAsc,a.cssDesc,L.css.sortAsc,L.css.sortDesc,L.css.sortNone].join(" ")).removeAttr("data-column").removeAttr("aria-label").attr("aria-disabled","true"),i.find(a.selectorSort).unbind("mousedown mouseup keypress ".split(" ").join(a.namespace+" ").replace(L.regex.spaces," ")),L.restoreHeaders(e),s.toggleClass(L.css.table+" "+a.tableClass+" tablesorter-"+a.theme,!1===t),s.removeClass(a.namespace.slice(1)),e.hasInitialized=!1,delete e.config.cache,"function"==typeof r&&r(e),L.debug(a,"core")&&console.log("tablesorter has been removed")}}};A.fn.tablesorter=function(t){return this.each(function(){var e=A.extend(!0,{},L.defaults,t,L.instanceMethods);e.originalSettings=t,!this.hasInitialized&&L.buildTable&&"TABLE"!==this.nodeName?L.buildTable(this,e):L.setup(this,e)})},window.console&&window.console.log||(L.logs=[],console={},console.log=console.warn=console.error=console.table=function(){var e=1<arguments.length?arguments:arguments[0];L.logs[L.logs.length]={date:Date.now(),log:e}}),L.addParser({id:"no-parser",is:function(){return!1},format:function(){return""},type:"text"}),L.addParser({id:"text",is:function(){return!0},format:function(e,t){var r=t.config;return e&&(e=A.trim(r.ignoreCase?e.toLocaleLowerCase():e),e=r.sortLocaleCompare?L.replaceAccents(e):e),e},type:"text"}),L.regex.nondigit=/[^\w,. \-()]/g,L.addParser({id:"digit",is:function(e){return L.isDigit(e)},format:function(e,t){var r=L.formatFloat((e||"").replace(L.regex.nondigit,""),t);return e&&"number"==typeof r?r:e?A.trim(e&&t.config.ignoreCase?e.toLocaleLowerCase():e):e},type:"numeric"}),L.regex.currencyReplace=/[+\-,. ]/g,L.regex.currencyTest=/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/,L.addParser({id:"currency",is:function(e){return e=(e||"").replace(L.regex.currencyReplace,""),L.regex.currencyTest.test(e)},format:function(e,t){var r=L.formatFloat((e||"").replace(L.regex.nondigit,""),t);return e&&"number"==typeof r?r:e?A.trim(e&&t.config.ignoreCase?e.toLocaleLowerCase():e):e},type:"numeric"}),L.regex.urlProtocolTest=/^(https?|ftp|file):\/\//,L.regex.urlProtocolReplace=/(https?|ftp|file):\/\/(www\.)?/,L.addParser({id:"url",is:function(e){return L.regex.urlProtocolTest.test(e)},format:function(e){return e?A.trim(e.replace(L.regex.urlProtocolReplace,"")):e},type:"text"}),L.regex.dash=/-/g,L.regex.isoDate=/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/,L.addParser({id:"isoDate",is:function(e){return L.regex.isoDate.test(e)},format:function(e){var t=e?new Date(e.replace(L.regex.dash,"/")):e;return t instanceof Date&&isFinite(t)?t.getTime():e},type:"numeric"}),L.regex.percent=/%/g,L.regex.percentTest=/(\d\s*?%|%\s*?\d)/,L.addParser({id:"percent",is:function(e){return L.regex.percentTest.test(e)&&e.length<15},format:function(e,t){return e?L.formatFloat(e.replace(L.regex.percent,""),t):e},type:"numeric"}),L.addParser({id:"image",is:function(e,t,r,o){return 0<o.find("img").length},format:function(e,t,r){return A(r).find("img").attr(t.config.imgAttr||"alt")||e},parsed:!0,type:"text"}),L.regex.dateReplace=/(\S)([AP]M)$/i,L.regex.usLongDateTest1=/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i,L.regex.usLongDateTest2=/^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i,L.addParser({id:"usLongDate",is:function(e){return L.regex.usLongDateTest1.test(e)||L.regex.usLongDateTest2.test(e)},format:function(e){var t=e?new Date(e.replace(L.regex.dateReplace,"$1 $2")):e;return t instanceof Date&&isFinite(t)?t.getTime():e},type:"numeric"}),L.regex.shortDateTest=/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/,L.regex.shortDateReplace=/[\-.,]/g,L.regex.shortDateXXY=/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/,L.regex.shortDateYMD=/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/,L.convertFormat=function(e,t){e=(e||"").replace(L.regex.spaces," ").replace(L.regex.shortDateReplace,"/"),"mmddyyyy"===t?e=e.replace(L.regex.shortDateXXY,"$3/$1/$2"):"ddmmyyyy"===t?e=e.replace(L.regex.shortDateXXY,"$3/$2/$1"):"yyyymmdd"===t&&(e=e.replace(L.regex.shortDateYMD,"$1/$2/$3"));var r=new Date(e);return r instanceof Date&&isFinite(r)?r.getTime():""},L.addParser({id:"shortDate",is:function(e){return e=(e||"").replace(L.regex.spaces," ").replace(L.regex.shortDateReplace,"/"),L.regex.shortDateTest.test(e)},format:function(e,t,r,o){if(e){var s=t.config,a=s.$headerIndexed[o],n=a.length&&a.data("dateFormat")||L.getData(a,L.getColumnData(t,s.headers,o),"dateFormat")||s.dateFormat;return a.length&&a.data("dateFormat",n),L.convertFormat(e,n)||e}return e},type:"numeric"}),L.regex.timeTest=/^(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)$|^((?:[01]\d|[2][0-4]):[0-5]\d)$/i,L.regex.timeMatch=/(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i,L.addParser({id:"time",is:function(e){return L.regex.timeTest.test(e)},format:function(e){var t=(e||"").match(L.regex.timeMatch),r=new Date(e),o=e&&(null!==t?t[0]:"00:00 AM"),s=o?new Date("2000/01/01 "+o.replace(L.regex.dateReplace,"$1 $2")):o;return s instanceof Date&&isFinite(s)?(r instanceof Date&&isFinite(r)?r.getTime():0)?parseFloat(s.getTime()+"."+r.getTime()):s.getTime():e},type:"numeric"}),L.addParser({id:"metadata",is:function(){return!1},format:function(e,t,r){var o=t.config,s=o.parserMetadataName?o.parserMetadataName:"sortValue";return A(r).metadata()[s]},type:"numeric"}),L.addWidget({id:"zebra",priority:90,format:function(e,t,r){var o,s,a,n,i,d,l,c=new RegExp(t.cssChildRow,"i"),g=t.$tbodies.add(A(t.namespace+"_extra_table").children("tbody:not(."+t.cssInfoBlock+")"));for(i=0;i<g.length;i++)for(a=0,l=(o=g.eq(i).children("tr:visible").not(t.selectorRemove)).length,d=0;d<l;d++)s=o.eq(d),c.test(s[0].className)||a++,n=a%2==0,s.removeClass(r.zebra[n?1:0]).addClass(r.zebra[n?0:1])},remove:function(e,t,r,o){if(!o){var s,a,n=t.$tbodies,i=(r.zebra||["even","odd"]).join(" ");for(s=0;s<n.length;s++)(a=L.processTbody(e,n.eq(s),!0)).children().removeClass(i),L.processTbody(e,a,!1)}}})}(e),e.tablesorter}); diff --git a/web/modules/simple_sitemap/xsl/parser-date-iso8601.min.js b/web/modules/simple_sitemap/xsl/parser-date-iso8601.min.js new file mode 100644 index 0000000000..fe116cb92d --- /dev/null +++ b/web/modules/simple_sitemap/xsl/parser-date-iso8601.min.js @@ -0,0 +1,4 @@ +(function(factory){if (typeof define === 'function' && define.amd){define(['jquery'], factory);} else if (typeof module === 'object' && typeof module.exports === 'object'){module.exports = factory(require('jquery'));} else {factory(jQuery);}}(function(jQuery){ + +/*! Parser: ISO-8601 date - updated 10/26/2014 (v2.18.0) */ +!function(e){"use strict";var s=/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$/;e.tablesorter.addParser({id:"iso8601date",is:function(e){return!!e&&e.match(s)},format:function(e){var t=e?e.match(s):e;if(t){var r=new Date(t[1],0,1);return t[3]&&r.setMonth(t[3]-1),t[5]&&r.setDate(t[5]),t[7]&&r.setHours(t[7]),t[8]&&r.setMinutes(t[8]),t[10]&&r.setSeconds(t[10]),t[12]&&r.setMilliseconds(1e3*Number("0."+t[12])),r.getTime()}return e},type:"numeric"})}(jQuery);return jQuery;})); diff --git a/web/modules/simple_sitemap/xsl/simple_sitemap.xsl b/web/modules/simple_sitemap/xsl/simple_sitemap.xsl new file mode 100644 index 0000000000..b1f5def9ad --- /dev/null +++ b/web/modules/simple_sitemap/xsl/simple_sitemap.xsl @@ -0,0 +1,192 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xsl:stylesheet version="2.0" + xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" + xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:xhtml="http://www.w3.org/1999/xhtml"> + <xsl:output method="html" encoding="UTF-8" indent="yes"/> + + <!-- Root template --> + <xsl:template match="/"> + <html> + <head> + <title>[title]</title> + <script type="text/javascript" src="[jquery]"/> + <script type="text/javascript" src="[jquery-tablesorter]"/> + <script type="text/javascript" src="[parser-date-iso8601]"/> + <script type="text/javascript" src="[xsl-js]"/> + <link href="[xsl-css]" type="text/css" rel="stylesheet"/> + </head> + <body> + <h1>[title]</h1> + + <xsl:choose> + <xsl:when test="//sitemap:url"> + <xsl:call-template name="sitemapTable"/> + </xsl:when> + <xsl:otherwise> + <xsl:call-template name="sitemapIndexTable"/> + </xsl:otherwise> + </xsl:choose> + + <div id="footer"> + <p>[generated-by]</p> + </div> + </body> + </html> + </xsl:template> + + <!-- sitemapIndexTable template --> + <xsl:template name="sitemapIndexTable"> + <div id="information"> + <p>[number-of-sitemaps]: + <xsl:value-of select="count(sitemap:sitemapindex/sitemap:sitemap)"/> + </p> + </div> + <table class="sitemap index"> + <thead> + <tr> + <th>[sitemap-url]</th> + <th>[lastmod]</th> + </tr> + </thead> + <tbody> + <xsl:apply-templates select="sitemap:sitemapindex/sitemap:sitemap"/> + </tbody> + </table> + </xsl:template> + + <!-- sitemapTable template --> + <xsl:template name="sitemapTable"> + <div id="information"> + <p>[number-of-urls]: + <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> + </p> + </div> + <table class="sitemap"> + <thead> + <tr> + <th>[url-location]</th> + <th>[lastmod]</th> + <th>[changefreq]</th> + <th>[priority]</th> + <!-- Show this header only if xhtml:link elements are present --> + <xsl:if test="sitemap:urlset/sitemap:url/xhtml:link"> + <th>[translation-set]</th> + </xsl:if> + <!-- Show this header only if image:image elements are present --> + <xsl:if test="sitemap:urlset/sitemap:url/image:image"> + <th>[images]</th> + </xsl:if> + </tr> + </thead> + <tbody> + <xsl:apply-templates select="sitemap:urlset/sitemap:url"/> + </tbody> + </table> + </xsl:template> + + <!-- sitemap:sitemap template --> + <xsl:template match="sitemap:sitemap"> + <tr> + <td> + <xsl:variable name="sitemap_location"> + <xsl:value-of select="sitemap:loc"/> + </xsl:variable> + <a href="{$sitemap_location}"> + <xsl:value-of select="$sitemap_location"/> + </a> + </td> + <td> + <xsl:value-of select="sitemap:lastmod"/> + </td> + </tr> + </xsl:template> + + <!-- sitemap:url template --> + <xsl:template match="sitemap:url"> + <tr> + <td> + <xsl:variable name="url_location"> + <xsl:value-of select="sitemap:loc"/> + </xsl:variable> + <a href="{$url_location}"> + <xsl:value-of select="$url_location"/> + </a> + </td> + <td> + <xsl:value-of select="sitemap:lastmod"/> + </td> + <td> + <xsl:value-of select="sitemap:changefreq"/> + </td> + <td> + <xsl:choose> + <!-- If priority is not defined, show the default value of 0.5 --> + <xsl:when test="sitemap:priority"> + <xsl:value-of select="sitemap:priority"/> + </xsl:when> + <xsl:otherwise>0.5</xsl:otherwise> + </xsl:choose> + </td> + <!-- Show this column only if xhtml:link elements are present --> + <xsl:if test="/sitemap:urlset/sitemap:url/xhtml:link"> + <td> + <xsl:if test="xhtml:link"> + <ul class="translation-set"> + <xsl:apply-templates select="xhtml:link"/> + </ul> + </xsl:if> + </td> + </xsl:if> + <!-- Show this column only if image:image elements are present --> + <xsl:if test="/sitemap:urlset/sitemap:url/image:image"> + <td> + <xsl:if test="image:image"> + <ul class="images"> + <xsl:apply-templates select="image:image"/> + </ul> + </xsl:if> + </td> + </xsl:if> + </tr> + </xsl:template> + + <!-- xhtml:link template --> + <xsl:template match="xhtml:link"> + <xsl:variable name="url_location"> + <xsl:value-of select="@href"/> + </xsl:variable> + <li> + <span> + <xsl:value-of select="@hreflang"/> + </span> + <a href="{$url_location}"> + <xsl:value-of select="$url_location"/> + </a> + </li> + </xsl:template> + + <!-- image:image template --> + <xsl:template match="image:image"> + <xsl:variable name="image_location"> + <xsl:value-of select="image:loc"/> + </xsl:variable> + <xsl:variable name="image_title"> + <xsl:value-of select="image:title"/> + </xsl:variable> + <li> + <a href="{$image_location}" title="{$image_title}"> + <xsl:choose> + <xsl:when test="image:caption"> + <xsl:value-of select="image:caption"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="$image_location"/> + </xsl:otherwise> + </xsl:choose> + </a> + </li> + </xsl:template> + +</xsl:stylesheet> diff --git a/web/modules/simple_sitemap/xsl/simple_sitemap.xsl.css b/web/modules/simple_sitemap/xsl/simple_sitemap.xsl.css new file mode 100644 index 0000000000..69c94893a2 --- /dev/null +++ b/web/modules/simple_sitemap/xsl/simple_sitemap.xsl.css @@ -0,0 +1,91 @@ +body { + background-color: #fff; + font-family: Verdana, sans-serif; + font-size: 10pt; +} + +h1 { + font-size: 1.25em; +} + +table.sitemap { + background-color: #cdcdcd; + margin: 10px 0 15px; + font-size: 8pt; + width: 100%; + text-align: left; +} + +table.sitemap thead tr th, +table.sitemap tfoot tr th { + background-color: #e6eeee; + border: 1px solid #fff; + font-size: 8pt; + padding: 3px; +} + +table.sitemap thead tr .tablesorter-header:not(.sorter-false) { + cursor: pointer; +} + +table.sitemap thead tr .tablesorter-header .tablesorter-header-inner { + position: relative; + display: inline-block; + padding-right: 15px; +} + +table.sitemap tbody td { + color: #3d3d3d; + padding: 3px; + background-color: #fff; + vertical-align: top; +} + +table.sitemap tbody .odd td { + background-color: #efefef; +} + +table.sitemap thead tr .tablesorter-headerAsc, +table.sitemap thead tr .tablesorter-headerDesc { + background-color: #5050d3; + color: #fff; + font-style: italic; +} + +table.sitemap thead tr .tablesorter-headerAsc .tablesorter-header-inner:after { + content: '\25b2'; + position:absolute; + right: 0; +} + +table.sitemap thead tr .tablesorter-headerDesc .tablesorter-header-inner:after { + content: '\25bc'; + position:absolute; + right: 0; +} + +table.sitemap tbody tr ul { + list-style: none; + padding: 0; + margin: 0; +} + +table.sitemap tbody tr ul li:not(:first-of-type) { + margin-top: 5px; +} + +table.sitemap tbody tr ul li span { + margin-right: 5px; +} + +table.sitemap tbody tr ul li span:after { + content: ":"; +} + +table.sitemap tbody tr ul.translation-set li span { + text-transform: uppercase; +} + +table.sitemap tbody tr ul.images li span { + font-style: italic; +} diff --git a/web/modules/simple_sitemap/xsl/simple_sitemap.xsl.js b/web/modules/simple_sitemap/xsl/simple_sitemap.xsl.js new file mode 100644 index 0000000000..eb23b63f85 --- /dev/null +++ b/web/modules/simple_sitemap/xsl/simple_sitemap.xsl.js @@ -0,0 +1,69 @@ +/** + * @file + * Alters jquery.tablesorter behaviour. + */ + +(function ($) { + + 'use strict'; + + $.tablesorter.addParser({ + // Set a unique id. + id: 'changefreq', + is: function (s) { + return false; + }, + format: function (s) { + switch (s) { + case 'always': + return 0; + + case 'hourly': + return 1; + + case 'daily': + return 2; + + case 'weekly': + return 3; + + case 'monthly': + return 4; + + case 'yearly': + return 5; + + default: + return 6; + } + }, + type: 'numeric' + }); + + $(document).ready(function () { + // Set some location variables. + var $h1 = $('h1'); + $h1.text($h1.text() + ': ' + location); + document.title = $h1.text(); + + var $table = $('table'); + var options = {widgets: ['zebra']}; + + if ($table.hasClass('index')) { + // Options for sitemap index table. + options.sortList = [[0, 0]]; + } + else { + // Options for sitemap table. + options.sortList = [[3, 1]]; + options.headers = { + 2: {sorter: 'changefreq'}, + 4: {sorter: false }, + 5: {sorter: false } + }; + } + + $table.tablesorter(options); + }); + +})(jQuery); -- GitLab