Completed
Push — EZP-31112-custom-aliases-gone-... ( 7e52f9...5caa2f )
by
unknown
18:51
created

deleteUrlNopAliasesWithoutChildren()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

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