Completed
Push — ezp-26146_location_swap_incons... ( 4a02af...baaf24 )
by
unknown
21:53
created

Handler::swap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 2
c 1
b 1
f 0
nc 1
nop 2
dl 0
loc 4
rs 10
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
    /**
97
     * This method creates or updates an urlalias from a new or changed content name in a language
98
     * (if published). It also can be used to create an alias for a new location of content.
99
     * On update the old alias is linked to the new one (i.e. a history alias is generated).
100
     *
101
     * $alwaysAvailable controls whether the url alias is accessible in all
102
     * languages.
103
     *
104
     * @param mixed $locationId
105
     * @param mixed $parentLocationId
106
     * @param string $name the new name computed by the name schema or url alias schema
107
     * @param string $languageCode
108
     * @param bool $alwaysAvailable
109
     * @param bool $updatePathIdentificationString legacy storage specific for updating ezcontentobject_tree.path_identification_string
110
     */
111
    public function publishUrlAliasForLocation(
112
        $locationId,
113
        $parentLocationId,
114
        $name,
115
        $languageCode,
116
        $alwaysAvailable = false,
117
        $updatePathIdentificationString = false
118
    ) {
119
        $parentId = $this->getRealAliasId($parentLocationId);
120
        $uniqueCounter = $this->slugConverter->getUniqueCounterValue($name, $parentId == 0);
121
        $name = $this->slugConverter->convert($name, 'location_' . $locationId);
122
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
123
        $languageMask = $languageId | (int)$alwaysAvailable;
124
        $action = 'eznode:' . $locationId;
125
        $cleanup = false;
126
127
        // Exiting the loop with break;
128
        while (true) {
129
            $newText = '';
130
            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...
131
                $newText = $name . ($uniqueCounter > 1 ? $uniqueCounter : '');
132
            }
133
            $newTextMD5 = $this->getHash($newText);
134
135
            // Try to load existing entry
136
            $row = $this->gateway->loadRow($parentId, $newTextMD5);
137
138
            // If nothing was returned insert new entry
139
            if (empty($row)) {
140
                // Check for existing active location entry on this level and reuse it's id
141
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
142
                if (!empty($existingLocationEntry)) {
143
                    $cleanup = true;
144
                    $newId = $existingLocationEntry['id'];
145
                } else {
146
                    $newId = null;
147
                }
148
149
                $newId = $this->gateway->insertRow(
150
                    array(
151
                        'id' => $newId,
152
                        'link' => $newId,
153
                        'parent' => $parentId,
154
                        'action' => $action,
155
                        'lang_mask' => $languageMask,
156
                        'text' => $newText,
157
                        'text_md5' => $newTextMD5,
158
                    )
159
                );
160
161
                break;
162
            }
163
164
            // Row exists, check if it is reusable. There are 3 cases when this is possible:
165
            // 1. NOP entry
166
            // 2. existing location or custom alias entry
167
            // 3. history entry
168
            if ($row['action'] == 'nop:' || $row['action'] == $action || $row['is_original'] == 0) {
169
                // Check for existing location entry on this level, if it exists and it's id differs from reusable
170
                // entry id then reusable entry should be updated with the existing location entry id.
171
                // Note: existing location entry may be downgraded and relinked later, depending on its language.
172
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
173
                $newId = $row['id'];
174
                if (!empty($existingLocationEntry)) {
175
                    if ($existingLocationEntry['id'] != $row['id']) {
176
                        $cleanup = true;
177
                        $newId = $existingLocationEntry['id'];
178
                    } else {
179
                        // If we are reusing existing location entry merge existing language mask
180
                        $languageMask |= ($row['lang_mask'] & ~1);
181
                    }
182
                }
183
                $this->gateway->updateRow(
184
                    $parentId,
185
                    $newTextMD5,
186
                    array(
187
                        'action' => $action,
188
                        // In case when NOP row was reused
189
                        'action_type' => 'eznode',
190
                        'lang_mask' => $languageMask,
191
                        // Updating text ensures that letter case changes are stored
192
                        'text' => $newText,
193
                        // Set "id" and "link" for case when reusable entry is history
194
                        'id' => $newId,
195
                        'link' => $newId,
196
                        // Entry should be active location entry (original and not alias).
197
                        // Note: this takes care of taking over custom alias entry for the location on the same level
198
                        // and with same name and action.
199
                        'alias_redirects' => 1,
200
                        'is_original' => 1,
201
                        'is_alias' => 0,
202
                    )
203
                );
204
205
                break;
206
            }
207
208
            // If existing row is not reusable, increment $uniqueCounter and try again
209
            $uniqueCounter += 1;
210
        }
211
212
        if ($updatePathIdentificationString) {
213
            $this->locationGateway->updatePathIdentificationString(
214
                $locationId,
215
                $parentLocationId,
216
                $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...
217
            );
218
        }
219
220
        /* @var $newId */
221
        /* @var $newTextMD5 */
222
        // Note: cleanup does not touch custom and global entries
223
        if ($cleanup) {
224
            $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...
225
        }
226
    }
227
228
    /**
229
     * Create a user chosen $alias pointing to $locationId in $languageCode.
230
     *
231
     * If $languageCode is null the $alias is created in the system's default
232
     * language. $alwaysAvailable makes the alias available in all languages.
233
     *
234
     * @param mixed $locationId
235
     * @param string $path
236
     * @param bool $forwarding
237
     * @param string $languageCode
238
     * @param bool $alwaysAvailable
239
     *
240
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
241
     */
242
    public function createCustomUrlAlias($locationId, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
243
    {
244
        return $this->createUrlAlias(
245
            'eznode:' . $locationId,
246
            $path,
247
            $forwarding,
248
            $languageCode,
249
            $alwaysAvailable
250
        );
251
    }
252
253
    /**
254
     * Create a user chosen $alias pointing to a resource in $languageCode.
255
     * This method does not handle location resources - if a user enters a location target
256
     * the createCustomUrlAlias method has to be used.
257
     *
258
     * If $languageCode is null the $alias is created in the system's default
259
     * language. $alwaysAvailable makes the alias available in all languages.
260
     *
261
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException if the path already exists for the given language
262
     *
263
     * @param string $resource
264
     * @param string $path
265
     * @param bool $forwarding
266
     * @param string $languageCode
267
     * @param bool $alwaysAvailable
268
     *
269
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
270
     */
271
    public function createGlobalUrlAlias($resource, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
272
    {
273
        return $this->createUrlAlias(
274
            $resource,
275
            $path,
276
            $forwarding,
277
            $languageCode,
278
            $alwaysAvailable
279
        );
280
    }
281
282
    /**
283
     * Internal method for creating global or custom URL alias (these are handled in the same way).
284
     *
285
     * @throws \eZ\Publish\Core\Base\Exceptions\ForbiddenException if the path already exists for the given language
286
     *
287
     * @param string $action
288
     * @param string $path
289
     * @param bool $forward
290
     * @param string|null $languageCode
291
     * @param bool $alwaysAvailable
292
     *
293
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
294
     */
295
    protected function createUrlAlias($action, $path, $forward, $languageCode, $alwaysAvailable)
296
    {
297
        $pathElements = explode('/', $path);
298
        $topElement = array_pop($pathElements);
299
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
300
        $parentId = 0;
301
302
        // Handle all path elements except topmost one
303
        $isPathNew = false;
304
        foreach ($pathElements as $level => $pathElement) {
305
            $pathElement = $this->slugConverter->convert($pathElement, 'noname' . ($level + 1));
306
            $pathElementMD5 = $this->getHash($pathElement);
307
            if (!$isPathNew) {
308
                $row = $this->gateway->loadRow($parentId, $pathElementMD5);
309
                if (empty($row)) {
310
                    $isPathNew = true;
311
                } else {
312
                    $parentId = $row['link'];
313
                }
314
            }
315
316
            if ($isPathNew) {
317
                $parentId = $this->insertNopEntry($parentId, $pathElement, $pathElementMD5);
318
            }
319
        }
320
321
        // Handle topmost path element
322
        $topElement = $this->slugConverter->convert($topElement, 'noname' . (count($pathElements) + 1));
323
324
        // If last (next to topmost) entry parent is special root entry we handle topmost entry as first level entry
325
        // That is why we need to reset $parentId to 0
326
        if ($parentId != 0 && $this->gateway->isRootEntry($parentId)) {
327
            $parentId = 0;
328
        }
329
330
        $topElementMD5 = $this->getHash($topElement);
331
        // Set common values for two cases below
332
        $data = array(
333
            'action' => $action,
334
            'is_alias' => 1,
335
            'alias_redirects' => $forward ? 1 : 0,
336
            'parent' => $parentId,
337
            'text' => $topElement,
338
            'text_md5' => $topElementMD5,
339
            'is_original' => 1,
340
        );
341
        // Try to load topmost element
342
        if (!$isPathNew) {
343
            $row = $this->gateway->loadRow($parentId, $topElementMD5);
344
        }
345
346
        // If nothing was returned perform insert
347
        if ($isPathNew || empty($row)) {
348
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
349
            $id = $this->gateway->insertRow($data);
350
        } elseif ($row['action'] == 'nop:' || $row['is_original'] == 0) {
351
            // Row exists, check if it is reusable. There are 2 cases when this is possible:
352
            // 1. NOP entry
353
            // 2. history entry
354
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
355
            // If history is reused move link to id
356
            $data['link'] = $id = $row['id'];
357
            $this->gateway->updateRow(
358
                $parentId,
359
                $topElementMD5,
360
                $data
361
            );
362
        } else {
363
            throw new ForbiddenException("Path '%path%' already exists for the given language", ['%path%' => $path]);
364
        }
365
366
        $data['raw_path_data'] = $this->gateway->loadPathData($id);
367
368
        return $this->mapper->extractUrlAliasFromData($data);
369
    }
370
371
    /**
372
     * Convenience method for inserting nop type row.
373
     *
374
     * @param mixed $parentId
375
     * @param string $text
376
     * @param string $textMD5
377
     *
378
     * @return mixed
379
     */
380
    protected function insertNopEntry($parentId, $text, $textMD5)
381
    {
382
        return $this->gateway->insertRow(
383
            array(
384
                'lang_mask' => 1,
385
                'action' => 'nop:',
386
                'parent' => $parentId,
387
                'text' => $text,
388
                'text_md5' => $textMD5,
389
            )
390
        );
391
    }
392
393
    /**
394
     * List of user generated or autogenerated url entries, pointing to $locationId.
395
     *
396
     * @param mixed $locationId
397
     * @param bool $custom if true the user generated aliases are listed otherwise the autogenerated
398
     *
399
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
400
     */
401 View Code Duplication
    public function listURLAliasesForLocation($locationId, $custom = false)
402
    {
403
        $data = $this->gateway->loadLocationEntries($locationId, $custom);
404
        foreach ($data as &$entry) {
405
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
406
        }
407
408
        return $this->mapper->extractUrlAliasListFromData($data);
409
    }
410
411
    /**
412
     * List global aliases.
413
     *
414
     * @param string|null $languageCode
415
     * @param int $offset
416
     * @param int $limit
417
     *
418
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
419
     */
420 View Code Duplication
    public function listGlobalURLAliases($languageCode = null, $offset = 0, $limit = -1)
421
    {
422
        $data = $this->gateway->listGlobalEntries($languageCode, $offset, $limit);
423
        foreach ($data as &$entry) {
424
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
425
        }
426
427
        return $this->mapper->extractUrlAliasListFromData($data);
428
    }
429
430
    /**
431
     * Removes url aliases.
432
     *
433
     * Autogenerated aliases are not removed by this method.
434
     *
435
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $urlAliases
436
     *
437
     * @return bool
438
     */
439
    public function removeURLAliases(array $urlAliases)
440
    {
441
        foreach ($urlAliases as $urlAlias) {
442
            if ($urlAlias->isCustom) {
443
                list($parentId, $textMD5) = explode('-', $urlAlias->id);
444
                if (!$this->gateway->removeCustomAlias($parentId, $textMD5)) {
445
                    return false;
446
                }
447
            }
448
        }
449
450
        return true;
451
    }
452
453
    /**
454
     * Looks up a url alias for the given url.
455
     *
456
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
457
     * @throws \RuntimeException
458
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
459
     *
460
     * @param string $url
461
     *
462
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
463
     */
464
    public function lookup($url)
465
    {
466
        $urlHashes = array();
467
        foreach (explode('/', $url) as $level => $text) {
468
            $urlHashes[$level] = $this->getHash($text);
469
        }
470
471
        $data = $this->gateway->loadUrlAliasData($urlHashes);
472
        if (empty($data)) {
473
            throw new NotFoundException('URLAlias', $url);
474
        }
475
476
        $pathDepth = count($urlHashes);
477
        $hierarchyData = array();
478
        $isPathHistory = false;
479
        for ($level = 0; $level < $pathDepth; ++$level) {
480
            $prefix = $level === $pathDepth - 1 ? '' : 'ezurlalias_ml' . $level . '_';
481
            $isPathHistory = $isPathHistory ?: ($data[$prefix . 'link'] != $data[$prefix . 'id']);
482
            $hierarchyData[$level] = array(
483
                'id' => $data[$prefix . 'id'],
484
                'parent' => $data[$prefix . 'parent'],
485
                'action' => $data[$prefix . 'action'],
486
            );
487
        }
488
489
        $data['is_path_history'] = $isPathHistory;
490
        $data['raw_path_data'] = ($data['action_type'] == 'eznode' && !$data['is_alias'])
491
            ? $this->gateway->loadPathDataByHierarchy($hierarchyData)
492
            : $this->gateway->loadPathData($data['id']);
493
494
        return $this->mapper->extractUrlAliasFromData($data);
495
    }
496
497
    /**
498
     * Loads URL alias by given $id.
499
     *
500
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
501
     *
502
     * @param string $id
503
     *
504
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
505
     */
506
    public function loadUrlAlias($id)
507
    {
508
        list($parentId, $textMD5) = explode('-', $id);
509
        $data = $this->gateway->loadRow($parentId, $textMD5);
510
511
        if (empty($data)) {
512
            throw new NotFoundException('URLAlias', $id);
513
        }
514
515
        $data['raw_path_data'] = $this->gateway->loadPathData($data['id']);
516
517
        return $this->mapper->extractUrlAliasFromData($data);
518
    }
519
520
    /**
521
     * Swaps the content being referred to by two aliases.
522
     *
523
     * @param mixed $locationId1
524
     * @param mixed $locationId2
525
     *
526
     * @return bool
527
     */
528
    public function swap($locationId1, $locationId2)
529
    {
530
        $this->gateway->swap($locationId1, $locationId2);
531
    }
532
533
    /**
534
     * Notifies the underlying engine that a location has moved.
535
     *
536
     * This method triggers the change of the autogenerated aliases.
537
     *
538
     * @param mixed $locationId
539
     * @param mixed $oldParentId
540
     * @param mixed $newParentId
541
     */
542
    public function locationMoved($locationId, $oldParentId, $newParentId)
543
    {
544
        // @todo optimize: $newLocationAliasId is already available in self::publishUrlAliasForLocation() as $newId
545
        $newParentLocationAliasId = $this->getRealAliasId($newParentId);
546
        $newLocationAlias = $this->gateway->loadAutogeneratedEntry(
547
            'eznode:' . $locationId,
548
            $newParentLocationAliasId
549
        );
550
551
        $oldParentLocationAliasId = $this->getRealAliasId($oldParentId);
552
        $oldLocationAlias = $this->gateway->loadAutogeneratedEntry(
553
            'eznode:' . $locationId,
554
            $oldParentLocationAliasId
555
        );
556
557
        // Historize alias for old location
558
        $this->gateway->historizeId($oldLocationAlias['id'], $newLocationAlias['id']);
559
        // Reparent subtree of old location to new location
560
        $this->gateway->reparent($oldLocationAlias['id'], $newLocationAlias['id']);
561
    }
562
563
    /**
564
     * Notifies the underlying engine that a location has moved.
565
     *
566
     * This method triggers the creation of the autogenerated aliases for the copied locations
567
     *
568
     * @param mixed $locationId
569
     * @param mixed $newLocationId
570
     * @param mixed $newParentId
571
     */
572
    public function locationCopied($locationId, $newLocationId, $newParentId)
573
    {
574
        $newParentAliasId = $this->getRealAliasId($newLocationId);
575
        $oldParentAliasId = $this->getRealAliasId($locationId);
576
577
        $actionMap = $this->getCopiedLocationsMap($locationId, $newLocationId);
578
579
        $this->copySubtree(
580
            $actionMap,
581
            $oldParentAliasId,
582
            $newParentAliasId,
583
            $locationId
584
        );
585
    }
586
587
    /**
588
     * Returns possibly corrected alias id for given $locationId !! For use as parent id in logic.
589
     *
590
     * First level entries must have parent id set to 0 instead of their parent location alias id.
591
     * There are two cases when alias id needs to be corrected:
592
     * 1) location is special location without URL alias (location with id=1 in standard installation)
593
     * 2) location is site root location, having special root entry in the ezurlalias_ml table (location with id=2
594
     *    in standard installation)
595
     *
596
     * @param mixed $locationId
597
     *
598
     * @return mixed
599
     */
600
    protected function getRealAliasId($locationId)
601
    {
602
        // Absolute root location does have a url alias entry so we can skip lookup
603
        if ($locationId == self::ROOT_LOCATION_ID) {
604
            return 0;
605
        }
606
607
        $data = $this->gateway->loadAutogeneratedEntry('eznode:' . $locationId);
608
609
        // Root entries (URL wise) can return 0 as the returned value is used as parent (parent is 0 for root entries)
610
        if (empty($data) || $data['id'] != 0 && $data['parent'] == 0 && strlen($data['text']) == 0) {
611
            $id = 0;
612
        } else {
613
            $id = $data['id'];
614
        }
615
616
        return $id;
617
    }
618
619
    /**
620
     * Recursively copies aliases from old parent under new parent.
621
     *
622
     * @param array $actionMap
623
     * @param mixed $oldParentAliasId
624
     * @param mixed $newParentAliasId
625
     */
626
    protected function copySubtree($actionMap, $oldParentAliasId, $newParentAliasId)
627
    {
628
        $rows = $this->gateway->loadAutogeneratedEntries($oldParentAliasId);
629
        $newIdsMap = array();
630
        foreach ($rows as $row) {
631
            $oldParentAliasId = $row['id'];
632
633
            // Ensure that same action entries remain grouped by the same id
634
            if (!isset($newIdsMap[$oldParentAliasId])) {
635
                $newIdsMap[$oldParentAliasId] = $this->gateway->getNextId();
636
            }
637
638
            $row['action'] = $actionMap[$row['action']];
639
            $row['parent'] = $newParentAliasId;
640
            $row['id'] = $row['link'] = $newIdsMap[$oldParentAliasId];
641
            $this->gateway->insertRow($row);
642
643
            $this->copySubtree(
644
                $actionMap,
645
                $oldParentAliasId,
646
                $row['id']
647
            );
648
        }
649
    }
650
651
    /**
652
     * @param mixed $oldParentId
653
     * @param mixed $newParentId
654
     *
655
     * @return array
656
     */
657
    protected function getCopiedLocationsMap($oldParentId, $newParentId)
658
    {
659
        $originalLocations = $this->locationGateway->getSubtreeContent($oldParentId);
660
        $copiedLocations = $this->locationGateway->getSubtreeContent($newParentId);
661
662
        $map = array();
663
        foreach ($originalLocations as $index => $originalLocation) {
664
            $map['eznode:' . $originalLocation['node_id']] = 'eznode:' . $copiedLocations[$index]['node_id'];
665
        }
666
667
        return $map;
668
    }
669
670
    /**
671
     * Notifies the underlying engine that a location was deleted or moved to trash.
672
     *
673
     * @param mixed $locationId
674
     */
675
    public function locationDeleted($locationId)
676
    {
677
        $action = 'eznode:' . $locationId;
678
        $entry = $this->gateway->loadAutogeneratedEntry($action);
679
680
        $this->removeSubtree($entry['id'], $action, $entry['is_original']);
681
    }
682
683
    /**
684
     * Recursively removes aliases by given $id and $action.
685
     *
686
     * $original parameter is used to limit removal of moved Location aliases to history entries only.
687
     *
688
     * @param mixed $id
689
     * @param string $action
690
     * @param mixed $original
691
     */
692
    protected function removeSubtree($id, $action, $original)
693
    {
694
        // Remove first to avoid unnecessary recursion.
695
        if ($original) {
696
            // If entry is original remove all for action (history and custom entries included).
697
            $this->gateway->remove($action);
698
        } else {
699
            // Else entry is history, so remove only for action with the id.
700
            // This means $id grouped history entries are removed, other history, active autogenerated
701
            // and custom are left alone.
702
            $this->gateway->remove($action, $id);
703
        }
704
705
        // Load all autogenerated for parent $id, including history.
706
        $entries = $this->gateway->loadAutogeneratedEntries($id, true);
707
708
        foreach ($entries as $entry) {
709
            $this->removeSubtree($entry['id'], $entry['action'], $entry['is_original']);
710
        }
711
    }
712
713
    /**
714
     * @param string $text
715
     *
716
     * @return string
717
     */
718
    protected function getHash($text)
719
    {
720
        return md5(mb_strtolower($text, 'UTF-8'));
721
    }
722
}
723