Completed
Push — master ( ace5fb...9c2de1 )
by
unknown
20:35 queued 07:41
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
 * File containing the UrlAlias Handler.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias;
10
11
use eZ\Publish\Core\Base\Exceptions\BadStateException;
12
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
13
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator;
14
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\SwappedLocationProperties;
15
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\UrlAliasForSwappedLocation;
16
use eZ\Publish\SPI\Persistence\Content\Language;
17
use eZ\Publish\SPI\Persistence\Content\UrlAlias\Handler as UrlAliasHandlerInterface;
18
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
19
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway as ContentGateway;
20
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway;
21
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
22
use eZ\Publish\Core\Base\Exceptions\ForbiddenException;
23
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
24
use eZ\Publish\SPI\Persistence\TransactionHandler;
25
26
/**
27
 * The UrlAlias Handler provides nice urls management.
28
 *
29
 * Its methods operate on a representation of the url alias data structure held
30
 * inside a storage engine.
31
 */
32
class Handler implements UrlAliasHandlerInterface
33
{
34
    const ROOT_LOCATION_ID = 1;
35
36
    /**
37
     * This is intentionally hardcoded for now as:
38
     * 1. We don't implement this configuration option.
39
     * 2. Such option should not be in this layer, should be handled higher up.
40
     *
41
     * @deprecated
42
     */
43
    const CONTENT_REPOSITORY_ROOT_LOCATION_ID = 2;
44
45
    /**
46
     * The maximum level of alias depth.
47
     */
48
    const MAX_URL_ALIAS_DEPTH_LEVEL = 60;
49
50
    /**
51
     * UrlAlias Gateway.
52
     *
53
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway
54
     */
55
    protected $gateway;
56
57
    /**
58
     * Gateway for handling location data.
59
     *
60
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway
61
     */
62
    protected $locationGateway;
63
64
    /**
65
     * UrlAlias Mapper.
66
     *
67
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper
68
     */
69
    protected $mapper;
70
71
    /**
72
     * Caching language handler.
73
     *
74
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
75
     */
76
    protected $languageHandler;
77
78
    /**
79
     * URL slug converter.
80
     *
81
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter
82
     */
83
    protected $slugConverter;
84
85
    /**
86
     * Gateway for handling content data.
87
     *
88
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway
89
     */
90
    protected $contentGateway;
91
92
    /**
93
     * Language mask generator.
94
     *
95
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
96
     */
97
    protected $maskGenerator;
98
99
    /** @var \eZ\Publish\SPI\Persistence\TransactionHandler */
100
    private $transactionHandler;
101
102
    /**
103
     * Creates a new UrlAlias Handler.
104
     *
105
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $gateway
106
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper $mapper
107
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway $locationGateway
108
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
109
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter $slugConverter
110
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Gateway $contentGateway
111
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $maskGenerator
112
     * @param \eZ\Publish\SPI\Persistence\TransactionHandler $transactionHandler
113
     */
114
    public function __construct(
115
        Gateway $gateway,
116
        Mapper $mapper,
117
        LocationGateway $locationGateway,
118
        LanguageHandler $languageHandler,
119
        SlugConverter $slugConverter,
120
        ContentGateway $contentGateway,
121
        MaskGenerator $maskGenerator,
122
        TransactionHandler $transactionHandler
123
    ) {
124
        $this->gateway = $gateway;
125
        $this->mapper = $mapper;
126
        $this->locationGateway = $locationGateway;
127
        $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...
128
        $this->slugConverter = $slugConverter;
129
        $this->contentGateway = $contentGateway;
130
        $this->maskGenerator = $maskGenerator;
131
        $this->transactionHandler = $transactionHandler;
132
    }
133
134
    public function publishUrlAliasForLocation(
135
        $locationId,
136
        $parentLocationId,
137
        $name,
138
        $languageCode,
139
        $alwaysAvailable = false,
140
        $updatePathIdentificationString = false
141
    ) {
142
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
143
144
        $this->internalPublishUrlAliasForLocation(
145
            $locationId,
146
            $parentLocationId,
147
            $name,
148
            $languageId,
149
            $alwaysAvailable,
150
            $updatePathIdentificationString
151
        );
152
    }
153
154
    /**
155
     * Internal publish method, accepting language ID instead of language code and optionally
156
     * new alias ID (used when swapping Locations).
157
     *
158
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped()
159
     *
160
     * @param int $locationId
161
     * @param int $parentLocationId
162
     * @param string $name
163
     * @param int $languageId
164
     * @param bool $alwaysAvailable
165
     * @param bool $updatePathIdentificationString legacy storage specific for updating ezcontentobject_tree.path_identification_string
166
     * @param int $newId
167
     */
168
    private function internalPublishUrlAliasForLocation(
169
        $locationId,
170
        $parentLocationId,
171
        $name,
172
        $languageId,
173
        $alwaysAvailable = false,
174
        $updatePathIdentificationString = false,
175
        $newId = null
176
    ) {
177
        $parentId = $this->getRealAliasId($parentLocationId);
178
        $name = $this->slugConverter->convert($name, 'location_' . $locationId);
179
        $uniqueCounter = $this->slugConverter->getUniqueCounterValue($name, $parentId == 0);
180
        $languageMask = $languageId | (int)$alwaysAvailable;
181
        $action = 'eznode:' . $locationId;
182
        $cleanup = false;
183
184
        // Exiting the loop with break;
185
        while (true) {
186
            $newText = '';
187
            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...
188
                $newText = $name . ($name == '' || $uniqueCounter > 1 ? $uniqueCounter : '');
189
            }
190
            $newTextMD5 = $this->getHash($newText);
191
192
            // Try to load existing entry
193
            $row = $this->gateway->loadRow($parentId, $newTextMD5);
194
195
            // If nothing was returned insert new entry
196
            if (empty($row)) {
197
                // Check for existing active location entry on this level and reuse it's id
198
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
199
                if (!empty($existingLocationEntry)) {
200
                    $cleanup = true;
201
                    $newId = $existingLocationEntry['id'];
202
                }
203
204
                try {
205
                    $newId = $this->gateway->insertRow(
206
                        [
207
                            'id' => $newId,
208
                            'link' => $newId,
209
                            'parent' => $parentId,
210
                            'action' => $action,
211
                            'lang_mask' => $languageMask,
212
                            'text' => $newText,
213
                            'text_md5' => $newTextMD5,
214
                        ]
215
                    );
216
                } catch (\RuntimeException $e) {
217
                    while ($e->getPrevious() !== null) {
218
                        $e = $e->getPrevious();
219
                        if ($e instanceof UniqueConstraintViolationException) {
220
                            // Concurrency! someone else inserted the same row that we where going to.
221
                            // let's do another loop pass
222
                            $uniqueCounter += 1;
223
                            continue 2;
224
                        }
225
                    }
226
227
                    throw $e;
228
                }
229
230
                break;
231
            }
232
233
            // Row exists, check if it is reusable. There are 3 cases when this is possible:
234
            // 1. NOP entry
235
            // 2. existing location or custom alias entry
236
            // 3. history entry
237
            if ($row['action'] == 'nop:' || $row['action'] == $action || $row['is_original'] == 0) {
238
                // Check for existing location entry on this level, if it exists and it's id differs from reusable
239
                // entry id then reusable entry should be updated with the existing location entry id.
240
                // Note: existing location entry may be downgraded and relinked later, depending on its language.
241
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
242
243
                if (!empty($existingLocationEntry)) {
244
                    // Always cleanup when active autogenerated entry exists on the same level
245
                    $cleanup = true;
246
                    $newId = $existingLocationEntry['id'];
247
                    if ($existingLocationEntry['id'] == $row['id']) {
248
                        // If we are reusing existing location entry merge existing language mask
249
                        $languageMask |= ($row['lang_mask'] & ~1);
250
                    }
251
                } elseif ($newId === null) {
252
                    // Use reused row ID only if publishing normally, else use given $newId
253
                    $newId = $row['id'];
254
                }
255
256
                $this->gateway->updateRow(
257
                    $parentId,
258
                    $newTextMD5,
259
                    [
260
                        'action' => $action,
261
                        // In case when NOP row was reused
262
                        'action_type' => 'eznode',
263
                        'lang_mask' => $languageMask,
264
                        // Updating text ensures that letter case changes are stored
265
                        'text' => $newText,
266
                        // Set "id" and "link" for case when reusable entry is history
267
                        'id' => $newId,
268
                        'link' => $newId,
269
                        // Entry should be active location entry (original and not alias).
270
                        // Note: this takes care of taking over custom alias entry for the location on the same level
271
                        // and with same name and action.
272
                        'alias_redirects' => 1,
273
                        'is_original' => 1,
274
                        'is_alias' => 0,
275
                    ]
276
                );
277
278
                break;
279
            }
280
281
            // If existing row is not reusable, increment $uniqueCounter and try again
282
            $uniqueCounter += 1;
283
        }
284
285
        /* @var $newText */
286
        if ($updatePathIdentificationString) {
287
            $this->locationGateway->updatePathIdentificationString(
288
                $locationId,
289
                $parentLocationId,
290
                $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...
291
            );
292
        }
293
294
        /* @var $newId */
295
        /* @var $newTextMD5 */
296
        // Note: cleanup does not touch custom and global entries
297
        if ($cleanup) {
298
            $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...
299
        }
300
    }
301
302
    /**
303
     * Create a user chosen $alias pointing to $locationId in $languageCode.
304
     *
305
     * If $languageCode is null the $alias is created in the system's default
306
     * language. $alwaysAvailable makes the alias available in all languages.
307
     *
308
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
309
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
310
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
311
     *
312
     * @param mixed $locationId
313
     * @param string $path
314
     * @param bool $forwarding
315
     * @param string $languageCode
316
     * @param bool $alwaysAvailable
317
     *
318
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
319
     */
320
    public function createCustomUrlAlias($locationId, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
321
    {
322
        return $this->createUrlAlias(
323
            'eznode:' . $locationId,
324
            $path,
325
            $forwarding,
326
            $languageCode,
327
            $alwaysAvailable
328
        );
329
    }
330
331
    /**
332
     * Create a user chosen $alias pointing to a resource in $languageCode.
333
     * This method does not handle location resources - if a user enters a location target
334
     * the createCustomUrlAlias method has to be used.
335
     *
336
     * If $languageCode is null the $alias is created in the system's default
337
     * language. $alwaysAvailable makes the alias available in all languages.
338
     *
339
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException if the path already exists for the given language
340
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the path is broken
341
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
342
     *
343
     * @param string $resource
344
     * @param string $path
345
     * @param bool $forwarding
346
     * @param string $languageCode
347
     * @param bool $alwaysAvailable
348
     *
349
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
350
     */
351
    public function createGlobalUrlAlias($resource, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
352
    {
353
        return $this->createUrlAlias(
354
            $resource,
355
            $path,
356
            $forwarding,
357
            $languageCode,
358
            $alwaysAvailable
359
        );
360
    }
361
362
    /**
363
     * Internal method for creating global or custom URL alias (these are handled in the same way).
364
     *
365
     * @throws \eZ\Publish\Core\Base\Exceptions\ForbiddenException if the path already exists for the given language
366
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
367
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
368
     *
369
     * @param string $action
370
     * @param string $path
371
     * @param bool $forward
372
     * @param string|null $languageCode
373
     * @param bool $alwaysAvailable
374
     *
375
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
376
     */
377
    protected function createUrlAlias($action, $path, $forward, $languageCode, $alwaysAvailable)
378
    {
379
        $pathElements = explode('/', $path);
380
        $topElement = array_pop($pathElements);
381
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
382
        $parentId = 0;
383
384
        // Handle all path elements except topmost one
385
        $isPathNew = false;
386
        foreach ($pathElements as $level => $pathElement) {
387
            $pathElement = $this->slugConverter->convert($pathElement, 'noname' . ($level + 1));
388
            $pathElementMD5 = $this->getHash($pathElement);
389
            if (!$isPathNew) {
390
                $row = $this->gateway->loadRow($parentId, $pathElementMD5);
391
                if (empty($row)) {
392
                    $isPathNew = true;
393
                } else {
394
                    $parentId = $row['link'];
395
                }
396
            }
397
398
            if ($isPathNew) {
399
                $parentId = $this->insertNopEntry($parentId, $pathElement, $pathElementMD5);
400
            }
401
        }
402
403
        // Handle topmost path element
404
        $topElement = $this->slugConverter->convert($topElement, 'noname' . (count($pathElements) + 1));
405
406
        // If last (next to topmost) entry parent is special root entry we handle topmost entry as first level entry
407
        // That is why we need to reset $parentId to 0
408
        if ($parentId != 0 && $this->gateway->isRootEntry($parentId)) {
409
            $parentId = 0;
410
        }
411
412
        $topElementMD5 = $this->getHash($topElement);
413
        // Set common values for two cases below
414
        $data = [
415
            'action' => $action,
416
            'is_alias' => 1,
417
            'alias_redirects' => $forward ? 1 : 0,
418
            'parent' => $parentId,
419
            'text' => $topElement,
420
            'text_md5' => $topElementMD5,
421
            'is_original' => 1,
422
        ];
423
        // Try to load topmost element
424
        if (!$isPathNew) {
425
            $row = $this->gateway->loadRow($parentId, $topElementMD5);
426
        }
427
428
        // If nothing was returned perform insert
429
        if ($isPathNew || empty($row)) {
430
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
431
            $id = $this->gateway->insertRow($data);
432
        } elseif ($row['action'] == 'nop:' || $row['is_original'] == 0) {
433
            // Row exists, check if it is reusable. There are 2 cases when this is possible:
434
            // 1. NOP entry
435
            // 2. history entry
436
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
437
            // If history is reused move link to id
438
            $data['link'] = $id = $row['id'];
439
            $this->gateway->updateRow(
440
                $parentId,
441
                $topElementMD5,
442
                $data
443
            );
444
        } elseif ($row['action'] == $action && 0 === ((int)$row['lang_mask'] & $languageId)) {
445
            // add another language to the same custom alias
446
            $data['link'] = $id = $row['id'];
447
            $data['lang_mask'] = $row['lang_mask'] | $languageId | (int)$alwaysAvailable;
448
            $this->gateway->updateRow(
449
                $parentId,
450
                $topElementMD5,
451
                $data
452
            );
453
        } else {
454
            throw new ForbiddenException("Path '%path%' already exists for the given language", ['%path%' => $path]);
455
        }
456
457
        $data['raw_path_data'] = $this->gateway->loadPathData($id);
458
459
        return $this->mapper->extractUrlAliasFromData($data);
460
    }
461
462
    /**
463
     * Convenience method for inserting nop type row.
464
     *
465
     * @param mixed $parentId
466
     * @param string $text
467
     * @param string $textMD5
468
     *
469
     * @return mixed
470
     */
471
    protected function insertNopEntry($parentId, $text, $textMD5)
472
    {
473
        return $this->gateway->insertRow(
474
            [
475
                'lang_mask' => 1,
476
                'action' => 'nop:',
477
                'parent' => $parentId,
478
                'text' => $text,
479
                'text_md5' => $textMD5,
480
            ]
481
        );
482
    }
483
484
    /**
485
     * List of user generated or autogenerated url entries, pointing to $locationId.
486
     *
487
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
488
     *
489
     * @param mixed $locationId
490
     * @param bool $custom if true the user generated aliases are listed otherwise the autogenerated
491
     *
492
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
493
     */
494 View Code Duplication
    public function listURLAliasesForLocation($locationId, $custom = false)
495
    {
496
        $data = $this->gateway->loadLocationEntries($locationId, $custom);
497
        foreach ($data as &$entry) {
498
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
499
        }
500
501
        return $this->mapper->extractUrlAliasListFromData($data);
502
    }
503
504
    /**
505
     * List global aliases.
506
     *
507
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
508
     *
509
     * @param string|null $languageCode
510
     * @param int $offset
511
     * @param int $limit
512
     *
513
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
514
     */
515 View Code Duplication
    public function listGlobalURLAliases($languageCode = null, $offset = 0, $limit = -1)
516
    {
517
        $data = $this->gateway->listGlobalEntries($languageCode, $offset, $limit);
518
        foreach ($data as &$entry) {
519
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
520
        }
521
522
        return $this->mapper->extractUrlAliasListFromData($data);
523
    }
524
525
    /**
526
     * Removes url aliases.
527
     *
528
     * Autogenerated aliases are not removed by this method.
529
     *
530
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $urlAliases
531
     *
532
     * @return bool
533
     */
534
    public function removeURLAliases(array $urlAliases)
535
    {
536
        foreach ($urlAliases as $urlAlias) {
537
            if ($urlAlias->isCustom) {
538
                list($parentId, $textMD5) = explode('-', $urlAlias->id);
539
                if (!$this->gateway->removeCustomAlias($parentId, $textMD5)) {
540
                    return false;
541
                }
542
            }
543
        }
544
545
        return true;
546
    }
547
548
    /**
549
     * Looks up a url alias for the given url.
550
     *
551
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
552
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
553
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
554
     *
555
     * @param string $url
556
     *
557
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
558
     */
559
    public function lookup($url)
560
    {
561
        $urlHashes = [];
562
        foreach (explode('/', $url) as $level => $text) {
563
            $urlHashes[$level] = $this->getHash($text);
564
        }
565
566
        $pathDepth = count($urlHashes);
567
        if ($pathDepth > self::MAX_URL_ALIAS_DEPTH_LEVEL) {
568
            throw new InvalidArgumentException('$urlHashes', 'Exceeded maximum depth level for content URL alias.');
569
        }
570
571
        $data = $this->gateway->loadUrlAliasData($urlHashes);
572
        if (empty($data)) {
573
            throw new NotFoundException('URLAlias', $url);
574
        }
575
576
        $hierarchyData = [];
577
        $isPathHistory = false;
578
        for ($level = 0; $level < $pathDepth; ++$level) {
579
            $prefix = $level === $pathDepth - 1 ? '' : 'ezurlalias_ml' . $level . '_';
580
            $isPathHistory = $isPathHistory ?: ($data[$prefix . 'link'] != $data[$prefix . 'id']);
581
            $hierarchyData[$level] = [
582
                'id' => $data[$prefix . 'id'],
583
                'parent' => $data[$prefix . 'parent'],
584
                'action' => $data[$prefix . 'action'],
585
            ];
586
        }
587
588
        $data['is_path_history'] = $isPathHistory;
589
        $data['raw_path_data'] = ($data['action_type'] == 'eznode' && !$data['is_alias'])
590
            ? $this->gateway->loadPathDataByHierarchy($hierarchyData)
591
            : $this->gateway->loadPathData($data['id']);
592
593
        return $this->mapper->extractUrlAliasFromData($data);
594
    }
595
596
    /**
597
     * Loads URL alias by given $id.
598
     *
599
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
600
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
601
     *
602
     * @param string $id
603
     *
604
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
605
     */
606
    public function loadUrlAlias($id)
607
    {
608
        list($parentId, $textMD5) = explode('-', $id);
609
        $data = $this->gateway->loadRow((int)$parentId, $textMD5);
610
611
        if (empty($data)) {
612
            throw new NotFoundException('URLAlias', $id);
613
        }
614
615
        $data['raw_path_data'] = $this->gateway->loadPathData((int)$data['id']);
616
617
        return $this->mapper->extractUrlAliasFromData($data);
618
    }
619
620
    /**
621
     * Notifies the underlying engine that a location has moved.
622
     *
623
     * This method triggers the change of the autogenerated aliases.
624
     *
625
     * @param mixed $locationId
626
     * @param mixed $oldParentId
627
     * @param mixed $newParentId
628
     */
629
    public function locationMoved($locationId, $oldParentId, $newParentId)
630
    {
631
        // @todo optimize: $newLocationAliasId is already available in self::publishUrlAliasForLocation() as $newId
632
        $newParentLocationAliasId = $this->getRealAliasId($newParentId);
633
        $newLocationAlias = $this->gateway->loadAutogeneratedEntry(
634
            'eznode:' . $locationId,
635
            $newParentLocationAliasId
636
        );
637
638
        $oldParentLocationAliasId = $this->getRealAliasId($oldParentId);
639
        $oldLocationAlias = $this->gateway->loadAutogeneratedEntry(
640
            'eznode:' . $locationId,
641
            $oldParentLocationAliasId
642
        );
643
644
        // Historize alias for old location
645
        $this->gateway->historizeId($oldLocationAlias['id'], $newLocationAlias['id']);
646
        // Reparent subtree of old location to new location
647
        $this->gateway->reparent($oldLocationAlias['id'], $newLocationAlias['id']);
648
    }
649
650
    /**
651
     * Notifies the underlying engine that a location was copied.
652
     *
653
     * This method triggers the creation of the autogenerated aliases for the copied locations
654
     *
655
     * @param mixed $locationId
656
     * @param mixed $newLocationId
657
     * @param mixed $newParentId
658
     */
659
    public function locationCopied($locationId, $newLocationId, $newParentId)
660
    {
661
        $newParentAliasId = $this->getRealAliasId($newLocationId);
662
        $oldParentAliasId = $this->getRealAliasId($locationId);
663
664
        $actionMap = $this->getCopiedLocationsMap($locationId, $newLocationId);
665
666
        $this->copySubtree(
667
            $actionMap,
668
            $oldParentAliasId,
669
            $newParentAliasId
670
        );
671
    }
672
673
    /**
674
     * Notify the underlying engine that a Location has been swapped.
675
     *
676
     * This method triggers the change of the autogenerated aliases.
677
     *
678
     * @param int $location1Id
679
     * @param int $location1ParentId
680
     * @param int $location2Id
681
     * @param int $location2ParentId
682
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
683
     */
684
    public function locationSwapped($location1Id, $location1ParentId, $location2Id, $location2ParentId)
685
    {
686
        $location1 = new SwappedLocationProperties($location1Id, $location1ParentId);
687
        $location2 = new SwappedLocationProperties($location2Id, $location2ParentId);
688
689
        $location1->entries = $this->gateway->loadAllLocationEntries($location1Id);
690
        $location2->entries = $this->gateway->loadAllLocationEntries($location2Id);
691
692
        $location1->mainLanguageId = $this->gateway->getLocationContentMainLanguageId($location1Id);
693
        $location2->mainLanguageId = $this->gateway->getLocationContentMainLanguageId($location2Id);
694
695
        // Load autogenerated entries to find alias ID
696
        $location1->autogeneratedId = $this->gateway->loadAutogeneratedEntry("eznode:{$location1Id}")['id'];
697
        $location2->autogeneratedId = $this->gateway->loadAutogeneratedEntry("eznode:{$location2Id}")['id'];
698
699
        $contentInfo1 = $this->contentGateway->loadContentInfoByLocationId($location1Id);
700
        $contentInfo2 = $this->contentGateway->loadContentInfoByLocationId($location2Id);
701
702
        $names1 = $this->getNamesForAllLanguages($contentInfo1);
703
        $names2 = $this->getNamesForAllLanguages($contentInfo2);
704
705
        $location1->isAlwaysAvailable = $this->maskGenerator->isAlwaysAvailable($contentInfo1['language_mask']);
706
        $location2->isAlwaysAvailable = $this->maskGenerator->isAlwaysAvailable($contentInfo2['language_mask']);
707
708
        $languages = $this->languageHandler->loadAll();
709
710
        // Historize everything first to avoid name conflicts in case swapped Locations are siblings
711
        $this->historizeBeforeSwap($location1->entries, $location2->entries);
712
713
        foreach ($languages as $languageCode => $language) {
714
            $location1->name = isset($names1[$languageCode]) ? $names1[$languageCode] : null;
715
            $location2->name = isset($names2[$languageCode]) ? $names2[$languageCode] : null;
716
            $urlAliasesForSwappedLocations = $this->getUrlAliasesForSwappedLocations(
717
                $language,
718
                $location1,
719
                $location2
720
            );
721
            foreach ($urlAliasesForSwappedLocations as $urlAliasForLocation) {
722
                $this->internalPublishUrlAliasForLocation(
723
                    $urlAliasForLocation->id,
724
                    $urlAliasForLocation->parentId,
725
                    $urlAliasForLocation->name,
726
                    $language->id,
727
                    $urlAliasForLocation->isAlwaysAvailable,
728
                    $urlAliasForLocation->isPathIdentificationStringModified,
729
                    $urlAliasForLocation->newId
730
                );
731
            }
732
        }
733
734
        $this->internalPublishCustomUrlAliasForLocation($location1, $contentInfo1['language_mask']);
735
        $this->internalPublishCustomUrlAliasForLocation($location2, $contentInfo2['language_mask']);
736
    }
737
738
    /**
739
     * @param array $contentInfo
740
     *
741
     * @return array
742
     */
743
    private function getNamesForAllLanguages(array $contentInfo)
744
    {
745
        $nameDataArray = $this->contentGateway->loadVersionedNameData([
746
            [
747
                'id' => $contentInfo['id'],
748
                'version' => $contentInfo['current_version'],
749
            ],
750
        ]);
751
752
        $namesForAllLanguages = [];
753
        foreach ($nameDataArray as $nameData) {
754
            $namesForAllLanguages[$nameData['ezcontentobject_name_content_translation']]
755
                = $nameData['ezcontentobject_name_name'];
756
        }
757
758
        return $namesForAllLanguages;
759
    }
760
761
    /**
762
     * Historizes given existing active entries for two swapped Locations.
763
     *
764
     * This should be done before republishing URL aliases, in order to avoid unnecessary
765
     * conflicts when swapped Locations are siblings.
766
     *
767
     * We need to historize everything separately per language (mask), in case the entries
768
     * remain history future publishing reusages need to be able to take them over cleanly.
769
     *
770
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped()
771
     *
772
     * @param array $location1Entries
773
     * @param array $location2Entries
774
     */
775
    private function historizeBeforeSwap($location1Entries, $location2Entries)
776
    {
777
        foreach ($location1Entries as $row) {
778
            $this->gateway->historizeBeforeSwap($row['action'], $row['lang_mask']);
779
        }
780
781
        foreach ($location2Entries as $row) {
782
            $this->gateway->historizeBeforeSwap($row['action'], $row['lang_mask']);
783
        }
784
    }
785
786
    /**
787
     * Decides if UrlAlias for $location2 should be published first.
788
     *
789
     * The order in which Locations are published only matters if swapped Locations are siblings and they have the same
790
     * name in a given language. In this case, the UrlAlias for Location which previously had lower number at the end of
791
     * its UrlAlias text (or no number at all) should be published first. This ensures that the number still stays lower
792
     * for this Location after the swap. If it wouldn't stay lower, then swapping Locations in conjunction with swapping
793
     * UrlAliases would effectively cancel each other.
794
     *
795
     * @param array $location1Entries
796
     * @param int $location1ParentId
797
     * @param string $name1
798
     * @param array $location2Entries
799
     * @param int $location2ParentId
800
     * @param string $name2
801
     * @param int $languageId
802
     *
803
     * @return bool
804
     */
805
    private function shouldUrlAliasForSecondLocationBePublishedFirst(
806
        array $location1Entries,
807
        $location1ParentId,
808
        $name1,
809
        array $location2Entries,
810
        $location2ParentId,
811
        $name2,
812
        $languageId
813
    ) {
814
        if ($location1ParentId === $location2ParentId && $name1 === $name2) {
815
            $locationEntry1 = $this->getLocationEntryInLanguage($location1Entries, $languageId);
816
            $locationEntry2 = $this->getLocationEntryInLanguage($location2Entries, $languageId);
817
818
            if ($locationEntry1 === null || $locationEntry2 === null) {
819
                return false;
820
            }
821
822
            if ($locationEntry2['text'] < $locationEntry1['text']) {
823
                return true;
824
            }
825
        }
826
827
        return false;
828
    }
829
830
    /**
831
     * Get in a proper order - to be published - a list of URL aliases for swapped Locations.
832
     *
833
     * @see shouldUrlAliasForSecondLocationBePublishedFirst
834
     *
835
     * @param \eZ\Publish\SPI\Persistence\Content\Language $language
836
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\SwappedLocationProperties $location1
837
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\SwappedLocationProperties $location2
838
     *
839
     * @return \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\UrlAliasForSwappedLocation[]
840
     */
841
    private function getUrlAliasesForSwappedLocations(
842
        Language $language,
843
        SwappedLocationProperties $location1,
844
        SwappedLocationProperties $location2
845
    ) {
846
        $isMainLanguage1 = $language->id == $location1->mainLanguageId;
847
        $isMainLanguage2 = $language->id == $location2->mainLanguageId;
848
        $urlAliases = [];
849
        if (isset($location1->name)) {
850
            $urlAliases[] = new UrlAliasForSwappedLocation(
851
                $location1->id,
852
                $location1->parentId,
853
                $location1->name,
854
                $isMainLanguage2 && $location1->isAlwaysAvailable,
855
                $isMainLanguage2,
856
                $location1->autogeneratedId
857
            );
858
        }
859
860
        if (isset($location2->name)) {
861
            $urlAliases[] = new UrlAliasForSwappedLocation(
862
                $location2->id,
863
                $location2->parentId,
864
                $location2->name,
865
                $isMainLanguage1 && $location2->isAlwaysAvailable,
866
                $isMainLanguage1,
867
                $location2->autogeneratedId
868
            );
869
870
            if (isset($location1->name) && $this->shouldUrlAliasForSecondLocationBePublishedFirst(
871
                    $location1->entries,
872
                    $location1->parentId,
873
                    $location1->name,
874
                    $location2->entries,
875
                    $location2->parentId,
876
                    $location2->name,
877
                    $language->id
878
                )) {
879
                $urlAliases = array_reverse($urlAliases);
880
            }
881
        }
882
883
        return $urlAliases;
884
    }
885
886
    /**
887
     * @param array $locationEntries
888
     * @param int $languageId
889
     *
890
     * @return array|null
891
     */
892
    private function getLocationEntryInLanguage(array $locationEntries, $languageId)
893
    {
894
        $entries = array_filter(
895
            $locationEntries,
896
            function (array $row) use ($languageId) {
897
                return (bool) ($row['lang_mask'] & $languageId);
898
            }
899
        );
900
901
        return !empty($entries) ? array_shift($entries) : null;
902
    }
903
904
    /**
905
     * Returns possibly corrected alias id for given $locationId !! For use as parent id in logic.
906
     *
907
     * First level entries must have parent id set to 0 instead of their parent location alias id.
908
     * There are two cases when alias id needs to be corrected:
909
     * 1) location is special location without URL alias (location with id=1 in standard installation)
910
     * 2) location is site root location, having special root entry in the ezurlalias_ml table (location with id=2
911
     *    in standard installation)
912
     *
913
     * @param mixed $locationId
914
     *
915
     * @return mixed
916
     */
917
    protected function getRealAliasId($locationId)
918
    {
919
        // Absolute root location does have a url alias entry so we can skip lookup
920
        if ($locationId == self::ROOT_LOCATION_ID) {
921
            return 0;
922
        }
923
924
        $data = $this->gateway->loadAutogeneratedEntry('eznode:' . $locationId);
925
926
        // Root entries (URL wise) can return 0 as the returned value is used as parent (parent is 0 for root entries)
927
        if (empty($data) || ($data['id'] != 0 && $data['parent'] == 0 && strlen($data['text']) == 0)) {
928
            $id = 0;
929
        } else {
930
            $id = $data['id'];
931
        }
932
933
        return $id;
934
    }
935
936
    /**
937
     * Recursively copies aliases from old parent under new parent.
938
     *
939
     * @param array $actionMap
940
     * @param mixed $oldParentAliasId
941
     * @param mixed $newParentAliasId
942
     */
943
    protected function copySubtree($actionMap, $oldParentAliasId, $newParentAliasId)
944
    {
945
        $rows = $this->gateway->loadAutogeneratedEntries($oldParentAliasId);
946
        $newIdsMap = [];
947
        foreach ($rows as $row) {
948
            $oldParentAliasId = $row['id'];
949
950
            // Ensure that same action entries remain grouped by the same id
951
            if (!isset($newIdsMap[$oldParentAliasId])) {
952
                $newIdsMap[$oldParentAliasId] = $this->gateway->getNextId();
953
            }
954
955
            $row['action'] = $actionMap[$row['action']];
956
            $row['parent'] = $newParentAliasId;
957
            $row['id'] = $row['link'] = $newIdsMap[$oldParentAliasId];
958
            $this->gateway->insertRow($row);
959
960
            $this->copySubtree(
961
                $actionMap,
962
                $oldParentAliasId,
963
                $row['id']
964
            );
965
        }
966
    }
967
968
    /**
969
     * @param mixed $oldParentId
970
     * @param mixed $newParentId
971
     *
972
     * @return array
973
     */
974
    protected function getCopiedLocationsMap($oldParentId, $newParentId)
975
    {
976
        $originalLocations = $this->locationGateway->getSubtreeContent($oldParentId);
977
        $copiedLocations = $this->locationGateway->getSubtreeContent($newParentId);
978
979
        $map = [];
980
        foreach ($originalLocations as $index => $originalLocation) {
981
            $map['eznode:' . $originalLocation['node_id']] = 'eznode:' . $copiedLocations[$index]['node_id'];
982
        }
983
984
        return $map;
985
    }
986
987
    public function locationDeleted($locationId): array
988
    {
989
        $action = 'eznode:' . $locationId;
990
        $entry = $this->gateway->loadAutogeneratedEntry($action);
991
        $entryId = $entry['id'];
992
993
        $this->removeSubtree($entryId, $action, $entry['is_original']);
994
995
        // after location-delete process completed
996
        // check if entry had children; if yes - then restore them as nop-type
997
        // for historical aliases relates to that entry
998
        $notDeletedChildrenAliases = $this->gateway->getAllChildrenAliases($entryId);
999
        if (count($notDeletedChildrenAliases) > 0) {
1000
            $this->insertAliasEntryAsNop($entry);
1001
        }
1002
1003
        return $notDeletedChildrenAliases;
1004
    }
1005
1006
    /**
1007
     * Notifies the underlying engine that Locations Content Translation was removed.
1008
     *
1009
     * @param int[] $locationIds all Locations of the Content that got Translation removed
1010
     * @param string $languageCode language code of the removed Translation
1011
     */
1012
    public function translationRemoved(array $locationIds, $languageCode)
1013
    {
1014
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
1015
1016
        $actions = [];
1017
        foreach ($locationIds as $locationId) {
1018
            $actions[] = 'eznode:' . $locationId;
1019
        }
1020
        $this->gateway->bulkRemoveTranslation($languageId, $actions);
1021
    }
1022
1023
    /**
1024
     * Recursively removes aliases by given $id and $action.
1025
     *
1026
     * $original parameter is used to limit removal of moved Location aliases to history entries only.
1027
     *
1028
     * @param mixed $id
1029
     * @param string $action
1030
     * @param mixed $original
1031
     */
1032
    protected function removeSubtree($id, $action, $original)
1033
    {
1034
        // Remove first to avoid unnecessary recursion.
1035
        if ($original) {
1036
            // If entry is original remove all for action (history and custom entries included).
1037
            $this->gateway->remove($action);
1038
        } else {
1039
            // Else entry is history, so remove only for action with the id.
1040
            // This means $id grouped history entries are removed, other history, active autogenerated
1041
            // and custom are left alone.
1042
            $this->gateway->remove($action, $id);
1043
        }
1044
1045
        // Load all autogenerated for parent $id, including history.
1046
        $entries = $this->gateway->loadAutogeneratedEntries($id, true);
1047
1048
        foreach ($entries as $entry) {
1049
            $this->removeSubtree($entry['id'], $entry['action'], $entry['is_original']);
1050
        }
1051
    }
1052
1053
    /**
1054
     * @param string $text
1055
     *
1056
     * @return string
1057
     */
1058
    protected function getHash($text)
1059
    {
1060
        return md5(mb_strtolower($text, 'UTF-8'));
1061
    }
1062
1063
    /**
1064
     * {@inheritdoc}
1065
     */
1066
    public function archiveUrlAliasesForDeletedTranslations($locationId, $parentLocationId, array $languageCodes)
1067
    {
1068
        $parentId = $this->getRealAliasId($parentLocationId);
1069
1070
        $data = $this->gateway->loadLocationEntries($locationId);
1071
        // filter removed Translations
1072
        $removedLanguages = array_diff(
1073
            $this->mapper->extractLanguageCodesFromData($data),
1074
            $languageCodes
1075
        );
1076
1077
        if (empty($removedLanguages)) {
1078
            return;
1079
        }
1080
1081
        // map languageCodes to their IDs
1082
        $languageIds = array_map(
1083
            function ($languageCode) {
1084
                return $this->languageHandler->loadByLanguageCode($languageCode)->id;
1085
            },
1086
            $removedLanguages
1087
        );
1088
1089
        $this->gateway->archiveUrlAliasesForDeletedTranslations($locationId, $parentId, $languageIds);
1090
    }
1091
1092
    /**
1093
     * Remove corrupted URL aliases (global, custom and system).
1094
     *
1095
     * @return int Number of removed URL aliases
1096
     *
1097
     * @throws \Exception
1098
     */
1099
    public function deleteCorruptedUrlAliases()
1100
    {
1101
        $this->transactionHandler->beginTransaction();
1102
        try {
1103
            $totalCount = $this->gateway->deleteUrlAliasesWithoutLocation();
1104
            $totalCount += $this->gateway->deleteUrlAliasesWithoutParent();
1105
            $totalCount += $this->gateway->deleteUrlAliasesWithBrokenLink();
1106
            $totalCount += $this->gateway->deleteUrlNopAliasesWithoutChildren();
1107
1108
            $this->transactionHandler->commit();
1109
1110
            return $totalCount;
1111
        } catch (\Exception $e) {
1112
            $this->transactionHandler->rollback();
1113
            throw $e;
1114
        }
1115
    }
1116
1117
    /**
1118
     * Attempt repairing auto-generated URL aliases for the given Location (including history).
1119
     *
1120
     * Note: it is assumed that at this point original, working, URL Alias for Location is published.
1121
     *
1122
     * @param int $locationId
1123
     *
1124
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
1125
     */
1126
    public function repairBrokenUrlAliasesForLocation(int $locationId)
1127
    {
1128
        try {
1129
            $this->gateway->repairBrokenUrlAliasesForLocation($locationId);
1130
        } catch (\RuntimeException $e) {
1131
            throw new BadStateException('locationId', $e->getMessage(), $e);
1132
        }
1133
    }
1134
1135
    private function insertAliasEntryAsNop(array $aliasEntry): void
1136
    {
1137
        $aliasEntry['action'] = 'nop:';
1138
        $aliasEntry['action_type'] = 'nop';
1139
1140
        $this->gateway->insertRow($aliasEntry);
1141
    }
1142
1143
    /**
1144
     * Internal publish custom aliases method, accepting language mask to set correct language mask on url aliases
1145
     * new alias ID (used when swapping Locations).
1146
     */
1147
    private function internalPublishCustomUrlAliasForLocation(SwappedLocationProperties $location, int $languageMask)
1148
    {
1149
        foreach ($location->entries as $entry) {
1150
            if ((int)$entry['is_alias'] === 0) {
1151
                continue;
1152
            }
1153
1154
            $mask = (int)$entry['lang_mask'] & $languageMask;
1155
1156
            if ($mask <= 1) {
1157
                continue;
1158
            }
1159
1160
            $this->gateway->updateRow(
1161
                (int)$entry['parent'],
1162
                $entry['text_md5'],
1163
                [
1164
                    'id' => (int)$entry['id'],
1165
                    'is_original' => 1,
1166
                    'is_alias' => 1,
1167
                    'lang_mask' => $mask,
1168
                ]
1169
            );
1170
        }
1171
    }
1172
}
1173