Completed
Push — feature-EZP-25696 ( d6b5d6...ab4954 )
by André
356:19 queued 326:15
created

Content::loadContentVersions()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 2
dl 0
loc 26
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the Content controller 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
 * @version //autogentag//
10
 */
11
namespace eZ\Publish\Core\REST\Server\Controller;
12
13
use eZ\Publish\Core\REST\Common\Message;
14
use eZ\Publish\Core\REST\Common\Exceptions;
15
use eZ\Publish\Core\REST\Server\Values;
16
use eZ\Publish\Core\REST\Server\Controller as RestController;
17
use eZ\Publish\API\Repository\Values\Content\Relation;
18
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
19
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
20
use eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException;
21
use eZ\Publish\API\Repository\Exceptions\ContentValidationException;
22
use eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException;
23
use eZ\Publish\Core\REST\Server\Exceptions\BadRequestException;
24
use eZ\Publish\Core\REST\Server\Exceptions\ContentFieldValidationException as RESTContentFieldValidationException;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpKernel\HttpKernelInterface;
27
28
/**
29
 * Content controller.
30
 */
31
class Content extends RestController
32
{
33
    /**
34
     * Loads a content info by remote ID.
35
     *
36
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException
37
     *
38
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
39
     */
40 View Code Duplication
    public function redirectContent(Request $request)
41
    {
42
        if (!$request->query->has('remoteId')) {
43
            throw new BadRequestException("'remoteId' parameter is required.");
44
        }
45
46
        $contentInfo = $this->repository->getContentService()->loadContentInfoByRemoteId(
47
            $request->query->get('remoteId')
48
        );
49
50
        return new Values\TemporaryRedirect(
51
            $this->router->generate(
52
                'ezpublish_rest_loadContent',
53
                array(
54
                    'contentId' => $contentInfo->id,
55
                )
56
            )
57
        );
58
    }
59
60
    /**
61
     * Loads a content info, potentially with the current version embedded.
62
     *
63
     * @param mixed $contentId
64
     * @param \Symfony\Component\HttpFoundation\Request $request
65
     *
66
     * @return \eZ\Publish\Core\REST\Server\Values\RestContent
67
     */
68
    public function loadContent($contentId, Request $request)
69
    {
70
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
71
72
        $mainLocation = null;
73
        if (!empty($contentInfo->mainLocationId)) {
74
            $mainLocation = $this->repository->getLocationService()->loadLocation($contentInfo->mainLocationId);
75
        }
76
77
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
78
79
        $contentVersion = null;
80
        $relations = null;
81
        if ($this->getMediaType($request) === 'application/vnd.ez.api.content') {
82
            $languages = null;
83
            if ($request->query->has('languages')) {
84
                $languages = explode(',', $request->query->get('languages'));
85
            }
86
87
            $contentVersion = $this->repository->getContentService()->loadContent($contentId, $languages);
88
            $relations = $this->repository->getContentService()->loadRelations($contentVersion->getVersionInfo());
89
        }
90
91
        $restContent = new Values\RestContent(
92
            $contentInfo,
93
            $mainLocation,
94
            $contentVersion,
95
            $contentType,
96
            $relations,
97
            $request->getPathInfo()
98
        );
99
100
        if ($contentInfo->mainLocationId === null) {
101
            return $restContent;
102
        }
103
104
        return new Values\CachedValue(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \eZ\Publish\C...Info->mainLocationId)); (eZ\Publish\Core\REST\Server\Values\CachedValue) is incompatible with the return type documented by eZ\Publish\Core\REST\Ser...er\Content::loadContent of type eZ\Publish\Core\REST\Server\Values\RestContent.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
105
            $restContent,
106
            array('location' => $contentInfo->mainLocationId)
107
        );
108
    }
109
110
    /**
111
     * Updates a content's metadata.
112
     *
113
     * @param mixed $contentId
114
     *
115
     * @return \eZ\Publish\Core\REST\Server\Values\RestContent
116
     */
117
    public function updateContentMetadata($contentId, Request $request)
118
    {
119
        $updateStruct = $this->inputDispatcher->parse(
120
            new Message(
121
                array('Content-Type' => $request->headers->get('Content-Type')),
122
                $request->getContent()
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, eZ\Publish\Core\REST\Common\Message::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
123
            )
124
        );
125
126
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
127
128
        // update section
129
        if ($updateStruct->sectionId !== null) {
0 ignored issues
show
Documentation introduced by
The property sectionId does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
130
            $section = $this->repository->getSectionService()->loadSection($updateStruct->sectionId);
0 ignored issues
show
Documentation introduced by
The property sectionId does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
131
            $this->repository->getSectionService()->assignSection($contentInfo, $section);
132
            $updateStruct->sectionId = null;
0 ignored issues
show
Documentation introduced by
The property sectionId does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
133
        }
134
135
        // @todo Consider refactoring! ContentService::updateContentMetadata throws the same exception
136
        // in case the updateStruct is empty and if remoteId already exists. Since REST version of update struct
137
        // includes section ID in addition to other fields, we cannot throw exception if only sectionId property
138
        // is set, so we must skip updating content in that case instead of allowing propagation of the exception.
139
        foreach ($updateStruct as $propertyName => $propertyValue) {
0 ignored issues
show
Bug introduced by
The expression $updateStruct of type object<eZ\Publish\API\Re...ory\Values\ValueObject> is not traversable.
Loading history...
140
            if ($propertyName !== 'sectionId' && $propertyValue !== null) {
141
                // update content
142
                $this->repository->getContentService()->updateContentMetadata($contentInfo, $updateStruct);
0 ignored issues
show
Compatibility introduced by
$updateStruct of type object<eZ\Publish\API\Re...ory\Values\ValueObject> is not a sub-type of object<eZ\Publish\API\Re...ntMetadataUpdateStruct>. It seems like you assume a child class of the class eZ\Publish\API\Repository\Values\ValueObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
143
                $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
144
                break;
145
            }
146
        }
147
148
        try {
149
            $locationInfo = $this->repository->getLocationService()->loadLocation($contentInfo->mainLocationId);
150
        } catch (NotFoundException $e) {
151
            $locationInfo = null;
152
        }
153
154
        return new Values\RestContent(
155
            $contentInfo,
156
            $locationInfo
157
        );
158
    }
159
160
    /**
161
     * Loads a specific version of a given content object.
162
     *
163
     * @param mixed $contentId
164
     *
165
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
166
     */
167 View Code Duplication
    public function redirectCurrentVersion($contentId)
168
    {
169
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
170
171
        return new Values\TemporaryRedirect(
172
            $this->router->generate(
173
                'ezpublish_rest_loadContentInVersion',
174
                array(
175
                    'contentId' => $contentId,
176
                    'versionNumber' => $contentInfo->currentVersionNo,
177
                )
178
            )
179
        );
180
    }
181
182
    /**
183
     * Loads a specific version of a given content object.
184
     *
185
     * @param mixed $contentId
186
     * @param int $versionNumber
187
     *
188
     * @return \eZ\Publish\Core\REST\Server\Values\Version
189
     */
190
    public function loadContentInVersion($contentId, $versionNumber, Request $request)
191
    {
192
        $languages = null;
193
        if ($request->query->has('languages')) {
194
            $languages = explode(',', $request->query->get('languages'));
195
        }
196
197
        $content = $this->repository->getContentService()->loadContent(
198
            $contentId,
199
            $languages,
200
            $versionNumber
201
        );
202
        $contentType = $this->repository->getContentTypeService()->loadContentType(
203
            $content->getVersionInfo()->getContentInfo()->contentTypeId
204
        );
205
206
        $versionValue = new Values\Version(
207
            $content,
208
            $contentType,
209
            $this->repository->getContentService()->loadRelations($content->getVersionInfo()),
210
            $request->getPathInfo()
211
        );
212
213
        if ($content->contentInfo->mainLocationId === null) {
214
            return $versionValue;
215
        }
216
217
        return new Values\CachedValue(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \eZ\Publish\C...Info->mainLocationId)); (eZ\Publish\Core\REST\Server\Values\CachedValue) is incompatible with the return type documented by eZ\Publish\Core\REST\Ser...t::loadContentInVersion of type eZ\Publish\Core\REST\Server\Values\Version.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
218
            $versionValue,
219
            array('location' => $content->contentInfo->mainLocationId)
220
        );
221
    }
222
223
    /**
224
     * Creates a new content draft assigned to the authenticated user.
225
     * If a different userId is given in the input it is assigned to the
226
     * given user but this required special rights for the authenticated
227
     * user (this is useful for content staging where the transfer process
228
     * does not have to authenticate with the user which created the content
229
     * object in the source server). The user has to publish the content if
230
     * it should be visible.
231
     *
232
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedContent
233
     */
234
    public function createContent(Request $request)
235
    {
236
        $contentCreate = $this->inputDispatcher->parse(
237
            new Message(
238
                array('Content-Type' => $request->headers->get('Content-Type')),
239
                $request->getContent()
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, eZ\Publish\Core\REST\Common\Message::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
240
            )
241
        );
242
243
        try {
244
            $content = $this->repository->getContentService()->createContent(
245
                $contentCreate->contentCreateStruct,
0 ignored issues
show
Documentation introduced by
The property contentCreateStruct does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
246
                array($contentCreate->locationCreateStruct)
0 ignored issues
show
Documentation introduced by
The property locationCreateStruct does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
247
            );
248
        } catch (ContentValidationException $e) {
249
            throw new BadRequestException($e->getMessage());
250
        } catch (ContentFieldValidationException $e) {
251
            throw new RESTContentFieldValidationException($e);
252
        }
253
254
        $contentValue = null;
255
        $contentType = null;
256
        $relations = null;
257
        if ($this->getMediaType($request) === 'application/vnd.ez.api.content') {
258
            $contentValue = $content;
259
            $contentType = $this->repository->getContentTypeService()->loadContentType(
260
                $content->getVersionInfo()->getContentInfo()->contentTypeId
261
            );
262
            $relations = $this->repository->getContentService()->loadRelations($contentValue->getVersionInfo());
263
        }
264
265
        return new Values\CreatedContent(
266
            array(
267
                'content' => new Values\RestContent(
268
                    $content->contentInfo,
269
                    null,
270
                    $contentValue,
271
                    $contentType,
272
                    $relations
273
                ),
274
            )
275
        );
276
    }
277
278
    /**
279
     * The content is deleted. If the content has locations (which is required in 4.x)
280
     * on delete all locations assigned the content object are deleted via delete subtree.
281
     *
282
     * @param mixed $contentId
283
     *
284
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
285
     */
286
    public function deleteContent($contentId)
287
    {
288
        $this->repository->getContentService()->deleteContent(
289
            $this->repository->getContentService()->loadContentInfo($contentId)
290
        );
291
292
        return new Values\NoContent();
293
    }
294
295
    /**
296
     * Creates a new content object as copy under the given parent location given in the destination header.
297
     *
298
     * @param mixed $contentId
299
     *
300
     * @return \eZ\Publish\Core\REST\Server\Values\ResourceCreated
301
     */
302
    public function copyContent($contentId, Request $request)
303
    {
304
        $destination = $request->headers->get('Destination');
305
306
        $parentLocationParts = explode('/', $destination);
307
        $copiedContent = $this->repository->getContentService()->copyContent(
308
            $this->repository->getContentService()->loadContentInfo($contentId),
309
            $this->repository->getLocationService()->newLocationCreateStruct(array_pop($parentLocationParts))
310
        );
311
312
        return new Values\ResourceCreated(
313
            $this->router->generate(
314
                'ezpublish_rest_loadContent',
315
                array('contentId' => $copiedContent->id)
316
            )
317
        );
318
    }
319
320
    /**
321
     * Returns a list of all versions of the content. This method does not
322
     * include fields and relations in the Version elements of the response.
323
     *
324
     * @param mixed $contentId
325
     *
326
     * @return \eZ\Publish\Core\REST\Server\Values\VersionList
327
     */
328
    public function loadContentVersions($contentId, Request $request)
329
    {
330
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
331
332
        $versionList = new Values\VersionList(
333
            $this->repository->getContentService()->loadVersions($contentInfo),
334
            $request->getPathInfo()
335
        );
336
337
        if ($contentInfo->mainLocationId === null) {
338
            return new Values\CachedValue(
339
                $versionList,
340
                [
341
                    'content' => $contentInfo->id,
342
                ]
343
            );
344
        }
345
346
        return new Values\CachedValue(
347
            $versionList,
348
            [
349
                'location' => $contentInfo->mainLocationId,
350
                'content' => $contentInfo->id,
351
            ]
352
        );
353
    }
354
355
    /**
356
     * The version is deleted.
357
     *
358
     * @param mixed $contentId
359
     * @param mixed $versionNumber
360
     *
361
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
362
     *
363
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
364
     */
365 View Code Duplication
    public function deleteContentVersion($contentId, $versionNumber)
366
    {
367
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
368
            $this->repository->getContentService()->loadContentInfo($contentId),
369
            $versionNumber
370
        );
371
372
        if ($versionInfo->status === VersionInfo::STATUS_PUBLISHED) {
373
            throw new ForbiddenException('Version in status PUBLISHED cannot be deleted');
374
        }
375
376
        $this->repository->getContentService()->deleteVersion(
377
            $versionInfo
378
        );
379
380
        return new Values\NoContent();
381
    }
382
383
    /**
384
     * The system creates a new draft version as a copy from the given version.
385
     *
386
     * @param mixed $contentId
387
     * @param mixed $versionNumber
388
     *
389
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedVersion
390
     */
391
    public function createDraftFromVersion($contentId, $versionNumber)
392
    {
393
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
394
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
395
        $contentDraft = $this->repository->getContentService()->createContentDraft(
396
            $contentInfo,
397
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
398
        );
399
400
        return new Values\CreatedVersion(
401
            array(
402
                'version' => new Values\Version(
403
                    $contentDraft,
404
                    $contentType,
405
                    $this->repository->getContentService()->loadRelations($contentDraft->getVersionInfo())
406
                ),
407
            )
408
        );
409
    }
410
411
    /**
412
     * The system creates a new draft version as a copy from the current version.
413
     *
414
     * @param mixed $contentId
415
     *
416
     * @throws ForbiddenException if the current version is already a draft
417
     *
418
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedVersion
419
     */
420
    public function createDraftFromCurrentVersion($contentId)
421
    {
422
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
423
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
424
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
425
            $contentInfo
426
        );
427
428
        if ($versionInfo->status === VersionInfo::STATUS_DRAFT) {
429
            throw new ForbiddenException('Current version is already in status DRAFT');
430
        }
431
432
        $contentDraft = $this->repository->getContentService()->createContentDraft($contentInfo);
433
434
        return new Values\CreatedVersion(
435
            array(
436
                'version' => new Values\Version(
437
                    $contentDraft,
438
                    $contentType,
439
                    $this->repository->getContentService()->loadRelations($contentDraft->getVersionInfo())
440
                ),
441
            )
442
        );
443
    }
444
445
    /**
446
     * A specific draft is updated.
447
     *
448
     * @param mixed $contentId
449
     * @param mixed $versionNumber
450
     *
451
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
452
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException
453
     *
454
     * @return \eZ\Publish\Core\REST\Server\Values\Version
455
     */
456
    public function updateVersion($contentId, $versionNumber, Request $request)
457
    {
458
        $contentUpdateStruct = $this->inputDispatcher->parse(
459
            new Message(
460
                array(
461
                    'Content-Type' => $request->headers->get('Content-Type'),
462
                    'Url' => $this->router->generate(
463
                        'ezpublish_rest_updateVersion',
464
                        array(
465
                            'contentId' => $contentId,
466
                            'versionNumber' => $versionNumber,
467
                        )
468
                    ),
469
                ),
470
                $request->getContent()
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, eZ\Publish\Core\REST\Common\Message::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
471
            )
472
        );
473
474
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
475
            $this->repository->getContentService()->loadContentInfo($contentId),
476
            $versionNumber
477
        );
478
479
        if ($versionInfo->status !== VersionInfo::STATUS_DRAFT) {
480
            throw new ForbiddenException('Only version in status DRAFT can be updated');
481
        }
482
483
        try {
484
            $this->repository->getContentService()->updateContent($versionInfo, $contentUpdateStruct);
0 ignored issues
show
Compatibility introduced by
$contentUpdateStruct of type object<eZ\Publish\API\Re...ory\Values\ValueObject> is not a sub-type of object<eZ\Publish\API\Re...nt\ContentUpdateStruct>. It seems like you assume a child class of the class eZ\Publish\API\Repository\Values\ValueObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
485
        } catch (ContentValidationException $e) {
486
            throw new BadRequestException($e->getMessage());
487
        } catch (ContentFieldValidationException $e) {
488
            throw new RESTContentFieldValidationException($e);
489
        }
490
491
        $languages = null;
492
        if ($request->query->has('languages')) {
493
            $languages = explode(',', $request->query->get('languages'));
494
        }
495
496
        // Reload the content to handle languages GET parameter
497
        $content = $this->repository->getContentService()->loadContent(
498
            $contentId,
499
            $languages,
500
            $versionInfo->versionNo
501
        );
502
        $contentType = $this->repository->getContentTypeService()->loadContentType(
503
            $content->getVersionInfo()->getContentInfo()->contentTypeId
504
        );
505
506
        return new Values\Version(
507
            $content,
508
            $contentType,
509
            $this->repository->getContentService()->loadRelations($content->getVersionInfo()),
510
            $request->getPathInfo()
511
        );
512
    }
513
514
    /**
515
     * The content version is published.
516
     *
517
     * @param mixed $contentId
518
     * @param mixed $versionNumber
519
     *
520
     * @throws ForbiddenException if version $versionNumber isn't a draft
521
     *
522
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
523
     */
524 View Code Duplication
    public function publishVersion($contentId, $versionNumber)
525
    {
526
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
527
            $this->repository->getContentService()->loadContentInfo($contentId),
528
            $versionNumber
529
        );
530
531
        if ($versionInfo->status !== VersionInfo::STATUS_DRAFT) {
532
            throw new ForbiddenException('Only version in status DRAFT can be published');
533
        }
534
535
        $this->repository->getContentService()->publishVersion(
536
            $versionInfo
537
        );
538
539
        return new Values\NoContent();
540
    }
541
542
    /**
543
     * Redirects to the relations of the current version.
544
     *
545
     * @param mixed $contentId
546
     *
547
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
548
     */
549 View Code Duplication
    public function redirectCurrentVersionRelations($contentId)
550
    {
551
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
552
553
        return new Values\TemporaryRedirect(
554
            $this->router->generate(
555
                'ezpublish_rest_redirectCurrentVersionRelations',
556
                array(
557
                    'contentId' => $contentId,
558
                    'versionNumber' => $contentInfo->currentVersionNo,
559
                )
560
            )
561
        );
562
    }
563
564
    /**
565
     * Loads the relations of the given version.
566
     *
567
     * @param mixed $contentId
568
     * @param mixed $versionNumber
569
     *
570
     * @return \eZ\Publish\Core\REST\Server\Values\RelationList
571
     */
572
    public function loadVersionRelations($contentId, $versionNumber, Request $request)
573
    {
574
        $offset = $request->query->has('offset') ? (int)$request->query->get('offset') : 0;
575
        $limit = $request->query->has('limit') ? (int)$request->query->get('limit') : -1;
576
577
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
578
        $relationList = $this->repository->getContentService()->loadRelations(
579
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
580
        );
581
582
        $relationList = array_slice(
583
            $relationList,
584
            $offset >= 0 ? $offset : 0,
585
            $limit >= 0 ? $limit : null
586
        );
587
588
        $relationListValue = new Values\RelationList(
589
            $relationList,
590
            $contentId,
591
            $versionNumber,
592
            $request->getPathInfo()
593
        );
594
595
        if ($contentInfo->mainLocationId === null) {
596
            return $relationListValue;
597
        }
598
599
        return new Values\CachedValue(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \eZ\Publish\C...Info->mainLocationId)); (eZ\Publish\Core\REST\Server\Values\CachedValue) is incompatible with the return type documented by eZ\Publish\Core\REST\Ser...t::loadVersionRelations of type eZ\Publish\Core\REST\Server\Values\RelationList.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
600
            $relationListValue,
601
            array('location' => $contentInfo->mainLocationId)
602
        );
603
    }
604
605
    /**
606
     * Loads a relation for the given content object and version.
607
     *
608
     * @param mixed $contentId
609
     * @param int $versionNumber
610
     * @param mixed $relationId
611
     *
612
     * @throws \eZ\Publish\Core\REST\Common\Exceptions\NotFoundException
613
     *
614
     * @return \eZ\Publish\Core\REST\Server\Values\RestRelation
615
     */
616
    public function loadVersionRelation($contentId, $versionNumber, $relationId, Request $request)
617
    {
618
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
619
        $relationList = $this->repository->getContentService()->loadRelations(
620
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
621
        );
622
623
        foreach ($relationList as $relation) {
624
            if ($relation->id == $relationId) {
625
                $relation = new Values\RestRelation($relation, $contentId, $versionNumber);
626
627
                if ($contentInfo->mainLocationId === null) {
628
                    return $relation;
629
                }
630
631
                return new Values\CachedValue(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \eZ\Publish\C...Info->mainLocationId)); (eZ\Publish\Core\REST\Server\Values\CachedValue) is incompatible with the return type documented by eZ\Publish\Core\REST\Ser...nt::loadVersionRelation of type eZ\Publish\Core\REST\Server\Values\RestRelation.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
632
                    $relation,
633
                    array('location' => $contentInfo->mainLocationId)
634
                );
635
            }
636
        }
637
638
        throw new Exceptions\NotFoundException("Relation not found: '{$request->getPathInfo()}'.");
639
    }
640
641
    /**
642
     * Deletes a relation of the given draft.
643
     *
644
     * @param mixed $contentId
645
     * @param int   $versionNumber
646
     * @param mixed $relationId
647
     *
648
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
649
     * @throws \eZ\Publish\Core\REST\Common\Exceptions\NotFoundException
650
     *
651
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
652
     */
653
    public function removeRelation($contentId, $versionNumber, $relationId, Request $request)
654
    {
655
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
656
            $this->repository->getContentService()->loadContentInfo($contentId),
657
            $versionNumber
658
        );
659
660
        $versionRelations = $this->repository->getContentService()->loadRelations($versionInfo);
661
        foreach ($versionRelations as $relation) {
662
            if ($relation->id == $relationId) {
663
                if ($relation->type !== Relation::COMMON) {
664
                    throw new ForbiddenException('Relation is not of type COMMON');
665
                }
666
667
                if ($versionInfo->status !== VersionInfo::STATUS_DRAFT) {
668
                    throw new ForbiddenException('Relation of type COMMON can only be removed from drafts');
669
                }
670
671
                $this->repository->getContentService()->deleteRelation($versionInfo, $relation->getDestinationContentInfo());
672
673
                return new Values\NoContent();
674
            }
675
        }
676
677
        throw new Exceptions\NotFoundException("Relation not found: '{$request->getPathInfo()}'.");
678
    }
679
680
    /**
681
     * Creates a new relation of type COMMON for the given draft.
682
     *
683
     * @param mixed $contentId
684
     * @param int $versionNumber
685
     *
686
     * @throws ForbiddenException if version $versionNumber isn't a draft
687
     * @throws ForbiddenException if a relation to the same content already exists
688
     *
689
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedRelation
690
     */
691
    public function createRelation($contentId, $versionNumber, Request $request)
692
    {
693
        $destinationContentId = $this->inputDispatcher->parse(
694
            new Message(
695
                array('Content-Type' => $request->headers->get('Content-Type')),
696
                $request->getContent()
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, eZ\Publish\Core\REST\Common\Message::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
697
            )
698
        );
699
700
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
701
        $versionInfo = $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber);
702
        if ($versionInfo->status !== VersionInfo::STATUS_DRAFT) {
703
            throw new ForbiddenException('Relation of type COMMON can only be added to drafts');
704
        }
705
706
        try {
707
            $destinationContentInfo = $this->repository->getContentService()->loadContentInfo($destinationContentId);
0 ignored issues
show
Documentation introduced by
$destinationContentId is of type object<eZ\Publish\API\Re...ory\Values\ValueObject>, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
708
        } catch (NotFoundException $e) {
709
            throw new ForbiddenException($e->getMessage());
710
        }
711
712
        $existingRelations = $this->repository->getContentService()->loadRelations($versionInfo);
713
        foreach ($existingRelations as $existingRelation) {
714
            if ($existingRelation->getDestinationContentInfo()->id == $destinationContentId) {
715
                throw new ForbiddenException('Relation of type COMMON to selected destination content ID already exists');
716
            }
717
        }
718
719
        $relation = $this->repository->getContentService()->addRelation($versionInfo, $destinationContentInfo);
720
721
        return new Values\CreatedRelation(
722
            array(
723
                'relation' => new Values\RestRelation($relation, $contentId, $versionNumber),
724
            )
725
        );
726
    }
727
728
    /**
729
     * Creates and executes a content view.
730
     *
731
     * @deprecated Since platform 1.0. Forwards the request to the new /views location, but returns a 301.
732
     *
733
     * @return \eZ\Publish\Core\REST\Server\Values\RestExecutedView
734
     */
735
    public function createView()
736
    {
737
        $response = $this->forward('ezpublish_rest.controller.views:createView');
738
739
        // Add 301 status code and location href
740
        $response->setStatusCode(301);
741
        $response->headers->set('Location', $this->router->generate('ezpublish_rest_views_create'));
742
743
        return $response;
744
    }
745
746
    /**
747
     * @param string $controller
748
     *
749
     * @return \Symfony\Component\HttpFoundation\Response
750
     */
751
    protected function forward($controller)
752
    {
753
        $path['_controller'] = $controller;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$path was never initialized. Although not strictly required by PHP, it is generally a good practice to add $path = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
754
        $subRequest = $this->container->get('request_stack')->getCurrentRequest()->duplicate(null, null, $path);
755
756
        return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
757
    }
758
}
759