Completed
Push — master ( 18c4e8...24e938 )
by André
39:56 queued 23:48
created

Handler::translationRemoved()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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