Issues (3103)

Branch: master

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

Content/UrlAlias/Gateway/DoctrineDatabase.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
declare(strict_types=1);
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 eZ\Publish\Core\Base\Exceptions\BadStateException;
16
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator as LanguageMaskGenerator;
17
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway;
18
use RuntimeException;
19
20
/**
21
 * UrlAlias gateway implementation using the Doctrine database.
22
 *
23
 * @internal Gateway implementation is considered internal. Use Persistence UrlAlias Handler instead.
24
 *
25
 * @see \eZ\Publish\SPI\Persistence\Content\UrlAlias\Handler
26
 */
27
final 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
    private const URL_ALIAS_DATA_COLUMN_TYPE_MAP = [
36
        'id' => ParameterType::INTEGER,
37
        'link' => ParameterType::INTEGER,
38
        'is_alias' => ParameterType::INTEGER,
39
        'alias_redirects' => ParameterType::INTEGER,
40
        'is_original' => ParameterType::INTEGER,
41
        'action' => ParameterType::STRING,
42
        'action_type' => ParameterType::STRING,
43
        'lang_mask' => ParameterType::INTEGER,
44
        'text' => ParameterType::STRING,
45
        'parent' => ParameterType::INTEGER,
46
        'text_md5' => ParameterType::STRING,
47
    ];
48
49
    /** @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator */
50
    private $languageMaskGenerator;
51
52
    /**
53
     * Main URL database table name.
54
     *
55
     * @var string
56
     */
57
    private $table;
58
59
    /** @var \Doctrine\DBAL\Connection */
60
    private $connection;
61
62
    /** @var \Doctrine\DBAL\Platforms\AbstractPlatform */
63
    private $dbPlatform;
64
65
    /**
66
     * @throws \Doctrine\DBAL\DBALException
67
     */
68
    public function __construct(
69
        Connection $connection,
70
        LanguageMaskGenerator $languageMaskGenerator
71
    ) {
72
        $this->connection = $connection;
73
        $this->languageMaskGenerator = $languageMaskGenerator;
74
        $this->table = static::TABLE;
75
        $this->dbPlatform = $this->connection->getDatabasePlatform();
76
    }
77
78
    public function setTable(string $name): void
79
    {
80
        $this->table = $name;
81
    }
82
83
    /**
84
     * Loads all list of aliases by given $locationId.
85
     */
86
    public function loadAllLocationEntries(int $locationId): array
87
    {
88
        $query = $this->connection->createQueryBuilder();
89
        $query
90
            ->select(array_keys(self::URL_ALIAS_DATA_COLUMN_TYPE_MAP))
91
            ->from($this->connection->quoteIdentifier($this->table))
92
            ->where('action = :action')
93
            ->andWhere('is_original = :is_original')
94
            ->setParameter('action', "eznode:{$locationId}", ParameterType::STRING)
95
            ->setParameter('is_original', 1, ParameterType::INTEGER);
96
97
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
98
    }
99
100
    public function loadLocationEntries(
101
        int $locationId,
102
        bool $custom = false,
103
        ?int $languageId = null
104
    ): array {
105
        $query = $this->connection->createQueryBuilder();
106
        $expr = $query->expr();
107
        $query
108
            ->select(
109
                'id',
110
                'link',
111
                'is_alias',
112
                'alias_redirects',
113
                'lang_mask',
114
                'is_original',
115
                'parent',
116
                'text',
117
                'text_md5',
118
                'action'
119
            )
120
            ->from($this->connection->quoteIdentifier($this->table))
121
            ->where(
122
                $expr->eq(
123
                    'action',
124
                    $query->createPositionalParameter(
125
                        "eznode:{$locationId}",
126
                        ParameterType::STRING
127
                    )
128
                )
129
            )
130
            ->andWhere(
131
                $expr->eq(
132
                    'is_original',
133
                    $query->createPositionalParameter(1, ParameterType::INTEGER)
134
                )
135
            )
136
            ->andWhere(
137
                $expr->eq(
138
                    'is_alias',
139
                    $query->createPositionalParameter($custom ? 1 : 0, ParameterType::INTEGER)
140
                )
141
            )
142
        ;
143
144
        if (null !== $languageId) {
145
            $query->andWhere(
146
                $expr->gt(
147
                    $this->dbPlatform->getBitAndComparisonExpression(
148
                        'lang_mask',
149
                        $query->createPositionalParameter($languageId, ParameterType::INTEGER)
150
                    ),
151
                    0
152
                )
153
            );
154
        }
155
156
        $statement = $query->execute();
157
158
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
159
    }
160
161
    public function listGlobalEntries(
162
        ?string $languageCode = null,
163
        int $offset = 0,
164
        int $limit = -1
165
    ): array {
166
        $limit = $limit === -1 ? self::MAX_LIMIT : $limit;
167
168
        $query = $this->connection->createQueryBuilder();
169
        $expr = $query->expr();
170
        $query
171
            ->select(
172
                'action',
173
                'id',
174
                'link',
175
                'is_alias',
176
                'alias_redirects',
177
                'lang_mask',
178
                'is_original',
179
                'parent',
180
                'text_md5'
181
            )
182
            ->from($this->connection->quoteIdentifier($this->table))
183
            ->where(
184
                $expr->eq(
185
                    'action_type',
186
                    $query->createPositionalParameter(
187
                        'module',
188
                        ParameterType::STRING
189
                    )
190
                )
191
            )
192
            ->andWhere(
193
                $expr->eq(
194
                    'is_original',
195
                    $query->createPositionalParameter(1, ParameterType::INTEGER)
196
                )
197
            )
198
            ->andWhere(
199
                $expr->eq(
200
                    'is_alias',
201
                    $query->createPositionalParameter(1, ParameterType::INTEGER)
202
                )
203
            )
204
            ->setMaxResults(
205
                $limit
206
            )
207
            ->setFirstResult($offset);
208
209
        if (isset($languageCode)) {
210
            $query->andWhere(
211
                $expr->gt(
212
                    $this->dbPlatform->getBitAndComparisonExpression(
213
                        'lang_mask',
214
                        $query->createPositionalParameter(
215
                            $this->languageMaskGenerator->generateLanguageIndicator(
216
                                $languageCode,
217
                                false
218
                            ),
219
                            ParameterType::INTEGER
220
                        )
221
                    ),
222
                    0
223
                )
224
            );
225
        }
226
        $statement = $query->execute();
227
228
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
229
    }
230
231
    public function isRootEntry(int $id): bool
232
    {
233
        $query = $this->connection->createQueryBuilder();
234
        $query
235
            ->select(
236
                'text',
237
                'parent'
238
            )
239
            ->from($this->connection->quoteIdentifier($this->table))
240
            ->where(
241
                $query->expr()->eq(
242
                    'id',
243
                    $query->createPositionalParameter($id, ParameterType::INTEGER)
244
                )
245
            );
246
        $statement = $query->execute();
247
248
        $row = $statement->fetch(FetchMode::ASSOCIATIVE);
249
250
        return strlen($row['text']) == 0 && $row['parent'] == 0;
251
    }
252
253
    public function cleanupAfterPublish(
254
        string $action,
255
        int $languageId,
256
        int $newId,
257
        int $parentId,
258
        string $textMD5
259
    ): void {
260
        $query = $this->connection->createQueryBuilder();
261
        $expr = $query->expr();
262
        $query
263
            ->select(
264
                'parent',
265
                'text_md5',
266
                'lang_mask'
267
            )
268
            ->from($this->connection->quoteIdentifier($this->table))
269
            // 1) Autogenerated aliases that match action and language...
270
            ->where(
271
                $expr->eq(
272
                    'action',
273
                    $query->createPositionalParameter($action, ParameterType::STRING)
274
                )
275
            )
276
            ->andWhere(
277
                $expr->eq(
278
                    'is_original',
279
                    $query->createPositionalParameter(1, ParameterType::INTEGER)
280
                )
281
            )
282
            ->andWhere(
283
                $expr->eq(
284
                    'is_alias',
285
                    $query->createPositionalParameter(0, ParameterType::INTEGER)
286
                )
287
            )
288
            ->andWhere(
289
                $expr->gt(
290
                    $this->dbPlatform->getBitAndComparisonExpression(
291
                        'lang_mask',
292
                        $query->createPositionalParameter($languageId, ParameterType::INTEGER)
293
                    ),
294
                    0
295
                )
296
            )
297
            // 2) ...but not newly published entry
298
            ->andWhere(
299
                sprintf(
300
                    'NOT (%s)',
301
                    $expr->andX(
302
                        $expr->eq(
303
                            'parent',
304
                            $query->createPositionalParameter($parentId, ParameterType::INTEGER)
305
                        ),
306
                        $expr->eq(
307
                            'text_md5',
308
                            $query->createPositionalParameter($textMD5, ParameterType::STRING)
309
                        )
310
                    )
311
                )
312
            );
313
314
        $statement = $query->execute();
315
316
        $row = $statement->fetch(FetchMode::ASSOCIATIVE);
317
318
        if (!empty($row)) {
319
            $this->archiveUrlAliasForDeletedTranslation(
320
                (int)$row['lang_mask'],
321
                (int)$languageId,
322
                (int)$row['parent'],
323
                $row['text_md5'],
324
                (int)$newId
325
            );
326
        }
327
    }
328
329
    /**
330
     * Archive (remove or historize) obsolete URL aliases (for translations that were removed).
331
     *
332
     * @param int $languageMask all languages bit mask
333
     * @param int $languageId removed language Id
334
     * @param string $textMD5 checksum
335
     */
336
    private function archiveUrlAliasForDeletedTranslation(
337
        int $languageMask,
338
        int $languageId,
339
        int $parent,
340
        string $textMD5,
341
        int $linkId
342
    ): void {
343
        // If language mask is composite (consists of multiple languages) then remove given language from entry
344
        if ($languageMask & ~($languageId | 1)) {
345
            $this->removeTranslation($parent, $textMD5, $languageId);
346
        } else {
347
            // Otherwise mark entry as history
348
            $this->historize($parent, $textMD5, $linkId);
349
        }
350
    }
351
352
    public function historizeBeforeSwap(string $action, int $languageMask): void
353
    {
354
        $query = $this->connection->createQueryBuilder();
355
        $query
356
            ->update($this->connection->quoteIdentifier($this->table))
357
            ->set(
358
                'is_original',
359
                $query->createPositionalParameter(0, ParameterType::INTEGER)
360
            )
361
            ->set(
362
                'id',
363
                $query->createPositionalParameter(
364
                    $this->getNextId(),
365
                    ParameterType::INTEGER
366
                )
367
            )
368
            ->where(
369
                $query->expr()->andX(
370
                    $query->expr()->eq(
371
                        'action',
372
                        $query->createPositionalParameter($action, ParameterType::STRING)
373
                    ),
374
                    $query->expr()->eq(
375
                        'is_original',
376
                        $query->createPositionalParameter(1, ParameterType::INTEGER)
377
                    ),
378
                    $query->expr()->gt(
379
                        $this->dbPlatform->getBitAndComparisonExpression(
380
                            'lang_mask',
381
                            $query->createPositionalParameter(
382
                                $languageMask & ~1,
383
                                ParameterType::INTEGER
384
                            )
385
                        ),
386
                        0
387
                    )
388
                )
389
            );
390
391
        $query->execute();
392
    }
393
394
    /**
395
     * Update single row matched by composite primary key.
396
     *
397
     * Sets "is_original" to 0 thus marking entry as history.
398
     *
399
     * Re-links history entries.
400
     *
401
     * When location alias is published we need to check for new history entries created with self::downgrade()
402
     * with the same action and language, update their "link" column with id of the published entry.
403
     * History entry "id" column is moved to next id value so that all active (non-history) entries are kept
404
     * under the same id.
405
     */
406
    private function historize(int $parentId, string $textMD5, int $newId): void
407
    {
408
        $query = $this->connection->createQueryBuilder();
409
        $query
410
            ->update($this->connection->quoteIdentifier($this->table))
411
            ->set(
412
                'is_original',
413
                $query->createPositionalParameter(0, ParameterType::INTEGER)
414
            )
415
            ->set(
416
                'link',
417
                $query->createPositionalParameter($newId, ParameterType::INTEGER)
418
            )
419
            ->set(
420
                'id',
421
                $query->createPositionalParameter(
422
                    $this->getNextId(),
423
                    ParameterType::INTEGER
424
                )
425
            )
426
            ->where(
427
                $query->expr()->andX(
428
                    $query->expr()->eq(
429
                        'parent',
430
                        $query->createPositionalParameter($parentId, ParameterType::INTEGER)
431
                    ),
432
                    $query->expr()->eq(
433
                        'text_md5',
434
                        $query->createPositionalParameter($textMD5, ParameterType::STRING)
435
                    )
436
                )
437
            );
438
        $query->execute();
439
    }
440
441
    /**
442
     * Update single row data matched by composite primary key.
443
     *
444
     * Removes given $languageId from entry's language mask
445
     */
446
    private function removeTranslation(int $parentId, string $textMD5, int $languageId): void
447
    {
448
        $query = $this->connection->createQueryBuilder();
449
        $query
450
            ->update($this->connection->quoteIdentifier($this->table))
451
            ->set(
452
                'lang_mask',
453
                $this->dbPlatform->getBitAndComparisonExpression(
454
                    'lang_mask',
455
                    $query->createPositionalParameter(
456
                        ~$languageId,
457
                        ParameterType::INTEGER
458
                    )
459
                )
460
            )
461
            ->where(
462
                $query->expr()->eq(
463
                    'parent',
464
                    $query->createPositionalParameter(
465
                        $parentId,
466
                        ParameterType::INTEGER
467
                    )
468
                )
469
            )
470
            ->andWhere(
471
                $query->expr()->eq(
472
                    'text_md5',
473
                    $query->createPositionalParameter(
474
                        $textMD5,
475
                        ParameterType::STRING
476
                    )
477
                )
478
            )
479
        ;
480
        $query->execute();
481
    }
482
483
    public function historizeId(int $id, int $link): void
484
    {
485
        $query = $this->connection->createQueryBuilder();
486
        $query->select(
487
            'parent',
488
            'text_md5'
489
        )->from(
490
            $this->connection->quoteIdentifier($this->table)
491
        )->where(
492
            $query->expr()->andX(
493
                $query->expr()->eq(
494
                    'is_alias',
495
                    $query->createPositionalParameter(0, ParameterType::INTEGER)
496
                ),
497
                $query->expr()->eq(
498
                    'is_original',
499
                    $query->createPositionalParameter(1, ParameterType::INTEGER)
500
                ),
501
                $query->expr()->eq(
502
                    'action_type',
503
                    $query->createPositionalParameter(
504
                        'eznode',
505
                        ParameterType::STRING
506
                    )
507
                ),
508
                $query->expr()->eq(
509
                    'link',
510
                    $query->createPositionalParameter($id, ParameterType::INTEGER)
511
                )
512
            )
513
        );
514
515
        $statement = $query->execute();
516
517
        $rows = $statement->fetchAll(FetchMode::ASSOCIATIVE);
518
519
        foreach ($rows as $row) {
520
            $this->historize((int)$row['parent'], $row['text_md5'], $link);
521
        }
522
    }
523
524
    public function reparent(int $oldParentId, int $newParentId): void
525
    {
526
        $query = $this->connection->createQueryBuilder();
527
        $query->update(
528
            $this->connection->quoteIdentifier($this->table)
529
        )->set(
530
            'parent',
531
            $query->createPositionalParameter($newParentId, ParameterType::INTEGER)
532
        )->where(
533
            $query->expr()->andX(
534
                $query->expr()->eq(
535
                    'is_alias',
536
                    $query->createPositionalParameter(0, ParameterType::INTEGER)
537
                ),
538
                $query->expr()->eq(
539
                    'parent',
540
                    $query->createPositionalParameter(
541
                        $oldParentId,
542
                        ParameterType::INTEGER
543
                    )
544
                )
545
            )
546
        );
547
548
        $query->execute();
549
    }
550
551
    public function updateRow(int $parentId, string $textMD5, array $values): void
552
    {
553
        $query = $this->connection->createQueryBuilder();
554
        $query->update($this->connection->quoteIdentifier($this->table));
555
        foreach ($values as $columnName => $value) {
556
            $query->set(
557
                $columnName,
558
                $query->createNamedParameter(
559
                    $value,
560
                    self::URL_ALIAS_DATA_COLUMN_TYPE_MAP[$columnName],
561
                    ":{$columnName}"
562
                )
563
            );
564
        }
565
        $query
566
            ->where(
567
                $query->expr()->eq(
568
                    'parent',
569
                    $query->createNamedParameter($parentId, ParameterType::INTEGER, ':parent')
570
                )
571
            )
572
            ->andWhere(
573
                $query->expr()->eq(
574
                    'text_md5',
575
                    $query->createNamedParameter($textMD5, ParameterType::STRING, ':text_md5')
576
                )
577
            );
578
        $query->execute();
579
    }
580
581
    public function insertRow(array $values): int
582
    {
583
        if (!isset($values['id'])) {
584
            $values['id'] = $this->getNextId();
585
        }
586
        if (!isset($values['link'])) {
587
            $values['link'] = $values['id'];
588
        }
589
        if (!isset($values['is_original'])) {
590
            $values['is_original'] = ($values['id'] == $values['link'] ? 1 : 0);
591
        }
592
        if (!isset($values['is_alias'])) {
593
            $values['is_alias'] = 0;
594
        }
595
        if (!isset($values['alias_redirects'])) {
596
            $values['alias_redirects'] = 0;
597
        }
598
        if (
599
            !isset($values['action_type'])
600
            && preg_match('#^(.+):.*#', $values['action'], $matches)
601
        ) {
602
            $values['action_type'] = $matches[1];
603
        }
604
        if ($values['is_alias']) {
605
            $values['is_original'] = 1;
606
        }
607
        if ($values['action'] === 'nop:') {
608
            $values['is_original'] = 0;
609
        }
610
611
        $query = $this->connection->createQueryBuilder();
612
        $query->insert($this->connection->quoteIdentifier($this->table));
613
        foreach ($values as $columnName => $value) {
614
            $query->setValue(
615
                $columnName,
616
                $query->createNamedParameter(
617
                    $value,
618
                    self::URL_ALIAS_DATA_COLUMN_TYPE_MAP[$columnName],
619
                    ":{$columnName}"
620
                )
621
            );
622
        }
623
        $query->execute();
624
625
        return (int)$values['id'];
626
    }
627
628
    public function getNextId(): int
629
    {
630
        $query = $this->connection->createQueryBuilder();
631
        $query
632
            ->insert(self::INCR_TABLE)
633
            ->values(
634
                [
635
                    'id' => $this->dbPlatform->supportsSequences()
636
                        ? sprintf('NEXTVAL(\'%s\')', self::INCR_TABLE_SEQ)
637
                        : $query->createPositionalParameter(null, ParameterType::NULL),
638
                ]
639
            );
640
641
        $query->execute();
642
643
        return (int)$this->connection->lastInsertId(self::INCR_TABLE_SEQ);
644
    }
645
646
    public function loadRow(int $parentId, string $textMD5): array
647
    {
648
        $query = $this->connection->createQueryBuilder();
649
        $query->select('*')->from(
650
            $this->connection->quoteIdentifier($this->table)
651
        )->where(
652
            $query->expr()->andX(
653
                $query->expr()->eq(
654
                    'parent',
655
                    $query->createPositionalParameter(
656
                        $parentId,
657
                        ParameterType::INTEGER
658
                    )
659
                ),
660
                $query->expr()->eq(
661
                    'text_md5',
662
                    $query->createPositionalParameter(
663
                        $textMD5,
664
                        ParameterType::STRING
665
                    )
666
                )
667
            )
668
        );
669
670
        $result = $query->execute()->fetch(FetchMode::ASSOCIATIVE);
671
672
        return false !== $result ? $result : [];
673
    }
674
675
    public function loadUrlAliasData(array $urlHashes): array
676
    {
677
        $query = $this->connection->createQueryBuilder();
678
        $expr = $query->expr();
679
680
        $count = count($urlHashes);
681
        foreach ($urlHashes as $level => $urlPartHash) {
682
            $tableAlias = $level !== $count - 1 ? $this->table . $level : 'u';
683
            $query
684
                ->addSelect(
685
                    array_map(
686
                        function (string $columnName) use ($tableAlias) {
687
                            // do not alias data for top level url part
688
                            $columnAlias = 'u' === $tableAlias
689
                                ? $columnName
690
                                : "{$tableAlias}_{$columnName}";
691
                            $columnName = "{$tableAlias}.{$columnName}";
692
693
                            return "{$columnName} AS {$columnAlias}";
694
                        },
695
                        array_keys(self::URL_ALIAS_DATA_COLUMN_TYPE_MAP)
696
                    )
697
                )
698
                ->from($this->connection->quoteIdentifier($this->table), $tableAlias);
699
700
            $query
701
                ->andWhere(
702
                    $expr->eq(
703
                        "{$tableAlias}.text_md5",
704
                        $query->createPositionalParameter($urlPartHash, ParameterType::STRING)
705
                    )
706
                )
707
                ->andWhere(
708
                    $expr->eq(
709
                        "{$tableAlias}.parent",
710
                        // root entry has parent column set to 0
711
                        isset($previousTableName) ? $previousTableName . '.link' : $query->createPositionalParameter(
712
                            0,
713
                            ParameterType::INTEGER
714
                        )
715
                    )
716
                );
717
718
            $previousTableName = $tableAlias;
719
        }
720
        $query->setMaxResults(1);
721
722
        $result = $query->execute()->fetch(FetchMode::ASSOCIATIVE);
723
724
        return false !== $result ? $result : [];
725
    }
726
727
    public function loadAutogeneratedEntry(string $action, ?int $parentId = null): array
728
    {
729
        $query = $this->connection->createQueryBuilder();
730
        $query->select(
731
            '*'
732
        )->from(
733
            $this->connection->quoteIdentifier($this->table)
734
        )->where(
735
            $query->expr()->andX(
736
                $query->expr()->eq(
737
                    'action',
738
                    $query->createPositionalParameter($action, ParameterType::STRING)
739
                ),
740
                $query->expr()->eq(
741
                    'is_original',
742
                    $query->createPositionalParameter(1, ParameterType::INTEGER)
743
                ),
744
                $query->expr()->eq(
745
                    'is_alias',
746
                    $query->createPositionalParameter(0, ParameterType::INTEGER)
747
                )
748
            )
749
        );
750
751
        if (isset($parentId)) {
752
            $query->andWhere(
753
                $query->expr()->eq(
754
                    'parent',
755
                    $query->createPositionalParameter(
756
                        $parentId,
757
                        ParameterType::INTEGER
758
                    )
759
                )
760
            );
761
        }
762
763
        $entry = $query->execute()->fetch(FetchMode::ASSOCIATIVE);
764
765
        return false !== $entry ? $entry : [];
766
    }
767
768
    public function loadPathData(int $id): array
769
    {
770
        $pathData = [];
771
772
        while ($id != 0) {
773
            $query = $this->connection->createQueryBuilder();
774
            $query->select(
775
                'parent',
776
                'lang_mask',
777
                'text'
778
            )->from(
779
                $this->connection->quoteIdentifier($this->table)
780
            )->where(
781
                $query->expr()->eq(
782
                    'id',
783
                    $query->createPositionalParameter($id, ParameterType::INTEGER)
784
                )
785
            );
786
787
            $statement = $query->execute();
788
789
            $rows = $statement->fetchAll(FetchMode::ASSOCIATIVE);
790
            if (empty($rows)) {
791
                // Normally this should never happen
792
                $pathDataArray = [];
793
                foreach ($pathData as $path) {
794
                    if (!isset($path[0]['text'])) {
795
                        continue;
796
                    }
797
798
                    $pathDataArray[] = $path[0]['text'];
799
                }
800
801
                $path = implode('/', $pathDataArray);
802
                throw new BadStateException(
803
                    'id',
804
                    "Unable to load path data, path '{$path}' is broken, alias with ID '{$id}' not found. " .
805
                    'To fix all broken paths run the ezplatform:urls:regenerate-aliases command'
806
                );
807
            }
808
809
            $id = $rows[0]['parent'];
810
            array_unshift($pathData, $rows);
811
        }
812
813
        return $pathData;
814
    }
815
816
    public function loadPathDataByHierarchy(array $hierarchyData): array
817
    {
818
        $query = $this->connection->createQueryBuilder();
819
820
        $hierarchyConditions = [];
821
        foreach ($hierarchyData as $levelData) {
822
            $hierarchyConditions[] = $query->expr()->andX(
823
                $query->expr()->eq(
824
                    'parent',
825
                    $query->createPositionalParameter(
826
                        $levelData['parent'],
827
                        ParameterType::INTEGER
828
                    )
829
                ),
830
                $query->expr()->eq(
831
                    'action',
832
                    $query->createPositionalParameter(
833
                        $levelData['action'],
834
                        ParameterType::STRING
835
                    )
836
                ),
837
                $query->expr()->eq(
838
                    'id',
839
                    $query->createPositionalParameter(
840
                        $levelData['id'],
841
                        ParameterType::INTEGER
842
                    )
843
                )
844
            );
845
        }
846
847
        $query->select(
848
            'action',
849
            'lang_mask',
850
            'text'
851
        )->from(
852
            $this->connection->quoteIdentifier($this->table)
853
        )->where(
854
            $query->expr()->orX(...$hierarchyConditions)
855
        );
856
857
        $statement = $query->execute();
858
859
        $rows = $statement->fetchAll(FetchMode::ASSOCIATIVE);
860
        $rowsMap = [];
861
        foreach ($rows as $row) {
862
            $rowsMap[$row['action']][] = $row;
863
        }
864
865
        if (count($rowsMap) !== count($hierarchyData)) {
866
            throw new RuntimeException('The path is corrupted.');
867
        }
868
869
        $data = [];
870
        foreach ($hierarchyData as $levelData) {
871
            $data[] = $rowsMap[$levelData['action']];
872
        }
873
874
        return $data;
875
    }
876
877
    public function removeCustomAlias(int $parentId, string $textMD5): bool
878
    {
879
        $query = $this->connection->createQueryBuilder();
880
        $query->delete(
881
            $this->connection->quoteIdentifier($this->table)
882
        )->where(
883
            $query->expr()->andX(
884
                $query->expr()->eq(
885
                    'parent',
886
                    $query->createPositionalParameter(
887
                        $parentId,
888
                        ParameterType::INTEGER
889
                    )
890
                ),
891
                $query->expr()->eq(
892
                    'text_md5',
893
                    $query->createPositionalParameter(
894
                        $textMD5,
895
                        ParameterType::STRING
896
                    )
897
                ),
898
                $query->expr()->eq(
899
                    'is_alias',
900
                    $query->createPositionalParameter(1, ParameterType::INTEGER)
901
                )
902
            )
903
        );
904
905
        return $query->execute() === 1;
906
    }
907
908
    public function remove(string $action, ?int $id = null): void
909
    {
910
        $query = $this->connection->createQueryBuilder();
911
        $expr = $query->expr();
912
        $query
913
            ->delete($this->connection->quoteIdentifier($this->table))
914
            ->where(
915
                $expr->eq(
916
                    'action',
917
                    $query->createPositionalParameter($action, ParameterType::STRING)
918
                )
919
            );
920
921
        if ($id !== null) {
922
            $query
923
                ->andWhere(
924
                    $expr->eq(
925
                        'is_alias',
926
                        $query->createPositionalParameter(0, ParameterType::INTEGER)
927
                    ),
928
                    )
0 ignored issues
show
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected ')'
Loading history...
929
                ->andWhere(
930
                    $expr->eq(
931
                        'id',
932
                        $query->createPositionalParameter(
933
                            $id,
934
                            ParameterType::INTEGER
935
                        )
936
                    )
937
                );
938
        }
939
940
        $query->execute();
941
    }
942
943
    public function loadAutogeneratedEntries(int $parentId, bool $includeHistory = false): array
944
    {
945
        $query = $this->connection->createQueryBuilder();
946
        $expr = $query->expr();
947
        $query
948
            ->select('*')
949
            ->from($this->connection->quoteIdentifier($this->table))
950
            ->where(
951
                $expr->eq(
952
                    'parent',
953
                    $query->createPositionalParameter(
954
                        $parentId,
955
                        ParameterType::INTEGER
956
                    )
957
                ),
958
                )
959
            ->andWhere(
960
                $expr->eq(
961
                    'action_type',
962
                    $query->createPositionalParameter(
963
                        'eznode',
964
                        ParameterType::STRING
965
                    )
966
                )
967
            )
968
            ->andWhere(
969
                $expr->eq(
970
                    'is_alias',
971
                    $query->createPositionalParameter(0, ParameterType::INTEGER)
972
                )
973
            );
974
975
        if (!$includeHistory) {
976
            $query->andWhere(
977
                $expr->eq(
978
                    'is_original',
979
                    $query->createPositionalParameter(1, ParameterType::INTEGER)
980
                )
981
            );
982
        }
983
984
        $statement = $query->execute();
985
986
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
987
    }
988
989
    public function getLocationContentMainLanguageId(int $locationId): int
990
    {
991
        $queryBuilder = $this->connection->createQueryBuilder();
992
        $expr = $queryBuilder->expr();
993
        $queryBuilder
994
            ->select('c.initial_language_id')
995
            ->from('ezcontentobject', 'c')
996
            ->join('c', 'ezcontentobject_tree', 't', $expr->eq('t.contentobject_id', 'c.id'))
997
            ->where(
998
                $expr->eq('t.node_id', ':locationId')
999
            )
1000
            ->setParameter('locationId', $locationId, ParameterType::INTEGER);
1001
1002
        $statement = $queryBuilder->execute();
1003
        $languageId = $statement->fetchColumn();
1004
1005
        if ($languageId === false) {
1006
            throw new RuntimeException("Could not find Content for Location #{$locationId}");
1007
        }
1008
1009
        return (int)$languageId;
1010
    }
1011
1012
    public function bulkRemoveTranslation(int $languageId, array $actions): void
1013
    {
1014
        $query = $this->connection->createQueryBuilder();
1015
        $query
1016
            ->update($this->connection->quoteIdentifier($this->table))
1017
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
1018
            ->set('lang_mask', 'lang_mask & ~ ' . $languageId)
1019
            ->where('action IN (:actions)')
1020
            ->setParameter(':actions', $actions, Connection::PARAM_STR_ARRAY);
1021
        $query->execute();
1022
1023
        // cleanup: delete single language rows (including alwaysAvailable)
1024
        $query = $this->connection->createQueryBuilder();
1025
        $query
1026
            ->delete($this->connection->quoteIdentifier($this->table))
1027
            ->where('action IN (:actions)')
1028
            ->andWhere('lang_mask IN (0, 1)')
1029
            ->setParameter(':actions', $actions, Connection::PARAM_STR_ARRAY);
1030
        $query->execute();
1031
    }
1032
1033
    public function archiveUrlAliasesForDeletedTranslations(
1034
        int $locationId,
1035
        int $parentId,
1036
        array $languageIds
1037
    ): void {
1038
        // determine proper parent for linking historized entry
1039
        $existingLocationEntry = $this->loadAutogeneratedEntry(
1040
            'eznode:' . $locationId,
1041
            $parentId
1042
        );
1043
1044
        // filter existing URL alias entries by any of the specified removed languages
1045
        $rows = $this->loadLocationEntriesMatchingMultipleLanguages(
1046
            $locationId,
1047
            $languageIds
1048
        );
1049
1050
        // remove specific languages from a bit mask
1051
        foreach ($rows as $row) {
1052
            // filter mask to reduce the number of calls to storage engine
1053
            $rowLanguageMask = (int)$row['lang_mask'];
1054
            $languageIdsToBeRemoved = array_filter(
1055
                $languageIds,
1056
                function ($languageId) use ($rowLanguageMask) {
1057
                    return $languageId & $rowLanguageMask;
1058
                }
1059
            );
1060
1061
            if (empty($languageIdsToBeRemoved)) {
1062
                continue;
1063
            }
1064
1065
            // use existing entry to link archived alias or use current alias id
1066
            $linkToId = !empty($existingLocationEntry)
1067
                ? (int)$existingLocationEntry['id']
1068
                : (int)$row['id'];
1069
            foreach ($languageIdsToBeRemoved as $languageId) {
1070
                $this->archiveUrlAliasForDeletedTranslation(
1071
                    (int)$row['lang_mask'],
1072
                    (int)$languageId,
1073
                    (int)$row['parent'],
1074
                    $row['text_md5'],
1075
                    $linkToId
1076
                );
1077
            }
1078
        }
1079
    }
1080
1081
    /**
1082
     * Load list of aliases for given $locationId matching any of the specified Languages.
1083
     *
1084
     * @param int[] $languageIds
1085
     */
1086
    private function loadLocationEntriesMatchingMultipleLanguages(
1087
        int $locationId,
1088
        array $languageIds
1089
    ): array {
1090
        // note: alwaysAvailable for this use case is not relevant
1091
        $languageMask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageIds(
1092
            $languageIds,
1093
            false
1094
        );
1095
1096
        /** @var \Doctrine\DBAL\Connection $connection */
1097
        $query = $this->connection->createQueryBuilder();
1098
        $query
1099
            ->select('id', 'lang_mask', 'parent', 'text_md5')
1100
            ->from($this->connection->quoteIdentifier($this->table))
1101
            ->where('action = :action')
1102
            // fetch rows matching any of the given Languages
1103
            ->andWhere('lang_mask & :languageMask <> 0')
1104
            ->setParameter(':action', 'eznode:' . $locationId)
1105
            ->setParameter(':languageMask', $languageMask);
1106
1107
        $statement = $query->execute();
1108
1109
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
1110
    }
1111
1112
    /**
1113
     * @throws \Doctrine\DBAL\DBALException
1114
     */
1115
    public function deleteUrlAliasesWithoutLocation(): int
1116
    {
1117
        $dbPlatform = $this->connection->getDatabasePlatform();
1118
1119
        $subQuery = $this->connection->createQueryBuilder();
1120
        $subQuery
1121
            ->select('node_id')
1122
            ->from('ezcontentobject_tree', 't')
1123
            ->where(
1124
                $subQuery->expr()->eq(
1125
                    't.node_id',
1126
                    sprintf(
1127
                        'CAST(%s as %s)',
1128
                        $dbPlatform->getSubstringExpression(
1129
                            $this->connection->quoteIdentifier($this->table) . '.action',
1130
                            8
1131
                        ),
1132
                        $this->getIntegerType()
1133
                    )
1134
                )
1135
            );
1136
1137
        $deleteQuery = $this->connection->createQueryBuilder();
1138
        $deleteQuery
1139
            ->delete($this->connection->quoteIdentifier($this->table))
1140
            ->where(
1141
                $deleteQuery->expr()->eq(
1142
                    'action_type',
1143
                    $deleteQuery->createPositionalParameter('eznode')
1144
                )
1145
            )
1146
            ->andWhere(
1147
                sprintf('NOT EXISTS (%s)', $subQuery->getSQL())
1148
            );
1149
1150
        return $deleteQuery->execute();
1151
    }
1152
1153
    public function deleteUrlAliasesWithoutParent(): int
1154
    {
1155
        $existingAliasesQuery = $this->getAllUrlAliasesQuery();
1156
1157
        $query = $this->connection->createQueryBuilder();
1158
        $query
1159
            ->delete($this->connection->quoteIdentifier($this->table))
1160
            ->where(
1161
                $query->expr()->neq(
1162
                    'parent',
1163
                    $query->createPositionalParameter(0, ParameterType::INTEGER)
1164
                )
1165
            )
1166
            ->andWhere(
1167
                $query->expr()->notIn(
1168
                    'parent',
1169
                    $existingAliasesQuery
1170
                )
1171
            );
1172
1173
        return $query->execute();
1174
    }
1175
1176
    public function deleteUrlAliasesWithBrokenLink(): int
1177
    {
1178
        $existingAliasesQuery = $this->getAllUrlAliasesQuery();
1179
1180
        $query = $this->connection->createQueryBuilder();
1181
        $query
1182
            ->delete($this->connection->quoteIdentifier($this->table))
1183
            ->where(
1184
                $query->expr()->neq('id', 'link')
1185
            )
1186
            ->andWhere(
1187
                $query->expr()->notIn(
1188
                    'link',
1189
                    $existingAliasesQuery
1190
                )
1191
            );
1192
1193
        return (int)$query->execute();
1194
    }
1195
1196
    public function repairBrokenUrlAliasesForLocation(int $locationId): void
1197
    {
1198
        $urlAliasesData = $this->getUrlAliasesForLocation($locationId);
1199
1200
        $originalUrlAliases = $this->filterOriginalAliases($urlAliasesData);
1201
1202
        if (count($originalUrlAliases) === count($urlAliasesData)) {
1203
            // no archived aliases - nothing to fix
1204
            return;
1205
        }
1206
1207
        $updateQueryBuilder = $this->connection->createQueryBuilder();
1208
        $expr = $updateQueryBuilder->expr();
1209
        $updateQueryBuilder
1210
            ->update($this->connection->quoteIdentifier($this->table))
1211
            ->set('link', ':linkId')
1212
            ->set('parent', ':newParentId')
1213
            ->where(
1214
                $expr->eq('action', ':action')
1215
            )
1216
            ->andWhere(
1217
                $expr->eq(
1218
                    'is_original',
1219
                    $updateQueryBuilder->createNamedParameter(0, ParameterType::INTEGER)
1220
                )
1221
            )
1222
            ->andWhere(
1223
                $expr->eq('parent', ':oldParentId')
1224
            )
1225
            ->andWhere(
1226
                $expr->eq('text_md5', ':textMD5')
1227
            )
1228
            ->setParameter(':action', "eznode:{$locationId}");
1229
1230
        foreach ($urlAliasesData as $urlAliasData) {
1231
            if ($urlAliasData['is_original'] === 1 || !isset($originalUrlAliases[$urlAliasData['lang_mask']])) {
1232
                // ignore non-archived entries and deleted Translations
1233
                continue;
1234
            }
1235
1236
            $originalUrlAlias = $originalUrlAliases[$urlAliasData['lang_mask']];
1237
1238
            if ($urlAliasData['link'] === $originalUrlAlias['link']) {
1239
                // ignore correct entries to avoid unnecessary updates
1240
                continue;
1241
            }
1242
1243
            $updateQueryBuilder
1244
                ->setParameter(':linkId', $originalUrlAlias['link'], ParameterType::INTEGER)
1245
                // attempt to fix missing parent case
1246
                ->setParameter(
1247
                    ':newParentId',
1248
                    $urlAliasData['existing_parent'] ?? $originalUrlAlias['parent'],
1249
                    ParameterType::INTEGER
1250
                )
1251
                ->setParameter(':oldParentId', $urlAliasData['parent'], ParameterType::INTEGER)
1252
                ->setParameter(':textMD5', $urlAliasData['text_md5']);
1253
1254
            try {
1255
                $updateQueryBuilder->execute();
1256
            } catch (UniqueConstraintViolationException $e) {
1257
                // edge case: if such row already exists, there's no way to restore history
1258
                $this->deleteRow($urlAliasData['parent'], $urlAliasData['text_md5']);
1259
            }
1260
        }
1261
    }
1262
1263
    /**
1264
     * @throws \Doctrine\DBAL\DBALException
1265
     */
1266
    public function deleteUrlNopAliasesWithoutChildren(): int
1267
    {
1268
        $platform = $this->connection->getDatabasePlatform();
1269
        $queryBuilder = $this->connection->createQueryBuilder();
1270
1271
        // The wrapper select is needed for SQL "Derived Table Merge" issue for deleting
1272
        $wrapperQueryBuilder = clone $queryBuilder;
1273
        $selectQueryBuilder = clone $queryBuilder;
1274
        $expressionBuilder = $queryBuilder->expr();
1275
1276
        $selectQueryBuilder
1277
            ->select('u_parent.id AS inner_id')
1278
            ->from($this->table, 'u_parent')
1279
            ->leftJoin(
1280
                'u_parent',
1281
                $this->table,
1282
                'u',
1283
                $expressionBuilder->eq('u_parent.id', 'u.parent')
1284
            )
1285
            ->where(
1286
                $expressionBuilder->eq(
1287
                    'u_parent.action_type',
1288
                    ':actionType'
1289
                )
1290
            )
1291
            ->groupBy('u_parent.id')
1292
            ->having(
1293
                $expressionBuilder->eq($platform->getCountExpression('u.id'), 0)
1294
            );
1295
1296
        $wrapperQueryBuilder
1297
            ->select('inner_id')
1298
            ->from(
1299
                sprintf('(%s)', $selectQueryBuilder), 'wrapper'
1300
            )
1301
            ->where('id = inner_id');
1302
1303
        $queryBuilder
1304
            ->delete($this->table)
1305
            ->where(
1306
                sprintf('EXISTS (%s)', $wrapperQueryBuilder)
1307
            )
1308
            ->setParameter('actionType', 'nop');
1309
1310
        return $queryBuilder->execute();
1311
    }
1312
1313
    /**
1314
     * @throws \Doctrine\DBAL\DBALException
1315
     */
1316
    public function getAllChildrenAliases(int $parentId): array
1317
    {
1318
        $queryBuilder = $this->connection->createQueryBuilder();
1319
        $expressionBuilder = $queryBuilder->expr();
1320
1321
        $queryBuilder
1322
            ->select('parent', 'text_md5')
1323
            ->from($this->table)
1324
            ->where(
1325
                $expressionBuilder->eq(
1326
                    'parent',
1327
                    $queryBuilder->createPositionalParameter($parentId, ParameterType::INTEGER)
1328
                )
1329
            )->andWhere(
1330
                $expressionBuilder->eq(
1331
                    'is_alias',
1332
                    $queryBuilder->createPositionalParameter(1, ParameterType::INTEGER)
1333
                )
1334
            );
1335
1336
        return $queryBuilder->execute()->fetchAll();
1337
    }
1338
1339
    /**
1340
     * Filter from the given result set original (current) only URL aliases and index them by language_mask.
1341
     *
1342
     * Note: each language_mask can have one URL Alias.
1343
     *
1344
     * @param array $urlAliasesData
1345
     */
1346
    private function filterOriginalAliases(array $urlAliasesData): array
1347
    {
1348
        $originalUrlAliases = array_filter(
1349
            $urlAliasesData,
1350
            function ($urlAliasData) {
1351
                // filter is_original=true ignoring broken parent records (cleaned up elsewhere)
1352
                return (bool)$urlAliasData['is_original'] && $urlAliasData['existing_parent'] !== null;
1353
            }
1354
        );
1355
1356
        // return language_mask-indexed array
1357
        return array_combine(
1358
            array_column($originalUrlAliases, 'lang_mask'),
1359
            $originalUrlAliases
1360
        );
1361
    }
1362
1363
    /**
1364
     * Get sub-query for IDs of all URL aliases.
1365
     */
1366
    private function getAllUrlAliasesQuery(): string
1367
    {
1368
        $existingAliasesQueryBuilder = $this->connection->createQueryBuilder();
1369
        $innerQueryBuilder = $this->connection->createQueryBuilder();
1370
1371
        return $existingAliasesQueryBuilder
1372
            ->select('tmp.id')
1373
            ->from(
1374
            // nest sub-query to avoid same-table update error
1375
                '(' . $innerQueryBuilder->select('id')->from(
1376
                    $this->connection->quoteIdentifier($this->table)
1377
                )->getSQL() . ')',
1378
                'tmp'
1379
            )
1380
            ->getSQL();
1381
    }
1382
1383
    /**
1384
     * Get DBMS-specific integer type.
1385
     */
1386
    private function getIntegerType(): string
1387
    {
1388
        return $this->dbPlatform->getName() === 'mysql' ? 'signed' : 'integer';
1389
    }
1390
1391
    /**
1392
     * Get all URL aliases for the given Location (including archived ones).
1393
     */
1394
    private function getUrlAliasesForLocation(int $locationId): array
1395
    {
1396
        $queryBuilder = $this->connection->createQueryBuilder();
1397
        $queryBuilder
1398
            ->select(
1399
                't1.id',
1400
                't1.is_original',
1401
                't1.lang_mask',
1402
                't1.link',
1403
                't1.parent',
1404
                // show existing parent only if its row exists, special case for root parent
1405
                'CASE t1.parent WHEN 0 THEN 0 ELSE t2.id END AS existing_parent',
1406
                't1.text_md5'
1407
            )
1408
            ->from($this->connection->quoteIdentifier($this->table), 't1')
1409
            // selecting t2.id above will result in null if parent is broken
1410
            ->leftJoin(
1411
                't1',
1412
                $this->connection->quoteIdentifier($this->table),
1413
                't2',
1414
                $queryBuilder->expr()->eq('t1.parent', 't2.id')
1415
            )
1416
            ->where(
1417
                $queryBuilder->expr()->eq(
1418
                    't1.action',
1419
                    $queryBuilder->createPositionalParameter("eznode:{$locationId}")
1420
                )
1421
            );
1422
1423
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1424
    }
1425
1426
    /**
1427
     * Delete URL alias row by its primary composite key.
1428
     */
1429
    private function deleteRow(int $parentId, string $textMD5): int
1430
    {
1431
        $queryBuilder = $this->connection->createQueryBuilder();
1432
        $expr = $queryBuilder->expr();
1433
        $queryBuilder
1434
            ->delete($this->connection->quoteIdentifier($this->table))
1435
            ->where(
1436
                $expr->eq(
1437
                    'parent',
1438
                    $queryBuilder->createPositionalParameter($parentId, ParameterType::INTEGER)
1439
                )
1440
            )
1441
            ->andWhere(
1442
                $expr->eq(
1443
                    'text_md5',
1444
                    $queryBuilder->createPositionalParameter($textMD5)
1445
                )
1446
            )
1447
        ;
1448
1449
        return $queryBuilder->execute();
1450
    }
1451
}
1452