Completed
Push — EZP-26146-location-swap-urlali... ( 03f4c9...c13c1a )
by
unknown
29:41
created

Handler::cleanupAfterSwap()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 15
nc 3
nop 4
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0

1 Method

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