Completed
Push — EZEE-3159 ( 9c86bd )
by
unknown
19:29
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\ParameterType;
12
use Doctrine\DBAL\Query\QueryBuilder;
13
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator;
14
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
15
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
16
use eZ\Publish\Core\Persistence\Database\SelectQuery;
17
use eZ\Publish\Core\Persistence\Database\Query as DatabaseQuery;
18
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
19
use eZ\Publish\SPI\Persistence\Content\Location;
20
use eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct;
21
use eZ\Publish\SPI\Persistence\Content\Location\CreateStruct;
22
use eZ\Publish\API\Repository\Values\Content\Query;
23
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;
24
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
25
use RuntimeException;
26
use PDO;
27
28
/**
29
 * Location gateway implementation using the Doctrine database.
30
 */
31
class DoctrineDatabase extends Gateway
32
{
33
    /**
34
     * 2^30, since PHP_INT_MAX can cause overflows in DB systems, if PHP is run
35
     * on 64 bit systems.
36
     */
37
    const MAX_LIMIT = 1073741824;
38
39
    /**
40
     * Database handler.
41
     *
42
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
43
     */
44
    protected $handler;
45
46
    /** @var \Doctrine\DBAL\Connection */
47
    protected $connection;
48
49
    /**
50
     * Language mask generator.
51
     *
52
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
53
     */
54
    protected $languageMaskGenerator;
55
56
    /**
57
     * Construct from database handler.
58
     *
59
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $handler
60
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
61
     */
62
    public function __construct(DatabaseHandler $handler, MaskGenerator $languageMaskGenerator)
63
    {
64
        $this->handler = $handler;
65
        $this->connection = $handler->getConnection();
66
        $this->languageMaskGenerator = $languageMaskGenerator;
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 View Code Duplication
    public function getBasicNodeData($nodeId, array $translations = null, bool $useAlwaysAvailable = true)
73
    {
74
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
75
        $q->andWhere(
76
            $q->expr()->eq('t.node_id', $q->createNamedParameter($nodeId, PDO::PARAM_INT))
77
        );
78
79
        if ($row = $q->execute()->fetch(FetchMode::ASSOCIATIVE)) {
80
            return $row;
81
        }
82
83
        throw new NotFound('location', $nodeId);
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function getNodeDataList(array $locationIds, array $translations = null, bool $useAlwaysAvailable = true): iterable
90
    {
91
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
92
        $q->andWhere(
93
            $q->expr()->in(
94
                't.node_id',
95
                $q->createNamedParameter($locationIds, Connection::PARAM_INT_ARRAY)
96
            )
97
        );
98
99
        return $q->execute()->fetchAll(FetchMode::ASSOCIATIVE);
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 View Code Duplication
    public function getBasicNodeDataByRemoteId($remoteId, array $translations = null, bool $useAlwaysAvailable = true)
106
    {
107
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
108
        $q->andWhere(
109
            $q->expr()->eq('t.remote_id', $q->createNamedParameter($remoteId, PDO::PARAM_STR))
110
        );
111
112
        if ($row = $q->execute()->fetch(FetchMode::ASSOCIATIVE)) {
113
            return $row;
114
        }
115
116
        throw new NotFound('location', $remoteId);
117
    }
118
119
    /**
120
     * Loads data for all Locations for $contentId in trash, optionally only in the
121
     * subtree starting at $rootLocationId.
122
     *
123
     * @param int $contentId
124
     * @param int $rootLocationId
125
     *
126
     * @return array
127
     */
128
    public function loadLocationDataByContent($contentId, $rootLocationId = null)
129
    {
130
        $query = $this->handler->createSelectQuery();
131
        $query
132
            ->select('*')
133
            ->from($this->handler->quoteTable('ezcontentobject_trash'))
134
            ->where(
135
                $query->expr->eq(
136
                    $this->handler->quoteColumn('contentobject_id'),
137
                    $query->bindValue($contentId)
138
                )
139
            );
140
        if ($rootLocationId !== null) {
141
            $this->applySubtreeLimitation($query, $rootLocationId);
142
        }
143
        $statement = $query->prepare();
144
        $statement->execute();
145
146
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
147
    }
148
149
    /**
150
     * Loads data for all Locations for $contentId, optionally only in the
151
     * subtree starting at $rootLocationId.
152
     *
153
     * @param int $contentId
154
     * @param int $rootLocationId
155
     *
156
     * @return array
157
     */
158
    public function loadLocationDataByTrashContent($contentId, $rootLocationId = null)
159
    {
160
        $query = $this->connection->createQueryBuilder();
161
        $query
162
            ->select('*')
163
            ->from($this->connection->quoteIdentifier('ezcontentobject_trash'))
164
            ->where('contentobject_id = :contentobject_id')
165
            ->setParameter('contentobject_id', $contentId, ParameterType::INTEGER);
166
167
        if ($rootLocationId !== null) {
168
            $this->applySubtreeLimitation($query, $rootLocationId);
169
        }
170
        $statement = $query->prepare();
0 ignored issues
show
Bug introduced by
The method prepare() does not seem to exist on object<Doctrine\DBAL\Query\QueryBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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