Completed
Push — EZP-31112-custom-aliases-gone-... ( 9e7d59 )
by
unknown
21:19
created

archiveUrlAliasForDeletedTranslation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 5
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
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\Connection;
12
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
13
use Doctrine\DBAL\FetchMode;
14
use Doctrine\DBAL\ParameterType;
15
use Doctrine\DBAL\Platforms\AbstractPlatform;
16
use eZ\Publish\Core\Base\Exceptions\BadStateException;
17
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
18
use eZ\Publish\Core\Persistence\Database\Query;
19
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator as LanguageMaskGenerator;
20
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway;
21
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Language;
22
use RuntimeException;
23
use PDO;
24
25
/**
26
 * UrlAlias Gateway.
27
 */
28
class DoctrineDatabase extends Gateway
29
{
30
    /**
31
     * 2^30, since PHP_INT_MAX can cause overflows in DB systems, if PHP is run
32
     * on 64 bit systems.
33
     */
34
    const MAX_LIMIT = 1073741824;
35
36
    /**
37
     * Columns of database tables.
38
     *
39
     * @var array
40
     *
41
     * @todo remove after testing
42
     */
43
    protected $columns = [
44
        'ezurlalias_ml' => [
45
            'action',
46
            'action_type',
47
            'alias_redirects',
48
            'id',
49
            'is_alias',
50
            'is_original',
51
            'lang_mask',
52
            'link',
53
            'parent',
54
            'text',
55
            'text_md5',
56
        ],
57
    ];
58
59
    /**
60
     * Doctrine database handler.
61
     *
62
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
63
     * @deprecated Start to use DBAL $connection instead.
64
     */
65
    protected $dbHandler;
66
67
    /**
68
     * Language mask generator.
69
     *
70
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
71
     */
72
    protected $languageMaskGenerator;
73
74
    /**
75
     * Main URL database table name.
76
     *
77
     * @var string
78
     */
79
    protected $table;
80
81
    /** @var \Doctrine\DBAL\Connection */
82
    private $connection;
83
84
    /**
85
     * Creates a new DoctrineDatabase UrlAlias Gateway.
86
     *
87
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $dbHandler
88
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
89
     */
90
    public function __construct(
91
        DatabaseHandler $dbHandler,
92
        LanguageMaskGenerator $languageMaskGenerator
93
    ) {
94
        $this->dbHandler = $dbHandler;
95
        $this->languageMaskGenerator = $languageMaskGenerator;
96
        $this->table = static::TABLE;
97
        $this->connection = $dbHandler->getConnection();
98
    }
99
100
    public function setTable($name)
101
    {
102
        $this->table = $name;
103
    }
104
105
    /**
106
     * Loads all list of aliases by given $locationId.
107
     *
108
     * @param int $locationId
109
     * @return false|mixed
110
     */
111 View Code Duplication
    public function loadAllLocationEntries($locationId)
112
    {
113
        $query = $this->connection->createQueryBuilder();
114
        $query
115
            ->select(...$this->columns[$this->table])
116
            ->from($this->connection->quoteIdentifier($this->table))
117
            ->where(
118
                $query->expr()->andX(
119
                    $query->expr()->eq(
120
                        $this->connection->quoteIdentifier('action'),
121
                        ':action'
122
                    ),
123
                    $query->expr()->eq(
124
                        $this->connection->quoteIdentifier('is_original'),
125
                        ':isOriginal'
126
                    )
127
                )
128
            )
129
            ->setParameter(':action', "eznode:{$locationId}", PDO::PARAM_STR)
130
            ->setParameter(':isOriginal', 1, PDO::PARAM_INT);
131
132
        return $query->execute()->fetchAll(PDO::FETCH_ASSOC);
133
    }
134
135
    /**
136
     * Loads list of aliases by given $locationId.
137
     *
138
     * @param mixed $locationId
139
     * @param bool $custom
140
     * @param mixed $languageId
141
     *
142
     * @return array
143
     */
144
    public function loadLocationEntries($locationId, $custom = false, $languageId = false)
145
    {
146
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
147
        $query = $this->dbHandler->createSelectQuery();
148
        $query->select(
149
            $this->dbHandler->quoteColumn('id'),
150
            $this->dbHandler->quoteColumn('link'),
151
            $this->dbHandler->quoteColumn('is_alias'),
152
            $this->dbHandler->quoteColumn('alias_redirects'),
153
            $this->dbHandler->quoteColumn('lang_mask'),
154
            $this->dbHandler->quoteColumn('is_original'),
155
            $this->dbHandler->quoteColumn('parent'),
156
            $this->dbHandler->quoteColumn('text'),
157
            $this->dbHandler->quoteColumn('text_md5'),
158
            $this->dbHandler->quoteColumn('action')
159
        )->from(
160
            $this->dbHandler->quoteTable($this->table)
161
        )->where(
162
            $query->expr->lAnd(
163
                $query->expr->eq(
164
                    $this->dbHandler->quoteColumn('action'),
165
                    $query->bindValue("eznode:{$locationId}", null, \PDO::PARAM_STR)
166
                ),
167
                $query->expr->eq(
168
                    $this->dbHandler->quoteColumn('is_original'),
169
                    $query->bindValue(1, null, \PDO::PARAM_INT)
170
                ),
171
                $query->expr->eq(
172
                    $this->dbHandler->quoteColumn('is_alias'),
173
                    $query->bindValue(
174
                        $custom ? 1 : 0,
175
                        null,
176
                        \PDO::PARAM_INT
177
                    )
178
                )
179
            )
180
        );
181
182 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...
183
            $query->where(
184
                $query->expr->gt(
185
                    $query->expr->bitAnd(
186
                        $this->dbHandler->quoteColumn('lang_mask'),
187
                        $query->bindValue($languageId, null, \PDO::PARAM_INT)
188
                    ),
189
                    0
190
                )
191
            );
192
        }
193
194
        $statement = $query->prepare();
195
        $statement->execute();
196
197
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
198
    }
199
200
    /**
201
     * Loads paged list of global aliases.
202
     *
203
     * @param string|null $languageCode
204
     * @param int $offset
205
     * @param int $limit
206
     *
207
     * @return array
208
     */
209
    public function listGlobalEntries($languageCode = null, $offset = 0, $limit = -1)
210
    {
211
        $limit = $limit === -1 ? self::MAX_LIMIT : $limit;
212
213
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
214
        $query = $this->dbHandler->createSelectQuery();
215
        $query->select(
216
            $this->dbHandler->quoteColumn('action'),
217
            $this->dbHandler->quoteColumn('id'),
218
            $this->dbHandler->quoteColumn('link'),
219
            $this->dbHandler->quoteColumn('is_alias'),
220
            $this->dbHandler->quoteColumn('alias_redirects'),
221
            $this->dbHandler->quoteColumn('lang_mask'),
222
            $this->dbHandler->quoteColumn('is_original'),
223
            $this->dbHandler->quoteColumn('parent'),
224
            $this->dbHandler->quoteColumn('text_md5')
225
        )->from(
226
            $this->dbHandler->quoteTable($this->table)
227
        )->where(
228
            $query->expr->lAnd(
229
                $query->expr->eq(
230
                    $this->dbHandler->quoteColumn('action_type'),
231
                    $query->bindValue('module', null, \PDO::PARAM_STR)
232
                ),
233
                $query->expr->eq(
234
                    $this->dbHandler->quoteColumn('is_original'),
235
                    $query->bindValue(1, null, \PDO::PARAM_INT)
236
                ),
237
                $query->expr->eq(
238
                    $this->dbHandler->quoteColumn('is_alias'),
239
                    $query->bindValue(1, null, \PDO::PARAM_INT)
240
                )
241
            )
242
        )->limit(
243
            $limit,
244
            $offset
245
        );
246 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...
247
            $query->where(
248
                $query->expr->gt(
249
                    $query->expr->bitAnd(
250
                        $this->dbHandler->quoteColumn('lang_mask'),
251
                        $query->bindValue(
252
                            $this->languageMaskGenerator->generateLanguageIndicator($languageCode, false),
253
                            null,
254
                            \PDO::PARAM_INT
255
                        )
256
                    ),
257
                    0
258
                )
259
            );
260
        }
261
        $statement = $query->prepare();
262
        $statement->execute();
263
264
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
265
    }
266
267
    /**
268
     * Returns boolean indicating if the row with given $id is special root entry.
269
     *
270
     * Special root entry entry will have parentId=0 and text=''.
271
     * In standard installation this entry will point to location with id=2.
272
     *
273
     * @param mixed $id
274
     *
275
     * @return bool
276
     */
277
    public function isRootEntry($id)
278
    {
279
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
280
        $query = $this->dbHandler->createSelectQuery();
281
        $query->select(
282
            $this->dbHandler->quoteColumn('text'),
283
            $this->dbHandler->quoteColumn('parent')
284
        )->from(
285
            $this->dbHandler->quoteTable($this->table)
286
        )->where(
287
            $query->expr->eq(
288
                $this->dbHandler->quoteColumn('id'),
289
                $query->bindValue($id, null, \PDO::PARAM_INT)
290
            )
291
        );
292
        $statement = $query->prepare();
293
        $statement->execute();
294
        $row = $statement->fetch(\PDO::FETCH_ASSOC);
295
296
        return strlen($row['text']) == 0 && $row['parent'] == 0;
297
    }
298
299
    /**
300
     * Downgrades autogenerated entry matched by given $action and $languageId and negatively matched by
301
     * composite primary key.
302
     *
303
     * If language mask of the found entry is composite (meaning it consists of multiple language ids) given
304
     * $languageId will be removed from mask. Otherwise entry will be marked as history.
305
     *
306
     * @param string $action
307
     * @param mixed $languageId
308
     * @param mixed $newId
309
     * @param mixed $parentId
310
     * @param string $textMD5
311
     */
312
    public function cleanupAfterPublish($action, $languageId, $newId, $parentId, $textMD5)
313
    {
314
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
315
        $query = $this->dbHandler->createSelectQuery();
316
        $query->select(
317
            $this->dbHandler->quoteColumn('parent'),
318
            $this->dbHandler->quoteColumn('text_md5'),
319
            $this->dbHandler->quoteColumn('lang_mask')
320
        )->from(
321
            $this->dbHandler->quoteTable($this->table)
322
        )->where(
323
            $query->expr->lAnd(
324
                // 1) Autogenerated aliases that match action and language...
325
                $query->expr->eq(
326
                    $this->dbHandler->quoteColumn('action'),
327
                    $query->bindValue($action, null, \PDO::PARAM_STR)
328
                ),
329
                $query->expr->eq(
330
                    $this->dbHandler->quoteColumn('is_original'),
331
                    $query->bindValue(1, null, \PDO::PARAM_INT)
332
                ),
333
                $query->expr->eq(
334
                    $this->dbHandler->quoteColumn('is_alias'),
335
                    $query->bindValue(0, null, \PDO::PARAM_INT)
336
                ),
337
                $query->expr->gt(
338
                    $query->expr->bitAnd(
339
                        $this->dbHandler->quoteColumn('lang_mask'),
340
                        $query->bindValue($languageId, null, \PDO::PARAM_INT)
341
                    ),
342
                    0
343
                ),
344
                // 2) ...but not newly published entry
345
                $query->expr->not(
346
                    $query->expr->lAnd(
347
                        $query->expr->eq(
348
                            $this->dbHandler->quoteColumn('parent'),
349
                            $query->bindValue($parentId, null, \PDO::PARAM_INT)
350
                        ),
351
                        $query->expr->eq(
352
                            $this->dbHandler->quoteColumn('text_md5'),
353
                            $query->bindValue($textMD5, null, \PDO::PARAM_STR)
354
                        )
355
                    )
356
                )
357
            )
358
        );
359
360
        $statement = $query->prepare();
361
        $statement->execute();
362
        $row = $statement->fetch(\PDO::FETCH_ASSOC);
363
364
        if (!empty($row)) {
365
            $this->archiveUrlAliasForDeletedTranslation($row['lang_mask'], $languageId, $row['parent'], $row['text_md5'], $newId);
366
        }
367
    }
368
369
    /**
370
     * Archive (remove or historize) obsolete URL aliases (for translations that were removed).
371
     *
372
     * @param int $languageMask all languages bit mask
373
     * @param int $languageId removed language Id
374
     * @param int $parent
375
     * @param string $textMD5 checksum
376
     * @param $linkId
377
     */
378
    private function archiveUrlAliasForDeletedTranslation($languageMask, $languageId, $parent, $textMD5, $linkId)
379
    {
380
        // If language mask is composite (consists of multiple languages) then remove given language from entry
381
        if ($languageMask & ~($languageId | 1)) {
382
            $this->removeTranslation($parent, $textMD5, $languageId);
383
        } else {
384
            // Otherwise mark entry as history
385
            $this->historize($parent, $textMD5, $linkId);
386
        }
387
    }
388
389
    public function historizeBeforeSwap($action, $languageMask)
390
    {
391
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
392
        $query = $this->dbHandler->createUpdateQuery();
393
        $query->update(
394
            $this->dbHandler->quoteTable($this->table)
395
        )->set(
396
            $this->dbHandler->quoteColumn('is_original'),
397
            $query->bindValue(0, null, \PDO::PARAM_INT)
398
        )->set(
399
            $this->dbHandler->quoteColumn('id'),
400
            $query->bindValue(
401
                $this->getNextId(),
402
                null,
403
                \PDO::PARAM_INT
404
            )
405
        )->where(
406
            $query->expr->lAnd(
407
                $query->expr->eq(
408
                    $this->dbHandler->quoteColumn('action'),
409
                    $query->bindValue($action, null, \PDO::PARAM_STR)
410
                ),
411
                $query->expr->eq(
412
                    $this->dbHandler->quoteColumn('is_original'),
413
                    $query->bindValue(1, null, \PDO::PARAM_INT)
414
                ),
415
                $query->expr->gt(
416
                    $query->expr->bitAnd(
417
                        $this->dbHandler->quoteColumn('lang_mask'),
418
                        $query->bindValue($languageMask & ~1, null, \PDO::PARAM_INT)
419
                    ),
420
                    0
421
                )
422
            )
423
        );
424
        $query->prepare()->execute();
425
    }
426
427
    /**
428
     * Updates single row matched by composite primary key.
429
     *
430
     * Sets "is_original" to 0 thus marking entry as history.
431
     *
432
     * Re-links history entries.
433
     *
434
     * When location alias is published we need to check for new history entries created with self::downgrade()
435
     * with the same action and language, update their "link" column with id of the published entry.
436
     * History entry "id" column is moved to next id value so that all active (non-history) entries are kept
437
     * under the same id.
438
     *
439
     * @param int $parentId
440
     * @param string $textMD5
441
     * @param int $newId
442
     */
443
    protected function historize($parentId, $textMD5, $newId)
444
    {
445
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
446
        $query = $this->dbHandler->createUpdateQuery();
447
        $query->update(
448
            $this->dbHandler->quoteTable($this->table)
449
        )->set(
450
            $this->dbHandler->quoteColumn('is_original'),
451
            $query->bindValue(0, null, \PDO::PARAM_INT)
452
        )->set(
453
            $this->dbHandler->quoteColumn('link'),
454
            $query->bindValue($newId, null, \PDO::PARAM_INT)
455
        )->set(
456
            $this->dbHandler->quoteColumn('id'),
457
            $query->bindValue(
458
                $this->getNextId(),
459
                null,
460
                \PDO::PARAM_INT
461
            )
462
        )->where(
463
            $query->expr->lAnd(
464
                $query->expr->eq(
465
                    $this->dbHandler->quoteColumn('parent'),
466
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
467
                ),
468
                $query->expr->eq(
469
                    $this->dbHandler->quoteColumn('text_md5'),
470
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
471
                )
472
            )
473
        );
474
        $query->prepare()->execute();
475
    }
476
477
    /**
478
     * Updates single row data matched by composite primary key.
479
     *
480
     * Removes given $languageId from entry's language mask
481
     *
482
     * @param mixed $parentId
483
     * @param string $textMD5
484
     * @param mixed $languageId
485
     */
486
    protected function removeTranslation($parentId, $textMD5, $languageId)
487
    {
488
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
489
        $query = $this->dbHandler->createUpdateQuery();
490
        $query->update(
491
            $this->dbHandler->quoteTable($this->table)
492
        )->set(
493
            $this->dbHandler->quoteColumn('lang_mask'),
494
            $query->expr->bitAnd(
495
                $this->dbHandler->quoteColumn('lang_mask'),
496
                $query->bindValue(~$languageId, null, \PDO::PARAM_INT)
497
            )
498
        )->where(
499
            $query->expr->lAnd(
500
                $query->expr->eq(
501
                    $this->dbHandler->quoteColumn('parent'),
502
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
503
                ),
504
                $query->expr->eq(
505
                    $this->dbHandler->quoteColumn('text_md5'),
506
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
507
                )
508
            )
509
        );
510
        $query->prepare()->execute();
511
    }
512
513
    /**
514
     * Marks all entries with given $id as history entries.
515
     *
516
     * This method is used by Handler::locationMoved(). Each row is separately historized
517
     * because future publishing needs to be able to take over history entries safely.
518
     *
519
     * @param mixed $id
520
     * @param mixed $link
521
     */
522
    public function historizeId($id, $link)
523
    {
524
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
525
        $query = $this->dbHandler->createSelectQuery();
526
        $query->select(
527
            $this->dbHandler->quoteColumn('parent'),
528
            $this->dbHandler->quoteColumn('text_md5')
529
        )->from(
530
            $this->dbHandler->quoteTable($this->table)
531
        )->where(
532
            $query->expr->lAnd(
533
                $query->expr->eq(
534
                    $this->dbHandler->quoteColumn('is_alias'),
535
                    $query->bindValue(0, null, \PDO::PARAM_INT)
536
                ),
537
                $query->expr->eq(
538
                    $this->dbHandler->quoteColumn('is_original'),
539
                    $query->bindValue(1, null, \PDO::PARAM_INT)
540
                ),
541
                $query->expr->eq(
542
                    $this->dbHandler->quoteColumn('action_type'),
543
                    $query->bindValue('eznode', null, \PDO::PARAM_STR)
544
                ),
545
                $query->expr->eq(
546
                    $this->dbHandler->quoteColumn('link'),
547
                    $query->bindValue($id, null, \PDO::PARAM_INT)
548
                )
549
            )
550
        );
551
552
        $statement = $query->prepare();
553
        $statement->execute();
554
555
        $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
556
557
        foreach ($rows as $row) {
558
            $this->historize($row['parent'], $row['text_md5'], $link);
559
        }
560
    }
561
562
    /**
563
     * Updates parent id of autogenerated entries.
564
     *
565
     * Update includes history entries.
566
     *
567
     * @param mixed $oldParentId
568
     * @param mixed $newParentId
569
     */
570
    public function reparent($oldParentId, $newParentId)
571
    {
572
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
573
        $query = $this->dbHandler->createUpdateQuery();
574
        $query->update(
575
            $this->dbHandler->quoteTable($this->table)
576
        )->set(
577
            $this->dbHandler->quoteColumn('parent'),
578
            $query->bindValue($newParentId, null, \PDO::PARAM_INT)
579
        )->where(
580
            $query->expr->lAnd(
581
                $query->expr->eq(
582
                    $this->dbHandler->quoteColumn('is_alias'),
583
                    $query->bindValue(0, null, \PDO::PARAM_INT)
584
                ),
585
                $query->expr->eq(
586
                    $this->dbHandler->quoteColumn('parent'),
587
                    $query->bindValue($oldParentId, null, \PDO::PARAM_INT)
588
                )
589
            )
590
        );
591
592
        $query->prepare()->execute();
593
    }
594
595
    /**
596
     * Updates single row data matched by composite primary key.
597
     *
598
     * Use optional parameter $languageMaskMatch to additionally limit the query match with languages.
599
     *
600
     * @param mixed $parentId
601
     * @param string $textMD5
602
     * @param array $values associative array with column names as keys and column values as values
603
     */
604 View Code Duplication
    public function updateRow($parentId, $textMD5, array $values)
605
    {
606
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
607
        $query = $this->dbHandler->createUpdateQuery();
608
        $query->update($this->dbHandler->quoteTable($this->table));
609
        $this->setQueryValues($query, $values);
610
        $query->where(
611
            $query->expr->lAnd(
612
                $query->expr->eq(
613
                    $this->dbHandler->quoteColumn('parent'),
614
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
615
                ),
616
                $query->expr->eq(
617
                    $this->dbHandler->quoteColumn('text_md5'),
618
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
619
                )
620
            )
621
        );
622
        $query->prepare()->execute();
623
    }
624
625
    /**
626
     * Inserts new row in urlalias_ml table.
627
     *
628
     * @param array $values
629
     *
630
     * @return mixed
631
     */
632
    public function insertRow(array $values)
633
    {
634
        // @todo remove after testing
635
        if (
636
            !isset($values['text']) ||
637
            !isset($values['text_md5']) ||
638
            !isset($values['action']) ||
639
            !isset($values['parent']) ||
640
            !isset($values['lang_mask'])) {
641
            throw new \Exception('value set is incomplete: ' . var_export($values, true) . ", can't execute insert");
642
        }
643
        if (!isset($values['id'])) {
644
            $values['id'] = $this->getNextId();
645
        }
646
        if (!isset($values['link'])) {
647
            $values['link'] = $values['id'];
648
        }
649
        if (!isset($values['is_original'])) {
650
            $values['is_original'] = ($values['id'] == $values['link'] ? 1 : 0);
651
        }
652
        if (!isset($values['is_alias'])) {
653
            $values['is_alias'] = 0;
654
        }
655
        if (!isset($values['alias_redirects'])) {
656
            $values['alias_redirects'] = 0;
657
        }
658
        if (!isset($values['action_type'])) {
659
            if (preg_match('#^(.+):.*#', $values['action'], $matches)) {
660
                $values['action_type'] = $matches[1];
661
            }
662
        }
663
        if ($values['is_alias']) {
664
            $values['is_original'] = 1;
665
        }
666
        if ($values['action'] === 'nop:') {
667
            $values['is_original'] = 0;
668
        }
669
670
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
671
        $query = $this->dbHandler->createInsertQuery();
672
        $query->insertInto($this->dbHandler->quoteTable($this->table));
673
        $this->setQueryValues($query, $values);
674
        $query->prepare()->execute();
675
676
        return $values['id'];
677
    }
678
679
    /**
680
     * Sets value for insert or update query.
681
     *
682
     * @param \eZ\Publish\Core\Persistence\Database\Query|\eZ\Publish\Core\Persistence\Database\InsertQuery|\eZ\Publish\Core\Persistence\Database\UpdateQuery $query
683
     * @param array $values
684
     *
685
     * @throws \Exception
686
     */
687
    protected function setQueryValues(Query $query, $values)
688
    {
689
        foreach ($values as $column => $value) {
690
            // @todo remove after testing
691
            if (!in_array($column, $this->columns['ezurlalias_ml'])) {
692
                throw new \Exception("unknown column '$column' for table 'ezurlalias_ml'");
693
            }
694
            switch ($column) {
695
                case 'text':
696
                case 'action':
697
                case 'text_md5':
698
                case 'action_type':
699
                    $pdoDataType = \PDO::PARAM_STR;
700
                    break;
701
                default:
702
                    $pdoDataType = \PDO::PARAM_INT;
703
            }
704
            $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...
705
                $this->dbHandler->quoteColumn($column),
706
                $query->bindValue($value, null, $pdoDataType)
707
            );
708
        }
709
    }
710
711
    /**
712
     * Returns next value for "id" column.
713
     *
714
     * @return mixed
715
     */
716 View Code Duplication
    public function getNextId()
717
    {
718
        $sequence = $this->dbHandler->getSequenceName('ezurlalias_ml_incr', 'id');
719
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
720
        $query = $this->dbHandler->createInsertQuery();
721
        $query->insertInto(
722
            $this->dbHandler->quoteTable('ezurlalias_ml_incr')
723
        );
724
        // ezcDatabase does not abstract the "auto increment id"
725
        // INSERT INTO ezurlalias_ml_incr VALUES(DEFAULT) is not an option due
726
        // to this mysql bug: http://bugs.mysql.com/bug.php?id=42270
727
        // as a result we are forced to check which database is currently used
728
        // to generate the correct SQL query
729
        // see https://jira.ez.no/browse/EZP-20652
730
        if ($this->dbHandler->useSequences()) {
731
            $query->set(
732
                $this->dbHandler->quoteColumn('id'),
733
                "nextval('{$sequence}')"
734
            );
735
        } else {
736
            $query->set(
737
                $this->dbHandler->quoteColumn('id'),
738
                $query->bindValue(null, null, \PDO::PARAM_NULL)
739
            );
740
        }
741
        $query->prepare()->execute();
742
743
        return $this->dbHandler->lastInsertId($sequence);
744
    }
745
746
    /**
747
     * Loads single row data matched by composite primary key.
748
     *
749
     * @param mixed $parentId
750
     * @param string $textMD5
751
     *
752
     * @return array
753
     */
754 View Code Duplication
    public function loadRow($parentId, $textMD5)
755
    {
756
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
757
        $query = $this->dbHandler->createSelectQuery();
758
        $query->select('*')->from(
759
            $this->dbHandler->quoteTable($this->table)
760
        )->where(
761
            $query->expr->lAnd(
762
                $query->expr->eq(
763
                    $this->dbHandler->quoteColumn('parent'),
764
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
765
                ),
766
                $query->expr->eq(
767
                    $this->dbHandler->quoteColumn('text_md5'),
768
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
769
                )
770
            )
771
        );
772
773
        $statement = $query->prepare();
774
        $statement->execute();
775
776
        return $statement->fetch(\PDO::FETCH_ASSOC);
777
    }
778
779
    /**
780
     * Loads complete URL alias data by given array of path hashes.
781
     *
782
     * @param string[] $urlHashes URL string hashes
783
     *
784
     * @return array
785
     */
786
    public function loadUrlAliasData(array $urlHashes)
787
    {
788
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
789
        $query = $this->dbHandler->createSelectQuery();
790
791
        $count = count($urlHashes);
792
        foreach ($urlHashes as $level => $urlPartHash) {
793
            $tableName = $this->table . ($level === $count - 1 ? '' : $level);
794
795
            if ($level === $count - 1) {
796
                $query->select(
797
                    $this->dbHandler->quoteColumn('id', $tableName),
798
                    $this->dbHandler->quoteColumn('link', $tableName),
799
                    $this->dbHandler->quoteColumn('is_alias', $tableName),
800
                    $this->dbHandler->quoteColumn('alias_redirects', $tableName),
801
                    $this->dbHandler->quoteColumn('is_original', $tableName),
802
                    $this->dbHandler->quoteColumn('action', $tableName),
803
                    $this->dbHandler->quoteColumn('action_type', $tableName),
804
                    $this->dbHandler->quoteColumn('lang_mask', $tableName),
805
                    $this->dbHandler->quoteColumn('text', $tableName),
806
                    $this->dbHandler->quoteColumn('parent', $tableName),
807
                    $this->dbHandler->quoteColumn('text_md5', $tableName)
808
                )->from(
809
                    $this->dbHandler->quoteTable($this->table)
810
                );
811
            } else {
812
                $query->select(
813
                    $this->dbHandler->aliasedColumn($query, 'id', $tableName),
814
                    $this->dbHandler->aliasedColumn($query, 'link', $tableName),
815
                    $this->dbHandler->aliasedColumn($query, 'is_alias', $tableName),
816
                    $this->dbHandler->aliasedColumn($query, 'alias_redirects', $tableName),
817
                    $this->dbHandler->aliasedColumn($query, 'is_original', $tableName),
818
                    $this->dbHandler->aliasedColumn($query, 'action', $tableName),
819
                    $this->dbHandler->aliasedColumn($query, 'action_type', $tableName),
820
                    $this->dbHandler->aliasedColumn($query, 'lang_mask', $tableName),
821
                    $this->dbHandler->aliasedColumn($query, 'text', $tableName),
822
                    $this->dbHandler->aliasedColumn($query, 'parent', $tableName),
823
                    $this->dbHandler->aliasedColumn($query, 'text_md5', $tableName)
824
                )->from(
825
                    $query->alias($this->table, $tableName)
826
                );
827
            }
828
829
            $query->where(
830
                $query->expr->lAnd(
831
                    $query->expr->eq(
832
                        $this->dbHandler->quoteColumn('text_md5', $tableName),
833
                        $query->bindValue($urlPartHash, null, \PDO::PARAM_STR)
834
                    ),
835
                    $query->expr->eq(
836
                        $this->dbHandler->quoteColumn('parent', $tableName),
837
                        // root entry has parent column set to 0
838
                        isset($previousTableName) ? $this->dbHandler->quoteColumn('link', $previousTableName) : $query->bindValue(0, null, \PDO::PARAM_INT)
839
                    )
840
                )
841
            );
842
843
            $previousTableName = $tableName;
844
        }
845
        $query->limit(1);
846
847
        $statement = $query->prepare();
848
        $statement->execute();
849
850
        return $statement->fetch(\PDO::FETCH_ASSOC);
851
    }
852
853
    /**
854
     * Loads autogenerated entry id by given $action and optionally $parentId.
855
     *
856
     * @param string $action
857
     * @param mixed|null $parentId
858
     *
859
     * @return array
860
     */
861 View Code Duplication
    public function loadAutogeneratedEntry($action, $parentId = null)
862
    {
863
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
864
        $query = $this->dbHandler->createSelectQuery();
865
        $query->select(
866
            '*'
867
        )->from(
868
            $this->dbHandler->quoteTable($this->table)
869
        )->where(
870
            $query->expr->lAnd(
871
                $query->expr->eq(
872
                    $this->dbHandler->quoteColumn('action'),
873
                    $query->bindValue($action, null, \PDO::PARAM_STR)
874
                ),
875
                $query->expr->eq(
876
                    $this->dbHandler->quoteColumn('is_original'),
877
                    $query->bindValue(1, null, \PDO::PARAM_INT)
878
                ),
879
                $query->expr->eq(
880
                    $this->dbHandler->quoteColumn('is_alias'),
881
                    $query->bindValue(0, null, \PDO::PARAM_INT)
882
                )
883
            )
884
        );
885
886
        if (isset($parentId)) {
887
            $query->where(
888
                $query->expr->eq(
889
                    $this->dbHandler->quoteColumn('parent'),
890
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
891
                )
892
            );
893
        }
894
895
        $statement = $query->prepare();
896
        $statement->execute();
897
898
        return $statement->fetch(\PDO::FETCH_ASSOC);
899
    }
900
901
    /**
902
     * Loads all data for the path identified by given $id.
903
     *
904
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
905
     *
906
     * @param int $id
907
     *
908
     * @return array
909
     */
910
    public function loadPathData($id)
911
    {
912
        $pathData = [];
913
914
        while ($id != 0) {
915
            /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
916
            $query = $this->dbHandler->createSelectQuery();
917
            $query->select(
918
                $this->dbHandler->quoteColumn('parent'),
919
                $this->dbHandler->quoteColumn('lang_mask'),
920
                $this->dbHandler->quoteColumn('text')
921
            )->from(
922
                $this->dbHandler->quoteTable($this->table)
923
            )->where(
924
                $query->expr->eq(
925
                    $this->dbHandler->quoteColumn('id'),
926
                    $query->bindValue($id, null, \PDO::PARAM_INT)
927
                )
928
            );
929
930
            $statement = $query->prepare();
931
            $statement->execute();
932
933
            $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
934
            if (empty($rows)) {
935
                // Normally this should never happen
936
                $pathDataArray = [];
937
                foreach ($pathData as $path) {
938
                    if (!isset($path[0]['text'])) {
939
                        continue;
940
                    }
941
942
                    $pathDataArray[] = $path[0]['text'];
943
                }
944
945
                $path = implode('/', $pathDataArray);
946
                throw new BadStateException(
947
                    'id',
948
                    "Unable to load path data, the path ...'{$path}' is broken, alias id '{$id}' not found. " .
949
                    'To fix all broken paths run the ezplatform:urls:regenerate-aliases command'
950
                );
951
            }
952
953
            $id = $rows[0]['parent'];
954
            array_unshift($pathData, $rows);
955
        }
956
957
        return $pathData;
958
    }
959
960
    /**
961
     * Loads path data identified by given ordered array of hierarchy data.
962
     *
963
     * The first entry in $hierarchyData corresponds to the top-most path element in the path, the second entry the
964
     * child of the first path element and so on.
965
     * This method is faster than self::getPath() since it can fetch all elements using only one query, but can be used
966
     * only for autogenerated paths.
967
     *
968
     * @param array $hierarchyData
969
     *
970
     * @return array
971
     */
972
    public function loadPathDataByHierarchy(array $hierarchyData)
973
    {
974
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
975
        $query = $this->dbHandler->createSelectQuery();
976
977
        $hierarchyConditions = [];
978
        foreach ($hierarchyData as $levelData) {
979
            $hierarchyConditions[] = $query->expr->lAnd(
980
                $query->expr->eq(
981
                    $this->dbHandler->quoteColumn('parent'),
982
                    $query->bindValue(
983
                        $levelData['parent'],
984
                        null,
985
                        \PDO::PARAM_INT
986
                    )
987
                ),
988
                $query->expr->eq(
989
                    $this->dbHandler->quoteColumn('action'),
990
                    $query->bindValue(
991
                        $levelData['action'],
992
                        null,
993
                        \PDO::PARAM_STR
994
                    )
995
                ),
996
                $query->expr->eq(
997
                    $this->dbHandler->quoteColumn('id'),
998
                    $query->bindValue(
999
                        $levelData['id'],
1000
                        null,
1001
                        \PDO::PARAM_INT
1002
                    )
1003
                )
1004
            );
1005
        }
1006
1007
        $query->select(
1008
            $this->dbHandler->quoteColumn('action'),
1009
            $this->dbHandler->quoteColumn('lang_mask'),
1010
            $this->dbHandler->quoteColumn('text')
1011
        )->from(
1012
            $this->dbHandler->quoteTable($this->table)
1013
        )->where(
1014
            $query->expr->lOr($hierarchyConditions)
1015
        );
1016
1017
        $statement = $query->prepare();
1018
        $statement->execute();
1019
1020
        $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
1021
        $rowsMap = [];
1022
        foreach ($rows as $row) {
1023
            $rowsMap[$row['action']][] = $row;
1024
        }
1025
1026
        if (count($rowsMap) !== count($hierarchyData)) {
1027
            throw new \RuntimeException('The path is corrupted.');
1028
        }
1029
1030
        $data = [];
1031
        foreach ($hierarchyData as $levelData) {
1032
            $data[] = $rowsMap[$levelData['action']];
1033
        }
1034
1035
        return $data;
1036
    }
1037
1038
    /**
1039
     * Deletes single custom alias row matched by composite primary key.
1040
     *
1041
     * @param mixed $parentId
1042
     * @param string $textMD5
1043
     *
1044
     * @return bool
1045
     */
1046
    public function removeCustomAlias($parentId, $textMD5)
1047
    {
1048
        /** @var $query \eZ\Publish\Core\Persistence\Database\DeleteQuery */
1049
        $query = $this->dbHandler->createDeleteQuery();
1050
        $query->deleteFrom(
1051
            $this->dbHandler->quoteTable($this->table)
1052
        )->where(
1053
            $query->expr->lAnd(
1054
                $query->expr->eq(
1055
                    $this->dbHandler->quoteColumn('parent'),
1056
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
1057
                ),
1058
                $query->expr->eq(
1059
                    $this->dbHandler->quoteColumn('text_md5'),
1060
                    $query->bindValue($textMD5, null, \PDO::PARAM_STR)
1061
                ),
1062
                $query->expr->eq(
1063
                    $this->dbHandler->quoteColumn('is_alias'),
1064
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1065
                )
1066
            )
1067
        );
1068
        $statement = $query->prepare();
1069
        $statement->execute();
1070
1071
        return $statement->rowCount() === 1 ?: false;
1072
    }
1073
1074
    /**
1075
     * Deletes all rows with given $action and optionally $id.
1076
     *
1077
     * If $id is set only autogenerated entries will be removed.
1078
     *
1079
     * @param mixed $action
1080
     * @param mixed|null $id
1081
     *
1082
     * @return bool
1083
     */
1084
    public function remove($action, $id = null)
1085
    {
1086
        /** @var $query \eZ\Publish\Core\Persistence\Database\DeleteQuery */
1087
        $query = $this->dbHandler->createDeleteQuery();
1088
        $query->deleteFrom(
1089
            $this->dbHandler->quoteTable($this->table)
1090
        )->where(
1091
            $query->expr->eq(
1092
                $this->dbHandler->quoteColumn('action'),
1093
                $query->bindValue($action, null, \PDO::PARAM_STR)
1094
            )
1095
        );
1096
1097
        if ($id !== null) {
1098
            $query->where(
1099
                $query->expr->lAnd(
1100
                    $query->expr->eq(
1101
                        $this->dbHandler->quoteColumn('is_alias'),
1102
                        $query->bindValue(0, null, \PDO::PARAM_INT)
1103
                    ),
1104
                    $query->expr->eq(
1105
                        $this->dbHandler->quoteColumn('id'),
1106
                        $query->bindValue($id, null, \PDO::PARAM_INT)
1107
                    )
1108
                )
1109
            );
1110
        }
1111
1112
        $query->prepare()->execute();
1113
    }
1114
1115
    /**
1116
     * Loads all autogenerated entries with given $parentId with optionally included history entries.
1117
     *
1118
     * @param mixed $parentId
1119
     * @param bool $includeHistory
1120
     *
1121
     * @return array
1122
     */
1123 View Code Duplication
    public function loadAutogeneratedEntries($parentId, $includeHistory = false)
1124
    {
1125
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
1126
        $query = $this->dbHandler->createSelectQuery();
1127
        $query->select(
1128
            '*'
1129
        )->from(
1130
            $this->dbHandler->quoteTable($this->table)
1131
        )->where(
1132
            $query->expr->lAnd(
1133
                $query->expr->eq(
1134
                    $this->dbHandler->quoteColumn('parent'),
1135
                    $query->bindValue($parentId, null, \PDO::PARAM_INT)
1136
                ),
1137
                $query->expr->eq(
1138
                    $this->dbHandler->quoteColumn('action_type'),
1139
                    $query->bindValue('eznode', null, \PDO::PARAM_STR)
1140
                ),
1141
                $query->expr->eq(
1142
                    $this->dbHandler->quoteColumn('is_alias'),
1143
                    $query->bindValue(0, null, \PDO::PARAM_INT)
1144
                )
1145
            )
1146
        );
1147
1148
        if (!$includeHistory) {
1149
            $query->where(
1150
                $query->expr->eq(
1151
                    $this->dbHandler->quoteColumn('is_original'),
1152
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1153
                )
1154
            );
1155
        }
1156
1157
        $statement = $query->prepare();
1158
        $statement->execute();
1159
1160
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1161
    }
1162
1163
    public function getLocationContentMainLanguageId($locationId)
1164
    {
1165
        $queryBuilder = $this->connection->createQueryBuilder();
1166
        $expr = $queryBuilder->expr();
1167
        $queryBuilder
1168
            ->select('c.initial_language_id')
1169
            ->from('ezcontentobject', 'c')
1170
            ->join('c', 'ezcontentobject_tree', 't', $expr->eq('t.contentobject_id', 'c.id'))
1171
            ->where(
1172
                $expr->eq('t.node_id', ':locationId')
1173
            )
1174
            ->setParameter('locationId', $locationId, ParameterType::INTEGER)
1175
        ;
1176
1177
        $statement = $queryBuilder->execute();
1178
        $languageId = $statement->fetchColumn();
1179
1180
        if ($languageId === false) {
1181
            throw new RuntimeException("Could not find Content for Location #{$locationId}");
1182
        }
1183
1184
        return $languageId;
1185
    }
1186
1187
    /**
1188
     * Removes languageId of removed translation from lang_mask and deletes single language rows for multiple Locations.
1189
     *
1190
     * Note: URL aliases are not historized as translation removal from all Versions is permanent w/o preserving history.
1191
     *
1192
     * @param int $languageId Language Id to be removed
1193
     * @param string[] $actions actions for which to perform the update
1194
     */
1195
    public function bulkRemoveTranslation($languageId, $actions)
1196
    {
1197
        $connection = $this->dbHandler->getConnection();
1198
        /** @var \Doctrine\DBAL\Connection $connection */
1199
        $query = $connection->createQueryBuilder();
1200
        $query
1201
            ->update($connection->quoteIdentifier($this->table))
1202
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
1203
            ->set('lang_mask', 'lang_mask & ~ ' . $languageId)
1204
            ->where('action IN (:actions)')
1205
            ->setParameter(':actions', $actions, Connection::PARAM_STR_ARRAY)
1206
        ;
1207
        $query->execute();
1208
1209
        // cleanup: delete single language rows (including alwaysAvailable)
1210
        $query = $connection->createQueryBuilder();
1211
        $query
1212
            ->delete($this->table)
1213
            ->where('action IN (:actions)')
1214
            ->andWhere('lang_mask IN (0, 1)')
1215
            ->setParameter(':actions', $actions, Connection::PARAM_STR_ARRAY)
1216
        ;
1217
        $query->execute();
1218
    }
1219
1220
    /**
1221
     * Archive (remove or historize) URL aliases for removed Translations.
1222
     *
1223
     * @param int $locationId
1224
     * @param int $parentId
1225
     * @param int[] $languageIds Language IDs of removed Translations
1226
     */
1227
    public function archiveUrlAliasesForDeletedTranslations($locationId, $parentId, array $languageIds)
1228
    {
1229
        // determine proper parent for linking historized entry
1230
        $existingLocationEntry = $this->loadAutogeneratedEntry(
1231
            'eznode:' . $locationId,
1232
            $parentId
1233
        );
1234
1235
        // filter existing URL alias entries by any of the specified removed languages
1236
        $rows = $this->loadLocationEntriesMatchingMultipleLanguages(
1237
            $locationId,
1238
            $languageIds
1239
        );
1240
1241
        // remove specific languages from a bit mask
1242
        foreach ($rows as $row) {
1243
            // filter mask to reduce the number of calls to storage engine
1244
            $rowLanguageMask = (int) $row['lang_mask'];
1245
            $languageIdsToBeRemoved = array_filter(
1246
                $languageIds,
1247
                function ($languageId) use ($rowLanguageMask) {
1248
                    return $languageId & $rowLanguageMask;
1249
                }
1250
            );
1251
1252
            if (empty($languageIdsToBeRemoved)) {
1253
                continue;
1254
            }
1255
1256
            // use existing entry to link archived alias or use current alias id
1257
            $linkToId = !empty($existingLocationEntry) ? $existingLocationEntry['id'] : $row['id'];
1258
            foreach ($languageIdsToBeRemoved as $languageId) {
1259
                $this->archiveUrlAliasForDeletedTranslation(
1260
                    $row['lang_mask'],
1261
                    $languageId,
1262
                    $row['parent'],
1263
                    $row['text_md5'],
1264
                    $linkToId
1265
                );
1266
            }
1267
        }
1268
    }
1269
1270
    /**
1271
     * Load list of aliases for given $locationId matching any of the Languages specified by $languageMask.
1272
     *
1273
     * @param int $locationId
1274
     * @param int[] $languageIds
1275
     *
1276
     * @return array[]|\Generator
1277
     */
1278
    private function loadLocationEntriesMatchingMultipleLanguages($locationId, array $languageIds)
1279
    {
1280
        // note: alwaysAvailable for this use case is not relevant
1281
        $languageMask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageIds(
1282
            $languageIds,
1283
            false
1284
        );
1285
1286
        $connection = $this->dbHandler->getConnection();
1287
        /** @var \Doctrine\DBAL\Connection $connection */
1288
        $query = $connection->createQueryBuilder();
1289
        $query
1290
            ->select('id', 'lang_mask', 'parent', 'text_md5')
1291
            ->from($this->table)
1292
            ->where('action = :action')
1293
            // fetch rows matching any of the given Languages
1294
            ->andWhere('lang_mask & :languageMask <> 0')
1295
            ->setParameter(':action', 'eznode:' . $locationId)
1296
            ->setParameter(':languageMask', $languageMask)
1297
        ;
1298
1299
        $statement = $query->execute();
1300
        $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
1301
1302
        return $rows ?: [];
1303
    }
1304
1305
    /**
1306
     * Delete URL aliases pointing to non-existent Locations.
1307
     *
1308
     * @return int Number of affected rows.
1309
     *
1310
     * @throws \Doctrine\DBAL\DBALException
1311
     */
1312
    public function deleteUrlAliasesWithoutLocation(): int
1313
    {
1314
        $dbPlatform = $this->connection->getDatabasePlatform();
1315
1316
        $subquery = $this->connection->createQueryBuilder();
1317
        $subquery
1318
            ->select('node_id')
1319
            ->from('ezcontentobject_tree', 't')
1320
            ->where(
1321
                $subquery->expr()->eq(
1322
                    't.node_id',
1323
                    sprintf(
1324
                        'CAST(%s as %s)',
1325
                        $dbPlatform->getSubstringExpression($this->table . '.action', 8),
1326
                        $this->getIntegerType($dbPlatform)
1327
                    )
1328
                )
1329
            )
1330
        ;
1331
1332
        $deleteQuery = $this->connection->createQueryBuilder();
1333
        $deleteQuery
1334
            ->delete($this->table)
1335
            ->where(
1336
                $deleteQuery->expr()->eq(
1337
                    'action_type',
1338
                    $deleteQuery->createPositionalParameter('eznode')
1339
                )
1340
            )
1341
            ->andWhere(
1342
                sprintf('NOT EXISTS (%s)', $subquery->getSQL())
1343
            )
1344
        ;
1345
1346
        return $deleteQuery->execute();
1347
    }
1348
1349
    /**
1350
     * Delete URL aliases pointing to non-existent parent nodes.
1351
     *
1352
     * @return int Number of affected rows.
1353
     */
1354 View Code Duplication
    public function deleteUrlAliasesWithoutParent(): int
1355
    {
1356
        $existingAliasesQuery = $this->getAllUrlAliasesQuery();
1357
1358
        $query = $this->connection->createQueryBuilder();
1359
        $query
1360
            ->delete($this->table)
1361
            ->where(
1362
                $query->expr()->neq(
1363
                    'parent',
1364
                    $query->createPositionalParameter(0, \PDO::PARAM_INT)
1365
                )
1366
            )
1367
            ->andWhere(
1368
                $query->expr()->notIn(
1369
                    'parent',
1370
                    $existingAliasesQuery
1371
                )
1372
            )
1373
        ;
1374
1375
        return $query->execute();
1376
    }
1377
1378
    /**
1379
     * Delete URL aliases which do not link to any existing URL alias node.
1380
     *
1381
     * Note: Typically link column value is used to determine original alias for an archived entries.
1382
     */
1383 View Code Duplication
    public function deleteUrlAliasesWithBrokenLink()
1384
    {
1385
        $existingAliasesQuery = $this->getAllUrlAliasesQuery();
1386
1387
        $query = $this->connection->createQueryBuilder();
1388
        $query
1389
            ->delete($this->table)
1390
            ->where(
1391
                $query->expr()->neq('id', 'link')
1392
            )
1393
            ->andWhere(
1394
                $query->expr()->notIn(
1395
                    'link',
1396
                    $existingAliasesQuery
1397
                )
1398
            )
1399
        ;
1400
1401
        return $query->execute();
1402
    }
1403
1404
    /**
1405
     * Attempt repairing data corruption for broken archived URL aliases for Location,
1406
     * assuming there exists restored original (current) entry.
1407
     *
1408
     * @param int $locationId
1409
     */
1410
    public function repairBrokenUrlAliasesForLocation(int $locationId)
1411
    {
1412
        $urlAliasesData = $this->getUrlAliasesForLocation($locationId);
1413
1414
        $originalUrlAliases = $this->filterOriginalAliases($urlAliasesData);
1415
1416
        if (count($originalUrlAliases) === count($urlAliasesData)) {
1417
            // no archived aliases - nothing to fix
1418
            return;
1419
        }
1420
1421
        $updateQueryBuilder = $this->connection->createQueryBuilder();
1422
        $expr = $updateQueryBuilder->expr();
1423
        $updateQueryBuilder
1424
            ->update('ezurlalias_ml')
1425
            ->set('link', ':linkId')
1426
            ->set('parent', ':newParentId')
1427
            ->where(
1428
                $expr->eq('action', ':action')
1429
            )
1430
            ->andWhere(
1431
                $expr->eq(
1432
                    'is_original',
1433
                    $updateQueryBuilder->createNamedParameter(0, ParameterType::INTEGER)
1434
                )
1435
            )
1436
            ->andWhere(
1437
                $expr->eq('parent', ':oldParentId')
1438
            )
1439
            ->andWhere(
1440
                $expr->eq('text_md5', ':textMD5')
1441
            )
1442
            ->setParameter(':action', "eznode:{$locationId}");
1443
1444
        foreach ($urlAliasesData as $urlAliasData) {
1445
            if ($urlAliasData['is_original'] === 1 || !isset($originalUrlAliases[$urlAliasData['lang_mask']])) {
1446
                // ignore non-archived entries and deleted Translations
1447
                continue;
1448
            }
1449
1450
            $originalUrlAlias = $originalUrlAliases[$urlAliasData['lang_mask']];
1451
1452
            if ($urlAliasData['link'] === $originalUrlAlias['link']) {
1453
                // ignore correct entries to avoid unnecessary updates
1454
                continue;
1455
            }
1456
1457
            $updateQueryBuilder
1458
                ->setParameter(':linkId', $originalUrlAlias['link'], ParameterType::INTEGER)
1459
                // attempt to fix missing parent case
1460
                ->setParameter(
1461
                    ':newParentId',
1462
                    $urlAliasData['existing_parent'] ?? $originalUrlAlias['parent'],
1463
                    ParameterType::INTEGER
1464
                )
1465
                ->setParameter(':oldParentId', $urlAliasData['parent'], ParameterType::INTEGER)
1466
                ->setParameter(':textMD5', $urlAliasData['text_md5']);
1467
1468
            try {
1469
                $updateQueryBuilder->execute();
1470
            } catch (UniqueConstraintViolationException $e) {
1471
                // edge case: if such row already exists, there's no way to restore history
1472
                $this->deleteRow($urlAliasData['parent'], $urlAliasData['text_md5']);
1473
            }
1474
        }
1475
    }
1476
1477
    /**
1478
     * Filter from the given result set original (current) only URL aliases and index them by language_mask.
1479
     *
1480
     * Note: each language_mask can have one URL Alias.
1481
     *
1482
     * @param array $urlAliasesData
1483
     *
1484
     * @return array
1485
     */
1486
    private function filterOriginalAliases(array $urlAliasesData): array
1487
    {
1488
        $originalUrlAliases = array_filter(
1489
            $urlAliasesData,
1490
            function ($urlAliasData) {
1491
                // filter is_original=true ignoring broken parent records (cleaned up elsewhere)
1492
                return (bool)$urlAliasData['is_original'] && $urlAliasData['existing_parent'] !== null;
1493
            }
1494
        );
1495
        // return language_mask-indexed array
1496
        return array_combine(
1497
            array_column($originalUrlAliases, 'lang_mask'),
1498
            $originalUrlAliases
1499
        );
1500
    }
1501
1502
    /**
1503
     * Get subquery for IDs of all URL aliases.
1504
     *
1505
     * @return string Query
1506
     */
1507
    private function getAllUrlAliasesQuery(): string
1508
    {
1509
        $existingAliasesQueryBuilder = $this->connection->createQueryBuilder();
1510
        $innerQueryBuilder = $this->connection->createQueryBuilder();
1511
1512
        return $existingAliasesQueryBuilder
1513
            ->select('tmp.id')
1514
            ->from(
1515
                // nest subquery to avoid same-table update error
1516
                '(' . $innerQueryBuilder->select('id')->from($this->table)->getSQL() . ')',
1517
                'tmp'
1518
            )
1519
            ->getSQL();
1520
    }
1521
1522
    /**
1523
     * Get DBMS-specific integer type.
1524
     *
1525
     * @param \Doctrine\DBAL\Platforms\AbstractPlatform $databasePlatform
1526
     *
1527
     * @return string
1528
     */
1529
    private function getIntegerType(AbstractPlatform $databasePlatform): string
1530
    {
1531
        switch ($databasePlatform->getName()) {
1532
            case 'mysql':
1533
                return 'signed';
1534
            default:
1535
                return 'integer';
1536
        }
1537
    }
1538
1539
    /**
1540
     * Get all URL aliases for the given Location (including archived ones).
1541
     *
1542
     * @param int $locationId
1543
     *
1544
     * @return array
1545
     */
1546
    protected function getUrlAliasesForLocation(int $locationId): array
1547
    {
1548
        $queryBuilder = $this->connection->createQueryBuilder();
1549
        $queryBuilder
1550
            ->select(
1551
                't1.id',
1552
                't1.is_original',
1553
                't1.lang_mask',
1554
                't1.link',
1555
                't1.parent',
1556
                // show existing parent only if its row exists, special case for root parent
1557
                'CASE t1.parent WHEN 0 THEN 0 ELSE t2.id END AS existing_parent',
1558
                't1.text_md5'
1559
            )
1560
            ->from($this->table, 't1')
1561
            // selecting t2.id above will result in null if parent is broken
1562
            ->leftJoin('t1', $this->table, 't2', $queryBuilder->expr()->eq('t1.parent', 't2.id'))
1563
            ->where(
1564
                $queryBuilder->expr()->eq(
1565
                    't1.action',
1566
                    $queryBuilder->createPositionalParameter("eznode:{$locationId}")
1567
                )
1568
            );
1569
1570
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1571
    }
1572
1573
    /**
1574
     * Delete URL alias row by its primary composite key.
1575
     *
1576
     * @param int $parentId
1577
     * @param string $textMD5
1578
     *
1579
     * @return int number of affected rows
1580
     */
1581
    private function deleteRow(int $parentId, string $textMD5): int
1582
    {
1583
        $queryBuilder = $this->connection->createQueryBuilder();
1584
        $expr = $queryBuilder->expr();
1585
        $queryBuilder
1586
            ->delete($this->table)
1587
            ->where(
1588
                $expr->andX(
1589
                    $expr->eq(
1590
                        'parent',
1591
                        $queryBuilder->createPositionalParameter($parentId, ParameterType::INTEGER)
1592
                    ),
1593
                    $expr->eq(
1594
                        'text_md5',
1595
                        $queryBuilder->createPositionalParameter($textMD5)
1596
                    )
1597
                )
1598
            );
1599
1600
        return $queryBuilder->execute();
1601
    }
1602
}
1603