Completed
Push — master ( ee6594...6a5050 )
by
unknown
76:11 queued 51:14
created

DoctrineDatabase::deleteRow()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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