Completed
Push — master ( dfa392...375d71 )
by
unknown
13:09
created

Content   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 769
Duplicated Lines 16.12 %

Coupling/Cohesion

Components 1
Dependencies 35

Importance

Changes 0
Metric Value
dl 124
loc 769
rs 1.0434
c 0
b 0
f 0
wmc 64
lcom 1
cbo 35

23 Methods

Rating   Name   Duplication   Size   Complexity  
B loadContent() 0 41 5
B updateContentMetadata() 0 42 6
A redirectCurrentVersion() 14 14 1
A redirectContent() 19 19 2
B loadContentInVersion() 0 32 4
B createContent() 0 43 4
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
B createDraftFromCurrentVersion() 24 24 2
B updateVersion() 0 57 5
A publishVersion() 17 17 2
A redirectCurrentVersionRelations() 14 14 1
B loadVersionRelations() 0 32 6
B loadVersionRelation() 0 24 4
B removeRelation() 0 26 5
B createRelation() 0 36 5
A createView() 0 10 1
A forward() 0 7 1

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\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 Symfony\Component\HttpFoundation\Request;
24
use Symfony\Component\HttpKernel\HttpKernelInterface;
25
26
/**
27
 * Content controller.
28
 */
29
class Content extends RestController
30
{
31
    /**
32
     * Loads a content info by remote ID.
33
     *
34
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException
35
     *
36
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
37
     */
38 View Code Duplication
    public function redirectContent(Request $request)
39
    {
40
        if (!$request->query->has('remoteId')) {
41
            throw new BadRequestException("'remoteId' parameter is required.");
42
        }
43
44
        $contentInfo = $this->repository->getContentService()->loadContentInfoByRemoteId(
45
            $request->query->get('remoteId')
46
        );
47
48
        return new Values\TemporaryRedirect(
49
            $this->router->generate(
50
                'ezpublish_rest_loadContent',
51
                array(
52
                    'contentId' => $contentInfo->id,
53
                )
54
            )
55
        );
56
    }
57
58
    /**
59
     * Loads a content info, potentially with the current version embedded.
60
     *
61
     * @param mixed $contentId
62
     * @param \Symfony\Component\HttpFoundation\Request $request
63
     *
64
     * @return \eZ\Publish\Core\REST\Server\Values\RestContent
65
     */
66
    public function loadContent($contentId, Request $request)
67
    {
68
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
69
70
        $mainLocation = null;
71
        if (!empty($contentInfo->mainLocationId)) {
72
            $mainLocation = $this->repository->getLocationService()->loadLocation($contentInfo->mainLocationId);
73
        }
74
75
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
76
77
        $contentVersion = null;
78
        $relations = null;
79
        if ($this->getMediaType($request) === 'application/vnd.ez.api.content') {
80
            $languages = null;
81
            if ($request->query->has('languages')) {
82
                $languages = explode(',', $request->query->get('languages'));
83
            }
84
85
            $contentVersion = $this->repository->getContentService()->loadContent($contentId, $languages);
86
            $relations = $this->repository->getContentService()->loadRelations($contentVersion->getVersionInfo());
87
        }
88
89
        $restContent = new Values\RestContent(
90
            $contentInfo,
91
            $mainLocation,
92
            $contentVersion,
93
            $contentType,
94
            $relations,
95
            $request->getPathInfo()
96
        );
97
98
        if ($contentInfo->mainLocationId === null) {
99
            return $restContent;
100
        }
101
102
        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...
103
            $restContent,
104
            array('locationId' => $contentInfo->mainLocationId)
105
        );
106
    }
107
108
    /**
109
     * Updates a content's metadata.
110
     *
111
     * @param mixed $contentId
112
     *
113
     * @return \eZ\Publish\Core\REST\Server\Values\RestContent
114
     */
115
    public function updateContentMetadata($contentId, Request $request)
116
    {
117
        $updateStruct = $this->inputDispatcher->parse(
118
            new Message(
119
                array('Content-Type' => $request->headers->get('Content-Type')),
120
                $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...
121
            )
122
        );
123
124
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
125
126
        // update section
127
        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...
128
            $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...
129
            $this->repository->getSectionService()->assignSection($contentInfo, $section);
130
            $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...
131
        }
132
133
        // @todo Consider refactoring! ContentService::updateContentMetadata throws the same exception
134
        // in case the updateStruct is empty and if remoteId already exists. Since REST version of update struct
135
        // includes section ID in addition to other fields, we cannot throw exception if only sectionId property
136
        // is set, so we must skip updating content in that case instead of allowing propagation of the exception.
137
        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...
138
            if ($propertyName !== 'sectionId' && $propertyValue !== null) {
139
                // update content
140
                $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...
141
                $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
142
                break;
143
            }
144
        }
145
146
        try {
147
            $locationInfo = $this->repository->getLocationService()->loadLocation($contentInfo->mainLocationId);
148
        } catch (NotFoundException $e) {
149
            $locationInfo = null;
150
        }
151
152
        return new Values\RestContent(
153
            $contentInfo,
154
            $locationInfo
155
        );
156
    }
157
158
    /**
159
     * Loads a specific version of a given content object.
160
     *
161
     * @param mixed $contentId
162
     *
163
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
164
     */
165 View Code Duplication
    public function redirectCurrentVersion($contentId)
166
    {
167
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
168
169
        return new Values\TemporaryRedirect(
170
            $this->router->generate(
171
                'ezpublish_rest_loadContentInVersion',
172
                array(
173
                    'contentId' => $contentId,
174
                    'versionNumber' => $contentInfo->currentVersionNo,
175
                )
176
            )
177
        );
178
    }
179
180
    /**
181
     * Loads a specific version of a given content object.
182
     *
183
     * @param mixed $contentId
184
     * @param int $versionNumber
185
     *
186
     * @return \eZ\Publish\Core\REST\Server\Values\Version
187
     */
188
    public function loadContentInVersion($contentId, $versionNumber, Request $request)
189
    {
190
        $languages = null;
191
        if ($request->query->has('languages')) {
192
            $languages = explode(',', $request->query->get('languages'));
193
        }
194
195
        $content = $this->repository->getContentService()->loadContent(
196
            $contentId,
197
            $languages,
198
            $versionNumber
199
        );
200
        $contentType = $this->repository->getContentTypeService()->loadContentType(
201
            $content->getVersionInfo()->getContentInfo()->contentTypeId
202
        );
203
204
        $versionValue = new Values\Version(
205
            $content,
206
            $contentType,
207
            $this->repository->getContentService()->loadRelations($content->getVersionInfo()),
208
            $request->getPathInfo()
209
        );
210
211
        if ($content->contentInfo->mainLocationId === null || $content->versionInfo->status === VersionInfo::STATUS_DRAFT) {
212
            return $versionValue;
213
        }
214
215
        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...
216
            $versionValue,
217
            array('locationId' => $content->contentInfo->mainLocationId)
218
        );
219
    }
220
221
    /**
222
     * Creates a new content draft assigned to the authenticated user.
223
     * If a different userId is given in the input it is assigned to the
224
     * given user but this required special rights for the authenticated
225
     * user (this is useful for content staging where the transfer process
226
     * does not have to authenticate with the user which created the content
227
     * object in the source server). The user has to publish the content if
228
     * it should be visible.
229
     *
230
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedContent
231
     */
232
    public function createContent(Request $request)
233
    {
234
        $contentCreate = $this->inputDispatcher->parse(
235
            new Message(
236
                array('Content-Type' => $request->headers->get('Content-Type')),
237
                $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...
238
            )
239
        );
240
241
        try {
242
            $content = $this->repository->getContentService()->createContent(
243
                $contentCreate->contentCreateStruct,
0 ignored issues
show
Documentation introduced by
The property contentCreateStruct does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
245
            );
246
        } catch (ContentValidationException $e) {
247
            throw new BadRequestException($e->getMessage());
248
        } catch (ContentFieldValidationException $e) {
249
            throw new RESTContentFieldValidationException($e);
250
        }
251
252
        $contentValue = null;
253
        $contentType = null;
254
        $relations = null;
255
        if ($this->getMediaType($request) === 'application/vnd.ez.api.content') {
256
            $contentValue = $content;
257
            $contentType = $this->repository->getContentTypeService()->loadContentType(
258
                $content->getVersionInfo()->getContentInfo()->contentTypeId
259
            );
260
            $relations = $this->repository->getContentService()->loadRelations($contentValue->getVersionInfo());
261
        }
262
263
        return new Values\CreatedContent(
264
            array(
265
                'content' => new Values\RestContent(
266
                    $content->contentInfo,
267
                    null,
268
                    $contentValue,
269
                    $contentType,
270
                    $relations
271
                ),
272
            )
273
        );
274
    }
275
276
    /**
277
     * The content is deleted. If the content has locations (which is required in 4.x)
278
     * on delete all locations assigned the content object are deleted via delete subtree.
279
     *
280
     * @param mixed $contentId
281
     *
282
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
283
     */
284
    public function deleteContent($contentId)
285
    {
286
        $this->repository->getContentService()->deleteContent(
287
            $this->repository->getContentService()->loadContentInfo($contentId)
288
        );
289
290
        return new Values\NoContent();
291
    }
292
293
    /**
294
     * Creates a new content object as copy under the given parent location given in the destination header.
295
     *
296
     * @param mixed $contentId
297
     *
298
     * @return \eZ\Publish\Core\REST\Server\Values\ResourceCreated
299
     */
300
    public function copyContent($contentId, Request $request)
301
    {
302
        $destination = $request->headers->get('Destination');
303
304
        $parentLocationParts = explode('/', $destination);
305
        $copiedContent = $this->repository->getContentService()->copyContent(
306
            $this->repository->getContentService()->loadContentInfo($contentId),
307
            $this->repository->getLocationService()->newLocationCreateStruct(array_pop($parentLocationParts))
308
        );
309
310
        return new Values\ResourceCreated(
311
            $this->router->generate(
312
                'ezpublish_rest_loadContent',
313
                array('contentId' => $copiedContent->id)
314
            )
315
        );
316
    }
317
318
    /**
319
     * Deletes a translation from all the Versions of the given Content Object.
320
     *
321
     * If any non-published Version contains only the Translation to be deleted, that entire Version will be deleted
322
     *
323
     * @param int $contentId
324
     * @param string $languageCode
325
     *
326
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
327
     *
328
     * @throws \Exception
329
     */
330
    public function deleteContentTranslation($contentId, $languageCode)
331
    {
332
        $contentService = $this->repository->getContentService();
333
334
        $this->repository->beginTransaction();
335
        try {
336
            $contentInfo = $contentService->loadContentInfo($contentId);
337
            $contentService->deleteTranslation(
338
                $contentInfo,
339
                $languageCode
340
            );
341
342
            $this->repository->commit();
343
344
            return new Values\NoContent();
345
        } catch (\Exception $e) {
346
            $this->repository->rollback();
347
            throw $e;
348
        }
349
    }
350
351
    /**
352
     * Returns a list of all versions of the content. This method does not
353
     * include fields and relations in the Version elements of the response.
354
     *
355
     * @param mixed $contentId
356
     *
357
     * @return \eZ\Publish\Core\REST\Server\Values\VersionList
358
     */
359
    public function loadContentVersions($contentId, Request $request)
360
    {
361
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
362
363
        return new Values\VersionList(
364
            $this->repository->getContentService()->loadVersions($contentInfo),
365
            $request->getPathInfo()
366
        );
367
    }
368
369
    /**
370
     * The version is deleted.
371
     *
372
     * @param mixed $contentId
373
     * @param mixed $versionNumber
374
     *
375
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
376
     *
377
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
378
     */
379 View Code Duplication
    public function deleteContentVersion($contentId, $versionNumber)
380
    {
381
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
382
            $this->repository->getContentService()->loadContentInfo($contentId),
383
            $versionNumber
384
        );
385
386
        if ($versionInfo->isPublished()) {
387
            throw new ForbiddenException('Version in status PUBLISHED cannot be deleted');
388
        }
389
390
        $this->repository->getContentService()->deleteVersion(
391
            $versionInfo
392
        );
393
394
        return new Values\NoContent();
395
    }
396
397
    /**
398
     * Remove the given Translation from the given Version Draft.
399
     *
400
     * @param int $contentId
401
     * @param int $versionNumber
402
     * @param string $languageCode
403
     *
404
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
405
     *
406
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
407
     */
408
    public function deleteTranslationFromDraft($contentId, $versionNumber, $languageCode)
409
    {
410
        $contentService = $this->repository->getContentService();
411
        $versionInfo = $contentService->loadVersionInfoById($contentId, $versionNumber);
412
413
        if (!$versionInfo->isDraft()) {
414
            throw new ForbiddenException('Translation can be deleted from DRAFT Version only');
415
        }
416
417
        $contentService->deleteTranslationFromDraft($versionInfo, $languageCode);
418
419
        return new Values\NoContent();
420
    }
421
422
    /**
423
     * The system creates a new draft version as a copy from the given version.
424
     *
425
     * @param mixed $contentId
426
     * @param mixed $versionNumber
427
     *
428
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedVersion
429
     */
430 View Code Duplication
    public function createDraftFromVersion($contentId, $versionNumber)
431
    {
432
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
433
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
434
        $contentDraft = $this->repository->getContentService()->createContentDraft(
435
            $contentInfo,
436
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
437
        );
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
     * The system creates a new draft version as a copy from the current version.
452
     *
453
     * @param mixed $contentId
454
     *
455
     * @throws ForbiddenException if the current version is already a draft
456
     *
457
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedVersion
458
     */
459 View Code Duplication
    public function createDraftFromCurrentVersion($contentId)
460
    {
461
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
462
        $contentType = $this->repository->getContentTypeService()->loadContentType($contentInfo->contentTypeId);
463
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
464
            $contentInfo
465
        );
466
467
        if ($versionInfo->isDraft()) {
468
            throw new ForbiddenException('Current version is already in status DRAFT');
469
        }
470
471
        $contentDraft = $this->repository->getContentService()->createContentDraft($contentInfo);
472
473
        return new Values\CreatedVersion(
474
            array(
475
                'version' => new Values\Version(
476
                    $contentDraft,
477
                    $contentType,
478
                    $this->repository->getContentService()->loadRelations($contentDraft->getVersionInfo())
479
                ),
480
            )
481
        );
482
    }
483
484
    /**
485
     * A specific draft is updated.
486
     *
487
     * @param mixed $contentId
488
     * @param mixed $versionNumber
489
     *
490
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
491
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException
492
     *
493
     * @return \eZ\Publish\Core\REST\Server\Values\Version
494
     */
495
    public function updateVersion($contentId, $versionNumber, Request $request)
496
    {
497
        $contentUpdateStruct = $this->inputDispatcher->parse(
498
            new Message(
499
                array(
500
                    'Content-Type' => $request->headers->get('Content-Type'),
501
                    'Url' => $this->router->generate(
502
                        'ezpublish_rest_updateVersion',
503
                        array(
504
                            'contentId' => $contentId,
505
                            'versionNumber' => $versionNumber,
506
                        )
507
                    ),
508
                ),
509
                $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...
510
            )
511
        );
512
513
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
514
            $this->repository->getContentService()->loadContentInfo($contentId),
515
            $versionNumber
516
        );
517
518
        if (!$versionInfo->isDraft()) {
519
            throw new ForbiddenException('Only version in status DRAFT can be updated');
520
        }
521
522
        try {
523
            $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...
524
        } catch (ContentValidationException $e) {
525
            throw new BadRequestException($e->getMessage());
526
        } catch (ContentFieldValidationException $e) {
527
            throw new RESTContentFieldValidationException($e);
528
        }
529
530
        $languages = null;
531
        if ($request->query->has('languages')) {
532
            $languages = explode(',', $request->query->get('languages'));
533
        }
534
535
        // Reload the content to handle languages GET parameter
536
        $content = $this->repository->getContentService()->loadContent(
537
            $contentId,
538
            $languages,
539
            $versionInfo->versionNo
540
        );
541
        $contentType = $this->repository->getContentTypeService()->loadContentType(
542
            $content->getVersionInfo()->getContentInfo()->contentTypeId
543
        );
544
545
        return new Values\Version(
546
            $content,
547
            $contentType,
548
            $this->repository->getContentService()->loadRelations($content->getVersionInfo()),
549
            $request->getPathInfo()
550
        );
551
    }
552
553
    /**
554
     * The content version is published.
555
     *
556
     * @param mixed $contentId
557
     * @param mixed $versionNumber
558
     *
559
     * @throws ForbiddenException if version $versionNumber isn't a draft
560
     *
561
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
562
     */
563 View Code Duplication
    public function publishVersion($contentId, $versionNumber)
564
    {
565
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
566
            $this->repository->getContentService()->loadContentInfo($contentId),
567
            $versionNumber
568
        );
569
570
        if (!$versionInfo->isDraft()) {
571
            throw new ForbiddenException('Only version in status DRAFT can be published');
572
        }
573
574
        $this->repository->getContentService()->publishVersion(
575
            $versionInfo
576
        );
577
578
        return new Values\NoContent();
579
    }
580
581
    /**
582
     * Redirects to the relations of the current version.
583
     *
584
     * @param mixed $contentId
585
     *
586
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
587
     */
588 View Code Duplication
    public function redirectCurrentVersionRelations($contentId)
589
    {
590
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
591
592
        return new Values\TemporaryRedirect(
593
            $this->router->generate(
594
                'ezpublish_rest_redirectCurrentVersionRelations',
595
                array(
596
                    'contentId' => $contentId,
597
                    'versionNumber' => $contentInfo->currentVersionNo,
598
                )
599
            )
600
        );
601
    }
602
603
    /**
604
     * Loads the relations of the given version.
605
     *
606
     * @param mixed $contentId
607
     * @param mixed $versionNumber
608
     *
609
     * @return \eZ\Publish\Core\REST\Server\Values\RelationList
610
     */
611
    public function loadVersionRelations($contentId, $versionNumber, Request $request)
612
    {
613
        $offset = $request->query->has('offset') ? (int)$request->query->get('offset') : 0;
614
        $limit = $request->query->has('limit') ? (int)$request->query->get('limit') : -1;
615
616
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
617
        $relationList = $this->repository->getContentService()->loadRelations(
618
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
619
        );
620
621
        $relationList = array_slice(
622
            $relationList,
623
            $offset >= 0 ? $offset : 0,
624
            $limit >= 0 ? $limit : null
625
        );
626
627
        $relationListValue = new Values\RelationList(
628
            $relationList,
629
            $contentId,
630
            $versionNumber,
631
            $request->getPathInfo()
632
        );
633
634
        if ($contentInfo->mainLocationId === null) {
635
            return $relationListValue;
636
        }
637
638
        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...
639
            $relationListValue,
640
            array('locationId' => $contentInfo->mainLocationId)
641
        );
642
    }
643
644
    /**
645
     * Loads a relation for the given content object and version.
646
     *
647
     * @param mixed $contentId
648
     * @param int $versionNumber
649
     * @param mixed $relationId
650
     *
651
     * @throws \eZ\Publish\Core\REST\Common\Exceptions\NotFoundException
652
     *
653
     * @return \eZ\Publish\Core\REST\Server\Values\RestRelation
654
     */
655
    public function loadVersionRelation($contentId, $versionNumber, $relationId, Request $request)
656
    {
657
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
658
        $relationList = $this->repository->getContentService()->loadRelations(
659
            $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber)
660
        );
661
662
        foreach ($relationList as $relation) {
663
            if ($relation->id == $relationId) {
664
                $relation = new Values\RestRelation($relation, $contentId, $versionNumber);
665
666
                if ($contentInfo->mainLocationId === null) {
667
                    return $relation;
668
                }
669
670
                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...
671
                    $relation,
672
                    array('locationId' => $contentInfo->mainLocationId)
673
                );
674
            }
675
        }
676
677
        throw new Exceptions\NotFoundException("Relation not found: '{$request->getPathInfo()}'.");
678
    }
679
680
    /**
681
     * Deletes a relation of the given draft.
682
     *
683
     * @param mixed $contentId
684
     * @param int   $versionNumber
685
     * @param mixed $relationId
686
     *
687
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
688
     * @throws \eZ\Publish\Core\REST\Common\Exceptions\NotFoundException
689
     *
690
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
691
     */
692
    public function removeRelation($contentId, $versionNumber, $relationId, Request $request)
693
    {
694
        $versionInfo = $this->repository->getContentService()->loadVersionInfo(
695
            $this->repository->getContentService()->loadContentInfo($contentId),
696
            $versionNumber
697
        );
698
699
        $versionRelations = $this->repository->getContentService()->loadRelations($versionInfo);
700
        foreach ($versionRelations as $relation) {
701
            if ($relation->id == $relationId) {
702
                if ($relation->type !== Relation::COMMON) {
703
                    throw new ForbiddenException('Relation is not of type COMMON');
704
                }
705
706
                if (!$versionInfo->isDraft()) {
707
                    throw new ForbiddenException('Relation of type COMMON can only be removed from drafts');
708
                }
709
710
                $this->repository->getContentService()->deleteRelation($versionInfo, $relation->getDestinationContentInfo());
711
712
                return new Values\NoContent();
713
            }
714
        }
715
716
        throw new Exceptions\NotFoundException("Relation not found: '{$request->getPathInfo()}'.");
717
    }
718
719
    /**
720
     * Creates a new relation of type COMMON for the given draft.
721
     *
722
     * @param mixed $contentId
723
     * @param int $versionNumber
724
     *
725
     * @throws ForbiddenException if version $versionNumber isn't a draft
726
     * @throws ForbiddenException if a relation to the same content already exists
727
     *
728
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedRelation
729
     */
730
    public function createRelation($contentId, $versionNumber, Request $request)
731
    {
732
        $destinationContentId = $this->inputDispatcher->parse(
733
            new Message(
734
                array('Content-Type' => $request->headers->get('Content-Type')),
735
                $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...
736
            )
737
        );
738
739
        $contentInfo = $this->repository->getContentService()->loadContentInfo($contentId);
740
        $versionInfo = $this->repository->getContentService()->loadVersionInfo($contentInfo, $versionNumber);
741
        if (!$versionInfo->isDraft()) {
742
            throw new ForbiddenException('Relation of type COMMON can only be added to drafts');
743
        }
744
745
        try {
746
            $destinationContentInfo = $this->repository->getContentService()->loadContentInfo($destinationContentId);
0 ignored issues
show
Documentation introduced by
$destinationContentId is of type object<eZ\Publish\API\Re...ory\Values\ValueObject>, but the function expects a integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
747
        } catch (NotFoundException $e) {
748
            throw new ForbiddenException($e->getMessage());
749
        }
750
751
        $existingRelations = $this->repository->getContentService()->loadRelations($versionInfo);
752
        foreach ($existingRelations as $existingRelation) {
753
            if ($existingRelation->getDestinationContentInfo()->id == $destinationContentId) {
754
                throw new ForbiddenException('Relation of type COMMON to selected destination content ID already exists');
755
            }
756
        }
757
758
        $relation = $this->repository->getContentService()->addRelation($versionInfo, $destinationContentInfo);
759
760
        return new Values\CreatedRelation(
761
            array(
762
                'relation' => new Values\RestRelation($relation, $contentId, $versionNumber),
763
            )
764
        );
765
    }
766
767
    /**
768
     * Creates and executes a content view.
769
     *
770
     * @deprecated Since platform 1.0. Forwards the request to the new /views location, but returns a 301.
771
     *
772
     * @return \eZ\Publish\Core\REST\Server\Values\RestExecutedView
773
     */
774
    public function createView()
775
    {
776
        $response = $this->forward('ezpublish_rest.controller.views:createView');
777
778
        // Add 301 status code and location href
779
        $response->setStatusCode(301);
780
        $response->headers->set('Location', $this->router->generate('ezpublish_rest_views_create'));
781
782
        return $response;
783
    }
784
785
    /**
786
     * @param string $controller
787
     *
788
     * @return \Symfony\Component\HttpFoundation\Response
789
     */
790
    protected function forward($controller)
791
    {
792
        $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...
793
        $subRequest = $this->container->get('request_stack')->getCurrentRequest()->duplicate(null, null, $path);
794
795
        return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
796
    }
797
}
798