Completed
Push — paratest ( 20b7df...126921 )
by André
31:14
created

DoctrineDatabase::cleanupTrash()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the DoctrineDatabase Location Gateway class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
10
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\FetchMode;
13
use Doctrine\DBAL\Query\QueryBuilder;
14
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator;
15
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
16
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
17
use eZ\Publish\Core\Persistence\Database\SelectQuery;
18
use eZ\Publish\Core\Persistence\Database\Query as DatabaseQuery;
19
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
20
use eZ\Publish\SPI\Persistence\Content\Location;
21
use eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct;
22
use eZ\Publish\SPI\Persistence\Content\Location\CreateStruct;
23
use eZ\Publish\API\Repository\Values\Content\Query;
24
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;
25
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
26
use RuntimeException;
27
use PDO;
28
29
/**
30
 * Location gateway implementation using the Doctrine database.
31
 */
32
class DoctrineDatabase extends Gateway
33
{
34
    /**
35
     * 2^30, since PHP_INT_MAX can cause overflows in DB systems, if PHP is run
36
     * on 64 bit systems.
37
     */
38
    const MAX_LIMIT = 1073741824;
39
40
    /**
41
     * Database handler.
42
     *
43
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
44
     */
45
    protected $handler;
46
47
    /**
48
     * @var \Doctrine\DBAL\Connection
49
     */
50
    protected $connection;
51
52
    /**
53
     * Language mask generator.
54
     *
55
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
56
     */
57
    protected $languageMaskGenerator;
58
59
    /**
60
     * Construct from database handler.
61
     *
62
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $handler
63
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
64
     */
65
    public function __construct(DatabaseHandler $handler, MaskGenerator $languageMaskGenerator)
66
    {
67
        $this->handler = $handler;
68
        $this->connection = $handler->getConnection();
69
        $this->languageMaskGenerator = $languageMaskGenerator;
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 View Code Duplication
    public function getBasicNodeData($nodeId, array $translations = null, bool $useAlwaysAvailable = true)
76
    {
77
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
78
        $q->where(
79
            $q->expr()->eq('t.node_id', $q->createNamedParameter($nodeId, PDO::PARAM_INT))
80
        );
81
82
        if ($row = $q->execute()->fetch(FetchMode::ASSOCIATIVE)) {
83
            return $row;
84
        }
85
86
        throw new NotFound('location', $nodeId);
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92 View Code Duplication
    public function getBasicNodeDataByRemoteId($remoteId, array $translations = null, bool $useAlwaysAvailable = true)
93
    {
94
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
95
        $q->where(
96
            $q->expr()->eq('t.remote_id', $q->createNamedParameter($remoteId, PDO::PARAM_STR))
97
        );
98
99
        if ($row = $q->execute()->fetch(FetchMode::ASSOCIATIVE)) {
100
            return $row;
101
        }
102
103
        throw new NotFound('location', $remoteId);
104
    }
105
106
    /**
107
     * Loads data for all Locations for $contentId, optionally only in the
108
     * subtree starting at $rootLocationId.
109
     *
110
     * @param int $contentId
111
     * @param int $rootLocationId
112
     *
113
     * @return array
114
     */
115
    public function loadLocationDataByContent($contentId, $rootLocationId = null)
116
    {
117
        $query = $this->handler->createSelectQuery();
118
        $query
119
            ->select('*')
120
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
121
            ->where(
122
                $query->expr->eq(
123
                    $this->handler->quoteColumn('contentobject_id'),
124
                    $query->bindValue($contentId)
125
                )
126
            );
127
128
        if ($rootLocationId !== null) {
129
            $this->applySubtreeLimitation($query, $rootLocationId);
130
        }
131
132
        $statement = $query->prepare();
133
        $statement->execute();
134
135
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
136
    }
137
138
    /**
139
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway::loadParentLocationsDataForDraftContent
140
     */
141
    public function loadParentLocationsDataForDraftContent($contentId, $drafts = null)
142
    {
143
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
144
        $query = $this->handler->createSelectQuery();
145
        $query->selectDistinct(
146
            'ezcontentobject_tree.*'
147
        )->from(
148
            $this->handler->quoteTable('ezcontentobject_tree')
149
        )->innerJoin(
150
            $this->handler->quoteTable('eznode_assignment'),
151
            $query->expr->lAnd(
152
                $query->expr->eq(
153
                    $this->handler->quoteColumn('node_id', 'ezcontentobject_tree'),
154
                    $this->handler->quoteColumn('parent_node', 'eznode_assignment')
155
                ),
156
                $query->expr->eq(
157
                    $this->handler->quoteColumn('contentobject_id', 'eznode_assignment'),
158
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
159
                ),
160
                $query->expr->eq(
161
                    $this->handler->quoteColumn('op_code', 'eznode_assignment'),
162
                    $query->bindValue(self::NODE_ASSIGNMENT_OP_CODE_CREATE, null, \PDO::PARAM_INT)
163
                )
164
            )
165
        )->innerJoin(
166
            $this->handler->quoteTable('ezcontentobject'),
167
            $query->expr->lAnd(
168
                $query->expr->lOr(
169
                    $query->expr->eq(
170
                        $this->handler->quoteColumn('contentobject_id', 'eznode_assignment'),
171
                        $this->handler->quoteColumn('id', 'ezcontentobject')
172
                    )
173
                ),
174
                $query->expr->eq(
175
                    $this->handler->quoteColumn('status', 'ezcontentobject'),
176
                    $query->bindValue(ContentInfo::STATUS_DRAFT, null, \PDO::PARAM_INT)
177
                )
178
            )
179
        );
180
181
        $statement = $query->prepare();
182
        $statement->execute();
183
184
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
185
    }
186
187
    /**
188
     * Find all content in the given subtree.
189
     *
190
     * @param mixed $sourceId
191
     * @param bool $onlyIds
192
     *
193
     * @return array
194
     */
195
    public function getSubtreeContent($sourceId, $onlyIds = false)
196
    {
197
        $query = $this->handler->createSelectQuery();
198
        $query->select($onlyIds ? 'node_id, contentobject_id, depth' : '*')->from(
199
            $this->handler->quoteTable('ezcontentobject_tree')
200
        );
201
        $this->applySubtreeLimitation($query, $sourceId);
202
        $query->orderBy(
203
            $this->handler->quoteColumn('depth', 'ezcontentobject_tree')
204
        )->orderBy(
205
            $this->handler->quoteColumn('node_id', 'ezcontentobject_tree')
206
        );
207
        $statement = $query->prepare();
208
        $statement->execute();
209
210
        $results = $statement->fetchAll($onlyIds ? (PDO::FETCH_COLUMN | PDO::FETCH_GROUP) : PDO::FETCH_ASSOC);
211
        // array_map() is used to to map all elements stored as $results[$i][0] to $results[$i]
212
        return $onlyIds ? array_map('reset', $results) : $results;
213
    }
214
215
    /**
216
     * Limits the given $query to the subtree starting at $rootLocationId.
217
     *
218
     * @param \eZ\Publish\Core\Persistence\Database\Query $query
219
     * @param string $rootLocationId
220
     */
221
    protected function applySubtreeLimitation(DatabaseQuery $query, $rootLocationId)
222
    {
223
        $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...
224
            $query->expr->like(
225
                $this->handler->quoteColumn('path_string', 'ezcontentobject_tree'),
226
                $query->bindValue('%/' . $rootLocationId . '/%')
227
            )
228
        );
229
    }
230
231
    /**
232
     * Returns data for the first level children of the location identified by given $locationId.
233
     *
234
     * @param mixed $locationId
235
     *
236
     * @return array
237
     */
238
    public function getChildren($locationId)
239
    {
240
        $query = $this->handler->createSelectQuery();
241
        $query->select('*')->from(
242
            $this->handler->quoteTable('ezcontentobject_tree')
243
        )->where(
244
            $query->expr->eq(
245
                $this->handler->quoteColumn('parent_node_id', 'ezcontentobject_tree'),
246
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
247
            )
248
        );
249
        $statement = $query->prepare();
250
        $statement->execute();
251
252
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
253
    }
254
255
    /**
256
     * Update path strings to move nodes in the ezcontentobject_tree table.
257
     *
258
     * This query can likely be optimized to use some more advanced string
259
     * operations, which then depend on the respective database.
260
     *
261
     * @todo optimize
262
     *
263
     * @param array $sourceNodeData
264
     * @param array $destinationNodeData
265
     */
266
    public function moveSubtreeNodes(array $sourceNodeData, array $destinationNodeData)
267
    {
268
        $fromPathString = $sourceNodeData['path_string'];
269
270
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
271
        $query = $this->handler->createSelectQuery();
272
        $query
273
            ->select(
274
                $this->handler->quoteColumn('node_id'),
275
                $this->handler->quoteColumn('parent_node_id'),
276
                $this->handler->quoteColumn('path_string'),
277
                $this->handler->quoteColumn('path_identification_string')
278
            )
279
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
280
            ->where(
281
                $query->expr->like(
282
                    $this->handler->quoteColumn('path_string'),
283
                    $query->bindValue($fromPathString . '%')
284
                )
285
            );
286
        $statement = $query->prepare();
287
        $statement->execute();
288
289
        $rows = $statement->fetchAll();
290
        $oldParentPathString = implode('/', array_slice(explode('/', $fromPathString), 0, -2)) . '/';
291
        $oldParentPathIdentificationString = implode(
292
            '/',
293
            array_slice(explode('/', $sourceNodeData['path_identification_string']), 0, -1)
294
        );
295
296
        foreach ($rows as $row) {
297
            // Prefixing ensures correct replacement when old parent is root node
298
            $newPathString = str_replace(
299
                'prefix' . $oldParentPathString,
300
                $destinationNodeData['path_string'],
301
                'prefix' . $row['path_string']
302
            );
303
            $newPathIdentificationString = str_replace(
304
                'prefix' . $oldParentPathIdentificationString,
305
                $destinationNodeData['path_identification_string'] . '/',
306
                'prefix' . $row['path_identification_string']
307
            );
308
309
            $newParentId = $row['parent_node_id'];
310
            if ($row['path_string'] === $fromPathString) {
311
                $newParentId = (int)implode('', array_slice(explode('/', $newPathString), -3, 1));
312
            }
313
314
            /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
315
            $query = $this->handler->createUpdateQuery();
316
            $query
317
                ->update($this->handler->quoteTable('ezcontentobject_tree'))
318
                ->set(
319
                    $this->handler->quoteColumn('path_string'),
320
                    $query->bindValue($newPathString)
321
                )
322
                ->set(
323
                    $this->handler->quoteColumn('path_identification_string'),
324
                    $query->bindValue($newPathIdentificationString)
325
                )
326
                ->set(
327
                    $this->handler->quoteColumn('depth'),
328
                    $query->bindValue(substr_count($newPathString, '/') - 2)
329
                )
330
                ->set(
331
                    $this->handler->quoteColumn('parent_node_id'),
332
                    $query->bindValue($newParentId)
333
                );
334
335
            if ($destinationNodeData['is_hidden'] || $destinationNodeData['is_invisible']) {
336
                // CASE 1: Mark whole tree as invisible if destination is invisible and/or hidden
337
                $query->set(
338
                    $this->handler->quoteColumn('is_invisible'),
339
                    $query->bindValue(1)
340
                );
341
            } elseif (!$sourceNodeData['is_hidden'] && $sourceNodeData['is_invisible']) {
342
                // CASE 2: source is only invisible, we will need to re-calculate whole moved tree visibility
343
                $query->set(
344
                    $this->handler->quoteColumn('is_invisible'),
345
                    $query->bindValue($this->isHiddenByParent($newPathString, $rows) ? 1 : 0)
346
                );
347
            } else {
348
                // CASE 3: keep invisible flags as is (source is either hidden or not hidden/invisible at all)
349
            }
350
351
            $query->where(
352
                    $query->expr->eq(
353
                        $this->handler->quoteColumn('node_id'),
354
                        $query->bindValue($row['node_id'])
355
                    )
356
                );
357
            $query->prepare()->execute();
358
        }
359
    }
360
361
    private function isHiddenByParent($pathString, array $rows)
362
    {
363
        $parentNodeIds = explode('/', trim($pathString, '/'));
364
        array_pop($parentNodeIds); // remove self
365
        foreach ($rows as $row) {
366
            if ($row['is_hidden'] && in_array($row['node_id'], $parentNodeIds)) {
367
                return true;
368
            }
369
        }
370
371
        return false;
372
    }
373
374
    /**
375
     * Updated subtree modification time for all nodes on path.
376
     *
377
     * @param string $pathString
378
     * @param int|null $timestamp
379
     */
380
    public function updateSubtreeModificationTime($pathString, $timestamp = null)
381
    {
382
        $nodes = array_filter(explode('/', $pathString));
383
        $query = $this->handler->createUpdateQuery();
384
        $query
385
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
386
            ->set(
387
                $this->handler->quoteColumn('modified_subnode'),
388
                $query->bindValue(
389
                    $timestamp ?: time()
390
                )
391
            )
392
            ->where(
393
                $query->expr->in(
394
                    $this->handler->quoteColumn('node_id'),
395
                    $nodes
396
                )
397
            );
398
        $query->prepare()->execute();
399
    }
400
401
    /**
402
     * Sets a location to be hidden, and it self + all children to invisible.
403
     *
404
     * @param string $pathString
405
     */
406
    public function hideSubtree($pathString)
407
    {
408
        $query = $this->handler->createUpdateQuery();
409
        $query
410
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
411
            ->set(
412
                $this->handler->quoteColumn('is_invisible'),
413
                $query->bindValue(1)
414
            )
415
            ->set(
416
                $this->handler->quoteColumn('modified_subnode'),
417
                $query->bindValue(time())
418
            )
419
            ->where(
420
                $query->expr->like(
421
                    $this->handler->quoteColumn('path_string'),
422
                    $query->bindValue($pathString . '%')
423
                )
424
            );
425
        $query->prepare()->execute();
426
427
        $query = $this->handler->createUpdateQuery();
428
        $query
429
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
430
            ->set(
431
                $this->handler->quoteColumn('is_hidden'),
432
                $query->bindValue(1)
433
            )
434
            ->where(
435
                $query->expr->eq(
436
                    $this->handler->quoteColumn('path_string'),
437
                    $query->bindValue($pathString)
438
                )
439
            );
440
        $query->prepare()->execute();
441
    }
442
443
    /**
444
     * Sets a location to be unhidden, and self + children to visible unless a parent is hiding the tree.
445
     * If not make sure only children down to first hidden node is marked visible.
446
     *
447
     * @param string $pathString
448
     */
449
    public function unHideSubtree($pathString)
450
    {
451
        // Unhide the requested node
452
        $query = $this->handler->createUpdateQuery();
453
        $query
454
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
455
            ->set(
456
                $this->handler->quoteColumn('is_hidden'),
457
                $query->bindValue(0)
458
            )
459
            ->where(
460
                $query->expr->eq(
461
                    $this->handler->quoteColumn('path_string'),
462
                    $query->bindValue($pathString)
463
                )
464
            );
465
        $query->prepare()->execute();
466
467
        // Check if any parent nodes are explicitly hidden
468
        $query = $this->handler->createSelectQuery();
469
        $query
470
            ->select($this->handler->quoteColumn('path_string'))
471
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
472
            ->where(
473
                $query->expr->lAnd(
474
                    $query->expr->eq(
475
                        $this->handler->quoteColumn('is_hidden'),
476
                        $query->bindValue(1)
477
                    ),
478
                    $query->expr->in(
479
                        $this->handler->quoteColumn('node_id'),
480
                        array_filter(explode('/', $pathString))
481
                    )
482
                )
483
            );
484
        $statement = $query->prepare();
485
        $statement->execute();
486
        if (count($statement->fetchAll(\PDO::FETCH_COLUMN))) {
487
            // There are parent nodes set hidden, so that we can skip marking
488
            // something visible again.
489
            return;
490
        }
491
492
        // Find nodes of explicitly hidden subtrees in the subtree which
493
        // should be unhidden
494
        $query = $this->handler->createSelectQuery();
495
        $query
496
            ->select($this->handler->quoteColumn('path_string'))
497
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
498
            ->where(
499
                $query->expr->lAnd(
500
                    $query->expr->eq(
501
                        $this->handler->quoteColumn('is_hidden'),
502
                        $query->bindValue(1)
503
                    ),
504
                    $query->expr->like(
505
                        $this->handler->quoteColumn('path_string'),
506
                        $query->bindValue($pathString . '%')
507
                    )
508
                )
509
            );
510
        $statement = $query->prepare();
511
        $statement->execute();
512
        $hiddenSubtrees = $statement->fetchAll(\PDO::FETCH_COLUMN);
513
514
        $query = $this->handler->createUpdateQuery();
515
        $query
516
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
517
            ->set(
518
                $this->handler->quoteColumn('is_invisible'),
519
                $query->bindValue(0)
520
            )
521
            ->set(
522
                $this->handler->quoteColumn('modified_subnode'),
523
                $query->bindValue(time())
524
            );
525
526
        // Build where expression selecting the nodes, which should be made
527
        // visible again
528
        $where = $query->expr->like(
529
            $this->handler->quoteColumn('path_string'),
530
            $query->bindValue($pathString . '%')
531
        );
532
        if (count($hiddenSubtrees)) {
533
            $handler = $this->handler;
534
            $where = $query->expr->lAnd(
535
                $where,
536
                $query->expr->lAnd(
537
                    array_map(
538
                        function ($pathString) use ($query, $handler) {
539
                            return $query->expr->not(
540
                                $query->expr->like(
541
                                    $handler->quoteColumn('path_string'),
542
                                    $query->bindValue($pathString . '%')
543
                                )
544
                            );
545
                        },
546
                        $hiddenSubtrees
547
                    )
548
                )
549
            );
550
        }
551
        $query->where($where);
552
        $statement = $query->prepare()->execute();
0 ignored issues
show
Unused Code introduced by
$statement is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
553
    }
554
555
    /**
556
     * Swaps the content object being pointed to by a location object.
557
     *
558
     * Make the location identified by $locationId1 refer to the Content
559
     * referred to by $locationId2 and vice versa.
560
     *
561
     * @param mixed $locationId1
562
     * @param mixed $locationId2
563
     *
564
     * @return bool
565
     */
566
    public function swap($locationId1, $locationId2)
567
    {
568
        $queryBuilder = $this->connection->createQueryBuilder();
569
        $expr = $queryBuilder->expr();
570
        $queryBuilder
571
            ->select('node_id', 'main_node_id', 'contentobject_id', 'contentobject_version')
572
            ->from('ezcontentobject_tree')
573
            ->where(
574
                $expr->in(
575
                    'node_id',
576
                    ':locationIds'
577
                )
578
            )
579
            ->setParameter('locationIds', [$locationId1, $locationId2], Connection::PARAM_INT_ARRAY)
580
        ;
581
        $statement = $queryBuilder->execute();
582
        $contentObjects = [];
583
        foreach ($statement->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
584
            $row['is_main_node'] = (int)$row['main_node_id'] === (int)$row['node_id'];
585
            $contentObjects[$row['node_id']] = $row;
586
        }
587
588
        if (!isset($contentObjects[$locationId1], $contentObjects[$locationId2])) {
589
            throw new RuntimeException(
590
                sprintf(
591
                    '%s: failed to fetch either Location %d or Location %d',
592
                    __METHOD__,
593
                    $locationId1,
594
                    $locationId2
595
                )
596
            );
597
        }
598
        $content1data = $contentObjects[$locationId1];
599
        $content2data = $contentObjects[$locationId2];
600
601
        $queryBuilder = $this->connection->createQueryBuilder();
602
        $queryBuilder
603
            ->update('ezcontentobject_tree')
604
            ->set('contentobject_id', ':contentId')
605
            ->set('contentobject_version', ':versionNo')
606
            ->set('main_node_id', ':mainNodeId')
607
            ->where(
608
                $expr->eq('node_id', ':locationId')
609
            );
610
611
        $queryBuilder
612
            ->setParameter(':contentId', $content2data['contentobject_id'])
613
            ->setParameter(':versionNo', $content2data['contentobject_version'])
614
            ->setParameter(
615
                ':mainNodeId',
616
                // make main Location main again, preserve main Location id of non-main one
617
                $content2data['is_main_node']
618
                    ? $content1data['node_id']
619
                    : $content2data['main_node_id']
620
            )
621
            ->setParameter('locationId', $locationId1);
622
623
        // update Location 1 entry
624
        $queryBuilder->execute();
625
626
        $queryBuilder
627
            ->setParameter(':contentId', $content1data['contentobject_id'])
628
            ->setParameter(':versionNo', $content1data['contentobject_version'])
629
            ->setParameter(
630
                ':mainNodeId',
631
                $content1data['is_main_node']
632
                    // make main Location main again, preserve main Location id of non-main one
633
                    ? $content2data['node_id']
634
                    : $content1data['main_node_id']
635
            )
636
            ->setParameter('locationId', $locationId2);
637
638
        // update Location 2 entry
639
        $queryBuilder->execute();
640
641
        return true;
642
    }
643
644
    /**
645
     * Creates a new location in given $parentNode.
646
     *
647
     * @param \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct $createStruct
648
     * @param array $parentNode
649
     *
650
     * @return \eZ\Publish\SPI\Persistence\Content\Location
651
     */
652
    public function create(CreateStruct $createStruct, array $parentNode)
653
    {
654
        $location = new Location();
655
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
656
        $query = $this->handler->createInsertQuery();
657
        $query
658
            ->insertInto($this->handler->quoteTable('ezcontentobject_tree'))
659
            ->set(
660
                $this->handler->quoteColumn('contentobject_id'),
661
                $query->bindValue($location->contentId = $createStruct->contentId, null, \PDO::PARAM_INT)
662
            )->set(
663
                $this->handler->quoteColumn('contentobject_is_published'),
664
                $query->bindValue(1, null, \PDO::PARAM_INT)
665
            )->set(
666
                $this->handler->quoteColumn('contentobject_version'),
667
                $query->bindValue($createStruct->contentVersion, null, \PDO::PARAM_INT)
668
            )->set(
669
                $this->handler->quoteColumn('depth'),
670
                $query->bindValue($location->depth = $parentNode['depth'] + 1, null, \PDO::PARAM_INT)
671
            )->set(
672
                $this->handler->quoteColumn('is_hidden'),
673
                $query->bindValue($location->hidden = $createStruct->hidden, null, \PDO::PARAM_INT)
674
            )->set(
675
                $this->handler->quoteColumn('is_invisible'),
676
                $query->bindValue($location->invisible = $createStruct->invisible, null, \PDO::PARAM_INT)
677
            )->set(
678
                $this->handler->quoteColumn('modified_subnode'),
679
                $query->bindValue(time(), null, \PDO::PARAM_INT)
680
            )->set(
681
                $this->handler->quoteColumn('node_id'),
682
                $this->handler->getAutoIncrementValue('ezcontentobject_tree', 'node_id')
683
            )->set(
684
                $this->handler->quoteColumn('parent_node_id'),
685
                $query->bindValue($location->parentId = $parentNode['node_id'], null, \PDO::PARAM_INT)
686
            )->set(
687
                $this->handler->quoteColumn('path_identification_string'),
688
                $query->bindValue($location->pathIdentificationString = $createStruct->pathIdentificationString, null, \PDO::PARAM_STR)
689
            )->set(
690
                $this->handler->quoteColumn('path_string'),
691
                $query->bindValue('dummy') // Set later
692
            )->set(
693
                $this->handler->quoteColumn('priority'),
694
                $query->bindValue($location->priority = $createStruct->priority, null, \PDO::PARAM_INT)
695
            )->set(
696
                $this->handler->quoteColumn('remote_id'),
697
                $query->bindValue($location->remoteId = $createStruct->remoteId, null, \PDO::PARAM_STR)
698
            )->set(
699
                $this->handler->quoteColumn('sort_field'),
700
                $query->bindValue($location->sortField = $createStruct->sortField, null, \PDO::PARAM_INT)
701
            )->set(
702
                $this->handler->quoteColumn('sort_order'),
703
                $query->bindValue($location->sortOrder = $createStruct->sortOrder, null, \PDO::PARAM_INT)
704
            );
705
        $query->prepare()->execute();
706
707
        $location->id = $this->handler->lastInsertId($this->handler->getSequenceName('ezcontentobject_tree', 'node_id'));
708
709
        $mainLocationId = $createStruct->mainLocationId === true ? $location->id : $createStruct->mainLocationId;
710
        $location->pathString = $parentNode['path_string'] . $location->id . '/';
711
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
712
        $query = $this->handler->createUpdateQuery();
713
        $query
714
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
715
            ->set(
716
                $this->handler->quoteColumn('path_string'),
717
                $query->bindValue($location->pathString)
718
            )
719
            ->set(
720
                $this->handler->quoteColumn('main_node_id'),
721
                $query->bindValue($mainLocationId, null, \PDO::PARAM_INT)
722
            )
723
            ->where(
724
                $query->expr->eq(
725
                    $this->handler->quoteColumn('node_id'),
726
                    $query->bindValue($location->id, null, \PDO::PARAM_INT)
727
                )
728
            );
729
        $query->prepare()->execute();
730
731
        return $location;
732
    }
733
734
    /**
735
     * Create an entry in the node assignment table.
736
     *
737
     * @param \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct $createStruct
738
     * @param mixed $parentNodeId
739
     * @param int $type
740
     */
741
    public function createNodeAssignment(CreateStruct $createStruct, $parentNodeId, $type = self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP)
742
    {
743
        $isMain = ($createStruct->mainLocationId === true ? 1 : 0);
744
745
        $query = $this->handler->createInsertQuery();
746
        $query
747
            ->insertInto($this->handler->quoteTable('eznode_assignment'))
748
            ->set(
749
                $this->handler->quoteColumn('contentobject_id'),
750
                $query->bindValue($createStruct->contentId, null, \PDO::PARAM_INT)
751
            )->set(
752
                $this->handler->quoteColumn('contentobject_version'),
753
                $query->bindValue($createStruct->contentVersion, null, \PDO::PARAM_INT)
754
            )->set(
755
                $this->handler->quoteColumn('from_node_id'),
756
                $query->bindValue(0, null, \PDO::PARAM_INT) // unused field
757
            )->set(
758
                $this->handler->quoteColumn('id'),
759
                $this->handler->getAutoIncrementValue('eznode_assignment', 'id')
760
            )->set(
761
                $this->handler->quoteColumn('is_main'),
762
                $query->bindValue($isMain, null, \PDO::PARAM_INT) // Changed by the business layer, later
763
            )->set(
764
                $this->handler->quoteColumn('op_code'),
765
                $query->bindValue($type, null, \PDO::PARAM_INT)
766
            )->set(
767
                $this->handler->quoteColumn('parent_node'),
768
                $query->bindValue($parentNodeId, null, \PDO::PARAM_INT)
769
            )->set(
770
                // parent_remote_id column should contain the remote id of the corresponding Location
771
                $this->handler->quoteColumn('parent_remote_id'),
772
                $query->bindValue($createStruct->remoteId, null, \PDO::PARAM_STR)
773
            )->set(
774
                // remote_id column should contain the remote id of the node assignment itself,
775
                // however this was never implemented completely in Legacy Stack, so we just set
776
                // it to default value '0'
777
                $this->handler->quoteColumn('remote_id'),
778
                $query->bindValue('0', null, \PDO::PARAM_STR)
779
            )->set(
780
                $this->handler->quoteColumn('sort_field'),
781
                $query->bindValue($createStruct->sortField, null, \PDO::PARAM_INT)
782
            )->set(
783
                $this->handler->quoteColumn('sort_order'),
784
                $query->bindValue($createStruct->sortOrder, null, \PDO::PARAM_INT)
785
            )->set(
786
                $this->handler->quoteColumn('priority'),
787
                $query->bindValue($createStruct->priority, null, \PDO::PARAM_INT)
788
            )->set(
789
                $this->handler->quoteColumn('is_hidden'),
790
                $query->bindValue($createStruct->hidden, null, \PDO::PARAM_INT)
791
            );
792
        $query->prepare()->execute();
793
    }
794
795
    /**
796
     * Deletes node assignment for given $contentId and $versionNo.
797
     *
798
     * If $versionNo is not passed all node assignments for given $contentId are deleted
799
     *
800
     * @param int $contentId
801
     * @param int|null $versionNo
802
     */
803
    public function deleteNodeAssignment($contentId, $versionNo = null)
804
    {
805
        $query = $this->handler->createDeleteQuery();
806
        $query->deleteFrom(
807
            'eznode_assignment'
808
        )->where(
809
            $query->expr->eq(
810
                $this->handler->quoteColumn('contentobject_id'),
811
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
812
            )
813
        );
814
        if (isset($versionNo)) {
815
            $query->where(
816
                $query->expr->eq(
817
                    $this->handler->quoteColumn('contentobject_version'),
818
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
819
                )
820
            );
821
        }
822
        $query->prepare()->execute();
823
    }
824
825
    /**
826
     * Update node assignment table.
827
     *
828
     * @param int $contentObjectId
829
     * @param int $oldParent
830
     * @param int $newParent
831
     * @param int $opcode
832
     */
833
    public function updateNodeAssignment($contentObjectId, $oldParent, $newParent, $opcode)
834
    {
835
        $query = $this->handler->createUpdateQuery();
836
        $query
837
            ->update($this->handler->quoteTable('eznode_assignment'))
838
            ->set(
839
                $this->handler->quoteColumn('parent_node'),
840
                $query->bindValue($newParent, null, \PDO::PARAM_INT)
841
            )
842
            ->set(
843
                $this->handler->quoteColumn('op_code'),
844
                $query->bindValue($opcode, null, \PDO::PARAM_INT)
845
            )
846
            ->where(
847
                $query->expr->lAnd(
848
                    $query->expr->eq(
849
                        $this->handler->quoteColumn('contentobject_id'),
850
                        $query->bindValue($contentObjectId, null, \PDO::PARAM_INT)
851
                    ),
852
                    $query->expr->eq(
853
                        $this->handler->quoteColumn('parent_node'),
854
                        $query->bindValue($oldParent, null, \PDO::PARAM_INT)
855
                    )
856
                )
857
            );
858
        $query->prepare()->execute();
859
    }
860
861
    /**
862
     * Create locations from node assignments.
863
     *
864
     * Convert existing node assignments into real locations.
865
     *
866
     * @param mixed $contentId
867
     * @param mixed $versionNo
868
     */
869
    public function createLocationsFromNodeAssignments($contentId, $versionNo)
870
    {
871
        // select all node assignments with OP_CODE_CREATE (3) for this content
872
        $query = $this->handler->createSelectQuery();
873
        $query
874
            ->select('*')
875
            ->from($this->handler->quoteTable('eznode_assignment'))
876
            ->where(
877
                $query->expr->lAnd(
878
                    $query->expr->eq(
879
                        $this->handler->quoteColumn('contentobject_id'),
880
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
881
                    ),
882
                    $query->expr->eq(
883
                        $this->handler->quoteColumn('contentobject_version'),
884
                        $query->bindValue($versionNo, null, \PDO::PARAM_INT)
885
                    ),
886
                    $query->expr->eq(
887
                        $this->handler->quoteColumn('op_code'),
888
                        $query->bindValue(self::NODE_ASSIGNMENT_OP_CODE_CREATE, null, \PDO::PARAM_INT)
889
                    )
890
                )
891
            )->orderBy('id');
892
        $statement = $query->prepare();
893
        $statement->execute();
894
895
        // convert all these assignments to nodes
896
897
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
898
            if ((bool)$row['is_main'] === true) {
899
                $mainLocationId = true;
900
            } else {
901
                $mainLocationId = $this->getMainNodeId($contentId);
902
            }
903
904
            $parentLocationData = $this->getBasicNodeData($row['parent_node']);
905
            $isInvisible = $row['is_hidden'] || $parentLocationData['is_hidden'] || $parentLocationData['is_invisible'];
906
            $this->create(
907
                new CreateStruct(
908
                    array(
909
                        'contentId' => $row['contentobject_id'],
910
                        'contentVersion' => $row['contentobject_version'],
911
                        'mainLocationId' => $mainLocationId,
912
                        'remoteId' => $row['parent_remote_id'],
913
                        'sortField' => $row['sort_field'],
914
                        'sortOrder' => $row['sort_order'],
915
                        'priority' => $row['priority'],
916
                        'hidden' => $row['is_hidden'],
917
                        'invisible' => $isInvisible,
918
                    )
919
                ),
920
                $parentLocationData
921
            );
922
923
            $this->updateNodeAssignment(
924
                $row['contentobject_id'],
925
                $row['parent_node'],
926
                $row['parent_node'],
927
                self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP
928
            );
929
        }
930
    }
931
932
    /**
933
     * Updates all Locations of content identified with $contentId with $versionNo.
934
     *
935
     * @param mixed $contentId
936
     * @param mixed $versionNo
937
     */
938 View Code Duplication
    public function updateLocationsContentVersionNo($contentId, $versionNo)
939
    {
940
        $query = $this->handler->createUpdateQuery();
941
        $query->update(
942
            $this->handler->quoteTable('ezcontentobject_tree')
943
        )->set(
944
            $this->handler->quoteColumn('contentobject_version'),
945
            $query->bindValue($versionNo, null, \PDO::PARAM_INT)
946
        )->where(
947
            $query->expr->eq(
948
                $this->handler->quoteColumn('contentobject_id'),
949
                $contentId
950
            )
951
        );
952
        $query->prepare()->execute();
953
    }
954
955
    /**
956
     * Searches for the main nodeId of $contentId in $versionId.
957
     *
958
     * @param int $contentId
959
     *
960
     * @return int|bool
961
     */
962
    private function getMainNodeId($contentId)
963
    {
964
        $query = $this->handler->createSelectQuery();
965
        $query
966
            ->select('node_id')
967
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
968
            ->where(
969
                $query->expr->lAnd(
970
                    $query->expr->eq(
971
                        $this->handler->quoteColumn('contentobject_id'),
972
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
973
                    ),
974
                    $query->expr->eq(
975
                        $this->handler->quoteColumn('node_id'),
976
                        $this->handler->quoteColumn('main_node_id')
977
                    )
978
                )
979
            );
980
        $statement = $query->prepare();
981
        $statement->execute();
982
983
        $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
984
        if (count($result) === 1) {
985
            return (int)$result[0]['node_id'];
986
        } else {
987
            return false;
988
        }
989
    }
990
991
    /**
992
     * Updates an existing location.
993
     *
994
     * Will not throw anything if location id is invalid or no entries are affected.
995
     *
996
     * @param \eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct $location
997
     * @param int $locationId
998
     */
999
    public function update(UpdateStruct $location, $locationId)
1000
    {
1001
        $query = $this->handler->createUpdateQuery();
1002
1003
        $query
1004
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
1005
            ->set(
1006
                $this->handler->quoteColumn('priority'),
1007
                $query->bindValue($location->priority)
1008
            )
1009
            ->set(
1010
                $this->handler->quoteColumn('remote_id'),
1011
                $query->bindValue($location->remoteId)
1012
            )
1013
            ->set(
1014
                $this->handler->quoteColumn('sort_order'),
1015
                $query->bindValue($location->sortOrder)
1016
            )
1017
            ->set(
1018
                $this->handler->quoteColumn('sort_field'),
1019
                $query->bindValue($location->sortField)
1020
            )
1021
            ->where(
1022
                $query->expr->eq(
1023
                    $this->handler->quoteColumn('node_id'),
1024
                    $locationId
1025
                )
1026
            );
1027
        $statement = $query->prepare();
1028
        $statement->execute();
1029
1030
        // Commented due to EZP-23302: Update Location fails if no change is performed with the update
1031
        // Should be fixed with PDO::MYSQL_ATTR_FOUND_ROWS instead
1032
        /*if ( $statement->rowCount() < 1 )
1033
        {
1034
            throw new NotFound( 'location', $locationId );
1035
        }*/
1036
    }
1037
1038
    /**
1039
     * Updates path identification string for given $locationId.
1040
     *
1041
     * @param mixed $locationId
1042
     * @param mixed $parentLocationId
1043
     * @param string $text
1044
     */
1045
    public function updatePathIdentificationString($locationId, $parentLocationId, $text)
1046
    {
1047
        $parentData = $this->getBasicNodeData($parentLocationId);
1048
1049
        $newPathIdentificationString = empty($parentData['path_identification_string']) ?
1050
            $text :
1051
            $parentData['path_identification_string'] . '/' . $text;
1052
1053
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1054
        $query = $this->handler->createUpdateQuery();
1055
        $query->update(
1056
            'ezcontentobject_tree'
1057
        )->set(
1058
            $this->handler->quoteColumn('path_identification_string'),
1059
            $query->bindValue($newPathIdentificationString, null, \PDO::PARAM_STR)
1060
        )->where(
1061
            $query->expr->eq(
1062
                $this->handler->quoteColumn('node_id'),
1063
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
1064
            )
1065
        );
1066
        $query->prepare()->execute();
1067
    }
1068
1069
    /**
1070
     * Deletes ezcontentobject_tree row for given $locationId (node_id).
1071
     *
1072
     * @param mixed $locationId
1073
     */
1074
    public function removeLocation($locationId)
1075
    {
1076
        $query = $this->handler->createDeleteQuery();
1077
        $query->deleteFrom(
1078
            'ezcontentobject_tree'
1079
        )->where(
1080
            $query->expr->eq(
1081
                $this->handler->quoteColumn('node_id'),
1082
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
1083
            )
1084
        );
1085
        $query->prepare()->execute();
1086
    }
1087
1088
    /**
1089
     * Returns id of the next in line node to be set as a new main node.
1090
     *
1091
     * This returns lowest node id for content identified by $contentId, and not of
1092
     * the node identified by given $locationId (current main node).
1093
     * Assumes that content has more than one location.
1094
     *
1095
     * @param mixed $contentId
1096
     * @param mixed $locationId
1097
     *
1098
     * @return array
1099
     */
1100
    public function getFallbackMainNodeData($contentId, $locationId)
1101
    {
1102
        $query = $this->handler->createSelectQuery();
1103
        $query->select(
1104
            $this->handler->quoteColumn('node_id'),
1105
            $this->handler->quoteColumn('contentobject_version'),
1106
            $this->handler->quoteColumn('parent_node_id')
1107
        )->from(
1108
            $this->handler->quoteTable('ezcontentobject_tree')
1109
        )->where(
1110
            $query->expr->lAnd(
1111
                $query->expr->eq(
1112
                    $this->handler->quoteColumn('contentobject_id'),
1113
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1114
                ),
1115
                $query->expr->neq(
1116
                    $this->handler->quoteColumn('node_id'),
1117
                    $query->bindValue($locationId, null, \PDO::PARAM_INT)
1118
                )
1119
            )
1120
        )->orderBy('node_id', SelectQuery::ASC)->limit(1);
1121
        $statement = $query->prepare();
1122
        $statement->execute();
1123
1124
        return $statement->fetch(\PDO::FETCH_ASSOC);
1125
    }
1126
1127
    /**
1128
     * Sends a single location identified by given $locationId to the trash.
1129
     *
1130
     * The associated content object is left untouched.
1131
     *
1132
     * @param mixed $locationId
1133
     *
1134
     * @return bool
1135
     */
1136
    public function trashLocation($locationId)
1137
    {
1138
        $locationRow = $this->getBasicNodeData($locationId);
1139
1140
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
1141
        $query = $this->handler->createInsertQuery();
1142
        $query->insertInto($this->handler->quoteTable('ezcontentobject_trash'));
1143
1144
        unset($locationRow['contentobject_is_published']);
1145
        $locationRow['trashed'] = time();
1146
        foreach ($locationRow as $key => $value) {
1147
            $query->set($key, $query->bindValue($value));
1148
        }
1149
1150
        $query->prepare()->execute();
1151
1152
        $this->removeLocation($locationRow['node_id']);
1153
        $this->setContentStatus($locationRow['contentobject_id'], ContentInfo::STATUS_TRASHED);
1154
    }
1155
1156
    /**
1157
     * Returns a trashed location to normal state.
1158
     *
1159
     * Recreates the originally trashed location in the new position. If no new
1160
     * position has been specified, it will be tried to re-create the location
1161
     * at the old position. If this is not possible ( because the old location
1162
     * does not exist any more) and exception is thrown.
1163
     *
1164
     * @param mixed $locationId
1165
     * @param mixed|null $newParentId
1166
     *
1167
     * @return \eZ\Publish\SPI\Persistence\Content\Location
1168
     */
1169
    public function untrashLocation($locationId, $newParentId = null)
1170
    {
1171
        $row = $this->loadTrashByLocation($locationId);
1172
1173
        $newLocation = $this->create(
1174
            new CreateStruct(
1175
                array(
1176
                    'priority' => $row['priority'],
1177
                    'hidden' => $row['is_hidden'],
1178
                    'invisible' => $row['is_invisible'],
1179
                    'remoteId' => $row['remote_id'],
1180
                    'contentId' => $row['contentobject_id'],
1181
                    'contentVersion' => $row['contentobject_version'],
1182
                    'mainLocationId' => true, // Restored location is always main location
1183
                    'sortField' => $row['sort_field'],
1184
                    'sortOrder' => $row['sort_order'],
1185
                )
1186
            ),
1187
            $this->getBasicNodeData($newParentId ?: $row['parent_node_id'])
1188
        );
1189
1190
        $this->removeElementFromTrash($locationId);
1191
        $this->setContentStatus($row['contentobject_id'], ContentInfo::STATUS_PUBLISHED);
1192
1193
        return $newLocation;
1194
    }
1195
1196
    /**
1197
     * @param mixed $contentId
1198
     * @param int $status
1199
     */
1200
    protected function setContentStatus($contentId, $status)
1201
    {
1202
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1203
        $query = $this->handler->createUpdateQuery();
1204
        $query->update(
1205
            'ezcontentobject'
1206
        )->set(
1207
            $this->handler->quoteColumn('status'),
1208
            $query->bindValue($status, null, \PDO::PARAM_INT)
1209
        )->where(
1210
            $query->expr->eq(
1211
                $this->handler->quoteColumn('id'),
1212
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1213
            )
1214
        );
1215
        $query->prepare()->execute();
1216
    }
1217
1218
    /**
1219
     * Loads trash data specified by location ID.
1220
     *
1221
     * @param mixed $locationId
1222
     *
1223
     * @return array
1224
     */
1225 View Code Duplication
    public function loadTrashByLocation($locationId)
1226
    {
1227
        $query = $this->handler->createSelectQuery();
1228
        $query
1229
            ->select('*')
1230
            ->from($this->handler->quoteTable('ezcontentobject_trash'))
1231
            ->where(
1232
                $query->expr->eq(
1233
                    $this->handler->quoteColumn('node_id'),
1234
                    $query->bindValue($locationId)
1235
                )
1236
            );
1237
        $statement = $query->prepare();
1238
        $statement->execute();
1239
1240
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1241
            return $row;
1242
        }
1243
1244
        throw new NotFound('trash', $locationId);
1245
    }
1246
1247
    /**
1248
     * List trashed items.
1249
     *
1250
     * @param int $offset
1251
     * @param int $limit
1252
     * @param array $sort
1253
     *
1254
     * @return array
1255
     */
1256
    public function listTrashed($offset, $limit, array $sort = null)
1257
    {
1258
        $query = $this->handler->createSelectQuery();
1259
        $query
1260
            ->select('*')
1261
            ->from($this->handler->quoteTable('ezcontentobject_trash'));
1262
1263
        $sort = $sort ?: array();
1264
        foreach ($sort as $condition) {
1265
            $sortDirection = $condition->direction === Query::SORT_ASC ? SelectQuery::ASC : SelectQuery::DESC;
1266
            switch (true) {
1267
                case $condition instanceof SortClause\Location\Depth:
1268
                    $query->orderBy('depth', $sortDirection);
1269
                    break;
1270
1271
                case $condition instanceof SortClause\Location\Path:
1272
                    $query->orderBy('path_string', $sortDirection);
1273
                    break;
1274
1275
                case $condition instanceof SortClause\Location\Priority:
1276
                    $query->orderBy('priority', $sortDirection);
1277
                    break;
1278
1279
                default:
1280
                    // Only handle location related sort clauses. The others
1281
                    // require data aggregation which is not sensible here.
1282
                    // Since also criteria are yet ignored, because they are
1283
                    // simply not used yet in eZ Publish, we skip that for now.
1284
                    throw new RuntimeException('Unhandled sort clause: ' . get_class($condition));
1285
            }
1286
        }
1287
1288
        if ($limit !== null) {
1289
            $query->limit($limit, $offset);
1290
        }
1291
1292
        $statement = $query->prepare();
1293
        $statement->execute();
1294
1295
        $rows = array();
1296
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1297
            $rows[] = $row;
1298
        }
1299
1300
        return $rows;
1301
    }
1302
1303
    /**
1304
     * Removes every entries in the trash.
1305
     * Will NOT remove associated content objects nor attributes.
1306
     *
1307
     * Basically truncates ezcontentobject_trash table.
1308
     */
1309
    public function cleanupTrash()
1310
    {
1311
        $query = $this->handler->createDeleteQuery();
1312
        $query->deleteFrom('ezcontentobject_trash');
1313
        $query->prepare()->execute();
1314
    }
1315
1316
    /**
1317
     * Removes trashed element identified by $id from trash.
1318
     * Will NOT remove associated content object nor attributes.
1319
     *
1320
     * @param int $id The trashed location Id
1321
     */
1322
    public function removeElementFromTrash($id)
1323
    {
1324
        $query = $this->handler->createDeleteQuery();
1325
        $query
1326
            ->deleteFrom('ezcontentobject_trash')
1327
            ->where(
1328
                $query->expr->eq(
1329
                    $this->handler->quoteColumn('node_id'),
1330
                    $query->bindValue($id, null, \PDO::PARAM_INT)
1331
                )
1332
            );
1333
        $query->prepare()->execute();
1334
    }
1335
1336
    /**
1337
     * Set section on all content objects in the subtree.
1338
     *
1339
     * @param string $pathString
1340
     * @param int $sectionId
1341
     *
1342
     * @return bool
1343
     */
1344
    public function setSectionForSubtree($pathString, $sectionId)
1345
    {
1346
        $selectContentIdsQuery = $this->connection->createQueryBuilder();
1347
        $selectContentIdsQuery
1348
            ->select('t.contentobject_id')
1349
            ->from('ezcontentobject_tree', 't')
1350
            ->where(
1351
                $selectContentIdsQuery->expr()->like(
1352
                    't.path_string',
1353
                    $selectContentIdsQuery->createPositionalParameter("{$pathString}%")
1354
                )
1355
            );
1356
1357
        $contentIds = array_map(
1358
            'intval',
1359
            $selectContentIdsQuery->execute()->fetchAll(PDO::FETCH_COLUMN)
1360
        );
1361
1362
        if (empty($contentIds)) {
1363
            return false;
1364
        }
1365
1366
        $updateSectionQuery = $this->connection->createQueryBuilder();
1367
        $updateSectionQuery
1368
            ->update('ezcontentobject')
1369
            ->set(
1370
                'section_id',
1371
                $updateSectionQuery->createPositionalParameter($sectionId, PDO::PARAM_INT)
1372
            )
1373
            ->where(
1374
                $updateSectionQuery->expr()->in(
1375
                    'id',
1376
                    $contentIds
1377
                )
1378
            );
1379
        $affectedRows = $updateSectionQuery->execute();
1380
1381
        return $affectedRows > 0;
1382
    }
1383
1384
    /**
1385
     * Returns how many locations given content object identified by $contentId has.
1386
     *
1387
     * @param int $contentId
1388
     *
1389
     * @return int
1390
     */
1391
    public function countLocationsByContentId($contentId)
1392
    {
1393
        $q = $this->handler->createSelectQuery();
1394
        $q
1395
            ->select(
1396
                $q->alias($q->expr->count('*'), 'count')
1397
            )
1398
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
1399
            ->where(
1400
                $q->expr->eq(
1401
                    $this->handler->quoteColumn('contentobject_id'),
1402
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1403
                )
1404
            );
1405
        $stmt = $q->prepare();
1406
        $stmt->execute();
1407
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1408
1409
        return (int)$res[0]['count'];
1410
    }
1411
1412
    /**
1413
     * Changes main location of content identified by given $contentId to location identified by given $locationId.
1414
     *
1415
     * Updates ezcontentobject_tree table for the given $contentId and eznode_assignment table for the given
1416
     * $contentId, $parentLocationId and $versionNo
1417
     *
1418
     * @param mixed $contentId
1419
     * @param mixed $locationId
1420
     * @param mixed $versionNo version number, needed to update eznode_assignment table
1421
     * @param mixed $parentLocationId parent location of location identified by $locationId, needed to update
1422
     *        eznode_assignment table
1423
     */
1424
    public function changeMainLocation($contentId, $locationId, $versionNo, $parentLocationId)
1425
    {
1426
        // Update ezcontentobject_tree table
1427
        $q = $this->handler->createUpdateQuery();
1428
        $q->update(
1429
            $this->handler->quoteTable('ezcontentobject_tree')
1430
        )->set(
1431
            $this->handler->quoteColumn('main_node_id'),
1432
            $q->bindValue($locationId, null, \PDO::PARAM_INT)
1433
        )->where(
1434
            $q->expr->eq(
1435
                $this->handler->quoteColumn('contentobject_id'),
1436
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
1437
            )
1438
        );
1439
        $q->prepare()->execute();
1440
1441
        // Erase is_main in eznode_assignment table
1442
        $q = $this->handler->createUpdateQuery();
1443
        $q->update(
1444
            $this->handler->quoteTable('eznode_assignment')
1445
        )->set(
1446
            $this->handler->quoteColumn('is_main'),
1447
            $q->bindValue(0, null, \PDO::PARAM_INT)
1448
        )->where(
1449
            $q->expr->lAnd(
1450
                $q->expr->eq(
1451
                    $this->handler->quoteColumn('contentobject_id'),
1452
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1453
                ),
1454
                $q->expr->eq(
1455
                    $this->handler->quoteColumn('contentobject_version'),
1456
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1457
                ),
1458
                $q->expr->neq(
1459
                    $this->handler->quoteColumn('parent_node'),
1460
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1461
                )
1462
            )
1463
        );
1464
        $q->prepare()->execute();
1465
1466
        // Set new is_main in eznode_assignment table
1467
        $q = $this->handler->createUpdateQuery();
1468
        $q->update(
1469
            $this->handler->quoteTable('eznode_assignment')
1470
        )->set(
1471
            $this->handler->quoteColumn('is_main'),
1472
            $q->bindValue(1, null, \PDO::PARAM_INT)
1473
        )->where(
1474
            $q->expr->lAnd(
1475
                $q->expr->eq(
1476
                    $this->handler->quoteColumn('contentobject_id'),
1477
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1478
                ),
1479
                $q->expr->eq(
1480
                    $this->handler->quoteColumn('contentobject_version'),
1481
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1482
                ),
1483
                $q->expr->eq(
1484
                    $this->handler->quoteColumn('parent_node'),
1485
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1486
                )
1487
            )
1488
        );
1489
        $q->prepare()->execute();
1490
    }
1491
1492
    /**
1493
     * Get the total number of all Locations, except the Root node.
1494
     *
1495
     * @see loadAllLocationsData
1496
     *
1497
     * @return int
1498
     */
1499
    public function countAllLocations()
1500
    {
1501
        $query = $this->getAllLocationsQueryBuilder(['count(node_id)']);
1502
1503
        $statement = $query->execute();
1504
1505
        return (int) $statement->fetch(PDO::FETCH_COLUMN);
1506
    }
1507
1508
    /**
1509
     * Load data of every Location, except the Root node.
1510
     *
1511
     * @param int $offset Paginator offset
1512
     * @param int $limit Paginator limit
1513
     *
1514
     * @return array
1515
     */
1516
    public function loadAllLocationsData($offset, $limit)
1517
    {
1518
        $query = $this
1519
            ->getAllLocationsQueryBuilder(
1520
                [
1521
                    'node_id',
1522
                    'priority',
1523
                    'is_hidden',
1524
                    'is_invisible',
1525
                    'remote_id',
1526
                    'contentobject_id',
1527
                    'parent_node_id',
1528
                    'path_identification_string',
1529
                    'path_string',
1530
                    'depth',
1531
                    'sort_field',
1532
                    'sort_order',
1533
                ]
1534
            )
1535
            ->setFirstResult($offset)
1536
            ->setMaxResults($limit)
1537
            ->orderBy('depth', 'ASC')
1538
            ->addOrderBy('node_id', 'ASC')
1539
        ;
1540
1541
        $statement = $query->execute();
1542
1543
        return $statement->fetchAll(PDO::FETCH_ASSOC);
1544
    }
1545
1546
    /**
1547
     * Get Query Builder for fetching data of all Locations except the Root node.
1548
     *
1549
     * @todo Align with createNodeQueryBuilder, removing the need for both(?)
1550
     *
1551
     * @param array $columns list of columns to fetch
1552
     *
1553
     * @return \Doctrine\DBAL\Query\QueryBuilder
1554
     */
1555
    private function getAllLocationsQueryBuilder(array $columns)
1556
    {
1557
        $query = $this->connection->createQueryBuilder();
1558
        $query
1559
            ->select($columns)
1560
            ->from('ezcontentobject_tree')
1561
            ->where($query->expr()->neq('node_id', 'parent_node_id'))
1562
        ;
1563
1564
        return $query;
1565
    }
1566
1567
    /**
1568
     * Create QueryBuilder for selecting node data.
1569
     *
1570
     * @param array|null $translations Filters on language mask of content if provided.
1571
     * @param bool $useAlwaysAvailable Respect always available flag on content when filtering on $translations.
1572
     *
1573
     * @return \Doctrine\DBAL\Query\QueryBuilder
1574
     */
1575
    private function createNodeQueryBuilder(array $translations = null, bool $useAlwaysAvailable = true): QueryBuilder
1576
    {
1577
        $queryBuilder = $this->connection->createQueryBuilder();
1578
        $queryBuilder
1579
            ->select('t.*')
1580
            ->from('ezcontentobject_tree', 't');
1581
1582
        if (!empty($translations)) {
1583
            $dbPlatform = $this->connection->getDatabasePlatform();
1584
            $expr = $queryBuilder->expr();
1585
            $mask = $this->generateLanguageMaskFromLanguageCodes($translations, $useAlwaysAvailable);
1586
1587
            $queryBuilder->innerJoin(
1588
                't',
1589
                'ezcontentobject',
1590
                'c',
1591
                $expr->andX(
1592
                    $expr->eq('t.contentobject_id', 'c.id'),
1593
                    $expr->gt(
1594
                        $dbPlatform->getBitAndComparisonExpression('c.language_mask', $mask),
1595
                        0
1596
                    )
1597
                )
1598
            );
1599
        }
1600
1601
        return $queryBuilder;
1602
    }
1603
1604
    /**
1605
     * Generates a language mask for $translations argument.
1606
     *
1607
     * @todo Move logic to languageMaskGenerator in master.
1608
     */
1609 View Code Duplication
    private function generateLanguageMaskFromLanguageCodes(array $translations, bool $useAlwaysAvailable = true): int
1610
    {
1611
        $languages = [];
1612
        foreach ($translations as $translation) {
1613
            if (isset($languages[$translation])) {
1614
                continue;
1615
            }
1616
1617
            $languages[$translation] = true;
1618
        }
1619
1620
        if ($useAlwaysAvailable) {
1621
            $languages['always-available'] = true;
1622
        }
1623
1624
        return $this->languageMaskGenerator->generateLanguageMask($languages);
1625
    }
1626
}
1627