Completed
Push — 7.5 ( 22ec60...932e58 )
by André
23:01
created

DoctrineDatabase::countTrashed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 9
Ratio 100 %

Importance

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