Completed
Push — EZP-26146-location-swap-urlali... ( 3d141e...44a3c1 )
by
unknown
31:05
created

Handler::internalPublishUrlAliasForLocation()   D

Complexity

Conditions 13
Paths 76

Size

Total Lines 119
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 68
nc 76
nop 6
dl 0
loc 119
rs 4.9922
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * 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
 * @version //autogentag//
10
 */
11
namespace eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias;
12
13
use eZ\Publish\SPI\Persistence\Content\UrlAlias\Handler as UrlAliasHandlerInterface;
14
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
15
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway;
16
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
17
use eZ\Publish\Core\Base\Exceptions\ForbiddenException;
18
19
/**
20
 * The UrlAlias Handler provides nice urls management.
21
 *
22
 * Its methods operate on a representation of the url alias data structure held
23
 * inside a storage engine.
24
 */
25
class Handler implements UrlAliasHandlerInterface
26
{
27
    const ROOT_LOCATION_ID = 1;
28
29
    /**
30
     * This is intentionally hardcoded for now as:
31
     * 1. We don't implement this configuration option.
32
     * 2. Such option should not be in this layer, should be handled higher up.
33
     *
34
     * @deprecated
35
     */
36
    const CONTENT_REPOSITORY_ROOT_LOCATION_ID = 2;
37
38
    /**
39
     * UrlAlias Gateway.
40
     *
41
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway
42
     */
43
    protected $gateway;
44
45
    /**
46
     * Gateway for handling location data.
47
     *
48
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway
49
     */
50
    protected $locationGateway;
51
52
    /**
53
     * UrlAlias Mapper.
54
     *
55
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper
56
     */
57
    protected $mapper;
58
59
    /**
60
     * Caching language handler.
61
     *
62
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
63
     */
64
    protected $languageHandler;
65
66
    /**
67
     * URL slug converter.
68
     *
69
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter
70
     */
71
    protected $slugConverter;
72
73
    /**
74
     * Creates a new UrlAlias Handler.
75
     *
76
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $gateway
77
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper $mapper
78
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway $locationGateway
79
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
80
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter $slugConverter
81
     */
82
    public function __construct(
83
        Gateway $gateway,
84
        Mapper $mapper,
85
        LocationGateway $locationGateway,
86
        LanguageHandler $languageHandler,
87
        SlugConverter $slugConverter
88
    ) {
89
        $this->gateway = $gateway;
90
        $this->mapper = $mapper;
91
        $this->locationGateway = $locationGateway;
92
        $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...
93
        $this->slugConverter = $slugConverter;
94
    }
95
96
    public function publishUrlAliasForLocation(
97
        $locationId,
98
        $parentLocationId,
99
        $name,
100
        $languageCode,
101
        $alwaysAvailable = false,
102
        $updatePathIdentificationString = false
103
    ) {
104
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
105
106
        $this->internalPublishUrlAliasForLocation(
107
            $locationId,
108
            $parentLocationId,
109
            $name,
110
            $languageId,
111
            $alwaysAvailable,
112
            $updatePathIdentificationString
113
        );
114
    }
115
116
    /**
117
     * todo document
118
     *
119
     * @param int $locationId
120
     * @param int $parentLocationId
121
     * @param string $name
122
     * @param int $languageId
123
     * @param bool $alwaysAvailable
124
     * @param bool $updatePathIdentificationString legacy storage specific for updating ezcontentobject_tree.path_identification_string
125
     *
126
     * @return int
127
     */
128
    private function internalPublishUrlAliasForLocation(
129
        $locationId,
130
        $parentLocationId,
131
        $name,
132
        $languageId,
133
        $alwaysAvailable = false,
134
        $updatePathIdentificationString = false
135
    ) {
136
        $parentId = $this->getRealAliasId($parentLocationId);
137
        $uniqueCounter = $this->slugConverter->getUniqueCounterValue($name, $parentId == 0);
138
        $name = $this->slugConverter->convert($name, 'location_' . $locationId);
139
        $languageMask = $languageId | (int)$alwaysAvailable;
140
        $action = 'eznode:' . $locationId;
141
        $cleanup = false;
142
143
        // Exiting the loop with break;
144
        while (true) {
145
            $newText = '';
146
            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...
147
                $newText = $name . ($uniqueCounter > 1 ? $uniqueCounter : '');
148
            }
149
            $newTextMD5 = $this->getHash($newText);
150
151
            // Try to load existing entry
152
            $row = $this->gateway->loadRow($parentId, $newTextMD5);
153
154
            // If nothing was returned insert new entry
155
            if (empty($row)) {
156
                // Check for existing active location entry on this level and reuse it's id
157
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
158
                if (!empty($existingLocationEntry)) {
159
                    $cleanup = true;
160
                    $newId = $existingLocationEntry['id'];
161
                } else {
162
                    $newId = null;
163
                }
164
165
                $newId = $this->gateway->insertRow(
166
                    array(
167
                        'id' => $newId,
168
                        'link' => $newId,
169
                        'parent' => $parentId,
170
                        'action' => $action,
171
                        'lang_mask' => $languageMask,
172
                        'text' => $newText,
173
                        'text_md5' => $newTextMD5,
174
                    )
175
                );
176
177
                break;
178
            }
179
180
            // Row exists, check if it is reusable. There are 3 cases when this is possible:
181
            // 1. NOP entry
182
            // 2. existing location or custom alias entry
183
            // 3. history entry
184
            if ($row['action'] == 'nop:' || $row['action'] == $action || $row['is_original'] == 0) {
185
                // Check for existing location entry on this level, if it exists and it's id differs from reusable
186
                // entry id then reusable entry should be updated with the existing location entry id.
187
                // Note: existing location entry may be downgraded and relinked later, depending on its language.
188
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
189
                $newId = $row['id'];
190
                if (!empty($existingLocationEntry)) {
191
                    // Always cleanup when active autogenerated entry exists on the same level
192
                    $cleanup = true;
193
                    if ($existingLocationEntry['id'] != $row['id']) {
194
                        $newId = $existingLocationEntry['id'];
195
                    } else {
196
                        // If we are reusing existing location entry merge existing language mask
197
                        $languageMask |= ($row['lang_mask'] & ~1);
198
                    }
199
                }
200
                $this->gateway->updateRow(
201
                    $parentId,
202
                    $newTextMD5,
203
                    array(
204
                        'action' => $action,
205
                        // In case when NOP row was reused
206
                        'action_type' => 'eznode',
207
                        'lang_mask' => $languageMask,
208
                        // Updating text ensures that letter case changes are stored
209
                        'text' => $newText,
210
                        // Set "id" and "link" for case when reusable entry is history
211
                        'id' => $newId,
212
                        'link' => $newId,
213
                        // Entry should be active location entry (original and not alias).
214
                        // Note: this takes care of taking over custom alias entry for the location on the same level
215
                        // and with same name and action.
216
                        'alias_redirects' => 1,
217
                        'is_original' => 1,
218
                        'is_alias' => 0,
219
                    )
220
                );
221
222
                break;
223
            }
224
225
            // If existing row is not reusable, increment $uniqueCounter and try again
226
            $uniqueCounter += 1;
227
        }
228
229
        /* @var $newText */
230
        if ($updatePathIdentificationString) {
231
            $this->locationGateway->updatePathIdentificationString(
232
                $locationId,
233
                $parentLocationId,
234
                $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...
235
            );
236
        }
237
238
        /* @var $newId */
239
        /* @var $newTextMD5 */
240
        // Note: cleanup does not touch custom and global entries
241
        if ($cleanup) {
242
            $this->gateway->cleanupAfterPublish($action, $languageId, $newId, $parentId, $newTextMD5);
0 ignored issues
show
Bug introduced by
The variable $newId 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...
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...
243
        }
244
245
        return $newId;
246
    }
247
248
    /**
249
     * Create a user chosen $alias pointing to $locationId in $languageCode.
250
     *
251
     * If $languageCode is null the $alias is created in the system's default
252
     * language. $alwaysAvailable makes the alias available in all languages.
253
     *
254
     * @param mixed $locationId
255
     * @param string $path
256
     * @param bool $forwarding
257
     * @param string $languageCode
258
     * @param bool $alwaysAvailable
259
     *
260
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
261
     */
262
    public function createCustomUrlAlias($locationId, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
263
    {
264
        return $this->createUrlAlias(
265
            'eznode:' . $locationId,
266
            $path,
267
            $forwarding,
268
            $languageCode,
269
            $alwaysAvailable
270
        );
271
    }
272
273
    /**
274
     * Create a user chosen $alias pointing to a resource in $languageCode.
275
     * This method does not handle location resources - if a user enters a location target
276
     * the createCustomUrlAlias method has to be used.
277
     *
278
     * If $languageCode is null the $alias is created in the system's default
279
     * language. $alwaysAvailable makes the alias available in all languages.
280
     *
281
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException if the path already exists for the given language
282
     *
283
     * @param string $resource
284
     * @param string $path
285
     * @param bool $forwarding
286
     * @param string $languageCode
287
     * @param bool $alwaysAvailable
288
     *
289
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
290
     */
291
    public function createGlobalUrlAlias($resource, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
292
    {
293
        return $this->createUrlAlias(
294
            $resource,
295
            $path,
296
            $forwarding,
297
            $languageCode,
298
            $alwaysAvailable
299
        );
300
    }
301
302
    /**
303
     * Internal method for creating global or custom URL alias (these are handled in the same way).
304
     *
305
     * @throws \eZ\Publish\Core\Base\Exceptions\ForbiddenException if the path already exists for the given language
306
     *
307
     * @param string $action
308
     * @param string $path
309
     * @param bool $forward
310
     * @param string|null $languageCode
311
     * @param bool $alwaysAvailable
312
     *
313
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
314
     */
315
    protected function createUrlAlias($action, $path, $forward, $languageCode, $alwaysAvailable)
316
    {
317
        $pathElements = explode('/', $path);
318
        $topElement = array_pop($pathElements);
319
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
320
        $parentId = 0;
321
322
        // Handle all path elements except topmost one
323
        $isPathNew = false;
324
        foreach ($pathElements as $level => $pathElement) {
325
            $pathElement = $this->slugConverter->convert($pathElement, 'noname' . ($level + 1));
326
            $pathElementMD5 = $this->getHash($pathElement);
327
            if (!$isPathNew) {
328
                $row = $this->gateway->loadRow($parentId, $pathElementMD5);
329
                if (empty($row)) {
330
                    $isPathNew = true;
331
                } else {
332
                    $parentId = $row['link'];
333
                }
334
            }
335
336
            if ($isPathNew) {
337
                $parentId = $this->insertNopEntry($parentId, $pathElement, $pathElementMD5);
338
            }
339
        }
340
341
        // Handle topmost path element
342
        $topElement = $this->slugConverter->convert($topElement, 'noname' . (count($pathElements) + 1));
343
344
        // If last (next to topmost) entry parent is special root entry we handle topmost entry as first level entry
345
        // That is why we need to reset $parentId to 0
346
        if ($parentId != 0 && $this->gateway->isRootEntry($parentId)) {
347
            $parentId = 0;
348
        }
349
350
        $topElementMD5 = $this->getHash($topElement);
351
        // Set common values for two cases below
352
        $data = array(
353
            'action' => $action,
354
            'is_alias' => 1,
355
            'alias_redirects' => $forward ? 1 : 0,
356
            'parent' => $parentId,
357
            'text' => $topElement,
358
            'text_md5' => $topElementMD5,
359
            'is_original' => 1,
360
        );
361
        // Try to load topmost element
362
        if (!$isPathNew) {
363
            $row = $this->gateway->loadRow($parentId, $topElementMD5);
364
        }
365
366
        // If nothing was returned perform insert
367
        if ($isPathNew || empty($row)) {
368
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
369
            $id = $this->gateway->insertRow($data);
370
        } elseif ($row['action'] == 'nop:' || $row['is_original'] == 0) {
371
            // Row exists, check if it is reusable. There are 2 cases when this is possible:
372
            // 1. NOP entry
373
            // 2. history entry
374
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
375
            // If history is reused move link to id
376
            $data['link'] = $id = $row['id'];
377
            $this->gateway->updateRow(
378
                $parentId,
379
                $topElementMD5,
380
                $data
381
            );
382
        } else {
383
            throw new ForbiddenException("Path '%path%' already exists for the given language", ['%path%' => $path]);
384
        }
385
386
        $data['raw_path_data'] = $this->gateway->loadPathData($id);
387
388
        return $this->mapper->extractUrlAliasFromData($data);
389
    }
390
391
    /**
392
     * Convenience method for inserting nop type row.
393
     *
394
     * @param mixed $parentId
395
     * @param string $text
396
     * @param string $textMD5
397
     *
398
     * @return mixed
399
     */
400
    protected function insertNopEntry($parentId, $text, $textMD5)
401
    {
402
        return $this->gateway->insertRow(
403
            array(
404
                'lang_mask' => 1,
405
                'action' => 'nop:',
406
                'parent' => $parentId,
407
                'text' => $text,
408
                'text_md5' => $textMD5,
409
            )
410
        );
411
    }
412
413
    /**
414
     * List of user generated or autogenerated url entries, pointing to $locationId.
415
     *
416
     * @param mixed $locationId
417
     * @param bool $custom if true the user generated aliases are listed otherwise the autogenerated
418
     *
419
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
420
     */
421 View Code Duplication
    public function listURLAliasesForLocation($locationId, $custom = false)
422
    {
423
        $data = $this->gateway->loadLocationEntries($locationId, $custom);
424
        foreach ($data as &$entry) {
425
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
426
        }
427
428
        return $this->mapper->extractUrlAliasListFromData($data);
429
    }
430
431
    /**
432
     * List global aliases.
433
     *
434
     * @param string|null $languageCode
435
     * @param int $offset
436
     * @param int $limit
437
     *
438
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
439
     */
440 View Code Duplication
    public function listGlobalURLAliases($languageCode = null, $offset = 0, $limit = -1)
441
    {
442
        $data = $this->gateway->listGlobalEntries($languageCode, $offset, $limit);
443
        foreach ($data as &$entry) {
444
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
445
        }
446
447
        return $this->mapper->extractUrlAliasListFromData($data);
448
    }
449
450
    /**
451
     * Removes url aliases.
452
     *
453
     * Autogenerated aliases are not removed by this method.
454
     *
455
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $urlAliases
456
     *
457
     * @return bool
458
     */
459
    public function removeURLAliases(array $urlAliases)
460
    {
461
        foreach ($urlAliases as $urlAlias) {
462
            if ($urlAlias->isCustom) {
463
                list($parentId, $textMD5) = explode('-', $urlAlias->id);
464
                if (!$this->gateway->removeCustomAlias($parentId, $textMD5)) {
465
                    return false;
466
                }
467
            }
468
        }
469
470
        return true;
471
    }
472
473
    /**
474
     * Looks up a url alias for the given url.
475
     *
476
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
477
     * @throws \RuntimeException
478
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
479
     *
480
     * @param string $url
481
     *
482
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
483
     */
484
    public function lookup($url)
485
    {
486
        $urlHashes = array();
487
        foreach (explode('/', $url) as $level => $text) {
488
            $urlHashes[$level] = $this->getHash($text);
489
        }
490
491
        $data = $this->gateway->loadUrlAliasData($urlHashes);
492
        if (empty($data)) {
493
            throw new NotFoundException('URLAlias', $url);
494
        }
495
496
        $pathDepth = count($urlHashes);
497
        $hierarchyData = array();
498
        $isPathHistory = false;
499
        for ($level = 0; $level < $pathDepth; ++$level) {
500
            $prefix = $level === $pathDepth - 1 ? '' : 'ezurlalias_ml' . $level . '_';
501
            $isPathHistory = $isPathHistory ?: ($data[$prefix . 'link'] != $data[$prefix . 'id']);
502
            $hierarchyData[$level] = array(
503
                'id' => $data[$prefix . 'id'],
504
                'parent' => $data[$prefix . 'parent'],
505
                'action' => $data[$prefix . 'action'],
506
            );
507
        }
508
509
        $data['is_path_history'] = $isPathHistory;
510
        $data['raw_path_data'] = ($data['action_type'] == 'eznode' && !$data['is_alias'])
511
            ? $this->gateway->loadPathDataByHierarchy($hierarchyData)
512
            : $this->gateway->loadPathData($data['id']);
513
514
        return $this->mapper->extractUrlAliasFromData($data);
515
    }
516
517
    /**
518
     * Loads URL alias by given $id.
519
     *
520
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
521
     *
522
     * @param string $id
523
     *
524
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
525
     */
526
    public function loadUrlAlias($id)
527
    {
528
        list($parentId, $textMD5) = explode('-', $id);
529
        $data = $this->gateway->loadRow($parentId, $textMD5);
530
531
        if (empty($data)) {
532
            throw new NotFoundException('URLAlias', $id);
533
        }
534
535
        $data['raw_path_data'] = $this->gateway->loadPathData($data['id']);
536
537
        return $this->mapper->extractUrlAliasFromData($data);
538
    }
539
540
    /**
541
     * Notifies the underlying engine that a location has moved.
542
     *
543
     * This method triggers the change of the autogenerated aliases.
544
     *
545
     * @param mixed $locationId
546
     * @param mixed $oldParentId
547
     * @param mixed $newParentId
548
     */
549
    public function locationMoved($locationId, $oldParentId, $newParentId)
550
    {
551
        // @todo optimize: $newLocationAliasId is already available in self::publishUrlAliasForLocation() as $newId
552
        $newParentLocationAliasId = $this->getRealAliasId($newParentId);
553
        $newLocationAlias = $this->gateway->loadAutogeneratedEntry(
554
            'eznode:' . $locationId,
555
            $newParentLocationAliasId
556
        );
557
558
        $oldParentLocationAliasId = $this->getRealAliasId($oldParentId);
559
        $oldLocationAlias = $this->gateway->loadAutogeneratedEntry(
560
            'eznode:' . $locationId,
561
            $oldParentLocationAliasId
562
        );
563
564
        // Historize alias for old location
565
        $this->gateway->historizeId($oldLocationAlias['id'], $newLocationAlias['id']);
566
        // Reparent subtree of old location to new location
567
        $this->gateway->reparent($oldLocationAlias['id'], $newLocationAlias['id']);
568
    }
569
570
    /**
571
     * Notifies the underlying engine that a location was copied.
572
     *
573
     * This method triggers the creation of the autogenerated aliases for the copied locations
574
     *
575
     * @param mixed $locationId
576
     * @param mixed $newLocationId
577
     * @param mixed $newParentId
578
     */
579
    public function locationCopied($locationId, $newLocationId, $newParentId)
580
    {
581
        $newParentAliasId = $this->getRealAliasId($newLocationId);
582
        $oldParentAliasId = $this->getRealAliasId($locationId);
583
584
        $actionMap = $this->getCopiedLocationsMap($locationId, $newLocationId);
585
586
        $this->copySubtree(
587
            $actionMap,
588
            $oldParentAliasId,
589
            $newParentAliasId
590
        );
591
    }
592
593
    public function locationSwapped($location1Id, $location1ParentId, $location2Id, $location2ParentId)
594
    {
595
        $location1Entries = $this->gateway->loadLocationEntries($location1Id);
596
        $location2Entries = $this->gateway->loadLocationEntries($location2Id);
597
        $location1NewAliasId = null;
598
        $location2NewAliasId = null;
599
        $languages1 = [];
600
        $languages2 = [];
601
602 View Code Duplication
        foreach ($location2Entries as $row) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
603
            $alwaysAvailable = (bool)($row['lang_mask'] & 1);
604
            $languageIds = $this->extractLanguageIdsFromMask($row['lang_mask']);
605
606
            foreach ($languageIds as $languageId) {
607
                $languages1[] = $languageId;
608
                $location1NewAliasId = $this->internalPublishUrlAliasForLocation(
609
                    $location1Id,
610
                    $location1ParentId,
611
                    $row['text'],
612
                    $languageId,
613
                    $alwaysAvailable,
614
                    $alwaysAvailable
615
                );
616
            }
617
        }
618
619 View Code Duplication
        foreach ($location1Entries as $row) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
620
            $alwaysAvailable = (bool)($row['lang_mask'] & 1);
621
            $languageIds = $this->extractLanguageIdsFromMask($row['lang_mask']);
622
623
            foreach ($languageIds as $languageId) {
624
                $languages2[] = $languageId;
625
                $location2NewAliasId = $this->internalPublishUrlAliasForLocation(
626
                    $location2Id,
627
                    $location2ParentId,
628
                    $row['text'],
629
                    $languageId,
630
                    $alwaysAvailable,
631
                    $alwaysAvailable
632
                );
633
            }
634
        }
635
636
        $this->cleanupAfterSwap($location1Id, $location1NewAliasId, $languages2, $languages1);
637
        $this->cleanupAfterSwap($location2Id, $location2NewAliasId, $languages1, $languages2);
638
    }
639
640
    /**
641
     * Cleans up URL aliases after performing Location swap.
642
     *
643
     * Old URL aliases in languages not present on the swapped Content are downgraded.
644
     *
645
     * @param int $locationId Location ID for which cleanup is performed.
646
     * @param int $locationNewAliasId ID of the autogenerated alias for the Location.
647
     * @param int[] $languages1 Languages of the Content being swapped in.
648
     * @param int[] $languages2 Languages of the Content being swapped out.
649
     */
650
    private function cleanupAfterSwap(
651
        $locationId,
652
        $locationNewAliasId,
653
        array $languages1,
654
        array $languages2
655
    ) {
656
        $languagesIdsDiff = array_diff($languages1, $languages2);
657
658
        if (empty($languagesIdsDiff)) {
659
            return;
660
        }
661
662
        $languageMask = $this->generateLanguageMask($languages2);
663
664
        foreach ($languagesIdsDiff as $languageId) {
665
            $this->gateway->cleanupAfterSwap(
666
                'eznode:' . $locationId,
667
                $languageId,
668
                $locationNewAliasId,
669
                $languageMask
670
            );
671
        }
672
    }
673
674
    /**
675
     * Generates language mask from the given array of language IDs.
676
     *
677
     * @param int[] $languageIds
678
     *
679
     * @return int
680
     */
681
    private function generateLanguageMask(array $languageIds)
682
    {
683
        $mask = 0;
684
685
        foreach ($languageIds as $languageId) {
686
            $mask |= $languageId;
687
        }
688
689
        return $mask;
690
    }
691
692
    /**
693
     * Extracts every language Ids contained in $languageMask.
694
     *
695
     * @param int $languageMask
696
     *
697
     * @return int[] An array of language IDs
698
     */
699
    private function extractLanguageIdsFromMask($languageMask)
700
    {
701
        $exp = 2;
702
        $languageIds = [];
703
704
        // Decomposition of $languageMask into its binary components.
705
        while ($exp <= $languageMask) {
706
            if ($languageMask & $exp) {
707
                $languageIds[] = $exp;
708
            }
709
710
            $exp *= 2;
711
        }
712
713
        return $languageIds;
714
    }
715
716
    /**
717
     * Returns possibly corrected alias id for given $locationId !! For use as parent id in logic.
718
     *
719
     * First level entries must have parent id set to 0 instead of their parent location alias id.
720
     * There are two cases when alias id needs to be corrected:
721
     * 1) location is special location without URL alias (location with id=1 in standard installation)
722
     * 2) location is site root location, having special root entry in the ezurlalias_ml table (location with id=2
723
     *    in standard installation)
724
     *
725
     * @param mixed $locationId
726
     *
727
     * @return mixed
728
     */
729
    protected function getRealAliasId($locationId)
730
    {
731
        // Absolute root location does have a url alias entry so we can skip lookup
732
        if ($locationId == self::ROOT_LOCATION_ID) {
733
            return 0;
734
        }
735
736
        $data = $this->gateway->loadAutogeneratedEntry('eznode:' . $locationId);
737
738
        // Root entries (URL wise) can return 0 as the returned value is used as parent (parent is 0 for root entries)
739
        if (empty($data) || $data['id'] != 0 && $data['parent'] == 0 && strlen($data['text']) == 0) {
740
            $id = 0;
741
        } else {
742
            $id = $data['id'];
743
        }
744
745
        return $id;
746
    }
747
748
    /**
749
     * Recursively copies aliases from old parent under new parent.
750
     *
751
     * @param array $actionMap
752
     * @param mixed $oldParentAliasId
753
     * @param mixed $newParentAliasId
754
     */
755
    protected function copySubtree($actionMap, $oldParentAliasId, $newParentAliasId)
756
    {
757
        $rows = $this->gateway->loadAutogeneratedEntries($oldParentAliasId);
758
        $newIdsMap = array();
759
        foreach ($rows as $row) {
760
            $oldParentAliasId = $row['id'];
761
762
            // Ensure that same action entries remain grouped by the same id
763
            if (!isset($newIdsMap[$oldParentAliasId])) {
764
                $newIdsMap[$oldParentAliasId] = $this->gateway->getNextId();
765
            }
766
767
            $row['action'] = $actionMap[$row['action']];
768
            $row['parent'] = $newParentAliasId;
769
            $row['id'] = $row['link'] = $newIdsMap[$oldParentAliasId];
770
            $this->gateway->insertRow($row);
771
772
            $this->copySubtree(
773
                $actionMap,
774
                $oldParentAliasId,
775
                $row['id']
776
            );
777
        }
778
    }
779
780
    /**
781
     * @param mixed $oldParentId
782
     * @param mixed $newParentId
783
     *
784
     * @return array
785
     */
786
    protected function getCopiedLocationsMap($oldParentId, $newParentId)
787
    {
788
        $originalLocations = $this->locationGateway->getSubtreeContent($oldParentId);
789
        $copiedLocations = $this->locationGateway->getSubtreeContent($newParentId);
790
791
        $map = array();
792
        foreach ($originalLocations as $index => $originalLocation) {
793
            $map['eznode:' . $originalLocation['node_id']] = 'eznode:' . $copiedLocations[$index]['node_id'];
794
        }
795
796
        return $map;
797
    }
798
799
    /**
800
     * Notifies the underlying engine that a location was deleted or moved to trash.
801
     *
802
     * @param mixed $locationId
803
     */
804
    public function locationDeleted($locationId)
805
    {
806
        $action = 'eznode:' . $locationId;
807
        $entry = $this->gateway->loadAutogeneratedEntry($action);
808
809
        $this->removeSubtree($entry['id'], $action, $entry['is_original']);
810
    }
811
812
    /**
813
     * Recursively removes aliases by given $id and $action.
814
     *
815
     * $original parameter is used to limit removal of moved Location aliases to history entries only.
816
     *
817
     * @param mixed $id
818
     * @param string $action
819
     * @param mixed $original
820
     */
821
    protected function removeSubtree($id, $action, $original)
822
    {
823
        // Remove first to avoid unnecessary recursion.
824
        if ($original) {
825
            // If entry is original remove all for action (history and custom entries included).
826
            $this->gateway->remove($action);
827
        } else {
828
            // Else entry is history, so remove only for action with the id.
829
            // This means $id grouped history entries are removed, other history, active autogenerated
830
            // and custom are left alone.
831
            $this->gateway->remove($action, $id);
832
        }
833
834
        // Load all autogenerated for parent $id, including history.
835
        $entries = $this->gateway->loadAutogeneratedEntries($id, true);
836
837
        foreach ($entries as $entry) {
838
            $this->removeSubtree($entry['id'], $entry['action'], $entry['is_original']);
839
        }
840
    }
841
842
    /**
843
     * @param string $text
844
     *
845
     * @return string
846
     */
847
    protected function getHash($text)
848
    {
849
        return md5(mb_strtolower($text, 'UTF-8'));
850
    }
851
}
852