Completed
Push — ezp25946-migrate_files_to_othe... ( 7be322...b25fe1 )
by
unknown
116:15 queued 101:21
created

Handler   D

Complexity

Total Complexity 84

Size/Duplication

Total Lines 821
Duplicated Lines 6.33 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 52
loc 821
rs 4.4444
c 0
b 0
f 0
wmc 84
lcom 1
cbo 9

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A publishUrlAliasForLocation() 0 19 1
D internalPublishUrlAliasForLocation() 0 133 17
A createCustomUrlAlias() 0 10 1
A createGlobalUrlAlias() 0 10 1
D createUrlAlias() 0 75 13
A insertNopEntry() 0 12 1
A listURLAliasesForLocation() 9 9 2
A listGlobalURLAliases() 9 9 2
A removeURLAliases() 0 13 4
C lookup() 0 32 8
A loadUrlAlias() 0 13 2
A locationMoved() 0 20 1
A locationCopied() 0 13 1
C locationSwapped() 34 51 7
A historizeBeforeSwap() 0 10 3
A extractLanguageIdsFromMask() 0 16 3
B getRealAliasId() 0 18 6
B copySubtree() 0 24 3
A getCopiedLocationsMap() 0 12 2
A locationDeleted() 0 7 1
A removeSubtree() 0 20 3
A getHash() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Handler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Handler, and based on these observations, apply Extract Interface, too.

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\SPI\Persistence\Content\UrlAlias\Handler as UrlAliasHandlerInterface;
12
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
13
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway;
14
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
15
use eZ\Publish\Core\Base\Exceptions\ForbiddenException;
16
17
/**
18
 * The UrlAlias Handler provides nice urls management.
19
 *
20
 * Its methods operate on a representation of the url alias data structure held
21
 * inside a storage engine.
22
 */
23
class Handler implements UrlAliasHandlerInterface
24
{
25
    const ROOT_LOCATION_ID = 1;
26
27
    /**
28
     * This is intentionally hardcoded for now as:
29
     * 1. We don't implement this configuration option.
30
     * 2. Such option should not be in this layer, should be handled higher up.
31
     *
32
     * @deprecated
33
     */
34
    const CONTENT_REPOSITORY_ROOT_LOCATION_ID = 2;
35
36
    /**
37
     * UrlAlias Gateway.
38
     *
39
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway
40
     */
41
    protected $gateway;
42
43
    /**
44
     * Gateway for handling location data.
45
     *
46
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway
47
     */
48
    protected $locationGateway;
49
50
    /**
51
     * UrlAlias Mapper.
52
     *
53
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper
54
     */
55
    protected $mapper;
56
57
    /**
58
     * Caching language handler.
59
     *
60
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
61
     */
62
    protected $languageHandler;
63
64
    /**
65
     * URL slug converter.
66
     *
67
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter
68
     */
69
    protected $slugConverter;
70
71
    /**
72
     * Creates a new UrlAlias Handler.
73
     *
74
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $gateway
75
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper $mapper
76
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway $locationGateway
77
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
78
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter $slugConverter
79
     */
80
    public function __construct(
81
        Gateway $gateway,
82
        Mapper $mapper,
83
        LocationGateway $locationGateway,
84
        LanguageHandler $languageHandler,
85
        SlugConverter $slugConverter
86
    ) {
87
        $this->gateway = $gateway;
88
        $this->mapper = $mapper;
89
        $this->locationGateway = $locationGateway;
90
        $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...
91
        $this->slugConverter = $slugConverter;
92
    }
93
94
    public function publishUrlAliasForLocation(
95
        $locationId,
96
        $parentLocationId,
97
        $name,
98
        $languageCode,
99
        $alwaysAvailable = false,
100
        $updatePathIdentificationString = false
101
    ) {
102
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
103
104
        $this->internalPublishUrlAliasForLocation(
105
            $locationId,
106
            $parentLocationId,
107
            $name,
108
            $languageId,
109
            $alwaysAvailable,
110
            $updatePathIdentificationString
111
        );
112
    }
113
114
    /**
115
     * Internal publish method, accepting language ID instead of language code and optionally
116
     * new alias ID (used when swapping Locations).
117
     *
118
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped()
119
     *
120
     * @param int $locationId
121
     * @param int $parentLocationId
122
     * @param string $name
123
     * @param int $languageId
124
     * @param bool $alwaysAvailable
125
     * @param bool $updatePathIdentificationString legacy storage specific for updating ezcontentobject_tree.path_identification_string
126
     * @param int $newId
127
     */
128
    private function internalPublishUrlAliasForLocation(
129
        $locationId,
130
        $parentLocationId,
131
        $name,
132
        $languageId,
133
        $alwaysAvailable = false,
134
        $updatePathIdentificationString = false,
135
        $newId = null
136
    ) {
137
        $parentId = $this->getRealAliasId($parentLocationId);
138
        $name = $this->slugConverter->convert($name, 'location_' . $locationId);
139
        $uniqueCounter = $this->slugConverter->getUniqueCounterValue($name, $parentId == 0);
140
        $languageMask = $languageId | (int)$alwaysAvailable;
141
        $action = 'eznode:' . $locationId;
142
        $cleanup = false;
143
144
        // Exiting the loop with break;
145
        while (true) {
146
            $newText = '';
147
            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...
148
                $newText = $name . ($uniqueCounter > 1 ? $uniqueCounter : '');
149
            }
150
            $newTextMD5 = $this->getHash($newText);
151
152
            // Try to load existing entry
153
            $row = $this->gateway->loadRow($parentId, $newTextMD5);
154
155
            // If nothing was returned insert new entry
156
            if (empty($row)) {
157
                // Check for existing active location entry on this level and reuse it's id
158
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
159
                if (!empty($existingLocationEntry)) {
160
                    $cleanup = true;
161
                    $newId = $existingLocationEntry['id'];
162
                }
163
164
                try {
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
                } catch (\RuntimeException $e) {
177
                    while ($e->getPrevious() !== null) {
178
                        $e = $e->getPrevious();
179
                        if ($e instanceof UniqueConstraintViolationException) {
0 ignored issues
show
Bug introduced by
The class eZ\Publish\Core\Persiste...raintViolationException does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
180
                            // Concurrency! someone else inserted the same row that we where going to.
181
                            // let's do another loop pass
182
                            $uniqueCounter += 1;
183
                            continue 2;
184
                        }
185
                    }
186
187
                    throw $e;
188
                }
189
190
                break;
191
            }
192
193
            // Row exists, check if it is reusable. There are 3 cases when this is possible:
194
            // 1. NOP entry
195
            // 2. existing location or custom alias entry
196
            // 3. history entry
197
            if ($row['action'] == 'nop:' || $row['action'] == $action || $row['is_original'] == 0) {
198
                // Check for existing location entry on this level, if it exists and it's id differs from reusable
199
                // entry id then reusable entry should be updated with the existing location entry id.
200
                // Note: existing location entry may be downgraded and relinked later, depending on its language.
201
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
202
203
                if (!empty($existingLocationEntry)) {
204
                    // Always cleanup when active autogenerated entry exists on the same level
205
                    $cleanup = true;
206
                    $newId = $existingLocationEntry['id'];
207
                    if ($existingLocationEntry['id'] == $row['id']) {
208
                        // If we are reusing existing location entry merge existing language mask
209
                        $languageMask |= ($row['lang_mask'] & ~1);
210
                    }
211
                } elseif ($newId === null) {
212
                    // Use reused row ID only if publishing normally, else use given $newId
213
                    $newId = $row['id'];
214
                }
215
216
                $this->gateway->updateRow(
217
                    $parentId,
218
                    $newTextMD5,
219
                    array(
220
                        'action' => $action,
221
                        // In case when NOP row was reused
222
                        'action_type' => 'eznode',
223
                        'lang_mask' => $languageMask,
224
                        // Updating text ensures that letter case changes are stored
225
                        'text' => $newText,
226
                        // Set "id" and "link" for case when reusable entry is history
227
                        'id' => $newId,
228
                        'link' => $newId,
229
                        // Entry should be active location entry (original and not alias).
230
                        // Note: this takes care of taking over custom alias entry for the location on the same level
231
                        // and with same name and action.
232
                        'alias_redirects' => 1,
233
                        'is_original' => 1,
234
                        'is_alias' => 0,
235
                    )
236
                );
237
238
                break;
239
            }
240
241
            // If existing row is not reusable, increment $uniqueCounter and try again
242
            $uniqueCounter += 1;
243
        }
244
245
        /* @var $newText */
246
        if ($updatePathIdentificationString) {
247
            $this->locationGateway->updatePathIdentificationString(
248
                $locationId,
249
                $parentLocationId,
250
                $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...
251
            );
252
        }
253
254
        /* @var $newId */
255
        /* @var $newTextMD5 */
256
        // Note: cleanup does not touch custom and global entries
257
        if ($cleanup) {
258
            $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...
259
        }
260
    }
261
262
    /**
263
     * Create a user chosen $alias pointing to $locationId in $languageCode.
264
     *
265
     * If $languageCode is null the $alias is created in the system's default
266
     * language. $alwaysAvailable makes the alias available in all languages.
267
     *
268
     * @param mixed $locationId
269
     * @param string $path
270
     * @param bool $forwarding
271
     * @param string $languageCode
272
     * @param bool $alwaysAvailable
273
     *
274
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
275
     */
276
    public function createCustomUrlAlias($locationId, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
277
    {
278
        return $this->createUrlAlias(
279
            'eznode:' . $locationId,
280
            $path,
281
            $forwarding,
282
            $languageCode,
283
            $alwaysAvailable
284
        );
285
    }
286
287
    /**
288
     * Create a user chosen $alias pointing to a resource in $languageCode.
289
     * This method does not handle location resources - if a user enters a location target
290
     * the createCustomUrlAlias method has to be used.
291
     *
292
     * If $languageCode is null the $alias is created in the system's default
293
     * language. $alwaysAvailable makes the alias available in all languages.
294
     *
295
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException if the path already exists for the given language
296
     *
297
     * @param string $resource
298
     * @param string $path
299
     * @param bool $forwarding
300
     * @param string $languageCode
301
     * @param bool $alwaysAvailable
302
     *
303
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
304
     */
305
    public function createGlobalUrlAlias($resource, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
306
    {
307
        return $this->createUrlAlias(
308
            $resource,
309
            $path,
310
            $forwarding,
311
            $languageCode,
312
            $alwaysAvailable
313
        );
314
    }
315
316
    /**
317
     * Internal method for creating global or custom URL alias (these are handled in the same way).
318
     *
319
     * @throws \eZ\Publish\Core\Base\Exceptions\ForbiddenException if the path already exists for the given language
320
     *
321
     * @param string $action
322
     * @param string $path
323
     * @param bool $forward
324
     * @param string|null $languageCode
325
     * @param bool $alwaysAvailable
326
     *
327
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
328
     */
329
    protected function createUrlAlias($action, $path, $forward, $languageCode, $alwaysAvailable)
330
    {
331
        $pathElements = explode('/', $path);
332
        $topElement = array_pop($pathElements);
333
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
334
        $parentId = 0;
335
336
        // Handle all path elements except topmost one
337
        $isPathNew = false;
338
        foreach ($pathElements as $level => $pathElement) {
339
            $pathElement = $this->slugConverter->convert($pathElement, 'noname' . ($level + 1));
340
            $pathElementMD5 = $this->getHash($pathElement);
341
            if (!$isPathNew) {
342
                $row = $this->gateway->loadRow($parentId, $pathElementMD5);
343
                if (empty($row)) {
344
                    $isPathNew = true;
345
                } else {
346
                    $parentId = $row['link'];
347
                }
348
            }
349
350
            if ($isPathNew) {
351
                $parentId = $this->insertNopEntry($parentId, $pathElement, $pathElementMD5);
352
            }
353
        }
354
355
        // Handle topmost path element
356
        $topElement = $this->slugConverter->convert($topElement, 'noname' . (count($pathElements) + 1));
357
358
        // If last (next to topmost) entry parent is special root entry we handle topmost entry as first level entry
359
        // That is why we need to reset $parentId to 0
360
        if ($parentId != 0 && $this->gateway->isRootEntry($parentId)) {
361
            $parentId = 0;
362
        }
363
364
        $topElementMD5 = $this->getHash($topElement);
365
        // Set common values for two cases below
366
        $data = array(
367
            'action' => $action,
368
            'is_alias' => 1,
369
            'alias_redirects' => $forward ? 1 : 0,
370
            'parent' => $parentId,
371
            'text' => $topElement,
372
            'text_md5' => $topElementMD5,
373
            'is_original' => 1,
374
        );
375
        // Try to load topmost element
376
        if (!$isPathNew) {
377
            $row = $this->gateway->loadRow($parentId, $topElementMD5);
378
        }
379
380
        // If nothing was returned perform insert
381
        if ($isPathNew || empty($row)) {
382
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
383
            $id = $this->gateway->insertRow($data);
384
        } elseif ($row['action'] == 'nop:' || $row['is_original'] == 0) {
385
            // Row exists, check if it is reusable. There are 2 cases when this is possible:
386
            // 1. NOP entry
387
            // 2. history entry
388
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
389
            // If history is reused move link to id
390
            $data['link'] = $id = $row['id'];
391
            $this->gateway->updateRow(
392
                $parentId,
393
                $topElementMD5,
394
                $data
395
            );
396
        } else {
397
            throw new ForbiddenException("Path '%path%' already exists for the given language", ['%path%' => $path]);
398
        }
399
400
        $data['raw_path_data'] = $this->gateway->loadPathData($id);
401
402
        return $this->mapper->extractUrlAliasFromData($data);
403
    }
404
405
    /**
406
     * Convenience method for inserting nop type row.
407
     *
408
     * @param mixed $parentId
409
     * @param string $text
410
     * @param string $textMD5
411
     *
412
     * @return mixed
413
     */
414
    protected function insertNopEntry($parentId, $text, $textMD5)
415
    {
416
        return $this->gateway->insertRow(
417
            array(
418
                'lang_mask' => 1,
419
                'action' => 'nop:',
420
                'parent' => $parentId,
421
                'text' => $text,
422
                'text_md5' => $textMD5,
423
            )
424
        );
425
    }
426
427
    /**
428
     * List of user generated or autogenerated url entries, pointing to $locationId.
429
     *
430
     * @param mixed $locationId
431
     * @param bool $custom if true the user generated aliases are listed otherwise the autogenerated
432
     *
433
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
434
     */
435 View Code Duplication
    public function listURLAliasesForLocation($locationId, $custom = false)
436
    {
437
        $data = $this->gateway->loadLocationEntries($locationId, $custom);
438
        foreach ($data as &$entry) {
439
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
440
        }
441
442
        return $this->mapper->extractUrlAliasListFromData($data);
443
    }
444
445
    /**
446
     * List global aliases.
447
     *
448
     * @param string|null $languageCode
449
     * @param int $offset
450
     * @param int $limit
451
     *
452
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
453
     */
454 View Code Duplication
    public function listGlobalURLAliases($languageCode = null, $offset = 0, $limit = -1)
455
    {
456
        $data = $this->gateway->listGlobalEntries($languageCode, $offset, $limit);
457
        foreach ($data as &$entry) {
458
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
459
        }
460
461
        return $this->mapper->extractUrlAliasListFromData($data);
462
    }
463
464
    /**
465
     * Removes url aliases.
466
     *
467
     * Autogenerated aliases are not removed by this method.
468
     *
469
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $urlAliases
470
     *
471
     * @return bool
472
     */
473
    public function removeURLAliases(array $urlAliases)
474
    {
475
        foreach ($urlAliases as $urlAlias) {
476
            if ($urlAlias->isCustom) {
477
                list($parentId, $textMD5) = explode('-', $urlAlias->id);
478
                if (!$this->gateway->removeCustomAlias($parentId, $textMD5)) {
479
                    return false;
480
                }
481
            }
482
        }
483
484
        return true;
485
    }
486
487
    /**
488
     * Looks up a url alias for the given url.
489
     *
490
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
491
     * @throws \RuntimeException
492
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
493
     *
494
     * @param string $url
495
     *
496
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
497
     */
498
    public function lookup($url)
499
    {
500
        $urlHashes = array();
501
        foreach (explode('/', $url) as $level => $text) {
502
            $urlHashes[$level] = $this->getHash($text);
503
        }
504
505
        $data = $this->gateway->loadUrlAliasData($urlHashes);
506
        if (empty($data)) {
507
            throw new NotFoundException('URLAlias', $url);
508
        }
509
510
        $pathDepth = count($urlHashes);
511
        $hierarchyData = array();
512
        $isPathHistory = false;
513
        for ($level = 0; $level < $pathDepth; ++$level) {
514
            $prefix = $level === $pathDepth - 1 ? '' : 'ezurlalias_ml' . $level . '_';
515
            $isPathHistory = $isPathHistory ?: ($data[$prefix . 'link'] != $data[$prefix . 'id']);
516
            $hierarchyData[$level] = array(
517
                'id' => $data[$prefix . 'id'],
518
                'parent' => $data[$prefix . 'parent'],
519
                'action' => $data[$prefix . 'action'],
520
            );
521
        }
522
523
        $data['is_path_history'] = $isPathHistory;
524
        $data['raw_path_data'] = ($data['action_type'] == 'eznode' && !$data['is_alias'])
525
            ? $this->gateway->loadPathDataByHierarchy($hierarchyData)
526
            : $this->gateway->loadPathData($data['id']);
527
528
        return $this->mapper->extractUrlAliasFromData($data);
529
    }
530
531
    /**
532
     * Loads URL alias by given $id.
533
     *
534
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
535
     *
536
     * @param string $id
537
     *
538
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
539
     */
540
    public function loadUrlAlias($id)
541
    {
542
        list($parentId, $textMD5) = explode('-', $id);
543
        $data = $this->gateway->loadRow($parentId, $textMD5);
544
545
        if (empty($data)) {
546
            throw new NotFoundException('URLAlias', $id);
547
        }
548
549
        $data['raw_path_data'] = $this->gateway->loadPathData($data['id']);
550
551
        return $this->mapper->extractUrlAliasFromData($data);
552
    }
553
554
    /**
555
     * Notifies the underlying engine that a location has moved.
556
     *
557
     * This method triggers the change of the autogenerated aliases.
558
     *
559
     * @param mixed $locationId
560
     * @param mixed $oldParentId
561
     * @param mixed $newParentId
562
     */
563
    public function locationMoved($locationId, $oldParentId, $newParentId)
564
    {
565
        // @todo optimize: $newLocationAliasId is already available in self::publishUrlAliasForLocation() as $newId
566
        $newParentLocationAliasId = $this->getRealAliasId($newParentId);
567
        $newLocationAlias = $this->gateway->loadAutogeneratedEntry(
568
            'eznode:' . $locationId,
569
            $newParentLocationAliasId
570
        );
571
572
        $oldParentLocationAliasId = $this->getRealAliasId($oldParentId);
573
        $oldLocationAlias = $this->gateway->loadAutogeneratedEntry(
574
            'eznode:' . $locationId,
575
            $oldParentLocationAliasId
576
        );
577
578
        // Historize alias for old location
579
        $this->gateway->historizeId($oldLocationAlias['id'], $newLocationAlias['id']);
580
        // Reparent subtree of old location to new location
581
        $this->gateway->reparent($oldLocationAlias['id'], $newLocationAlias['id']);
582
    }
583
584
    /**
585
     * Notifies the underlying engine that a location was copied.
586
     *
587
     * This method triggers the creation of the autogenerated aliases for the copied locations
588
     *
589
     * @param mixed $locationId
590
     * @param mixed $newLocationId
591
     * @param mixed $newParentId
592
     */
593
    public function locationCopied($locationId, $newLocationId, $newParentId)
594
    {
595
        $newParentAliasId = $this->getRealAliasId($newLocationId);
596
        $oldParentAliasId = $this->getRealAliasId($locationId);
597
598
        $actionMap = $this->getCopiedLocationsMap($locationId, $newLocationId);
599
600
        $this->copySubtree(
601
            $actionMap,
602
            $oldParentAliasId,
603
            $newParentAliasId
604
        );
605
    }
606
607
    public function locationSwapped($location1Id, $location1ParentId, $location2Id, $location2ParentId)
608
    {
609
        $location1Entries = $this->gateway->loadLocationEntries($location1Id);
610
        $location2Entries = $this->gateway->loadLocationEntries($location2Id);
611
612
        $location1MainLanguageId = $this->gateway->getLocationContentMainLanguageId($location1Id);
613
        $location2MainLanguageId = $this->gateway->getLocationContentMainLanguageId($location2Id);
614
615
        // Load autogenerated entries to find alias ID
616
        $autoLocation1 = $this->gateway->loadAutogeneratedEntry("eznode:{$location1Id}");
617
        $autoLocation2 = $this->gateway->loadAutogeneratedEntry("eznode:{$location2Id}");
618
619
        // Historize everything first to avoid name conflicts in case swapped Locations are siblings
620
        $this->historizeBeforeSwap($location1Entries, $location2Entries);
621
622 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...
623
            $alwaysAvailable = (bool)($row['lang_mask'] & 1);
624
            $languageIds = $this->extractLanguageIdsFromMask($row['lang_mask']);
625
626
            foreach ($languageIds as $languageId) {
627
                $isMainLanguage = $languageId == $location2MainLanguageId;
628
                $this->internalPublishUrlAliasForLocation(
629
                    $location1Id,
630
                    $location1ParentId,
631
                    $row['text'],
632
                    $languageId,
633
                    $isMainLanguage && $alwaysAvailable,
634
                    $isMainLanguage,
635
                    $autoLocation1['id']
636
                );
637
            }
638
        }
639
640 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...
641
            $alwaysAvailable = (bool)($row['lang_mask'] & 1);
642
            $languageIds = $this->extractLanguageIdsFromMask($row['lang_mask']);
643
644
            foreach ($languageIds as $languageId) {
645
                $isMainLanguage = $languageId == $location1MainLanguageId;
646
                $this->internalPublishUrlAliasForLocation(
647
                    $location2Id,
648
                    $location2ParentId,
649
                    $row['text'],
650
                    $languageId,
651
                    $isMainLanguage && $alwaysAvailable,
652
                    $isMainLanguage,
653
                    $autoLocation2['id']
654
                );
655
            }
656
        }
657
    }
658
659
    /**
660
     * Historizes given existing active entries for two swapped Locations.
661
     *
662
     * This should be done before republishing URL aliases, in order to avoid unnecessary
663
     * conflicts when swapped Locations are siblings.
664
     *
665
     * We need to historize everything separately per language (mask), in case the entries
666
     * remain history future publishing reusages need to be able to take them over cleanly.
667
     *
668
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped()
669
     *
670
     * @param array $location1Entries
671
     * @param array $location2Entries
672
     */
673
    private function historizeBeforeSwap($location1Entries, $location2Entries)
674
    {
675
        foreach ($location1Entries as $row) {
676
            $this->gateway->historizeBeforeSwap($row['action'], $row['lang_mask']);
677
        }
678
679
        foreach ($location2Entries as $row) {
680
            $this->gateway->historizeBeforeSwap($row['action'], $row['lang_mask']);
681
        }
682
    }
683
684
    /**
685
     * Extracts every language Ids contained in $languageMask.
686
     *
687
     * @param int $languageMask
688
     *
689
     * @return int[] An array of language IDs
690
     */
691
    private function extractLanguageIdsFromMask($languageMask)
692
    {
693
        $exp = 2;
694
        $languageIds = [];
695
696
        // Decomposition of $languageMask into its binary components.
697
        while ($exp <= $languageMask) {
698
            if ($languageMask & $exp) {
699
                $languageIds[] = $exp;
700
            }
701
702
            $exp *= 2;
703
        }
704
705
        return $languageIds;
706
    }
707
708
    /**
709
     * Returns possibly corrected alias id for given $locationId !! For use as parent id in logic.
710
     *
711
     * First level entries must have parent id set to 0 instead of their parent location alias id.
712
     * There are two cases when alias id needs to be corrected:
713
     * 1) location is special location without URL alias (location with id=1 in standard installation)
714
     * 2) location is site root location, having special root entry in the ezurlalias_ml table (location with id=2
715
     *    in standard installation)
716
     *
717
     * @param mixed $locationId
718
     *
719
     * @return mixed
720
     */
721
    protected function getRealAliasId($locationId)
722
    {
723
        // Absolute root location does have a url alias entry so we can skip lookup
724
        if ($locationId == self::ROOT_LOCATION_ID) {
725
            return 0;
726
        }
727
728
        $data = $this->gateway->loadAutogeneratedEntry('eznode:' . $locationId);
729
730
        // Root entries (URL wise) can return 0 as the returned value is used as parent (parent is 0 for root entries)
731
        if (empty($data) || $data['id'] != 0 && $data['parent'] == 0 && strlen($data['text']) == 0) {
732
            $id = 0;
733
        } else {
734
            $id = $data['id'];
735
        }
736
737
        return $id;
738
    }
739
740
    /**
741
     * Recursively copies aliases from old parent under new parent.
742
     *
743
     * @param array $actionMap
744
     * @param mixed $oldParentAliasId
745
     * @param mixed $newParentAliasId
746
     */
747
    protected function copySubtree($actionMap, $oldParentAliasId, $newParentAliasId)
748
    {
749
        $rows = $this->gateway->loadAutogeneratedEntries($oldParentAliasId);
750
        $newIdsMap = array();
751
        foreach ($rows as $row) {
752
            $oldParentAliasId = $row['id'];
753
754
            // Ensure that same action entries remain grouped by the same id
755
            if (!isset($newIdsMap[$oldParentAliasId])) {
756
                $newIdsMap[$oldParentAliasId] = $this->gateway->getNextId();
757
            }
758
759
            $row['action'] = $actionMap[$row['action']];
760
            $row['parent'] = $newParentAliasId;
761
            $row['id'] = $row['link'] = $newIdsMap[$oldParentAliasId];
762
            $this->gateway->insertRow($row);
763
764
            $this->copySubtree(
765
                $actionMap,
766
                $oldParentAliasId,
767
                $row['id']
768
            );
769
        }
770
    }
771
772
    /**
773
     * @param mixed $oldParentId
774
     * @param mixed $newParentId
775
     *
776
     * @return array
777
     */
778
    protected function getCopiedLocationsMap($oldParentId, $newParentId)
779
    {
780
        $originalLocations = $this->locationGateway->getSubtreeContent($oldParentId);
781
        $copiedLocations = $this->locationGateway->getSubtreeContent($newParentId);
782
783
        $map = array();
784
        foreach ($originalLocations as $index => $originalLocation) {
785
            $map['eznode:' . $originalLocation['node_id']] = 'eznode:' . $copiedLocations[$index]['node_id'];
786
        }
787
788
        return $map;
789
    }
790
791
    /**
792
     * Notifies the underlying engine that a location was deleted or moved to trash.
793
     *
794
     * @param mixed $locationId
795
     */
796
    public function locationDeleted($locationId)
797
    {
798
        $action = 'eznode:' . $locationId;
799
        $entry = $this->gateway->loadAutogeneratedEntry($action);
800
801
        $this->removeSubtree($entry['id'], $action, $entry['is_original']);
802
    }
803
804
    /**
805
     * Recursively removes aliases by given $id and $action.
806
     *
807
     * $original parameter is used to limit removal of moved Location aliases to history entries only.
808
     *
809
     * @param mixed $id
810
     * @param string $action
811
     * @param mixed $original
812
     */
813
    protected function removeSubtree($id, $action, $original)
814
    {
815
        // Remove first to avoid unnecessary recursion.
816
        if ($original) {
817
            // If entry is original remove all for action (history and custom entries included).
818
            $this->gateway->remove($action);
819
        } else {
820
            // Else entry is history, so remove only for action with the id.
821
            // This means $id grouped history entries are removed, other history, active autogenerated
822
            // and custom are left alone.
823
            $this->gateway->remove($action, $id);
824
        }
825
826
        // Load all autogenerated for parent $id, including history.
827
        $entries = $this->gateway->loadAutogeneratedEntries($id, true);
828
829
        foreach ($entries as $entry) {
830
            $this->removeSubtree($entry['id'], $entry['action'], $entry['is_original']);
831
        }
832
    }
833
834
    /**
835
     * @param string $text
836
     *
837
     * @return string
838
     */
839
    protected function getHash($text)
840
    {
841
        return md5(mb_strtolower($text, 'UTF-8'));
842
    }
843
}
844