Completed
Push — test-fieldrelation-location-se... ( 448993...a417bd )
by
unknown
13:37
created

Handler::copySubtree()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 15
nc 3
nop 3
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
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
     * Notifies the underlying engine that a location has moved.
522
     *
523
     * This method triggers the change of the autogenerated aliases.
524
     *
525
     * @param mixed $locationId
526
     * @param mixed $oldParentId
527
     * @param mixed $newParentId
528
     */
529
    public function locationMoved($locationId, $oldParentId, $newParentId)
530
    {
531
        // @todo optimize: $newLocationAliasId is already available in self::publishUrlAliasForLocation() as $newId
532
        $newParentLocationAliasId = $this->getRealAliasId($newParentId);
533
        $newLocationAlias = $this->gateway->loadAutogeneratedEntry(
534
            'eznode:' . $locationId,
535
            $newParentLocationAliasId
536
        );
537
538
        $oldParentLocationAliasId = $this->getRealAliasId($oldParentId);
539
        $oldLocationAlias = $this->gateway->loadAutogeneratedEntry(
540
            'eznode:' . $locationId,
541
            $oldParentLocationAliasId
542
        );
543
544
        // Historize alias for old location
545
        $this->gateway->historizeId($oldLocationAlias['id'], $newLocationAlias['id']);
546
        // Reparent subtree of old location to new location
547
        $this->gateway->reparent($oldLocationAlias['id'], $newLocationAlias['id']);
548
    }
549
550
    /**
551
     * Notifies the underlying engine that a location has moved.
552
     *
553
     * This method triggers the creation of the autogenerated aliases for the copied locations
554
     *
555
     * @param mixed $locationId
556
     * @param mixed $newLocationId
557
     * @param mixed $newParentId
558
     */
559
    public function locationCopied($locationId, $newLocationId, $newParentId)
560
    {
561
        $newParentAliasId = $this->getRealAliasId($newLocationId);
562
        $oldParentAliasId = $this->getRealAliasId($locationId);
563
564
        $actionMap = $this->getCopiedLocationsMap($locationId, $newLocationId);
565
566
        $this->copySubtree(
567
            $actionMap,
568
            $oldParentAliasId,
569
            $newParentAliasId,
570
            $locationId
571
        );
572
    }
573
574
    /**
575
     * Returns possibly corrected alias id for given $locationId !! For use as parent id in logic.
576
     *
577
     * First level entries must have parent id set to 0 instead of their parent location alias id.
578
     * There are two cases when alias id needs to be corrected:
579
     * 1) location is special location without URL alias (location with id=1 in standard installation)
580
     * 2) location is site root location, having special root entry in the ezurlalias_ml table (location with id=2
581
     *    in standard installation)
582
     *
583
     * @param mixed $locationId
584
     *
585
     * @return mixed
586
     */
587
    protected function getRealAliasId($locationId)
588
    {
589
        // Absolute root location does have a url alias entry so we can skip lookup
590
        if ($locationId == self::ROOT_LOCATION_ID) {
591
            return 0;
592
        }
593
594
        $data = $this->gateway->loadAutogeneratedEntry('eznode:' . $locationId);
595
596
        // Root entries (URL wise) can return 0 as the returned value is used as parent (parent is 0 for root entries)
597
        if (empty($data) || $data['id'] != 0 && $data['parent'] == 0 && strlen($data['text']) == 0) {
598
            $id = 0;
599
        } else {
600
            $id = $data['id'];
601
        }
602
603
        return $id;
604
    }
605
606
    /**
607
     * Recursively copies aliases from old parent under new parent.
608
     *
609
     * @param array $actionMap
610
     * @param mixed $oldParentAliasId
611
     * @param mixed $newParentAliasId
612
     */
613
    protected function copySubtree($actionMap, $oldParentAliasId, $newParentAliasId)
614
    {
615
        $rows = $this->gateway->loadAutogeneratedEntries($oldParentAliasId);
616
        $newIdsMap = array();
617
        foreach ($rows as $row) {
618
            $oldParentAliasId = $row['id'];
619
620
            // Ensure that same action entries remain grouped by the same id
621
            if (!isset($newIdsMap[$oldParentAliasId])) {
622
                $newIdsMap[$oldParentAliasId] = $this->gateway->getNextId();
623
            }
624
625
            $row['action'] = $actionMap[$row['action']];
626
            $row['parent'] = $newParentAliasId;
627
            $row['id'] = $row['link'] = $newIdsMap[$oldParentAliasId];
628
            $this->gateway->insertRow($row);
629
630
            $this->copySubtree(
631
                $actionMap,
632
                $oldParentAliasId,
633
                $row['id']
634
            );
635
        }
636
    }
637
638
    /**
639
     * @param mixed $oldParentId
640
     * @param mixed $newParentId
641
     *
642
     * @return array
643
     */
644
    protected function getCopiedLocationsMap($oldParentId, $newParentId)
645
    {
646
        $originalLocations = $this->locationGateway->getSubtreeContent($oldParentId);
647
        $copiedLocations = $this->locationGateway->getSubtreeContent($newParentId);
648
649
        $map = array();
650
        foreach ($originalLocations as $index => $originalLocation) {
651
            $map['eznode:' . $originalLocation['node_id']] = 'eznode:' . $copiedLocations[$index]['node_id'];
652
        }
653
654
        return $map;
655
    }
656
657
    /**
658
     * Notifies the underlying engine that a location was deleted or moved to trash.
659
     *
660
     * @param mixed $locationId
661
     */
662
    public function locationDeleted($locationId)
663
    {
664
        $action = 'eznode:' . $locationId;
665
        $entry = $this->gateway->loadAutogeneratedEntry($action);
666
667
        $this->removeSubtree($entry['id'], $action, $entry['is_original']);
668
    }
669
670
    /**
671
     * Recursively removes aliases by given $id and $action.
672
     *
673
     * $original parameter is used to limit removal of moved Location aliases to history entries only.
674
     *
675
     * @param mixed $id
676
     * @param string $action
677
     * @param mixed $original
678
     */
679
    protected function removeSubtree($id, $action, $original)
680
    {
681
        // Remove first to avoid unnecessary recursion.
682
        if ($original) {
683
            // If entry is original remove all for action (history and custom entries included).
684
            $this->gateway->remove($action);
685
        } else {
686
            // Else entry is history, so remove only for action with the id.
687
            // This means $id grouped history entries are removed, other history, active autogenerated
688
            // and custom are left alone.
689
            $this->gateway->remove($action, $id);
690
        }
691
692
        // Load all autogenerated for parent $id, including history.
693
        $entries = $this->gateway->loadAutogeneratedEntries($id, true);
694
695
        foreach ($entries as $entry) {
696
            $this->removeSubtree($entry['id'], $entry['action'], $entry['is_original']);
697
        }
698
    }
699
700
    /**
701
     * @param string $text
702
     *
703
     * @return string
704
     */
705
    protected function getHash($text)
706
    {
707
        return md5(mb_strtolower($text, 'UTF-8'));
708
    }
709
}
710