Completed
Push — 7.0 ( 6c9b08...431e20 )
by André
18:57
created

Handler::getCopiedLocationsMap()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 12
rs 9.4285
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
namespace eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias;
10
11
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
12
use eZ\Publish\SPI\Persistence\Content\UrlAlias;
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
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
19
20
/**
21
 * The UrlAlias Handler provides nice urls management.
22
 *
23
 * Its methods operate on a representation of the url alias data structure held
24
 * inside a storage engine.
25
 */
26
class Handler implements UrlAliasHandlerInterface
27
{
28
    const ROOT_LOCATION_ID = 1;
29
30
    /**
31
     * This is intentionally hardcoded for now as:
32
     * 1. We don't implement this configuration option.
33
     * 2. Such option should not be in this layer, should be handled higher up.
34
     *
35
     * @deprecated
36
     */
37
    const CONTENT_REPOSITORY_ROOT_LOCATION_ID = 2;
38
39
    /**
40
     * The maximum level of alias depth.
41
     */
42
    const MAX_URL_ALIAS_DEPTH_LEVEL = 60;
43
44
    /**
45
     * UrlAlias Gateway.
46
     *
47
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway
48
     */
49
    protected $gateway;
50
51
    /**
52
     * Gateway for handling location data.
53
     *
54
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway
55
     */
56
    protected $locationGateway;
57
58
    /**
59
     * UrlAlias Mapper.
60
     *
61
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper
62
     */
63
    protected $mapper;
64
65
    /**
66
     * Caching language handler.
67
     *
68
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
69
     */
70
    protected $languageHandler;
71
72
    /**
73
     * URL slug converter.
74
     *
75
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter
76
     */
77
    protected $slugConverter;
78
79
    /**
80
     * Creates a new UrlAlias Handler.
81
     *
82
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $gateway
83
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper $mapper
84
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway $locationGateway
85
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
86
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter $slugConverter
87
     */
88
    public function __construct(
89
        Gateway $gateway,
90
        Mapper $mapper,
91
        LocationGateway $locationGateway,
92
        LanguageHandler $languageHandler,
93
        SlugConverter $slugConverter
94
    ) {
95
        $this->gateway = $gateway;
96
        $this->mapper = $mapper;
97
        $this->locationGateway = $locationGateway;
98
        $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...
99
        $this->slugConverter = $slugConverter;
100
    }
101
102
    public function publishUrlAliasForLocation(
103
        $locationId,
104
        $parentLocationId,
105
        $name,
106
        $languageCode,
107
        $alwaysAvailable = false,
108
        $updatePathIdentificationString = false
109
    ) {
110
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
111
112
        $this->internalPublishUrlAliasForLocation(
113
            $locationId,
114
            $parentLocationId,
115
            $name,
116
            $languageId,
117
            $alwaysAvailable,
118
            $updatePathIdentificationString
119
        );
120
    }
121
122
    /**
123
     * Internal publish method, accepting language ID instead of language code and optionally
124
     * new alias ID (used when swapping Locations).
125
     *
126
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped()
127
     *
128
     * @param int $locationId
129
     * @param int $parentLocationId
130
     * @param string $name
131
     * @param int $languageId
132
     * @param bool $alwaysAvailable
133
     * @param bool $updatePathIdentificationString legacy storage specific for updating ezcontentobject_tree.path_identification_string
134
     * @param int $newId
135
     */
136
    private function internalPublishUrlAliasForLocation(
137
        $locationId,
138
        $parentLocationId,
139
        $name,
140
        $languageId,
141
        $alwaysAvailable = false,
142
        $updatePathIdentificationString = false,
143
        $newId = null
144
    ) {
145
        $parentId = $this->getRealAliasId($parentLocationId);
146
        $name = $this->slugConverter->convert($name, 'location_' . $locationId);
147
        $uniqueCounter = $this->slugConverter->getUniqueCounterValue($name, $parentId == 0);
148
        $languageMask = $languageId | (int)$alwaysAvailable;
149
        $action = 'eznode:' . $locationId;
150
        $cleanup = false;
151
152
        // Exiting the loop with break;
153
        while (true) {
154
            $newText = '';
155
            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...
156
                $newText = $name . ($uniqueCounter > 1 ? $uniqueCounter : '');
157
            }
158
            $newTextMD5 = $this->getHash($newText);
159
160
            // Try to load existing entry
161
            $row = $this->gateway->loadRow($parentId, $newTextMD5);
162
163
            // If nothing was returned insert new entry
164
            if (empty($row)) {
165
                // Check for existing active location entry on this level and reuse it's id
166
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
167
                if (!empty($existingLocationEntry)) {
168
                    $cleanup = true;
169
                    $newId = $existingLocationEntry['id'];
170
                }
171
172
                try {
173
                    $newId = $this->gateway->insertRow(
174
                        array(
175
                            'id' => $newId,
176
                            'link' => $newId,
177
                            'parent' => $parentId,
178
                            'action' => $action,
179
                            'lang_mask' => $languageMask,
180
                            'text' => $newText,
181
                            'text_md5' => $newTextMD5,
182
                        )
183
                    );
184
                } catch (\RuntimeException $e) {
185
                    while ($e->getPrevious() !== null) {
186
                        $e = $e->getPrevious();
187
                        if ($e instanceof UniqueConstraintViolationException) {
188
                            // Concurrency! someone else inserted the same row that we where going to.
189
                            // let's do another loop pass
190
                            $uniqueCounter += 1;
191
                            continue 2;
192
                        }
193
                    }
194
195
                    throw $e;
196
                }
197
198
                break;
199
            }
200
201
            // Row exists, check if it is reusable. There are 3 cases when this is possible:
202
            // 1. NOP entry
203
            // 2. existing location or custom alias entry
204
            // 3. history entry
205
            if ($row['action'] == 'nop:' || $row['action'] == $action || $row['is_original'] == 0) {
206
                // Check for existing location entry on this level, if it exists and it's id differs from reusable
207
                // entry id then reusable entry should be updated with the existing location entry id.
208
                // Note: existing location entry may be downgraded and relinked later, depending on its language.
209
                $existingLocationEntry = $this->gateway->loadAutogeneratedEntry($action, $parentId);
210
211
                if (!empty($existingLocationEntry)) {
212
                    // Always cleanup when active autogenerated entry exists on the same level
213
                    $cleanup = true;
214
                    $newId = $existingLocationEntry['id'];
215
                    if ($existingLocationEntry['id'] == $row['id']) {
216
                        // If we are reusing existing location entry merge existing language mask
217
                        $languageMask |= ($row['lang_mask'] & ~1);
218
                    }
219
                } elseif ($newId === null) {
220
                    // Use reused row ID only if publishing normally, else use given $newId
221
                    $newId = $row['id'];
222
                }
223
224
                $this->gateway->updateRow(
225
                    $parentId,
226
                    $newTextMD5,
227
                    array(
228
                        'action' => $action,
229
                        // In case when NOP row was reused
230
                        'action_type' => 'eznode',
231
                        'lang_mask' => $languageMask,
232
                        // Updating text ensures that letter case changes are stored
233
                        'text' => $newText,
234
                        // Set "id" and "link" for case when reusable entry is history
235
                        'id' => $newId,
236
                        'link' => $newId,
237
                        // Entry should be active location entry (original and not alias).
238
                        // Note: this takes care of taking over custom alias entry for the location on the same level
239
                        // and with same name and action.
240
                        'alias_redirects' => 1,
241
                        'is_original' => 1,
242
                        'is_alias' => 0,
243
                    )
244
                );
245
246
                break;
247
            }
248
249
            // If existing row is not reusable, increment $uniqueCounter and try again
250
            $uniqueCounter += 1;
251
        }
252
253
        /* @var $newText */
254
        if ($updatePathIdentificationString) {
255
            $this->locationGateway->updatePathIdentificationString(
256
                $locationId,
257
                $parentLocationId,
258
                $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...
259
            );
260
        }
261
262
        /* @var $newId */
263
        /* @var $newTextMD5 */
264
        // Note: cleanup does not touch custom and global entries
265
        if ($cleanup) {
266
            $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...
267
        }
268
    }
269
270
    /**
271
     * Create a user chosen $alias pointing to $locationId in $languageCode.
272
     *
273
     * If $languageCode is null the $alias is created in the system's default
274
     * language. $alwaysAvailable makes the alias available in all languages.
275
     *
276
     * @param mixed $locationId
277
     * @param string $path
278
     * @param bool $forwarding
279
     * @param string $languageCode
280
     * @param bool $alwaysAvailable
281
     *
282
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
283
     */
284
    public function createCustomUrlAlias($locationId, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
285
    {
286
        return $this->createUrlAlias(
287
            'eznode:' . $locationId,
288
            $path,
289
            $forwarding,
290
            $languageCode,
291
            $alwaysAvailable
292
        );
293
    }
294
295
    /**
296
     * Create a user chosen $alias pointing to a resource in $languageCode.
297
     * This method does not handle location resources - if a user enters a location target
298
     * the createCustomUrlAlias method has to be used.
299
     *
300
     * If $languageCode is null the $alias is created in the system's default
301
     * language. $alwaysAvailable makes the alias available in all languages.
302
     *
303
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException if the path already exists for the given language
304
     *
305
     * @param string $resource
306
     * @param string $path
307
     * @param bool $forwarding
308
     * @param string $languageCode
309
     * @param bool $alwaysAvailable
310
     *
311
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
312
     */
313
    public function createGlobalUrlAlias($resource, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false)
314
    {
315
        return $this->createUrlAlias(
316
            $resource,
317
            $path,
318
            $forwarding,
319
            $languageCode,
320
            $alwaysAvailable
321
        );
322
    }
323
324
    /**
325
     * Internal method for creating global or custom URL alias (these are handled in the same way).
326
     *
327
     * @throws \eZ\Publish\Core\Base\Exceptions\ForbiddenException if the path already exists for the given language
328
     *
329
     * @param string $action
330
     * @param string $path
331
     * @param bool $forward
332
     * @param string|null $languageCode
333
     * @param bool $alwaysAvailable
334
     *
335
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
336
     */
337
    protected function createUrlAlias($action, $path, $forward, $languageCode, $alwaysAvailable)
338
    {
339
        $pathElements = explode('/', $path);
340
        $topElement = array_pop($pathElements);
341
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
342
        $parentId = 0;
343
344
        // Handle all path elements except topmost one
345
        $isPathNew = false;
346
        foreach ($pathElements as $level => $pathElement) {
347
            $pathElement = $this->slugConverter->convert($pathElement, 'noname' . ($level + 1));
348
            $pathElementMD5 = $this->getHash($pathElement);
349
            if (!$isPathNew) {
350
                $row = $this->gateway->loadRow($parentId, $pathElementMD5);
351
                if (empty($row)) {
352
                    $isPathNew = true;
353
                } else {
354
                    $parentId = $row['link'];
355
                }
356
            }
357
358
            if ($isPathNew) {
359
                $parentId = $this->insertNopEntry($parentId, $pathElement, $pathElementMD5);
360
            }
361
        }
362
363
        // Handle topmost path element
364
        $topElement = $this->slugConverter->convert($topElement, 'noname' . (count($pathElements) + 1));
365
366
        // If last (next to topmost) entry parent is special root entry we handle topmost entry as first level entry
367
        // That is why we need to reset $parentId to 0
368
        if ($parentId != 0 && $this->gateway->isRootEntry($parentId)) {
369
            $parentId = 0;
370
        }
371
372
        $topElementMD5 = $this->getHash($topElement);
373
        // Set common values for two cases below
374
        $data = array(
375
            'action' => $action,
376
            'is_alias' => 1,
377
            'alias_redirects' => $forward ? 1 : 0,
378
            'parent' => $parentId,
379
            'text' => $topElement,
380
            'text_md5' => $topElementMD5,
381
            'is_original' => 1,
382
        );
383
        // Try to load topmost element
384
        if (!$isPathNew) {
385
            $row = $this->gateway->loadRow($parentId, $topElementMD5);
386
        }
387
388
        // If nothing was returned perform insert
389
        if ($isPathNew || empty($row)) {
390
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
391
            $id = $this->gateway->insertRow($data);
392
        } elseif ($row['action'] == 'nop:' || $row['is_original'] == 0) {
393
            // Row exists, check if it is reusable. There are 2 cases when this is possible:
394
            // 1. NOP entry
395
            // 2. history entry
396
            $data['lang_mask'] = $languageId | (int)$alwaysAvailable;
397
            // If history is reused move link to id
398
            $data['link'] = $id = $row['id'];
399
            $this->gateway->updateRow(
400
                $parentId,
401
                $topElementMD5,
402
                $data
403
            );
404
        } else {
405
            throw new ForbiddenException("Path '%path%' already exists for the given language", ['%path%' => $path]);
406
        }
407
408
        $data['raw_path_data'] = $this->gateway->loadPathData($id);
409
410
        return $this->mapper->extractUrlAliasFromData($data);
411
    }
412
413
    /**
414
     * Convenience method for inserting nop type row.
415
     *
416
     * @param mixed $parentId
417
     * @param string $text
418
     * @param string $textMD5
419
     *
420
     * @return mixed
421
     */
422
    protected function insertNopEntry($parentId, $text, $textMD5)
423
    {
424
        return $this->gateway->insertRow(
425
            array(
426
                'lang_mask' => 1,
427
                'action' => 'nop:',
428
                'parent' => $parentId,
429
                'text' => $text,
430
                'text_md5' => $textMD5,
431
            )
432
        );
433
    }
434
435
    /**
436
     * List of user generated or autogenerated url entries, pointing to $locationId.
437
     *
438
     * @param mixed $locationId
439
     * @param bool $custom if true the user generated aliases are listed otherwise the autogenerated
440
     *
441
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
442
     */
443 View Code Duplication
    public function listURLAliasesForLocation($locationId, $custom = false)
444
    {
445
        $data = $this->gateway->loadLocationEntries($locationId, $custom);
446
        foreach ($data as &$entry) {
447
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
448
        }
449
450
        return $this->mapper->extractUrlAliasListFromData($data);
451
    }
452
453
    /**
454
     * List global aliases.
455
     *
456
     * @param string|null $languageCode
457
     * @param int $offset
458
     * @param int $limit
459
     *
460
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[]
461
     */
462 View Code Duplication
    public function listGlobalURLAliases($languageCode = null, $offset = 0, $limit = -1)
463
    {
464
        $data = $this->gateway->listGlobalEntries($languageCode, $offset, $limit);
465
        foreach ($data as &$entry) {
466
            $entry['raw_path_data'] = $this->gateway->loadPathData($entry['id']);
467
        }
468
469
        return $this->mapper->extractUrlAliasListFromData($data);
470
    }
471
472
    /**
473
     * Removes url aliases.
474
     *
475
     * Autogenerated aliases are not removed by this method.
476
     *
477
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $urlAliases
478
     *
479
     * @return bool
480
     */
481
    public function removeURLAliases(array $urlAliases)
482
    {
483
        foreach ($urlAliases as $urlAlias) {
484
            if ($urlAlias->isCustom) {
485
                list($parentId, $textMD5) = explode('-', $urlAlias->id);
486
                if (!$this->gateway->removeCustomAlias($parentId, $textMD5)) {
487
                    return false;
488
                }
489
            }
490
        }
491
492
        return true;
493
    }
494
495
    /**
496
     * Looks up a url alias for the given url.
497
     *
498
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
499
     * @throws \RuntimeException
500
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
501
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
502
     *
503
     * @param string $url
504
     *
505
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
506
     */
507
    public function lookup($url)
508
    {
509
        $urlHashes = array();
510
        foreach (explode('/', $url) as $level => $text) {
511
            $urlHashes[$level] = $this->getHash($text);
512
        }
513
514
        $pathDepth = count($urlHashes);
515
        if ($pathDepth > self::MAX_URL_ALIAS_DEPTH_LEVEL) {
516
            throw new InvalidArgumentException('$urlHashes', 'Exceeded maximum depth level of content url alias.');
517
        }
518
519
        $data = $this->gateway->loadUrlAliasData($urlHashes);
520
        if (empty($data)) {
521
            throw new NotFoundException('URLAlias', $url);
522
        }
523
524
        $hierarchyData = array();
525
        $isPathHistory = false;
526
        for ($level = 0; $level < $pathDepth; ++$level) {
527
            $prefix = $level === $pathDepth - 1 ? '' : 'ezurlalias_ml' . $level . '_';
528
            $isPathHistory = $isPathHistory ?: ($data[$prefix . 'link'] != $data[$prefix . 'id']);
529
            $hierarchyData[$level] = array(
530
                'id' => $data[$prefix . 'id'],
531
                'parent' => $data[$prefix . 'parent'],
532
                'action' => $data[$prefix . 'action'],
533
            );
534
        }
535
536
        $data['is_path_history'] = $isPathHistory;
537
        $data['raw_path_data'] = ($data['action_type'] == 'eznode' && !$data['is_alias'])
538
            ? $this->gateway->loadPathDataByHierarchy($hierarchyData)
539
            : $this->gateway->loadPathData($data['id']);
540
541
        return $this->mapper->extractUrlAliasFromData($data);
542
    }
543
544
    /**
545
     * Loads URL alias by given $id.
546
     *
547
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
548
     *
549
     * @param string $id
550
     *
551
     * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias
552
     */
553
    public function loadUrlAlias($id)
554
    {
555
        list($parentId, $textMD5) = explode('-', $id);
556
        $data = $this->gateway->loadRow($parentId, $textMD5);
557
558
        if (empty($data)) {
559
            throw new NotFoundException('URLAlias', $id);
560
        }
561
562
        $data['raw_path_data'] = $this->gateway->loadPathData($data['id']);
563
564
        return $this->mapper->extractUrlAliasFromData($data);
565
    }
566
567
    /**
568
     * Notifies the underlying engine that a location has moved.
569
     *
570
     * This method triggers the change of the autogenerated aliases.
571
     *
572
     * @param mixed $locationId
573
     * @param mixed $oldParentId
574
     * @param mixed $newParentId
575
     */
576
    public function locationMoved($locationId, $oldParentId, $newParentId)
577
    {
578
        // @todo optimize: $newLocationAliasId is already available in self::publishUrlAliasForLocation() as $newId
579
        $newParentLocationAliasId = $this->getRealAliasId($newParentId);
580
        $newLocationAlias = $this->gateway->loadAutogeneratedEntry(
581
            'eznode:' . $locationId,
582
            $newParentLocationAliasId
583
        );
584
585
        $oldParentLocationAliasId = $this->getRealAliasId($oldParentId);
586
        $oldLocationAlias = $this->gateway->loadAutogeneratedEntry(
587
            'eznode:' . $locationId,
588
            $oldParentLocationAliasId
589
        );
590
591
        // Historize alias for old location
592
        $this->gateway->historizeId($oldLocationAlias['id'], $newLocationAlias['id']);
593
        // Reparent subtree of old location to new location
594
        $this->gateway->reparent($oldLocationAlias['id'], $newLocationAlias['id']);
595
    }
596
597
    /**
598
     * Notifies the underlying engine that a location was copied.
599
     *
600
     * This method triggers the creation of the autogenerated aliases for the copied locations
601
     *
602
     * @param mixed $locationId
603
     * @param mixed $newLocationId
604
     * @param mixed $newParentId
605
     */
606
    public function locationCopied($locationId, $newLocationId, $newParentId)
607
    {
608
        $newParentAliasId = $this->getRealAliasId($newLocationId);
609
        $oldParentAliasId = $this->getRealAliasId($locationId);
610
611
        $actionMap = $this->getCopiedLocationsMap($locationId, $newLocationId);
612
613
        $this->copySubtree(
614
            $actionMap,
615
            $oldParentAliasId,
616
            $newParentAliasId
617
        );
618
    }
619
620
    public function locationSwapped($location1Id, $location1ParentId, $location2Id, $location2ParentId)
621
    {
622
        $location1Entries = $this->gateway->loadLocationEntries($location1Id);
623
        $location2Entries = $this->gateway->loadLocationEntries($location2Id);
624
625
        $location1MainLanguageId = $this->gateway->getLocationContentMainLanguageId($location1Id);
626
        $location2MainLanguageId = $this->gateway->getLocationContentMainLanguageId($location2Id);
627
628
        // Load autogenerated entries to find alias ID
629
        $autoLocation1 = $this->gateway->loadAutogeneratedEntry("eznode:{$location1Id}");
630
        $autoLocation2 = $this->gateway->loadAutogeneratedEntry("eznode:{$location2Id}");
631
632
        // Historize everything first to avoid name conflicts in case swapped Locations are siblings
633
        $this->historizeBeforeSwap($location1Entries, $location2Entries);
634
635 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...
636
            $alwaysAvailable = (bool)($row['lang_mask'] & 1);
637
            $languageIds = $this->extractLanguageIdsFromMask($row['lang_mask']);
638
639
            foreach ($languageIds as $languageId) {
640
                $isMainLanguage = $languageId == $location2MainLanguageId;
641
                $this->internalPublishUrlAliasForLocation(
642
                    $location1Id,
643
                    $location1ParentId,
644
                    $row['text'],
645
                    $languageId,
646
                    $isMainLanguage && $alwaysAvailable,
647
                    $isMainLanguage,
648
                    $autoLocation1['id']
649
                );
650
            }
651
        }
652
653 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...
654
            $alwaysAvailable = (bool)($row['lang_mask'] & 1);
655
            $languageIds = $this->extractLanguageIdsFromMask($row['lang_mask']);
656
657
            foreach ($languageIds as $languageId) {
658
                $isMainLanguage = $languageId == $location1MainLanguageId;
659
                $this->internalPublishUrlAliasForLocation(
660
                    $location2Id,
661
                    $location2ParentId,
662
                    $row['text'],
663
                    $languageId,
664
                    $isMainLanguage && $alwaysAvailable,
665
                    $isMainLanguage,
666
                    $autoLocation2['id']
667
                );
668
            }
669
        }
670
    }
671
672
    /**
673
     * Historizes given existing active entries for two swapped Locations.
674
     *
675
     * This should be done before republishing URL aliases, in order to avoid unnecessary
676
     * conflicts when swapped Locations are siblings.
677
     *
678
     * We need to historize everything separately per language (mask), in case the entries
679
     * remain history future publishing reusages need to be able to take them over cleanly.
680
     *
681
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped()
682
     *
683
     * @param array $location1Entries
684
     * @param array $location2Entries
685
     */
686
    private function historizeBeforeSwap($location1Entries, $location2Entries)
687
    {
688
        foreach ($location1Entries as $row) {
689
            $this->gateway->historizeBeforeSwap($row['action'], $row['lang_mask']);
690
        }
691
692
        foreach ($location2Entries as $row) {
693
            $this->gateway->historizeBeforeSwap($row['action'], $row['lang_mask']);
694
        }
695
    }
696
697
    /**
698
     * Extracts every language Ids contained in $languageMask.
699
     *
700
     * @param int $languageMask
701
     *
702
     * @return int[] An array of language IDs
703
     */
704
    private function extractLanguageIdsFromMask($languageMask)
705
    {
706
        $exp = 2;
707
        $languageIds = [];
708
709
        // Decomposition of $languageMask into its binary components.
710
        while ($exp <= $languageMask) {
711
            if ($languageMask & $exp) {
712
                $languageIds[] = $exp;
713
            }
714
715
            $exp *= 2;
716
        }
717
718
        return $languageIds;
719
    }
720
721
    /**
722
     * Returns possibly corrected alias id for given $locationId !! For use as parent id in logic.
723
     *
724
     * First level entries must have parent id set to 0 instead of their parent location alias id.
725
     * There are two cases when alias id needs to be corrected:
726
     * 1) location is special location without URL alias (location with id=1 in standard installation)
727
     * 2) location is site root location, having special root entry in the ezurlalias_ml table (location with id=2
728
     *    in standard installation)
729
     *
730
     * @param mixed $locationId
731
     *
732
     * @return mixed
733
     */
734
    protected function getRealAliasId($locationId)
735
    {
736
        // Absolute root location does have a url alias entry so we can skip lookup
737
        if ($locationId == self::ROOT_LOCATION_ID) {
738
            return 0;
739
        }
740
741
        $data = $this->gateway->loadAutogeneratedEntry('eznode:' . $locationId);
742
743
        // Root entries (URL wise) can return 0 as the returned value is used as parent (parent is 0 for root entries)
744
        if (empty($data) || $data['id'] != 0 && $data['parent'] == 0 && strlen($data['text']) == 0) {
745
            $id = 0;
746
        } else {
747
            $id = $data['id'];
748
        }
749
750
        return $id;
751
    }
752
753
    /**
754
     * Recursively copies aliases from old parent under new parent.
755
     *
756
     * @param array $actionMap
757
     * @param mixed $oldParentAliasId
758
     * @param mixed $newParentAliasId
759
     */
760
    protected function copySubtree($actionMap, $oldParentAliasId, $newParentAliasId)
761
    {
762
        $rows = $this->gateway->loadAutogeneratedEntries($oldParentAliasId);
763
        $newIdsMap = array();
764
        foreach ($rows as $row) {
765
            $oldParentAliasId = $row['id'];
766
767
            // Ensure that same action entries remain grouped by the same id
768
            if (!isset($newIdsMap[$oldParentAliasId])) {
769
                $newIdsMap[$oldParentAliasId] = $this->gateway->getNextId();
770
            }
771
772
            $row['action'] = $actionMap[$row['action']];
773
            $row['parent'] = $newParentAliasId;
774
            $row['id'] = $row['link'] = $newIdsMap[$oldParentAliasId];
775
            $this->gateway->insertRow($row);
776
777
            $this->copySubtree(
778
                $actionMap,
779
                $oldParentAliasId,
780
                $row['id']
781
            );
782
        }
783
    }
784
785
    /**
786
     * @param mixed $oldParentId
787
     * @param mixed $newParentId
788
     *
789
     * @return array
790
     */
791
    protected function getCopiedLocationsMap($oldParentId, $newParentId)
792
    {
793
        $originalLocations = $this->locationGateway->getSubtreeContent($oldParentId);
794
        $copiedLocations = $this->locationGateway->getSubtreeContent($newParentId);
795
796
        $map = array();
797
        foreach ($originalLocations as $index => $originalLocation) {
798
            $map['eznode:' . $originalLocation['node_id']] = 'eznode:' . $copiedLocations[$index]['node_id'];
799
        }
800
801
        return $map;
802
    }
803
804
    /**
805
     * Notifies the underlying engine that a location was deleted or moved to trash.
806
     *
807
     * @param mixed $locationId
808
     */
809
    public function locationDeleted($locationId)
810
    {
811
        $action = 'eznode:' . $locationId;
812
        $entry = $this->gateway->loadAutogeneratedEntry($action);
813
814
        $this->removeSubtree($entry['id'], $action, $entry['is_original']);
815
    }
816
817
    /**
818
     * Notifies the underlying engine that Locations Content Translation was removed.
819
     *
820
     * @param int[] $locationIds all Locations of the Content that got Translation removed
821
     * @param string $languageCode language code of the removed Translation
822
     */
823
    public function translationRemoved(array $locationIds, $languageCode)
824
    {
825
        $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id;
826
827
        $actions = [];
828
        foreach ($locationIds as $locationId) {
829
            $actions[] = 'eznode:' . $locationId;
830
        }
831
        $this->gateway->bulkRemoveTranslation($languageId, $actions);
832
    }
833
834
    /**
835
     * Recursively removes aliases by given $id and $action.
836
     *
837
     * $original parameter is used to limit removal of moved Location aliases to history entries only.
838
     *
839
     * @param mixed $id
840
     * @param string $action
841
     * @param mixed $original
842
     */
843
    protected function removeSubtree($id, $action, $original)
844
    {
845
        // Remove first to avoid unnecessary recursion.
846
        if ($original) {
847
            // If entry is original remove all for action (history and custom entries included).
848
            $this->gateway->remove($action);
849
        } else {
850
            // Else entry is history, so remove only for action with the id.
851
            // This means $id grouped history entries are removed, other history, active autogenerated
852
            // and custom are left alone.
853
            $this->gateway->remove($action, $id);
854
        }
855
856
        // Load all autogenerated for parent $id, including history.
857
        $entries = $this->gateway->loadAutogeneratedEntries($id, true);
858
859
        foreach ($entries as $entry) {
860
            $this->removeSubtree($entry['id'], $entry['action'], $entry['is_original']);
861
        }
862
    }
863
864
    /**
865
     * @param string $text
866
     *
867
     * @return string
868
     */
869
    protected function getHash($text)
870
    {
871
        return md5(mb_strtolower($text, 'UTF-8'));
872
    }
873
874
    /**
875
     * {@inheritdoc}
876
     */
877
    public function archiveUrlAliasesForDeletedTranslations($locationId, $parentLocationId, array $languageCodes)
878
    {
879
        $parentId = $this->getRealAliasId($parentLocationId);
880
881
        // filter removed Translations
882
        $urlAliases = $this->listURLAliasesForLocation($locationId);
883
        $removedLanguages = [];
884
        foreach ($urlAliases as $urlAlias) {
885
            $removedLanguages = array_merge(
886
                $removedLanguages,
887
                array_filter(
888
                    $urlAlias->languageCodes,
889
                    function ($languageCode) use ($languageCodes) {
890
                        return !in_array($languageCode, $languageCodes);
891
                    }
892
                )
893
            );
894
        }
895
896
        if (empty($removedLanguages)) {
897
            return;
898
        }
899
900
        // map languageCodes to their IDs
901
        $languageIds = array_map(
902
            function ($languageCode) {
903
                return $this->languageHandler->loadByLanguageCode($languageCode)->id;
904
            },
905
            $removedLanguages
906
        );
907
908
        $this->gateway->archiveUrlAliasesForDeletedTranslations($locationId, $parentId, $languageIds);
909
    }
910
}
911