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.

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

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
namespace eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
8
9
use Doctrine\DBAL\Connection;
10
use Doctrine\DBAL\DBALException;
11
use Doctrine\DBAL\FetchMode;
12
use Doctrine\DBAL\ParameterType;
13
use Doctrine\DBAL\Query\QueryBuilder as DoctrineQueryBuilder;
14
use eZ\Publish\API\Repository\Values\Content\Relation;
15
use eZ\Publish\Core\Base\Exceptions\BadStateException;
16
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
17
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder;
18
use eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue;
19
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator as LanguageMaskGenerator;
20
use eZ\Publish\Core\Persistence\Legacy\SharedGateway\Gateway as SharedGateway;
21
use eZ\Publish\SPI\Persistence\Content;
22
use eZ\Publish\SPI\Persistence\Content\CreateStruct;
23
use eZ\Publish\SPI\Persistence\Content\UpdateStruct;
24
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct;
25
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
26
use eZ\Publish\SPI\Persistence\Content\VersionInfo;
27
use eZ\Publish\SPI\Persistence\Content\Field;
28
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct;
29
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
30
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
31
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
32
use DOMXPath;
33
use DOMDocument;
34
35
/**
36
 * Doctrine database based content gateway.
37
 *
38
 * @internal Gateway implementation is considered internal. Use Persistence Content Handler instead.
39
 *
40
 * @see \eZ\Publish\SPI\Persistence\Content\Handler
41
 */
42
final class DoctrineDatabase extends Gateway
43
{
44
    /**
45
     * Pre-computed integer constant which, when combined with proper bit-wise operator,
46
     * removes always available flag from the mask.
47
     */
48
    private const REMOVE_ALWAYS_AVAILABLE_LANG_MASK_OPERAND = -2;
49
50
    /**
51
     * The native Doctrine connection.
52
     *
53
     * Meant to be used to transition from eZ/Zeta interface to Doctrine.
54
     *
55
     * @var \Doctrine\DBAL\Connection
56
     */
57
    protected $connection;
58
59
    /**
60
     * Query builder.
61
     *
62
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder
63
     */
64
    protected $queryBuilder;
65
66
    /**
67
     * Caching language handler.
68
     *
69
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
70
     */
71
    protected $languageHandler;
72
73
    /**
74
     * Language mask generator.
75
     *
76
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
77
     */
78
    protected $languageMaskGenerator;
79
80
    /** @var \eZ\Publish\Core\Persistence\Legacy\SharedGateway\Gateway */
81
    private $sharedGateway;
82
83
    /** @var \Doctrine\DBAL\Platforms\AbstractPlatform */
84
    private $databasePlatform;
85
86
    /**
87
     * @throws \Doctrine\DBAL\DBALException
88
     */
89
    public function __construct(
90
        Connection $connection,
91
        SharedGateway $sharedGateway,
92
        QueryBuilder $queryBuilder,
93
        LanguageHandler $languageHandler,
94
        LanguageMaskGenerator $languageMaskGenerator
95
    ) {
96
        $this->connection = $connection;
97
        $this->databasePlatform = $connection->getDatabasePlatform();
98
        $this->sharedGateway = $sharedGateway;
99
        $this->queryBuilder = $queryBuilder;
100
        $this->languageHandler = $languageHandler;
101
        $this->languageMaskGenerator = $languageMaskGenerator;
102
    }
103
104
    public function insertContentObject(CreateStruct $struct, int $currentVersionNo = 1): int
105
    {
106
        $initialLanguageId = !empty($struct->mainLanguageId) ? $struct->mainLanguageId : $struct->initialLanguageId;
107
        $initialLanguageCode = $this->languageHandler->load($initialLanguageId)->languageCode;
108
109
        $name = $struct->name[$initialLanguageCode] ?? '';
110
111
        $query = $this->connection->createQueryBuilder();
112
        $query
113
            ->insert(self::CONTENT_ITEM_TABLE)
114
            ->values(
115
                [
116
                    'current_version' => $query->createPositionalParameter(
117
                        $currentVersionNo,
118
                        ParameterType::INTEGER
119
                    ),
120
                    'name' => $query->createPositionalParameter($name),
121
                    'contentclass_id' => $query->createPositionalParameter(
122
                        $struct->typeId,
123
                        ParameterType::INTEGER
124
                    ),
125
                    'section_id' => $query->createPositionalParameter(
126
                        $struct->sectionId,
127
                        ParameterType::INTEGER
128
                    ),
129
                    'owner_id' => $query->createPositionalParameter(
130
                        $struct->ownerId,
131
                        ParameterType::INTEGER
132
                    ),
133
                    'initial_language_id' => $query->createPositionalParameter(
134
                        $initialLanguageId,
135
                        ParameterType::INTEGER
136
                    ),
137
                    'remote_id' => $query->createPositionalParameter($struct->remoteId),
138
                    'modified' => $query->createPositionalParameter(0, ParameterType::INTEGER),
139
                    'published' => $query->createPositionalParameter(0, ParameterType::INTEGER),
140
                    'status' => $query->createPositionalParameter(
141
                        ContentInfo::STATUS_DRAFT,
142
                        ParameterType::INTEGER
143
                    ),
144
                    'language_mask' => $query->createPositionalParameter(
145
                        $this->languageMaskGenerator->generateLanguageMaskForFields(
146
                            $struct->fields,
147
                            $initialLanguageCode,
148
                            $struct->alwaysAvailable
149
                        ),
150
                        ParameterType::INTEGER
151
                    ),
152
                ]
153
            );
154
155
        $query->execute();
156
157
        return (int)$this->connection->lastInsertId(self::CONTENT_ITEM_SEQ);
158
    }
159
160
    public function insertVersion(VersionInfo $versionInfo, array $fields): int
161
    {
162
        $query = $this->connection->createQueryBuilder();
163
        $query
164
            ->insert(self::CONTENT_VERSION_TABLE)
165
            ->values(
166
                [
167
                    'version' => $query->createPositionalParameter(
168
                        $versionInfo->versionNo,
169
                        ParameterType::INTEGER
170
                    ),
171
                    'modified' => $query->createPositionalParameter(
172
                        $versionInfo->modificationDate,
173
                        ParameterType::INTEGER
174
                    ),
175
                    'creator_id' => $query->createPositionalParameter(
176
                        $versionInfo->creatorId,
177
                        ParameterType::INTEGER
178
                    ),
179
                    'created' => $query->createPositionalParameter(
180
                        $versionInfo->creationDate,
181
                        ParameterType::INTEGER
182
                    ),
183
                    'status' => $query->createPositionalParameter(
184
                        $versionInfo->status,
185
                        ParameterType::INTEGER
186
                    ),
187
                    'initial_language_id' => $query->createPositionalParameter(
188
                        $this->languageHandler->loadByLanguageCode(
189
                            $versionInfo->initialLanguageCode
190
                        )->id,
191
                        ParameterType::INTEGER
192
                    ),
193
                    'contentobject_id' => $query->createPositionalParameter(
194
                        $versionInfo->contentInfo->id,
195
                        ParameterType::INTEGER
196
                    ),
197
                    'language_mask' => $query->createPositionalParameter(
198
                        $this->languageMaskGenerator->generateLanguageMaskForFields(
199
                            $fields,
200
                            $versionInfo->initialLanguageCode,
201
                            $versionInfo->contentInfo->alwaysAvailable
202
                        ),
203
                        ParameterType::INTEGER
204
                    ),
205
                ]
206
            );
207
208
        $query->execute();
209
210
        return (int)$this->connection->lastInsertId(self::CONTENT_VERSION_SEQ);
211
    }
212
213
    public function updateContent(
214
        int $contentId,
215
        MetadataUpdateStruct $struct,
216
        ?VersionInfo $prePublishVersionInfo = null
217
    ): void {
218
        $query = $this->connection->createQueryBuilder();
219
        $query->update(self::CONTENT_ITEM_TABLE);
220
221
        $fieldsForUpdateMap = [
222
            'name' => [
223
                'value' => $struct->name,
224
                'type' => ParameterType::STRING,
225
            ],
226
            'initial_language_id' => [
227
                'value' => $struct->mainLanguageId,
228
                'type' => ParameterType::INTEGER,
229
            ],
230
            'modified' => [
231
                'value' => $struct->modificationDate,
232
                'type' => ParameterType::INTEGER,
233
            ],
234
            'owner_id' => [
235
                'value' => $struct->ownerId,
236
                'type' => ParameterType::INTEGER,
237
            ],
238
            'published' => [
239
                'value' => $struct->publicationDate,
240
                'type' => ParameterType::INTEGER,
241
            ],
242
            'remote_id' => [
243
                'value' => $struct->remoteId,
244
                'type' => ParameterType::STRING,
245
            ],
246
            'is_hidden' => [
247
                'value' => $struct->isHidden,
248
                'type' => ParameterType::BOOLEAN,
249
            ],
250
        ];
251
252
        foreach ($fieldsForUpdateMap as $fieldName => $field) {
253
            if (null === $field['value']) {
254
                continue;
255
            }
256
            $query->set(
257
                $fieldName,
258
                $query->createNamedParameter($field['value'], $field['type'], ":{$fieldName}")
259
            );
260
        }
261
262
        if ($prePublishVersionInfo !== null) {
263
            $mask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(
264
                $prePublishVersionInfo->languageCodes,
265
                $struct->alwaysAvailable ?? $prePublishVersionInfo->contentInfo->alwaysAvailable
266
            );
267
            $query->set(
268
                'language_mask',
269
                $query->createNamedParameter($mask, ParameterType::INTEGER, ':languageMask')
270
            );
271
        }
272
273
        $query->where(
274
            $query->expr()->eq(
275
                'id',
276
                $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
277
            )
278
        );
279
280
        if (!empty($query->getQueryPart('set'))) {
281
            $query->execute();
282
        }
283
284
        // Handle alwaysAvailable flag update separately as it's a more complex task and has impact on several tables
285
        if (isset($struct->alwaysAvailable) || isset($struct->mainLanguageId)) {
286
            $this->updateAlwaysAvailableFlag($contentId, $struct->alwaysAvailable);
287
        }
288
    }
289
290
    /**
291
     * Updates version $versionNo for content identified by $contentId, in respect to $struct.
292
     *
293
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
294
     */
295
    public function updateVersion(int $contentId, int $versionNo, UpdateStruct $struct): void
296
    {
297
        $query = $this->connection->createQueryBuilder();
298
299
        $query
300
            ->update(self::CONTENT_VERSION_TABLE)
301
            ->set('creator_id', ':creator_id')
302
            ->set('modified', ':modified')
303
            ->set('initial_language_id', ':initial_language_id')
304
            ->set(
305
                'language_mask',
306
                $this->databasePlatform->getBitOrComparisonExpression(
307
                    'language_mask',
308
                    ':language_mask'
309
                )
310
            )
311
            ->setParameter('creator_id', $struct->creatorId, ParameterType::INTEGER)
312
            ->setParameter('modified', $struct->modificationDate, ParameterType::INTEGER)
313
            ->setParameter(
314
                'initial_language_id',
315
                $struct->initialLanguageId,
316
                ParameterType::INTEGER
317
            )
318
            ->setParameter(
319
                'language_mask',
320
                $this->languageMaskGenerator->generateLanguageMaskForFields(
321
                    $struct->fields,
322
                    $this->languageHandler->load($struct->initialLanguageId)->languageCode,
323
                    false
324
                ),
325
                ParameterType::INTEGER
326
            )
327
            ->where('contentobject_id = :content_id')
328
            ->andWhere('version = :version_no')
329
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
330
            ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
331
332
        $query->execute();
333
    }
334
335
    public function updateAlwaysAvailableFlag(int $contentId, ?bool $alwaysAvailable = null): void
336
    {
337
        // We will need to know some info on the current language mask to update the flag
338
        // everywhere needed
339
        $contentInfoRow = $this->loadContentInfo($contentId);
340
        $versionNo = (int)$contentInfoRow['current_version'];
341
        $languageMask = (int)$contentInfoRow['language_mask'];
342
        $initialLanguageId = (int)$contentInfoRow['initial_language_id'];
343
        if (!isset($alwaysAvailable)) {
344
            $alwaysAvailable = 1 === ($languageMask & 1);
345
        }
346
347
        $this->updateContentItemAlwaysAvailableFlag($contentId, $alwaysAvailable);
348
        $this->updateContentNameAlwaysAvailableFlag(
349
            $contentId,
350
            $versionNo,
351
            $alwaysAvailable
352
        );
353
        $this->updateContentFieldsAlwaysAvailableFlag(
354
            $contentId,
355
            $versionNo,
356
            $alwaysAvailable,
357
            $languageMask,
358
            $initialLanguageId
359
        );
360
    }
361
362
    private function updateContentItemAlwaysAvailableFlag(
363
        int $contentId,
364
        bool $alwaysAvailable
365
    ): void {
366
        $query = $this->connection->createQueryBuilder();
367
        $expr = $query->expr();
368
        $query
369
            ->update(self::CONTENT_ITEM_TABLE);
370
        $this
371
            ->setLanguageMaskForUpdateQuery($alwaysAvailable, $query, 'language_mask')
372
            ->where(
373
                $expr->eq(
374
                    'id',
375
                    $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
376
                )
377
            );
378
        $query->execute();
379
    }
380
381
    private function updateContentNameAlwaysAvailableFlag(
382
        int $contentId,
383
        int $versionNo,
384
        bool $alwaysAvailable
385
    ): void {
386
        $query = $this->connection->createQueryBuilder();
387
        $expr = $query->expr();
388
        $query
389
            ->update(self::CONTENT_NAME_TABLE);
390
        $this
391
            ->setLanguageMaskForUpdateQuery($alwaysAvailable, $query, 'language_id')
392
            ->where(
393
                $expr->eq(
394
                    'contentobject_id',
395
                    $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
396
                )
397
            )
398
            ->andWhere(
399
                $expr->eq(
400
                    'content_version',
401
                    $query->createNamedParameter($versionNo, ParameterType::INTEGER, ':versionNo')
402
                )
403
            );
404
        $query->execute();
405
    }
406
407
    private function updateContentFieldsAlwaysAvailableFlag(
408
        int $contentId,
409
        int $versionNo,
410
        bool $alwaysAvailable,
411
        int $languageMask,
412
        int $initialLanguageId
413
    ): void {
414
        $query = $this->connection->createQueryBuilder();
415
        $expr = $query->expr();
416
        $query
417
            ->update(self::CONTENT_FIELD_TABLE)
418
            ->where(
419
                $expr->eq(
420
                    'contentobject_id',
421
                    $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
422
                )
423
            )
424
            ->andWhere(
425
                $expr->eq(
426
                    'version',
427
                    $query->createNamedParameter($versionNo, ParameterType::INTEGER, ':versionNo')
428
                )
429
            );
430
431
        // If there is only a single language, update all fields and return
432
        if (!$this->languageMaskGenerator->isLanguageMaskComposite($languageMask)) {
433
            $this->setLanguageMaskForUpdateQuery($alwaysAvailable, $query, 'language_id');
434
435
            $query->execute();
436
437
            return;
438
        }
439
440
        // Otherwise:
441
        // 1. Remove always available flag on all fields
442
        $query
443
            ->set(
444
                'language_id',
445
                $this->databasePlatform->getBitAndComparisonExpression(
446
                    'language_id',
447
                    ':languageMaskOperand'
448
                )
449
            )
450
            ->setParameter('languageMaskOperand', self::REMOVE_ALWAYS_AVAILABLE_LANG_MASK_OPERAND)
451
        ;
452
        $query->execute();
453
        $query->resetQueryPart('set');
454
455
        // 2. If Content is always available set the flag only on fields in main language
456
        if ($alwaysAvailable) {
457
            $query
458
                ->set(
459
                    'language_id',
460
                    $this->databasePlatform->getBitOrComparisonExpression(
461
                        'language_id',
462
                        ':languageMaskOperand'
463
                    )
464
                )
465
                ->setParameter(
466
                    'languageMaskOperand',
467
                    $alwaysAvailable ? 1 : self::REMOVE_ALWAYS_AVAILABLE_LANG_MASK_OPERAND
468
                );
469
470
            $query->andWhere(
471
                $expr->gt(
472
                    $this->databasePlatform->getBitAndComparisonExpression(
473
                        'language_id',
474
                        $query->createNamedParameter($initialLanguageId, ParameterType::INTEGER, ':initialLanguageId')
475
                    ),
476
                    $query->createNamedParameter(0, ParameterType::INTEGER, ':zero')
477
                )
478
            );
479
            $query->execute();
480
        }
481
    }
482
483
    public function setStatus(int $contentId, int $version, int $status): bool
484
    {
485
        if ($status !== APIVersionInfo::STATUS_PUBLISHED) {
486
            $query = $this->queryBuilder->getSetVersionStatusQuery($contentId, $version, $status);
487
            $rowCount = $query->execute();
488
489
            return $rowCount > 0;
490
        } else {
491
            // If the version's status is PUBLISHED, we use dedicated method for publishing
492
            $this->setPublishedStatus($contentId, $version);
493
494
            return true;
495
        }
496
    }
497
498
    public function setPublishedStatus(int $contentId, int $versionNo): void
499
    {
500
        $query = $this->queryBuilder->getSetVersionStatusQuery(
501
            $contentId,
502
            $versionNo,
503
            VersionInfo::STATUS_PUBLISHED
504
        );
505
506
        /* this part allows set status `published` only if there is no other published version of the content */
507
        $notExistPublishedVersion = <<<SQL
508
            NOT EXISTS (
509
                SELECT 1 FROM (
510
                    SELECT 1 FROM ezcontentobject_version
511
                    WHERE contentobject_id = :contentId AND status = :status
512
                ) as V
513
            )
514
            SQL;
515
516
        $query->andWhere($notExistPublishedVersion);
517
        if (0 === $query->execute()) {
518
            throw new BadStateException(
519
                '$contentId', "Someone just published another version of Content item {$contentId}"
520
            );
521
        }
522
        $this->markContentAsPublished($contentId, $versionNo);
523
    }
524
525
    private function markContentAsPublished(int $contentId, int $versionNo): void
526
    {
527
        $query = $this->connection->createQueryBuilder();
528
        $query
529
            ->update('ezcontentobject')
530
            ->set('status', ':status')
531
            ->set('current_version', ':versionNo')
532
            ->where('id =:contentId')
533
            ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER)
534
            ->setParameter('versionNo', $versionNo, ParameterType::INTEGER)
535
            ->setParameter('contentId', $contentId, ParameterType::INTEGER);
536
        $query->execute();
537
    }
538
539
    /**
540
     * @return int ID
541
     *
542
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
543
     */
544
    public function insertNewField(Content $content, Field $field, StorageFieldValue $value): int
545
    {
546
        $query = $this->connection->createQueryBuilder();
547
548
        $this->setInsertFieldValues($query, $content, $field, $value);
549
550
        // Insert with auto increment ID
551
        $nextId = $this->sharedGateway->getColumnNextIntegerValue(
552
            self::CONTENT_FIELD_TABLE,
553
            'id',
554
            self::CONTENT_FIELD_SEQ
555
        );
556
        // avoid trying to insert NULL to trigger default column value behavior
557
        if (null !== $nextId) {
558
            $query
559
                ->setValue('id', ':field_id')
560
                ->setParameter('field_id', $nextId, ParameterType::INTEGER);
561
        }
562
563
        $query->execute();
564
565
        return (int)$this->sharedGateway->getLastInsertedId(self::CONTENT_FIELD_SEQ);
566
    }
567
568
    public function insertExistingField(
569
        Content $content,
570
        Field $field,
571
        StorageFieldValue $value
572
    ): void {
573
        $query = $this->connection->createQueryBuilder();
574
575
        $this->setInsertFieldValues($query, $content, $field, $value);
576
577
        $query
578
            ->setValue('id', ':field_id')
579
            ->setParameter('field_id', $field->id, ParameterType::INTEGER);
580
581
        $query->execute();
582
    }
583
584
    /**
585
     * Set the given query field (ezcontentobject_attribute) values.
586
     *
587
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
588
     */
589
    private function setInsertFieldValues(
590
        DoctrineQueryBuilder $query,
591
        Content $content,
592
        Field $field,
593
        StorageFieldValue $value
594
    ): void {
595
        $query
596
            ->insert(self::CONTENT_FIELD_TABLE)
597
            ->values(
598
                [
599
                    'contentobject_id' => ':content_id',
600
                    'contentclassattribute_id' => ':field_definition_id',
601
                    'data_type_string' => ':data_type_string',
602
                    'language_code' => ':language_code',
603
                    'version' => ':version_no',
604
                    'data_float' => ':data_float',
605
                    'data_int' => ':data_int',
606
                    'data_text' => ':data_text',
607
                    'sort_key_int' => ':sort_key_int',
608
                    'sort_key_string' => ':sort_key_string',
609
                    'language_id' => ':language_id',
610
                ]
611
            )
612
            ->setParameter(
613
                'content_id',
614
                $content->versionInfo->contentInfo->id,
615
                ParameterType::INTEGER
616
            )
617
            ->setParameter('field_definition_id', $field->fieldDefinitionId, ParameterType::INTEGER)
618
            ->setParameter('data_type_string', $field->type, ParameterType::STRING)
619
            ->setParameter('language_code', $field->languageCode, ParameterType::STRING)
620
            ->setParameter('version_no', $field->versionNo, ParameterType::INTEGER)
621
            ->setParameter('data_float', $value->dataFloat)
622
            ->setParameter('data_int', $value->dataInt, ParameterType::INTEGER)
623
            ->setParameter('data_text', $value->dataText, ParameterType::STRING)
624
            ->setParameter('sort_key_int', $value->sortKeyInt, ParameterType::INTEGER)
625
            ->setParameter(
626
                'sort_key_string',
627
                mb_substr((string)$value->sortKeyString, 0, 255),
628
                ParameterType::STRING
629
            )
630
            ->setParameter(
631
                'language_id',
632
                $this->languageMaskGenerator->generateLanguageIndicator(
633
                    $field->languageCode,
634
                    $this->isLanguageAlwaysAvailable($content, $field->languageCode)
635
                ),
636
                ParameterType::INTEGER
637
            );
638
    }
639
640
    /**
641
     * Check if $languageCode is always available in $content.
642
     */
643
    private function isLanguageAlwaysAvailable(Content $content, string $languageCode): bool
644
    {
645
        return
646
            $content->versionInfo->contentInfo->alwaysAvailable &&
647
            $content->versionInfo->contentInfo->mainLanguageCode === $languageCode
648
        ;
649
    }
650
651
    public function updateField(Field $field, StorageFieldValue $value): void
652
    {
653
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
654
        // cannot change on update
655
        $query = $this->connection->createQueryBuilder();
656
        $this->setFieldUpdateValues($query, $value);
657
        $query
658
            ->where('id = :field_id')
659
            ->andWhere('version = :version_no')
660
            ->setParameter('field_id', $field->id, ParameterType::INTEGER)
661
            ->setParameter('version_no', $field->versionNo, ParameterType::INTEGER);
662
663
        $query->execute();
664
    }
665
666
    /**
667
     * Set update fields on $query based on $value.
668
     */
669
    private function setFieldUpdateValues(
670
        DoctrineQueryBuilder $query,
671
        StorageFieldValue $value
672
    ): void {
673
        $query
674
            ->update(self::CONTENT_FIELD_TABLE)
675
            ->set('data_float', ':data_float')
676
            ->set('data_int', ':data_int')
677
            ->set('data_text', ':data_text')
678
            ->set('sort_key_int', ':sort_key_int')
679
            ->set('sort_key_string', ':sort_key_string')
680
            ->setParameter('data_float', $value->dataFloat)
681
            ->setParameter('data_int', $value->dataInt, ParameterType::INTEGER)
682
            ->setParameter('data_text', $value->dataText, ParameterType::STRING)
683
            ->setParameter('sort_key_int', $value->sortKeyInt, ParameterType::INTEGER)
684
            ->setParameter('sort_key_string', mb_substr((string)$value->sortKeyString, 0, 255))
685
        ;
686
    }
687
688
    /**
689
     * Update an existing, non-translatable field.
690
     */
691
    public function updateNonTranslatableField(
692
        Field $field,
693
        StorageFieldValue $value,
694
        int $contentId
695
    ): void {
696
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
697
        // cannot change on update
698
        $query = $this->connection->createQueryBuilder();
699
        $this->setFieldUpdateValues($query, $value);
700
        $query
701
            ->where('contentclassattribute_id = :field_definition_id')
702
            ->andWhere('contentobject_id = :content_id')
703
            ->andWhere('version = :version_no')
704
            ->setParameter('field_definition_id', $field->fieldDefinitionId, ParameterType::INTEGER)
705
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
706
            ->setParameter('version_no', $field->versionNo, ParameterType::INTEGER);
707
708
        $query->execute();
709
    }
710
711
    public function load(int $contentId, ?int $version = null, ?array $translations = null): array
712
    {
713
        return $this->internalLoadContent([$contentId], $version, $translations);
714
    }
715
716
    public function loadContentList(array $contentIds, ?array $translations = null): array
717
    {
718
        return $this->internalLoadContent($contentIds, null, $translations);
719
    }
720
721
    /**
722
     * Build query for the <code>load</code> and <code>loadContentList</code> methods.
723
     *
724
     * @param int[] $contentIds
725
     * @param string[]|null $translations a list of language codes
726
     *
727
     * @see load(), loadContentList()
728
     */
729
    private function internalLoadContent(
730
        array $contentIds,
731
        ?int $version = null,
732
        ?array $translations = null
733
    ): array {
734
        $queryBuilder = $this->connection->createQueryBuilder();
735
        $expr = $queryBuilder->expr();
736
        $queryBuilder
737
            ->select(
738
                'c.id AS ezcontentobject_id',
739
                'c.contentclass_id AS ezcontentobject_contentclass_id',
740
                'c.section_id AS ezcontentobject_section_id',
741
                'c.owner_id AS ezcontentobject_owner_id',
742
                'c.remote_id AS ezcontentobject_remote_id',
743
                'c.current_version AS ezcontentobject_current_version',
744
                'c.initial_language_id AS ezcontentobject_initial_language_id',
745
                'c.modified AS ezcontentobject_modified',
746
                'c.published AS ezcontentobject_published',
747
                'c.status AS ezcontentobject_status',
748
                'c.name AS ezcontentobject_name',
749
                'c.language_mask AS ezcontentobject_language_mask',
750
                'c.is_hidden AS ezcontentobject_is_hidden',
751
                'v.id AS ezcontentobject_version_id',
752
                'v.version AS ezcontentobject_version_version',
753
                'v.modified AS ezcontentobject_version_modified',
754
                'v.creator_id AS ezcontentobject_version_creator_id',
755
                'v.created AS ezcontentobject_version_created',
756
                'v.status AS ezcontentobject_version_status',
757
                'v.language_mask AS ezcontentobject_version_language_mask',
758
                'v.initial_language_id AS ezcontentobject_version_initial_language_id',
759
                'a.id AS ezcontentobject_attribute_id',
760
                'a.contentclassattribute_id AS ezcontentobject_attribute_contentclassattribute_id',
761
                'a.data_type_string AS ezcontentobject_attribute_data_type_string',
762
                'a.language_code AS ezcontentobject_attribute_language_code',
763
                'a.language_id AS ezcontentobject_attribute_language_id',
764
                'a.data_float AS ezcontentobject_attribute_data_float',
765
                'a.data_int AS ezcontentobject_attribute_data_int',
766
                'a.data_text AS ezcontentobject_attribute_data_text',
767
                'a.sort_key_int AS ezcontentobject_attribute_sort_key_int',
768
                'a.sort_key_string AS ezcontentobject_attribute_sort_key_string',
769
                't.main_node_id AS ezcontentobject_tree_main_node_id'
770
            )
771
            ->from('ezcontentobject', 'c')
772
            ->innerJoin(
773
                'c',
774
                'ezcontentobject_version',
775
                'v',
776
                $expr->andX(
777
                    $expr->eq('c.id', 'v.contentobject_id'),
778
                    $expr->eq('v.version', $version ?? 'c.current_version')
779
                )
780
            )
781
            ->innerJoin(
782
                'v',
783
                'ezcontentobject_attribute',
784
                'a',
785
                $expr->andX(
786
                    $expr->eq('v.contentobject_id', 'a.contentobject_id'),
787
                    $expr->eq('v.version', 'a.version')
788
                )
789
            )
790
            ->leftJoin(
791
                'c',
792
                'ezcontentobject_tree',
793
                't',
794
                $expr->andX(
795
                    $expr->eq('c.id', 't.contentobject_id'),
796
                    $expr->eq('t.node_id', 't.main_node_id')
797
                )
798
            );
799
800
        $queryBuilder->where(
801
            $expr->in(
802
                'c.id',
803
                $queryBuilder->createNamedParameter($contentIds, Connection::PARAM_INT_ARRAY)
804
            )
805
        );
806
807
        if (!empty($translations)) {
808
            $queryBuilder->andWhere(
809
                $expr->in(
810
                    'a.language_code',
811
                    $queryBuilder->createNamedParameter($translations, Connection::PARAM_STR_ARRAY)
812
                )
813
            );
814
        }
815
816
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
817
    }
818
819
    public function loadContentInfo(int $contentId): array
820
    {
821
        $queryBuilder = $this->queryBuilder->createLoadContentInfoQueryBuilder();
822
        $queryBuilder
823
            ->where('c.id = :id')
824
            ->setParameter('id', $contentId, ParameterType::INTEGER);
825
826
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
827
        if (empty($results)) {
828
            throw new NotFound('content', "id: $contentId");
829
        }
830
831
        return $results[0];
832
    }
833
834
    public function loadContentInfoList(array $contentIds): array
835
    {
836
        $queryBuilder = $this->queryBuilder->createLoadContentInfoQueryBuilder();
837
        $queryBuilder
838
            ->where('c.id IN (:ids)')
839
            ->setParameter('ids', $contentIds, Connection::PARAM_INT_ARRAY);
840
841
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
842
    }
843
844
    public function loadContentInfoByRemoteId(string $remoteId): array
845
    {
846
        $queryBuilder = $this->queryBuilder->createLoadContentInfoQueryBuilder();
847
        $queryBuilder
848
            ->where('c.remote_id = :id')
849
            ->setParameter('id', $remoteId, ParameterType::STRING);
850
851
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
852
        if (empty($results)) {
853
            throw new NotFound('content', "remote_id: $remoteId");
854
        }
855
856
        return $results[0];
857
    }
858
859
    public function loadContentInfoByLocationId(int $locationId): array
860
    {
861
        $queryBuilder = $this->queryBuilder->createLoadContentInfoQueryBuilder(false);
862
        $queryBuilder
863
            ->where('t.node_id = :id')
864
            ->setParameter('id', $locationId, ParameterType::INTEGER);
865
866
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
867
        if (empty($results)) {
868
            throw new NotFound('content', "node_id: $locationId");
869
        }
870
871
        return $results[0];
872
    }
873
874
    public function loadVersionInfo(int $contentId, ?int $versionNo = null): array
875
    {
876
        $queryBuilder = $this->queryBuilder->createVersionInfoFindQueryBuilder();
877
        $expr = $queryBuilder->expr();
878
879
        $queryBuilder
880
            ->where(
881
                $expr->eq(
882
                    'v.contentobject_id',
883
                    $queryBuilder->createNamedParameter(
884
                        $contentId,
885
                        ParameterType::INTEGER,
886
                        ':content_id'
887
                    )
888
                )
889
            );
890
891
        if (null !== $versionNo) {
892
            $queryBuilder
893
                ->andWhere(
894
                    $expr->eq(
895
                        'v.version',
896
                        $queryBuilder->createNamedParameter(
897
                            $versionNo,
898
                            ParameterType::INTEGER,
899
                            ':version_no'
900
                        )
901
                    )
902
                );
903
        } else {
904
            $queryBuilder->andWhere($expr->eq('v.version', 'c.current_version'));
905
        }
906
907
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
908
    }
909
910
    public function countVersionsForUser(int $userId, int $status = VersionInfo::STATUS_DRAFT): int
911
    {
912
        $query = $this->connection->createQueryBuilder();
913
        $expr = $query->expr();
914
        $query
915
            ->select($this->databasePlatform->getCountExpression('v.id'))
916
            ->from('ezcontentobject_version', 'v')
917
            ->innerJoin(
918
                'v',
919
                'ezcontentobject',
920
                'c',
921
                $expr->andX(
922
                    $expr->eq('c.id', 'v.contentobject_id'),
923
                    $expr->neq('c.status', ContentInfo::STATUS_TRASHED)
924
                )
925
            )
926
            ->where(
927
                $query->expr()->andX(
928
                    $query->expr()->eq('v.status', ':status'),
929
                    $query->expr()->eq('v.creator_id', ':user_id')
930
                )
931
            )
932
            ->setParameter(':status', $status, ParameterType::INTEGER)
933
            ->setParameter(':user_id', $userId, ParameterType::INTEGER);
934
935
        return (int) $query->execute()->fetchColumn();
936
    }
937
938
    /**
939
     * Return data for all versions with the given status created by the given $userId.
940
     *
941
     * @return string[][]
942
     */
943
    public function listVersionsForUser(int $userId, int $status = VersionInfo::STATUS_DRAFT): array
944
    {
945
        $query = $this->queryBuilder->createVersionInfoFindQueryBuilder();
946
        $query
947
            ->where('v.status = :status')
948
            ->andWhere('v.creator_id = :user_id')
949
            ->setParameter('status', $status, ParameterType::INTEGER)
950
            ->setParameter('user_id', $userId, ParameterType::INTEGER)
951
            ->orderBy('v.id');
952
953
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
954
    }
955
956
    public function loadVersionsForUser(
957
        int $userId,
958
        int $status = VersionInfo::STATUS_DRAFT,
959
        int $offset = 0,
960
        int $limit = -1
961
    ): array {
962
        $query = $this->queryBuilder->createVersionInfoFindQueryBuilder();
963
        $expr = $query->expr();
964
        $query->where(
965
            $expr->andX(
966
                $expr->eq('v.status', ':status'),
967
                $expr->eq('v.creator_id', ':user_id'),
968
                $expr->neq('c.status', ContentInfo::STATUS_TRASHED)
969
            )
970
        )
971
        ->setFirstResult($offset)
972
        ->setParameter(':status', $status, ParameterType::INTEGER)
973
        ->setParameter(':user_id', $userId, ParameterType::INTEGER);
974
975
        if ($limit > 0) {
976
            $query->setMaxResults($limit);
977
        }
978
979
        $query->orderBy('v.modified', 'DESC');
980
        $query->addOrderBy('v.id', 'DESC');
981
982
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
983
    }
984
985
    public function listVersions(int $contentId, ?int $status = null, int $limit = -1): array
986
    {
987
        $query = $this->queryBuilder->createVersionInfoFindQueryBuilder();
988
        $query
989
            ->where('v.contentobject_id = :content_id')
990
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
991
992
        if ($status !== null) {
993
            $query
994
                ->andWhere('v.status = :status')
995
                ->setParameter('status', $status);
996
        }
997
998
        if ($limit > 0) {
999
            $query->setMaxResults($limit);
1000
        }
1001
1002
        $query->orderBy('v.id');
1003
1004
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1005
    }
1006
1007
    /**
1008
     * @return int[]
1009
     */
1010
    public function listVersionNumbers(int $contentId): array
1011
    {
1012
        $query = $this->connection->createQueryBuilder();
1013
        $query
1014
            ->select('version')
1015
            ->from(self::CONTENT_VERSION_TABLE)
1016
            ->where('contentobject_id = :contentId')
1017
            ->groupBy('version')
1018
            ->setParameter('contentId', $contentId, ParameterType::INTEGER);
1019
1020
        return array_map('intval', $query->execute()->fetchAll(FetchMode::COLUMN));
1021
    }
1022
1023
    public function getLastVersionNumber(int $contentId): int
1024
    {
1025
        $query = $this->connection->createQueryBuilder();
1026
        $query
1027
            ->select($this->databasePlatform->getMaxExpression('version'))
1028
            ->from(self::CONTENT_VERSION_TABLE)
1029
            ->where('contentobject_id = :content_id')
1030
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1031
1032
        $statement = $query->execute();
1033
1034
        return (int)$statement->fetchColumn();
1035
    }
1036
1037
    /**
1038
     * @return int[]
1039
     */
1040
    public function getAllLocationIds(int $contentId): array
1041
    {
1042
        $query = $this->connection->createQueryBuilder();
1043
        $query
1044
            ->select('node_id')
1045
            ->from('ezcontentobject_tree')
1046
            ->where('contentobject_id = :content_id')
1047
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1048
1049
        $statement = $query->execute();
1050
1051
        return $statement->fetchAll(FetchMode::COLUMN);
1052
    }
1053
1054
    /**
1055
     * @return int[][]
1056
     */
1057
    public function getFieldIdsByType(
1058
        int $contentId,
1059
        ?int $versionNo = null,
1060
        ?string $languageCode = null
1061
    ): array {
1062
        $query = $this->connection->createQueryBuilder();
1063
        $query
1064
            ->select('id', 'data_type_string')
1065
            ->from(self::CONTENT_FIELD_TABLE)
1066
            ->where('contentobject_id = :content_id')
1067
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1068
1069
        if (null !== $versionNo) {
1070
            $query
1071
                ->andWhere('version = :version_no')
1072
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1073
        }
1074
1075
        if (!empty($languageCode)) {
1076
            $query
1077
                ->andWhere('language_code = :language_code')
1078
                ->setParameter('language_code', $languageCode, ParameterType::STRING);
1079
        }
1080
1081
        $statement = $query->execute();
1082
1083
        $result = [];
1084
        foreach ($statement->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
1085
            if (!isset($result[$row['data_type_string']])) {
0 ignored issues
show
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '[', expecting ']'
Loading history...
1086
                $result[$row['data_type_string']] = [];
1087
            }
1088
            $result[$row['data_type_string']][] = (int)$row['id'];
1089
        }
1090
1091
        return $result;
1092
    }
1093
1094
    public function deleteRelations(int $contentId, ?int $versionNo = null): void
1095
    {
1096
        $query = $this->connection->createQueryBuilder();
1097
        $query
1098
            ->delete(self::CONTENT_RELATION_TABLE)
1099
            ->where('from_contentobject_id = :content_id')
1100
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1101
1102
        if (null !== $versionNo) {
1103
            $query
1104
                ->andWhere('from_contentobject_version = :version_no')
1105
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1106
        } else {
1107
            $query->orWhere('to_contentobject_id = :content_id');
1108
        }
1109
1110
        $query->execute();
1111
    }
1112
1113
    public function removeReverseFieldRelations(int $contentId): void
1114
    {
1115
        $query = $this->connection->createQueryBuilder();
1116
        $expr = $query->expr();
1117
        $query
1118
            ->select(['a.id', 'a.version', 'a.data_type_string', 'a.data_text'])
1119
            ->from(self::CONTENT_FIELD_TABLE, 'a')
1120
            ->innerJoin(
1121
                'a',
1122
                'ezcontentobject_link',
1123
                'l',
1124
                $expr->andX(
1125
                    'l.from_contentobject_id = a.contentobject_id',
1126
                    'l.from_contentobject_version = a.version',
1127
                    'l.contentclassattribute_id = a.contentclassattribute_id'
1128
                )
1129
            )
1130
            ->where('l.to_contentobject_id = :content_id')
1131
            ->andWhere(
1132
                $expr->gt(
1133
                    $this->databasePlatform->getBitAndComparisonExpression(
1134
                        'l.relation_type',
1135
                        ':relation_type'
1136
                    ),
1137
                    0
1138
                )
1139
            )
1140
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1141
            ->setParameter('relation_type', Relation::FIELD, ParameterType::INTEGER);
1142
1143
        $statement = $query->execute();
1144
1145
        while ($row = $statement->fetch(FetchMode::ASSOCIATIVE)) {
1146
            if ($row['data_type_string'] === 'ezobjectrelation') {
1147
                $this->removeRelationFromRelationField($row);
1148
            }
1149
1150
            if ($row['data_type_string'] === 'ezobjectrelationlist') {
1151
                $this->removeRelationFromRelationListField($contentId, $row);
1152
            }
1153
        }
1154
    }
1155
1156
    /**
1157
     * Update field value of RelationList field type identified by given $row data,
1158
     * removing relations toward given $contentId.
1159
     *
1160
     * @param array $row
1161
     */
1162
    private function removeRelationFromRelationListField(int $contentId, array $row): void
1163
    {
1164
        $document = new DOMDocument('1.0', 'utf-8');
1165
        $document->loadXML($row['data_text']);
1166
1167
        $xpath = new DOMXPath($document);
1168
        $xpathExpression = "//related-objects/relation-list/relation-item[@contentobject-id='{$contentId}']";
1169
1170
        $relationItems = $xpath->query($xpathExpression);
1171
        foreach ($relationItems as $relationItem) {
1172
            $relationItem->parentNode->removeChild($relationItem);
1173
        }
1174
1175
        $query = $this->connection->createQueryBuilder();
1176
        $query
1177
            ->update(self::CONTENT_FIELD_TABLE)
1178
            ->set('data_text', ':data_text')
1179
            ->setParameter('data_text', $document->saveXML(), ParameterType::STRING)
1180
            ->where('id = :attribute_id')
1181
            ->andWhere('version = :version_no')
1182
            ->setParameter('attribute_id', (int)$row['id'], ParameterType::INTEGER)
1183
            ->setParameter('version_no', (int)$row['version'], ParameterType::INTEGER);
1184
1185
        $query->execute();
1186
    }
1187
1188
    /**
1189
     * Update field value of Relation field type identified by given $row data,
1190
     * removing relation data.
1191
     *
1192
     * @param array $row
1193
     */
1194
    private function removeRelationFromRelationField(array $row): void
1195
    {
1196
        $query = $this->connection->createQueryBuilder();
1197
        $query
1198
            ->update(self::CONTENT_FIELD_TABLE)
1199
            ->set('data_int', ':data_int')
1200
            ->set('sort_key_int', ':sort_key_int')
1201
            ->setParameter('data_int', null, ParameterType::NULL)
1202
            ->setParameter('sort_key_int', 0, ParameterType::INTEGER)
1203
            ->where('id = :attribute_id')
1204
            ->andWhere('version = :version_no')
1205
            ->setParameter('attribute_id', (int)$row['id'], ParameterType::INTEGER)
1206
            ->setParameter('version_no', (int)$row['version'], ParameterType::INTEGER);
1207
1208
        $query->execute();
1209
    }
1210
1211
    public function deleteField(int $fieldId): void
1212
    {
1213
        $query = $this->connection->createQueryBuilder();
1214
        $query
1215
            ->delete(self::CONTENT_FIELD_TABLE)
1216
            ->where('id = :field_id')
1217
            ->setParameter('field_id', $fieldId, ParameterType::INTEGER)
1218
        ;
1219
1220
        $query->execute();
1221
    }
1222
1223
    public function deleteFields(int $contentId, ?int $versionNo = null): void
1224
    {
1225
        $query = $this->connection->createQueryBuilder();
1226
        $query
1227
            ->delete(self::CONTENT_FIELD_TABLE)
1228
            ->where('contentobject_id = :content_id')
1229
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1230
1231
        if (null !== $versionNo) {
1232
            $query
1233
                ->andWhere('version = :version_no')
1234
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1235
        }
1236
1237
        $query->execute();
1238
    }
1239
1240
    public function deleteVersions(int $contentId, ?int $versionNo = null): void
1241
    {
1242
        $query = $this->connection->createQueryBuilder();
1243
        $query
1244
            ->delete(self::CONTENT_VERSION_TABLE)
1245
            ->where('contentobject_id = :content_id')
1246
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1247
1248
        if (null !== $versionNo) {
1249
            $query
1250
                ->andWhere('version = :version_no')
1251
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1252
        }
1253
1254
        $query->execute();
1255
    }
1256
1257
    public function deleteNames(int $contentId, int $versionNo = null): void
1258
    {
1259
        $query = $this->connection->createQueryBuilder();
1260
        $query
1261
            ->delete(self::CONTENT_NAME_TABLE)
1262
            ->where('contentobject_id = :content_id')
1263
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1264
1265
        if (isset($versionNo)) {
1266
            $query
1267
                ->andWhere('content_version = :version_no')
1268
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1269
        }
1270
1271
        $query->execute();
1272
    }
1273
1274
    /**
1275
     * Query Content name table to find if a name record for the given parameters exists.
1276
     */
1277
    private function contentNameExists(int $contentId, int $version, string $languageCode): bool
1278
    {
1279
        $query = $this->connection->createQueryBuilder();
1280
        $query
1281
            ->select($this->databasePlatform->getCountExpression('contentobject_id'))
1282
            ->from(self::CONTENT_NAME_TABLE)
1283
            ->where('contentobject_id = :content_id')
1284
            ->andWhere('content_version = :version_no')
1285
            ->andWhere('content_translation = :language_code')
1286
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1287
            ->setParameter('version_no', $version, ParameterType::INTEGER)
1288
            ->setParameter('language_code', $languageCode, ParameterType::STRING);
1289
1290
        $stmt = $query->execute();
1291
1292
        return (int)$stmt->fetch(FetchMode::COLUMN) > 0;
1293
    }
1294
1295
    public function setName(int $contentId, int $version, string $name, string $languageCode): void
1296
    {
1297
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
1298
1299
        $query = $this->connection->createQueryBuilder();
1300
1301
        // prepare parameters
1302
        $query
1303
            ->setParameter('name', $name, ParameterType::STRING)
1304
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1305
            ->setParameter('version_no', $version, ParameterType::INTEGER)
1306
            ->setParameter('language_id', $language->id, ParameterType::INTEGER)
1307
            ->setParameter('language_code', $language->languageCode, ParameterType::STRING)
1308
        ;
1309
1310
        if (!$this->contentNameExists($contentId, $version, $language->languageCode)) {
1311
            $query
1312
                ->insert(self::CONTENT_NAME_TABLE)
1313
                ->values(
1314
                    [
1315
                        'contentobject_id' => ':content_id',
1316
                        'content_version' => ':version_no',
1317
                        'content_translation' => ':language_code',
1318
                        'name' => ':name',
1319
                        'language_id' => $this->getSetNameLanguageMaskSubQuery(),
1320
                        'real_translation' => ':language_code',
1321
                    ]
1322
                );
1323
        } else {
1324
            $query
1325
                ->update(self::CONTENT_NAME_TABLE)
1326
                ->set('name', ':name')
1327
                ->set('language_id', $this->getSetNameLanguageMaskSubQuery())
1328
                ->set('real_translation', ':language_code')
1329
                ->where('contentobject_id = :content_id')
1330
                ->andWhere('content_version = :version_no')
1331
                ->andWhere('content_translation = :language_code');
1332
        }
1333
1334
        $query->execute();
1335
    }
1336
1337
    /**
1338
     * Return a language sub select query for setName.
1339
     *
1340
     * The query generates the proper language mask at the runtime of the INSERT/UPDATE query
1341
     * generated by setName.
1342
     *
1343
     * @see setName
1344
     */
1345
    private function getSetNameLanguageMaskSubQuery(): string
1346
    {
1347
        return <<<SQL
1348
            (SELECT
1349
                CASE
1350
                    WHEN (initial_language_id = :language_id AND (language_mask & :language_id) <> 0 )
1351
                    THEN (:language_id | 1)
1352
                    ELSE :language_id
1353
                END
1354
                FROM ezcontentobject
1355
                WHERE id = :content_id)
1356
            SQL;
1357
    }
1358
1359
    public function deleteContent(int $contentId): void
1360
    {
1361
        $query = $this->connection->createQueryBuilder();
1362
        $query
1363
            ->delete(self::CONTENT_ITEM_TABLE)
1364
            ->where('id = :content_id')
1365
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1366
        ;
1367
1368
        $query->execute();
1369
    }
1370
1371
    public function loadRelations(
1372
        int $contentId,
1373
        ?int $contentVersionNo = null,
1374
        ?int $relationType = null
1375
    ): array {
1376
        $query = $this->queryBuilder->createRelationFindQueryBuilder();
1377
        $expr = $query->expr();
1378
        $query
1379
            ->innerJoin(
1380
                'l',
1381
                'ezcontentobject',
1382
                'ezcontentobject_to',
1383
                $expr->andX(
1384
                    'l.to_contentobject_id = ezcontentobject_to.id',
1385
                    'ezcontentobject_to.status = :status'
1386
                )
1387
            )
1388
            ->where(
1389
                'l.from_contentobject_id = :content_id'
1390
            )
1391
            ->setParameter(
1392
                'status',
1393
                ContentInfo::STATUS_PUBLISHED,
1394
                ParameterType::INTEGER
1395
            )
1396
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1397
1398
        // source version number
1399
        if (null !== $contentVersionNo) {
1400
            $query
1401
                ->andWhere('l.from_contentobject_version = :version_no')
1402
                ->setParameter('version_no', $contentVersionNo, ParameterType::INTEGER);
1403
        } else {
1404
            // from published version only
1405
            $query
1406
                ->innerJoin(
1407
                    'ezcontentobject_to',
1408
                    'ezcontentobject',
1409
                    'c',
1410
                    $expr->andX(
1411
                        'c.id = l.from_contentobject_id',
1412
                        'c.current_version = l.from_contentobject_version'
1413
                    )
1414
                );
1415
        }
1416
1417
        // relation type
1418
        if (null !== $relationType) {
1419
            $query
1420
                ->andWhere(
1421
                    $expr->gt(
1422
                        $this->databasePlatform->getBitAndComparisonExpression(
1423
                            'l.relation_type',
1424
                            ':relation_type'
1425
                        ),
1426
                        0
1427
                    )
1428
                )
1429
                ->setParameter('relation_type', $relationType, ParameterType::INTEGER);
1430
        }
1431
1432
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1433
    }
1434
1435
    public function countReverseRelations(int $toContentId, ?int $relationType = null): int
1436
    {
1437
        $query = $this->connection->createQueryBuilder();
1438
        $expr = $query->expr();
1439
        $query
1440
            ->select($this->databasePlatform->getCountExpression('l.id'))
1441
            ->from(self::CONTENT_RELATION_TABLE, 'l')
1442
            ->innerJoin(
1443
                'l',
1444
                'ezcontentobject',
1445
                'c',
1446
                $expr->andX(
1447
                    $expr->eq('l.from_contentobject_id', 'c.id'),
1448
                    $expr->eq('l.from_contentobject_version', 'c.current_version'),
1449
                    $expr->eq('c.status', ':status')
1450
                )
1451
            )
1452
            ->where(
1453
                $expr->eq('l.to_contentobject_id', ':to_content_id')
1454
            )
1455
            ->setParameter('to_content_id', $toContentId, ParameterType::INTEGER)
1456
            ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER)
1457
        ;
1458
1459
        // relation type
1460
        if ($relationType !== null) {
1461
            $query->andWhere(
1462
                $expr->gt(
1463
                    $this->databasePlatform->getBitAndComparisonExpression(
1464
                        'l.relation_type',
1465
                        $relationType
1466
                    ),
1467
                    0
1468
                )
1469
            );
1470
        }
1471
1472
        return (int)$query->execute()->fetchColumn();
1473
    }
1474
1475
    public function loadReverseRelations(int $toContentId, ?int $relationType = null): array
1476
    {
1477
        $query = $this->queryBuilder->createRelationFindQueryBuilder();
1478
        $expr = $query->expr();
1479
        $query
1480
            ->join(
1481
                'l',
1482
                'ezcontentobject',
1483
                'c',
1484
                $expr->andX(
1485
                    'c.id = l.from_contentobject_id',
1486
                    'c.current_version = l.from_contentobject_version',
1487
                    'c.status = :status'
1488
                )
1489
            )
1490
            ->where('l.to_contentobject_id = :to_content_id')
1491
            ->setParameter('to_content_id', $toContentId, ParameterType::INTEGER)
1492
            ->setParameter(
1493
                'status',
1494
                ContentInfo::STATUS_PUBLISHED,
1495
                ParameterType::INTEGER
1496
            );
1497
1498
        // relation type
1499
        if (null !== $relationType) {
1500
            $query->andWhere(
1501
                $expr->gt(
1502
                    $this->databasePlatform->getBitAndComparisonExpression(
1503
                        'l.relation_type',
1504
                        ':relation_type'
1505
                    ),
1506
                    0
1507
                )
1508
            )
1509
                ->setParameter('relation_type', $relationType, ParameterType::INTEGER);
1510
        }
1511
1512
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1513
    }
1514
1515
    public function listReverseRelations(
1516
        int $toContentId,
1517
        int $offset = 0,
1518
        int $limit = -1,
1519
        ?int $relationType = null
1520
    ): array {
1521
        $query = $this->queryBuilder->createRelationFindQueryBuilder();
1522
        $expr = $query->expr();
1523
        $query
1524
            ->innerJoin(
1525
                'l',
1526
                'ezcontentobject',
1527
                'c',
1528
                $expr->andX(
1529
                    $expr->eq('l.from_contentobject_id', 'c.id'),
1530
                    $expr->eq('l.from_contentobject_version', 'c.current_version'),
1531
                    $expr->eq('c.status', ContentInfo::STATUS_PUBLISHED)
1532
                )
1533
            )
1534
            ->where(
1535
                $expr->eq('l.to_contentobject_id', ':toContentId')
1536
            )
1537
            ->setParameter(':toContentId', $toContentId, ParameterType::INTEGER);
1538
1539
        // relation type
1540
        if ($relationType !== null) {
1541
            $query->andWhere(
1542
                $expr->gt(
1543
                    $this->databasePlatform->getBitAndComparisonExpression(
1544
                        'l.relation_type',
1545
                        $relationType
1546
                    ),
1547
                    0
1548
                )
1549
            );
1550
        }
1551
        $query->setFirstResult($offset);
1552
        if ($limit > 0) {
1553
            $query->setMaxResults($limit);
1554
        }
1555
        $query->orderBy('l.id', 'DESC');
1556
1557
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1558
    }
1559
1560
    public function insertRelation(RelationCreateStruct $createStruct): int
1561
    {
1562
        $query = $this->connection->createQueryBuilder();
1563
        $query
1564
            ->insert(self::CONTENT_RELATION_TABLE)
1565
            ->values(
1566
                [
1567
                    'contentclassattribute_id' => ':field_definition_id',
1568
                    'from_contentobject_id' => ':from_content_id',
1569
                    'from_contentobject_version' => ':from_version_no',
1570
                    'relation_type' => ':relation_type',
1571
                    'to_contentobject_id' => ':to_content_id',
1572
                ]
1573
            )
1574
            ->setParameter(
1575
                'field_definition_id',
1576
                (int)$createStruct->sourceFieldDefinitionId,
1577
                ParameterType::INTEGER
1578
            )
1579
            ->setParameter(
1580
                'from_content_id',
1581
                $createStruct->sourceContentId,
1582
                ParameterType::INTEGER
1583
            )
1584
            ->setParameter(
1585
                'from_version_no',
1586
                $createStruct->sourceContentVersionNo,
1587
                ParameterType::INTEGER
1588
            )
1589
            ->setParameter('relation_type', $createStruct->type, ParameterType::INTEGER)
1590
            ->setParameter(
1591
                'to_content_id',
1592
                $createStruct->destinationContentId,
1593
                ParameterType::INTEGER
1594
            );
1595
1596
        $query->execute();
1597
1598
        return (int)$this->connection->lastInsertId(self::CONTENT_RELATION_SEQ);
1599
    }
1600
1601
    public function deleteRelation(int $relationId, int $type): void
1602
    {
1603
        // Legacy Storage stores COMMON, LINK and EMBED types using bitmask, therefore first load
1604
        // existing relation type by given $relationId for comparison
1605
        $query = $this->connection->createQueryBuilder();
1606
        $query
1607
            ->select('relation_type')
1608
            ->from(self::CONTENT_RELATION_TABLE)
1609
            ->where('id = :relation_id')
1610
            ->setParameter('relation_id', $relationId, ParameterType::INTEGER)
1611
        ;
1612
1613
        $loadedRelationType = $query->execute()->fetchColumn();
1614
1615
        if (!$loadedRelationType) {
1616
            return;
1617
        }
1618
1619
        $query = $this->connection->createQueryBuilder();
1620
        // If relation type matches then delete
1621
        if (((int)$loadedRelationType) === ((int)$type)) {
1622
            $query
1623
                ->delete(self::CONTENT_RELATION_TABLE)
1624
                ->where('id = :relation_id')
1625
                ->setParameter('relation_id', $relationId, ParameterType::INTEGER)
1626
            ;
1627
1628
            $query->execute();
1629
        } elseif ($loadedRelationType & $type) {
1630
            // If relation type is composite update bitmask
1631
1632
            $query
1633
                ->update(self::CONTENT_RELATION_TABLE)
1634
                ->set(
1635
                    'relation_type',
1636
                    // make & operation removing given $type from the bitmask
1637
                    $this->databasePlatform->getBitAndComparisonExpression(
1638
                        'relation_type',
1639
                        ':relation_type'
1640
                    )
1641
                )
1642
                // set the relation type as needed for the above & expression
1643
                ->setParameter('relation_type', ~$type, ParameterType::INTEGER)
1644
                ->where('id = :relation_id')
1645
                ->setParameter('relation_id', $relationId, ParameterType::INTEGER)
1646
            ;
1647
1648
            $query->execute();
1649
        }
1650
    }
1651
1652
    /**
1653
     * @return int[]
1654
     */
1655
    public function getContentIdsByContentTypeId(int $contentTypeId): array
1656
    {
1657
        $query = $this->connection->createQueryBuilder();
1658
        $query
1659
            ->select('id')
1660
            ->from(self::CONTENT_ITEM_TABLE)
1661
            ->where('contentclass_id = :content_type_id')
1662
            ->setParameter('content_type_id', $contentTypeId, ParameterType::INTEGER);
1663
1664
        $statement = $query->execute();
1665
1666
        return array_map('intval', $statement->fetchAll(FetchMode::COLUMN));
1667
    }
1668
1669
    public function loadVersionedNameData(array $rows): array
1670
    {
1671
        $query = $this->queryBuilder->createNamesQuery();
1672
        $expr = $query->expr();
1673
        $conditions = [];
1674
        foreach ($rows as $row) {
1675
            $conditions[] = $expr->andX(
1676
                $expr->eq(
1677
                    'contentobject_id',
1678
                    $query->createPositionalParameter($row['id'], ParameterType::INTEGER)
1679
                ),
1680
                $expr->eq(
1681
                    'content_version',
1682
                    $query->createPositionalParameter($row['version'], ParameterType::INTEGER)
1683
                ),
1684
            );
1685
        }
1686
1687
        $query->where($expr->orX(...$conditions));
1688
1689
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1690
    }
1691
1692
    /**
1693
     * @throws \Doctrine\DBAL\DBALException
1694
     */
1695
    public function copyRelations(
1696
        int $originalContentId,
1697
        int $copiedContentId,
1698
        ?int $versionNo = null
1699
    ): void {
1700
        $selectQuery = $this->connection->createQueryBuilder();
1701
        $selectQuery
1702
            ->select(
1703
                'l.contentclassattribute_id',
1704
                ':copied_id',
1705
                'l.from_contentobject_version',
1706
                'l.relation_type',
1707
                'l.to_contentobject_id'
1708
            )
1709
            ->from(self::CONTENT_RELATION_TABLE, 'l')
1710
            ->where('l.from_contentobject_id = :original_id')
1711
            ->setParameter('copied_id', $copiedContentId, ParameterType::INTEGER)
1712
            ->setParameter('original_id', $originalContentId, ParameterType::INTEGER);
1713
1714
        if ($versionNo) {
1715
            $selectQuery
1716
                ->andWhere('l.from_contentobject_version = :version')
1717
                ->setParameter(':version', $versionNo, ParameterType::INTEGER);
1718
        }
1719
        // Given we can retain all columns, we just create copies with new `from_contentobject_id` using INSERT INTO SELECT
1720
        $insertQuery = <<<SQL
1721
            INSERT INTO ezcontentobject_link (
1722
                contentclassattribute_id,
1723
                from_contentobject_id,
1724
                from_contentobject_version,
1725
                relation_type,
1726
                to_contentobject_id
1727
            )
1728
            SQL;
1729
1730
        $insertQuery .= $selectQuery->getSQL();
1731
1732
        $this->connection->executeUpdate(
1733
            $insertQuery,
1734
            $selectQuery->getParameters(),
1735
            $selectQuery->getParameterTypes()
1736
        );
1737
    }
1738
1739
    /**
1740
     * {@inheritdoc}
1741
     *
1742
     * @throws \Doctrine\DBAL\ConnectionException
1743
     * @throws \Doctrine\DBAL\DBALException
1744
     */
1745
    public function deleteTranslationFromContent(int $contentId, string $languageCode): void
1746
    {
1747
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
1748
1749
        $this->connection->beginTransaction();
1750
        try {
1751
            $this->deleteTranslationFromContentVersions($contentId, $language->id);
1752
            $this->deleteTranslationFromContentNames($contentId, $languageCode);
1753
            $this->deleteTranslationFromContentObject($contentId, $language->id);
1754
1755
            $this->connection->commit();
1756
        } catch (DBALException $e) {
1757
            $this->connection->rollBack();
1758
            throw $e;
1759
        }
1760
    }
1761
1762
    public function deleteTranslatedFields(
1763
        string $languageCode,
1764
        int $contentId,
1765
        ?int $versionNo = null
1766
    ): void {
1767
        $query = $this->connection->createQueryBuilder();
1768
        $query
1769
            ->delete('ezcontentobject_attribute')
1770
            ->where('contentobject_id = :contentId')
1771
            ->andWhere('language_code = :languageCode')
1772
            ->setParameters(
1773
                [
1774
                    ':contentId' => $contentId,
1775
                    ':languageCode' => $languageCode,
1776
                ]
1777
            )
1778
        ;
1779
1780
        if (null !== $versionNo) {
1781
            $query
1782
                ->andWhere('version = :versionNo')
1783
                ->setParameter(':versionNo', $versionNo)
1784
            ;
1785
        }
1786
1787
        $query->execute();
1788
    }
1789
1790
    /**
1791
     * {@inheritdoc}
1792
     *
1793
     * @throws \Doctrine\DBAL\DBALException
1794
     */
1795
    public function deleteTranslationFromVersion(
1796
        int $contentId,
1797
        int $versionNo,
1798
        string $languageCode
1799
    ): void {
1800
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
1801
1802
        $this->connection->beginTransaction();
1803
        try {
1804
            $this->deleteTranslationFromContentVersions($contentId, $language->id, $versionNo);
1805
            $this->deleteTranslationFromContentNames($contentId, $languageCode, $versionNo);
1806
1807
            $this->connection->commit();
1808
        } catch (DBALException $e) {
1809
            $this->connection->rollBack();
1810
            throw $e;
1811
        }
1812
    }
1813
1814
    /**
1815
     * Delete translation from the ezcontentobject_name table.
1816
     *
1817
     * @param int $versionNo optional, if specified, apply to this Version only.
1818
     */
1819
    private function deleteTranslationFromContentNames(
1820
        int $contentId,
1821
        string $languageCode,
1822
        ?int $versionNo = null
1823
    ) {
1824
        $query = $this->connection->createQueryBuilder();
1825
        $query
1826
            ->delete('ezcontentobject_name')
1827
            ->where('contentobject_id=:contentId')
1828
            ->andWhere('real_translation=:languageCode')
1829
            ->setParameters(
1830
                [
1831
                    ':languageCode' => $languageCode,
1832
                    ':contentId' => $contentId,
1833
                ]
1834
            )
1835
        ;
1836
1837
        if (null !== $versionNo) {
1838
            $query
1839
                ->andWhere('content_version = :versionNo')
1840
                ->setParameter(':versionNo', $versionNo)
1841
            ;
1842
        }
1843
1844
        $query->execute();
1845
    }
1846
1847
    /**
1848
     * Remove language from language_mask of ezcontentobject.
1849
     *
1850
     * @param int $contentId
1851
     * @param int $languageId
1852
     *
1853
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
1854
     */
1855
    private function deleteTranslationFromContentObject($contentId, $languageId)
1856
    {
1857
        $query = $this->connection->createQueryBuilder();
1858
        $query->update('ezcontentobject')
1859
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
1860
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
1861
            ->set('modified', ':now')
1862
            ->where('id = :contentId')
1863
            ->andWhere(
1864
            // make sure removed translation is not the last one (incl. alwaysAvailable)
1865
                $query->expr()->andX(
1866
                    'language_mask & ~ ' . $languageId . ' <> 0',
1867
                    'language_mask & ~ ' . $languageId . ' <> 1'
1868
                )
1869
            )
1870
            ->setParameter(':now', time())
1871
            ->setParameter(':contentId', $contentId)
1872
        ;
1873
1874
        $rowCount = $query->execute();
1875
1876
        // no rows updated means that most likely somehow it was the last remaining translation
1877
        if ($rowCount === 0) {
1878
            throw new BadStateException(
1879
                '$languageCode',
1880
                'The provided translation is the only translation in this version'
1881
            );
1882
        }
1883
    }
1884
1885
    /**
1886
     * Remove language from language_mask of ezcontentobject_version and update initialLanguageId
1887
     * if it matches the removed one.
1888
     *
1889
     * @param int|null $versionNo optional, if specified, apply to this Version only.
1890
     *
1891
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1892
     */
1893
    private function deleteTranslationFromContentVersions(
1894
        int $contentId,
1895
        int $languageId,
1896
        ?int $versionNo = null
1897
    ) {
1898
        $query = $this->connection->createQueryBuilder();
1899
        $query->update('ezcontentobject_version')
1900
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
1901
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
1902
            ->set('modified', ':now')
1903
            // update initial_language_id only if it matches removed translation languageId
1904
            ->set(
1905
                'initial_language_id',
1906
                'CASE WHEN initial_language_id = :languageId ' .
1907
                'THEN (SELECT initial_language_id AS main_language_id FROM ezcontentobject c WHERE c.id = :contentId) ' .
1908
                'ELSE initial_language_id END'
1909
            )
1910
            ->where('contentobject_id = :contentId')
1911
            ->andWhere(
1912
            // make sure removed translation is not the last one (incl. alwaysAvailable)
1913
                $query->expr()->andX(
1914
                    'language_mask & ~ ' . $languageId . ' <> 0',
1915
                    'language_mask & ~ ' . $languageId . ' <> 1'
1916
                )
1917
            )
1918
            ->setParameter(':now', time())
1919
            ->setParameter(':contentId', $contentId)
1920
            ->setParameter(':languageId', $languageId)
1921
        ;
1922
1923
        if (null !== $versionNo) {
1924
            $query
1925
                ->andWhere('version = :versionNo')
1926
                ->setParameter(':versionNo', $versionNo)
1927
            ;
1928
        }
1929
1930
        $rowCount = $query->execute();
1931
1932
        // no rows updated means that most likely somehow it was the last remaining translation
1933
        if ($rowCount === 0) {
1934
            throw new BadStateException(
1935
                '$languageCode',
1936
                'The provided translation is the only translation in this version'
1937
            );
1938
        }
1939
    }
1940
1941
    /**
1942
     * Compute language mask and append it to a QueryBuilder (both column and parameter).
1943
     *
1944
     * **Can be used on UPDATE queries only!**
1945
     */
1946
    private function setLanguageMaskForUpdateQuery(
1947
        bool $alwaysAvailable,
1948
        DoctrineQueryBuilder $query,
1949
        string $languageMaskColumnName
1950
    ): DoctrineQueryBuilder {
1951
        if ($alwaysAvailable) {
1952
            $languageMaskExpr = $this->databasePlatform->getBitOrComparisonExpression(
1953
                $languageMaskColumnName,
1954
                ':languageMaskOperand'
1955
            );
1956
        } else {
1957
            $languageMaskExpr = $this->databasePlatform->getBitAndComparisonExpression(
1958
                $languageMaskColumnName,
1959
                ':languageMaskOperand'
1960
            );
1961
        }
1962
1963
        $query
1964
            ->set($languageMaskColumnName, $languageMaskExpr)
1965
            ->setParameter(
1966
                'languageMaskOperand',
1967
                $alwaysAvailable ? 1 : self::REMOVE_ALWAYS_AVAILABLE_LANG_MASK_OPERAND
1968
            );
1969
1970
        return $query;
1971
    }
1972
}
1973