Completed
Push — EZP-31287 ( fe8c5c...af9523 )
by
unknown
34:01
created

Handler::createUrlAlias()   F

Complexity

Conditions 16
Paths 224

Size

Total Lines 94

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 224
nop 5
dl 0
loc 94
rs 3.6775
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias;
8
9
use eZ\Publish\Core\Base\Exceptions\BadStateException;
10
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
11
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator;
12
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\SwappedLocationProperties;
13
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\UrlAliasForSwappedLocation;
14
use eZ\Publish\SPI\Persistence\Content\Language;
15
use eZ\Publish\SPI\Persistence\Content\UrlAlias;
16
use eZ\Publish\SPI\Persistence\Content\UrlAlias\Handler as UrlAliasHandlerInterface;
17
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
18
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway as ContentGateway;
19
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway;
20
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
21
use eZ\Publish\Core\Base\Exceptions\ForbiddenException;
22
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
23
use eZ\Publish\SPI\Persistence\TransactionHandler;
24
25
/**
26
 * The UrlAlias Handler provides nice urls management.
27
 *
28
 * Its methods operate on a representation of the url alias data structure held
29
 * inside a storage engine.
30
 */
31
class Handler implements UrlAliasHandlerInterface
32
{
33
    const ROOT_LOCATION_ID = 1;
34
35
    /**
36
     * This is intentionally hardcoded for now as:
37
     * 1. We don't implement this configuration option.
38
     * 2. Such option should not be in this layer, should be handled higher up.
39
     *
40
     * @deprecated
41
     */
42
    const CONTENT_REPOSITORY_ROOT_LOCATION_ID = 2;
43
44
    /**
45
     * The maximum level of alias depth.
46
     */
47
    const MAX_URL_ALIAS_DEPTH_LEVEL = 60;
48
49
    /**
50
     * UrlAlias Gateway.
51
     *
52
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway
53
     */
54
    protected $gateway;
55
56
    /**
57
     * Gateway for handling location data.
58
     *
59
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway
60
     */
61
    protected $locationGateway;
62
63
    /**
64
     * UrlAlias Mapper.
65
     *
66
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper
67
     */
68
    protected $mapper;
69
70
    /**
71
     * Caching language handler.
72
     *
73
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
74
     */
75
    protected $languageHandler;
76
77
    /**
78
     * URL slug converter.
79
     *
80
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter
81
     */
82
    protected $slugConverter;
83
84
    /**
85
     * Gateway for handling content data.
86
     *
87
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway
88
     */
89
    protected $contentGateway;
90
91
    /**
92
     * Language mask generator.
93
     *
94
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
95
     */
96
    protected $maskGenerator;
97
98
    /** @var \eZ\Publish\SPI\Persistence\TransactionHandler */
99
    private $transactionHandler;
100
101
    /**
102
     * Creates a new UrlAlias Handler.
103
     *
104
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $gateway
105
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper $mapper
106
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway $locationGateway
107
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
108
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter $slugConverter
109
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Gateway $contentGateway
110
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $maskGenerator
111
     * @param \eZ\Publish\SPI\Persistence\TransactionHandler $transactionHandler
112
     */
113
    public function __construct(
114
        Gateway $gateway,
115
        Mapper $mapper,
116
        LocationGateway $locationGateway,
117
        LanguageHandler $languageHandler,
118
        SlugConverter $slugConverter,
119
        ContentGateway $contentGateway,
120
        MaskGenerator $maskGenerator,
121
        TransactionHandler $transactionHandler
122
    ) {
123
        $this->gateway = $gateway;
124
        $this->mapper = $mapper;
125
        $this->locationGateway = $locationGateway;
126
        $this->languageHandler = $languageHandler;
0 ignored issues
show
Documentation Bug introduced by
$languageHandler is of type object<eZ\Publish\SPI\Pe...ntent\Language\Handler>, but the property $languageHandler was declared to be of type object<eZ\Publish\Core\P...anguage\CachingHandler>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
127
        $this->slugConverter = $slugConverter;
128
        $this->contentGateway = $contentGateway;
129
        $this->maskGenerator = $maskGenerator;
130
        $this->transactionHandler = $transactionHandler;
131
    }
132
133
    public function publishUrlAliasForLocation(
134
        $locationId,
135
        $parentLocationId,
136
        $name,
137
        $languageCode,
138
        $alwaysAvailable = false,
139
        $updatePathIdentificationString = false
140
    ) {
141
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
142
143
        $this->internalPublishUrlAliasForLocation(
144
            $locationId,
145
            $parentLocationId,
146
            $name,
147
            $languageId,
148
            $alwaysAvailable,
149
            $updatePathIdentificationString
150
        );
151
    }
152
153
    /**
154
     * Internal publish method, accepting language ID instead of language code and optionally
155
     * new alias ID (used when swapping Locations).
156
     *
157
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped()
158
     *
159
     * @param int $locationId
160
     * @param int $parentLocationId
161
     * @param string $name
162
     * @param int $languageId
163
     * @param bool $alwaysAvailable
164
     * @param bool $updatePathIdentificationString legacy storage specific for updating ezcontentobject_tree.path_identification_string
165
     * @param int $newId
166
     */
167
    private function internalPublishUrlAliasForLocation(
168
        $locationId,
169
        $parentLocationId,
170
        $name,
171
        $languageId,
172
        $alwaysAvailable = false,
173
        $updatePathIdentificationString = false,
174
        $newId = null
175
    ) {
176
        $parentId = $this->getRealAliasId($parentLocationId);
177
        $name = $this->slugConverter->convert($name, 'location_' . $locationId);
178
        $uniqueCounter = $this->slugConverter->getUniqueCounterValue($name, $parentId == 0);
179
        $languageMask = $languageId | (int)$alwaysAvailable;
180
        $action = 'eznode:' . $locationId;
181
        $cleanup = false;
182
183
        // Exiting the loop with break;
184
        while (true) {
185
            $newText = '';
186
            if ($locationId != self::CONTENT_REPOSITORY_ROOT_LOCATION_ID) {
0 ignored issues
show
Deprecated Code introduced by
The constant eZ\Publish\Core\Persiste...SITORY_ROOT_LOCATION_ID has been deprecated.

This class constant has been deprecated.

Loading history...
187
                $newText = $name . ($name == '' || $uniqueCounter > 1 ? $uniqueCounter : '');
188
            }
189
            $newTextMD5 = $this->getHash($newText);
190
191
            // Try to load existing entry
192
            $row = $this->gateway->loadRow($parentId, $newTextMD5);
193
194
            // If nothing was returned insert new entry
195
            if (empty($row)) {
196
                // Check for existing active location entry on this level and reuse it's id
197
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
198
                if (!empty($existingLocationEntry)) {
199
                    $cleanup = true;
200
                    $newId = $existingLocationEntry['id'];
201
                }
202
203
                try {
204
                    $newId = $this->gateway->insertRow(
205
                        [
206
                            'id' => $newId,
207
                            'link' => $newId,
208
                            'parent' => $parentId,
209
                            'action' => $action,
210
                            'lang_mask' => $languageMask,
211
                            'text' => $newText,
212
                            'text_md5' => $newTextMD5,
213
                        ]
214
                    );
215
                } catch (\RuntimeException $e) {
216
                    while ($e->getPrevious() !== null) {
217
                        $e = $e->getPrevious();
218
                        if ($e instanceof UniqueConstraintViolationException) {
219
                            // Concurrency! someone else inserted the same row that we where going to.
220
                            // let's do another loop pass
221
                            ++$uniqueCounter;
222
                            continue 2;
223
                        }
224
                    }
225
226
                    throw $e;
227
                }
228
229
                break;
230
            }
231
232
            // Row exists, check if it is reusable. There are 3 cases when this is possible:
233
            // 1. NOP entry
234
            // 2. existing location or custom alias entry
235
            // 3. history entry
236
            if (
237
                $row['action'] === Gateway::NOP_ACTION ||
238
                $row['action'] === $action ||
239
                (int)$row['is_original'] === 0
240
            ) {
241
                // Check for existing location entry on this level, if it exists and it's id differs from reusable
242
                // entry id then reusable entry should be updated with the existing location entry id.
243
                // Note: existing location entry may be downgraded and relinked later, depending on its language.
244
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
245
246
                if (!empty($existingLocationEntry)) {
247
                    // Always cleanup when active autogenerated entry exists on the same level
248
                    $cleanup = true;
249
                    $newId = $existingLocationEntry['id'];
250
                    if ($existingLocationEntry['id'] == $row['id']) {
251
                        // If we are reusing existing location entry merge existing language mask
252
                        $languageMask |= ($row['lang_mask'] & ~1);
253
                    }
254
                } elseif ($newId === null) {
255
                    // Use reused row ID only if publishing normally, else use given $newId
256
                    $newId = $row['id'];
257
                }
258
259
                $this->gateway->updateRow(
260
                    $parentId,
261
                    $newTextMD5,
262
                    [
263
                        'action' => $action,
264
                        // In case when NOP row was reused
265
                        'action_type' => 'eznode',
266
                        'lang_mask' => $languageMask,
267
                        // Updating text ensures that letter case changes are stored
268
                        'text' => $newText,
269
                        // Set "id" and "link" for case when reusable entry is history
270
                        'id' => $newId,
271
                        'link' => $newId,
272
                        // Entry should be active location entry (original and not alias).
273
                        // Note: this takes care of taking over custom alias entry for the location on the same level
274
                        // and with same name and action.
275
                        'alias_redirects' => 1,
276
                        'is_original' => 1,
277
                        'is_alias' => 0,
278
                    ]
279
                );
280
281
                break;
282
            }
283
284
            // If existing row is not reusable, increment $uniqueCounter and try again
285
            ++$uniqueCounter;
286
        }
287
288
        /* @var $newText */
289
        if ($updatePathIdentificationString) {
290
            $this->locationGateway->updatePathIdentificationString(
291
                $locationId,
292
                $parentLocationId,
293
                $this->slugConverter->convert($newText, 'node_' . $locationId, 'urlalias_compat')
0 ignored issues
show
Bug introduced by
The variable $newText does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
294
            );
295
        }
296
297
        /* @var $newId */
298
        /* @var $newTextMD5 */
299
        // Note: cleanup does not touch custom and global entries
300
        if ($cleanup) {
301
            $this->gateway->cleanupAfterPublish($action, $languageId, $newId, $parentId, $newTextMD5);
0 ignored issues
show
Bug introduced by
The variable $newTextMD5 does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
302
        }
303
    }
304
305
    /**
306
     * Create a user chosen $alias pointing to $locationId in $languageCode.
307
     *
308
     * If $languageCode is null the $alias is created in the system's default
309
     * language. $alwaysAvailable makes the alias available in all languages.
310
     *
311
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
312
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
313
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
314
     *
315
     * @param mixed $locationId
316
     * @param string $path
317
     * @param bool $forwarding
318
     * @param string $languageCode
319
     * @param bool $alwaysAvailable
320
     *
321
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
322
     */
323
    public function createCustomUrlAlias($locationId, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
324
    {
325
        return $this->createUrlAlias(
326
            'eznode:' . $locationId,
327
            $path,
328
            $forwarding,
329
            $languageCode,
330
            $alwaysAvailable
331
        );
332
    }
333
334
    /**
335
     * Create a user chosen $alias pointing to a resource in $languageCode.
336
     * This method does not handle location resources - if a user enters a location target
337
     * the createCustomUrlAlias method has to be used.
338
     *
339
     * If $languageCode is null the $alias is created in the system's default
340
     * language. $alwaysAvailable makes the alias available in all languages.
341
     *
342
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException if the path already exists for the given resource
343
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the path is broken
344
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
345
     *
346
     * @param string $resource
347
     * @param string $path
348
     * @param bool $forwarding
349
     * @param string $languageCode
350
     * @param bool $alwaysAvailable
351
     *
352
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
353
     */
354
    public function createGlobalUrlAlias($resource, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
355
    {
356
        return $this->createUrlAlias(
357
            $resource,
358
            $path,
359
            $forwarding,
360
            $languageCode,
361
            $alwaysAvailable
362
        );
363
    }
364
365
    /**
366
     * Internal method for creating global or custom URL alias (these are handled in the same way).
367
     *
368
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException if the path already exists for the given context
369
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
370
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
371
     *
372
     * @param string $action
373
     * @param string $path
374
     * @param bool $forward
375
     * @param string|null $languageCode
376
     * @param bool $alwaysAvailable
377
     *
378
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
379
     */
380
    protected function createUrlAlias($action, $path, $forward, $languageCode, $alwaysAvailable): UrlAlias
381
    {
382
        $pathElements = explode('/', $path);
383
        $topElement = array_pop($pathElements);
384
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
385
        $parentId = 0;
386
387
        // Handle all path elements except topmost one
388
        $isPathNew = false;
389
        foreach ($pathElements as $level => $pathElement) {
390
            $pathElement = $this->slugConverter->convert($pathElement, 'noname' . ($level + 1));
391
            $pathElementMD5 = $this->getHash($pathElement);
392
            if (!$isPathNew) {
393
                $row = $this->gateway->loadRow($parentId, $pathElementMD5);
394
                if (empty($row)) {
395
                    $isPathNew = true;
396
                } else {
397
                    $parentId = $row['link'];
398
                }
399
            }
400
401
            if ($isPathNew) {
402
                $parentId = $this->insertNopEntry($parentId, $pathElement, $pathElementMD5);
403
            }
404
        }
405
406
        // Handle topmost path element
407
        $topElement = $this->slugConverter->convert(
408
            $topElement,
409
            'noname' . (count($pathElements) + 1)
410
        );
411
412
        // If last (next to topmost) entry parent is special root entry we handle topmost entry as first level entry
413
        // That is why we need to reset $parentId to 0
414
        if ($parentId !== 0 && $this->gateway->isRootEntry($parentId)) {
415
            $parentId = 0;
416
        }
417
418
        $topElementMD5 = $this->getHash($topElement);
419
        // Set common values for two cases below
420
        $data = [
421
            'action' => $action,
422
            'is_alias' => 1,
423
            'alias_redirects' => $forward ? 1 : 0,
424
            'parent' => $parentId,
425
            'text' => $topElement,
426
            'text_md5' => $topElementMD5,
427
            'is_original' => 1,
428
        ];
429
        // Try to load topmost element
430
        if (!$isPathNew) {
431
            $row = $this->gateway->loadRow($parentId, $topElementMD5);
432
        }
433
434
        // If nothing was returned perform insert
435
        if ($isPathNew || empty($row)) {
436
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
437
            $id = $this->gateway->insertRow($data);
438
        } elseif ($row['action'] === Gateway::NOP_ACTION || (int)$row['is_original'] === 0) {
439
            // Row exists, check if it is reusable. There are 2 cases when this is possible:
440
            // 1. NOP entry
441
            // 2. history entry
442
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
443
            // If history is reused move link to id
444
            $data['link'] = $id = $row['id'];
445
            $this->gateway->updateRow(
446
                $parentId,
447
                $topElementMD5,
448
                $data
449
            );
450
        } elseif (
451
            $row['action'] === $action &&
452
            (int)$row['is_alias'] === 1 &&
453
            0 === ((int)$row['lang_mask'] & $languageId)
454
        ) {
455
            // add another language to the same custom alias
456
            $data['link'] = $id = $row['id'];
457
            $data['lang_mask'] = $row['lang_mask'] | $languageId | (int)$alwaysAvailable;
458
            $this->gateway->updateRow(
459
                $parentId,
460
                $topElementMD5,
461
                $data
462
            );
463
        } else {
464
            throw new ForbiddenException(
465
                "Path '%path%' already exists for the given context",
466
                ['%path%' => $path]
467
            );
468
        }
469
470
        $data['raw_path_data'] = $this->gateway->loadPathData($id);
471
472
        return $this->mapper->extractUrlAliasFromData($data);
473
    }
474
475
    /**
476
     * Convenience method for inserting nop type row.
477
     *
478
     * @param mixed $parentId
479
     * @param string $text
480
     * @param string $textMD5
481
     *
482
     * @return mixed
483
     */
484
    protected function insertNopEntry($parentId, $text, $textMD5)
485
    {
486
        return $this->gateway->insertRow(
487
            [
488
                'lang_mask' => 1,
489
                'action' => Gateway::NOP_ACTION,
490
                'parent' => $parentId,
491
                'text' => $text,
492
                'text_md5' => $textMD5,
493
            ]
494
        );
495
    }
496
497
    /**
498
     * List of user generated or autogenerated url entries, pointing to $locationId.
499
     *
500
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
501
     *
502
     * @param mixed $locationId
503
     * @param bool $custom if true the user generated aliases are listed otherwise the autogenerated
504
     *
505
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
506
     */
507 View Code Duplication
    public function listURLAliasesForLocation($locationId, $custom = false)
508
    {
509
        $data = $this->gateway->loadLocationEntries($locationId, $custom);
510
        foreach ($data as &$entry) {
511
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
512
        }
513
514
        return $this->mapper->extractUrlAliasListFromData($data);
515
    }
516
517
    /**
518
     * List global aliases.
519
     *
520
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
521
     *
522
     * @param string|null $languageCode
523
     * @param int $offset
524
     * @param int $limit
525
     *
526
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
527
     */
528 View Code Duplication
    public function listGlobalURLAliases($languageCode = null, $offset = 0, $limit = -1)
529
    {
530
        $data = $this->gateway->listGlobalEntries($languageCode, $offset, $limit);
531
        foreach ($data as &$entry) {
532
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
533
        }
534
535
        return $this->mapper->extractUrlAliasListFromData($data);
536
    }
537
538
    /**
539
     * Removes url aliases.
540
     *
541
     * Autogenerated aliases are not removed by this method.
542
     *
543
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $urlAliases
544
     *
545
     * @return bool
546
     */
547
    public function removeURLAliases(array $urlAliases)
548
    {
549
        foreach ($urlAliases as $urlAlias) {
550
            if ($urlAlias->isCustom) {
551
                list($parentId, $textMD5) = explode('-', $urlAlias->id);
552
                if (!$this->gateway->removeCustomAlias($parentId, $textMD5)) {
553
                    return false;
554
                }
555
            }
556
        }
557
558
        return true;
559
    }
560
561
    /**
562
     * Looks up a url alias for the given url.
563
     *
564
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
565
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
566
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
567
     *
568
     * @param string $url
569
     *
570
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
571
     */
572
    public function lookup($url)
573
    {
574
        $urlHashes = [];
575
        foreach (explode('/', $url) as $level => $text) {
576
            $urlHashes[$level] = $this->getHash($text);
577
        }
578
579
        $pathDepth = count($urlHashes);
580
        if ($pathDepth > self::MAX_URL_ALIAS_DEPTH_LEVEL) {
581
            throw new InvalidArgumentException('$urlHashes', 'Exceeded maximum depth level of content url alias.');
582
        }
583
584
        $data = $this->gateway->loadUrlAliasData($urlHashes);
585
        if (empty($data)) {
586
            throw new NotFoundException('URLAlias', $url);
587
        }
588
589
        $hierarchyData = [];
590
        $isPathHistory = false;
591
        for ($level = 0; $level < $pathDepth; ++$level) {
592
            $prefix = $level === $pathDepth - 1 ? '' : 'ezurlalias_ml' . $level . '_';
593
            $isPathHistory = $isPathHistory ?: ($data[$prefix . 'link'] != $data[$prefix . 'id']);
594
            $hierarchyData[$level] = [
595
                'id' => $data[$prefix . 'id'],
596
                'parent' => $data[$prefix . 'parent'],
597
                'action' => $data[$prefix . 'action'],
598
            ];
599
        }
600
601
        $data['is_path_history'] = $isPathHistory;
602
        $data['raw_path_data'] = ($data['action_type'] == 'eznode' && !$data['is_alias'])
603
            ? $this->gateway->loadPathDataByHierarchy($hierarchyData)
604
            : $this->gateway->loadPathData($data['id']);
605
606
        return $this->mapper->extractUrlAliasFromData($data);
607
    }
608
609
    /**
610
     * Loads URL alias by given $id.
611
     *
612
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
613
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
614
     *
615
     * @param string $id
616
     *
617
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
618
     */
619
    public function loadUrlAlias($id)
620
    {
621
        list($parentId, $textMD5) = explode('-', $id);
622
        $data = $this->gateway->loadRow($parentId, $textMD5);
623
624
        if (empty($data)) {
625
            throw new NotFoundException('URLAlias', $id);
626
        }
627
628
        $data['raw_path_data'] = $this->gateway->loadPathData($data['id']);
629
630
        return $this->mapper->extractUrlAliasFromData($data);
631
    }
632
633
    /**
634
     * Notifies the underlying engine that a location has moved.
635
     *
636
     * This method triggers the change of the autogenerated aliases.
637
     *
638
     * @param mixed $locationId
639
     * @param mixed $oldParentId
640
     * @param mixed $newParentId
641
     */
642
    public function locationMoved($locationId, $oldParentId, $newParentId)
643
    {
644
        if ($oldParentId === $newParentId) {
645
            return $newParentId;
646
        }
647
648
        // @todo optimize: $newLocationAliasId is already available in self::publishUrlAliasForLocation() as $newId
649
        $newParentLocationAliasId = $this->getRealAliasId($newParentId);
650
        $newLocationAlias = $this->gateway->loadAutogeneratedEntry(
651
            'eznode:' . $locationId,
652
            $newParentLocationAliasId
653
        );
654
655
        $oldParentLocationAliasId = $this->getRealAliasId($oldParentId);
656
        $oldLocationAlias = $this->gateway->loadAutogeneratedEntry(
657
            'eznode:' . $locationId,
658
            $oldParentLocationAliasId
659
        );
660
661
        // Historize alias for old location
662
        $this->gateway->historizeId($oldLocationAlias['id'], $newLocationAlias['id']);
663
        // Reparent subtree of old location to new location
664
        $this->gateway->reparent($oldLocationAlias['id'], $newLocationAlias['id']);
665
    }
666
667
    /**
668
     * Notifies the underlying engine that a location was copied.
669
     *
670
     * This method triggers the creation of the autogenerated aliases for the copied locations
671
     *
672
     * @param mixed $locationId
673
     * @param mixed $newLocationId
674
     * @param mixed $newParentId
675
     */
676
    public function locationCopied($locationId, $newLocationId, $newParentId)
677
    {
678
        $newParentAliasId = $this->getRealAliasId($newLocationId);
679
        $oldParentAliasId = $this->getRealAliasId($locationId);
680
681
        $actionMap = $this->getCopiedLocationsMap($locationId, $newLocationId);
682
683
        $this->copySubtree(
684
            $actionMap,
685
            $oldParentAliasId,
686
            $newParentAliasId
687
        );
688
    }
689
690
    /**
691
     * Notify the underlying engine that a Location has been swapped.
692
     *
693
     * This method triggers the change of the autogenerated aliases.
694
     *
695
     * @param int $location1Id
696
     * @param int $location1ParentId
697
     * @param int $location2Id
698
     * @param int $location2ParentId
699
     *
700
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
701
     */
702
    public function locationSwapped($location1Id, $location1ParentId, $location2Id, $location2ParentId)
703
    {
704
        $location1 = new SwappedLocationProperties($location1Id, $location1ParentId);
705
        $location2 = new SwappedLocationProperties($location2Id, $location2ParentId);
706
707
        $location1->entries = $this->gateway->loadAllLocationEntries($location1Id);
708
        $location2->entries = $this->gateway->loadAllLocationEntries($location2Id);
709
710
        $location1->mainLanguageId = $this->gateway->getLocationContentMainLanguageId($location1Id);
711
        $location2->mainLanguageId = $this->gateway->getLocationContentMainLanguageId($location2Id);
712
713
        // Load autogenerated entries to find alias ID
714
        $location1->autogeneratedId = $this->gateway->loadAutogeneratedEntry("eznode:{$location1Id}")['id'];
715
        $location2->autogeneratedId = $this->gateway->loadAutogeneratedEntry("eznode:{$location2Id}")['id'];
716
717
        $contentInfo1 = $this->contentGateway->loadContentInfoByLocationId($location1Id);
718
        $contentInfo2 = $this->contentGateway->loadContentInfoByLocationId($location2Id);
719
720
        $names1 = $this->getNamesForAllLanguages($contentInfo1);
721
        $names2 = $this->getNamesForAllLanguages($contentInfo2);
722
723
        $location1->isAlwaysAvailable = $this->maskGenerator->isAlwaysAvailable($contentInfo1['language_mask']);
724
        $location2->isAlwaysAvailable = $this->maskGenerator->isAlwaysAvailable($contentInfo2['language_mask']);
725
726
        $languages = $this->languageHandler->loadAll();
727
728
        // Historize everything first to avoid name conflicts in case swapped Locations are siblings
729
        $this->historizeBeforeSwap($location1->entries, $location2->entries);
730
731
        foreach ($languages as $languageCode => $language) {
732
            $location1->name = isset($names1[$languageCode]) ? $names1[$languageCode] : null;
733
            $location2->name = isset($names2[$languageCode]) ? $names2[$languageCode] : null;
734
            $urlAliasesForSwappedLocations = $this->getUrlAliasesForSwappedLocations(
735
                $language,
736
                $location1,
737
                $location2
738
            );
739
            foreach ($urlAliasesForSwappedLocations as $urlAliasForLocation) {
740
                $this->internalPublishUrlAliasForLocation(
741
                    $urlAliasForLocation->id,
742
                    $urlAliasForLocation->parentId,
743
                    $urlAliasForLocation->name,
744
                    $language->id,
745
                    $urlAliasForLocation->isAlwaysAvailable,
746
                    $urlAliasForLocation->isPathIdentificationStringModified,
747
                    $urlAliasForLocation->newId
748
                );
749
            }
750
        }
751
752
        $this->internalPublishCustomUrlAliasForLocation($location1, $contentInfo1['language_mask']);
753
        $this->internalPublishCustomUrlAliasForLocation($location2, $contentInfo2['language_mask']);
754
    }
755
756
    /**
757
     * @param array $contentInfo
758
     *
759
     * @return array
760
     */
761
    private function getNamesForAllLanguages(array $contentInfo)
762
    {
763
        $nameDataArray = $this->contentGateway->loadVersionedNameData([
764
            [
765
                'id' => $contentInfo['id'],
766
                'version' => $contentInfo['current_version'],
767
            ],
768
        ]);
769
770
        $namesForAllLanguages = [];
771
        foreach ($nameDataArray as $nameData) {
772
            $namesForAllLanguages[$nameData['ezcontentobject_name_content_translation']]
773
                = $nameData['ezcontentobject_name_name'];
774
        }
775
776
        return $namesForAllLanguages;
777
    }
778
779
    /**
780
     * Historizes given existing active entries for two swapped Locations.
781
     *
782
     * This should be done before republishing URL aliases, in order to avoid unnecessary
783
     * conflicts when swapped Locations are siblings.
784
     *
785
     * We need to historize everything separately per language (mask), in case the entries
786
     * remain history future publishing reusages need to be able to take them over cleanly.
787
     *
788
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped()
789
     *
790
     * @param array $location1Entries
791
     * @param array $location2Entries
792
     */
793
    private function historizeBeforeSwap($location1Entries, $location2Entries)
794
    {
795
        foreach ($location1Entries as $row) {
796
            $this->gateway->historizeBeforeSwap($row['action'], $row['lang_mask']);
797
        }
798
799
        foreach ($location2Entries as $row) {
800
            $this->gateway->historizeBeforeSwap($row['action'], $row['lang_mask']);
801
        }
802
    }
803
804
    /**
805
     * Decides if UrlAlias for $location2 should be published first.
806
     *
807
     * The order in which Locations are published only matters if swapped Locations are siblings and they have the same
808
     * name in a given language. In this case, the UrlAlias for Location which previously had lower number at the end of
809
     * its UrlAlias text (or no number at all) should be published first. This ensures that the number still stays lower
810
     * for this Location after the swap. If it wouldn't stay lower, then swapping Locations in conjunction with swapping
811
     * UrlAliases would effectively cancel each other.
812
     *
813
     * @param array $location1Entries
814
     * @param int $location1ParentId
815
     * @param string $name1
816
     * @param array $location2Entries
817
     * @param int $location2ParentId
818
     * @param string $name2
819
     * @param int $languageId
820
     *
821
     * @return bool
822
     */
823
    private function shouldUrlAliasForSecondLocationBePublishedFirst(
824
        array $location1Entries,
825
        $location1ParentId,
826
        $name1,
827
        array $location2Entries,
828
        $location2ParentId,
829
        $name2,
830
        $languageId
831
    ) {
832
        if ($location1ParentId === $location2ParentId && $name1 === $name2) {
833
            $locationEntry1 = $this->getLocationEntryInLanguage($location1Entries, $languageId);
834
            $locationEntry2 = $this->getLocationEntryInLanguage($location2Entries, $languageId);
835
836
            if ($locationEntry1 === null || $locationEntry2 === null) {
837
                return false;
838
            }
839
840
            if ($locationEntry2['text'] < $locationEntry1['text']) {
841
                return true;
842
            }
843
        }
844
845
        return false;
846
    }
847
848
    /**
849
     * Get in a proper order - to be published - a list of URL aliases for swapped Locations.
850
     *
851
     * @see shouldUrlAliasForSecondLocationBePublishedFirst
852
     *
853
     * @param \eZ\Publish\SPI\Persistence\Content\Language $language
854
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\SwappedLocationProperties $location1
855
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\SwappedLocationProperties $location2
856
     *
857
     * @return \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\UrlAliasForSwappedLocation[]
858
     */
859
    private function getUrlAliasesForSwappedLocations(
860
        Language $language,
861
        SwappedLocationProperties $location1,
862
        SwappedLocationProperties $location2
863
    ) {
864
        $isMainLanguage1 = $language->id == $location1->mainLanguageId;
865
        $isMainLanguage2 = $language->id == $location2->mainLanguageId;
866
        $urlAliases = [];
867
        if (isset($location1->name)) {
868
            $urlAliases[] = new UrlAliasForSwappedLocation(
869
                $location1->id,
870
                $location1->parentId,
871
                $location1->name,
872
                $isMainLanguage2 && $location1->isAlwaysAvailable,
873
                $isMainLanguage2,
874
                $location1->autogeneratedId
875
            );
876
        }
877
878
        if (isset($location2->name)) {
879
            $urlAliases[] = new UrlAliasForSwappedLocation(
880
                $location2->id,
881
                $location2->parentId,
882
                $location2->name,
883
                $isMainLanguage1 && $location2->isAlwaysAvailable,
884
                $isMainLanguage1,
885
                $location2->autogeneratedId
886
            );
887
888
            if (isset($location1->name) && $this->shouldUrlAliasForSecondLocationBePublishedFirst(
889
                    $location1->entries,
890
                    $location1->parentId,
891
                    $location1->name,
892
                    $location2->entries,
893
                    $location2->parentId,
894
                    $location2->name,
895
                    $language->id
896
                )) {
897
                $urlAliases = array_reverse($urlAliases);
898
            }
899
        }
900
901
        return $urlAliases;
902
    }
903
904
    /**
905
     * @param array $locationEntries
906
     * @param int $languageId
907
     *
908
     * @return array|null
909
     */
910
    private function getLocationEntryInLanguage(array $locationEntries, $languageId)
911
    {
912
        $entries = array_filter(
913
            $locationEntries,
914
            function (array $row) use ($languageId) {
915
                return (bool) ($row['lang_mask'] & $languageId);
916
            }
917
        );
918
919
        return !empty($entries) ? array_shift($entries) : null;
920
    }
921
922
    /**
923
     * Returns possibly corrected alias id for given $locationId !! For use as parent id in logic.
924
     *
925
     * First level entries must have parent id set to 0 instead of their parent location alias id.
926
     * There are two cases when alias id needs to be corrected:
927
     * 1) location is special location without URL alias (location with id=1 in standard installation)
928
     * 2) location is site root location, having special root entry in the ezurlalias_ml table (location with id=2
929
     *    in standard installation)
930
     *
931
     * @param mixed $locationId
932
     *
933
     * @return mixed
934
     */
935
    protected function getRealAliasId($locationId)
936
    {
937
        // Absolute root location does have a url alias entry so we can skip lookup
938
        if ($locationId == self::ROOT_LOCATION_ID) {
939
            return 0;
940
        }
941
942
        $data = $this->gateway->loadAutogeneratedEntry('eznode:' . $locationId);
943
944
        // Root entries (URL wise) can return 0 as the returned value is used as parent (parent is 0 for root entries)
945
        if (empty($data) || ($data['id'] != 0 && $data['parent'] == 0 && strlen($data['text']) == 0)) {
946
            $id = 0;
947
        } else {
948
            $id = $data['id'];
949
        }
950
951
        return $id;
952
    }
953
954
    /**
955
     * Recursively copies aliases from old parent under new parent.
956
     *
957
     * @param array $actionMap
958
     * @param mixed $oldParentAliasId
959
     * @param mixed $newParentAliasId
960
     */
961
    protected function copySubtree($actionMap, $oldParentAliasId, $newParentAliasId)
962
    {
963
        $rows = $this->gateway->loadAutogeneratedEntries($oldParentAliasId);
964
        $newIdsMap = [];
965
        foreach ($rows as $row) {
966
            $oldParentAliasId = $row['id'];
967
968
            // Ensure that same action entries remain grouped by the same id
969
            if (!isset($newIdsMap[$oldParentAliasId])) {
970
                $newIdsMap[$oldParentAliasId] = $this->gateway->getNextId();
971
            }
972
973
            $row['action'] = $actionMap[$row['action']];
974
            $row['parent'] = $newParentAliasId;
975
            $row['id'] = $row['link'] = $newIdsMap[$oldParentAliasId];
976
            $this->gateway->insertRow($row);
977
978
            $this->copySubtree(
979
                $actionMap,
980
                $oldParentAliasId,
981
                $row['id']
982
            );
983
        }
984
    }
985
986
    /**
987
     * @param mixed $oldParentId
988
     * @param mixed $newParentId
989
     *
990
     * @return array
991
     */
992
    protected function getCopiedLocationsMap($oldParentId, $newParentId)
993
    {
994
        $originalLocations = $this->locationGateway->getSubtreeContent($oldParentId);
995
        $copiedLocations = $this->locationGateway->getSubtreeContent($newParentId);
996
997
        $map = [];
998
        foreach ($originalLocations as $index => $originalLocation) {
999
            $map['eznode:' . $originalLocation['node_id']] = 'eznode:' . $copiedLocations[$index]['node_id'];
1000
        }
1001
1002
        return $map;
1003
    }
1004
1005
    public function locationDeleted($locationId): array
1006
    {
1007
        $action = 'eznode:' . $locationId;
1008
        $entry = $this->gateway->loadAutogeneratedEntry($action);
1009
        $entryId = $entry['id'];
1010
1011
        $this->removeSubtree($entryId, $action, $entry['is_original']);
1012
1013
        // after location-delete process completed
1014
        // check if entry had children; if yes - then restore them as nop-type
1015
        // for historical aliases relates to that entry
1016
        $notDeletedChildrenAliases = $this->gateway->getAllChildrenAliases($entryId);
1017
        if (count($notDeletedChildrenAliases) > 0) {
1018
            $this->insertAliasEntryAsNop($entry);
1019
        }
1020
1021
        return $notDeletedChildrenAliases;
1022
    }
1023
1024
    /**
1025
     * Notifies the underlying engine that Locations Content Translation was removed.
1026
     *
1027
     * @param int[] $locationIds all Locations of the Content that got Translation removed
1028
     * @param string $languageCode language code of the removed Translation
1029
     */
1030
    public function translationRemoved(array $locationIds, $languageCode)
1031
    {
1032
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
1033
1034
        $actions = [];
1035
        foreach ($locationIds as $locationId) {
1036
            $actions[] = 'eznode:' . $locationId;
1037
        }
1038
        $this->gateway->bulkRemoveTranslation($languageId, $actions);
1039
    }
1040
1041
    /**
1042
     * Recursively removes aliases by given $id and $action.
1043
     *
1044
     * $original parameter is used to limit removal of moved Location aliases to history entries only.
1045
     *
1046
     * @param mixed $id
1047
     * @param string $action
1048
     * @param mixed $original
1049
     */
1050
    protected function removeSubtree($id, $action, $original)
1051
    {
1052
        // Remove first to avoid unnecessary recursion.
1053
        if ($original) {
1054
            // If entry is original remove all for action (history and custom entries included).
1055
            $this->gateway->remove($action);
1056
        } else {
1057
            // Else entry is history, so remove only for action with the id.
1058
            // This means $id grouped history entries are removed, other history, active autogenerated
1059
            // and custom are left alone.
1060
            $this->gateway->remove($action, $id);
1061
        }
1062
1063
        // Load all autogenerated for parent $id, including history.
1064
        $entries = $this->gateway->loadAutogeneratedEntries($id, true);
1065
1066
        foreach ($entries as $entry) {
1067
            $this->removeSubtree($entry['id'], $entry['action'], $entry['is_original']);
1068
        }
1069
    }
1070
1071
    /**
1072
     * @param string $text
1073
     *
1074
     * @return string
1075
     */
1076
    protected function getHash($text)
1077
    {
1078
        return md5(mb_strtolower($text, 'UTF-8'));
1079
    }
1080
1081
    /**
1082
     * {@inheritdoc}
1083
     */
1084
    public function archiveUrlAliasesForDeletedTranslations($locationId, $parentLocationId, array $languageCodes)
1085
    {
1086
        $parentId = $this->getRealAliasId($parentLocationId);
1087
1088
        $data = $this->gateway->loadLocationEntries($locationId);
1089
        // filter removed Translations
1090
        $removedLanguages = array_diff(
1091
            $this->mapper->extractLanguageCodesFromData($data),
1092
            $languageCodes
1093
        );
1094
1095
        if (empty($removedLanguages)) {
1096
            return;
1097
        }
1098
1099
        // map languageCodes to their IDs
1100
        $languageIds = array_map(
1101
            function ($languageCode) {
1102
                return $this->languageHandler->loadByLanguageCode($languageCode)->id;
1103
            },
1104
            $removedLanguages
1105
        );
1106
1107
        $this->gateway->archiveUrlAliasesForDeletedTranslations($locationId, $parentId, $languageIds);
1108
    }
1109
1110
    /**
1111
     * Remove corrupted URL aliases (global, custom and system).
1112
     *
1113
     * @return int Number of removed URL aliases
1114
     *
1115
     * @throws \Exception
1116
     */
1117
    public function deleteCorruptedUrlAliases()
1118
    {
1119
        $this->transactionHandler->beginTransaction();
1120
        try {
1121
            $totalCount = $this->gateway->deleteUrlAliasesWithoutLocation();
1122
            $totalCount += $this->gateway->deleteUrlAliasesWithoutParent();
1123
            $totalCount += $this->gateway->deleteUrlAliasesWithBrokenLink();
1124
            $totalCount += $this->gateway->deleteUrlNopAliasesWithoutChildren();
1125
1126
            $this->transactionHandler->commit();
1127
1128
            return $totalCount;
1129
        } catch (\Exception $e) {
1130
            $this->transactionHandler->rollback();
1131
            throw $e;
1132
        }
1133
    }
1134
1135
    /**
1136
     * Attempt repairing auto-generated URL aliases for the given Location (including history).
1137
     *
1138
     * Note: it is assumed that at this point original, working, URL Alias for Location is published.
1139
     *
1140
     * @param int $locationId
1141
     *
1142
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
1143
     */
1144
    public function repairBrokenUrlAliasesForLocation(int $locationId)
1145
    {
1146
        try {
1147
            $this->gateway->repairBrokenUrlAliasesForLocation($locationId);
1148
        } catch (\RuntimeException $e) {
1149
            throw new BadStateException('locationId', $e->getMessage(), $e);
1150
        }
1151
    }
1152
1153
    private function insertAliasEntryAsNop(array $aliasEntry): void
1154
    {
1155
        $aliasEntry['action'] = Gateway::NOP_ACTION;
1156
        $aliasEntry['action_type'] = Gateway::NOP;
1157
1158
        $this->gateway->insertRow($aliasEntry);
1159
    }
1160
1161
    /**
1162
     * Internal publish custom aliases method, accepting language mask to set correct language mask on url aliases
1163
     * new alias ID (used when swapping Locations).
1164
     */
1165
    private function internalPublishCustomUrlAliasForLocation(SwappedLocationProperties $location, int $languageMask)
1166
    {
1167
        foreach ($location->entries as $entry) {
1168
            if ((int)$entry['is_alias'] === 0) {
1169
                continue;
1170
            }
1171
1172
            $mask = (int)$entry['lang_mask'] & $languageMask;
1173
1174
            if ($mask <= 1) {
1175
                continue;
1176
            }
1177
1178
            $this->gateway->updateRow(
1179
                (int)$entry['parent'],
1180
                $entry['text_md5'],
1181
                [
1182
                    'id' => (int)$entry['id'],
1183
                    'is_original' => 1,
1184
                    'is_alias' => 1,
1185
                    'lang_mask' => $mask,
1186
                ]
1187
            );
1188
        }
1189
    }
1190
}
1191