Completed
Push — EZEE-3159 ( 56bba0...79049c )
by
unknown
18:48
created

DoctrineDatabase::loadLocationDataByTrashContent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
8
9
use Doctrine\DBAL\Connection;
10
use Doctrine\DBAL\FetchMode;
11
use Doctrine\DBAL\Query\QueryBuilder;
12
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator;
13
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
14
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
15
use eZ\Publish\Core\Persistence\Database\SelectQuery;
16
use eZ\Publish\Core\Persistence\Database\Query as DatabaseQuery;
17
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
18
use eZ\Publish\SPI\Persistence\Content\Location;
19
use eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct;
20
use eZ\Publish\SPI\Persistence\Content\Location\CreateStruct;
21
use eZ\Publish\API\Repository\Values\Content\Query;
22
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;
23
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
24
use RuntimeException;
25
use PDO;
26
27
/**
28
 * Location gateway implementation using the Doctrine database.
29
 */
30
class DoctrineDatabase extends Gateway
31
{
32
    /**
33
     * 2^30, since PHP_INT_MAX can cause overflows in DB systems, if PHP is run
34
     * on 64 bit systems.
35
     */
36
    const MAX_LIMIT = 1073741824;
37
38
    /**
39
     * Database handler.
40
     *
41
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
42
     */
43
    protected $handler;
44
45
    /** @var \Doctrine\DBAL\Connection */
46
    protected $connection;
47
48
    /**
49
     * Language mask generator.
50
     *
51
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
52
     */
53
    protected $languageMaskGenerator;
54
55
    /**
56
     * Construct from database handler.
57
     *
58
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $handler
59
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
60
     */
61
    public function __construct(DatabaseHandler $handler, MaskGenerator $languageMaskGenerator)
62
    {
63
        $this->handler = $handler;
64
        $this->connection = $handler->getConnection();
65
        $this->languageMaskGenerator = $languageMaskGenerator;
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 View Code Duplication
    public function getBasicNodeData($nodeId, array $translations = null, bool $useAlwaysAvailable = true)
72
    {
73
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
74
        $q->andWhere(
75
            $q->expr()->eq('t.node_id', $q->createNamedParameter($nodeId, PDO::PARAM_INT))
76
        );
77
78
        if ($row = $q->execute()->fetch(FetchMode::ASSOCIATIVE)) {
79
            return $row;
80
        }
81
82
        throw new NotFound('location', $nodeId);
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function getNodeDataList(array $locationIds, array $translations = null, bool $useAlwaysAvailable = true): iterable
89
    {
90
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
91
        $q->andWhere(
92
            $q->expr()->in(
93
                't.node_id',
94
                $q->createNamedParameter($locationIds, Connection::PARAM_INT_ARRAY)
95
            )
96
        );
97
98
        return $q->execute()->fetchAll(FetchMode::ASSOCIATIVE);
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 View Code Duplication
    public function getBasicNodeDataByRemoteId($remoteId, array $translations = null, bool $useAlwaysAvailable = true)
105
    {
106
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
107
        $q->andWhere(
108
            $q->expr()->eq('t.remote_id', $q->createNamedParameter($remoteId, PDO::PARAM_STR))
109
        );
110
111
        if ($row = $q->execute()->fetch(FetchMode::ASSOCIATIVE)) {
112
            return $row;
113
        }
114
115
        throw new NotFound('location', $remoteId);
116
    }
117
118
    /**
119
     * Loads data for all Locations for $contentId, optionally only in the
120
     * subtree starting at $rootLocationId.
121
     *
122
     * @param int $contentId
123
     * @param int $rootLocationId
124
     *
125
     * @return array
126
     */
127
    public function loadLocationDataByContent($contentId, $rootLocationId = null)
128
    {
129
        $query = $this->handler->createSelectQuery();
130
        $query
131
            ->select('*')
132
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
133
            ->where(
134
                $query->expr->eq(
135
                    $this->handler->quoteColumn('contentobject_id'),
136
                    $query->bindValue($contentId)
137
                )
138
            );
139
140
        if ($rootLocationId !== null) {
141
            $this->applySubtreeLimitation($query, $rootLocationId);
142
        }
143
144
        $statement = $query->prepare();
145
        $statement->execute();
146
147
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
148
    }
149
150
    /**
151
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway::loadParentLocationsDataForDraftContent
152
     */
153
    public function loadParentLocationsDataForDraftContent($contentId, $drafts = null)
154
    {
155
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
156
        $query = $this->handler->createSelectQuery();
157
        $query->selectDistinct(
158
            'ezcontentobject_tree.*'
159
        )->from(
160
            $this->handler->quoteTable('ezcontentobject_tree')
161
        )->innerJoin(
162
            $this->handler->quoteTable('eznode_assignment'),
163
            $query->expr->lAnd(
164
                $query->expr->eq(
165
                    $this->handler->quoteColumn('node_id', 'ezcontentobject_tree'),
166
                    $this->handler->quoteColumn('parent_node', 'eznode_assignment')
167
                ),
168
                $query->expr->eq(
169
                    $this->handler->quoteColumn('contentobject_id', 'eznode_assignment'),
170
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
171
                ),
172
                $query->expr->eq(
173
                    $this->handler->quoteColumn('op_code', 'eznode_assignment'),
174
                    $query->bindValue(self::NODE_ASSIGNMENT_OP_CODE_CREATE, null, \PDO::PARAM_INT)
175
                )
176
            )
177
        )->innerJoin(
178
            $this->handler->quoteTable('ezcontentobject'),
179
            $query->expr->lAnd(
180
                $query->expr->lOr(
181
                    $query->expr->eq(
182
                        $this->handler->quoteColumn('contentobject_id', 'eznode_assignment'),
183
                        $this->handler->quoteColumn('id', 'ezcontentobject')
184
                    )
185
                ),
186
                $query->expr->eq(
187
                    $this->handler->quoteColumn('status', 'ezcontentobject'),
188
                    $query->bindValue(ContentInfo::STATUS_DRAFT, null, \PDO::PARAM_INT)
189
                )
190
            )
191
        );
192
193
        $statement = $query->prepare();
194
        $statement->execute();
195
196
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
197
    }
198
199
    /**
200
     * Find all content in the given subtree.
201
     *
202
     * @param mixed $sourceId
203
     * @param bool $onlyIds
204
     *
205
     * @return array
206
     */
207
    public function getSubtreeContent($sourceId, $onlyIds = false)
208
    {
209
        $query = $this->handler->createSelectQuery();
210
        $query->select($onlyIds ? 'node_id, contentobject_id, depth' : '*')->from(
211
            $this->handler->quoteTable('ezcontentobject_tree')
212
        );
213
        $this->applySubtreeLimitation($query, $sourceId);
214
        $query->orderBy(
215
            $this->handler->quoteColumn('depth', 'ezcontentobject_tree')
216
        )->orderBy(
217
            $this->handler->quoteColumn('node_id', 'ezcontentobject_tree')
218
        );
219
        $statement = $query->prepare();
220
        $statement->execute();
221
222
        $results = $statement->fetchAll($onlyIds ? (PDO::FETCH_COLUMN | PDO::FETCH_GROUP) : PDO::FETCH_ASSOC);
223
        // array_map() is used to to map all elements stored as $results[$i][0] to $results[$i]
224
        return $onlyIds ? array_map('reset', $results) : $results;
225
    }
226
227
    /**
228
     * Limits the given $query to the subtree starting at $rootLocationId.
229
     *
230
     * @param \eZ\Publish\Core\Persistence\Database\Query $query
231
     * @param string $rootLocationId
232
     */
233
    protected function applySubtreeLimitation(DatabaseQuery $query, $rootLocationId)
234
    {
235
        $query->where(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\Core\Persistence\Database\Query as the method where() does only exist in the following implementations of said interface: eZ\Publish\Core\Persiste...ine\DeleteDoctrineQuery, eZ\Publish\Core\Persiste...ine\SelectDoctrineQuery, eZ\Publish\Core\Persiste...\SubselectDoctrineQuery, eZ\Publish\Core\Persiste...ine\UpdateDoctrineQuery.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
236
            $query->expr->like(
237
                $this->handler->quoteColumn('path_string', 'ezcontentobject_tree'),
238
                $query->bindValue('%/' . $rootLocationId . '/%')
239
            )
240
        );
241
    }
242
243
    /**
244
     * Returns data for the first level children of the location identified by given $locationId.
245
     *
246
     * @param mixed $locationId
247
     *
248
     * @return array
249
     */
250
    public function getChildren($locationId)
251
    {
252
        $query = $this->handler->createSelectQuery();
253
        $query->select('*')->from(
254
            $this->handler->quoteTable('ezcontentobject_tree')
255
        )->where(
256
            $query->expr->eq(
257
                $this->handler->quoteColumn('parent_node_id', 'ezcontentobject_tree'),
258
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
259
            )
260
        );
261
        $statement = $query->prepare();
262
        $statement->execute();
263
264
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
265
    }
266
267
    /**
268
     * Update path strings to move nodes in the ezcontentobject_tree table.
269
     *
270
     * This query can likely be optimized to use some more advanced string
271
     * operations, which then depend on the respective database.
272
     *
273
     * @todo optimize
274
     *
275
     * @param array $sourceNodeData
276
     * @param array $destinationNodeData
277
     */
278
    public function moveSubtreeNodes(array $sourceNodeData, array $destinationNodeData)
279
    {
280
        $fromPathString = $sourceNodeData['path_string'];
281
282
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
283
        $query = $this->handler->createSelectQuery();
284
        $query
285
            ->select(
286
                $this->handler->quoteColumn('node_id'),
287
                $this->handler->quoteColumn('parent_node_id'),
288
                $this->handler->quoteColumn('path_string'),
289
                $this->handler->quoteColumn('path_identification_string'),
290
                $this->handler->quoteColumn('is_hidden')
291
            )
292
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
293
            ->where(
294
                $query->expr->like(
295
                    $this->handler->quoteColumn('path_string'),
296
                    $query->bindValue($fromPathString . '%')
297
                )
298
            );
299
        $statement = $query->prepare();
300
        $statement->execute();
301
302
        $rows = $statement->fetchAll();
303
        $oldParentPathString = implode('/', array_slice(explode('/', $fromPathString), 0, -2)) . '/';
304
        $oldParentPathIdentificationString = implode(
305
            '/',
306
            array_slice(explode('/', $sourceNodeData['path_identification_string']), 0, -1)
307
        );
308
309
        foreach ($rows as $row) {
310
            // Prefixing ensures correct replacement when old parent is root node
311
            $newPathString = str_replace(
312
                'prefix' . $oldParentPathString,
313
                $destinationNodeData['path_string'],
314
                'prefix' . $row['path_string']
315
            );
316
            $replace = rtrim($destinationNodeData['path_identification_string'], '/');
317
            if (empty($oldParentPathIdentificationString)) {
318
                $replace .= '/';
319
            }
320
            $newPathIdentificationString = str_replace(
321
                'prefix' . $oldParentPathIdentificationString,
322
                $replace,
323
                'prefix' . $row['path_identification_string']
324
            );
325
            $newParentId = $row['parent_node_id'];
326
            if ($row['path_string'] === $fromPathString) {
327
                $newParentId = (int)implode('', array_slice(explode('/', $newPathString), -3, 1));
328
            }
329
330
            /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
331
            $query = $this->handler->createUpdateQuery();
332
            $query
333
                ->update($this->handler->quoteTable('ezcontentobject_tree'))
334
                ->set(
335
                    $this->handler->quoteColumn('path_string'),
336
                    $query->bindValue($newPathString)
337
                )
338
                ->set(
339
                    $this->handler->quoteColumn('path_identification_string'),
340
                    $query->bindValue($newPathIdentificationString)
341
                )
342
                ->set(
343
                    $this->handler->quoteColumn('depth'),
344
                    $query->bindValue(substr_count($newPathString, '/') - 2)
345
                )
346
                ->set(
347
                    $this->handler->quoteColumn('parent_node_id'),
348
                    $query->bindValue($newParentId)
349
                );
350
351
            if ($destinationNodeData['is_hidden'] || $destinationNodeData['is_invisible']) {
352
                // CASE 1: Mark whole tree as invisible if destination is invisible and/or hidden
353
                $query->set(
354
                    $this->handler->quoteColumn('is_invisible'),
355
                    $query->bindValue(1)
356
                );
357
            } elseif (!$sourceNodeData['is_hidden'] && $sourceNodeData['is_invisible']) {
358
                // CASE 2: source is only invisible, we will need to re-calculate whole moved tree visibility
359
                $query->set(
360
                    $this->handler->quoteColumn('is_invisible'),
361
                    $query->bindValue($this->isHiddenByParent($newPathString, $rows) ? 1 : 0)
362
                );
363
            } else {
364
                // CASE 3: keep invisible flags as is (source is either hidden or not hidden/invisible at all)
365
            }
366
367
            $query->where(
368
                    $query->expr->eq(
369
                        $this->handler->quoteColumn('node_id'),
370
                        $query->bindValue($row['node_id'])
371
                    )
372
                );
373
            $query->prepare()->execute();
374
        }
375
    }
376
377
    private function isHiddenByParent($pathString, array $rows)
378
    {
379
        $parentNodeIds = explode('/', trim($pathString, '/'));
380
        array_pop($parentNodeIds); // remove self
381
        foreach ($rows as $row) {
382
            if ($row['is_hidden'] && in_array($row['node_id'], $parentNodeIds)) {
383
                return true;
384
            }
385
        }
386
387
        return false;
388
    }
389
390
    /**
391
     * Updated subtree modification time for all nodes on path.
392
     *
393
     * @param string $pathString
394
     * @param int|null $timestamp
395
     */
396
    public function updateSubtreeModificationTime($pathString, $timestamp = null)
397
    {
398
        $nodes = array_filter(explode('/', $pathString));
399
        $query = $this->handler->createUpdateQuery();
400
        $query
401
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
402
            ->set(
403
                $this->handler->quoteColumn('modified_subnode'),
404
                $query->bindValue(
405
                    $timestamp ?: time()
406
                )
407
            )
408
            ->where(
409
                $query->expr->in(
410
                    $this->handler->quoteColumn('node_id'),
411
                    $nodes
412
                )
413
            );
414
        $query->prepare()->execute();
415
    }
416
417
    /**
418
     * Sets a location to be hidden, and it self + all children to invisible.
419
     *
420
     * @param string $pathString
421
     */
422
    public function hideSubtree($pathString)
423
    {
424
        $this->setNodeWithChildrenInvisible($pathString);
425
        $this->setNodeHidden($pathString);
426
    }
427
428
    /**
429
     * @param string $pathString
430
     **/
431
    public function setNodeWithChildrenInvisible(string $pathString): void
432
    {
433
        $query = $this->handler->createUpdateQuery();
434
        $query
435
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
436
            ->set(
437
                $this->handler->quoteColumn('is_invisible'),
438
                $query->bindValue(1)
439
            )
440
            ->set(
441
                $this->handler->quoteColumn('modified_subnode'),
442
                $query->bindValue(time())
443
            )
444
            ->where(
445
                $query->expr->like(
446
                    $this->handler->quoteColumn('path_string'),
447
                    $query->bindValue($pathString . '%')
448
                )
449
            );
450
451
        $query->prepare()->execute();
452
    }
453
454
    /**
455
     * @param string $pathString
456
     **/
457
    public function setNodeHidden(string $pathString): void
458
    {
459
        $this->setNodeHiddenStatus($pathString, true);
460
    }
461
462
    /**
463
     * @param string $pathString
464
     * @param bool $isHidden
465
     */
466
    private function setNodeHiddenStatus(string $pathString, bool $isHidden): void
467
    {
468
        $query = $this->handler->createUpdateQuery();
469
        $query
470
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
471
            ->set(
472
                $this->handler->quoteColumn('is_hidden'),
473
                $query->bindValue((int) $isHidden)
474
            )
475
            ->where(
476
                $query->expr->eq(
477
                    $this->handler->quoteColumn('path_string'),
478
                    $query->bindValue($pathString)
479
                )
480
            );
481
482
        $query->prepare()->execute();
483
    }
484
485
    /**
486
     * Sets a location to be unhidden, and self + children to visible unless a parent is hiding the tree.
487
     * If not make sure only children down to first hidden node is marked visible.
488
     *
489
     * @param string $pathString
490
     */
491
    public function unHideSubtree($pathString)
492
    {
493
        $this->setNodeUnhidden($pathString);
494
        $this->setNodeWithChildrenVisible($pathString);
495
    }
496
497
    /**
498
     * Sets a location + children to visible unless a parent is hiding the tree.
499
     *
500
     * @param string $pathString
501
     **/
502
    public function setNodeWithChildrenVisible(string $pathString): void
503
    {
504
        // Check if any parent nodes are explicitly hidden
505
        $query = $this->handler->createSelectQuery();
506
        $query
507
            ->select($this->handler->quoteColumn('path_string'))
508
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
509
            ->leftJoin('ezcontentobject', 'ezcontentobject_tree.contentobject_id', 'ezcontentobject.id')
510
            ->where(
511
                $query->expr->lAnd(
512
                    $query->expr->lOr(
513
                        $query->expr->eq(
514
                            $this->handler->quoteColumn('is_hidden', 'ezcontentobject_tree'),
515
                            $query->bindValue(1)
516
                        ),
517
                        $query->expr->eq(
518
                            $this->handler->quoteColumn('is_hidden', 'ezcontentobject'),
519
                            $query->bindValue(1)
520
                        )
521
                    ),
522
                    $query->expr->in(
523
                        $this->handler->quoteColumn('node_id'),
524
                        array_filter(explode('/', $pathString))
525
                    )
526
                )
527
            );
528
529
        $statement = $query->prepare();
530
        $statement->execute();
531
        if (count($statement->fetchAll(\PDO::FETCH_COLUMN))) {
532
            // There are parent nodes set hidden, so that we can skip marking
533
            // something visible again.
534
            return;
535
        }
536
537
        // Find nodes of explicitly hidden subtrees in the subtree which
538
        // should be unhidden
539
        $query = $this->handler->createSelectQuery();
540
        $query
541
            ->select($this->handler->quoteColumn('path_string'))
542
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
543
            ->leftJoin('ezcontentobject', 'ezcontentobject_tree.contentobject_id', 'ezcontentobject.id')
544
            ->where(
545
                $query->expr->lAnd(
546
                    $query->expr->lOr(
547
                        $query->expr->eq(
548
                            $this->handler->quoteColumn('is_hidden', 'ezcontentobject_tree'),
549
                            $query->bindValue(1)
550
                        ),
551
                        $query->expr->eq(
552
                            $this->handler->quoteColumn('is_hidden', 'ezcontentobject'),
553
                            $query->bindValue(1)
554
                        )
555
                    ),
556
                    $query->expr->like(
557
                        $this->handler->quoteColumn('path_string'),
558
                        $query->bindValue($pathString . '%')
559
                    )
560
                )
561
            );
562
        $statement = $query->prepare();
563
        $statement->execute();
564
        $hiddenSubtrees = $statement->fetchAll(\PDO::FETCH_COLUMN);
565
566
        $query = $this->handler->createUpdateQuery();
567
        $query
568
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
569
            ->set(
570
                $this->handler->quoteColumn('is_invisible'),
571
                $query->bindValue(0)
572
            )
573
            ->set(
574
                $this->handler->quoteColumn('modified_subnode'),
575
                $query->bindValue(time())
576
            );
577
578
        // Build where expression selecting the nodes, which should be made
579
        // visible again
580
        $where = $query->expr->like(
581
            $this->handler->quoteColumn('path_string'),
582
            $query->bindValue($pathString . '%')
583
        );
584
        if (count($hiddenSubtrees)) {
585
            $handler = $this->handler;
586
            $where = $query->expr->lAnd(
587
                $where,
588
                $query->expr->lAnd(
589
                    array_map(
590
                        function ($pathString) use ($query, $handler) {
591
                            return $query->expr->not(
592
                                $query->expr->like(
593
                                    $handler->quoteColumn('path_string'),
594
                                    $query->bindValue($pathString . '%')
595
                                )
596
                            );
597
                        },
598
                        $hiddenSubtrees
599
                    )
600
                )
601
            );
602
        }
603
        $query->where($where);
604
        $query->prepare()->execute();
605
    }
606
607
    /**
608
     * Sets location to be unhidden.
609
     *
610
     * @param string $pathString
611
     **/
612
    public function setNodeUnhidden(string $pathString): void
613
    {
614
        $this->setNodeHiddenStatus($pathString, false);
615
    }
616
617
    /**
618
     * Swaps the content object being pointed to by a location object.
619
     *
620
     * Make the location identified by $locationId1 refer to the Content
621
     * referred to by $locationId2 and vice versa.
622
     *
623
     * @param int $locationId1
624
     * @param int $locationId2
625
     *
626
     * @return bool
627
     */
628
    public function swap(int $locationId1, int $locationId2): bool
629
    {
630
        $queryBuilder = $this->connection->createQueryBuilder();
631
        $expr = $queryBuilder->expr();
632
        $queryBuilder
633
            ->select('node_id', 'main_node_id', 'contentobject_id', 'contentobject_version')
634
            ->from('ezcontentobject_tree')
635
            ->where(
636
                $expr->in(
637
                    'node_id',
638
                    ':locationIds'
639
                )
640
            )
641
            ->setParameter('locationIds', [$locationId1, $locationId2], Connection::PARAM_INT_ARRAY)
642
        ;
643
        $statement = $queryBuilder->execute();
644
        $contentObjects = [];
645
        foreach ($statement->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
646
            $row['is_main_node'] = (int)$row['main_node_id'] === (int)$row['node_id'];
647
            $contentObjects[$row['node_id']] = $row;
648
        }
649
650
        if (!isset($contentObjects[$locationId1], $contentObjects[$locationId2])) {
651
            throw new RuntimeException(
652
                sprintf(
653
                    '%s: failed to fetch either Location %d or Location %d',
654
                    __METHOD__,
655
                    $locationId1,
656
                    $locationId2
657
                )
658
            );
659
        }
660
        $content1data = $contentObjects[$locationId1];
661
        $content2data = $contentObjects[$locationId2];
662
663
        $queryBuilder = $this->connection->createQueryBuilder();
664
        $queryBuilder
665
            ->update('ezcontentobject_tree')
666
            ->set('contentobject_id', ':contentId')
667
            ->set('contentobject_version', ':versionNo')
668
            ->set('main_node_id', ':mainNodeId')
669
            ->where(
670
                $expr->eq('node_id', ':locationId')
671
            );
672
673
        $queryBuilder
674
            ->setParameter(':contentId', $content2data['contentobject_id'])
675
            ->setParameter(':versionNo', $content2data['contentobject_version'])
676
            ->setParameter(
677
                ':mainNodeId',
678
                // make main Location main again, preserve main Location id of non-main one
679
                $content2data['is_main_node']
680
                    ? $content1data['node_id']
681
                    : $content2data['main_node_id']
682
            )
683
            ->setParameter('locationId', $locationId1);
684
685
        // update Location 1 entry
686
        $queryBuilder->execute();
687
688
        $queryBuilder
689
            ->setParameter(':contentId', $content1data['contentobject_id'])
690
            ->setParameter(':versionNo', $content1data['contentobject_version'])
691
            ->setParameter(
692
                ':mainNodeId',
693
                $content1data['is_main_node']
694
                    // make main Location main again, preserve main Location id of non-main one
695
                    ? $content2data['node_id']
696
                    : $content1data['main_node_id']
697
            )
698
            ->setParameter('locationId', $locationId2);
699
700
        // update Location 2 entry
701
        $queryBuilder->execute();
702
703
        return true;
704
    }
705
706
    /**
707
     * Creates a new location in given $parentNode.
708
     *
709
     * @param \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct $createStruct
710
     * @param array $parentNode
711
     *
712
     * @return \eZ\Publish\SPI\Persistence\Content\Location
713
     */
714
    public function create(CreateStruct $createStruct, array $parentNode)
715
    {
716
        $location = new Location();
717
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
718
        $query = $this->handler->createInsertQuery();
719
        $query
720
            ->insertInto($this->handler->quoteTable('ezcontentobject_tree'))
721
            ->set(
722
                $this->handler->quoteColumn('contentobject_id'),
723
                $query->bindValue($location->contentId = $createStruct->contentId, null, \PDO::PARAM_INT)
724
            )->set(
725
                $this->handler->quoteColumn('contentobject_is_published'),
726
                $query->bindValue(1, null, \PDO::PARAM_INT)
727
            )->set(
728
                $this->handler->quoteColumn('contentobject_version'),
729
                $query->bindValue($createStruct->contentVersion, null, \PDO::PARAM_INT)
730
            )->set(
731
                $this->handler->quoteColumn('depth'),
732
                $query->bindValue($location->depth = $parentNode['depth'] + 1, null, \PDO::PARAM_INT)
733
            )->set(
734
                $this->handler->quoteColumn('is_hidden'),
735
                $query->bindValue($location->hidden = $createStruct->hidden, null, \PDO::PARAM_INT)
736
            )->set(
737
                $this->handler->quoteColumn('is_invisible'),
738
                $query->bindValue($location->invisible = $createStruct->invisible, null, \PDO::PARAM_INT)
739
            )->set(
740
                $this->handler->quoteColumn('modified_subnode'),
741
                $query->bindValue(time(), null, \PDO::PARAM_INT)
742
            )->set(
743
                $this->handler->quoteColumn('node_id'),
744
                $this->handler->getAutoIncrementValue('ezcontentobject_tree', 'node_id')
745
            )->set(
746
                $this->handler->quoteColumn('parent_node_id'),
747
                $query->bindValue($location->parentId = $parentNode['node_id'], null, \PDO::PARAM_INT)
748
            )->set(
749
                $this->handler->quoteColumn('path_identification_string'),
750
                $query->bindValue($location->pathIdentificationString = $createStruct->pathIdentificationString, null, \PDO::PARAM_STR)
751
            )->set(
752
                $this->handler->quoteColumn('path_string'),
753
                $query->bindValue('dummy') // Set later
754
            )->set(
755
                $this->handler->quoteColumn('priority'),
756
                $query->bindValue($location->priority = $createStruct->priority, null, \PDO::PARAM_INT)
757
            )->set(
758
                $this->handler->quoteColumn('remote_id'),
759
                $query->bindValue($location->remoteId = $createStruct->remoteId, null, \PDO::PARAM_STR)
760
            )->set(
761
                $this->handler->quoteColumn('sort_field'),
762
                $query->bindValue($location->sortField = $createStruct->sortField, null, \PDO::PARAM_INT)
763
            )->set(
764
                $this->handler->quoteColumn('sort_order'),
765
                $query->bindValue($location->sortOrder = $createStruct->sortOrder, null, \PDO::PARAM_INT)
766
            );
767
        $query->prepare()->execute();
768
769
        $location->id = (int)$this->handler->lastInsertId($this->handler->getSequenceName('ezcontentobject_tree', 'node_id'));
770
771
        $mainLocationId = $createStruct->mainLocationId === true ? $location->id : $createStruct->mainLocationId;
772
        $location->pathString = $parentNode['path_string'] . $location->id . '/';
773
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
774
        $query = $this->handler->createUpdateQuery();
775
        $query
776
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
777
            ->set(
778
                $this->handler->quoteColumn('path_string'),
779
                $query->bindValue($location->pathString)
780
            )
781
            ->set(
782
                $this->handler->quoteColumn('main_node_id'),
783
                $query->bindValue($mainLocationId, null, \PDO::PARAM_INT)
784
            )
785
            ->where(
786
                $query->expr->eq(
787
                    $this->handler->quoteColumn('node_id'),
788
                    $query->bindValue($location->id, null, \PDO::PARAM_INT)
789
                )
790
            );
791
        $query->prepare()->execute();
792
793
        return $location;
794
    }
795
796
    /**
797
     * Create an entry in the node assignment table.
798
     *
799
     * @param \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct $createStruct
800
     * @param mixed $parentNodeId
801
     * @param int $type
802
     */
803
    public function createNodeAssignment(CreateStruct $createStruct, $parentNodeId, $type = self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP)
804
    {
805
        $isMain = ($createStruct->mainLocationId === true ? 1 : 0);
806
807
        $query = $this->handler->createInsertQuery();
808
        $query
809
            ->insertInto($this->handler->quoteTable('eznode_assignment'))
810
            ->set(
811
                $this->handler->quoteColumn('contentobject_id'),
812
                $query->bindValue($createStruct->contentId, null, \PDO::PARAM_INT)
813
            )->set(
814
                $this->handler->quoteColumn('contentobject_version'),
815
                $query->bindValue($createStruct->contentVersion, null, \PDO::PARAM_INT)
816
            )->set(
817
                $this->handler->quoteColumn('from_node_id'),
818
                $query->bindValue(0, null, \PDO::PARAM_INT) // unused field
819
            )->set(
820
                $this->handler->quoteColumn('id'),
821
                $this->handler->getAutoIncrementValue('eznode_assignment', 'id')
822
            )->set(
823
                $this->handler->quoteColumn('is_main'),
824
                $query->bindValue($isMain, null, \PDO::PARAM_INT) // Changed by the business layer, later
825
            )->set(
826
                $this->handler->quoteColumn('op_code'),
827
                $query->bindValue($type, null, \PDO::PARAM_INT)
828
            )->set(
829
                $this->handler->quoteColumn('parent_node'),
830
                $query->bindValue($parentNodeId, null, \PDO::PARAM_INT)
831
            )->set(
832
                // parent_remote_id column should contain the remote id of the corresponding Location
833
                $this->handler->quoteColumn('parent_remote_id'),
834
                $query->bindValue($createStruct->remoteId, null, \PDO::PARAM_STR)
835
            )->set(
836
                // remote_id column should contain the remote id of the node assignment itself,
837
                // however this was never implemented completely in Legacy Stack, so we just set
838
                // it to default value '0'
839
                $this->handler->quoteColumn('remote_id'),
840
                $query->bindValue('0', null, \PDO::PARAM_STR)
841
            )->set(
842
                $this->handler->quoteColumn('sort_field'),
843
                $query->bindValue($createStruct->sortField, null, \PDO::PARAM_INT)
844
            )->set(
845
                $this->handler->quoteColumn('sort_order'),
846
                $query->bindValue($createStruct->sortOrder, null, \PDO::PARAM_INT)
847
            )->set(
848
                $this->handler->quoteColumn('priority'),
849
                $query->bindValue($createStruct->priority, null, \PDO::PARAM_INT)
850
            )->set(
851
                $this->handler->quoteColumn('is_hidden'),
852
                $query->bindValue($createStruct->hidden, null, \PDO::PARAM_INT)
853
            );
854
        $query->prepare()->execute();
855
    }
856
857
    /**
858
     * Deletes node assignment for given $contentId and $versionNo.
859
     *
860
     * If $versionNo is not passed all node assignments for given $contentId are deleted
861
     *
862
     * @param int $contentId
863
     * @param int|null $versionNo
864
     */
865
    public function deleteNodeAssignment($contentId, $versionNo = null)
866
    {
867
        $query = $this->handler->createDeleteQuery();
868
        $query->deleteFrom(
869
            'eznode_assignment'
870
        )->where(
871
            $query->expr->eq(
872
                $this->handler->quoteColumn('contentobject_id'),
873
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
874
            )
875
        );
876
        if (isset($versionNo)) {
877
            $query->where(
878
                $query->expr->eq(
879
                    $this->handler->quoteColumn('contentobject_version'),
880
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
881
                )
882
            );
883
        }
884
        $query->prepare()->execute();
885
    }
886
887
    /**
888
     * Update node assignment table.
889
     *
890
     * @param int $contentObjectId
891
     * @param int $oldParent
892
     * @param int $newParent
893
     * @param int $opcode
894
     */
895
    public function updateNodeAssignment($contentObjectId, $oldParent, $newParent, $opcode)
896
    {
897
        $query = $this->handler->createUpdateQuery();
898
        $query
899
            ->update($this->handler->quoteTable('eznode_assignment'))
900
            ->set(
901
                $this->handler->quoteColumn('parent_node'),
902
                $query->bindValue($newParent, null, \PDO::PARAM_INT)
903
            )
904
            ->set(
905
                $this->handler->quoteColumn('op_code'),
906
                $query->bindValue($opcode, null, \PDO::PARAM_INT)
907
            )
908
            ->where(
909
                $query->expr->lAnd(
910
                    $query->expr->eq(
911
                        $this->handler->quoteColumn('contentobject_id'),
912
                        $query->bindValue($contentObjectId, null, \PDO::PARAM_INT)
913
                    ),
914
                    $query->expr->eq(
915
                        $this->handler->quoteColumn('parent_node'),
916
                        $query->bindValue($oldParent, null, \PDO::PARAM_INT)
917
                    )
918
                )
919
            );
920
        $query->prepare()->execute();
921
    }
922
923
    /**
924
     * Create locations from node assignments.
925
     *
926
     * Convert existing node assignments into real locations.
927
     *
928
     * @param mixed $contentId
929
     * @param mixed $versionNo
930
     */
931
    public function createLocationsFromNodeAssignments($contentId, $versionNo)
932
    {
933
        // select all node assignments with OP_CODE_CREATE (3) for this content
934
        $query = $this->handler->createSelectQuery();
935
        $query
936
            ->select('*')
937
            ->from($this->handler->quoteTable('eznode_assignment'))
938
            ->where(
939
                $query->expr->lAnd(
940
                    $query->expr->eq(
941
                        $this->handler->quoteColumn('contentobject_id'),
942
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
943
                    ),
944
                    $query->expr->eq(
945
                        $this->handler->quoteColumn('contentobject_version'),
946
                        $query->bindValue($versionNo, null, \PDO::PARAM_INT)
947
                    ),
948
                    $query->expr->eq(
949
                        $this->handler->quoteColumn('op_code'),
950
                        $query->bindValue(self::NODE_ASSIGNMENT_OP_CODE_CREATE, null, \PDO::PARAM_INT)
951
                    )
952
                )
953
            )->orderBy('id');
954
        $statement = $query->prepare();
955
        $statement->execute();
956
957
        // convert all these assignments to nodes
958
959
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
960
            if ((bool)$row['is_main'] === true) {
961
                $mainLocationId = true;
962
            } else {
963
                $mainLocationId = $this->getMainNodeId($contentId);
964
            }
965
966
            $parentLocationData = $this->getBasicNodeData($row['parent_node']);
967
            $isInvisible = $row['is_hidden'] || $parentLocationData['is_hidden'] || $parentLocationData['is_invisible'];
968
            $this->create(
969
                new CreateStruct(
970
                    [
971
                        'contentId' => $row['contentobject_id'],
972
                        'contentVersion' => $row['contentobject_version'],
973
                        'mainLocationId' => $mainLocationId,
974
                        'remoteId' => $row['parent_remote_id'],
975
                        'sortField' => $row['sort_field'],
976
                        'sortOrder' => $row['sort_order'],
977
                        'priority' => $row['priority'],
978
                        'hidden' => $row['is_hidden'],
979
                        'invisible' => $isInvisible,
980
                    ]
981
                ),
982
                $parentLocationData
983
            );
984
985
            $this->updateNodeAssignment(
986
                $row['contentobject_id'],
987
                $row['parent_node'],
988
                $row['parent_node'],
989
                self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP
990
            );
991
        }
992
    }
993
994
    /**
995
     * Updates all Locations of content identified with $contentId with $versionNo.
996
     *
997
     * @param mixed $contentId
998
     * @param mixed $versionNo
999
     */
1000 View Code Duplication
    public function updateLocationsContentVersionNo($contentId, $versionNo)
1001
    {
1002
        $query = $this->handler->createUpdateQuery();
1003
        $query->update(
1004
            $this->handler->quoteTable('ezcontentobject_tree')
1005
        )->set(
1006
            $this->handler->quoteColumn('contentobject_version'),
1007
            $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1008
        )->where(
1009
            $query->expr->eq(
1010
                $this->handler->quoteColumn('contentobject_id'),
1011
                $contentId
1012
            )
1013
        );
1014
        $query->prepare()->execute();
1015
    }
1016
1017
    /**
1018
     * Searches for the main nodeId of $contentId in $versionId.
1019
     *
1020
     * @param int $contentId
1021
     *
1022
     * @return int|bool
1023
     */
1024
    private function getMainNodeId($contentId)
1025
    {
1026
        $query = $this->handler->createSelectQuery();
1027
        $query
1028
            ->select('node_id')
1029
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
1030
            ->where(
1031
                $query->expr->lAnd(
1032
                    $query->expr->eq(
1033
                        $this->handler->quoteColumn('contentobject_id'),
1034
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1035
                    ),
1036
                    $query->expr->eq(
1037
                        $this->handler->quoteColumn('node_id'),
1038
                        $this->handler->quoteColumn('main_node_id')
1039
                    )
1040
                )
1041
            );
1042
        $statement = $query->prepare();
1043
        $statement->execute();
1044
1045
        $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
1046
        if (count($result) === 1) {
1047
            return (int)$result[0]['node_id'];
1048
        } else {
1049
            return false;
1050
        }
1051
    }
1052
1053
    /**
1054
     * Updates an existing location.
1055
     *
1056
     * Will not throw anything if location id is invalid or no entries are affected.
1057
     *
1058
     * @param \eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct $location
1059
     * @param int $locationId
1060
     */
1061
    public function update(UpdateStruct $location, $locationId)
1062
    {
1063
        $query = $this->handler->createUpdateQuery();
1064
1065
        $query
1066
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
1067
            ->set(
1068
                $this->handler->quoteColumn('priority'),
1069
                $query->bindValue($location->priority)
1070
            )
1071
            ->set(
1072
                $this->handler->quoteColumn('remote_id'),
1073
                $query->bindValue($location->remoteId)
1074
            )
1075
            ->set(
1076
                $this->handler->quoteColumn('sort_order'),
1077
                $query->bindValue($location->sortOrder)
1078
            )
1079
            ->set(
1080
                $this->handler->quoteColumn('sort_field'),
1081
                $query->bindValue($location->sortField)
1082
            )
1083
            ->where(
1084
                $query->expr->eq(
1085
                    $this->handler->quoteColumn('node_id'),
1086
                    $locationId
1087
                )
1088
            );
1089
        $statement = $query->prepare();
1090
        $statement->execute();
1091
1092
        // Commented due to EZP-23302: Update Location fails if no change is performed with the update
1093
        // Should be fixed with PDO::MYSQL_ATTR_FOUND_ROWS instead
1094
        /*if ( $statement->rowCount() < 1 )
1095
        {
1096
            throw new NotFound( 'location', $locationId );
1097
        }*/
1098
    }
1099
1100
    /**
1101
     * Updates path identification string for given $locationId.
1102
     *
1103
     * @param mixed $locationId
1104
     * @param mixed $parentLocationId
1105
     * @param string $text
1106
     */
1107
    public function updatePathIdentificationString($locationId, $parentLocationId, $text)
1108
    {
1109
        $parentData = $this->getBasicNodeData($parentLocationId);
1110
1111
        $newPathIdentificationString = empty($parentData['path_identification_string']) ?
1112
            $text :
1113
            $parentData['path_identification_string'] . '/' . $text;
1114
1115
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1116
        $query = $this->handler->createUpdateQuery();
1117
        $query->update(
1118
            'ezcontentobject_tree'
1119
        )->set(
1120
            $this->handler->quoteColumn('path_identification_string'),
1121
            $query->bindValue($newPathIdentificationString, null, \PDO::PARAM_STR)
1122
        )->where(
1123
            $query->expr->eq(
1124
                $this->handler->quoteColumn('node_id'),
1125
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
1126
            )
1127
        );
1128
        $query->prepare()->execute();
1129
    }
1130
1131
    /**
1132
     * Deletes ezcontentobject_tree row for given $locationId (node_id).
1133
     *
1134
     * @param mixed $locationId
1135
     */
1136
    public function removeLocation($locationId)
1137
    {
1138
        $query = $this->handler->createDeleteQuery();
1139
        $query->deleteFrom(
1140
            'ezcontentobject_tree'
1141
        )->where(
1142
            $query->expr->eq(
1143
                $this->handler->quoteColumn('node_id'),
1144
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
1145
            )
1146
        );
1147
        $query->prepare()->execute();
1148
    }
1149
1150
    /**
1151
     * Returns id of the next in line node to be set as a new main node.
1152
     *
1153
     * This returns lowest node id for content identified by $contentId, and not of
1154
     * the node identified by given $locationId (current main node).
1155
     * Assumes that content has more than one location.
1156
     *
1157
     * @param mixed $contentId
1158
     * @param mixed $locationId
1159
     *
1160
     * @return array
1161
     */
1162
    public function getFallbackMainNodeData($contentId, $locationId)
1163
    {
1164
        $query = $this->handler->createSelectQuery();
1165
        $query->select(
1166
            $this->handler->quoteColumn('node_id'),
1167
            $this->handler->quoteColumn('contentobject_version'),
1168
            $this->handler->quoteColumn('parent_node_id')
1169
        )->from(
1170
            $this->handler->quoteTable('ezcontentobject_tree')
1171
        )->where(
1172
            $query->expr->lAnd(
1173
                $query->expr->eq(
1174
                    $this->handler->quoteColumn('contentobject_id'),
1175
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1176
                ),
1177
                $query->expr->neq(
1178
                    $this->handler->quoteColumn('node_id'),
1179
                    $query->bindValue($locationId, null, \PDO::PARAM_INT)
1180
                )
1181
            )
1182
        )->orderBy('node_id', SelectQuery::ASC)->limit(1);
1183
        $statement = $query->prepare();
1184
        $statement->execute();
1185
1186
        return $statement->fetch(\PDO::FETCH_ASSOC);
1187
    }
1188
1189
    /**
1190
     * Sends a single location identified by given $locationId to the trash.
1191
     *
1192
     * The associated content object is left untouched.
1193
     *
1194
     * @param mixed $locationId
1195
     *
1196
     * @return bool
1197
     */
1198
    public function trashLocation($locationId)
1199
    {
1200
        $locationRow = $this->getBasicNodeData($locationId);
1201
1202
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
1203
        $query = $this->handler->createInsertQuery();
1204
        $query->insertInto($this->handler->quoteTable('ezcontentobject_trash'));
1205
1206
        unset($locationRow['contentobject_is_published']);
1207
        $locationRow['trashed'] = time();
1208
        foreach ($locationRow as $key => $value) {
1209
            $query->set($key, $query->bindValue($value));
1210
        }
1211
1212
        $query->prepare()->execute();
1213
1214
        $this->removeLocation($locationRow['node_id']);
1215
        $this->setContentStatus($locationRow['contentobject_id'], ContentInfo::STATUS_TRASHED);
1216
    }
1217
1218
    /**
1219
     * Returns a trashed location to normal state.
1220
     *
1221
     * Recreates the originally trashed location in the new position. If no new
1222
     * position has been specified, it will be tried to re-create the location
1223
     * at the old position. If this is not possible ( because the old location
1224
     * does not exist any more) and exception is thrown.
1225
     *
1226
     * @param mixed $locationId
1227
     * @param mixed|null $newParentId
1228
     *
1229
     * @return \eZ\Publish\SPI\Persistence\Content\Location
1230
     */
1231
    public function untrashLocation($locationId, $newParentId = null)
1232
    {
1233
        $row = $this->loadTrashByLocation($locationId);
1234
1235
        $newLocation = $this->create(
1236
            new CreateStruct(
1237
                [
1238
                    'priority' => $row['priority'],
1239
                    'hidden' => $row['is_hidden'],
1240
                    'invisible' => $row['is_invisible'],
1241
                    'remoteId' => $row['remote_id'],
1242
                    'contentId' => $row['contentobject_id'],
1243
                    'contentVersion' => $row['contentobject_version'],
1244
                    'mainLocationId' => true, // Restored location is always main location
1245
                    'sortField' => $row['sort_field'],
1246
                    'sortOrder' => $row['sort_order'],
1247
                ]
1248
            ),
1249
            $this->getBasicNodeData($newParentId ?: $row['parent_node_id'])
1250
        );
1251
1252
        $this->removeElementFromTrash($locationId);
1253
        $this->setContentStatus($row['contentobject_id'], ContentInfo::STATUS_PUBLISHED);
1254
1255
        return $newLocation;
1256
    }
1257
1258
    /**
1259
     * @param mixed $contentId
1260
     * @param int $status
1261
     */
1262
    protected function setContentStatus($contentId, $status)
1263
    {
1264
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1265
        $query = $this->handler->createUpdateQuery();
1266
        $query->update(
1267
            'ezcontentobject'
1268
        )->set(
1269
            $this->handler->quoteColumn('status'),
1270
            $query->bindValue($status, null, \PDO::PARAM_INT)
1271
        )->where(
1272
            $query->expr->eq(
1273
                $this->handler->quoteColumn('id'),
1274
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1275
            )
1276
        );
1277
        $query->prepare()->execute();
1278
    }
1279
1280
    /**
1281
     * Loads trash data specified by location ID.
1282
     *
1283
     * @param mixed $locationId
1284
     *
1285
     * @return array
1286
     */
1287
    public function loadTrashByLocation($locationId)
1288
    {
1289
        $query = $this->handler->createSelectQuery();
1290
        $query
1291
            ->select('*')
1292
            ->from($this->handler->quoteTable('ezcontentobject_trash'))
1293
            ->where(
1294
                $query->expr->eq(
1295
                    $this->handler->quoteColumn('node_id'),
1296
                    $query->bindValue($locationId)
1297
                )
1298
            );
1299
        $statement = $query->prepare();
1300
        $statement->execute();
1301
1302
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1303
            return $row;
1304
        }
1305
1306
        throw new NotFound('trash', $locationId);
1307
    }
1308
1309
    /**
1310
     * List trashed items.
1311
     *
1312
     * @param int $offset
1313
     * @param int $limit
1314
     * @param array $sort
1315
     *
1316
     * @return array
1317
     */
1318
    public function listTrashed($offset, $limit, array $sort = null)
1319
    {
1320
        $query = $this->handler->createSelectQuery();
1321
        $query
1322
            ->select('*')
1323
            ->from($this->handler->quoteTable('ezcontentobject_trash'));
1324
1325
        $sort = $sort ?: [];
1326
        foreach ($sort as $condition) {
1327
            $sortDirection = $condition->direction === Query::SORT_ASC ? SelectQuery::ASC : SelectQuery::DESC;
1328
            switch (true) {
1329
                case $condition instanceof SortClause\Location\Depth:
1330
                    $query->orderBy('depth', $sortDirection);
1331
                    break;
1332
1333
                case $condition instanceof SortClause\Location\Path:
1334
                    $query->orderBy('path_string', $sortDirection);
1335
                    break;
1336
1337
                case $condition instanceof SortClause\Location\Priority:
1338
                    $query->orderBy('priority', $sortDirection);
1339
                    break;
1340
1341
                case $condition instanceof SortClause\Trash\DateTrashed:
1342
                    $query->orderBy('trashed', $sortDirection);
1343
                    break;
1344
1345
                default:
1346
                    // Only handle location related sort clauses. The others
1347
                    // require data aggregation which is not sensible here.
1348
                    // Since also criteria are yet ignored, because they are
1349
                    // simply not used yet in eZ Publish, we skip that for now.
1350
                    throw new RuntimeException('Unhandled sort clause: ' . get_class($condition));
1351
            }
1352
        }
1353
1354
        if ($limit !== null) {
1355
            $query->limit($limit, $offset);
1356
        }
1357
1358
        $statement = $query->prepare();
1359
        $statement->execute();
1360
1361
        $rows = [];
1362
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1363
            $rows[] = $row;
1364
        }
1365
1366
        return $rows;
1367
    }
1368
1369 View Code Duplication
    public function countTrashed(): int
1370
    {
1371
        $dbPlatform = $this->connection->getDatabasePlatform();
1372
        $query = $this->connection->createQueryBuilder()
1373
            ->select($dbPlatform->getCountExpression('node_id'))
1374
            ->from('ezcontentobject_trash');
1375
1376
        return $query->execute()->fetchColumn();
1377
    }
1378
1379
    /**
1380
     * Removes every entries in the trash.
1381
     * Will NOT remove associated content objects nor attributes.
1382
     *
1383
     * Basically truncates ezcontentobject_trash table.
1384
     */
1385
    public function cleanupTrash()
1386
    {
1387
        $query = $this->handler->createDeleteQuery();
1388
        $query->deleteFrom('ezcontentobject_trash');
1389
        $query->prepare()->execute();
1390
    }
1391
1392
    /**
1393
     * Removes trashed element identified by $id from trash.
1394
     * Will NOT remove associated content object nor attributes.
1395
     *
1396
     * @param int $id The trashed location Id
1397
     */
1398
    public function removeElementFromTrash($id)
1399
    {
1400
        $query = $this->handler->createDeleteQuery();
1401
        $query
1402
            ->deleteFrom('ezcontentobject_trash')
1403
            ->where(
1404
                $query->expr->eq(
1405
                    $this->handler->quoteColumn('node_id'),
1406
                    $query->bindValue($id, null, \PDO::PARAM_INT)
1407
                )
1408
            );
1409
        $query->prepare()->execute();
1410
    }
1411
1412
    /**
1413
     * Set section on all content objects in the subtree.
1414
     *
1415
     * @param string $pathString
1416
     * @param int $sectionId
1417
     *
1418
     * @return bool
1419
     */
1420
    public function setSectionForSubtree($pathString, $sectionId)
1421
    {
1422
        $selectContentIdsQuery = $this->connection->createQueryBuilder();
1423
        $selectContentIdsQuery
1424
            ->select('t.contentobject_id')
1425
            ->from('ezcontentobject_tree', 't')
1426
            ->where(
1427
                $selectContentIdsQuery->expr()->like(
1428
                    't.path_string',
1429
                    $selectContentIdsQuery->createPositionalParameter("{$pathString}%")
1430
                )
1431
            );
1432
1433
        $contentIds = array_map(
1434
            'intval',
1435
            $selectContentIdsQuery->execute()->fetchAll(PDO::FETCH_COLUMN)
1436
        );
1437
1438
        if (empty($contentIds)) {
1439
            return false;
1440
        }
1441
1442
        $updateSectionQuery = $this->connection->createQueryBuilder();
1443
        $updateSectionQuery
1444
            ->update('ezcontentobject')
1445
            ->set(
1446
                'section_id',
1447
                $updateSectionQuery->createPositionalParameter($sectionId, PDO::PARAM_INT)
1448
            )
1449
            ->where(
1450
                $updateSectionQuery->expr()->in(
1451
                    'id',
1452
                    $contentIds
1453
                )
1454
            );
1455
        $affectedRows = $updateSectionQuery->execute();
1456
1457
        return $affectedRows > 0;
1458
    }
1459
1460
    /**
1461
     * Returns how many locations given content object identified by $contentId has.
1462
     *
1463
     * @param int $contentId
1464
     *
1465
     * @return int
1466
     */
1467
    public function countLocationsByContentId($contentId)
1468
    {
1469
        $q = $this->handler->createSelectQuery();
1470
        $q
1471
            ->select(
1472
                $q->alias($q->expr->count('*'), 'count')
1473
            )
1474
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
1475
            ->where(
1476
                $q->expr->eq(
1477
                    $this->handler->quoteColumn('contentobject_id'),
1478
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1479
                )
1480
            );
1481
        $stmt = $q->prepare();
1482
        $stmt->execute();
1483
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1484
1485
        return (int)$res[0]['count'];
1486
    }
1487
1488
    /**
1489
     * Changes main location of content identified by given $contentId to location identified by given $locationId.
1490
     *
1491
     * Updates ezcontentobject_tree table for the given $contentId and eznode_assignment table for the given
1492
     * $contentId, $parentLocationId and $versionNo
1493
     *
1494
     * @param mixed $contentId
1495
     * @param mixed $locationId
1496
     * @param mixed $versionNo version number, needed to update eznode_assignment table
1497
     * @param mixed $parentLocationId parent location of location identified by $locationId, needed to update
1498
     *        eznode_assignment table
1499
     */
1500
    public function changeMainLocation($contentId, $locationId, $versionNo, $parentLocationId)
1501
    {
1502
        // Update ezcontentobject_tree table
1503
        $q = $this->handler->createUpdateQuery();
1504
        $q->update(
1505
            $this->handler->quoteTable('ezcontentobject_tree')
1506
        )->set(
1507
            $this->handler->quoteColumn('main_node_id'),
1508
            $q->bindValue($locationId, null, \PDO::PARAM_INT)
1509
        )->where(
1510
            $q->expr->eq(
1511
                $this->handler->quoteColumn('contentobject_id'),
1512
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
1513
            )
1514
        );
1515
        $q->prepare()->execute();
1516
1517
        // Erase is_main in eznode_assignment table
1518
        $q = $this->handler->createUpdateQuery();
1519
        $q->update(
1520
            $this->handler->quoteTable('eznode_assignment')
1521
        )->set(
1522
            $this->handler->quoteColumn('is_main'),
1523
            $q->bindValue(0, null, \PDO::PARAM_INT)
1524
        )->where(
1525
            $q->expr->lAnd(
1526
                $q->expr->eq(
1527
                    $this->handler->quoteColumn('contentobject_id'),
1528
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1529
                ),
1530
                $q->expr->eq(
1531
                    $this->handler->quoteColumn('contentobject_version'),
1532
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1533
                ),
1534
                $q->expr->neq(
1535
                    $this->handler->quoteColumn('parent_node'),
1536
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1537
                )
1538
            )
1539
        );
1540
        $q->prepare()->execute();
1541
1542
        // Set new is_main in eznode_assignment table
1543
        $q = $this->handler->createUpdateQuery();
1544
        $q->update(
1545
            $this->handler->quoteTable('eznode_assignment')
1546
        )->set(
1547
            $this->handler->quoteColumn('is_main'),
1548
            $q->bindValue(1, null, \PDO::PARAM_INT)
1549
        )->where(
1550
            $q->expr->lAnd(
1551
                $q->expr->eq(
1552
                    $this->handler->quoteColumn('contentobject_id'),
1553
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1554
                ),
1555
                $q->expr->eq(
1556
                    $this->handler->quoteColumn('contentobject_version'),
1557
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1558
                ),
1559
                $q->expr->eq(
1560
                    $this->handler->quoteColumn('parent_node'),
1561
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1562
                )
1563
            )
1564
        );
1565
        $q->prepare()->execute();
1566
    }
1567
1568
    /**
1569
     * Get the total number of all Locations, except the Root node.
1570
     *
1571
     * @see loadAllLocationsData
1572
     *
1573
     * @return int
1574
     */
1575
    public function countAllLocations()
1576
    {
1577
        $query = $this->getAllLocationsQueryBuilder(['count(node_id)']);
1578
1579
        $statement = $query->execute();
1580
1581
        return (int) $statement->fetch(PDO::FETCH_COLUMN);
1582
    }
1583
1584
    /**
1585
     * Load data of every Location, except the Root node.
1586
     *
1587
     * @param int $offset Paginator offset
1588
     * @param int $limit Paginator limit
1589
     *
1590
     * @return array
1591
     */
1592
    public function loadAllLocationsData($offset, $limit)
1593
    {
1594
        $query = $this
1595
            ->getAllLocationsQueryBuilder(
1596
                [
1597
                    'node_id',
1598
                    'priority',
1599
                    'is_hidden',
1600
                    'is_invisible',
1601
                    'remote_id',
1602
                    'contentobject_id',
1603
                    'parent_node_id',
1604
                    'path_identification_string',
1605
                    'path_string',
1606
                    'depth',
1607
                    'sort_field',
1608
                    'sort_order',
1609
                ]
1610
            )
1611
            ->setFirstResult($offset)
1612
            ->setMaxResults($limit)
1613
            ->orderBy('depth', 'ASC')
1614
            ->addOrderBy('node_id', 'ASC')
1615
        ;
1616
1617
        $statement = $query->execute();
1618
1619
        return $statement->fetchAll(PDO::FETCH_ASSOC);
1620
    }
1621
1622
    /**
1623
     * Get Query Builder for fetching data of all Locations except the Root node.
1624
     *
1625
     * @todo Align with createNodeQueryBuilder, removing the need for both(?)
1626
     *
1627
     * @param array $columns list of columns to fetch
1628
     *
1629
     * @return \Doctrine\DBAL\Query\QueryBuilder
1630
     */
1631
    private function getAllLocationsQueryBuilder(array $columns)
1632
    {
1633
        $query = $this->connection->createQueryBuilder();
1634
        $query
1635
            ->select($columns)
1636
            ->from('ezcontentobject_tree')
1637
            ->where($query->expr()->neq('node_id', 'parent_node_id'))
1638
        ;
1639
1640
        return $query;
1641
    }
1642
1643
    /**
1644
     * Create QueryBuilder for selecting node data.
1645
     *
1646
     * @param array|null $translations Filters on language mask of content if provided.
1647
     * @param bool $useAlwaysAvailable Respect always available flag on content when filtering on $translations.
1648
     *
1649
     * @return \Doctrine\DBAL\Query\QueryBuilder
1650
     */
1651
    private function createNodeQueryBuilder(array $translations = null, bool $useAlwaysAvailable = true): QueryBuilder
1652
    {
1653
        $queryBuilder = $this->connection->createQueryBuilder();
1654
        $queryBuilder
1655
            ->select('t.*')
1656
            ->from('ezcontentobject_tree', 't');
1657
1658
        if (!empty($translations)) {
1659
            $dbPlatform = $this->connection->getDatabasePlatform();
1660
            $expr = $queryBuilder->expr();
1661
            $mask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(
1662
                $translations,
1663
                $useAlwaysAvailable
1664
            );
1665
1666
            $queryBuilder->leftJoin(
1667
                't',
1668
                'ezcontentobject',
1669
                'c',
1670
                $expr->eq('t.contentobject_id', 'c.id')
1671
            );
1672
1673
            $queryBuilder->where(
1674
                $expr->orX(
1675
                    $expr->gt(
1676
                        $dbPlatform->getBitAndComparisonExpression('c.language_mask', $mask),
1677
                        0
1678
                    ),
1679
                    // Root location doesn't have language mask
1680
                    $expr->eq(
1681
                        't.node_id', 't.parent_node_id'
1682
                    )
1683
                )
1684
            );
1685
        }
1686
1687
        return $queryBuilder;
1688
    }
1689
}
1690