Completed
Push — ezp-30696 ( 9bb3ad...3bd812 )
by
unknown
49:02 queued 18:35
created

Content   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 800
Duplicated Lines 15.5 %

Coupling/Cohesion

Components 1
Dependencies 39

Importance

Changes 0
Metric Value
dl 124
loc 800
rs 2.92
c 0
b 0
f 0
wmc 66
lcom 1
cbo 39

25 Methods

Rating   Name   Duplication   Size   Complexity  
B loadContent() 0 41 5
B updateContentMetadata() 0 42 6
A redirectCurrentVersion() 14 14 1
A loadContentInVersion() 0 32 4
A createContent() 0 6 1
A deleteContent() 0 8 1
A copyContent() 0 17 1
A deleteContentTranslation() 0 20 2
A loadContentVersions() 0 9 1
A deleteContentVersion() 17 17 2
A deleteTranslationFromDraft() 0 13 2
A createDraftFromVersion() 19 19 1
A createDraftFromCurrentVersion() 24 24 2
B updateVersion() 0 57 5
A publishVersion() 17 17 2
A redirectCurrentVersionRelations() 14 14 1
B loadVersionRelations() 0 32 6
A loadVersionRelation() 0 24 4
A removeRelation() 0 26 5
A createRelation() 0 36 5
A createView() 0 10 1
A forward() 0 7 1
A parseContentRequest() 0 9 1
A doCreateContent() 0 40 4
A redirectContent() 19 19 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Content often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Content, and based on these observations, apply Extract Interface, too.

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\API\Repository\Values\Content\Language;
12
use eZ\Publish\Core\REST\Common\Message;
13
use eZ\Publish\Core\REST\Common\Exceptions;
14
use eZ\Publish\Core\REST\Server\Values;
15
use eZ\Publish\Core\REST\Server\Controller as RestController;
16
use eZ\Publish\API\Repository\Values\Content\Relation;
17
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
18
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
19
use eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException;
20
use eZ\Publish\API\Repository\Exceptions\ContentValidationException;
21
use eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException;
22
use eZ\Publish\Core\REST\Server\Exceptions\BadRequestException;
23
use eZ\Publish\Core\REST\Server\Exceptions\ContentFieldValidationException as RESTContentFieldValidationException;
24
use eZ\Publish\Core\REST\Server\Values\RestContentCreateStruct;
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
                [
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 = Language::ALL;
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
            ['locationId' => $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
                ['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
                [
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 = Language::ALL;
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 || $content->versionInfo->status === VersionInfo::STATUS_DRAFT) {
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
            ['locationId' => $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
     * @param \Symfony\Component\HttpFoundation\Request $request
233
     *
234
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedContent
235
     */
236
    public function createContent(Request $request)
237
    {
238
        $contentCreate = $this->parseContentRequest($request);
239
240
        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...
241
    }
242
243
    /**
244
     * The content is deleted. If the content has locations (which is required in 4.x)
245
     * on delete all locations assigned the content object are deleted via delete subtree.
246
     *
247
     * @param mixed $contentId
248
     *
249
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
250
     */
251
    public function deleteContent($contentId)
252
    {
253
        $this->repository->getContentService()->deleteContent(
254
            $this->repository->getContentService()->loadContentInfo($contentId)
255
        );
256
257
        return new Values\NoContent();
258
    }
259
260
    /**
261
     * Creates a new content object as copy under the given parent location given in the destination header.
262
     *
263
     * @param mixed $contentId
264
     *
265
     * @return \eZ\Publish\Core\REST\Server\Values\ResourceCreated
266
     */
267
    public function copyContent($contentId, Request $request)
268
    {
269
        $destination = $request->headers->get('Destination');
270
271
        $parentLocationParts = explode('/', $destination);
272
        $copiedContent = $this->repository->getContentService()->copyContent(
273
            $this->repository->getContentService()->loadContentInfo($contentId),
274
            $this->repository->getLocationService()->newLocationCreateStruct(array_pop($parentLocationParts))
275
        );
276
277
        return new Values\ResourceCreated(
278
            $this->router->generate(
279
                'ezpublish_rest_loadContent',
280
                ['contentId' => $copiedContent->id]
281
            )
282
        );
283
    }
284
285
    /**
286
     * Deletes a translation from all the Versions of the given Content Object.
287
     *
288
     * If any non-published Version contains only the Translation to be deleted, that entire Version will be deleted
289
     *
290
     * @param int $contentId
291
     * @param string $languageCode
292
     *
293
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
294
     *
295
     * @throws \Exception
296
     */
297
    public function deleteContentTranslation($contentId, $languageCode)
298
    {
299
        $contentService = $this->repository->getContentService();
300
301
        $this->repository->beginTransaction();
302
        try {
303
            $contentInfo = $contentService->loadContentInfo($contentId);
304
            $contentService->deleteTranslation(
305
                $contentInfo,
306
                $languageCode
307
            );
308
309
            $this->repository->commit();
310
311
            return new Values\NoContent();
312
        } catch (\Exception $e) {
313
            $this->repository->rollback();
314
            throw $e;
315
        }
316
    }
317
318
    /**
319
     * Returns a list of all versions of the content. This method does not
320
     * include fields and relations in the Version elements of the response.
321
     *
322
     * @param mixed $contentId
323
     *
324
     * @return \eZ\Publish\Core\REST\Server\Values\VersionList
325
     */
326
    public function loadContentVersions($contentId, Request $request)
327
    {
328
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
329
330
        return new Values\VersionList(
331
            $this->repository->getContentService()->loadVersions($contentInfo),
332
            $request->getPathInfo()
333
        );
334
    }
335
336
    /**
337
     * The version is deleted.
338
     *
339
     * @param mixed $contentId
340
     * @param mixed $versionNumber
341
     *
342
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
343
     *
344
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
345
     */
346 View Code Duplication
    public function deleteContentVersion($contentId, $versionNumber)
347
    {
348
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
349
            $this->repository->getContentService()->loadContentInfo($contentId),
350
            $versionNumber
351
        );
352
353
        if ($versionInfo->isPublished()) {
354
            throw new ForbiddenException('Version in status PUBLISHED cannot be deleted');
355
        }
356
357
        $this->repository->getContentService()->deleteVersion(
358
            $versionInfo
359
        );
360
361
        return new Values\NoContent();
362
    }
363
364
    /**
365
     * Remove the given Translation from the given Version Draft.
366
     *
367
     * @param int $contentId
368
     * @param int $versionNumber
369
     * @param string $languageCode
370
     *
371
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
372
     *
373
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
374
     */
375
    public function deleteTranslationFromDraft($contentId, $versionNumber, $languageCode)
376
    {
377
        $contentService = $this->repository->getContentService();
378
        $versionInfo = $contentService->loadVersionInfoById($contentId, $versionNumber);
379
380
        if (!$versionInfo->isDraft()) {
381
            throw new ForbiddenException('Translation can be deleted from DRAFT Version only');
382
        }
383
384
        $contentService->deleteTranslationFromDraft($versionInfo, $languageCode);
385
386
        return new Values\NoContent();
387
    }
388
389
    /**
390
     * The system creates a new draft version as a copy from the given version.
391
     *
392
     * @param mixed $contentId
393
     * @param mixed $versionNumber
394
     *
395
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedVersion
396
     */
397 View Code Duplication
    public function createDraftFromVersion($contentId, $versionNumber)
398
    {
399
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
400
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
401
        $contentDraft = $this->repository->getContentService()->createContentDraft(
402
            $contentInfo,
403
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
404
        );
405
406
        return new Values\CreatedVersion(
407
            [
408
                'version' => new Values\Version(
409
                    $contentDraft,
410
                    $contentType,
411
                    $this->repository->getContentService()->loadRelations($contentDraft->getVersionInfo())
412
                ),
413
            ]
414
        );
415
    }
416
417
    /**
418
     * The system creates a new draft version as a copy from the current version.
419
     *
420
     * @param mixed $contentId
421
     *
422
     * @throws ForbiddenException if the current version is already a draft
423
     *
424
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedVersion
425
     */
426 View Code Duplication
    public function createDraftFromCurrentVersion($contentId)
427
    {
428
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
429
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
430
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
431
            $contentInfo
432
        );
433
434
        if ($versionInfo->isDraft()) {
435
            throw new ForbiddenException('Current version is already in status DRAFT');
436
        }
437
438
        $contentDraft = $this->repository->getContentService()->createContentDraft($contentInfo);
439
440
        return new Values\CreatedVersion(
441
            [
442
                'version' => new Values\Version(
443
                    $contentDraft,
444
                    $contentType,
445
                    $this->repository->getContentService()->loadRelations($contentDraft->getVersionInfo())
446
                ),
447
            ]
448
        );
449
    }
450
451
    /**
452
     * A specific draft is updated.
453
     *
454
     * @param mixed $contentId
455
     * @param mixed $versionNumber
456
     *
457
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
458
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException
459
     *
460
     * @return \eZ\Publish\Core\REST\Server\Values\Version
461
     */
462
    public function updateVersion($contentId, $versionNumber, Request $request)
463
    {
464
        $contentUpdateStruct = $this->inputDispatcher->parse(
465
            new Message(
466
                [
467
                    'Content-Type' => $request->headers->get('Content-Type'),
468
                    'Url' => $this->router->generate(
469
                        'ezpublish_rest_updateVersion',
470
                        [
471
                            'contentId' => $contentId,
472
                            'versionNumber' => $versionNumber,
473
                        ]
474
                    ),
475
                ],
476
                $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...
477
            )
478
        );
479
480
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
481
            $this->repository->getContentService()->loadContentInfo($contentId),
482
            $versionNumber
483
        );
484
485
        if (!$versionInfo->isDraft()) {
486
            throw new ForbiddenException('Only version in status DRAFT can be updated');
487
        }
488
489
        try {
490
            $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...
491
        } catch (ContentValidationException $e) {
492
            throw new BadRequestException($e->getMessage());
493
        } catch (ContentFieldValidationException $e) {
494
            throw new RESTContentFieldValidationException($e);
495
        }
496
497
        $languages = null;
498
        if ($request->query->has('languages')) {
499
            $languages = explode(',', $request->query->get('languages'));
500
        }
501
502
        // Reload the content to handle languages GET parameter
503
        $content = $this->repository->getContentService()->loadContent(
504
            $contentId,
505
            $languages,
506
            $versionInfo->versionNo
507
        );
508
        $contentType = $this->repository->getContentTypeService()->loadContentType(
509
            $content->getVersionInfo()->getContentInfo()->contentTypeId
510
        );
511
512
        return new Values\Version(
513
            $content,
514
            $contentType,
515
            $this->repository->getContentService()->loadRelations($content->getVersionInfo()),
516
            $request->getPathInfo()
517
        );
518
    }
519
520
    /**
521
     * The content version is published.
522
     *
523
     * @param mixed $contentId
524
     * @param mixed $versionNumber
525
     *
526
     * @throws ForbiddenException if version $versionNumber isn't a draft
527
     *
528
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
529
     */
530 View Code Duplication
    public function publishVersion($contentId, $versionNumber)
531
    {
532
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
533
            $this->repository->getContentService()->loadContentInfo($contentId),
534
            $versionNumber
535
        );
536
537
        if (!$versionInfo->isDraft()) {
538
            throw new ForbiddenException('Only version in status DRAFT can be published');
539
        }
540
541
        $this->repository->getContentService()->publishVersion(
542
            $versionInfo
543
        );
544
545
        return new Values\NoContent();
546
    }
547
548
    /**
549
     * Redirects to the relations of the current version.
550
     *
551
     * @param mixed $contentId
552
     *
553
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
554
     */
555 View Code Duplication
    public function redirectCurrentVersionRelations($contentId)
556
    {
557
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
558
559
        return new Values\TemporaryRedirect(
560
            $this->router->generate(
561
                'ezpublish_rest_redirectCurrentVersionRelations',
562
                [
563
                    'contentId' => $contentId,
564
                    'versionNumber' => $contentInfo->currentVersionNo,
565
                ]
566
            )
567
        );
568
    }
569
570
    /**
571
     * Loads the relations of the given version.
572
     *
573
     * @param mixed $contentId
574
     * @param mixed $versionNumber
575
     *
576
     * @return \eZ\Publish\Core\REST\Server\Values\RelationList
577
     */
578
    public function loadVersionRelations($contentId, $versionNumber, Request $request)
579
    {
580
        $offset = $request->query->has('offset') ? (int)$request->query->get('offset') : 0;
581
        $limit = $request->query->has('limit') ? (int)$request->query->get('limit') : -1;
582
583
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
584
        $relationList = $this->repository->getContentService()->loadRelations(
585
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
586
        );
587
588
        $relationList = array_slice(
589
            $relationList,
590
            $offset >= 0 ? $offset : 0,
591
            $limit >= 0 ? $limit : null
592
        );
593
594
        $relationListValue = new Values\RelationList(
595
            $relationList,
596
            $contentId,
597
            $versionNumber,
598
            $request->getPathInfo()
599
        );
600
601
        if ($contentInfo->mainLocationId === null) {
602
            return $relationListValue;
603
        }
604
605
        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...
606
            $relationListValue,
607
            ['locationId' => $contentInfo->mainLocationId]
608
        );
609
    }
610
611
    /**
612
     * Loads a relation for the given content object and version.
613
     *
614
     * @param mixed $contentId
615
     * @param int $versionNumber
616
     * @param mixed $relationId
617
     *
618
     * @throws \eZ\Publish\Core\REST\Common\Exceptions\NotFoundException
619
     *
620
     * @return \eZ\Publish\Core\REST\Server\Values\RestRelation
621
     */
622
    public function loadVersionRelation($contentId, $versionNumber, $relationId, Request $request)
623
    {
624
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
625
        $relationList = $this->repository->getContentService()->loadRelations(
626
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
627
        );
628
629
        foreach ($relationList as $relation) {
630
            if ($relation->id == $relationId) {
631
                $relation = new Values\RestRelation($relation, $contentId, $versionNumber);
632
633
                if ($contentInfo->mainLocationId === null) {
634
                    return $relation;
635
                }
636
637
                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...
638
                    $relation,
639
                    ['locationId' => $contentInfo->mainLocationId]
640
                );
641
            }
642
        }
643
644
        throw new Exceptions\NotFoundException("Relation not found: '{$request->getPathInfo()}'.");
645
    }
646
647
    /**
648
     * Deletes a relation of the given draft.
649
     *
650
     * @param mixed $contentId
651
     * @param int   $versionNumber
652
     * @param mixed $relationId
653
     *
654
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
655
     * @throws \eZ\Publish\Core\REST\Common\Exceptions\NotFoundException
656
     *
657
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
658
     */
659
    public function removeRelation($contentId, $versionNumber, $relationId, Request $request)
660
    {
661
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
662
            $this->repository->getContentService()->loadContentInfo($contentId),
663
            $versionNumber
664
        );
665
666
        $versionRelations = $this->repository->getContentService()->loadRelations($versionInfo);
667
        foreach ($versionRelations as $relation) {
668
            if ($relation->id == $relationId) {
669
                if ($relation->type !== Relation::COMMON) {
670
                    throw new ForbiddenException('Relation is not of type COMMON');
671
                }
672
673
                if (!$versionInfo->isDraft()) {
674
                    throw new ForbiddenException('Relation of type COMMON can only be removed from drafts');
675
                }
676
677
                $this->repository->getContentService()->deleteRelation($versionInfo, $relation->getDestinationContentInfo());
678
679
                return new Values\NoContent();
680
            }
681
        }
682
683
        throw new Exceptions\NotFoundException("Relation not found: '{$request->getPathInfo()}'.");
684
    }
685
686
    /**
687
     * Creates a new relation of type COMMON for the given draft.
688
     *
689
     * @param mixed $contentId
690
     * @param int $versionNumber
691
     *
692
     * @throws ForbiddenException if version $versionNumber isn't a draft
693
     * @throws ForbiddenException if a relation to the same content already exists
694
     *
695
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedRelation
696
     */
697
    public function createRelation($contentId, $versionNumber, Request $request)
698
    {
699
        $destinationContentId = $this->inputDispatcher->parse(
700
            new Message(
701
                ['Content-Type' => $request->headers->get('Content-Type')],
702
                $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...
703
            )
704
        );
705
706
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
707
        $versionInfo = $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber);
708
        if (!$versionInfo->isDraft()) {
709
            throw new ForbiddenException('Relation of type COMMON can only be added to drafts');
710
        }
711
712
        try {
713
            $destinationContentInfo = $this->repository->getContentService()->loadContentInfo($destinationContentId);
714
        } catch (NotFoundException $e) {
715
            throw new ForbiddenException($e->getMessage());
716
        }
717
718
        $existingRelations = $this->repository->getContentService()->loadRelations($versionInfo);
719
        foreach ($existingRelations as $existingRelation) {
720
            if ($existingRelation->getDestinationContentInfo()->id == $destinationContentId) {
721
                throw new ForbiddenException('Relation of type COMMON to selected destination content ID already exists');
722
            }
723
        }
724
725
        $relation = $this->repository->getContentService()->addRelation($versionInfo, $destinationContentInfo);
726
727
        return new Values\CreatedRelation(
728
            [
729
                'relation' => new Values\RestRelation($relation, $contentId, $versionNumber),
730
            ]
731
        );
732
    }
733
734
    /**
735
     * Creates and executes a content view.
736
     *
737
     * @deprecated Since platform 1.0. Forwards the request to the new /views location, but returns a 301.
738
     *
739
     * @return \eZ\Publish\Core\REST\Server\Values\RestExecutedView
740
     */
741
    public function createView()
742
    {
743
        $response = $this->forward('ezpublish_rest.controller.views:createView');
744
745
        // Add 301 status code and location href
746
        $response->setStatusCode(301);
747
        $response->headers->set('Location', $this->router->generate('ezpublish_rest_views_create'));
748
749
        return $response;
750
    }
751
752
    /**
753
     * @param string $controller
754
     *
755
     * @return \Symfony\Component\HttpFoundation\Response
756
     */
757
    protected function forward($controller)
758
    {
759
        $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...
760
        $subRequest = $this->container->get('request_stack')->getCurrentRequest()->duplicate(null, null, $path);
761
762
        return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
763
    }
764
765
    /**
766
     * @param \Symfony\Component\HttpFoundation\Request $request
767
     *
768
     * @return mixed
769
     */
770
    protected function parseContentRequest(Request $request)
771
    {
772
        return $this->inputDispatcher->parse(
773
            new Message(
774
                ['Content-Type' => $request->headers->get('Content-Type'), 'Url' => $request->getPathInfo()],
775
                $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...
776
            )
777
        );
778
    }
779
780
    /**
781
     * @param \Symfony\Component\HttpFoundation\Request $request
782
     * @param \eZ\Publish\Core\REST\Server\Values\RestContentCreateStruct $contentCreate
783
     *
784
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
785
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
786
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
787
     *
788
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedContent
789
     */
790
    protected function doCreateContent(Request $request, RestContentCreateStruct $contentCreate)
791
    {
792
        try {
793
            $contentCreateStruct = $contentCreate->contentCreateStruct;
794
            $contentCreate->locationCreateStruct->sortField = $contentCreateStruct->contentType->defaultSortField;
795
            $contentCreate->locationCreateStruct->sortOrder = $contentCreateStruct->contentType->defaultSortOrder;
796
797
            $content = $this->repository->getContentService()->createContent(
798
                $contentCreateStruct,
799
                [$contentCreate->locationCreateStruct]
800
            );
801
        } catch (ContentValidationException $e) {
802
            throw new BadRequestException($e->getMessage());
803
        } catch (ContentFieldValidationException $e) {
804
            throw new RESTContentFieldValidationException($e);
805
        }
806
807
        $contentValue = null;
808
        $contentType = null;
809
        $relations = null;
810
        if ($this->getMediaType($request) === 'application/vnd.ez.api.content') {
811
            $contentValue = $content;
812
            $contentType = $this->repository->getContentTypeService()->loadContentType(
813
                $content->getVersionInfo()->getContentInfo()->contentTypeId
814
            );
815
            $relations = $this->repository->getContentService()->loadRelations($contentValue->getVersionInfo());
816
        }
817
818
        return new Values\CreatedContent(
819
            [
820
                'content' => new Values\RestContent(
821
                    $content->contentInfo,
822
                    null,
823
                    $contentValue,
824
                    $contentType,
825
                    $relations
826
                ),
827
            ]
828
        );
829
    }
830
}
831