PathautoGenerator.php 12.2 KB
Newer Older
1
2
3
4
<?php

namespace Drupal\pathauto;

5
use Drupal\Core\Config\ConfigFactoryInterface;
kim.pepper's avatar
kim.pepper committed
6
7
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
8
use Drupal\Core\Entity\RevisionableInterface;
9
use Drupal\Core\Extension\ModuleHandlerInterface;
10
use Drupal\Core\Language\LanguageInterface;
11
use Drupal\Core\Messenger\MessengerTrait;
12
use Drupal\Core\Render\BubbleableMetadata;
13
14
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
15
use Drupal\Core\Utility\Token;
16
use Drupal\token\TokenEntityMapperInterface;
17
use Drupal\Core\Entity\EntityTypeManagerInterface;
18

kim.pepper's avatar
kim.pepper committed
19
/**
20
 * Provides methods for generating path aliases.
kim.pepper's avatar
kim.pepper committed
21
 */
22
class PathautoGenerator implements PathautoGeneratorInterface {
23

24
  use MessengerTrait;
25
26
  use StringTranslationTrait;

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  /**
   * Config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Token service.
   *
   * @var \Drupal\Core\Utility\Token
   */
  protected $token;

  /**
Berdir's avatar
Berdir committed
49
   * Calculated pattern for a specific entity.
50
51
52
   *
   * @var array
   */
53
  protected $patterns = [];
54

Berdir's avatar
Berdir committed
55
56
57
58
59
  /**
   * Available patterns per entity type ID.
   *
   * @var array
   */
60
  protected $patternsByEntityType = [];
Berdir's avatar
Berdir committed
61

kim.pepper's avatar
kim.pepper committed
62
63
64
65
66
67
68
  /**
   * The alias cleaner.
   *
   * @var \Drupal\pathauto\AliasCleanerInterface
   */
  protected $aliasCleaner;

kim.pepper's avatar
kim.pepper committed
69
70
71
  /**
   * The alias storage helper.
   *
kim.pepper's avatar
kim.pepper committed
72
   * @var \Drupal\pathauto\AliasStorageHelperInterface
kim.pepper's avatar
kim.pepper committed
73
74
75
   */
  protected $aliasStorageHelper;

76
77
78
79
80
81
82
  /**
   * The alias uniquifier.
   *
   * @var \Drupal\pathauto\AliasUniquifierInterface
   */
  protected $aliasUniquifier;

83
84
85
86
87
  /**
   * The messenger service.
   *
   * @var \Drupal\pathauto\MessengerInterface
   */
88
  protected $pathautoMessenger;
89

90
  /**
91
92
   * The token entity mapper.
   *
93
94
95
96
   * @var \Drupal\token\TokenEntityMapperInterface
   */
  protected $tokenEntityMapper;

97
  /**
98
99
100
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
101
102
103
   */
  protected $entityTypeManager;

104
105
106
107
108
109
110
  /**
   * Manages pathauto alias type plugins.
   *
   * @var \Drupal\pathauto\AliasTypeManager
   */
  protected $aliasTypeManager;

kim.pepper's avatar
kim.pepper committed
111
112
113
114
115
116
117
118
119
120
121
  /**
   * Creates a new Pathauto manager.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Utility\Token $token
   *   The token utility.
   * @param \Drupal\pathauto\AliasCleanerInterface $alias_cleaner
   *   The alias cleaner.
kim.pepper's avatar
kim.pepper committed
122
123
   * @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper
   *   The alias storage helper.
124
125
   * @param AliasUniquifierInterface $alias_uniquifier
   *   The alias uniquifier.
126
   * @param \Drupal\pathauto\MessengerInterface $pathauto_messenger
127
128
129
   *   The messenger service.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
   *   The string translation service.
130
131
132
133
   * @param \Drupal\token\TokenEntityMapperInterface $token_entity_mapper
   *   The token entity mapper.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
134
135
   * @param \Drupal\pathauto\AliasTypeManager $alias_type_manager
   *   Manages pathauto alias type plugins.
kim.pepper's avatar
kim.pepper committed
136
   */
137
  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, Token $token, AliasCleanerInterface $alias_cleaner, AliasStorageHelperInterface $alias_storage_helper, AliasUniquifierInterface $alias_uniquifier, MessengerInterface $pathauto_messenger, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mapper, EntityTypeManagerInterface $entity_type_manager, AliasTypeManager $alias_type_manager) {
138
139
140
    $this->configFactory = $config_factory;
    $this->moduleHandler = $module_handler;
    $this->token = $token;
kim.pepper's avatar
kim.pepper committed
141
    $this->aliasCleaner = $alias_cleaner;
kim.pepper's avatar
kim.pepper committed
142
    $this->aliasStorageHelper = $alias_storage_helper;
143
    $this->aliasUniquifier = $alias_uniquifier;
144
    $this->pathautoMessenger = $pathauto_messenger;
145
    $this->stringTranslation = $string_translation;
146
    $this->tokenEntityMapper = $token_entity_mapper;
147
    $this->entityTypeManager = $entity_type_manager;
148
    $this->aliasTypeManager = $alias_type_manager;
149
150
  }

151
  /**
152
   * {@inheritdoc}
153
   */
154
  public function createEntityAlias(EntityInterface $entity, $op) {
155
    // Retrieve and apply the pattern for this content type.
156
157
158
159
160
161
    $pattern = $this->getPatternByEntity($entity);
    if (empty($pattern)) {
      // No pattern? Do nothing (otherwise we may blow away existing aliases...)
      return NULL;
    }

Berdir's avatar
Berdir committed
162
    $source = '/' . $entity->toUrl()->getInternalPath();
163
164
    $config = $this->configFactory->get('pathauto.settings');
    $langcode = $entity->language()->getId();
165

166
167
168
169
170
    // Core does not handle aliases with language Not Applicable.
    if ($langcode == LanguageInterface::LANGCODE_NOT_APPLICABLE) {
      $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
    }

171
    // Build token data.
172
    $data = [
173
      $this->tokenEntityMapper->getTokenTypeForEntityType($entity->getEntityTypeId()) => $entity,
174
    ];
175
176

    // Allow other modules to alter the pattern.
177
    $context = [
178
      'module' => $entity->getEntityType()->getProvider(),
179
180
181
      'op' => $op,
      'source' => $source,
      'data' => $data,
182
      'bundle' => $entity->bundle(),
Berdir's avatar
Berdir committed
183
      'language' => &$langcode,
184
    ];
185
    $pattern_original = $pattern->getPattern();
186
    $this->moduleHandler->alter('pathauto_pattern', $pattern, $context);
187
    $pattern_altered = $pattern->getPattern();
188
189
190
191

    // Special handling when updating an item which is already aliased.
    $existing_alias = NULL;
    if ($op == 'update' || $op == 'bulkupdate') {
Berdir's avatar
Berdir committed
192
      if ($existing_alias = $this->aliasStorageHelper->loadBySource($source, $langcode)) {
193
        switch ($config->get('update_action')) {
194
          case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW:
195
196
197
            // If an alias already exists,
            // and the update action is set to do nothing,
            // then gosh-darn it, do nothing.
198
            return NULL;
199
200
201
202
203
204
        }
      }
    }

    // Replace any tokens in the pattern.
    // Uses callback option to clean replacements. No sanitization.
205
206
    // Pass empty BubbleableMetadata object to explicitly ignore cacheablity,
    // as the result is never rendered.
207
    $alias = $this->token->replace($pattern->getPattern(), $data, [
208
      'clear' => TRUE,
209
      'callback' => [$this->aliasCleaner, 'cleanTokenValues'],
Berdir's avatar
Berdir committed
210
      'langcode' => $langcode,
211
      'pathauto' => TRUE,
212
    ], new BubbleableMetadata());
213
214
215
216

    // Check if the token replacement has not actually replaced any values. If
    // that is the case, then stop because we should not generate an alias.
    // @see token_scan()
217
    $pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', $pattern->getPattern());
218
    if ($alias === $pattern_tokens_removed) {
219
      return NULL;
220
221
    }

kim.pepper's avatar
kim.pepper committed
222
    $alias = $this->aliasCleaner->cleanAlias($alias);
223
224
225
226

    // Allow other modules to alter the alias.
    $context['source'] = &$source;
    $context['pattern'] = $pattern;
227
    $this->moduleHandler->alter('pathauto_alias', $alias, $context);
228
229

    // If we have arrived at an empty string, discontinue.
230
    if (!mb_strlen($alias)) {
231
      return NULL;
232
233
234
235
    }

    // If the alias already exists, generate a new, hopefully unique, variant.
    $original_alias = $alias;
Berdir's avatar
Berdir committed
236
    $this->aliasUniquifier->uniquify($alias, $source, $langcode);
237
238
    if ($original_alias != $alias) {
      // Alert the user why this happened.
239
      $this->pathautoMessenger->addMessage($this->t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', [
240
241
        '%original_alias' => $original_alias,
        '%alias' => $alias,
242
      ]), $op);
243
244
245
246
247
248
249
250
    }

    // Return the generated alias if requested.
    if ($op == 'return') {
      return $alias;
    }

    // Build the new path alias array and send it off to be created.
251
    $path = [
252
253
      'source' => $source,
      'alias' => $alias,
Berdir's avatar
Berdir committed
254
      'language' => $langcode,
255
    ];
256

257
    $return = $this->aliasStorageHelper->save($path, $existing_alias, $op);
258
259
260
261
262
263
264
265

    // Because there is no way to set an altered pattern to not be cached,
    // change it back to the original value.
    if ($pattern_altered !== $pattern_original) {
      $pattern->setPattern($pattern_original);
    }

    return $return;
266
  }
267

Berdir's avatar
Berdir committed
268
  /**
269
   * Loads pathauto patterns for a given entity type ID.
Berdir's avatar
Berdir committed
270
271
272
273
274
275
276
277
278
   *
   * @param string $entity_type_id
   *   An entity type ID.
   *
   * @return \Drupal\pathauto\PathautoPatternInterface[]
   *   A list of patterns, sorted by weight.
   */
  protected function getPatternByEntityType($entity_type_id) {
    if (!isset($this->patternsByEntityType[$entity_type_id])) {
279
280
281
282
283
284

      $ids = $this->entityTypeManager->getStorage('pathauto_pattern')
        ->getQuery()
        ->condition('type', array_keys(
          $this->aliasTypeManager
            ->getPluginDefinitionByType($this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id))))
285
        ->condition('status', 1)
Berdir's avatar
Berdir committed
286
287
288
        ->sort('weight')
        ->execute();

289
      $this->patternsByEntityType[$entity_type_id] = $this->entityTypeManager
Berdir's avatar
Berdir committed
290
291
292
293
294
295
296
        ->getStorage('pathauto_pattern')
        ->loadMultiple($ids);
    }

    return $this->patternsByEntityType[$entity_type_id];
  }

297
  /**
298
   * {@inheritdoc}
299
   */
300
  public function getPatternByEntity(EntityInterface $entity) {
301
302
    $langcode = $entity->language()->getId();
    if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) {
Berdir's avatar
Berdir committed
303
304
      foreach ($this->getPatternByEntityType($entity->getEntityTypeId()) as $pattern) {
        if ($pattern->applies($entity)) {
305
          $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = $pattern;
Berdir's avatar
Berdir committed
306
          break;
307
308
        }
      }
309
      // If still not set.
310
311
      if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) {
        $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = NULL;
312
      }
313
    }
314
    return $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode];
315
316
317
  }

  /**
318
   * {@inheritdoc}
319
320
   */
  public function resetCaches() {
Berdir's avatar
Berdir committed
321
322
    $this->patterns = [];
    $this->patternsByEntityType = [];
323
    $this->aliasCleaner->resetCaches();
324
325
  }

kim.pepper's avatar
kim.pepper committed
326
327
328
  /**
   * {@inheritdoc}
   */
329
  public function updateEntityAlias(EntityInterface $entity, $op, array $options = []) {
kim.pepper's avatar
kim.pepper committed
330
331
332
333
334
335
    // Skip if the entity does not have the path field.
    if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) {
      return NULL;
    }

    // Skip if pathauto processing is disabled.
336
    if ($entity->path->pathauto != PathautoState::CREATE && empty($options['force'])) {
kim.pepper's avatar
kim.pepper committed
337
338
339
      return NULL;
    }

340
341
342
343
344
    // Only act if this is the default revision.
    if ($entity instanceof RevisionableInterface && !$entity->isDefaultRevision()) {
      return NULL;
    }

345
    $options += ['language' => $entity->language()->getId()];
kim.pepper's avatar
kim.pepper committed
346
347
348
    $type = $entity->getEntityTypeId();

    // Skip processing if the entity has no pattern.
349
    if (!$this->getPatternByEntity($entity)) {
kim.pepper's avatar
kim.pepper committed
350
351
352
353
      return NULL;
    }

    // Deal with taxonomy specific logic.
354
    // @todo Update and test forum related code.
kim.pepper's avatar
kim.pepper committed
355
356
357
358
359
360
361
362
    if ($type == 'taxonomy_term') {

      $config_forum = $this->configFactory->get('forum.settings');
      if ($entity->getVocabularyId() == $config_forum->get('vocabulary')) {
        $type = 'forum';
      }
    }

363
364
365
366
    try {
      $result = $this->createEntityAlias($entity, $op);
    }
    catch (\InvalidArgumentException $e) {
367
      $this->messenger()->addError($e->getMessage());
368
369
      return NULL;
    }
kim.pepper's avatar
kim.pepper committed
370

371
    // @todo Move this to a method on the pattern plugin.
372
373
    if ($type == 'taxonomy_term') {
      foreach ($this->loadTermChildren($entity->id()) as $subterm) {
374
        $this->updateEntityAlias($subterm, $op, $options);
kim.pepper's avatar
kim.pepper committed
375
376
377
378
379
380
      }
    }

    return $result;
  }

kim.pepper's avatar
kim.pepper committed
381
  /**
382
   * Finds all children of a term ID.
kim.pepper's avatar
kim.pepper committed
383
   *
384
385
   * @param int $tid
   *   Term ID to retrieve parents for.
kim.pepper's avatar
kim.pepper committed
386
   *
387
388
   * @return \Drupal\taxonomy\TermInterface[]
   *   An array of term objects that are the children of the term $tid.
kim.pepper's avatar
kim.pepper committed
389
   */
390
  protected function loadTermChildren($tid) {
391
    return $this->entityTypeManager->getStorage('taxonomy_term')->loadChildren($tid);
kim.pepper's avatar
kim.pepper committed
392
  }
393

394
}