Completed
Push — fallback_language_fields ( 531860 )
by André
37:34
created

ViewController::renderLocation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 4
1
<?php
2
3
/**
4
 * File containing the ViewController class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 *
9
 * @version //autogentag//
10
 */
11
namespace eZ\Publish\Core\MVC\Symfony\Controller\Content;
12
13
use eZ\Publish\API\Repository\Repository;
14
use eZ\Publish\API\Repository\Values\Content\Content;
15
use eZ\Publish\API\Repository\Values\Content\Location;
16
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
17
use eZ\Publish\Core\MVC\Symfony\Controller\Controller;
18
use eZ\Publish\Core\MVC\Symfony\MVCEvents;
19
use eZ\Publish\Core\MVC\Symfony\Event\APIContentExceptionEvent;
20
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute as AuthorizationAttribute;
21
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
22
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
23
use eZ\Publish\Core\MVC\Symfony\View\ContentView;
24
use eZ\Publish\Core\MVC\Symfony\View\ViewManagerInterface;
25
use Symfony\Component\HttpFoundation\Response;
26
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
27
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
28
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
29
use DateTime;
30
use Exception;
31
32
/**
33
 * This controller provides the content view feature.
34
 *
35
 * @since 6.0.0 All methods except `view()` are deprecated and will be removed in the future.
36
 */
37
class ViewController extends Controller
38
{
39
    /**
40
     * @var \eZ\Publish\Core\MVC\Symfony\View\ViewManagerInterface
41
     */
42
    protected $viewManager;
43
44
    /**
45
     * @var \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface
46
     */
47
    private $authorizationChecker;
48
49
    public function __construct(ViewManagerInterface $viewManager, AuthorizationCheckerInterface $authorizationChecker)
50
    {
51
        $this->viewManager = $viewManager;
52
        $this->authorizationChecker = $authorizationChecker;
53
    }
54
55
    /**
56
     * This is the default view action or a ContentView object.
57
     *
58
     * It doesn't do anything by itself: the returned View object is rendered by the ViewRendererListener
59
     * into an HttpFoundation Response.
60
     *
61
     * This action can be selectively replaced by a custom action by means of content_view
62
     * configuration. Custom actions can add parameters to the view and customize the Response the View will be
63
     * converted to. They may also bypass the ViewRenderer by returning an HttpFoundation Response.
64
     *
65
     * Cache is in both cases handled by the CacheViewResponseListener.
66
     *
67
     * @param \eZ\Publish\Core\MVC\Symfony\View\ContentView $view
68
     *
69
     * @return \eZ\Publish\Core\MVC\Symfony\View\ContentView
70
     */
71
    public function viewAction(ContentView $view)
72
    {
73
        return $view;
74
    }
75
76
    /**
77
     * Embed a content.
78
     * Behaves mostly like viewAction(), but with specific content load permission handling.
79
     *
80
     * @param \eZ\Publish\Core\MVC\Symfony\View\ContentView $view
81
     *
82
     * @return \eZ\Publish\Core\MVC\Symfony\View\ContentView
83
     */
84
    public function embedAction(ContentView $view)
85
    {
86
        return $view;
87
    }
88
89
    /**
90
     * Build the response so that depending on settings it's cacheable.
91
     *
92
     * @param string|null $etag
93
     * @param \DateTime|null $lastModified
94
     *
95
     * @return \Symfony\Component\HttpFoundation\Response
96
     */
97
    protected function buildResponse($etag = null, DateTime $lastModified = null)
98
    {
99
        $request = $this->getRequest();
100
        $response = new Response();
101
        if ($this->getParameter('content.view_cache') === true) {
102
            $response->setPublic();
103
            if ($etag !== null) {
104
                $response->setEtag($etag);
105
            }
106
107
            if ($this->getParameter('content.ttl_cache') === true) {
108
                $response->setSharedMaxAge(
109
                    $this->getParameter('content.default_ttl')
0 ignored issues
show
Documentation introduced by
$this->getParameter('content.default_ttl') is of type boolean, 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...
110
                );
111
            }
112
113
            // Make the response vary against X-User-Hash header ensures that an HTTP
114
            // reverse proxy caches the different possible variations of the
115
            // response as it can depend on user role for instance.
116
            if ($request->headers->has('X-User-Hash')) {
117
                $response->setVary('X-User-Hash');
118
            }
119
120
            if ($lastModified != null) {
121
                $response->setLastModified($lastModified);
122
            }
123
        }
124
125
        return $response;
126
    }
127
128
    /**
129
     * Main action for viewing content through a location in the repository.
130
     * Response will be cached with HttpCache validation model (Etag).
131
     *
132
     * @param int $locationId
133
     * @param string $viewType
134
     * @param bool $layout
135
     * @param array $params
136
     *
137
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
138
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
139
     * @throws \Exception
140
     *
141
     * @return \Symfony\Component\HttpFoundation\Response
142
     *
143
     * @deprecated Since 6.0.0. Viewing locations is now done with ViewContent.
144
     */
145
    public function viewLocation($locationId, $viewType, $layout = false, array $params = array())
146
    {
147
        $this->performAccessChecks();
148
        $response = $this->buildResponse();
149
150
        try {
151
            if (isset($params['location']) && $params['location'] instanceof Location) {
152
                $location = $params['location'];
153
            } else {
154
                $location = $this->getRepository()->getLocationService()->loadLocation($locationId);
155
                if ($location->invisible) {
156
                    throw new NotFoundHttpException("Location #$locationId cannot be displayed as it is flagged as invisible.");
157
                }
158
            }
159
160
            $response->headers->set('X-Location-Id', $locationId);
161
            $response->setContent(
162
                $this->renderLocation(
163
                    $location,
164
                    $viewType,
165
                    $layout,
166
                    $params
167
                )
168
            );
169
170
            return $response;
171
        } catch (UnauthorizedException $e) {
172
            throw new AccessDeniedException();
173
        } catch (NotFoundException $e) {
174
            throw new NotFoundHttpException($e->getMessage(), $e);
175
        } catch (NotFoundHttpException $e) {
176
            throw $e;
177
        } catch (Exception $e) {
178
            return $this->handleViewException($response, $params, $e, $viewType, null, $locationId);
179
        }
180
    }
181
182
    /**
183
     * Main action for viewing embedded location.
184
     * Response will be cached with HttpCache validation model (Etag).
185
     *
186
     * @param int $locationId
187
     * @param string $viewType
188
     * @param bool $layout
189
     * @param array $params
190
     *
191
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
192
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
193
     * @throws \Exception
194
     *
195
     * @return \Symfony\Component\HttpFoundation\Response
196
     *
197
     * @deprecated Since 6.0.0. Viewing locations is now done with ViewContent.
198
     */
199
    public function embedLocation($locationId, $viewType, $layout = false, array $params = array())
200
    {
201
        $this->performAccessChecks();
202
        $response = $this->buildResponse();
203
204
        try {
205
            /** @var \eZ\Publish\API\Repository\Values\Content\Location $location */
206
            $location = $this->getRepository()->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
207
                function (Repository $repository) use ($locationId) {
208
                    return $repository->getLocationService()->loadLocation($locationId);
209
                }
210
            );
211
212
            if ($location->invisible) {
213
                throw new NotFoundHttpException("Location #{$locationId} cannot be displayed as it is flagged as invisible.");
214
            }
215
216
            // Check both 'content/read' and 'content/view_embed'.
217 View Code Duplication
            if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
218
                !$this->authorizationChecker->isGranted(
219
                    new AuthorizationAttribute(
220
                        'content',
221
                        'read',
222
                        array('valueObject' => $location->contentInfo, 'targets' => $location)
223
                    )
224
                )
225
                && !$this->authorizationChecker->isGranted(
226
                    new AuthorizationAttribute(
227
                        'content',
228
                        'view_embed',
229
                        array('valueObject' => $location->contentInfo, 'targets' => $location)
230
                    )
231
                )
232
            ) {
233
                throw new AccessDeniedException();
234
            }
235
236
            if ($response->isNotModified($this->getRequest())) {
237
                return $response;
238
            }
239
240
            $response->headers->set('X-Location-Id', $locationId);
241
            $response->setContent(
242
                $this->renderLocation(
243
                    $location,
244
                    $viewType,
245
                    $layout,
246
                    $params
247
                )
248
            );
249
250
            return $response;
251
        } catch (UnauthorizedException $e) {
252
            throw new AccessDeniedException();
253
        } catch (NotFoundException $e) {
254
            throw new NotFoundHttpException($e->getMessage(), $e);
255
        } catch (Exception $e) {
256
            return $this->handleViewException($response, $params, $e, $viewType, null, $locationId);
257
        }
258
    }
259
260
    /**
261
     * Main action for viewing content.
262
     * Response will be cached with HttpCache validation model (Etag).
263
     *
264
     * @param int $contentId
265
     * @param string $viewType
266
     * @param bool $layout
267
     * @param array $params
268
     *
269
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
270
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
271
     * @throws \Exception
272
     *
273
     * @return \Symfony\Component\HttpFoundation\Response
274
     */
275
    public function viewContent($contentId, $viewType, $layout = false, array $params = array())
276
    {
277
        if ($viewType === 'embed') {
278
            return $this->embedContent($contentId, $viewType, $layout, $params);
279
        }
280
281
        $this->performAccessChecks();
282
        $response = $this->buildResponse();
283
284
        try {
285
            $content = $this->getRepository()->getContentService()->loadContent($contentId);
286
287
            if ($response->isNotModified($this->getRequest())) {
288
                return $response;
289
            }
290
291
            if (!isset($params['location']) && !isset($params['locationId'])) {
292
                $params['location'] = $this->getRepository()->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
293
            }
294
            $response->headers->set('X-Location-Id', $content->contentInfo->mainLocationId);
295
            $response->setContent(
296
                $this->renderContent($content, $viewType, $layout, $params)
297
            );
298
299
            return $response;
300
        } catch (UnauthorizedException $e) {
301
            throw new AccessDeniedException();
302
        } catch (NotFoundException $e) {
303
            throw new NotFoundHttpException($e->getMessage(), $e);
304
        } catch (Exception $e) {
305
            return $this->handleViewException($response, $params, $e, $viewType, $contentId);
306
        }
307
    }
308
309
    /**
310
     * Main action for viewing embedded content.
311
     * Response will be cached with HttpCache validation model (Etag).
312
     *
313
     * @param int $contentId
314
     * @param string $viewType
315
     * @param bool $layout
316
     * @param array $params
317
     *
318
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
319
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
320
     * @throws \Exception
321
     *
322
     * @return \Symfony\Component\HttpFoundation\Response
323
     */
324
    public function embedContent($contentId, $viewType, $layout = false, array $params = array())
325
    {
326
        $this->performAccessChecks();
327
        $response = $this->buildResponse();
328
329
        try {
330
            /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
331
            $content = $this->getRepository()->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
332
                function (Repository $repository) use ($contentId) {
333
                    return $repository->getContentService()->loadContent($contentId);
334
                }
335
            );
336
337
            // Check both 'content/read' and 'content/view_embed'.
338 View Code Duplication
            if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
339
                !$this->authorizationChecker->isGranted(
340
                    new AuthorizationAttribute('content', 'read', array('valueObject' => $content))
341
                )
342
                && !$this->authorizationChecker->isGranted(
343
                    new AuthorizationAttribute('content', 'view_embed', array('valueObject' => $content))
344
                )
345
            ) {
346
                throw new AccessDeniedException();
347
            }
348
349
            // Check that Content is published, since sudo allows loading unpublished content.
350 View Code Duplication
            if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
351
                $content->getVersionInfo()->status !== APIVersionInfo::STATUS_PUBLISHED
352
                && !$this->authorizationChecker->isGranted(
353
                    new AuthorizationAttribute('content', 'versionread', array('valueObject' => $content))
354
                )
355
            ) {
356
                throw new AccessDeniedException();
357
            }
358
359
            if ($response->isNotModified($this->getRequest())) {
360
                return $response;
361
            }
362
363
            $response->setContent(
364
                $this->renderContent($content, $viewType, $layout, $params)
365
            );
366
367
            return $response;
368
        } catch (UnauthorizedException $e) {
369
            throw new AccessDeniedException();
370
        } catch (NotFoundException $e) {
371
            throw new NotFoundHttpException($e->getMessage(), $e);
372
        } catch (Exception $e) {
373
            return $this->handleViewException($response, $params, $e, $viewType, $contentId);
374
        }
375
    }
376
377
    protected function handleViewException(Response $response, $params, Exception $e, $viewType, $contentId = null, $locationId = null)
378
    {
379
        $event = new APIContentExceptionEvent(
380
            $e,
381
            array(
382
                'contentId' => $contentId,
383
                'locationId' => $locationId,
384
                'viewType' => $viewType,
385
            )
386
        );
387
        $this->getEventDispatcher()->dispatch(MVCEvents::API_CONTENT_EXCEPTION, $event);
388
        if ($event->hasContentView()) {
389
            $response->setContent(
390
                $this->viewManager->renderContentView(
391
                    $event->getContentView(),
392
                    $params
393
                )
394
            );
395
396
            return $response;
397
        }
398
399
        throw $e;
400
    }
401
402
    /**
403
     * Creates the content to be returned when viewing a Location.
404
     *
405
     * @param Location $location
406
     * @param string $viewType
407
     * @param bool $layout
408
     * @param array $params
409
     *
410
     * @return string
411
     */
412
    protected function renderLocation(Location $location, $viewType, $layout = false, array $params = array())
413
    {
414
        return $this->viewManager->renderLocation($location, $viewType, $params + array('noLayout' => !$layout));
415
    }
416
417
    /**
418
     * Creates the content to be returned when viewing a Content.
419
     *
420
     * @param Content $content
421
     * @param string $viewType
422
     * @param bool $layout
423
     * @param array $params
424
     *
425
     * @return string
426
     */
427
    protected function renderContent(Content $content, $viewType, $layout = false, array $params = array())
428
    {
429
        return $this->viewManager->renderContent($content, $viewType, $params + array('noLayout' => !$layout));
430
    }
431
432
    /**
433
     * Performs the access checks.
434
     */
435
    protected function performAccessChecks()
436
    {
437
        if (!$this->isGranted(new AuthorizationAttribute('content', 'read'))) {
438
            throw new AccessDeniedException();
439
        }
440
    }
441
}
442