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