Completed
Push — master ( f2dfe9...84d91c )
by André
35:37 queued 16:15
created

DoctrineDatabase::generateLanguageMaskFromLanguageCodes()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17

Duplication

Lines 17
Ratio 100 %

Importance

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