Completed
Push — 6.7 ( 9d44b5...a6e586 )
by Łukasz
22:35
created

DoctrineDatabase   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 1523
Duplicated Lines 18.12 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
dl 276
loc 1523
rs 0.8
c 0
b 0
f 0
wmc 96
lcom 1
cbo 13

36 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A setTable() 0 4 1
B loadLocationEntries() 11 55 3
B listGlobalEntries() 15 57 3
A isRootEntry() 0 21 2
B cleanupAfterPublish() 0 56 2
A archiveUrlAliasForDeletedTranslation() 0 10 2
A historizeBeforeSwap() 0 37 1
A historize() 0 33 1
A removeTranslation() 26 26 1
A historizeId() 0 39 2
A reparent() 0 24 1
A updateRow() 20 20 1
F insertRow() 0 46 16
B setQueryValues() 0 23 7
A getNextId() 29 29 2
A loadRow() 24 24 1
B loadUrlAliasData() 0 66 5
A loadAutogeneratedEntry() 39 39 2
B loadPathData() 0 49 5
B loadPathDataByHierarchy() 0 65 5
A removeCustomAlias() 0 27 2
A remove() 30 30 2
A loadAutogeneratedEntries() 39 39 2
A getLocationContentMainLanguageId() 0 35 2
B archiveUrlAliasesForDeletedTranslations() 0 42 5
A deleteUrlAliasesWithoutLocation() 0 36 1
A deleteUrlAliasesWithoutParent() 23 23 1
A deleteUrlAliasesWithBrokenLink() 20 20 1
B repairBrokenUrlAliasesForLocation() 0 65 8
A filterOriginalAliases() 0 14 1
A getAllUrlAliasesQuery() 0 14 1
A loadLocationEntriesMatchingMultipleLanguages() 0 26 2
A getIntegerType() 0 9 2
A getUrlAliasesForLocation() 0 26 1
A deleteRow() 0 21 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DoctrineDatabase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DoctrineDatabase, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * File containing the DoctrineDatabase UrlAlias Gateway class.
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\Gateway;
10
11
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
12
use Doctrine\DBAL\Platforms\AbstractPlatform;
13
use eZ\Publish\Core\Base\Exceptions\BadStateException;
14
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
15
use eZ\Publish\Core\Persistence\Database\Query;
16
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator as LanguageMaskGenerator;
17
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway;
18
use RuntimeException;
19
20
/**
21
 * UrlAlias Gateway.
22
 */
23
class DoctrineDatabase extends Gateway
24
{
25
    /**
26
     * 2^30, since PHP_INT_MAX can cause overflows in DB systems, if PHP is run
27
     * on 64 bit systems.
28
     */
29
    const MAX_LIMIT = 1073741824;
30
31
    /**
32
     * Columns of database tables.
33
     *
34
     * @var array
35
     *
36
     * @todo remove after testing
37
     */
38
    protected $columns = array(
39
        'ezurlalias_ml' => array(
40
            'action',
41
            'action_type',
42
            'alias_redirects',
43
            'id',
44
            'is_alias',
45
            'is_original',
46
            'lang_mask',
47
            'link',
48
            'parent',
49
            'text',
50
            'text_md5',
51
        ),
52
    );
53
54
    /**
55
     * Doctrine database handler.
56
     *
57
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
58
     */
59
    protected $dbHandler;
60
61
    /**
62
     * Language mask generator.
63
     *
64
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
65
     */
66
    protected $languageMaskGenerator;
67
68
    /**
69
     * Main URL database table name.
70
     *
71
     * @var string
72
     */
73
    protected $table;
74
75
    /**
76
     * @var \Doctrine\DBAL\Connection
77
     */
78
    private $connection;
79
80
    /**
81
     * Creates a new DoctrineDatabase UrlAlias Gateway.
82
     *
83
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $dbHandler
84
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
85
     */
86
    public function __construct(
87
        DatabaseHandler $dbHandler,
88
        LanguageMaskGenerator $languageMaskGenerator
89
    ) {
90
        $this->dbHandler = $dbHandler;
91
        $this->languageMaskGenerator = $languageMaskGenerator;
92
        $this->table = static::TABLE;
93
        $this->connection = $dbHandler->getConnection();
94
    }
95
96
    public function setTable($name)
97
    {
98
        $this->table = $name;
99
    }
100
101
    /**
102
     * Loads list of aliases by given $locationId.
103
     *
104
     * @param mixed $locationId
105
     * @param bool $custom
106
     * @param mixed $languageId
107
     *
108
     * @return array
109
     */
110
    public function loadLocationEntries($locationId, $custom = false, $languageId = false)
111
    {
112
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
113
        $query = $this->dbHandler->createSelectQuery();
114
        $query->select(
115
            $this->dbHandler->quoteColumn('id'),
116
            $this->dbHandler->quoteColumn('link'),
117
            $this->dbHandler->quoteColumn('is_alias'),
118
            $this->dbHandler->quoteColumn('alias_redirects'),
119
            $this->dbHandler->quoteColumn('lang_mask'),
120
            $this->dbHandler->quoteColumn('is_original'),
121
            $this->dbHandler->quoteColumn('parent'),
122
            $this->dbHandler->quoteColumn('text'),
123
            $this->dbHandler->quoteColumn('text_md5'),
124
            $this->dbHandler->quoteColumn('action')
125
        )->from(
126
            $this->dbHandler->quoteTable($this->table)
127
        )->where(
128
            $query->expr->lAnd(
129
                $query->expr->eq(
130
                    $this->dbHandler->quoteColumn('action'),
131
                    $query->bindValue("eznode:{$locationId}", null, \PDO::PARAM_STR)
132
                ),
133
                $query->expr->eq(
134
                    $this->dbHandler->quoteColumn('is_original'),
135
                    $query->bindValue(1, null, \PDO::PARAM_INT)
136
                ),
137
                $query->expr->eq(
138
                    $this->dbHandler->quoteColumn('is_alias'),
139
                    $query->bindValue(
140
                        $custom ? 1 : 0,
141
                        null,
142
                        \PDO::PARAM_INT
143
                    )
144
                )
145
            )
146
        );
147
148 View Code Duplication
        if ($languageId !== false) {
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...
149
            $query->where(
150
                $query->expr->gt(
151
                    $query->expr->bitAnd(
152
                        $this->dbHandler->quoteColumn('lang_mask'),
153
                        $query->bindValue($languageId, null, \PDO::PARAM_INT)
154
                    ),
155
                    0
156
                )
157
            );
158
        }
159
160
        $statement = $query->prepare();
161
        $statement->execute();
162
163
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
164
    }
165
166
    /**
167
     * Loads paged list of global aliases.
168
     *
169
     * @param string|null $languageCode
170
     * @param int $offset
171
     * @param int $limit
172
     *
173
     * @return array
174
     */
175
    public function listGlobalEntries($languageCode = null, $offset = 0, $limit = -1)
176
    {
177
        $limit = $limit === -1 ? self::MAX_LIMIT : $limit;
178
179
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
180
        $query = $this->dbHandler->createSelectQuery();
181
        $query->select(
182
            $this->dbHandler->quoteColumn('action'),
183
            $this->dbHandler->quoteColumn('id'),
184
            $this->dbHandler->quoteColumn('link'),
185
            $this->dbHandler->quoteColumn('is_alias'),
186
            $this->dbHandler->quoteColumn('alias_redirects'),
187
            $this->dbHandler->quoteColumn('lang_mask'),
188
            $this->dbHandler->quoteColumn('is_original'),
189
            $this->dbHandler->quoteColumn('parent'),
190
            $this->dbHandler->quoteColumn('text_md5')
191
        )->from(
192
            $this->dbHandler->quoteTable($this->table)
193
        )->where(
194
            $query->expr->lAnd(
195
                $query->expr->eq(
196
                    $this->dbHandler->quoteColumn('action_type'),
197
                    $query->bindValue('module', null, \PDO::PARAM_STR)
198
                ),
199
                $query->expr->eq(
200
                    $this->dbHandler->quoteColumn('is_original'),
201
                    $query->bindValue(1, null, \PDO::PARAM_INT)
202
                ),
203
                $query->expr->eq(
204
                    $this->dbHandler->quoteColumn('is_alias'),
205
                    $query->bindValue(1, null, \PDO::PARAM_INT)
206
                )
207
            )
208
        )->limit(
209
            $limit,
210
            $offset
211
        );
212 View Code Duplication
        if (isset($languageCode)) {
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...
213
            $query->where(
214
                $query->expr->gt(
215
                    $query->expr->bitAnd(
216
                        $this->dbHandler->quoteColumn('lang_mask'),
217
                        $query->bindValue(
218
                            $this->languageMaskGenerator->generateLanguageIndicator($languageCode, false),
219
                            null,
220
                            \PDO::PARAM_INT
221
                        )
222
                    ),
223
                    0
224
                )
225
            );
226
        }
227
        $statement = $query->prepare();
228
        $statement->execute();
229
230
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
231
    }
232
233
    /**
234
     * Returns boolean indicating if the row with given $id is special root entry.
235
     *
236
     * Special root entry entry will have parentId=0 and text=''.
237
     * In standard installation this entry will point to location with id=2.
238
     *
239
     * @param mixed $id
240
     *
241
     * @return bool
242
     */
243
    public function isRootEntry($id)
244
    {
245
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
246
        $query = $this->dbHandler->createSelectQuery();
247
        $query->select(
248
            $this->dbHandler->quoteColumn('text'),
249
            $this->dbHandler->quoteColumn('parent')
250
        )->from(
251
            $this->dbHandler->quoteTable($this->table)
252
        )->where(
253
            $query->expr->eq(
254
                $this->dbHandler->quoteColumn('id'),
255
                $query->bindValue($id, null, \PDO::PARAM_INT)
256
            )
257
        );
258
        $statement = $query->prepare();
259
        $statement->execute();
260
        $row = $statement->fetch(\PDO::FETCH_ASSOC);
261
262
        return strlen($row['text']) == 0 && $row['parent'] == 0;
263
    }
264
265
    /**
266
     * Downgrades autogenerated entry matched by given $action and $languageId and negatively matched by
267
     * composite primary key.
268
     *
269
     * If language mask of the found entry is composite (meaning it consists of multiple language ids) given
270
     * $languageId will be removed from mask. Otherwise entry will be marked as history.
271
     *
272
     * @param string $action
273
     * @param mixed $languageId
274
     * @param mixed $newId
275
     * @param mixed $parentId
276
     * @param string $textMD5
277
     */
278
    public function cleanupAfterPublish($action, $languageId, $newId, $parentId, $textMD5)
279
    {
280
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
281
        $query = $this->dbHandler->createSelectQuery();
282
        $query->select(
283
            $this->dbHandler->quoteColumn('parent'),
284
            $this->dbHandler->quoteColumn('text_md5'),
285
            $this->dbHandler->quoteColumn('lang_mask')
286
        )->from(
287
            $this->dbHandler->quoteTable($this->table)
288
        )->where(
289
            $query->expr->lAnd(
290
                // 1) Autogenerated aliases that match action and language...
291
                $query->expr->eq(
292
                    $this->dbHandler->quoteColumn('action'),
293
                    $query->bindValue($action, null, \PDO::PARAM_STR)
294
                ),
295
                $query->expr->eq(
296
                    $this->dbHandler->quoteColumn('is_original'),
297
                    $query->bindValue(1, null, \PDO::PARAM_INT)
298
                ),
299
                $query->expr->eq(
300
                    $this->dbHandler->quoteColumn('is_alias'),
301
                    $query->bindValue(0, null, \PDO::PARAM_INT)
302
                ),
303
                $query->expr->gt(
304
                    $query->expr->bitAnd(
305
                        $this->dbHandler->quoteColumn('lang_mask'),
306
                        $query->bindValue($languageId, null, \PDO::PARAM_INT)
307
                    ),
308
                    0
309
                ),
310
                // 2) ...but not newly published entry
311
                $query->expr->not(
312
                    $query->expr->lAnd(
313
                        $query->expr->eq(
314
                            $this->dbHandler->quoteColumn('parent'),
315
                            $query->bindValue($parentId, null, \PDO::PARAM_INT)
316
                        ),
317
                        $query->expr->eq(
318
                            $this->dbHandler->quoteColumn('text_md5'),
319
                            $query->bindValue($textMD5, null, \PDO::PARAM_STR)
320
                        )
321
                    )
322
                )
323
            )
324
        );
325
326
        $statement = $query->prepare();
327
        $statement->execute();
328
        $row = $statement->fetch(\PDO::FETCH_ASSOC);
329
330
        if (!empty($row)) {
331
            $this->archiveUrlAliasForDeletedTranslation($row['lang_mask'], $languageId, $row['parent'], $row['text_md5'], $newId);
332
        }
333
    }
334
335
    /**
336
     * Archive (remove or historize) obsolete URL aliases (for translations that were removed).
337
     *
338
     * @param int $languageMask all languages bit mask
339
     * @param int $languageId removed language Id
340
     * @param int $parent
341
     * @param string $textMD5 checksum
342
     * @param $linkId
343
     */
344
    private function archiveUrlAliasForDeletedTranslation($languageMask, $languageId, $parent, $textMD5, $linkId)
345
    {
346
        // If language mask is composite (consists of multiple languages) then remove given language from entry
347
        if ($languageMask & ~($languageId | 1)) {
348
            $this->removeTranslation($parent, $textMD5, $languageId);
349
        } else {
350
            // Otherwise mark entry as history
351
            $this->historize($parent, $textMD5, $linkId);
352
        }
353
    }
354
355
    public function historizeBeforeSwap($action, $languageMask)
356
    {
357
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
358
        $query = $this->dbHandler->createUpdateQuery();
359
        $query->update(
360
            $this->dbHandler->quoteTable($this->table)
361
        )->set(
362
            $this->dbHandler->quoteColumn('is_original'),
363
            $query->bindValue(0, null, \PDO::PARAM_INT)
364
        )->set(
365
            $this->dbHandler->quoteColumn('id'),
366
            $query->bindValue(
367
                $this->getNextId(),
368
                null,
369
                \PDO::PARAM_INT
370
            )
371
        )->where(
372
            $query->expr->lAnd(
373
                $query->expr->eq(
374
                    $this->dbHandler->quoteColumn('action'),
375
                    $query->bindValue($action, null, \PDO::PARAM_STR)
376
                ),
377
                $query->expr->eq(
378
                    $this->dbHandler->quoteColumn('is_original'),
379
                    $query->bindValue(1, null, \PDO::PARAM_INT)
380
                ),
381
                $query->expr->gt(
382
                    $query->expr->bitAnd(
383
                        $this->dbHandler->quoteColumn('lang_mask'),
384
                        $query->bindValue($languageMask & ~1, null, \PDO::PARAM_INT)
385
                    ),
386
                    0
387
                )
388
            )
389
        );
390
        $query->prepare()->execute();
391
    }
392
393
    /**
394
     * Updates single row matched by composite primary key.
395
     *
396
     * Sets "is_original" to 0 thus marking entry as history.
397
     *
398
     * Re-links history entries.
399
     *
400
     * When location alias is published we need to check for new history entries created with self::downgrade()
401
     * with the same action and language, update their "link" column with id of the published entry.
402
     * History entry "id" column is moved to next id value so that all active (non-history) entries are kept
403
     * under the same id.
404
     *
405
     * @param int $parentId
406
     * @param string $textMD5
407
     * @param int $newId
408
     */
409
    protected function historize($parentId, $textMD5, $newId)
410
    {
411
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
412
        $query = $this->dbHandler->createUpdateQuery();
413
        $query->update(
414
            $this->dbHandler->quoteTable($this->table)
415
        )->set(
416
            $this->dbHandler->quoteColumn('is_original'),
417
            $query->bindValue(0, null, \PDO::PARAM_INT)
418
        )->set(
419
            $this->dbHandler->quoteColumn('link'),
420
            $query->bindValue($newId, null, \PDO::PARAM_INT)
421
        )->set(
422
            $this->dbHandler->quoteColumn('id'),
423
            $query->bindValue(
424
                $this->getNextId(),
425
                null,
426
                \PDO::PARAM_INT
427
            )
428
        )->where(
429
            $query->expr->lAnd(
430
                $query->expr->eq(
431
                    $this->dbHandler->quoteColumn('parent'),
432
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
433
                ),
434
                $query->expr->eq(
435
                    $this->dbHandler->quoteColumn('text_md5'),
436
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
437
                )
438
            )
439
        );
440
        $query->prepare()->execute();
441
    }
442
443
    /**
444
     * Updates single row data matched by composite primary key.
445
     *
446
     * Removes given $languageId from entry's language mask
447
     *
448
     * @param mixed $parentId
449
     * @param string $textMD5
450
     * @param mixed $languageId
451
     */
452 View Code Duplication
    protected function removeTranslation($parentId, $textMD5, $languageId)
453
    {
454
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
455
        $query = $this->dbHandler->createUpdateQuery();
456
        $query->update(
457
            $this->dbHandler->quoteTable($this->table)
458
        )->set(
459
            $this->dbHandler->quoteColumn('lang_mask'),
460
            $query->expr->bitAnd(
461
                $this->dbHandler->quoteColumn('lang_mask'),
462
                $query->bindValue(~$languageId, null, \PDO::PARAM_INT)
463
            )
464
        )->where(
465
            $query->expr->lAnd(
466
                $query->expr->eq(
467
                    $this->dbHandler->quoteColumn('parent'),
468
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
469
                ),
470
                $query->expr->eq(
471
                    $this->dbHandler->quoteColumn('text_md5'),
472
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
473
                )
474
            )
475
        );
476
        $query->prepare()->execute();
477
    }
478
479
    /**
480
     * Marks all entries with given $id as history entries.
481
     *
482
     * This method is used by Handler::locationMoved(). Each row is separately historized
483
     * because future publishing needs to be able to take over history entries safely.
484
     *
485
     * @param mixed $id
486
     * @param mixed $link
487
     */
488
    public function historizeId($id, $link)
489
    {
490
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
491
        $query = $this->dbHandler->createSelectQuery();
492
        $query->select(
493
            $this->dbHandler->quoteColumn('parent'),
494
            $this->dbHandler->quoteColumn('text_md5')
495
        )->from(
496
            $this->dbHandler->quoteTable($this->table)
497
        )->where(
498
            $query->expr->lAnd(
499
                $query->expr->eq(
500
                    $this->dbHandler->quoteColumn('is_alias'),
501
                    $query->bindValue(0, null, \PDO::PARAM_INT)
502
                ),
503
                $query->expr->eq(
504
                    $this->dbHandler->quoteColumn('is_original'),
505
                    $query->bindValue(1, null, \PDO::PARAM_INT)
506
                ),
507
                $query->expr->eq(
508
                    $this->dbHandler->quoteColumn('action_type'),
509
                    $query->bindValue('eznode', null, \PDO::PARAM_STR)
510
                ),
511
                $query->expr->eq(
512
                    $this->dbHandler->quoteColumn('link'),
513
                    $query->bindValue($id, null, \PDO::PARAM_INT)
514
                )
515
            )
516
        );
517
518
        $statement = $query->prepare();
519
        $statement->execute();
520
521
        $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
522
523
        foreach ($rows as $row) {
524
            $this->historize($row['parent'], $row['text_md5'], $link);
525
        }
526
    }
527
528
    /**
529
     * Updates parent id of autogenerated entries.
530
     *
531
     * Update includes history entries.
532
     *
533
     * @param mixed $oldParentId
534
     * @param mixed $newParentId
535
     */
536
    public function reparent($oldParentId, $newParentId)
537
    {
538
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
539
        $query = $this->dbHandler->createUpdateQuery();
540
        $query->update(
541
            $this->dbHandler->quoteTable($this->table)
542
        )->set(
543
            $this->dbHandler->quoteColumn('parent'),
544
            $query->bindValue($newParentId, null, \PDO::PARAM_INT)
545
        )->where(
546
            $query->expr->lAnd(
547
                $query->expr->eq(
548
                    $this->dbHandler->quoteColumn('is_alias'),
549
                    $query->bindValue(0, null, \PDO::PARAM_INT)
550
                ),
551
                $query->expr->eq(
552
                    $this->dbHandler->quoteColumn('parent'),
553
                    $query->bindValue($oldParentId, null, \PDO::PARAM_INT)
554
                )
555
            )
556
        );
557
558
        $query->prepare()->execute();
559
    }
560
561
    /**
562
     * Updates single row data matched by composite primary key.
563
     *
564
     * Use optional parameter $languageMaskMatch to additionally limit the query match with languages.
565
     *
566
     * @param mixed $parentId
567
     * @param string $textMD5
568
     * @param array $values associative array with column names as keys and column values as values
569
     */
570 View Code Duplication
    public function updateRow($parentId, $textMD5, array $values)
571
    {
572
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
573
        $query = $this->dbHandler->createUpdateQuery();
574
        $query->update($this->dbHandler->quoteTable($this->table));
575
        $this->setQueryValues($query, $values);
576
        $query->where(
577
            $query->expr->lAnd(
578
                $query->expr->eq(
579
                    $this->dbHandler->quoteColumn('parent'),
580
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
581
                ),
582
                $query->expr->eq(
583
                    $this->dbHandler->quoteColumn('text_md5'),
584
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
585
                )
586
            )
587
        );
588
        $query->prepare()->execute();
589
    }
590
591
    /**
592
     * Inserts new row in urlalias_ml table.
593
     *
594
     * @param array $values
595
     *
596
     * @return mixed
597
     */
598
    public function insertRow(array $values)
599
    {
600
        // @todo remove after testing
601
        if (
602
            !isset($values['text']) ||
603
            !isset($values['text_md5']) ||
604
            !isset($values['action']) ||
605
            !isset($values['parent']) ||
606
            !isset($values['lang_mask'])) {
607
            throw new \Exception('value set is incomplete: ' . var_export($values, true) . ", can't execute insert");
608
        }
609
        if (!isset($values['id'])) {
610
            $values['id'] = $this->getNextId();
611
        }
612
        if (!isset($values['link'])) {
613
            $values['link'] = $values['id'];
614
        }
615
        if (!isset($values['is_original'])) {
616
            $values['is_original'] = ($values['id'] == $values['link'] ? 1 : 0);
617
        }
618
        if (!isset($values['is_alias'])) {
619
            $values['is_alias'] = 0;
620
        }
621
        if (!isset($values['alias_redirects'])) {
622
            $values['alias_redirects'] = 0;
623
        }
624
        if (!isset($values['action_type'])) {
625
            if (preg_match('#^(.+):.*#', $values['action'], $matches)) {
626
                $values['action_type'] = $matches[1];
627
            }
628
        }
629
        if ($values['is_alias']) {
630
            $values['is_original'] = 1;
631
        }
632
        if ($values['action'] === 'nop:') {
633
            $values['is_original'] = 0;
634
        }
635
636
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
637
        $query = $this->dbHandler->createInsertQuery();
638
        $query->insertInto($this->dbHandler->quoteTable($this->table));
639
        $this->setQueryValues($query, $values);
640
        $query->prepare()->execute();
641
642
        return $values['id'];
643
    }
644
645
    /**
646
     * Sets value for insert or update query.
647
     *
648
     * @param \eZ\Publish\Core\Persistence\Database\Query|\eZ\Publish\Core\Persistence\Database\InsertQuery|\eZ\Publish\Core\Persistence\Database\UpdateQuery $query
649
     * @param array $values
650
     *
651
     * @throws \Exception
652
     */
653
    protected function setQueryValues(Query $query, $values)
654
    {
655
        foreach ($values as $column => $value) {
656
            // @todo remove after testing
657
            if (!in_array($column, $this->columns['ezurlalias_ml'])) {
658
                throw new \Exception("unknown column '$column' for table 'ezurlalias_ml'");
659
            }
660
            switch ($column) {
661
                case 'text':
662
                case 'action':
663
                case 'text_md5':
664
                case 'action_type':
665
                    $pdoDataType = \PDO::PARAM_STR;
666
                    break;
667
                default:
668
                    $pdoDataType = \PDO::PARAM_INT;
669
            }
670
            $query->set(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\Core\Persistence\Database\Query as the method set() does only exist in the following implementations of said interface: eZ\Publish\Core\Persiste...ine\InsertDoctrineQuery, eZ\Publish\Core\Persiste...ine\UpdateDoctrineQuery.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
671
                $this->dbHandler->quoteColumn($column),
672
                $query->bindValue($value, null, $pdoDataType)
673
            );
674
        }
675
    }
676
677
    /**
678
     * Returns next value for "id" column.
679
     *
680
     * @return mixed
681
     */
682 View Code Duplication
    public function getNextId()
683
    {
684
        $sequence = $this->dbHandler->getSequenceName('ezurlalias_ml_incr', 'id');
685
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
686
        $query = $this->dbHandler->createInsertQuery();
687
        $query->insertInto(
688
            $this->dbHandler->quoteTable('ezurlalias_ml_incr')
689
        );
690
        // ezcDatabase does not abstract the "auto increment id"
691
        // INSERT INTO ezurlalias_ml_incr VALUES(DEFAULT) is not an option due
692
        // to this mysql bug: http://bugs.mysql.com/bug.php?id=42270
693
        // as a result we are forced to check which database is currently used
694
        // to generate the correct SQL query
695
        // see https://jira.ez.no/browse/EZP-20652
696
        if ($this->dbHandler->useSequences()) {
697
            $query->set(
698
                $this->dbHandler->quoteColumn('id'),
699
                "nextval('{$sequence}')"
700
            );
701
        } else {
702
            $query->set(
703
                $this->dbHandler->quoteColumn('id'),
704
                $query->bindValue(null, null, \PDO::PARAM_NULL)
705
            );
706
        }
707
        $query->prepare()->execute();
708
709
        return $this->dbHandler->lastInsertId($sequence);
710
    }
711
712
    /**
713
     * Loads single row data matched by composite primary key.
714
     *
715
     * @param mixed $parentId
716
     * @param string $textMD5
717
     *
718
     * @return array
719
     */
720 View Code Duplication
    public function loadRow($parentId, $textMD5)
721
    {
722
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
723
        $query = $this->dbHandler->createSelectQuery();
724
        $query->select('*')->from(
725
            $this->dbHandler->quoteTable($this->table)
726
        )->where(
727
            $query->expr->lAnd(
728
                $query->expr->eq(
729
                    $this->dbHandler->quoteColumn('parent'),
730
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
731
                ),
732
                $query->expr->eq(
733
                    $this->dbHandler->quoteColumn('text_md5'),
734
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
735
                )
736
            )
737
        );
738
739
        $statement = $query->prepare();
740
        $statement->execute();
741
742
        return $statement->fetch(\PDO::FETCH_ASSOC);
743
    }
744
745
    /**
746
     * Loads complete URL alias data by given array of path hashes.
747
     *
748
     * @param string[] $urlHashes URL string hashes
749
     *
750
     * @return array
751
     */
752
    public function loadUrlAliasData(array $urlHashes)
753
    {
754
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
755
        $query = $this->dbHandler->createSelectQuery();
756
757
        $count = count($urlHashes);
758
        foreach ($urlHashes as $level => $urlPartHash) {
759
            $tableName = $this->table . ($level === $count - 1 ? '' : $level);
760
761
            if ($level === $count - 1) {
762
                $query->select(
763
                    $this->dbHandler->quoteColumn('id', $tableName),
764
                    $this->dbHandler->quoteColumn('link', $tableName),
765
                    $this->dbHandler->quoteColumn('is_alias', $tableName),
766
                    $this->dbHandler->quoteColumn('alias_redirects', $tableName),
767
                    $this->dbHandler->quoteColumn('is_original', $tableName),
768
                    $this->dbHandler->quoteColumn('action', $tableName),
769
                    $this->dbHandler->quoteColumn('action_type', $tableName),
770
                    $this->dbHandler->quoteColumn('lang_mask', $tableName),
771
                    $this->dbHandler->quoteColumn('text', $tableName),
772
                    $this->dbHandler->quoteColumn('parent', $tableName),
773
                    $this->dbHandler->quoteColumn('text_md5', $tableName)
774
                )->from(
775
                    $this->dbHandler->quoteTable($this->table)
776
                );
777
            } else {
778
                $query->select(
779
                    $this->dbHandler->aliasedColumn($query, 'id', $tableName),
780
                    $this->dbHandler->aliasedColumn($query, 'link', $tableName),
781
                    $this->dbHandler->aliasedColumn($query, 'is_alias', $tableName),
782
                    $this->dbHandler->aliasedColumn($query, 'alias_redirects', $tableName),
783
                    $this->dbHandler->aliasedColumn($query, 'is_original', $tableName),
784
                    $this->dbHandler->aliasedColumn($query, 'action', $tableName),
785
                    $this->dbHandler->aliasedColumn($query, 'action_type', $tableName),
786
                    $this->dbHandler->aliasedColumn($query, 'lang_mask', $tableName),
787
                    $this->dbHandler->aliasedColumn($query, 'text', $tableName),
788
                    $this->dbHandler->aliasedColumn($query, 'parent', $tableName),
789
                    $this->dbHandler->aliasedColumn($query, 'text_md5', $tableName)
790
                )->from(
791
                    $query->alias($this->table, $tableName)
792
                );
793
            }
794
795
            $query->where(
796
                $query->expr->lAnd(
797
                    $query->expr->eq(
798
                        $this->dbHandler->quoteColumn('text_md5', $tableName),
799
                        $query->bindValue($urlPartHash, null, \PDO::PARAM_STR)
800
                    ),
801
                    $query->expr->eq(
802
                        $this->dbHandler->quoteColumn('parent', $tableName),
803
                        // root entry has parent column set to 0
804
                        isset($previousTableName) ? $this->dbHandler->quoteColumn('link', $previousTableName) : $query->bindValue(0, null, \PDO::PARAM_INT)
805
                    )
806
                )
807
            );
808
809
            $previousTableName = $tableName;
810
        }
811
        $query->limit(1);
812
813
        $statement = $query->prepare();
814
        $statement->execute();
815
816
        return $statement->fetch(\PDO::FETCH_ASSOC);
817
    }
818
819
    /**
820
     * Loads autogenerated entry id by given $action and optionally $parentId.
821
     *
822
     * @param string $action
823
     * @param mixed|null $parentId
824
     *
825
     * @return array
826
     */
827 View Code Duplication
    public function loadAutogeneratedEntry($action, $parentId = null)
828
    {
829
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
830
        $query = $this->dbHandler->createSelectQuery();
831
        $query->select(
832
            '*'
833
        )->from(
834
            $this->dbHandler->quoteTable($this->table)
835
        )->where(
836
            $query->expr->lAnd(
837
                $query->expr->eq(
838
                    $this->dbHandler->quoteColumn('action'),
839
                    $query->bindValue($action, null, \PDO::PARAM_STR)
840
                ),
841
                $query->expr->eq(
842
                    $this->dbHandler->quoteColumn('is_original'),
843
                    $query->bindValue(1, null, \PDO::PARAM_INT)
844
                ),
845
                $query->expr->eq(
846
                    $this->dbHandler->quoteColumn('is_alias'),
847
                    $query->bindValue(0, null, \PDO::PARAM_INT)
848
                )
849
            )
850
        );
851
852
        if (isset($parentId)) {
853
            $query->where(
854
                $query->expr->eq(
855
                    $this->dbHandler->quoteColumn('parent'),
856
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
857
                )
858
            );
859
        }
860
861
        $statement = $query->prepare();
862
        $statement->execute();
863
864
        return $statement->fetch(\PDO::FETCH_ASSOC);
865
    }
866
867
    /**
868
     * Loads all data for the path identified by given $id.
869
     *
870
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
871
     *
872
     * @param int $id
873
     *
874
     * @return array
875
     */
876
    public function loadPathData($id)
877
    {
878
        $pathData = array();
879
880
        while ($id != 0) {
881
            /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
882
            $query = $this->dbHandler->createSelectQuery();
883
            $query->select(
884
                $this->dbHandler->quoteColumn('parent'),
885
                $this->dbHandler->quoteColumn('lang_mask'),
886
                $this->dbHandler->quoteColumn('text')
887
            )->from(
888
                $this->dbHandler->quoteTable($this->table)
889
            )->where(
890
                $query->expr->eq(
891
                    $this->dbHandler->quoteColumn('id'),
892
                    $query->bindValue($id, null, \PDO::PARAM_INT)
893
                )
894
            );
895
896
            $statement = $query->prepare();
897
            $statement->execute();
898
899
            $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
900
            if (empty($rows)) {
901
                // Normally this should never happen
902
                $pathDataArray = [];
903
                foreach ($pathData as $path) {
904
                    if (!isset($path[0]['text'])) {
905
                        continue;
906
                    }
907
908
                    $pathDataArray[] = $path[0]['text'];
909
                }
910
911
                $path = implode('/', $pathDataArray);
912
                throw new BadStateException(
913
                    'id',
914
                    "Unable to load path data, the path ...'{$path}' is broken, alias id '{$id}' not found. " .
915
                    'To fix all broken paths run the ezplatform:urls:regenerate-aliases command'
916
                );
917
            }
918
919
            $id = $rows[0]['parent'];
920
            array_unshift($pathData, $rows);
921
        }
922
923
        return $pathData;
924
    }
925
926
    /**
927
     * Loads path data identified by given ordered array of hierarchy data.
928
     *
929
     * The first entry in $hierarchyData corresponds to the top-most path element in the path, the second entry the
930
     * child of the first path element and so on.
931
     * This method is faster than self::getPath() since it can fetch all elements using only one query, but can be used
932
     * only for autogenerated paths.
933
     *
934
     * @param array $hierarchyData
935
     *
936
     * @return array
937
     */
938
    public function loadPathDataByHierarchy(array $hierarchyData)
939
    {
940
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
941
        $query = $this->dbHandler->createSelectQuery();
942
943
        $hierarchyConditions = array();
944
        foreach ($hierarchyData as $levelData) {
945
            $hierarchyConditions[] = $query->expr->lAnd(
946
                $query->expr->eq(
947
                    $this->dbHandler->quoteColumn('parent'),
948
                    $query->bindValue(
949
                        $levelData['parent'],
950
                        null,
951
                        \PDO::PARAM_INT
952
                    )
953
                ),
954
                $query->expr->eq(
955
                    $this->dbHandler->quoteColumn('action'),
956
                    $query->bindValue(
957
                        $levelData['action'],
958
                        null,
959
                        \PDO::PARAM_STR
960
                    )
961
                ),
962
                $query->expr->eq(
963
                    $this->dbHandler->quoteColumn('id'),
964
                    $query->bindValue(
965
                        $levelData['id'],
966
                        null,
967
                        \PDO::PARAM_INT
968
                    )
969
                )
970
            );
971
        }
972
973
        $query->select(
974
            $this->dbHandler->quoteColumn('action'),
975
            $this->dbHandler->quoteColumn('lang_mask'),
976
            $this->dbHandler->quoteColumn('text')
977
        )->from(
978
            $this->dbHandler->quoteTable($this->table)
979
        )->where(
980
            $query->expr->lOr($hierarchyConditions)
981
        );
982
983
        $statement = $query->prepare();
984
        $statement->execute();
985
986
        $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
987
        $rowsMap = array();
988
        foreach ($rows as $row) {
989
            $rowsMap[$row['action']][] = $row;
990
        }
991
992
        if (count($rowsMap) !== count($hierarchyData)) {
993
            throw new \RuntimeException('The path is corrupted.');
994
        }
995
996
        $data = array();
997
        foreach ($hierarchyData as $levelData) {
998
            $data[] = $rowsMap[$levelData['action']];
999
        }
1000
1001
        return $data;
1002
    }
1003
1004
    /**
1005
     * Deletes single custom alias row matched by composite primary key.
1006
     *
1007
     * @param mixed $parentId
1008
     * @param string $textMD5
1009
     *
1010
     * @return bool
1011
     */
1012
    public function removeCustomAlias($parentId, $textMD5)
1013
    {
1014
        /** @var $query \eZ\Publish\Core\Persistence\Database\DeleteQuery */
1015
        $query = $this->dbHandler->createDeleteQuery();
1016
        $query->deleteFrom(
1017
            $this->dbHandler->quoteTable($this->table)
1018
        )->where(
1019
            $query->expr->lAnd(
1020
                $query->expr->eq(
1021
                    $this->dbHandler->quoteColumn('parent'),
1022
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
1023
                ),
1024
                $query->expr->eq(
1025
                    $this->dbHandler->quoteColumn('text_md5'),
1026
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
1027
                ),
1028
                $query->expr->eq(
1029
                    $this->dbHandler->quoteColumn('is_alias'),
1030
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1031
                )
1032
            )
1033
        );
1034
        $statement = $query->prepare();
1035
        $statement->execute();
1036
1037
        return $statement->rowCount() === 1 ?: false;
1038
    }
1039
1040
    /**
1041
     * Deletes all rows with given $action and optionally $id.
1042
     *
1043
     * If $id is set only autogenerated entries will be removed.
1044
     *
1045
     * @param mixed $action
1046
     * @param mixed|null $id
1047
     *
1048
     * @return bool
1049
     */
1050 View Code Duplication
    public function remove($action, $id = null)
1051
    {
1052
        /** @var $query \eZ\Publish\Core\Persistence\Database\DeleteQuery */
1053
        $query = $this->dbHandler->createDeleteQuery();
1054
        $query->deleteFrom(
1055
            $this->dbHandler->quoteTable($this->table)
1056
        )->where(
1057
            $query->expr->eq(
1058
                $this->dbHandler->quoteColumn('action'),
1059
                $query->bindValue($action, null, \PDO::PARAM_STR)
1060
            )
1061
        );
1062
1063
        if ($id !== null) {
1064
            $query->where(
1065
                $query->expr->lAnd(
1066
                    $query->expr->eq(
1067
                        $this->dbHandler->quoteColumn('is_alias'),
1068
                        $query->bindValue(0, null, \PDO::PARAM_INT)
1069
                    ),
1070
                    $query->expr->eq(
1071
                        $this->dbHandler->quoteColumn('id'),
1072
                        $query->bindValue($id, null, \PDO::PARAM_INT)
1073
                    )
1074
                )
1075
            );
1076
        }
1077
1078
        $query->prepare()->execute();
1079
    }
1080
1081
    /**
1082
     * Loads all autogenerated entries with given $parentId with optionally included history entries.
1083
     *
1084
     * @param mixed $parentId
1085
     * @param bool $includeHistory
1086
     *
1087
     * @return array
1088
     */
1089 View Code Duplication
    public function loadAutogeneratedEntries($parentId, $includeHistory = false)
1090
    {
1091
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
1092
        $query = $this->dbHandler->createSelectQuery();
1093
        $query->select(
1094
            '*'
1095
        )->from(
1096
            $this->dbHandler->quoteTable($this->table)
1097
        )->where(
1098
            $query->expr->lAnd(
1099
                $query->expr->eq(
1100
                    $this->dbHandler->quoteColumn('parent'),
1101
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
1102
                ),
1103
                $query->expr->eq(
1104
                    $this->dbHandler->quoteColumn('action_type'),
1105
                    $query->bindValue('eznode', null, \PDO::PARAM_STR)
1106
                ),
1107
                $query->expr->eq(
1108
                    $this->dbHandler->quoteColumn('is_alias'),
1109
                    $query->bindValue(0, null, \PDO::PARAM_INT)
1110
                )
1111
            )
1112
        );
1113
1114
        if (!$includeHistory) {
1115
            $query->where(
1116
                $query->expr->eq(
1117
                    $this->dbHandler->quoteColumn('is_original'),
1118
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1119
                )
1120
            );
1121
        }
1122
1123
        $statement = $query->prepare();
1124
        $statement->execute();
1125
1126
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1127
    }
1128
1129
    public function getLocationContentMainLanguageId($locationId)
1130
    {
1131
        $dbHandler = $this->dbHandler;
1132
        $query = $dbHandler->createSelectQuery();
1133
        $query
1134
            ->select($dbHandler->quoteColumn('initial_language_id', 'ezcontentobject'))
1135
            ->from($dbHandler->quoteTable('ezcontentobject'))
1136
            ->innerJoin(
1137
                $dbHandler->quoteTable('ezcontentobject_tree'),
1138
                $query->expr->lAnd(
1139
                    $query->expr->eq(
1140
                        $dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_tree'),
1141
                        $dbHandler->quoteColumn('id', 'ezcontentobject')
1142
                    ),
1143
                    $query->expr->eq(
1144
                        $dbHandler->quoteColumn('node_id', 'ezcontentobject_tree'),
1145
                        $dbHandler->quoteColumn('main_node_id', 'ezcontentobject_tree')
1146
                    ),
1147
                    $query->expr->eq(
1148
                        $dbHandler->quoteColumn('node_id', 'ezcontentobject_tree'),
1149
                        $query->bindValue($locationId, null, \PDO::PARAM_INT)
1150
                    )
1151
                )
1152
            );
1153
1154
        $statement = $query->prepare();
1155
        $statement->execute();
1156
        $languageId = $statement->fetchColumn();
1157
1158
        if ($languageId === false) {
1159
            throw new RuntimeException("Could not find Content for Location #{$locationId}");
1160
        }
1161
1162
        return $languageId;
1163
    }
1164
1165
    /**
1166
     * Archive (remove or historize) URL aliases for removed Translations.
1167
     *
1168
     * @param int $locationId
1169
     * @param int $parentId
1170
     * @param int[] $languageIds Language IDs of removed Translations
1171
     */
1172
    public function archiveUrlAliasesForDeletedTranslations($locationId, $parentId, array $languageIds)
1173
    {
1174
        // determine proper parent for linking historized entry
1175
        $existingLocationEntry = $this->loadAutogeneratedEntry(
1176
            'eznode:' . $locationId,
1177
            $parentId
1178
        );
1179
1180
        // filter existing URL alias entries by any of the specified removed languages
1181
        $rows = $this->loadLocationEntriesMatchingMultipleLanguages(
1182
            $locationId,
1183
            $languageIds
1184
        );
1185
1186
        // remove specific languages from a bit mask
1187
        foreach ($rows as $row) {
1188
            // filter mask to reduce the number of calls to storage engine
1189
            $rowLanguageMask = (int) $row['lang_mask'];
1190
            $languageIdsToBeRemoved = array_filter(
1191
                $languageIds,
1192
                function ($languageId) use ($rowLanguageMask) {
1193
                    return $languageId & $rowLanguageMask;
1194
                }
1195
            );
1196
1197
            if (empty($languageIdsToBeRemoved)) {
1198
                continue;
1199
            }
1200
1201
            // use existing entry to link archived alias or use current alias id
1202
            $linkToId = !empty($existingLocationEntry) ? $existingLocationEntry['id'] : $row['id'];
1203
            foreach ($languageIdsToBeRemoved as $languageId) {
1204
                $this->archiveUrlAliasForDeletedTranslation(
1205
                    $row['lang_mask'],
1206
                    $languageId,
1207
                    $row['parent'],
1208
                    $row['text_md5'],
1209
                    $linkToId
1210
                );
1211
            }
1212
        }
1213
    }
1214
1215
    /**
1216
     * Delete URL aliases pointing to non-existent Locations.
1217
     *
1218
     * @return int Number of affected rows.
1219
     *
1220
     * @throws \Doctrine\DBAL\DBALException
1221
     */
1222
    public function deleteUrlAliasesWithoutLocation()
1223
    {
1224
        $dbPlatform = $this->connection->getDatabasePlatform();
1225
1226
        $subquery = $this->connection->createQueryBuilder();
1227
        $subquery
1228
            ->select('node_id')
1229
            ->from('ezcontentobject_tree', 't')
1230
            ->where(
1231
                $subquery->expr()->eq(
1232
                    't.node_id',
1233
                    sprintf(
1234
                        'CAST(%s as %s)',
1235
                        $dbPlatform->getSubstringExpression($this->table . '.action', 8),
1236
                        $this->getIntegerType($dbPlatform)
1237
                    )
1238
                )
1239
            )
1240
        ;
1241
1242
        $deleteQuery = $this->connection->createQueryBuilder();
1243
        $deleteQuery
1244
            ->delete($this->table)
1245
            ->where(
1246
                $deleteQuery->expr()->eq(
1247
                    'action_type',
1248
                    $deleteQuery->createPositionalParameter('eznode')
1249
                )
1250
            )
1251
            ->andWhere(
1252
                sprintf('NOT EXISTS (%s)', $subquery->getSQL())
1253
            )
1254
        ;
1255
1256
        return $deleteQuery->execute();
0 ignored issues
show
Bug Compatibility introduced by
The expression $deleteQuery->execute(); of type Doctrine\DBAL\Driver\ResultStatement|integer adds the type Doctrine\DBAL\Driver\ResultStatement to the return on line 1256 which is incompatible with the return type declared by the abstract method eZ\Publish\Core\Persiste...lAliasesWithoutLocation of type integer.
Loading history...
1257
    }
1258
1259
    /**
1260
     * Delete URL aliases pointing to non-existent parent nodes.
1261
     *
1262
     * @return int Number of affected rows.
1263
     */
1264 View Code Duplication
    public function deleteUrlAliasesWithoutParent()
1265
    {
1266
        $existingAliasesQuery = $this->getAllUrlAliasesQuery();
1267
1268
        $query = $this->connection->createQueryBuilder();
1269
        $query
1270
            ->delete($this->table)
1271
            ->where(
1272
                $query->expr()->neq(
1273
                    'parent',
1274
                    $query->createPositionalParameter(0, \PDO::PARAM_INT)
1275
                )
1276
            )
1277
            ->andWhere(
1278
                $query->expr()->notIn(
1279
                    'parent',
1280
                    $existingAliasesQuery
1281
                )
1282
            )
1283
        ;
1284
1285
        return $query->execute();
0 ignored issues
show
Bug Compatibility introduced by
The expression $query->execute(); of type Doctrine\DBAL\Driver\ResultStatement|integer adds the type Doctrine\DBAL\Driver\ResultStatement to the return on line 1285 which is incompatible with the return type declared by the abstract method eZ\Publish\Core\Persiste...UrlAliasesWithoutParent of type integer.
Loading history...
1286
    }
1287
1288
    /**
1289
     * Delete URL aliases which do not link to any existing URL alias node.
1290
     *
1291
     * Note: Typically link column value is used to determine original alias for an archived entries.
1292
     */
1293 View Code Duplication
    public function deleteUrlAliasesWithBrokenLink()
1294
    {
1295
        $existingAliasesQuery = $this->getAllUrlAliasesQuery();
1296
1297
        $query = $this->connection->createQueryBuilder();
1298
        $query
1299
            ->delete($this->table)
1300
            ->where(
1301
                $query->expr()->neq('id', 'link')
1302
            )
1303
            ->andWhere(
1304
                $query->expr()->notIn(
1305
                    'link',
1306
                    $existingAliasesQuery
1307
                )
1308
            )
1309
        ;
1310
1311
        return $query->execute();
1312
    }
1313
1314
    /**
1315
     * Attempt repairing data corruption for broken archived URL aliases for Location,
1316
     * assuming there exists restored original (current) entry.
1317
     *
1318
     * @param int $locationId
1319
     */
1320
    public function repairBrokenUrlAliasesForLocation($locationId)
1321
    {
1322
        $urlAliasesData = $this->getUrlAliasesForLocation($locationId);
1323
1324
        $originalUrlAliases = $this->filterOriginalAliases($urlAliasesData);
1325
1326
        if (count($originalUrlAliases) === count($urlAliasesData)) {
1327
            // no archived aliases - nothing to fix
1328
            return;
1329
        }
1330
1331
        $updateQueryBuilder = $this->connection->createQueryBuilder();
1332
        $expr = $updateQueryBuilder->expr();
1333
        $updateQueryBuilder
1334
            ->update('ezurlalias_ml')
1335
            ->set('link', ':linkId')
1336
            ->set('parent', ':newParentId')
1337
            ->where(
1338
                $expr->eq('action', ':action')
1339
            )
1340
            ->andWhere(
1341
                $expr->eq(
1342
                    'is_original',
1343
                    $updateQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1344
                )
1345
            )
1346
            ->andWhere(
1347
                $expr->eq('parent', ':oldParentId')
1348
            )
1349
            ->andWhere(
1350
                $expr->eq('text_md5', ':textMD5')
1351
            )
1352
            ->setParameter(':action', "eznode:{$locationId}");
1353
1354
        foreach ($urlAliasesData as $urlAliasData) {
1355
            if ($urlAliasData['is_original'] === 1 || !isset($originalUrlAliases[$urlAliasData['lang_mask']])) {
1356
                // ignore non-archived entries and deleted Translations
1357
                continue;
1358
            }
1359
1360
            $originalUrlAlias = $originalUrlAliases[$urlAliasData['lang_mask']];
1361
1362
            if ($urlAliasData['link'] === $originalUrlAlias['link']) {
1363
                // ignore correct entries to avoid unnecessary updates
1364
                continue;
1365
            }
1366
1367
            $updateQueryBuilder
1368
                ->setParameter(':linkId', $originalUrlAlias['link'], \PDO::PARAM_INT)
1369
                // attempt to fix missing parent case
1370
                ->setParameter(
1371
                    ':newParentId',
1372
                    null !== $urlAliasData['existing_parent'] ? $urlAliasData['existing_parent'] : $originalUrlAlias['parent']
1373
                )
1374
                ->setParameter(':oldParentId', $urlAliasData['parent'], \PDO::PARAM_INT)
1375
                ->setParameter(':textMD5', $urlAliasData['text_md5']);
1376
1377
            try {
1378
                $updateQueryBuilder->execute();
1379
            } catch (UniqueConstraintViolationException $e) {
1380
                // edge case: if such row already exists, there's no way to restore history
1381
                $this->deleteRow($urlAliasData['parent'], $urlAliasData['text_md5']);
1382
            }
1383
        }
1384
    }
1385
1386
    /**
1387
     * Filter from the given result set original (current) only URL aliases and index them by language_mask.
1388
     *
1389
     * Note: each language_mask can have one URL Alias.
1390
     *
1391
     * @param array $urlAliasesData
1392
     *
1393
     * @return array
1394
     */
1395
    private function filterOriginalAliases(array $urlAliasesData)
1396
    {
1397
        $originalUrlAliases = array_filter(
1398
            $urlAliasesData,
1399
            function ($urlAliasData) {
1400
                return (int)$urlAliasData['is_original'] === 1;
1401
            }
1402
        );
1403
        // return language_mask-indexed array
1404
        return array_combine(
1405
            array_column($originalUrlAliases, 'lang_mask'),
1406
            $originalUrlAliases
1407
        );
1408
    }
1409
1410
    /**
1411
     * Get subquery for IDs of all URL aliases.
1412
     *
1413
     * @return string Query
1414
     */
1415
    private function getAllUrlAliasesQuery()
1416
    {
1417
        $existingAliasesQueryBuilder = $this->connection->createQueryBuilder();
1418
        $innerQueryBuilder = $this->connection->createQueryBuilder();
1419
1420
        return $existingAliasesQueryBuilder
1421
            ->select('tmp.id')
1422
            ->from(
1423
                // nest subquery to avoid same-table update error
1424
                '(' . $innerQueryBuilder->select('id')->from($this->table)->getSQL() . ')',
1425
                'tmp'
1426
            )
1427
            ->getSQL();
1428
    }
1429
1430
    /**
1431
     * Load list of aliases for given $locationId matching any of the Languages specified by $languageMask.
1432
     *
1433
     * @param int $locationId
1434
     * @param int[] $languageIds
1435
     *
1436
     * @return array[]
1437
     */
1438
    private function loadLocationEntriesMatchingMultipleLanguages($locationId, array $languageIds)
1439
    {
1440
        // note: alwaysAvailable for this use case is not relevant
1441
        $languageMask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageIds(
1442
            $languageIds,
1443
            false
1444
        );
1445
1446
        /** @var \Doctrine\DBAL\Connection $connection */
1447
        $connection = $this->dbHandler->getConnection();
1448
        $query = $connection->createQueryBuilder();
1449
        $query
1450
            ->select('id', 'lang_mask', 'parent', 'text_md5')
1451
            ->from($this->table)
1452
            ->where('action = :action')
1453
            // fetch rows matching any of the given Languages
1454
            ->andWhere('lang_mask & :languageMask <> 0')
1455
            ->setParameter(':action', 'eznode:' . $locationId)
1456
            ->setParameter(':languageMask', $languageMask)
1457
        ;
1458
1459
        $statement = $query->execute();
1460
        $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
1461
1462
        return $rows ?: [];
1463
    }
1464
1465
    /**
1466
     * Get DBMS-specific integer type.
1467
     *
1468
     * @param \Doctrine\DBAL\Platforms\AbstractPlatform $databasePlatform
1469
     *
1470
     * @return string
1471
     */
1472
    private function getIntegerType(AbstractPlatform $databasePlatform)
1473
    {
1474
        switch ($databasePlatform->getName()) {
1475
            case 'mysql':
1476
                return 'signed';
1477
            default:
1478
                return 'integer';
1479
        }
1480
    }
1481
1482
    /**
1483
     * Get all URL aliases for the given Location (including archived ones).
1484
     *
1485
     * @param int $locationId
1486
     *
1487
     * @return array
1488
     */
1489
    protected function getUrlAliasesForLocation($locationId)
1490
    {
1491
        $queryBuilder = $this->connection->createQueryBuilder();
1492
        $queryBuilder
1493
            ->select(
1494
                't1.id',
1495
                't1.is_original',
1496
                't1.lang_mask',
1497
                't1.link',
1498
                't1.parent',
1499
                // show existing parent only if its row exists, special case for root parent
1500
                'CASE t1.parent WHEN 0 THEN 0 ELSE t2.id END AS existing_parent',
1501
                't1.text_md5'
1502
            )
1503
            ->from($this->table, 't1')
1504
            // selecting t2.id above will result in null if parent is broken
1505
            ->leftJoin('t1', $this->table, 't2', $queryBuilder->expr()->eq('t1.parent', 't2.id'))
1506
            ->where(
1507
                $queryBuilder->expr()->eq(
1508
                    't1.action',
1509
                    $queryBuilder->createPositionalParameter("eznode:{$locationId}")
1510
                )
1511
            );
1512
1513
        return $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1514
    }
1515
1516
    /**
1517
     * Delete URL alias row by its primary composite key.
1518
     *
1519
     * @param int $parentId
1520
     * @param string $textMD5
1521
     *
1522
     * @return int number of affected rows
1523
     */
1524
    private function deleteRow($parentId, $textMD5)
1525
    {
1526
        $queryBuilder = $this->connection->createQueryBuilder();
1527
        $expr = $queryBuilder->expr();
1528
        $queryBuilder
1529
            ->delete($this->table)
1530
            ->where(
1531
                $expr->andX(
1532
                    $expr->eq(
1533
                        'parent',
1534
                        $queryBuilder->createPositionalParameter($parentId, \PDO::PARAM_INT)
1535
                    ),
1536
                    $expr->eq(
1537
                        'text_md5',
1538
                        $queryBuilder->createPositionalParameter($textMD5)
1539
                    )
1540
                )
1541
            );
1542
1543
        return $queryBuilder->execute();
1544
    }
1545
}
1546