Completed
Push — master ( b18ea5...87a68d )
by André
133:56 queued 114:49
created

Content::createDraftFromCurrentVersion()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 14

Duplication

Lines 24
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
eloc 14
nc 2
nop 1
dl 24
loc 24
rs 8.9713
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
namespace eZ\Publish\Core\REST\Server\Controller;
10
11
use eZ\Publish\Core\REST\Common\Message;
12
use eZ\Publish\Core\REST\Common\Exceptions;
13
use eZ\Publish\Core\REST\Server\Values;
14
use eZ\Publish\Core\REST\Server\Controller as RestController;
15
use eZ\Publish\API\Repository\Values\Content\Relation;
16
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
17
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
18
use eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException;
19
use eZ\Publish\API\Repository\Exceptions\ContentValidationException;
20
use eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException;
21
use eZ\Publish\Core\REST\Server\Exceptions\BadRequestException;
22
use eZ\Publish\Core\REST\Server\Exceptions\ContentFieldValidationException as RESTContentFieldValidationException;
23
use eZ\Publish\Core\REST\Server\Values\RestContentCreateStruct;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\HttpKernel\HttpKernelInterface;
26
27
/**
28
 * Content controller.
29
 */
30
class Content extends RestController
31
{
32
    /**
33
     * Loads a content info by remote ID.
34
     *
35
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException
36
     *
37
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
38
     */
39 View Code Duplication
    public function redirectContent(Request $request)
40
    {
41
        if (!$request->query->has('remoteId')) {
42
            throw new BadRequestException("'remoteId' parameter is required.");
43
        }
44
45
        $contentInfo = $this->repository->getContentService()->loadContentInfoByRemoteId(
46
            $request->query->get('remoteId')
47
        );
48
49
        return new Values\TemporaryRedirect(
50
            $this->router->generate(
51
                'ezpublish_rest_loadContent',
52
                array(
53
                    'contentId' => $contentInfo->id,
54
                )
55
            )
56
        );
57
    }
58
59
    /**
60
     * Loads a content info, potentially with the current version embedded.
61
     *
62
     * @param mixed $contentId
63
     * @param \Symfony\Component\HttpFoundation\Request $request
64
     *
65
     * @return \eZ\Publish\Core\REST\Server\Values\RestContent
66
     */
67
    public function loadContent($contentId, Request $request)
68
    {
69
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
70
71
        $mainLocation = null;
72
        if (!empty($contentInfo->mainLocationId)) {
73
            $mainLocation = $this->repository->getLocationService()->loadLocation($contentInfo->mainLocationId);
74
        }
75
76
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
77
78
        $contentVersion = null;
79
        $relations = null;
80
        if ($this->getMediaType($request) === 'application/vnd.ez.api.content') {
81
            $languages = null;
82
            if ($request->query->has('languages')) {
83
                $languages = explode(',', $request->query->get('languages'));
84
            }
85
86
            $contentVersion = $this->repository->getContentService()->loadContent($contentId, $languages);
87
            $relations = $this->repository->getContentService()->loadRelations($contentVersion->getVersionInfo());
88
        }
89
90
        $restContent = new Values\RestContent(
91
            $contentInfo,
92
            $mainLocation,
93
            $contentVersion,
94
            $contentType,
95
            $relations,
96
            $request->getPathInfo()
97
        );
98
99
        if ($contentInfo->mainLocationId === null) {
100
            return $restContent;
101
        }
102
103
        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...
104
            $restContent,
105
            array('locationId' => $contentInfo->mainLocationId)
106
        );
107
    }
108
109
    /**
110
     * Updates a content's metadata.
111
     *
112
     * @param mixed $contentId
113
     *
114
     * @return \eZ\Publish\Core\REST\Server\Values\RestContent
115
     */
116
    public function updateContentMetadata($contentId, Request $request)
117
    {
118
        $updateStruct = $this->inputDispatcher->parse(
119
            new Message(
120
                array('Content-Type' => $request->headers->get('Content-Type')),
121
                $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...
122
            )
123
        );
124
125
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
126
127
        // update section
128
        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...
129
            $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...
130
            $this->repository->getSectionService()->assignSection($contentInfo, $section);
131
            $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...
132
        }
133
134
        // @todo Consider refactoring! ContentService::updateContentMetadata throws the same exception
135
        // in case the updateStruct is empty and if remoteId already exists. Since REST version of update struct
136
        // includes section ID in addition to other fields, we cannot throw exception if only sectionId property
137
        // is set, so we must skip updating content in that case instead of allowing propagation of the exception.
138
        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...
139
            if ($propertyName !== 'sectionId' && $propertyValue !== null) {
140
                // update content
141
                $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...
142
                $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
143
                break;
144
            }
145
        }
146
147
        try {
148
            $locationInfo = $this->repository->getLocationService()->loadLocation($contentInfo->mainLocationId);
149
        } catch (NotFoundException $e) {
150
            $locationInfo = null;
151
        }
152
153
        return new Values\RestContent(
154
            $contentInfo,
155
            $locationInfo
156
        );
157
    }
158
159
    /**
160
     * Loads a specific version of a given content object.
161
     *
162
     * @param mixed $contentId
163
     *
164
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
165
     */
166 View Code Duplication
    public function redirectCurrentVersion($contentId)
167
    {
168
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
169
170
        return new Values\TemporaryRedirect(
171
            $this->router->generate(
172
                'ezpublish_rest_loadContentInVersion',
173
                array(
174
                    'contentId' => $contentId,
175
                    'versionNumber' => $contentInfo->currentVersionNo,
176
                )
177
            )
178
        );
179
    }
180
181
    /**
182
     * Loads a specific version of a given content object.
183
     *
184
     * @param mixed $contentId
185
     * @param int $versionNumber
186
     *
187
     * @return \eZ\Publish\Core\REST\Server\Values\Version
188
     */
189
    public function loadContentInVersion($contentId, $versionNumber, Request $request)
190
    {
191
        $languages = null;
192
        if ($request->query->has('languages')) {
193
            $languages = explode(',', $request->query->get('languages'));
194
        }
195
196
        $content = $this->repository->getContentService()->loadContent(
197
            $contentId,
198
            $languages,
199
            $versionNumber
200
        );
201
        $contentType = $this->repository->getContentTypeService()->loadContentType(
202
            $content->getVersionInfo()->getContentInfo()->contentTypeId
203
        );
204
205
        $versionValue = new Values\Version(
206
            $content,
207
            $contentType,
208
            $this->repository->getContentService()->loadRelations($content->getVersionInfo()),
209
            $request->getPathInfo()
210
        );
211
212
        if ($content->contentInfo->mainLocationId === null || $content->versionInfo->status === VersionInfo::STATUS_DRAFT) {
213
            return $versionValue;
214
        }
215
216
        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...
217
            $versionValue,
218
            array('locationId' => $content->contentInfo->mainLocationId)
219
        );
220
    }
221
222
    /**
223
     * Creates a new content draft assigned to the authenticated user.
224
     * If a different userId is given in the input it is assigned to the
225
     * given user but this required special rights for the authenticated
226
     * user (this is useful for content staging where the transfer process
227
     * does not have to authenticate with the user which created the content
228
     * object in the source server). The user has to publish the content if
229
     * it should be visible.
230
     *
231
     * @param \Symfony\Component\HttpFoundation\Request $request
232
     *
233
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedContent
234
     */
235
    public function createContent(Request $request)
236
    {
237
        $contentCreate = $this->parseCreateContentRequest($request);
238
239
        return $this->doCreateContent($request, $contentCreate);
0 ignored issues
show
Compatibility introduced by
$contentCreate of type object<eZ\Publish\API\Re...ory\Values\ValueObject> is not a sub-type of object<eZ\Publish\Core\R...estContentCreateStruct>. 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...
240
    }
241
242
    /**
243
     * The content is deleted. If the content has locations (which is required in 4.x)
244
     * on delete all locations assigned the content object are deleted via delete subtree.
245
     *
246
     * @param mixed $contentId
247
     *
248
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
249
     */
250
    public function deleteContent($contentId)
251
    {
252
        $this->repository->getContentService()->deleteContent(
253
            $this->repository->getContentService()->loadContentInfo($contentId)
254
        );
255
256
        return new Values\NoContent();
257
    }
258
259
    /**
260
     * Creates a new content object as copy under the given parent location given in the destination header.
261
     *
262
     * @param mixed $contentId
263
     *
264
     * @return \eZ\Publish\Core\REST\Server\Values\ResourceCreated
265
     */
266
    public function copyContent($contentId, Request $request)
267
    {
268
        $destination = $request->headers->get('Destination');
269
270
        $parentLocationParts = explode('/', $destination);
271
        $copiedContent = $this->repository->getContentService()->copyContent(
272
            $this->repository->getContentService()->loadContentInfo($contentId),
273
            $this->repository->getLocationService()->newLocationCreateStruct(array_pop($parentLocationParts))
274
        );
275
276
        return new Values\ResourceCreated(
277
            $this->router->generate(
278
                'ezpublish_rest_loadContent',
279
                array('contentId' => $copiedContent->id)
280
            )
281
        );
282
    }
283
284
    /**
285
     * Deletes a translation from all the Versions of the given Content Object.
286
     *
287
     * If any non-published Version contains only the Translation to be deleted, that entire Version will be deleted
288
     *
289
     * @param int $contentId
290
     * @param string $languageCode
291
     *
292
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
293
     *
294
     * @throws \Exception
295
     */
296
    public function deleteContentTranslation($contentId, $languageCode)
297
    {
298
        $contentService = $this->repository->getContentService();
299
300
        $this->repository->beginTransaction();
301
        try {
302
            $contentInfo = $contentService->loadContentInfo($contentId);
303
            $contentService->deleteTranslation(
304
                $contentInfo,
305
                $languageCode
306
            );
307
308
            $this->repository->commit();
309
310
            return new Values\NoContent();
311
        } catch (\Exception $e) {
312
            $this->repository->rollback();
313
            throw $e;
314
        }
315
    }
316
317
    /**
318
     * Returns a list of all versions of the content. This method does not
319
     * include fields and relations in the Version elements of the response.
320
     *
321
     * @param mixed $contentId
322
     *
323
     * @return \eZ\Publish\Core\REST\Server\Values\VersionList
324
     */
325
    public function loadContentVersions($contentId, Request $request)
326
    {
327
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
328
329
        return new Values\VersionList(
330
            $this->repository->getContentService()->loadVersions($contentInfo),
331
            $request->getPathInfo()
332
        );
333
    }
334
335
    /**
336
     * The version is deleted.
337
     *
338
     * @param mixed $contentId
339
     * @param mixed $versionNumber
340
     *
341
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
342
     *
343
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
344
     */
345 View Code Duplication
    public function deleteContentVersion($contentId, $versionNumber)
346
    {
347
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
348
            $this->repository->getContentService()->loadContentInfo($contentId),
349
            $versionNumber
350
        );
351
352
        if ($versionInfo->isPublished()) {
353
            throw new ForbiddenException('Version in status PUBLISHED cannot be deleted');
354
        }
355
356
        $this->repository->getContentService()->deleteVersion(
357
            $versionInfo
358
        );
359
360
        return new Values\NoContent();
361
    }
362
363
    /**
364
     * Remove the given Translation from the given Version Draft.
365
     *
366
     * @param int $contentId
367
     * @param int $versionNumber
368
     * @param string $languageCode
369
     *
370
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
371
     *
372
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
373
     */
374
    public function deleteTranslationFromDraft($contentId, $versionNumber, $languageCode)
375
    {
376
        $contentService = $this->repository->getContentService();
377
        $versionInfo = $contentService->loadVersionInfoById($contentId, $versionNumber);
378
379
        if (!$versionInfo->isDraft()) {
380
            throw new ForbiddenException('Translation can be deleted from DRAFT Version only');
381
        }
382
383
        $contentService->deleteTranslationFromDraft($versionInfo, $languageCode);
384
385
        return new Values\NoContent();
386
    }
387
388
    /**
389
     * The system creates a new draft version as a copy from the given version.
390
     *
391
     * @param mixed $contentId
392
     * @param mixed $versionNumber
393
     *
394
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedVersion
395
     */
396 View Code Duplication
    public function createDraftFromVersion($contentId, $versionNumber)
397
    {
398
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
399
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
400
        $contentDraft = $this->repository->getContentService()->createContentDraft(
401
            $contentInfo,
402
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
403
        );
404
405
        return new Values\CreatedVersion(
406
            array(
407
                'version' => new Values\Version(
408
                    $contentDraft,
409
                    $contentType,
410
                    $this->repository->getContentService()->loadRelations($contentDraft->getVersionInfo())
411
                ),
412
            )
413
        );
414
    }
415
416
    /**
417
     * The system creates a new draft version as a copy from the current version.
418
     *
419
     * @param mixed $contentId
420
     *
421
     * @throws ForbiddenException if the current version is already a draft
422
     *
423
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedVersion
424
     */
425 View Code Duplication
    public function createDraftFromCurrentVersion($contentId)
426
    {
427
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
428
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
429
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
430
            $contentInfo
431
        );
432
433
        if ($versionInfo->isDraft()) {
434
            throw new ForbiddenException('Current version is already in status DRAFT');
435
        }
436
437
        $contentDraft = $this->repository->getContentService()->createContentDraft($contentInfo);
438
439
        return new Values\CreatedVersion(
440
            array(
441
                'version' => new Values\Version(
442
                    $contentDraft,
443
                    $contentType,
444
                    $this->repository->getContentService()->loadRelations($contentDraft->getVersionInfo())
445
                ),
446
            )
447
        );
448
    }
449
450
    /**
451
     * A specific draft is updated.
452
     *
453
     * @param mixed $contentId
454
     * @param mixed $versionNumber
455
     *
456
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
457
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException
458
     *
459
     * @return \eZ\Publish\Core\REST\Server\Values\Version
460
     */
461
    public function updateVersion($contentId, $versionNumber, Request $request)
462
    {
463
        $contentUpdateStruct = $this->inputDispatcher->parse(
464
            new Message(
465
                array(
466
                    'Content-Type' => $request->headers->get('Content-Type'),
467
                    'Url' => $this->router->generate(
468
                        'ezpublish_rest_updateVersion',
469
                        array(
470
                            'contentId' => $contentId,
471
                            'versionNumber' => $versionNumber,
472
                        )
473
                    ),
474
                ),
475
                $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...
476
            )
477
        );
478
479
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
480
            $this->repository->getContentService()->loadContentInfo($contentId),
481
            $versionNumber
482
        );
483
484
        if (!$versionInfo->isDraft()) {
485
            throw new ForbiddenException('Only version in status DRAFT can be updated');
486
        }
487
488
        try {
489
            $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...
490
        } catch (ContentValidationException $e) {
491
            throw new BadRequestException($e->getMessage());
492
        } catch (ContentFieldValidationException $e) {
493
            throw new RESTContentFieldValidationException($e);
494
        }
495
496
        $languages = null;
497
        if ($request->query->has('languages')) {
498
            $languages = explode(',', $request->query->get('languages'));
499
        }
500
501
        // Reload the content to handle languages GET parameter
502
        $content = $this->repository->getContentService()->loadContent(
503
            $contentId,
504
            $languages,
505
            $versionInfo->versionNo
506
        );
507
        $contentType = $this->repository->getContentTypeService()->loadContentType(
508
            $content->getVersionInfo()->getContentInfo()->contentTypeId
509
        );
510
511
        return new Values\Version(
512
            $content,
513
            $contentType,
514
            $this->repository->getContentService()->loadRelations($content->getVersionInfo()),
515
            $request->getPathInfo()
516
        );
517
    }
518
519
    /**
520
     * The content version is published.
521
     *
522
     * @param mixed $contentId
523
     * @param mixed $versionNumber
524
     *
525
     * @throws ForbiddenException if version $versionNumber isn't a draft
526
     *
527
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
528
     */
529 View Code Duplication
    public function publishVersion($contentId, $versionNumber)
530
    {
531
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
532
            $this->repository->getContentService()->loadContentInfo($contentId),
533
            $versionNumber
534
        );
535
536
        if (!$versionInfo->isDraft()) {
537
            throw new ForbiddenException('Only version in status DRAFT can be published');
538
        }
539
540
        $this->repository->getContentService()->publishVersion(
541
            $versionInfo
542
        );
543
544
        return new Values\NoContent();
545
    }
546
547
    /**
548
     * Redirects to the relations of the current version.
549
     *
550
     * @param mixed $contentId
551
     *
552
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
553
     */
554 View Code Duplication
    public function redirectCurrentVersionRelations($contentId)
555
    {
556
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
557
558
        return new Values\TemporaryRedirect(
559
            $this->router->generate(
560
                'ezpublish_rest_redirectCurrentVersionRelations',
561
                array(
562
                    'contentId' => $contentId,
563
                    'versionNumber' => $contentInfo->currentVersionNo,
564
                )
565
            )
566
        );
567
    }
568
569
    /**
570
     * Loads the relations of the given version.
571
     *
572
     * @param mixed $contentId
573
     * @param mixed $versionNumber
574
     *
575
     * @return \eZ\Publish\Core\REST\Server\Values\RelationList
576
     */
577
    public function loadVersionRelations($contentId, $versionNumber, Request $request)
578
    {
579
        $offset = $request->query->has('offset') ? (int)$request->query->get('offset') : 0;
580
        $limit = $request->query->has('limit') ? (int)$request->query->get('limit') : -1;
581
582
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
583
        $relationList = $this->repository->getContentService()->loadRelations(
584
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
585
        );
586
587
        $relationList = array_slice(
588
            $relationList,
589
            $offset >= 0 ? $offset : 0,
590
            $limit >= 0 ? $limit : null
591
        );
592
593
        $relationListValue = new Values\RelationList(
594
            $relationList,
595
            $contentId,
596
            $versionNumber,
597
            $request->getPathInfo()
598
        );
599
600
        if ($contentInfo->mainLocationId === null) {
601
            return $relationListValue;
602
        }
603
604
        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...
605
            $relationListValue,
606
            array('locationId' => $contentInfo->mainLocationId)
607
        );
608
    }
609
610
    /**
611
     * Loads a relation for the given content object and version.
612
     *
613
     * @param mixed $contentId
614
     * @param int $versionNumber
615
     * @param mixed $relationId
616
     *
617
     * @throws \eZ\Publish\Core\REST\Common\Exceptions\NotFoundException
618
     *
619
     * @return \eZ\Publish\Core\REST\Server\Values\RestRelation
620
     */
621
    public function loadVersionRelation($contentId, $versionNumber, $relationId, Request $request)
622
    {
623
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
624
        $relationList = $this->repository->getContentService()->loadRelations(
625
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
626
        );
627
628
        foreach ($relationList as $relation) {
629
            if ($relation->id == $relationId) {
630
                $relation = new Values\RestRelation($relation, $contentId, $versionNumber);
631
632
                if ($contentInfo->mainLocationId === null) {
633
                    return $relation;
634
                }
635
636
                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...
637
                    $relation,
638
                    array('locationId' => $contentInfo->mainLocationId)
639
                );
640
            }
641
        }
642
643
        throw new Exceptions\NotFoundException("Relation not found: '{$request->getPathInfo()}'.");
644
    }
645
646
    /**
647
     * Deletes a relation of the given draft.
648
     *
649
     * @param mixed $contentId
650
     * @param int   $versionNumber
651
     * @param mixed $relationId
652
     *
653
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
654
     * @throws \eZ\Publish\Core\REST\Common\Exceptions\NotFoundException
655
     *
656
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
657
     */
658
    public function removeRelation($contentId, $versionNumber, $relationId, Request $request)
659
    {
660
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
661
            $this->repository->getContentService()->loadContentInfo($contentId),
662
            $versionNumber
663
        );
664
665
        $versionRelations = $this->repository->getContentService()->loadRelations($versionInfo);
666
        foreach ($versionRelations as $relation) {
667
            if ($relation->id == $relationId) {
668
                if ($relation->type !== Relation::COMMON) {
669
                    throw new ForbiddenException('Relation is not of type COMMON');
670
                }
671
672
                if (!$versionInfo->isDraft()) {
673
                    throw new ForbiddenException('Relation of type COMMON can only be removed from drafts');
674
                }
675
676
                $this->repository->getContentService()->deleteRelation($versionInfo, $relation->getDestinationContentInfo());
677
678
                return new Values\NoContent();
679
            }
680
        }
681
682
        throw new Exceptions\NotFoundException("Relation not found: '{$request->getPathInfo()}'.");
683
    }
684
685
    /**
686
     * Creates a new relation of type COMMON for the given draft.
687
     *
688
     * @param mixed $contentId
689
     * @param int $versionNumber
690
     *
691
     * @throws ForbiddenException if version $versionNumber isn't a draft
692
     * @throws ForbiddenException if a relation to the same content already exists
693
     *
694
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedRelation
695
     */
696
    public function createRelation($contentId, $versionNumber, Request $request)
697
    {
698
        $destinationContentId = $this->inputDispatcher->parse(
699
            new Message(
700
                array('Content-Type' => $request->headers->get('Content-Type')),
701
                $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...
702
            )
703
        );
704
705
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
706
        $versionInfo = $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber);
707
        if (!$versionInfo->isDraft()) {
708
            throw new ForbiddenException('Relation of type COMMON can only be added to drafts');
709
        }
710
711
        try {
712
            $destinationContentInfo = $this->repository->getContentService()->loadContentInfo($destinationContentId);
713
        } catch (NotFoundException $e) {
714
            throw new ForbiddenException($e->getMessage());
715
        }
716
717
        $existingRelations = $this->repository->getContentService()->loadRelations($versionInfo);
718
        foreach ($existingRelations as $existingRelation) {
719
            if ($existingRelation->getDestinationContentInfo()->id == $destinationContentId) {
720
                throw new ForbiddenException('Relation of type COMMON to selected destination content ID already exists');
721
            }
722
        }
723
724
        $relation = $this->repository->getContentService()->addRelation($versionInfo, $destinationContentInfo);
725
726
        return new Values\CreatedRelation(
727
            array(
728
                'relation' => new Values\RestRelation($relation, $contentId, $versionNumber),
729
            )
730
        );
731
    }
732
733
    /**
734
     * Creates and executes a content view.
735
     *
736
     * @deprecated Since platform 1.0. Forwards the request to the new /views location, but returns a 301.
737
     *
738
     * @return \eZ\Publish\Core\REST\Server\Values\RestExecutedView
739
     */
740
    public function createView()
741
    {
742
        $response = $this->forward('ezpublish_rest.controller.views:createView');
743
744
        // Add 301 status code and location href
745
        $response->setStatusCode(301);
746
        $response->headers->set('Location', $this->router->generate('ezpublish_rest_views_create'));
747
748
        return $response;
749
    }
750
751
    /**
752
     * @param string $controller
753
     *
754
     * @return \Symfony\Component\HttpFoundation\Response
755
     */
756
    protected function forward($controller)
757
    {
758
        $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...
759
        $subRequest = $this->container->get('request_stack')->getCurrentRequest()->duplicate(null, null, $path);
760
761
        return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
762
    }
763
764
    /**
765
     * @param \Symfony\Component\HttpFoundation\Request $request
766
     *
767
     * @return mixed
768
     */
769
    protected function parseCreateContentRequest(Request $request)
770
    {
771
        return $this->inputDispatcher->parse(
772
            new Message(
773
                array('Content-Type' => $request->headers->get('Content-Type'), 'Url' => $request->getPathInfo()),
774
                $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...
775
            )
776
        );
777
    }
778
779
    /**
780
     * @param \Symfony\Component\HttpFoundation\Request $request
781
     * @param \eZ\Publish\Core\REST\Server\Values\RestContentCreateStruct $contentCreate
782
     *
783
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
784
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
785
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
786
     *
787
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedContent
788
     */
789
    protected function doCreateContent(Request $request, RestContentCreateStruct $contentCreate)
790
    {
791
        try {
792
            $content = $this->repository->getContentService()->createContent(
793
                $contentCreate->contentCreateStruct,
794
                array($contentCreate->locationCreateStruct)
795
            );
796
        } catch (ContentValidationException $e) {
797
            throw new BadRequestException($e->getMessage());
798
        } catch (ContentFieldValidationException $e) {
799
            throw new RESTContentFieldValidationException($e);
800
        }
801
802
        $contentValue = null;
803
        $contentType = null;
804
        $relations = null;
805
        if ($this->getMediaType($request) === 'application/vnd.ez.api.content') {
806
            $contentValue = $content;
807
            $contentType = $this->repository->getContentTypeService()->loadContentType(
808
                $content->getVersionInfo()->getContentInfo()->contentTypeId
809
            );
810
            $relations = $this->repository->getContentService()->loadRelations($contentValue->getVersionInfo());
811
        }
812
813
        return new Values\CreatedContent(
814
            array(
815
                'content' => new Values\RestContent(
816
                    $content->contentInfo,
817
                    null,
818
                    $contentValue,
819
                    $contentType,
820
                    $relations
821
                ),
822
            )
823
        );
824
    }
825
}
826