Completed
Push — EZP-26146-location-swap-urlali... ( 834e22...8f1bf0 )
by
unknown
88:59 queued 65:57
created

Handler::historizeBeforeSwap()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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