Pull merge.
[yaffs-website] / web / modules / contrib / simple_sitemap / src / Simplesitemap.php
1 <?php
2
3 namespace Drupal\simple_sitemap;
4
5 use Drupal\Core\Database\Connection;
6 use Drupal\Core\Entity\EntityTypeManagerInterface;
7 use Drupal\Core\Path\PathValidator;
8 use Drupal\Core\Config\ConfigFactory;
9 use Drupal\Core\Datetime\DateFormatter;
10 use Drupal\Component\Datetime\Time;
11 use Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager;
12
13 /**
14  * Class Simplesitemap
15  * @package Drupal\simple_sitemap
16  */
17 class Simplesitemap {
18
19   /**
20    * @var \Drupal\simple_sitemap\SitemapGenerator
21    */
22   protected $sitemapGenerator;
23
24   /**
25    * @var \Drupal\simple_sitemap\EntityHelper
26    */
27   protected $entityHelper;
28
29   /**
30    * @var \Drupal\Core\Config\ConfigFactory
31    */
32   protected $configFactory;
33
34   /**
35    * @var \Drupal\Core\Database\Connection
36    */
37   protected $db;
38
39   /**
40    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
41    */
42   protected $entityTypeManager;
43
44   /**
45    * @var \Drupal\Core\Path\PathValidator
46    */
47   protected $pathValidator;
48
49   /**
50    * @var \Drupal\Core\Datetime\DateFormatter
51    */
52   protected $dateFormatter;
53
54   /**
55    * @var \Drupal\Component\Datetime\Time
56    */
57   protected $time;
58
59   /**
60    * @var \Drupal\simple_sitemap\Batch
61    */
62   protected $batch;
63
64   /**
65    * @var \Drupal\Core\Extension\ModuleHandler
66    */
67   protected $moduleHandler;
68
69   /**
70    * @var \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager
71    */
72   protected $urlGeneratorManager;
73
74   /**
75    * @var array
76    */
77   protected static $allowedLinkSettings = [
78     'entity' => ['index', 'priority', 'changefreq', 'include_images'],
79     'custom' => ['priority', 'changefreq'],
80   ];
81
82   /**
83    * @var array
84    */
85   protected static $linkSettingDefaults = [
86     'index' => 1,
87     'priority' => 0.5,
88     'changefreq' => '',
89     'include_images' => 0,
90   ];
91
92   /**
93    * Simplesitemap constructor.
94    * @param \Drupal\simple_sitemap\SitemapGenerator $sitemapGenerator
95    * @param \Drupal\simple_sitemap\EntityHelper $entityHelper
96    * @param \Drupal\Core\Config\ConfigFactory $configFactory
97    * @param \Drupal\Core\Database\Connection $database
98    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
99    * @param \Drupal\Core\Path\PathValidator $pathValidator
100    * @param \Drupal\Core\Datetime\DateFormatter $dateFormatter
101    * @param \Drupal\Component\Datetime\Time $time
102    * @param \Drupal\simple_sitemap\Batch $batch
103    * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager $urlGeneratorManager
104    */
105   public function __construct(
106     SitemapGenerator $sitemapGenerator,
107     EntityHelper $entityHelper,
108     ConfigFactory $configFactory,
109     Connection $database,
110     EntityTypeManagerInterface $entityTypeManager,
111     PathValidator $pathValidator,
112     DateFormatter $dateFormatter,
113     Time $time,
114     Batch $batch,
115     UrlGeneratorManager $urlGeneratorManager
116   ) {
117     $this->sitemapGenerator = $sitemapGenerator;
118     $this->entityHelper = $entityHelper;
119     $this->configFactory = $configFactory;
120     $this->db = $database;
121     $this->entityTypeManager = $entityTypeManager;
122     $this->pathValidator = $pathValidator;
123     $this->dateFormatter = $dateFormatter;
124     $this->time = $time;
125     $this->batch = $batch;
126     $this->urlGeneratorManager = $urlGeneratorManager;
127   }
128
129   /**
130    * Returns a specific sitemap setting or a default value if setting does not
131    * exist.
132    *
133    * @param string $name
134    *   Name of the setting, like 'max_links'.
135    *
136    * @param mixed $default
137    *   Value to be returned if the setting does not exist in the configuration.
138    *
139    * @return mixed
140    *   The current setting from configuration or a default value.
141    */
142   public function getSetting($name, $default = FALSE) {
143     $setting = $this->configFactory
144       ->get('simple_sitemap.settings')
145       ->get($name);
146     return NULL !== $setting ? $setting : $default;
147   }
148
149   /**
150    * Stores a specific sitemap setting in configuration.
151    *
152    * @param string $name
153    *   Setting name, like 'max_links'.
154    * @param mixed $setting
155    *   The setting to be saved.
156    *
157    * @return $this
158    */
159   public function saveSetting($name, $setting) {
160     $this->configFactory->getEditable('simple_sitemap.settings')
161       ->set($name, $setting)->save();
162     return $this;
163   }
164
165   /**
166    * Returns the whole sitemap, a requested sitemap chunk,
167    * or the sitemap index file.
168    *
169    * @param int $chunk_id
170    *
171    * @return string|false
172    *   If no sitemap id provided, either a sitemap index is returned, or the
173    *   whole sitemap, if the amount of links does not exceed the max links
174    *   setting. If a sitemap id is provided, a sitemap chunk is returned. False
175    *   if sitemap is not retrievable from the database.
176    */
177   public function getSitemap($chunk_id = NULL) {
178     $chunk_info = $this->fetchSitemapChunkInfo();
179
180     if (NULL === $chunk_id || !isset($chunk_info[$chunk_id])) {
181
182       if (count($chunk_info) > 1) {
183         // Return sitemap index, if there are multiple sitemap chunks.
184         return $this->getSitemapIndex($chunk_info);
185       }
186       else {
187         // Return sitemap if there is only one chunk.
188         return count($chunk_info) === 1
189         && isset($chunk_info[SitemapGenerator::FIRST_CHUNK_INDEX])
190           ? $this->fetchSitemapChunk(SitemapGenerator::FIRST_CHUNK_INDEX)
191             ->sitemap_string
192           : FALSE;
193       }
194     }
195     else {
196       // Return specific sitemap chunk.
197       return $this->fetchSitemapChunk($chunk_id)->sitemap_string;
198     }
199   }
200
201   /**
202    * Fetches all sitemap chunk timestamps keyed by chunk ID.
203    *
204    * @return array
205    *   An array containing chunk creation timestamps keyed by chunk ID.
206    */
207   protected function fetchSitemapChunkInfo() {
208     return $this->db
209       ->query('SELECT id, sitemap_created FROM {simple_sitemap}')
210       ->fetchAllAssoc('id');
211   }
212
213   /**
214    * Fetches a single sitemap chunk by ID.
215    *
216    * @param int $id
217    *   The chunk ID.
218    *
219    * @return object
220    *   A sitemap chunk object.
221    */
222   protected function fetchSitemapChunk($id) {
223     return $this->db->query('SELECT * FROM {simple_sitemap} WHERE id = :id',
224       [':id' => $id])->fetchObject();
225   }
226
227   /**
228    * Generates the XML sitemap and saves it to the db.
229    *
230    * @param string $from
231    *   Can be 'form', 'backend', 'drush' or 'nobatch'.
232    *   This decides how the batch process is to be run.
233    *
234    * @return bool|\Drupal\simple_sitemap\Simplesitemap
235    */
236   public function generateSitemap($from = 'form') {
237
238     $this->batch->setBatchSettings([
239       'base_url' => $this->getSetting('base_url', ''),
240       'batch_process_limit' => $this->getSetting('batch_process_limit', NULL),
241       'max_links' => $this->getSetting('max_links', 2000),
242       'skip_untranslated' => $this->getSetting('skip_untranslated', FALSE),
243       'remove_duplicates' => $this->getSetting('remove_duplicates', TRUE),
244       'excluded_languages' => $this->getSetting('excluded_languages', []),
245       'from' => $from,
246     ]);
247
248     $plugins = $this->urlGeneratorManager->getDefinitions();
249
250     usort($plugins, function($a, $b) {
251       return $a['weight'] - $b['weight'];
252     });
253
254     foreach ($plugins as $plugin) {
255       if ($plugin['enabled']) {
256         if (!empty($plugin['settings']['instantiate_for_each_data_set'])) {
257           foreach ($this->urlGeneratorManager->createInstance($plugin['id'])->getDataSets() as $data_sets) {
258             $this->batch->addOperation($plugin['id'], $data_sets);
259           }
260         }
261         else {
262           $this->batch->addOperation($plugin['id']);
263         }
264       }
265     }
266
267     $success = $this->batch->start();
268     return $from === 'nobatch' ? $this : $success;
269   }
270
271   /**
272    * Generates and returns the sitemap index as string.
273    *
274    * @param array $chunk_info
275    *   Array containing chunk creation timestamps keyed by chunk ID.
276    *
277    * @return string
278    *   The sitemap index.
279    *
280    * @todo Need to make sure response is cached.
281    */
282   protected function getSitemapIndex($chunk_info) {
283     return $this->sitemapGenerator
284       ->setSettings(['base_url' => $this->getSetting('base_url', '')])
285       ->generateSitemapIndex($chunk_info);
286   }
287
288   /**
289    * Returns a 'time ago' string of last timestamp generation.
290    *
291    * @return string|false
292    *   Formatted timestamp of last sitemap generation, otherwise FALSE.
293    */
294   public function getGeneratedAgo() {
295     $chunks = $this->fetchSitemapChunkInfo();
296     if (isset($chunks[SitemapGenerator::FIRST_CHUNK_INDEX]->sitemap_created)) {
297       return $this->dateFormatter
298         ->formatInterval($this->time->getRequestTime() - $chunks[SitemapGenerator::FIRST_CHUNK_INDEX]
299             ->sitemap_created);
300     }
301     return FALSE;
302   }
303
304   /**
305    * Enables sitemap support for an entity type. Enabled entity types show
306    * sitemap settings on their bundle setting forms. If an enabled entity type
307    * features bundles (e.g. 'node'), it needs to be set up with
308    * setBundleSettings() as well.
309    *
310    * @param string $entity_type_id
311    *  Entity type id like 'node'.
312    *
313    * @return $this
314    */
315   public function enableEntityType($entity_type_id) {
316     $enabled_entity_types = $this->getSetting('enabled_entity_types');
317     if (!in_array($entity_type_id, $enabled_entity_types)) {
318       $enabled_entity_types[] = $entity_type_id;
319       $this->saveSetting('enabled_entity_types', $enabled_entity_types);
320     }
321     return $this;
322   }
323
324   /**
325    * Disables sitemap support for an entity type. Disabling support for an
326    * entity type deletes its sitemap settings permanently and removes sitemap
327    * settings from entity forms.
328    *
329    * @param string $entity_type_id
330    *  Entity type id like 'node'.
331    *
332    * @return $this
333    */
334   public function disableEntityType($entity_type_id) {
335
336     // Updating settings.
337     $enabled_entity_types = $this->getSetting('enabled_entity_types');
338     if (FALSE !== ($key = array_search($entity_type_id, $enabled_entity_types))) {
339       unset ($enabled_entity_types[$key]);
340       $this->saveSetting('enabled_entity_types', array_values($enabled_entity_types));
341     }
342
343     // Deleting inclusion settings.
344     $config_names = $this->configFactory->listAll("simple_sitemap.bundle_settings.$entity_type_id.");
345     foreach ($config_names as $config_name) {
346       $this->configFactory->getEditable($config_name)->delete();
347     }
348
349     // Deleting entity overrides.
350     $this->removeEntityInstanceSettings($entity_type_id);
351     return $this;
352   }
353
354   /**
355    * Sets sitemap settings for a non-bundle entity type (e.g. user) or a bundle
356    * of an entity type (e.g. page).
357    *
358    * @param string $entity_type_id
359    *  Entity type id like 'node' the bundle belongs to.
360    * @param string $bundle_name
361    *  Name of the bundle. NULL if entity type has no bundles.
362    * @param array $settings
363    *  An array of sitemap settings for this bundle/entity type.
364    *  Example: ['index' => TRUE, 'priority' => 0.5, 'changefreq' => 'never', 'include_images' => FALSE].
365    *
366    * @return $this
367    *
368    * @todo: enableEntityType automatically
369    */
370   public function setBundleSettings($entity_type_id, $bundle_name = NULL, $settings = []) {
371     $bundle_name = empty($bundle_name) ? $entity_type_id : $bundle_name;
372
373     if (!empty($old_settings = $this->getBundleSettings($entity_type_id, $bundle_name))) {
374       $settings = array_merge($old_settings, $settings);
375     }
376     else {
377       self::supplementDefaultSettings('entity', $settings);
378     }
379
380     $bundle_settings = $this->configFactory
381       ->getEditable("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name");
382     foreach ($settings as $setting_key => $setting) {
383       if ($setting_key === 'index') {
384         $setting = intval($setting);
385       }
386       $bundle_settings->set($setting_key, $setting);
387     }
388     $bundle_settings->save();
389
390     // Delete entity overrides which are identical to new bundle setting.
391     $sitemap_entity_types = $this->entityHelper->getSupportedEntityTypes();
392     if (isset($sitemap_entity_types[$entity_type_id])) {
393       $entity_type = $sitemap_entity_types[$entity_type_id];
394       $keys = $entity_type->getKeys();
395
396       // Menu fix.
397       $keys['bundle'] = $entity_type_id === 'menu_link_content' ? 'menu_name' : $keys['bundle'];
398
399       $query = $this->entityTypeManager->getStorage($entity_type_id)->getQuery();
400       if (!$this->entityHelper->entityTypeIsAtomic($entity_type_id)) {
401         $query->condition($keys['bundle'], $bundle_name);
402       }
403       $entity_ids = $query->execute();
404
405       $query = $this->db->select('simple_sitemap_entity_overrides', 'o')
406         ->fields('o', ['id', 'inclusion_settings'])
407         ->condition('o.entity_type', $entity_type_id);
408       if (!empty($entity_ids)) {
409         $query->condition('o.entity_id', $entity_ids, 'IN');
410       }
411
412       $delete_instances = [];
413       foreach ($query->execute()->fetchAll() as $result) {
414         $delete = TRUE;
415         $instance_settings = unserialize($result->inclusion_settings);
416         foreach ($instance_settings as $setting_key => $instance_setting) {
417           if ($instance_setting != $settings[$setting_key]) {
418             $delete = FALSE;
419             break;
420           }
421         }
422         if ($delete) {
423           $delete_instances[] = $result->id;
424         }
425       }
426       if (!empty($delete_instances)) {
427         $this->db->delete('simple_sitemap_entity_overrides')
428           ->condition('id', $delete_instances, 'IN')
429           ->execute();
430       }
431     }
432     else {
433       //todo: log error
434     }
435     return $this;
436   }
437
438   /**
439    * Gets sitemap settings for an entity bundle, a non-bundle entity type or for
440    * all entity types and their bundles.
441    *
442    * @param string|null $entity_type_id
443    *  If set to null, sitemap settings for all entity types and their bundles
444    *  are fetched.
445    * @param string|null $bundle_name
446    *
447    * @return array|false
448    *  Array of sitemap settings for an entity bundle, a non-bundle entity type
449    *  or for all entity types and their bundles.
450    *  False if entity type does not exist.
451    */
452   public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL) {
453     if (NULL !== $entity_type_id) {
454       $bundle_name = empty($bundle_name) ? $entity_type_id : $bundle_name;
455       $bundle_settings = $this->configFactory
456         ->get("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name")
457         ->get();
458       return !empty($bundle_settings) ? $bundle_settings : FALSE;
459     }
460     else {
461       $config_names = $this->configFactory->listAll('simple_sitemap.bundle_settings.');
462       $all_settings = [];
463       foreach ($config_names as $config_name) {
464         $config_name_parts = explode('.', $config_name);
465         $all_settings[$config_name_parts[2]][$config_name_parts[3]] = $this->configFactory->get($config_name)->get();
466       }
467       return $all_settings;
468     }
469   }
470
471   /**
472    * Supplements all missing link setting with default values.
473    *
474    * @param string $type
475    *  'entity'|'custom'
476    * @param array &$settings
477    * @param array $overrides
478    */
479   public static function supplementDefaultSettings($type, &$settings, $overrides = []) {
480     foreach (self::$allowedLinkSettings[$type] as $allowed_link_setting) {
481       if (!isset($settings[$allowed_link_setting])
482         && isset(self::$linkSettingDefaults[$allowed_link_setting])) {
483         $settings[$allowed_link_setting] = isset($overrides[$allowed_link_setting])
484           ? $overrides[$allowed_link_setting]
485           : self::$linkSettingDefaults[$allowed_link_setting];
486       }
487     }
488   }
489
490   /**
491    * Overrides entity bundle/entity type sitemap settings for a single entity.
492    *
493    * @param string $entity_type_id
494    * @param int $id
495    * @param array $settings
496    *
497    * @return $this
498    */
499   public function setEntityInstanceSettings($entity_type_id, $id, $settings) {
500     $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id);
501     $bundle_settings = $this->getBundleSettings(
502       $entity_type_id, $this->entityHelper->getEntityInstanceBundleName($entity)
503     );
504     if (!empty($bundle_settings)) {
505
506       // Check if overrides are different from bundle setting before saving.
507       $override = FALSE;
508       foreach ($settings as $key => $setting) {
509         if (!isset($bundle_settings[$key]) || $setting != $bundle_settings[$key]) {
510           $override = TRUE;
511           break;
512         }
513       }
514       // Save overrides for this entity if something is different.
515       if ($override) {
516         $this->db->merge('simple_sitemap_entity_overrides')
517           ->key([
518             'entity_type' => $entity_type_id,
519             'entity_id' => $id])
520           ->fields([
521             'entity_type' => $entity_type_id,
522             'entity_id' => $id,
523             'inclusion_settings' => serialize(array_merge($bundle_settings, $settings)),])
524           ->execute();
525       }
526       // Else unset override.
527       else {
528         $this->removeEntityInstanceSettings($entity_type_id, $id);
529       }
530     }
531     else {
532       //todo: log error
533     }
534     return $this;
535   }
536
537   /**
538    * Gets sitemap settings for an entity instance which overrides the sitemap
539    * settings of its bundle, or bundle settings, if they are not overridden.
540    *
541    * @param string $entity_type_id
542    * @param int $id
543    *
544    * @return array|false
545    */
546   public function getEntityInstanceSettings($entity_type_id, $id) {
547     $results = $this->db->select('simple_sitemap_entity_overrides', 'o')
548       ->fields('o', ['inclusion_settings'])
549       ->condition('o.entity_type', $entity_type_id)
550       ->condition('o.entity_id', $id)
551       ->execute()
552       ->fetchField();
553
554     if (!empty($results)) {
555       return unserialize($results);
556     }
557     else {
558       $entity = $this->entityTypeManager->getStorage($entity_type_id)
559         ->load($id);
560       return $this->getBundleSettings(
561         $entity_type_id,
562         $this->entityHelper->getEntityInstanceBundleName($entity)
563       );
564     }
565   }
566
567   /**
568    * Removes sitemap settings for an entity that overrides the sitemap settings
569    * of its bundle.
570    *
571    * @param string $entity_type_id
572    * @param string|null $entity_ids
573    *
574    * @return $this
575    */
576   public function removeEntityInstanceSettings($entity_type_id, $entity_ids = NULL) {
577     $query = $this->db->delete('simple_sitemap_entity_overrides')
578       ->condition('entity_type', $entity_type_id);
579     if (NULL !== $entity_ids) {
580       $entity_ids = !is_array($entity_ids) ? [$entity_ids] : $entity_ids;
581       $query->condition('entity_id', $entity_ids, 'IN');
582     }
583     $query->execute();
584     return $this;
585   }
586
587   /**
588    * Checks if an entity bundle (or a non-bundle entity type) is set to be
589    * indexed in the sitemap settings.
590    *
591    * @param string $entity_type_id
592    * @param string|null $bundle_name
593    *
594    * @return bool
595    */
596   public function bundleIsIndexed($entity_type_id, $bundle_name = NULL) {
597     $settings = $this->getBundleSettings($entity_type_id, $bundle_name);
598     return !empty($settings['index']);
599   }
600
601   /**
602    * Checks if an entity type is enabled in the sitemap settings.
603    *
604    * @param string $entity_type_id
605    *
606    * @return bool
607    */
608   public function entityTypeIsEnabled($entity_type_id) {
609     return in_array($entity_type_id, $this->getSetting('enabled_entity_types', []));
610   }
611
612   /**
613    * Stores a custom path along with its sitemap settings to configuration.
614    *
615    * @param string $path
616    * @param array $settings
617    *
618    * @return $this
619    *
620    * @todo Validate $settings and throw exceptions
621    */
622   public function addCustomLink($path, $settings = []) {
623     if (!$this->pathValidator->isValid($path)) {
624       // todo: log error.
625       return $this;
626     }
627     if ($path[0] !== '/') {
628       // todo: log error.
629       return $this;
630     }
631
632     $custom_links = $this->getCustomLinks(FALSE);
633     foreach ($custom_links as $key => $link) {
634       if ($link['path'] === $path) {
635         $link_key = $key;
636         break;
637       }
638     }
639     $link_key = isset($link_key) ? $link_key : count($custom_links);
640     $custom_links[$link_key] = ['path' => $path] + $settings;
641     $this->configFactory->getEditable('simple_sitemap.custom')
642       ->set('links', $custom_links)->save();
643     return $this;
644   }
645
646   /**
647    * Returns an array of custom paths and their sitemap settings.
648    *
649    * @param bool $supplement_default_settings
650    * @return array
651    */
652   public function getCustomLinks($supplement_default_settings = TRUE) {
653     $custom_links = $this->configFactory
654       ->get('simple_sitemap.custom')
655       ->get('links');
656
657     if ($supplement_default_settings) {
658       foreach ($custom_links as $i => $link_settings) {
659         self::supplementDefaultSettings('custom', $link_settings);
660         $custom_links[$i] = $link_settings;
661       }
662     }
663
664     return $custom_links !== NULL ? $custom_links : [];
665   }
666
667   /**
668    * Returns settings for a custom path added to the sitemap settings.
669    *
670    * @param string $path
671    *
672    * @return array|false
673    */
674   public function getCustomLink($path) {
675     foreach ($this->getCustomLinks() as $key => $link) {
676       if ($link['path'] === $path) {
677         return $link;
678       }
679     }
680     return FALSE;
681   }
682
683   /**
684    * Removes a custom path from the sitemap settings.
685    *
686    * @param string $path
687    *
688    * @return $this
689    */
690   public function removeCustomLink($path) {
691     $custom_links = $this->getCustomLinks(FALSE);
692     foreach ($custom_links as $key => $link) {
693       if ($link['path'] === $path) {
694         unset($custom_links[$key]);
695         $custom_links = array_values($custom_links);
696         $this->configFactory->getEditable('simple_sitemap.custom')
697           ->set('links', $custom_links)->save();
698         break;
699       }
700     }
701     return $this;
702   }
703
704   /**
705    * Removes all custom paths from the sitemap settings.
706    *
707    * @return $this
708    */
709   public function removeCustomLinks() {
710     $this->configFactory->getEditable('simple_sitemap.custom')
711       ->set('links', [])->save();
712     return $this;
713   }
714 }