Completed
Push — ezp-27864-rest-delete-transl-f... ( 5ba831...92c326 )
by
unknown
15:49
created

Content::deletePublishedVersionTranslation()   B

Complexity

Conditions 4
Paths 14

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

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

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

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

An additional type check may prevent trouble.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

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