Completed
Push — master ( db22b5...7f7ad9 )
by André
26:35 queued 07:02
created

ViewController::viewLocation()   C

Complexity

Conditions 8
Paths 34

Size

Total Lines 42
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 42
rs 5.3846
cc 8
eloc 30
nc 34
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
        trigger_error(
148
            "ViewController::viewLocation() is deprecated since kernel 6.0.0, and will be removed in the future.\n" .
149
            'Use ViewController::viewAction() instead.',
150
            E_USER_DEPRECATED
151
        );
152
153
        $this->performAccessChecks();
154
        $response = $this->buildResponse();
155
156
        try {
157
            if (isset($params['location']) && $params['location'] instanceof Location) {
158
                $location = $params['location'];
159
            } else {
160
                $location = $this->getRepository()->getLocationService()->loadLocation($locationId);
161
                if ($location->invisible) {
162
                    throw new NotFoundHttpException("Location #$locationId cannot be displayed as it is flagged as invisible.");
163
                }
164
            }
165
166
            $response->headers->set('X-Location-Id', $locationId);
167
            $response->setContent(
168
                $this->renderLocation(
169
                    $location,
170
                    $viewType,
171
                    $layout,
172
                    $params
173
                )
174
            );
175
176
            return $response;
177
        } catch (UnauthorizedException $e) {
178
            throw new AccessDeniedException();
179
        } catch (NotFoundException $e) {
180
            throw new NotFoundHttpException($e->getMessage(), $e);
181
        } catch (NotFoundHttpException $e) {
182
            throw $e;
183
        } catch (Exception $e) {
184
            return $this->handleViewException($response, $params, $e, $viewType, null, $locationId);
185
        }
186
    }
187
188
    /**
189
     * Main action for viewing embedded location.
190
     * Response will be cached with HttpCache validation model (Etag).
191
     *
192
     * @param int $locationId
193
     * @param string $viewType
194
     * @param bool $layout
195
     * @param array $params
196
     *
197
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
198
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
199
     * @throws \Exception
200
     *
201
     * @return \Symfony\Component\HttpFoundation\Response
202
     *
203
     * @deprecated Since 6.0.0. Viewing locations is now done with ViewContent.
204
     */
205
    public function embedLocation($locationId, $viewType, $layout = false, array $params = array())
206
    {
207
        trigger_error(
208
            "ViewController::embedLocation() is deprecated since kernel 6.0.0, and will be removed in the future.\n" .
209
            'Use ViewController::viewAction() instead.',
210
            E_USER_DEPRECATED
211
        );
212
213
        $this->performAccessChecks();
214
        $response = $this->buildResponse();
215
216
        try {
217
            /** @var \eZ\Publish\API\Repository\Values\Content\Location $location */
218
            $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...
219
                function (Repository $repository) use ($locationId) {
220
                    return $repository->getLocationService()->loadLocation($locationId);
221
                }
222
            );
223
224
            if ($location->invisible) {
225
                throw new NotFoundHttpException("Location #{$locationId} cannot be displayed as it is flagged as invisible.");
226
            }
227
228
            // Check both 'content/read' and 'content/view_embed'.
229 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...
230
                !$this->authorizationChecker->isGranted(
231
                    new AuthorizationAttribute(
232
                        'content',
233
                        'read',
234
                        array('valueObject' => $location->contentInfo, 'targets' => $location)
235
                    )
236
                )
237
                && !$this->authorizationChecker->isGranted(
238
                    new AuthorizationAttribute(
239
                        'content',
240
                        'view_embed',
241
                        array('valueObject' => $location->contentInfo, 'targets' => $location)
242
                    )
243
                )
244
            ) {
245
                throw new AccessDeniedException();
246
            }
247
248
            if ($response->isNotModified($this->getRequest())) {
249
                return $response;
250
            }
251
252
            $response->headers->set('X-Location-Id', $locationId);
253
            $response->setContent(
254
                $this->renderLocation(
255
                    $location,
256
                    $viewType,
257
                    $layout,
258
                    $params
259
                )
260
            );
261
262
            return $response;
263
        } catch (UnauthorizedException $e) {
264
            throw new AccessDeniedException();
265
        } catch (NotFoundException $e) {
266
            throw new NotFoundHttpException($e->getMessage(), $e);
267
        } catch (Exception $e) {
268
            return $this->handleViewException($response, $params, $e, $viewType, null, $locationId);
269
        }
270
    }
271
272
    /**
273
     * Main action for viewing content.
274
     * Response will be cached with HttpCache validation model (Etag).
275
     *
276
     * @param int $contentId
277
     * @param string $viewType
278
     * @param bool $layout
279
     * @param array $params
280
     *
281
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
282
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
283
     * @throws \Exception
284
     *
285
     * @return \Symfony\Component\HttpFoundation\Response
286
     */
287
    public function viewContent($contentId, $viewType, $layout = false, array $params = array())
288
    {
289
        trigger_error(
290
            "ViewController::viewContent() is deprecated since kernel 6.0.0, and will be removed in the future.\n" .
291
            'Use ViewController::viewAction() instead.',
292
            E_USER_DEPRECATED
293
        );
294
295
        if ($viewType === 'embed') {
296
            return $this->embedContent($contentId, $viewType, $layout, $params);
297
        }
298
299
        $this->performAccessChecks();
300
        $response = $this->buildResponse();
301
302
        try {
303
            $content = $this->getRepository()->getContentService()->loadContent($contentId);
304
305
            if ($response->isNotModified($this->getRequest())) {
306
                return $response;
307
            }
308
309
            if (!isset($params['location']) && !isset($params['locationId'])) {
310
                $params['location'] = $this->getRepository()->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
311
            }
312
            $response->headers->set('X-Location-Id', $content->contentInfo->mainLocationId);
313
            $response->setContent(
314
                $this->renderContent($content, $viewType, $layout, $params)
315
            );
316
317
            return $response;
318
        } catch (UnauthorizedException $e) {
319
            throw new AccessDeniedException();
320
        } catch (NotFoundException $e) {
321
            throw new NotFoundHttpException($e->getMessage(), $e);
322
        } catch (Exception $e) {
323
            return $this->handleViewException($response, $params, $e, $viewType, $contentId);
324
        }
325
    }
326
327
    /**
328
     * Main action for viewing embedded content.
329
     * Response will be cached with HttpCache validation model (Etag).
330
     *
331
     * @param int $contentId
332
     * @param string $viewType
333
     * @param bool $layout
334
     * @param array $params
335
     *
336
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
337
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
338
     * @throws \Exception
339
     *
340
     * @return \Symfony\Component\HttpFoundation\Response
341
     */
342
    public function embedContent($contentId, $viewType, $layout = false, array $params = array())
343
    {
344
        trigger_error(
345
            "ViewController::embedContent() is deprecated since kernel 6.0.0, and will be removed in the future.\n" .
346
            'Use ViewController::viewAction() instead.',
347
            E_USER_DEPRECATED
348
        );
349
350
        $this->performAccessChecks();
351
        $response = $this->buildResponse();
352
353
        try {
354
            /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
355
            $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...
356
                function (Repository $repository) use ($contentId) {
357
                    return $repository->getContentService()->loadContent($contentId);
358
                }
359
            );
360
361
            // Check both 'content/read' and 'content/view_embed'.
362 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...
363
                !$this->authorizationChecker->isGranted(
364
                    new AuthorizationAttribute('content', 'read', array('valueObject' => $content))
365
                )
366
                && !$this->authorizationChecker->isGranted(
367
                    new AuthorizationAttribute('content', 'view_embed', array('valueObject' => $content))
368
                )
369
            ) {
370
                throw new AccessDeniedException();
371
            }
372
373
            // Check that Content is published, since sudo allows loading unpublished content.
374 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...
375
                $content->getVersionInfo()->status !== APIVersionInfo::STATUS_PUBLISHED
376
                && !$this->authorizationChecker->isGranted(
377
                    new AuthorizationAttribute('content', 'versionread', array('valueObject' => $content))
378
                )
379
            ) {
380
                throw new AccessDeniedException();
381
            }
382
383
            if ($response->isNotModified($this->getRequest())) {
384
                return $response;
385
            }
386
387
            $response->setContent(
388
                $this->renderContent($content, $viewType, $layout, $params)
389
            );
390
391
            return $response;
392
        } catch (UnauthorizedException $e) {
393
            throw new AccessDeniedException();
394
        } catch (NotFoundException $e) {
395
            throw new NotFoundHttpException($e->getMessage(), $e);
396
        } catch (Exception $e) {
397
            return $this->handleViewException($response, $params, $e, $viewType, $contentId);
398
        }
399
    }
400
401
    protected function handleViewException(Response $response, $params, Exception $e, $viewType, $contentId = null, $locationId = null)
402
    {
403
        $event = new APIContentExceptionEvent(
404
            $e,
405
            array(
406
                'contentId' => $contentId,
407
                'locationId' => $locationId,
408
                'viewType' => $viewType,
409
            )
410
        );
411
        $this->getEventDispatcher()->dispatch(MVCEvents::API_CONTENT_EXCEPTION, $event);
412
        if ($event->hasContentView()) {
413
            $response->setContent(
414
                $this->viewManager->renderContentView(
415
                    $event->getContentView(),
416
                    $params
417
                )
418
            );
419
420
            return $response;
421
        }
422
423
        throw $e;
424
    }
425
426
    /**
427
     * Creates the content to be returned when viewing a Location.
428
     *
429
     * @param Location $location
430
     * @param string $viewType
431
     * @param bool $layout
432
     * @param array $params
433
     *
434
     * @return string
435
     */
436
    protected function renderLocation(Location $location, $viewType, $layout = false, array $params = array())
437
    {
438
        return $this->viewManager->renderLocation($location, $viewType, $params + array('noLayout' => !$layout));
439
    }
440
441
    /**
442
     * Creates the content to be returned when viewing a Content.
443
     *
444
     * @param Content $content
445
     * @param string $viewType
446
     * @param bool $layout
447
     * @param array $params
448
     *
449
     * @return string
450
     */
451
    protected function renderContent(Content $content, $viewType, $layout = false, array $params = array())
452
    {
453
        return $this->viewManager->renderContent($content, $viewType, $params + array('noLayout' => !$layout));
454
    }
455
456
    /**
457
     * Performs the access checks.
458
     */
459
    protected function performAccessChecks()
460
    {
461
        if (!$this->isGranted(new AuthorizationAttribute('content', 'read'))) {
462
            throw new AccessDeniedException();
463
        }
464
    }
465
}
466