Completed
Push — 7.5 ( 5ae6a9...706a7c )
by Łukasz
18:26
created

DoctrineDatabase::createNodeAssignment()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 0
loc 53
rs 9.0254
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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