Completed
Push — parallel_test_fork ( 28bfb3...1fd485 )
by
unknown
19:48
created

DoctrineDatabase::getSetVersionStatusQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 3
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the DoctrineDatabase Content Gateway class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
10
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\DBALException;
13
use Doctrine\DBAL\FetchMode;
14
use Doctrine\DBAL\ParameterType;
15
use Doctrine\DBAL\Query\QueryBuilder as DoctrineQueryBuilder;
16
use eZ\Publish\Core\Base\Exceptions\BadStateException;
17
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
18
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder;
19
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
20
use eZ\Publish\Core\Persistence\Database\UpdateQuery;
21
use eZ\Publish\Core\Persistence\Database\InsertQuery;
22
use eZ\Publish\Core\Persistence\Database\SelectQuery;
23
use eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue;
24
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator as LanguageMaskGenerator;
25
use eZ\Publish\SPI\Persistence\Content;
26
use eZ\Publish\SPI\Persistence\Content\CreateStruct;
27
use eZ\Publish\SPI\Persistence\Content\UpdateStruct;
28
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct;
29
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
30
use eZ\Publish\SPI\Persistence\Content\VersionInfo;
31
use eZ\Publish\SPI\Persistence\Content\Field;
32
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct;
33
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
34
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
35
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
36
use DOMXPath;
37
use DOMDocument;
38
use PDO;
39
40
/**
41
 * Doctrine database based content gateway.
42
 */
43
class DoctrineDatabase extends Gateway
44
{
45
    /**
46
     * eZ Doctrine database handler.
47
     *
48
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
49
     * @deprecated Start to use DBAL $connection instead.
50
     */
51
    protected $dbHandler;
52
53
    /**
54
     * The native Doctrine connection.
55
     *
56
     * Meant to be used to transition from eZ/Zeta interface to Doctrine.
57
     *
58
     * @var \Doctrine\DBAL\Connection
59
     */
60
    protected $connection;
61
62
    /**
63
     * Query builder.
64
     *
65
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder
66
     */
67
    protected $queryBuilder;
68
69
    /**
70
     * Caching language handler.
71
     *
72
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
73
     */
74
    protected $languageHandler;
75
76
    /**
77
     * Language mask generator.
78
     *
79
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
80
     */
81
    protected $languageMaskGenerator;
82
83
    /**
84
     * Creates a new gateway based on $db.
85
     *
86
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $db
87
     * @param \Doctrine\DBAL\Connection $connection
88
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder $queryBuilder
89
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
90
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
91
     */
92
    public function __construct(
93
        DatabaseHandler $db,
94
        Connection $connection,
95
        QueryBuilder $queryBuilder,
96
        LanguageHandler $languageHandler,
97
        LanguageMaskGenerator $languageMaskGenerator
98
    ) {
99
        $this->dbHandler = $db;
100
        $this->connection = $connection;
101
        $this->queryBuilder = $queryBuilder;
102
        $this->languageHandler = $languageHandler;
0 ignored issues
show
Documentation Bug introduced by
$languageHandler is of type object<eZ\Publish\SPI\Pe...ntent\Language\Handler>, but the property $languageHandler was declared to be of type object<eZ\Publish\Core\P...anguage\CachingHandler>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
103
        $this->languageMaskGenerator = $languageMaskGenerator;
104
    }
105
106
    /**
107
     * Get context definition for external storage layers.
108
     *
109
     * @return array
110
     */
111
    public function getContext()
112
    {
113
        return [
114
            'identifier' => 'LegacyStorage',
115
            'connection' => $this->dbHandler,
116
        ];
117
    }
118
119
    /**
120
     * Inserts a new content object.
121
     *
122
     * @param \eZ\Publish\SPI\Persistence\Content\CreateStruct $struct
123
     * @param mixed $currentVersionNo
124
     *
125
     * @return int ID
126
     */
127
    public function insertContentObject(CreateStruct $struct, $currentVersionNo = 1)
128
    {
129
        $initialLanguageId = !empty($struct->mainLanguageId) ? $struct->mainLanguageId : $struct->initialLanguageId;
130
        $initialLanguageCode = $this->languageHandler->load($initialLanguageId)->languageCode;
131
132
        if (isset($struct->name[$initialLanguageCode])) {
133
            $name = $struct->name[$initialLanguageCode];
134
        } else {
135
            $name = '';
136
        }
137
138
        $q = $this->dbHandler->createInsertQuery();
139
        $q->insertInto(
140
            $this->dbHandler->quoteTable('ezcontentobject')
141
        )->set(
142
            $this->dbHandler->quoteColumn('id'),
143
            $this->dbHandler->getAutoIncrementValue('ezcontentobject', 'id')
144
        )->set(
145
            $this->dbHandler->quoteColumn('current_version'),
146
            $q->bindValue($currentVersionNo, null, \PDO::PARAM_INT)
147
        )->set(
148
            $this->dbHandler->quoteColumn('name'),
149
            $q->bindValue($name, null, \PDO::PARAM_STR)
150
        )->set(
151
            $this->dbHandler->quoteColumn('contentclass_id'),
152
            $q->bindValue($struct->typeId, null, \PDO::PARAM_INT)
153
        )->set(
154
            $this->dbHandler->quoteColumn('section_id'),
155
            $q->bindValue($struct->sectionId, null, \PDO::PARAM_INT)
156
        )->set(
157
            $this->dbHandler->quoteColumn('owner_id'),
158
            $q->bindValue($struct->ownerId, null, \PDO::PARAM_INT)
159
        )->set(
160
            $this->dbHandler->quoteColumn('initial_language_id'),
161
            $q->bindValue($initialLanguageId, null, \PDO::PARAM_INT)
162
        )->set(
163
            $this->dbHandler->quoteColumn('remote_id'),
164
            $q->bindValue($struct->remoteId, null, \PDO::PARAM_STR)
165
        )->set(
166
            $this->dbHandler->quoteColumn('modified'),
167
            $q->bindValue(0, null, \PDO::PARAM_INT)
168
        )->set(
169
            $this->dbHandler->quoteColumn('published'),
170
            $q->bindValue(0, null, \PDO::PARAM_INT)
171
        )->set(
172
            $this->dbHandler->quoteColumn('status'),
173
            $q->bindValue(ContentInfo::STATUS_DRAFT, null, \PDO::PARAM_INT)
174
        )->set(
175
            $this->dbHandler->quoteColumn('language_mask'),
176
            $q->bindValue(
177
                $this->generateLanguageMask(
178
                    $struct->fields,
179
                    $initialLanguageCode,
180
                    $struct->alwaysAvailable
181
                ),
182
                null,
183
                \PDO::PARAM_INT
184
            )
185
        );
186
187
        $q->prepare()->execute();
188
189
        return $this->dbHandler->lastInsertId(
190
            $this->dbHandler->getSequenceName('ezcontentobject', 'id')
191
        );
192
    }
193
194
    /**
195
     * Generates a language mask for $fields.
196
     *
197
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $fields
198
     * @param string $initialLanguageCode
199
     * @param bool $isAlwaysAvailable
200
     *
201
     * @return int
202
     */
203
    protected function generateLanguageMask(array $fields, string $initialLanguageCode, bool $isAlwaysAvailable): int
204
    {
205
        $languages = [$initialLanguageCode => true];
206 View Code Duplication
        foreach ($fields as $field) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
207
            if (isset($languages[$field->languageCode])) {
208
                continue;
209
            }
210
211
            $languages[$field->languageCode] = true;
212
        }
213
214
        return $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(array_keys($languages), $isAlwaysAvailable);
215
    }
216
217
    /**
218
     * Inserts a new version.
219
     *
220
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $versionInfo
221
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $fields
222
     *
223
     * @return int ID
224
     */
225
    public function insertVersion(VersionInfo $versionInfo, array $fields)
226
    {
227
        /** @var $q \eZ\Publish\Core\Persistence\Database\InsertQuery */
228
        $q = $this->dbHandler->createInsertQuery();
229
        $q->insertInto(
230
            $this->dbHandler->quoteTable('ezcontentobject_version')
231
        )->set(
232
            $this->dbHandler->quoteColumn('id'),
233
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_version', 'id')
234
        )->set(
235
            $this->dbHandler->quoteColumn('version'),
236
            $q->bindValue($versionInfo->versionNo, null, \PDO::PARAM_INT)
237
        )->set(
238
            $this->dbHandler->quoteColumn('modified'),
239
            $q->bindValue($versionInfo->modificationDate, null, \PDO::PARAM_INT)
240
        )->set(
241
            $this->dbHandler->quoteColumn('creator_id'),
242
            $q->bindValue($versionInfo->creatorId, null, \PDO::PARAM_INT)
243
        )->set(
244
            $this->dbHandler->quoteColumn('created'),
245
            $q->bindValue($versionInfo->creationDate, null, \PDO::PARAM_INT)
246
        )->set(
247
            $this->dbHandler->quoteColumn('status'),
248
            $q->bindValue($versionInfo->status, null, \PDO::PARAM_INT)
249
        )->set(
250
            $this->dbHandler->quoteColumn('initial_language_id'),
251
            $q->bindValue(
252
                $this->languageHandler->loadByLanguageCode($versionInfo->initialLanguageCode)->id,
253
                null,
254
                \PDO::PARAM_INT
255
            )
256
        )->set(
257
            $this->dbHandler->quoteColumn('contentobject_id'),
258
            $q->bindValue($versionInfo->contentInfo->id, null, \PDO::PARAM_INT)
259
        )->set(
260
            // As described in field mapping document
261
            $this->dbHandler->quoteColumn('workflow_event_pos'),
262
            $q->bindValue(0, null, \PDO::PARAM_INT)
263
        )->set(
264
            $this->dbHandler->quoteColumn('language_mask'),
265
            $q->bindValue(
266
                $this->generateLanguageMask(
267
                    $fields,
268
                    $versionInfo->initialLanguageCode,
269
                    $versionInfo->contentInfo->alwaysAvailable
270
                ),
271
                null,
272
                \PDO::PARAM_INT
273
            )
274
        );
275
276
        $q->prepare()->execute();
277
278
        return $this->dbHandler->lastInsertId(
279
            $this->dbHandler->getSequenceName('ezcontentobject_version', 'id')
280
        );
281
    }
282
283
    /**
284
     * Updates an existing content identified by $contentId in respect to $struct.
285
     *
286
     * @param int $contentId
287
     * @param \eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct $struct
288
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $prePublishVersionInfo Provided on publish
289
     */
290
    public function updateContent($contentId, MetadataUpdateStruct $struct, VersionInfo $prePublishVersionInfo = null)
291
    {
292
        $q = $this->dbHandler->createUpdateQuery();
293
        $q->update($this->dbHandler->quoteTable('ezcontentobject'));
294
295
        if (isset($struct->name)) {
296
            $q->set(
297
                $this->dbHandler->quoteColumn('name'),
298
                $q->bindValue($struct->name, null, \PDO::PARAM_STR)
299
            );
300
        }
301
        if (isset($struct->mainLanguageId)) {
302
            $q->set(
303
                $this->dbHandler->quoteColumn('initial_language_id'),
304
                $q->bindValue($struct->mainLanguageId, null, \PDO::PARAM_INT)
305
            );
306
        }
307
        if (isset($struct->modificationDate)) {
308
            $q->set(
309
                $this->dbHandler->quoteColumn('modified'),
310
                $q->bindValue($struct->modificationDate, null, \PDO::PARAM_INT)
311
            );
312
        }
313
        if (isset($struct->ownerId)) {
314
            $q->set(
315
                $this->dbHandler->quoteColumn('owner_id'),
316
                $q->bindValue($struct->ownerId, null, \PDO::PARAM_INT)
317
            );
318
        }
319
        if (isset($struct->publicationDate)) {
320
            $q->set(
321
                $this->dbHandler->quoteColumn('published'),
322
                $q->bindValue($struct->publicationDate, null, \PDO::PARAM_INT)
323
            );
324
        }
325
        if (isset($struct->remoteId)) {
326
            $q->set(
327
                $this->dbHandler->quoteColumn('remote_id'),
328
                $q->bindValue($struct->remoteId, null, \PDO::PARAM_STR)
329
            );
330
        }
331
        if ($prePublishVersionInfo !== null) {
332
            $mask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(
333
                $prePublishVersionInfo->languageCodes,
334
                $struct->alwaysAvailable ?? $prePublishVersionInfo->contentInfo->alwaysAvailable
335
            );
336
337
            $q->set(
338
                $this->dbHandler->quoteColumn('language_mask'),
339
                $q->bindValue($mask, null, \PDO::PARAM_INT)
340
            );
341
        }
342
        if (isset($struct->isHidden)) {
343
            $q->set(
344
                $this->dbHandler->quoteColumn('is_hidden'),
345
                $q->bindValue($struct->isHidden, null, \PDO::PARAM_BOOL)
346
            );
347
        }
348
        $q->where(
349
            $q->expr->eq(
350
                $this->dbHandler->quoteColumn('id'),
351
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
352
            )
353
        );
354
        $q->prepare()->execute();
355
356
        // Handle alwaysAvailable flag update separately as it's a more complex task and has impact on several tables
357
        if (isset($struct->alwaysAvailable) || isset($struct->mainLanguageId)) {
358
            $this->updateAlwaysAvailableFlag($contentId, $struct->alwaysAvailable);
359
        }
360
    }
361
362
    /**
363
     * Updates version $versionNo for content identified by $contentId, in respect to $struct.
364
     *
365
     * @param int $contentId
366
     * @param int $versionNo
367
     * @param \eZ\Publish\SPI\Persistence\Content\UpdateStruct $struct
368
     */
369
    public function updateVersion($contentId, $versionNo, UpdateStruct $struct)
370
    {
371
        $q = $this->dbHandler->createUpdateQuery();
372
        $q->update(
373
            $this->dbHandler->quoteTable('ezcontentobject_version')
374
        )->set(
375
            $this->dbHandler->quoteColumn('creator_id'),
376
            $q->bindValue($struct->creatorId, null, \PDO::PARAM_INT)
377
        )->set(
378
            $this->dbHandler->quoteColumn('modified'),
379
            $q->bindValue($struct->modificationDate, null, \PDO::PARAM_INT)
380
        )->set(
381
            $this->dbHandler->quoteColumn('initial_language_id'),
382
            $q->bindValue($struct->initialLanguageId, null, \PDO::PARAM_INT)
383
        )->set(
384
            $this->dbHandler->quoteColumn('language_mask'),
385
            $q->expr->bitOr(
386
                $this->dbHandler->quoteColumn('language_mask'),
387
                $q->bindValue(
388
                    $this->generateLanguageMask(
389
                        $struct->fields,
390
                        $this->languageHandler->load($struct->initialLanguageId)->languageCode,
391
                        false
392
                    ),
393
                    null,
394
                    \PDO::PARAM_INT
395
                )
396
            )
397
        )->where(
398
            $q->expr->lAnd(
399
                $q->expr->eq(
400
                    $this->dbHandler->quoteColumn('contentobject_id'),
401
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
402
                ),
403
                $q->expr->eq(
404
                    $this->dbHandler->quoteColumn('version'),
405
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
406
                )
407
            )
408
        );
409
        $q->prepare()->execute();
410
    }
411
412
    /**
413
     * Updates "always available" flag for Content identified by $contentId, in respect to
414
     * Content's current main language and optionally new $alwaysAvailable state.
415
     *
416
     * @param int $contentId
417
     * @param bool|null $alwaysAvailable New "always available" value or null if not defined
418
     */
419
    public function updateAlwaysAvailableFlag($contentId, $alwaysAvailable = null)
420
    {
421
        // We will need to know some info on the current language mask to update the flag
422
        // everywhere needed
423
        $contentInfoRow = $this->loadContentInfo($contentId);
424
        if (!isset($alwaysAvailable)) {
425
            $alwaysAvailable = (bool)$contentInfoRow['language_mask'] & 1;
426
        }
427
428
        /** @var $q \eZ\Publish\Core\Persistence\Database\UpdateQuery */
429
        $q = $this->dbHandler->createUpdateQuery();
430
        $q
431
            ->update($this->dbHandler->quoteTable('ezcontentobject'))
432
            ->set(
433
                $this->dbHandler->quoteColumn('language_mask'),
434
                $alwaysAvailable ?
435
                    $q->expr->bitOr($this->dbHandler->quoteColumn('language_mask'), 1) :
436
                    $q->expr->bitAnd($this->dbHandler->quoteColumn('language_mask'), -2)
437
            )
438
            ->where(
439
                $q->expr->eq(
440
                    $this->dbHandler->quoteColumn('id'),
441
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
442
                )
443
            );
444
        $q->prepare()->execute();
445
446
        // Now we need to update ezcontentobject_name
447
        /** @var $qName \eZ\Publish\Core\Persistence\Database\UpdateQuery */
448
        $qName = $this->dbHandler->createUpdateQuery();
449
        $qName
450
            ->update($this->dbHandler->quoteTable('ezcontentobject_name'))
451
            ->set(
452
                $this->dbHandler->quoteColumn('language_id'),
453
                $alwaysAvailable ?
454
                    $qName->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1) :
455
                    $qName->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
456
            )
457
            ->where(
458
                $qName->expr->lAnd(
459
                    $qName->expr->eq(
460
                        $this->dbHandler->quoteColumn('contentobject_id'),
461
                        $qName->bindValue($contentId, null, \PDO::PARAM_INT)
462
                    ),
463
                    $qName->expr->eq(
464
                        $this->dbHandler->quoteColumn('content_version'),
465
                        $qName->bindValue(
466
                            $contentInfoRow['current_version'],
467
                            null,
468
                            \PDO::PARAM_INT
469
                        )
470
                    )
471
                )
472
            );
473
        $qName->prepare()->execute();
474
475
        // Now update ezcontentobject_attribute for current version
476
        // Create update query that will be reused
477
        /** @var $qAttr \eZ\Publish\Core\Persistence\Database\UpdateQuery */
478
        $qAttr = $this->dbHandler->createUpdateQuery();
479
        $qAttr
480
            ->update($this->dbHandler->quoteTable('ezcontentobject_attribute'))
481
            ->where(
482
                $qAttr->expr->lAnd(
483
                    $qAttr->expr->eq(
484
                        $this->dbHandler->quoteColumn('contentobject_id'),
485
                        $qAttr->bindValue($contentId, null, \PDO::PARAM_INT)
486
                    ),
487
                    $qAttr->expr->eq(
488
                        $this->dbHandler->quoteColumn('version'),
489
                        $qAttr->bindValue(
490
                            $contentInfoRow['current_version'],
491
                            null,
492
                            \PDO::PARAM_INT
493
                        )
494
                    )
495
                )
496
            );
497
498
        // If there is only a single language, update all fields and return
499
        if (!$this->languageMaskGenerator->isLanguageMaskComposite($contentInfoRow['language_mask'])) {
500
            $qAttr->set(
501
                $this->dbHandler->quoteColumn('language_id'),
502
                $alwaysAvailable ?
503
                    $qAttr->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1) :
504
                    $qAttr->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
505
            );
506
            $qAttr->prepare()->execute();
507
508
            return;
509
        }
510
511
        // Otherwise:
512
        // 1. Remove always available flag on all fields
513
        $qAttr->set(
514
            $this->dbHandler->quoteColumn('language_id'),
515
            $qAttr->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
516
        );
517
        $qAttr->prepare()->execute();
518
519
        // 2. If Content is always available set the flag only on fields in main language
520
        if ($alwaysAvailable) {
521
            $qAttr->set(
522
                $this->dbHandler->quoteColumn('language_id'),
523
                $qAttr->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1)
524
            );
525
            $qAttr->where(
526
                $qAttr->expr->gt(
527
                    $qAttr->expr->bitAnd(
528
                        $this->dbHandler->quoteColumn('language_id'),
529
                        $qAttr->bindValue($contentInfoRow['initial_language_id'], null, PDO::PARAM_INT)
530
                    ),
531
                    $qAttr->bindValue(0, null, PDO::PARAM_INT)
532
                )
533
            );
534
            $qAttr->prepare()->execute();
535
        }
536
    }
537
538
    /**
539
     * Sets the status of the version identified by $contentId and $version to $status.
540
     *
541
     * The $status can be one of STATUS_DRAFT, STATUS_PUBLISHED, STATUS_ARCHIVED
542
     *
543
     * @param int $contentId
544
     * @param int $version
545
     * @param int $status
546
     *
547
     * @return bool
548
     */
549
    public function setStatus($contentId, $version, $status)
550
    {
551
        $q = $this->dbHandler->createUpdateQuery();
552
        $q->update(
553
            $this->dbHandler->quoteTable('ezcontentobject_version')
554
        )->set(
555
            $this->dbHandler->quoteColumn('status'),
556
            $q->bindValue($status, null, \PDO::PARAM_INT)
557
        )->set(
558
            $this->dbHandler->quoteColumn('modified'),
559
            $q->bindValue(time(), null, \PDO::PARAM_INT)
560
        )->where(
561
            $q->expr->lAnd(
562
                $q->expr->eq(
563
                    $this->dbHandler->quoteColumn('contentobject_id'),
564
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
565
                ),
566
                $q->expr->eq(
567
                    $this->dbHandler->quoteColumn('version'),
568
                    $q->bindValue($version, null, \PDO::PARAM_INT)
569
                )
570
            )
571
        );
572
        $statement = $q->prepare();
573
        $statement->execute();
574
575
        if ((bool)$statement->rowCount() === false) {
576
            return false;
577
        }
578
579
        if ($status !== APIVersionInfo::STATUS_PUBLISHED) {
580
            return true;
581
        }
582
583
        // If the version's status is PUBLISHED, we set the content to published status as well
584
        $q = $this->dbHandler->createUpdateQuery();
585
        $q->update(
586
            $this->dbHandler->quoteTable('ezcontentobject')
587
        )->set(
588
            $this->dbHandler->quoteColumn('status'),
589
            $q->bindValue(ContentInfo::STATUS_PUBLISHED, null, \PDO::PARAM_INT)
590
        )->set(
591
            $this->dbHandler->quoteColumn('current_version'),
592
            $q->bindValue($version, null, \PDO::PARAM_INT)
593
        )->where(
594
            $q->expr->eq(
595
                $this->dbHandler->quoteColumn('id'),
596
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
597
            )
598
        );
599
        $statement = $q->prepare();
600
        $statement->execute();
601
602
        return (bool)$statement->rowCount();
603
    }
604
605
    public function setPublishedStatus(int $contentId, int $versionNo): void
606
    {
607
        $query = $this->getSetVersionStatusQuery(
608
            $contentId,
609
            $versionNo,
610
            VersionInfo::STATUS_PUBLISHED
611
        );
612
        // @todo refactor inner select to use QueryBuilder
613
        $query->andWhere(
614
            'NOT EXISTS (SELECT 1 FROM (SELECT * FROM ezcontentobject_version ) as V WHERE contentobject_id = :contentId AND status = :status)'
615
        );
616
        if (0 === $query->execute()) {
617
            throw new BadStateException('$contentId', "Someone just published another Version of the Content item {$contentId}");
618
        }
619
        $this->markContentAsPublished($contentId, $versionNo);
620
    }
621
622
    private function getSetVersionStatusQuery(
623
        int $contentId,
624
        int $versionNo,
625
        int $versionStatus
626
    ): DoctrineQueryBuilder {
627
        $query = $this->connection->createQueryBuilder();
628
        $query
629
            ->update('ezcontentobject_version')
630
            ->set('status', ':status')
631
            ->set('modified', ':modified')
632
            ->where('contentobject_id = :contentId')
633
            ->andWhere('version = :versionNo')
634
            ->setParameter('status', $versionStatus, ParameterType::INTEGER)
635
            ->setParameter('modified', time(), ParameterType::INTEGER)
636
            ->setParameter('contentId', $contentId, ParameterType::INTEGER)
637
            ->setParameter('versionNo', $versionNo, ParameterType::INTEGER);
638
639
        return $query;
640
    }
641
642
643 View Code Duplication
    private function markContentAsPublished(int $contentId, int $versionNo): void
644
    {
645
        $query = $this->connection->createQueryBuilder();
646
        $query
647
            ->update('ezcontentobject')
648
            ->set('status', ':status')
649
            ->set('current_version', ':versionNo')
650
            ->where('id =:contentId')
651
            ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER)
652
            ->setParameter('versionNo', $versionNo, ParameterType::INTEGER)
653
            ->setParameter('contentId', $contentId, ParameterType::INTEGER);
654
        $query->execute();
655
    }
656
657
    /**
658
     * Inserts a new field.
659
     *
660
     * Only used when a new field is created (i.e. a new object or a field in a
661
     * new language!). After that, field IDs need to stay the same, only the
662
     * version number changes.
663
     *
664
     * @param \eZ\Publish\SPI\Persistence\Content $content
665
     * @param \eZ\Publish\SPI\Persistence\Content\Field $field
666
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value
667
     *
668
     * @return int ID
669
     */
670
    public function insertNewField(Content $content, Field $field, StorageFieldValue $value)
671
    {
672
        $q = $this->dbHandler->createInsertQuery();
673
674
        $this->setInsertFieldValues($q, $content, $field, $value);
675
676
        // Insert with auto increment ID
677
        $q->set(
678
            $this->dbHandler->quoteColumn('id'),
679
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_attribute', 'id')
680
        );
681
682
        $q->prepare()->execute();
683
684
        return $this->dbHandler->lastInsertId(
685
            $this->dbHandler->getSequenceName('ezcontentobject_attribute', 'id')
686
        );
687
    }
688
689
    /**
690
     * Inserts an existing field.
691
     *
692
     * Used to insert a field with an exsting ID but a new version number.
693
     *
694
     * @param Content $content
695
     * @param Field $field
696
     * @param StorageFieldValue $value
697
     */
698
    public function insertExistingField(Content $content, Field $field, StorageFieldValue $value)
699
    {
700
        $q = $this->dbHandler->createInsertQuery();
701
702
        $this->setInsertFieldValues($q, $content, $field, $value);
703
704
        $q->set(
705
            $this->dbHandler->quoteColumn('id'),
706
            $q->bindValue($field->id, null, \PDO::PARAM_INT)
707
        );
708
709
        $q->prepare()->execute();
710
    }
711
712
    /**
713
     * Sets field (ezcontentobject_attribute) values to the given query.
714
     *
715
     * @param \eZ\Publish\Core\Persistence\Database\InsertQuery $q
716
     * @param Content $content
717
     * @param Field $field
718
     * @param StorageFieldValue $value
719
     */
720
    protected function setInsertFieldValues(InsertQuery $q, Content $content, Field $field, StorageFieldValue $value)
721
    {
722
        $q->insertInto(
723
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
724
        )->set(
725
            $this->dbHandler->quoteColumn('contentobject_id'),
726
            $q->bindValue($content->versionInfo->contentInfo->id, null, \PDO::PARAM_INT)
727
        )->set(
728
            $this->dbHandler->quoteColumn('contentclassattribute_id'),
729
            $q->bindValue($field->fieldDefinitionId, null, \PDO::PARAM_INT)
730
        )->set(
731
            $this->dbHandler->quoteColumn('data_type_string'),
732
            $q->bindValue($field->type)
733
        )->set(
734
            $this->dbHandler->quoteColumn('language_code'),
735
            $q->bindValue($field->languageCode)
736
        )->set(
737
            $this->dbHandler->quoteColumn('version'),
738
            $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
739
        )->set(
740
            $this->dbHandler->quoteColumn('data_float'),
741
            $q->bindValue($value->dataFloat)
742
        )->set(
743
            $this->dbHandler->quoteColumn('data_int'),
744
            $q->bindValue($value->dataInt, null, \PDO::PARAM_INT)
745
        )->set(
746
            $this->dbHandler->quoteColumn('data_text'),
747
            $q->bindValue($value->dataText)
748
        )->set(
749
            $this->dbHandler->quoteColumn('sort_key_int'),
750
            $q->bindValue($value->sortKeyInt, null, \PDO::PARAM_INT)
751
        )->set(
752
            $this->dbHandler->quoteColumn('sort_key_string'),
753
            $q->bindValue(mb_substr($value->sortKeyString, 0, 255))
754
        )->set(
755
            $this->dbHandler->quoteColumn('language_id'),
756
            $q->bindValue(
757
                $this->languageMaskGenerator->generateLanguageIndicator(
758
                    $field->languageCode,
759
                    $this->isLanguageAlwaysAvailable($content, $field->languageCode)
760
                ),
761
                null,
762
                \PDO::PARAM_INT
763
            )
764
        );
765
    }
766
767
    /**
768
     * Checks if $languageCode is always available in $content.
769
     *
770
     * @param \eZ\Publish\SPI\Persistence\Content $content
771
     * @param string $languageCode
772
     *
773
     * @return bool
774
     */
775
    protected function isLanguageAlwaysAvailable(Content $content, $languageCode)
776
    {
777
        return
778
            $content->versionInfo->contentInfo->alwaysAvailable &&
779
            $content->versionInfo->contentInfo->mainLanguageCode === $languageCode
780
        ;
781
    }
782
783
    /**
784
     * Updates an existing field.
785
     *
786
     * @param Field $field
787
     * @param StorageFieldValue $value
788
     */
789
    public function updateField(Field $field, StorageFieldValue $value)
790
    {
791
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
792
        // cannot change on update
793
        $q = $this->dbHandler->createUpdateQuery();
794
        $this->setFieldUpdateValues($q, $value);
795
        $q->where(
796
            $q->expr->lAnd(
797
                $q->expr->eq(
798
                    $this->dbHandler->quoteColumn('id'),
799
                    $q->bindValue($field->id, null, \PDO::PARAM_INT)
800
                ),
801
                $q->expr->eq(
802
                    $this->dbHandler->quoteColumn('version'),
803
                    $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
804
                )
805
            )
806
        );
807
        $q->prepare()->execute();
808
    }
809
810
    /**
811
     * Sets update fields for $value on $q.
812
     *
813
     * @param \eZ\Publish\Core\Persistence\Database\UpdateQuery $q
814
     * @param StorageFieldValue $value
815
     */
816
    protected function setFieldUpdateValues(UpdateQuery $q, StorageFieldValue $value)
817
    {
818
        $q->update(
819
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
820
        )->set(
821
            $this->dbHandler->quoteColumn('data_float'),
822
            $q->bindValue($value->dataFloat)
823
        )->set(
824
            $this->dbHandler->quoteColumn('data_int'),
825
            $q->bindValue($value->dataInt, null, \PDO::PARAM_INT)
826
        )->set(
827
            $this->dbHandler->quoteColumn('data_text'),
828
            $q->bindValue($value->dataText)
829
        )->set(
830
            $this->dbHandler->quoteColumn('sort_key_int'),
831
            $q->bindValue($value->sortKeyInt, null, \PDO::PARAM_INT)
832
        )->set(
833
            $this->dbHandler->quoteColumn('sort_key_string'),
834
            $q->bindValue(mb_substr($value->sortKeyString, 0, 255))
835
        );
836
    }
837
838
    /**
839
     * Updates an existing, non-translatable field.
840
     *
841
     * @param \eZ\Publish\SPI\Persistence\Content\Field $field
842
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value
843
     * @param int $contentId
844
     */
845
    public function updateNonTranslatableField(
846
        Field $field,
847
        StorageFieldValue $value,
848
        $contentId
849
    ) {
850
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
851
        // cannot change on update
852
        $q = $this->dbHandler->createUpdateQuery();
853
        $this->setFieldUpdateValues($q, $value);
854
        $q->where(
855
            $q->expr->lAnd(
856
                $q->expr->eq(
857
                    $this->dbHandler->quoteColumn('contentclassattribute_id'),
858
                    $q->bindValue($field->fieldDefinitionId, null, \PDO::PARAM_INT)
859
                ),
860
                $q->expr->eq(
861
                    $this->dbHandler->quoteColumn('contentobject_id'),
862
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
863
                ),
864
                $q->expr->eq(
865
                    $this->dbHandler->quoteColumn('version'),
866
                    $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
867
                )
868
            )
869
        );
870
        $q->prepare()->execute();
871
    }
872
873
    /**
874
     * {@inheritdoc}
875
     */
876
    public function load($contentId, $version = null, array $translations = null)
877
    {
878
        return $this->internalLoadContent([$contentId], $version, $translations);
0 ignored issues
show
Bug introduced by
It seems like $translations defined by parameter $translations on line 876 can also be of type array; however, eZ\Publish\Core\Persiste...::internalLoadContent() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
879
    }
880
881
    /**
882
     * {@inheritdoc}
883
     */
884
    public function loadContentList(array $contentIds, array $translations = null): array
885
    {
886
        return $this->internalLoadContent($contentIds, null, $translations);
0 ignored issues
show
Bug introduced by
It seems like $translations defined by parameter $translations on line 884 can also be of type array; however, eZ\Publish\Core\Persiste...::internalLoadContent() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
887
    }
888
889
    /**
890
     * @see load(), loadContentList()
891
     *
892
     * @param array $contentIds
893
     * @param int|null $version
894
     * @param string[]|null $translations
895
     *
896
     * @return array
897
     */
898
    private function internalLoadContent(array $contentIds, int $version = null, array $translations = null): array
899
    {
900
        $queryBuilder = $this->connection->createQueryBuilder();
901
        $expr = $queryBuilder->expr();
902
        $queryBuilder
903
            ->select(
904
                'c.id AS ezcontentobject_id',
905
                'c.contentclass_id AS ezcontentobject_contentclass_id',
906
                'c.section_id AS ezcontentobject_section_id',
907
                'c.owner_id AS ezcontentobject_owner_id',
908
                'c.remote_id AS ezcontentobject_remote_id',
909
                'c.current_version AS ezcontentobject_current_version',
910
                'c.initial_language_id AS ezcontentobject_initial_language_id',
911
                'c.modified AS ezcontentobject_modified',
912
                'c.published AS ezcontentobject_published',
913
                'c.status AS ezcontentobject_status',
914
                'c.name AS ezcontentobject_name',
915
                'c.language_mask AS ezcontentobject_language_mask',
916
                'c.is_hidden AS ezcontentobject_is_hidden',
917
                'v.id AS ezcontentobject_version_id',
918
                'v.version AS ezcontentobject_version_version',
919
                'v.modified AS ezcontentobject_version_modified',
920
                'v.creator_id AS ezcontentobject_version_creator_id',
921
                'v.created AS ezcontentobject_version_created',
922
                'v.status AS ezcontentobject_version_status',
923
                'v.language_mask AS ezcontentobject_version_language_mask',
924
                'v.initial_language_id AS ezcontentobject_version_initial_language_id',
925
                'a.id AS ezcontentobject_attribute_id',
926
                'a.contentclassattribute_id AS ezcontentobject_attribute_contentclassattribute_id',
927
                'a.data_type_string AS ezcontentobject_attribute_data_type_string',
928
                'a.language_code AS ezcontentobject_attribute_language_code',
929
                'a.language_id AS ezcontentobject_attribute_language_id',
930
                'a.data_float AS ezcontentobject_attribute_data_float',
931
                'a.data_int AS ezcontentobject_attribute_data_int',
932
                'a.data_text AS ezcontentobject_attribute_data_text',
933
                'a.sort_key_int AS ezcontentobject_attribute_sort_key_int',
934
                'a.sort_key_string AS ezcontentobject_attribute_sort_key_string',
935
                't.main_node_id AS ezcontentobject_tree_main_node_id'
936
            )
937
            ->from('ezcontentobject', 'c')
938
            ->innerJoin(
939
                'c',
940
                'ezcontentobject_version',
941
                'v',
942
                $expr->andX(
943
                    $expr->eq('c.id', 'v.contentobject_id'),
944
                    $expr->eq('v.version', $version ?? 'c.current_version')
945
                )
946
            )
947
            ->innerJoin(
948
                'v',
949
                'ezcontentobject_attribute',
950
                'a',
951
                $expr->andX(
952
                    $expr->eq('v.contentobject_id', 'a.contentobject_id'),
953
                    $expr->eq('v.version', 'a.version')
954
                )
955
            )
956
            ->leftJoin(
957
                'c',
958
                'ezcontentobject_tree',
959
                't',
960
                $expr->andX(
961
                    $expr->eq('c.id', 't.contentobject_id'),
962
                    $expr->eq('t.node_id', 't.main_node_id')
963
                )
964
            );
965
966
        $queryBuilder->where(
967
            $expr->in(
968
                'c.id',
969
                $queryBuilder->createNamedParameter($contentIds, Connection::PARAM_INT_ARRAY)
970
            )
971
        );
972
973
        if (!empty($translations)) {
974
            $queryBuilder->andWhere(
975
                $expr->in(
976
                    'a.language_code',
977
                    $queryBuilder->createNamedParameter($translations, Connection::PARAM_STR_ARRAY)
978
                )
979
            );
980
        }
981
982
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
983
    }
984
985
    /**
986
     * Get query builder to load Content Info data.
987
     *
988
     * @see loadContentInfo(), loadContentInfoByRemoteId(), loadContentInfoList(), loadContentInfoByLocationId()
989
     *
990
     * @param bool $joinMainLocation
991
     *
992
     * @return \Doctrine\DBAL\Query\QueryBuilder
993
     */
994
    private function createLoadContentInfoQueryBuilder(bool $joinMainLocation = true): DoctrineQueryBuilder
995
    {
996
        $queryBuilder = $this->connection->createQueryBuilder();
997
        $expr = $queryBuilder->expr();
998
999
        $joinCondition = $expr->eq('c.id', 't.contentobject_id');
1000
        if ($joinMainLocation) {
1001
            // wrap join condition with AND operator and join by a Main Location
1002
            $joinCondition = $expr->andX(
1003
                $joinCondition,
1004
                $expr->eq('t.node_id', 't.main_node_id')
1005
            );
1006
        }
1007
1008
        $queryBuilder
1009
            ->select('c.*', 't.main_node_id AS ezcontentobject_tree_main_node_id')
1010
            ->from('ezcontentobject', 'c')
1011
            ->leftJoin(
1012
                'c',
1013
                'ezcontentobject_tree',
1014
                't',
1015
                $joinCondition
0 ignored issues
show
Bug introduced by
It seems like $joinCondition defined by $expr->andX($joinConditi...id', 't.main_node_id')) on line 1002 can also be of type object<Doctrine\DBAL\Que...on\CompositeExpression>; however, Doctrine\DBAL\Query\QueryBuilder::leftJoin() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1016
            );
1017
1018
        return $queryBuilder;
1019
    }
1020
1021
    /**
1022
     * Loads info for content identified by $contentId.
1023
     * Will basically return a hash containing all field values for ezcontentobject table plus some additional keys:
1024
     *  - always_available => Boolean indicating if content's language mask contains alwaysAvailable bit field
1025
     *  - main_language_code => Language code for main (initial) language. E.g. "eng-GB".
1026
     *
1027
     * @param int $contentId
1028
     *
1029
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1030
     *
1031
     * @return array
1032
     */
1033 View Code Duplication
    public function loadContentInfo($contentId)
1034
    {
1035
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1036
        $queryBuilder
1037
            ->where('c.id = :id')
1038
            ->setParameter('id', $contentId, ParameterType::INTEGER);
1039
1040
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1041
        if (empty($results)) {
1042
            throw new NotFound('content', "id: $contentId");
1043
        }
1044
1045
        return $results[0];
1046
    }
1047
1048
    public function loadContentInfoList(array $contentIds)
1049
    {
1050
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1051
        $queryBuilder
1052
            ->where('c.id IN (:ids)')
1053
            ->setParameter('ids', $contentIds, Connection::PARAM_INT_ARRAY);
1054
1055
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1056
    }
1057
1058
    /**
1059
     * Loads info for a content object identified by its remote ID.
1060
     *
1061
     * Returns an array with the relevant data.
1062
     *
1063
     * @param mixed $remoteId
1064
     *
1065
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1066
     *
1067
     * @return array
1068
     */
1069 View Code Duplication
    public function loadContentInfoByRemoteId($remoteId)
1070
    {
1071
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1072
        $queryBuilder
1073
            ->where('c.remote_id = :id')
1074
            ->setParameter('id', $remoteId, ParameterType::STRING);
1075
1076
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1077
        if (empty($results)) {
1078
            throw new NotFound('content', "remote_id: $remoteId");
1079
        }
1080
1081
        return $results[0];
1082
    }
1083
1084
    /**
1085
     * Loads info for a content object identified by its location ID (node ID).
1086
     *
1087
     * Returns an array with the relevant data.
1088
     *
1089
     * @param int $locationId
1090
     *
1091
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1092
     *
1093
     * @return array
1094
     */
1095 View Code Duplication
    public function loadContentInfoByLocationId($locationId)
1096
    {
1097
        $queryBuilder = $this->createLoadContentInfoQueryBuilder(false);
1098
        $queryBuilder
1099
            ->where('t.node_id = :id')
1100
            ->setParameter('id', $locationId, ParameterType::INTEGER);
1101
1102
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1103
        if (empty($results)) {
1104
            throw new NotFound('content', "node_id: $locationId");
1105
        }
1106
1107
        return $results[0];
1108
    }
1109
1110
    /**
1111
     * Loads version info for content identified by $contentId and $versionNo.
1112
     * Will basically return a hash containing all field values from ezcontentobject_version table plus following keys:
1113
     *  - names => Hash of content object names. Key is the language code, value is the name.
1114
     *  - languages => Hash of language ids. Key is the language code (e.g. "eng-GB"), value is the language numeric id without the always available bit.
1115
     *  - initial_language_code => Language code for initial language in this version.
1116
     *
1117
     * @param int $contentId
1118
     * @param int $versionNo
1119
     *
1120
     * @return array
1121
     */
1122 View Code Duplication
    public function loadVersionInfo($contentId, $versionNo)
1123
    {
1124
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1125
        $query->where(
1126
            $query->expr->lAnd(
1127
                $query->expr->eq(
1128
                    $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_version'),
1129
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1130
                ),
1131
                $query->expr->eq(
1132
                    $this->dbHandler->quoteColumn('version', 'ezcontentobject_version'),
1133
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1134
                )
1135
            )
1136
        );
1137
        $statement = $query->prepare();
1138
        $statement->execute();
1139
1140
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1141
    }
1142
1143
    /**
1144
     * Returns data for all versions with given status created by the given $userId.
1145
     *
1146
     * @param int $userId
1147
     * @param int $status
1148
     *
1149
     * @return string[][]
1150
     */
1151
    public function listVersionsForUser($userId, $status = VersionInfo::STATUS_DRAFT)
1152
    {
1153
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1154
        $query->where(
1155
            $query->expr->lAnd(
1156
                $query->expr->eq(
1157
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_version'),
1158
                    $query->bindValue($status, null, \PDO::PARAM_INT)
1159
                ),
1160
                $query->expr->eq(
1161
                    $this->dbHandler->quoteColumn('creator_id', 'ezcontentobject_version'),
1162
                    $query->bindValue($userId, null, \PDO::PARAM_INT)
1163
                )
1164
            )
1165
        );
1166
1167
        return $this->listVersionsHelper($query);
1168
    }
1169
1170
    /**
1171
     * Returns all version data for the given $contentId, optionally filtered by status.
1172
     *
1173
     * Result is returned with oldest version first (using version id as it has index and is auto increment).
1174
     *
1175
     * @param mixed $contentId
1176
     * @param mixed|null $status Optional argument to filter versions by status, like {@see VersionInfo::STATUS_ARCHIVED}.
1177
     * @param int $limit Limit for items returned, -1 means none.
1178
     *
1179
     * @return string[][]
1180
     */
1181
    public function listVersions($contentId, $status = null, $limit = -1)
1182
    {
1183
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1184
1185
        $filter = $query->expr->eq(
1186
            $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_version'),
1187
            $query->bindValue($contentId, null, \PDO::PARAM_INT)
1188
        );
1189
1190
        if ($status !== null) {
1191
            $filter = $query->expr->lAnd(
1192
                $filter,
1193
                $query->expr->eq(
1194
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_version'),
1195
                    $query->bindValue($status, null, \PDO::PARAM_INT)
1196
                )
1197
            );
1198
        }
1199
1200
        $query->where($filter);
1201
1202
        if ($limit > 0) {
1203
            $query->limit($limit);
1204
        }
1205
1206
        return $this->listVersionsHelper($query);
1207
    }
1208
1209
    /**
1210
     * Helper for {@see listVersions()} and {@see listVersionsForUser()} that filters duplicates
1211
     * that are the result of the cartesian product performed by createVersionInfoFindQuery().
1212
     *
1213
     * @param \eZ\Publish\Core\Persistence\Database\SelectQuery $query
1214
     *
1215
     * @return string[][]
1216
     */
1217
    private function listVersionsHelper(SelectQuery $query)
1218
    {
1219
        $query->orderBy(
1220
            $this->dbHandler->quoteColumn('id', 'ezcontentobject_version')
1221
        );
1222
1223
        $statement = $query->prepare();
1224
        $statement->execute();
1225
1226
        $results = [];
1227
        $previousId = null;
1228
        foreach ($statement->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1229
            if ($row['ezcontentobject_version_id'] == $previousId) {
1230
                continue;
1231
            }
1232
1233
            $previousId = $row['ezcontentobject_version_id'];
1234
            $results[] = $row;
1235
        }
1236
1237
        return $results;
1238
    }
1239
1240
    /**
1241
     * Returns all version numbers for the given $contentId.
1242
     *
1243
     * @param mixed $contentId
1244
     *
1245
     * @return int[]
1246
     */
1247
    public function listVersionNumbers($contentId)
1248
    {
1249
        $query = $this->dbHandler->createSelectQuery();
1250
        $query->selectDistinct(
1251
            $this->dbHandler->quoteColumn('version')
1252
        )->from(
1253
            $this->dbHandler->quoteTable('ezcontentobject_version')
1254
        )->where(
1255
            $query->expr->eq(
1256
                $this->dbHandler->quoteColumn('contentobject_id'),
1257
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1258
            )
1259
        );
1260
1261
        $statement = $query->prepare();
1262
        $statement->execute();
1263
1264
        return $statement->fetchAll(\PDO::FETCH_COLUMN);
1265
    }
1266
1267
    /**
1268
     * Returns last version number for content identified by $contentId.
1269
     *
1270
     * @param int $contentId
1271
     *
1272
     * @return int
1273
     */
1274 View Code Duplication
    public function getLastVersionNumber($contentId)
1275
    {
1276
        $query = $this->dbHandler->createSelectQuery();
1277
        $query->select(
1278
            $query->expr->max($this->dbHandler->quoteColumn('version'))
1279
        )->from(
1280
            $this->dbHandler->quoteTable('ezcontentobject_version')
1281
        )->where(
1282
            $query->expr->eq(
1283
                $this->dbHandler->quoteColumn('contentobject_id'),
1284
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1285
            )
1286
        );
1287
1288
        $statement = $query->prepare();
1289
        $statement->execute();
1290
1291
        return (int)$statement->fetchColumn();
1292
    }
1293
1294
    /**
1295
     * Returns all IDs for locations that refer to $contentId.
1296
     *
1297
     * @param int $contentId
1298
     *
1299
     * @return int[]
1300
     */
1301
    public function getAllLocationIds($contentId)
1302
    {
1303
        $query = $this->dbHandler->createSelectQuery();
1304
        $query->select(
1305
            $this->dbHandler->quoteColumn('node_id')
1306
        )->from(
1307
            $this->dbHandler->quoteTable('ezcontentobject_tree')
1308
        )->where(
1309
            $query->expr->eq(
1310
                $this->dbHandler->quoteColumn('contentobject_id'),
1311
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1312
            )
1313
        );
1314
1315
        $statement = $query->prepare();
1316
        $statement->execute();
1317
1318
        return $statement->fetchAll(\PDO::FETCH_COLUMN);
1319
    }
1320
1321
    /**
1322
     * Returns all field IDs of $contentId grouped by their type.
1323
     * If $versionNo is set only field IDs for that version are returned.
1324
     * If $languageCode is set, only field IDs for that language are returned.
1325
     *
1326
     * @param int $contentId
1327
     * @param int|null $versionNo
1328
     * @param string|null $languageCode
1329
     *
1330
     * @return int[][]
1331
     */
1332
    public function getFieldIdsByType($contentId, $versionNo = null, $languageCode = null)
1333
    {
1334
        $query = $this->dbHandler->createSelectQuery();
1335
        $query->select(
1336
            $this->dbHandler->quoteColumn('id'),
1337
            $this->dbHandler->quoteColumn('data_type_string')
1338
        )->from(
1339
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
1340
        )->where(
1341
            $query->expr->eq(
1342
                $this->dbHandler->quoteColumn('contentobject_id'),
1343
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1344
            )
1345
        );
1346
1347
        if (isset($versionNo)) {
1348
            $query->where(
1349
                $query->expr->eq(
1350
                    $this->dbHandler->quoteColumn('version'),
1351
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1352
                )
1353
            );
1354
        }
1355
1356
        if (isset($languageCode)) {
1357
            $query->where(
1358
                $query->expr->eq(
1359
                    $this->dbHandler->quoteColumn('language_code'),
1360
                    $query->bindValue($languageCode, null, \PDO::PARAM_STR)
1361
                )
1362
            );
1363
        }
1364
1365
        $statement = $query->prepare();
1366
        $statement->execute();
1367
1368
        $result = [];
1369
        foreach ($statement->fetchAll() as $row) {
1370
            if (!isset($result[$row['data_type_string']])) {
1371
                $result[$row['data_type_string']] = [];
1372
            }
1373
            $result[$row['data_type_string']][] = (int)$row['id'];
1374
        }
1375
1376
        return $result;
1377
    }
1378
1379
    /**
1380
     * Deletes relations to and from $contentId.
1381
     * If $versionNo is set only relations for that version are deleted.
1382
     *
1383
     * @param int $contentId
1384
     * @param int|null $versionNo
1385
     */
1386
    public function deleteRelations($contentId, $versionNo = null)
1387
    {
1388
        $query = $this->dbHandler->createDeleteQuery();
1389
        $query->deleteFrom(
1390
            $this->dbHandler->quoteTable('ezcontentobject_link')
1391
        );
1392
1393
        if (isset($versionNo)) {
1394
            $query->where(
1395
                $query->expr->lAnd(
1396
                    $query->expr->eq(
1397
                        $this->dbHandler->quoteColumn('from_contentobject_id'),
1398
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1399
                    ),
1400
                    $query->expr->eq(
1401
                        $this->dbHandler->quoteColumn('from_contentobject_version'),
1402
                        $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1403
                    )
1404
                )
1405
            );
1406
        } else {
1407
            $query->where(
1408
                $query->expr->lOr(
1409
                    $query->expr->eq(
1410
                        $this->dbHandler->quoteColumn('from_contentobject_id'),
1411
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1412
                    ),
1413
                    $query->expr->eq(
1414
                        $this->dbHandler->quoteColumn('to_contentobject_id'),
1415
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1416
                    )
1417
                )
1418
            );
1419
        }
1420
1421
        $query->prepare()->execute();
1422
    }
1423
1424
    /**
1425
     * Removes relations to Content with $contentId from Relation and RelationList field type fields.
1426
     *
1427
     * @param int $contentId
1428
     */
1429
    public function removeReverseFieldRelations($contentId)
1430
    {
1431
        $query = $this->dbHandler->createSelectQuery();
1432
        $query
1433
            ->select('ezcontentobject_attribute.*')
1434
            ->from('ezcontentobject_attribute')
1435
            ->innerJoin(
1436
                'ezcontentobject_link',
1437
                $query->expr->lAnd(
1438
                    $query->expr->eq(
1439
                        $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link'),
1440
                        $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_attribute')
1441
                    ),
1442
                    $query->expr->eq(
1443
                        $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link'),
1444
                        $this->dbHandler->quoteColumn('version', 'ezcontentobject_attribute')
1445
                    ),
1446
                    $query->expr->eq(
1447
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_link'),
1448
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_attribute')
1449
                    )
1450
                )
1451
            )
1452
            ->where(
1453
                $query->expr->eq(
1454
                    $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1455
                    $query->bindValue($contentId, null, PDO::PARAM_INT)
1456
                ),
1457
                $query->expr->gt(
1458
                    $query->expr->bitAnd(
1459
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1460
                        $query->bindValue(8, null, PDO::PARAM_INT)
1461
                    ),
1462
                    0
1463
                )
1464
            );
1465
1466
        $statement = $query->prepare();
1467
        $statement->execute();
1468
1469
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
1470
            if ($row['data_type_string'] === 'ezobjectrelation') {
1471
                $this->removeRelationFromRelationField($row);
1472
            }
1473
1474
            if ($row['data_type_string'] === 'ezobjectrelationlist') {
1475
                $this->removeRelationFromRelationListField($contentId, $row);
1476
            }
1477
        }
1478
    }
1479
1480
    /**
1481
     * Updates field value of RelationList field type identified by given $row data,
1482
     * removing relations toward given $contentId.
1483
     *
1484
     * @param int $contentId
1485
     * @param array $row
1486
     */
1487
    protected function removeRelationFromRelationListField($contentId, array $row)
1488
    {
1489
        $document = new DOMDocument('1.0', 'utf-8');
1490
        $document->loadXML($row['data_text']);
1491
1492
        $xpath = new DOMXPath($document);
1493
        $xpathExpression = "//related-objects/relation-list/relation-item[@contentobject-id='{$contentId}']";
1494
1495
        $relationItems = $xpath->query($xpathExpression);
1496
        foreach ($relationItems as $relationItem) {
1497
            $relationItem->parentNode->removeChild($relationItem);
1498
        }
1499
1500
        $query = $this->dbHandler->createUpdateQuery();
1501
        $query
1502
            ->update('ezcontentobject_attribute')
1503
            ->set(
1504
                'data_text',
1505
                $query->bindValue($document->saveXML(), null, PDO::PARAM_STR)
1506
            )
1507
            ->where(
1508
                $query->expr->lAnd(
1509
                    $query->expr->eq(
1510
                        $this->dbHandler->quoteColumn('id'),
1511
                        $query->bindValue($row['id'], null, PDO::PARAM_INT)
1512
                    ),
1513
                    $query->expr->eq(
1514
                        $this->dbHandler->quoteColumn('version'),
1515
                        $query->bindValue($row['version'], null, PDO::PARAM_INT)
1516
                    )
1517
                )
1518
            );
1519
1520
        $query->prepare()->execute();
1521
    }
1522
1523
    /**
1524
     * Updates field value of Relation field type identified by given $row data,
1525
     * removing relation data.
1526
     *
1527
     * @param array $row
1528
     */
1529
    protected function removeRelationFromRelationField(array $row)
1530
    {
1531
        $query = $this->dbHandler->createUpdateQuery();
1532
        $query
1533
            ->update('ezcontentobject_attribute')
1534
            ->set('data_int', $query->bindValue(null, null, PDO::PARAM_INT))
1535
            ->set('sort_key_int', $query->bindValue(0, null, PDO::PARAM_INT))
1536
            ->where(
1537
                $query->expr->lAnd(
1538
                    $query->expr->eq(
1539
                        $this->dbHandler->quoteColumn('id'),
1540
                        $query->bindValue($row['id'], null, PDO::PARAM_INT)
1541
                    ),
1542
                    $query->expr->eq(
1543
                        $this->dbHandler->quoteColumn('version'),
1544
                        $query->bindValue($row['version'], null, PDO::PARAM_INT)
1545
                    )
1546
                )
1547
            );
1548
1549
        $query->prepare()->execute();
1550
    }
1551
1552
    /**
1553
     * Deletes the field with the given $fieldId.
1554
     *
1555
     * @param int $fieldId
1556
     */
1557 View Code Duplication
    public function deleteField($fieldId)
1558
    {
1559
        $query = $this->dbHandler->createDeleteQuery();
1560
        $query->deleteFrom(
1561
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
1562
        )->where(
1563
            $query->expr->eq(
1564
                $this->dbHandler->quoteColumn('id'),
1565
                $query->bindValue($fieldId, null, \PDO::PARAM_INT)
1566
            )
1567
        );
1568
1569
        $query->prepare()->execute();
1570
    }
1571
1572
    /**
1573
     * Deletes all fields of $contentId in all versions.
1574
     * If $versionNo is set only fields for that version are deleted.
1575
     *
1576
     * @param int $contentId
1577
     * @param int|null $versionNo
1578
     */
1579
    public function deleteFields($contentId, $versionNo = null)
1580
    {
1581
        $query = $this->dbHandler->createDeleteQuery();
1582
        $query->deleteFrom('ezcontentobject_attribute')
1583
            ->where(
1584
                $query->expr->eq(
1585
                    $this->dbHandler->quoteColumn('contentobject_id'),
1586
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1587
                )
1588
            );
1589
1590
        if (isset($versionNo)) {
1591
            $query->where(
1592
                $query->expr->eq(
1593
                    $this->dbHandler->quoteColumn('version'),
1594
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1595
                )
1596
            );
1597
        }
1598
1599
        $query->prepare()->execute();
1600
    }
1601
1602
    /**
1603
     * Deletes all versions of $contentId.
1604
     * If $versionNo is set only that version is deleted.
1605
     *
1606
     * @param int $contentId
1607
     * @param int|null $versionNo
1608
     */
1609
    public function deleteVersions($contentId, $versionNo = null)
1610
    {
1611
        $query = $this->dbHandler->createDeleteQuery();
1612
        $query->deleteFrom('ezcontentobject_version')
1613
            ->where(
1614
                $query->expr->eq(
1615
                    $this->dbHandler->quoteColumn('contentobject_id'),
1616
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1617
                )
1618
            );
1619
1620
        if (isset($versionNo)) {
1621
            $query->where(
1622
                $query->expr->eq(
1623
                    $this->dbHandler->quoteColumn('version'),
1624
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1625
                )
1626
            );
1627
        }
1628
1629
        $query->prepare()->execute();
1630
    }
1631
1632
    /**
1633
     * Deletes all names of $contentId.
1634
     * If $versionNo is set only names for that version are deleted.
1635
     *
1636
     * @param int $contentId
1637
     * @param int|null $versionNo
1638
     */
1639
    public function deleteNames($contentId, $versionNo = null)
1640
    {
1641
        $query = $this->dbHandler->createDeleteQuery();
1642
        $query->deleteFrom('ezcontentobject_name')
1643
            ->where(
1644
                $query->expr->eq(
1645
                    $this->dbHandler->quoteColumn('contentobject_id'),
1646
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1647
                )
1648
            );
1649
1650
        if (isset($versionNo)) {
1651
            $query->where(
1652
                $query->expr->eq(
1653
                    $this->dbHandler->quoteColumn('content_version'),
1654
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1655
                )
1656
            );
1657
        }
1658
1659
        $query->prepare()->execute();
1660
    }
1661
1662
    /**
1663
     * Sets the name for Content $contentId in version $version to $name in $language.
1664
     *
1665
     * @param int $contentId
1666
     * @param int $version
1667
     * @param string $name
1668
     * @param string $language
1669
     */
1670
    public function setName($contentId, $version, $name, $language)
1671
    {
1672
        $language = $this->languageHandler->loadByLanguageCode($language);
1673
1674
        // Is it an insert or an update ?
1675
        $qSelect = $this->dbHandler->createSelectQuery();
1676
        $qSelect
1677
            ->select(
1678
                $qSelect->alias($qSelect->expr->count('*'), 'count')
1679
            )
1680
            ->from($this->dbHandler->quoteTable('ezcontentobject_name'))
1681
            ->where(
1682
                $qSelect->expr->lAnd(
1683
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('contentobject_id'), $qSelect->bindValue($contentId)),
1684
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('content_version'), $qSelect->bindValue($version)),
1685
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('content_translation'), $qSelect->bindValue($language->languageCode))
1686
                )
1687
            );
1688
        $stmt = $qSelect->prepare();
1689
        $stmt->execute();
1690
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1691
1692
        $insert = $res[0]['count'] == 0;
1693
        if ($insert) {
1694
            $q = $this->dbHandler->createInsertQuery();
1695
            $q->insertInto($this->dbHandler->quoteTable('ezcontentobject_name'));
1696
        } else {
1697
            $q = $this->dbHandler->createUpdateQuery();
1698
            $q->update($this->dbHandler->quoteTable('ezcontentobject_name'))
1699
                ->where(
1700
                    $q->expr->lAnd(
1701
                        $q->expr->eq($this->dbHandler->quoteColumn('contentobject_id'), $q->bindValue($contentId)),
1702
                        $q->expr->eq($this->dbHandler->quoteColumn('content_version'), $q->bindValue($version)),
1703
                        $q->expr->eq($this->dbHandler->quoteColumn('content_translation'), $q->bindValue($language->languageCode))
1704
                    )
1705
                );
1706
        }
1707
1708
        $q->set(
1709
            $this->dbHandler->quoteColumn('contentobject_id'),
1710
            $q->bindValue($contentId, null, \PDO::PARAM_INT)
1711
        )->set(
1712
            $this->dbHandler->quoteColumn('content_version'),
1713
            $q->bindValue($version, null, \PDO::PARAM_INT)
1714
        )->set(
1715
            $this->dbHandler->quoteColumn('language_id'),
1716
            '(' . $this->getLanguageQuery()->getQuery() . ')'
1717
        )->set(
1718
            $this->dbHandler->quoteColumn('content_translation'),
1719
            $q->bindValue($language->languageCode)
1720
        )->set(
1721
            $this->dbHandler->quoteColumn('real_translation'),
1722
            $q->bindValue($language->languageCode)
1723
        )->set(
1724
            $this->dbHandler->quoteColumn('name'),
1725
            $q->bindValue($name)
1726
        );
1727
        $q->bindValue($language->id, ':languageId', \PDO::PARAM_INT);
1728
        $q->bindValue($contentId, ':contentId', \PDO::PARAM_INT);
1729
        $q->prepare()->execute();
1730
    }
1731
1732
    /**
1733
     * Returns a language sub select query for setName.
1734
     *
1735
     * Return sub select query which gets proper language mask for alwaysAvailable Content.
1736
     *
1737
     * @return \eZ\Publish\Core\Persistence\Database\SelectQuery
1738
     */
1739
    private function getLanguageQuery()
1740
    {
1741
        $languageQuery = $this->dbHandler->createSelectQuery();
1742
        $languageQuery
1743
            ->select(
1744
                $languageQuery->expr->searchedCase(
1745
                    [
1746
                        $languageQuery->expr->lAnd(
1747
                            $languageQuery->expr->eq(
1748
                                $this->dbHandler->quoteColumn('initial_language_id'),
1749
                                ':languageId'
1750
                            ),
1751
                            // wrap bitwise check into another "neq" to provide cross-DBMS compatibility
1752
                            $languageQuery->expr->neq(
1753
                                $languageQuery->expr->bitAnd(
1754
                                    $this->dbHandler->quoteColumn('language_mask'),
1755
                                    ':languageId'
1756
                                ),
1757
                                0
1758
                            )
1759
                        ),
1760
                        $languageQuery->expr->bitOr(
1761
                            ':languageId',
1762
                            1
1763
                        ),
1764
                    ],
1765
                    ':languageId'
1766
                )
1767
            )
1768
            ->from('ezcontentobject')
1769
            ->where(
1770
                $languageQuery->expr->eq(
1771
                    'id',
1772
                    ':contentId'
1773
                )
1774
            );
1775
1776
        return $languageQuery;
1777
    }
1778
1779
    /**
1780
     * Deletes the actual content object referred to by $contentId.
1781
     *
1782
     * @param int $contentId
1783
     */
1784
    public function deleteContent($contentId)
1785
    {
1786
        $query = $this->dbHandler->createDeleteQuery();
1787
        $query->deleteFrom('ezcontentobject')
1788
            ->where(
1789
                $query->expr->eq(
1790
                    $this->dbHandler->quoteColumn('id'),
1791
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1792
                )
1793
            );
1794
1795
        $query->prepare()->execute();
1796
    }
1797
1798
    /**
1799
     * Loads relations from $contentId to published content, optionally only from $contentVersionNo.
1800
     *
1801
     * $relationType can also be filtered.
1802
     *
1803
     * @param int $contentId
1804
     * @param int $contentVersionNo
1805
     * @param int $relationType
1806
     *
1807
     * @return string[][] array of relation data
1808
     */
1809
    public function loadRelations($contentId, $contentVersionNo = null, $relationType = null)
1810
    {
1811
        $query = $this->queryBuilder->createRelationFindQuery();
1812
        $query->innerJoin(
1813
            $query->alias(
1814
                $this->dbHandler->quoteTable('ezcontentobject'),
1815
                'ezcontentobject_to'
1816
            ),
1817
            $query->expr->lAnd(
1818
                $query->expr->eq(
1819
                    $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1820
                    $this->dbHandler->quoteColumn('id', 'ezcontentobject_to')
1821
                ),
1822
                $query->expr->eq(
1823
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_to'),
1824
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1825
                )
1826
            )
1827
        )->where(
1828
            $query->expr->eq(
1829
                $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link'),
1830
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1831
            )
1832
        );
1833
1834
        // source version number
1835
        if (isset($contentVersionNo)) {
1836
            $query->where(
1837
                $query->expr->eq(
1838
                    $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link'),
1839
                    $query->bindValue($contentVersionNo, null, \PDO::PARAM_INT)
1840
                )
1841
            );
1842
        } else { // from published version only
1843
            $query->from(
1844
                $this->dbHandler->quoteTable('ezcontentobject')
1845
            )->where(
1846
                $query->expr->lAnd(
1847
                    $query->expr->eq(
1848
                        $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
1849
                        $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link')
1850
                    ),
1851
                    $query->expr->eq(
1852
                        $this->dbHandler->quoteColumn('current_version', 'ezcontentobject'),
1853
                        $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link')
1854
                    )
1855
                )
1856
            );
1857
        }
1858
1859
        // relation type
1860 View Code Duplication
        if (isset($relationType)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1861
            $query->where(
1862
                $query->expr->gt(
1863
                    $query->expr->bitAnd(
1864
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1865
                        $query->bindValue($relationType, null, \PDO::PARAM_INT)
1866
                    ),
1867
                    0
1868
                )
1869
            );
1870
        }
1871
1872
        $statement = $query->prepare();
1873
        $statement->execute();
1874
1875
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1876
    }
1877
1878
    /**
1879
     * Loads data that related to $toContentId.
1880
     *
1881
     * @param int $toContentId
1882
     * @param int $relationType
1883
     *
1884
     * @return mixed[][] Content data, array structured like {@see \eZ\Publish\Core\Persistence\Legacy\Content\Gateway::load()}
1885
     */
1886
    public function loadReverseRelations($toContentId, $relationType = null)
1887
    {
1888
        $query = $this->queryBuilder->createRelationFindQuery();
1889
        $query->where(
1890
            $query->expr->eq(
1891
                $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1892
                $query->bindValue($toContentId, null, \PDO::PARAM_INT)
1893
            )
1894
        );
1895
1896
        // ezcontentobject join
1897
        $query->from(
1898
            $this->dbHandler->quoteTable('ezcontentobject')
1899
        )->where(
1900
            $query->expr->lAnd(
1901
                $query->expr->eq(
1902
                    $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
1903
                    $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link')
1904
                ),
1905
                $query->expr->eq(
1906
                    $this->dbHandler->quoteColumn('current_version', 'ezcontentobject'),
1907
                    $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link')
1908
                ),
1909
                $query->expr->eq(
1910
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject'),
1911
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1912
                )
1913
            )
1914
        );
1915
1916
        // relation type
1917 View Code Duplication
        if (isset($relationType)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1918
            $query->where(
1919
                $query->expr->gt(
1920
                    $query->expr->bitAnd(
1921
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1922
                        $query->bindValue($relationType, null, \PDO::PARAM_INT)
1923
                    ),
1924
                    0
1925
                )
1926
            );
1927
        }
1928
1929
        $statement = $query->prepare();
1930
1931
        $statement->execute();
1932
1933
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1934
    }
1935
1936
    /**
1937
     * Inserts a new relation database record.
1938
     *
1939
     * @param \eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct $createStruct
1940
     *
1941
     * @return int ID the inserted ID
1942
     */
1943
    public function insertRelation(RelationCreateStruct $createStruct)
1944
    {
1945
        $q = $this->dbHandler->createInsertQuery();
1946
        $q->insertInto(
1947
            $this->dbHandler->quoteTable('ezcontentobject_link')
1948
        )->set(
1949
            $this->dbHandler->quoteColumn('id'),
1950
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_link', 'id')
1951
        )->set(
1952
            $this->dbHandler->quoteColumn('contentclassattribute_id'),
1953
            $q->bindValue((int)$createStruct->sourceFieldDefinitionId, null, \PDO::PARAM_INT)
1954
        )->set(
1955
            $this->dbHandler->quoteColumn('from_contentobject_id'),
1956
            $q->bindValue($createStruct->sourceContentId, null, \PDO::PARAM_INT)
1957
        )->set(
1958
            $this->dbHandler->quoteColumn('from_contentobject_version'),
1959
            $q->bindValue($createStruct->sourceContentVersionNo, null, \PDO::PARAM_INT)
1960
        )->set(
1961
            $this->dbHandler->quoteColumn('relation_type'),
1962
            $q->bindValue($createStruct->type, null, \PDO::PARAM_INT)
1963
        )->set(
1964
            $this->dbHandler->quoteColumn('to_contentobject_id'),
1965
            $q->bindValue($createStruct->destinationContentId, null, \PDO::PARAM_INT)
1966
        );
1967
1968
        $q->prepare()->execute();
1969
1970
        return $this->dbHandler->lastInsertId(
1971
            $this->dbHandler->getSequenceName('ezcontentobject_link', 'id')
1972
        );
1973
    }
1974
1975
    /**
1976
     * Deletes the relation with the given $relationId.
1977
     *
1978
     * @param int $relationId
1979
     * @param int $type {@see \eZ\Publish\API\Repository\Values\Content\Relation::COMMON,
1980
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::EMBED,
1981
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::LINK,
1982
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::FIELD}
1983
     */
1984
    public function deleteRelation($relationId, $type)
1985
    {
1986
        // Legacy Storage stores COMMON, LINK and EMBED types using bitmask, therefore first load
1987
        // existing relation type by given $relationId for comparison
1988
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
1989
        $query = $this->dbHandler->createSelectQuery();
1990
        $query->select(
1991
            $this->dbHandler->quoteColumn('relation_type')
1992
        )->from(
1993
            $this->dbHandler->quoteTable('ezcontentobject_link')
1994
        )->where(
1995
            $query->expr->eq(
1996
                $this->dbHandler->quoteColumn('id'),
1997
                $query->bindValue($relationId, null, \PDO::PARAM_INT)
1998
            )
1999
        );
2000
2001
        $statement = $query->prepare();
2002
        $statement->execute();
2003
        $loadedRelationType = $statement->fetchColumn();
2004
2005
        if (!$loadedRelationType) {
2006
            return;
2007
        }
2008
2009
        // If relation type matches then delete
2010
        if ($loadedRelationType == $type) {
2011
            /** @var $query \eZ\Publish\Core\Persistence\Database\DeleteQuery */
2012
            $query = $this->dbHandler->createDeleteQuery();
2013
            $query->deleteFrom(
2014
                'ezcontentobject_link'
2015
            )->where(
2016
                $query->expr->eq(
2017
                    $this->dbHandler->quoteColumn('id'),
2018
                    $query->bindValue($relationId, null, \PDO::PARAM_INT)
2019
                )
2020
            );
2021
2022
            $query->prepare()->execute();
2023
        } elseif ($loadedRelationType & $type) { // If relation type is composite update bitmask
2024
            /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
2025
            $query = $this->dbHandler->createUpdateQuery();
2026
            $query->update(
2027
                $this->dbHandler->quoteTable('ezcontentobject_link')
2028
            )->set(
2029
                $this->dbHandler->quoteColumn('relation_type'),
2030
                $query->expr->bitAnd(
2031
                    $this->dbHandler->quoteColumn('relation_type'),
2032
                    $query->bindValue(~$type, null, \PDO::PARAM_INT)
2033
                )
2034
            )->where(
2035
                $query->expr->eq(
2036
                    $this->dbHandler->quoteColumn('id'),
2037
                    $query->bindValue($relationId, null, \PDO::PARAM_INT)
2038
                )
2039
            );
2040
2041
            $query->prepare()->execute();
2042
        } else {
2043
            // No match, do nothing
2044
        }
2045
    }
2046
2047
    /**
2048
     * Returns all Content IDs for a given $contentTypeId.
2049
     *
2050
     * @param int $contentTypeId
2051
     *
2052
     * @return int[]
2053
     */
2054
    public function getContentIdsByContentTypeId($contentTypeId)
2055
    {
2056
        $query = $this->dbHandler->createSelectQuery();
2057
        $query
2058
            ->select($this->dbHandler->quoteColumn('id'))
2059
            ->from($this->dbHandler->quoteTable('ezcontentobject'))
2060
            ->where(
2061
                $query->expr->eq(
2062
                    $this->dbHandler->quoteColumn('contentclass_id'),
2063
                    $query->bindValue($contentTypeId, null, PDO::PARAM_INT)
2064
                )
2065
            );
2066
2067
        $statement = $query->prepare();
2068
        $statement->execute();
2069
2070
        return $statement->fetchAll(PDO::FETCH_COLUMN);
2071
    }
2072
2073
    /**
2074
     * Load name data for set of content id's and corresponding version number.
2075
     *
2076
     * @param array[] $rows array of hashes with 'id' and 'version' to load names for
2077
     *
2078
     * @return array
2079
     */
2080
    public function loadVersionedNameData($rows)
2081
    {
2082
        $query = $this->queryBuilder->createNamesQuery();
2083
        $conditions = [];
2084
        foreach ($rows as $row) {
2085
            $conditions[] = $query->expr->lAnd(
2086
                $query->expr->eq(
2087
                    $this->dbHandler->quoteColumn('contentobject_id'),
2088
                    $query->bindValue($row['id'], null, \PDO::PARAM_INT)
2089
                ),
2090
                $query->expr->eq(
2091
                    $this->dbHandler->quoteColumn('content_version'),
2092
                    $query->bindValue($row['version'], null, \PDO::PARAM_INT)
2093
                )
2094
            );
2095
        }
2096
2097
        $query->where($query->expr->lOr($conditions));
2098
        $stmt = $query->prepare();
2099
        $stmt->execute();
2100
2101
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
2102
    }
2103
2104
    /**
2105
     * Batch method for copying all relation meta data for copied Content object.
2106
     *
2107
     * {@inheritdoc}
2108
     *
2109
     * @param int $originalContentId
2110
     * @param int $copiedContentId
2111
     * @param int|null $versionNo If specified only copy for a given version number, otherwise all.
2112
     */
2113
    public function copyRelations($originalContentId, $copiedContentId, $versionNo = null)
2114
    {
2115
        // Given we can retain all columns, we just create copies with new `from_contentobject_id` using INSERT INTO SELECT
2116
        $sql = 'INSERT INTO ezcontentobject_link ( contentclassattribute_id, from_contentobject_id, from_contentobject_version, relation_type, to_contentobject_id )
2117
                SELECT  L2.contentclassattribute_id, :copied_id, L2.from_contentobject_version, L2.relation_type, L2.to_contentobject_id
2118
                FROM    ezcontentobject_link AS L2
2119
                WHERE   L2.from_contentobject_id = :original_id';
2120
2121
        if ($versionNo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $versionNo of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2122
            $stmt = $this->connection->prepare($sql . ' AND L2.from_contentobject_version = :version');
2123
            $stmt->bindValue('version', $versionNo, PDO::PARAM_INT);
2124
        } else {
2125
            $stmt = $this->connection->prepare($sql);
2126
        }
2127
2128
        $stmt->bindValue('original_id', $originalContentId, PDO::PARAM_INT);
2129
        $stmt->bindValue('copied_id', $copiedContentId, PDO::PARAM_INT);
2130
2131
        $stmt->execute();
2132
    }
2133
2134
    /**
2135
     * Remove the specified translation from the Content Object Version.
2136
     *
2137
     * @param int $contentId
2138
     * @param string $languageCode language code of the translation
2139
     * @throws \Doctrine\DBAL\DBALException
2140
     */
2141 View Code Duplication
    public function deleteTranslationFromContent($contentId, $languageCode)
2142
    {
2143
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
2144
2145
        $this->connection->beginTransaction();
2146
        try {
2147
            $this->deleteTranslationFromContentVersions($contentId, $language->id);
2148
            $this->deleteTranslationFromContentNames($contentId, $languageCode);
2149
            $this->deleteTranslationFromContentObject($contentId, $language->id);
2150
2151
            $this->connection->commit();
2152
        } catch (DBALException $e) {
2153
            $this->connection->rollBack();
2154
            throw $e;
2155
        }
2156
    }
2157
2158
    /**
2159
     * Delete Content fields (attributes) for the given Translation.
2160
     * If $versionNo is given, fields for that Version only will be deleted.
2161
     *
2162
     * @param string $languageCode
2163
     * @param int $contentId
2164
     * @param int $versionNo (optional) filter by versionNo
2165
     */
2166 View Code Duplication
    public function deleteTranslatedFields($languageCode, $contentId, $versionNo = null)
2167
    {
2168
        $query = $this->connection->createQueryBuilder();
2169
        $query
2170
            ->delete('ezcontentobject_attribute')
2171
            ->where('contentobject_id = :contentId')
2172
            ->andWhere('language_code = :languageCode')
2173
            ->setParameters(
2174
                [
2175
                    ':contentId' => $contentId,
2176
                    ':languageCode' => $languageCode,
2177
                ]
2178
            )
2179
        ;
2180
2181
        if (null !== $versionNo) {
2182
            $query
2183
                ->andWhere('version = :versionNo')
2184
                ->setParameter(':versionNo', $versionNo)
2185
            ;
2186
        }
2187
2188
        $query->execute();
2189
    }
2190
2191
    /**
2192
     * Delete the specified Translation from the given Version.
2193
     *
2194
     * @param int $contentId
2195
     * @param int $versionNo
2196
     * @param string $languageCode
2197
     * @throws \Doctrine\DBAL\DBALException
2198
     */
2199 View Code Duplication
    public function deleteTranslationFromVersion($contentId, $versionNo, $languageCode)
2200
    {
2201
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
2202
2203
        $this->connection->beginTransaction();
2204
        try {
2205
            $this->deleteTranslationFromContentVersions($contentId, $language->id, $versionNo);
2206
            $this->deleteTranslationFromContentNames($contentId, $languageCode, $versionNo);
2207
2208
            $this->connection->commit();
2209
        } catch (DBALException $e) {
2210
            $this->connection->rollBack();
2211
            throw $e;
2212
        }
2213
    }
2214
2215
    /**
2216
     * Delete translation from the ezcontentobject_name table.
2217
     *
2218
     * @param int $contentId
2219
     * @param string $languageCode
2220
     * @param int $versionNo optional, if specified, apply to this Version only.
2221
     */
2222 View Code Duplication
    private function deleteTranslationFromContentNames($contentId, $languageCode, $versionNo = null)
2223
    {
2224
        $query = $this->connection->createQueryBuilder();
2225
        $query
2226
            ->delete('ezcontentobject_name')
2227
            ->where('contentobject_id=:contentId')
2228
            ->andWhere('real_translation=:languageCode')
2229
            ->setParameters(
2230
                [
2231
                    ':languageCode' => $languageCode,
2232
                    ':contentId' => $contentId,
2233
                ]
2234
            )
2235
        ;
2236
2237
        if (null !== $versionNo) {
2238
            $query
2239
                ->andWhere('content_version = :versionNo')
2240
                ->setParameter(':versionNo', $versionNo)
2241
            ;
2242
        }
2243
2244
        $query->execute();
2245
    }
2246
2247
    /**
2248
     * Remove language from language_mask of ezcontentobject.
2249
     *
2250
     * @param int $contentId
2251
     * @param int $languageId
2252
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
2253
     */
2254
    private function deleteTranslationFromContentObject($contentId, $languageId)
2255
    {
2256
        $query = $this->connection->createQueryBuilder();
2257
        $query->update('ezcontentobject')
2258
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
2259
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
2260
            ->set('modified', ':now')
2261
            ->where('id = :contentId')
2262
            ->andWhere(
2263
            // make sure removed translation is not the last one (incl. alwaysAvailable)
2264
                $query->expr()->andX(
2265
                    'language_mask & ~ ' . $languageId . ' <> 0',
2266
                    'language_mask & ~ ' . $languageId . ' <> 1'
2267
                )
2268
            )
2269
            ->setParameter(':now', time())
2270
            ->setParameter(':contentId', $contentId)
2271
        ;
2272
2273
        $rowCount = $query->execute();
2274
2275
        // no rows updated means that most likely somehow it was the last remaining translation
2276
        if ($rowCount === 0) {
2277
            throw new BadStateException(
2278
                '$languageCode',
2279
                'Specified translation is the only one Content Object Version has'
2280
            );
2281
        }
2282
    }
2283
2284
    /**
2285
     * Remove language from language_mask of ezcontentobject_version and update initialLanguageId
2286
     * if it matches the removed one.
2287
     *
2288
     * @param int $contentId
2289
     * @param int $languageId
2290
     * @param int $versionNo optional, if specified, apply to this Version only.
2291
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
2292
     */
2293
    private function deleteTranslationFromContentVersions($contentId, $languageId, $versionNo = null)
2294
    {
2295
        $query = $this->connection->createQueryBuilder();
2296
        $query->update('ezcontentobject_version')
2297
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
2298
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
2299
            ->set('modified', ':now')
2300
            // update initial_language_id only if it matches removed translation languageId
2301
            ->set(
2302
                'initial_language_id',
2303
                'CASE WHEN initial_language_id = :languageId ' .
2304
                'THEN (SELECT initial_language_id AS main_language_id FROM ezcontentobject c WHERE c.id = :contentId) ' .
2305
                'ELSE initial_language_id END'
2306
            )
2307
            ->where('contentobject_id = :contentId')
2308
            ->andWhere(
2309
            // make sure removed translation is not the last one (incl. alwaysAvailable)
2310
                $query->expr()->andX(
2311
                    'language_mask & ~ ' . $languageId . ' <> 0',
2312
                    'language_mask & ~ ' . $languageId . ' <> 1'
2313
                )
2314
            )
2315
            ->setParameter(':now', time())
2316
            ->setParameter(':contentId', $contentId)
2317
            ->setParameter(':languageId', $languageId)
2318
        ;
2319
2320
        if (null !== $versionNo) {
2321
            $query
2322
                ->andWhere('version = :versionNo')
2323
                ->setParameter(':versionNo', $versionNo)
2324
            ;
2325
        }
2326
2327
        $rowCount = $query->execute();
2328
2329
        // no rows updated means that most likely somehow it was the last remaining translation
2330
        if ($rowCount === 0) {
2331
            throw new BadStateException(
2332
                '$languageCode',
2333
                'Specified translation is the only one Content Object Version has'
2334
            );
2335
        }
2336
    }
2337
}
2338