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

Handler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 8
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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