Completed
Push — ezp26381-content_view_default_... ( 44cedb...4147ee )
by
unknown
67:00 queued 39:09
created

ContentViewBuilder::canRead()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 4
nop 2
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license For full copyright and license information view LICENSE file distributed with this source code.
5
 */
6
namespace eZ\Publish\Core\MVC\Symfony\View\Builder;
7
8
use eZ\Publish\API\Repository\Repository;
9
use eZ\Publish\API\Repository\Values\Content\Content;
10
use eZ\Publish\API\Repository\Values\Content\Location;
11
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
12
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
13
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
14
use eZ\Publish\Core\Helper\ContentInfoLocationLoader;
15
use eZ\Publish\Core\MVC\Symfony\View\Configurator;
16
use eZ\Publish\Core\MVC\Symfony\View\ContentView;
17
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute as AuthorizationAttribute;
18
use eZ\Publish\Core\MVC\Symfony\View\EmbedView;
19
use eZ\Publish\Core\MVC\Symfony\View\ParametersInjector;
20
use Symfony\Component\HttpKernel\Controller\ControllerReference;
21
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
22
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
23
24
/**
25
 * Builds ContentView objects.
26
 */
27
class ContentViewBuilder implements ViewBuilder
28
{
29
    /** @var \eZ\Publish\API\Repository\Repository */
30
    private $repository;
31
32
    /** @var AuthorizationCheckerInterface */
33
    private $authorizationChecker;
34
35
    /** @var \eZ\Publish\Core\MVC\Symfony\View\Configurator */
36
    private $viewConfigurator;
37
38
    /** @var \eZ\Publish\Core\MVC\Symfony\View\ParametersInjector */
39
    private $viewParametersInjector;
40
41
    /**
42
     * Default templates, indexed per viewType (full, line, ...).
43
     * @var array
44
     */
45
    private $defaultTemplates;
46
47
    /**
48
     * @var \eZ\Publish\Core\Helper\ContentInfoLocationLoader
49
     */
50
    private $locationLoader;
51
52
    public function __construct(
53
        Repository $repository,
54
        AuthorizationCheckerInterface $authorizationChecker,
55
        Configurator $viewConfigurator,
56
        ParametersInjector $viewParametersInjector,
57
        ContentInfoLocationLoader $locationLoader = null
58
    ) {
59
        $this->repository = $repository;
60
        $this->authorizationChecker = $authorizationChecker;
61
        $this->viewConfigurator = $viewConfigurator;
62
        $this->viewParametersInjector = $viewParametersInjector;
63
        $this->locationLoader = $locationLoader;
64
    }
65
66
    public function matches($argument)
67
    {
68
        return strpos($argument, 'ez_content:') !== false;
69
    }
70
71
    /**
72
     * @param array $parameters
73
     *
74
     * @return \eZ\Publish\Core\MVC\Symfony\View\ContentView|\eZ\Publish\Core\MVC\Symfony\View\View
75
     *         If both contentId and locationId parameters are missing
76
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
77
     *         If both contentId and locationId parameters are missing
78
     * @throws \eZ\Publish\Core\Base\Exceptions\UnauthorizedException
79
     */
80
    public function buildView(array $parameters)
81
    {
82
        $view = new ContentView(null, [], $parameters['viewType']);
83
        $view->setIsEmbed($this->isEmbed($parameters));
84
85
        if ($view->isEmbed() && $parameters['viewType'] === null) {
86
            $view->setViewType(EmbedView::DEFAULT_VIEW_TYPE);
87
        }
88
89
        if (isset($parameters['locationId'])) {
90
            $location = $this->loadLocation($parameters['locationId']);
91
        } elseif (isset($parameters['location'])) {
92
            $location = $parameters['location'];
93
        } else {
94
            $location = null;
95
        }
96
97
        if (isset($parameters['content'])) {
98
            $content = $parameters['content'];
99
        } else {
100
            if (isset($parameters['contentId'])) {
101
                $contentId = $parameters['contentId'];
102
            } elseif (isset($location)) {
103
                $contentId = $location->contentId;
104
            } else {
105
                throw new InvalidArgumentException('Content', 'No content could be loaded from parameters');
106
            }
107
108
            $content = $view->isEmbed() ? $this->loadContent($contentId) : $this->loadEmbeddedContent($contentId, $location);
109
        }
110
111
        $view->setContent($content);
112
113
        if (isset($location)) {
114
            if ($location->contentId !== $content->id) {
115
                throw new InvalidArgumentException('Location', 'Provided location does not belong to selected content');
116
            }
117
        } else if (isset($this->locationLoader)) {
118
            $location = $this->locationLoader->loadLocation($content->contentInfo);
119
        }
120
121
        if ($location !== null) {
122
            $view->setLocation($location);
123
        }
124
125
        $this->viewParametersInjector->injectViewParameters($view, $parameters);
126
        $this->viewConfigurator->configure($view);
127
128
        // deprecated controller actions are replaced with their new equivalent, viewAction and embedAction
129
        if (!$view->getControllerReference() instanceof ControllerReference) {
130
            if (in_array($parameters['_controller'], ['ez_content:viewLocation', 'ez_content:viewContent'])) {
131
                $view->setControllerReference(new ControllerReference('ez_content:viewAction'));
132
            } elseif (in_array($parameters['_controller'], ['ez_content:embedLocation', 'ez_content:embedContent'])) {
133
                $view->setControllerReference(new ControllerReference('ez_content:embedAction'));
134
            }
135
        }
136
137
        return $view;
138
    }
139
140
    /**
141
     * Loads Content with id $contentId.
142
     *
143
     * @param mixed $contentId
144
     *
145
     * @return \eZ\Publish\API\Repository\Values\Content\Content
146
     *
147
     * @throws \eZ\Publish\Core\Base\Exceptions\UnauthorizedException
148
     */
149
    private function loadContent($contentId)
150
    {
151
        return $this->repository->getContentService()->loadContent($contentId);
152
    }
153
154
    /**
155
     * Loads the embedded content with id $contentId.
156
     * Will load the content with sudo(), and check if the user can view_embed this content, for the given location
157
     * if provided.
158
     *
159
     * @param mixed $contentId
160
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
161
     *
162
     * @return \eZ\Publish\API\Repository\Values\Content\Content
163
     * @throws \eZ\Publish\Core\Base\Exceptions\UnauthorizedException
164
     */
165
    private function loadEmbeddedContent($contentId, Location $location = null)
166
    {
167
        $content = $this->repository->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...
168
            function (Repository $repository) use ($contentId) {
169
                return $repository->getContentService()->loadContent($contentId);
170
            }
171
        );
172
173
        if (!$this->canRead($content, $location)) {
174
            throw new UnauthorizedException(
175
                'content', 'read|view_embed',
176
                ['contentId' => $contentId, 'locationId' => $location !== null ? $location->id : 'n/a']
177
            );
178
        }
179
180
        // Check that Content is published, since sudo allows loading unpublished content.
181
        if (
182
            $content->getVersionInfo()->status !== VersionInfo::STATUS_PUBLISHED
183
            && !$this->authorizationChecker->isGranted(
184
                new AuthorizationAttribute('content', 'versionread', array('valueObject' => $content))
185
            )
186
        ) {
187
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId]);
188
        }
189
190
        return $content;
191
    }
192
193
    /**
194
     * Loads a visible Location.
195
     * @todo Do we need to handle permissions here ?
196
     *
197
     * @param $locationId
198
     *
199
     * @return \eZ\Publish\API\Repository\Values\Content\Location
200
     */
201
    private function loadLocation($locationId)
202
    {
203
        $location = $this->repository->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...
204
            function (Repository $repository) use ($locationId) {
205
                return $repository->getLocationService()->loadLocation($locationId);
206
            }
207
        );
208
        if ($location->invisible) {
209
            throw new NotFoundHttpException('Location cannot be displayed as it is flagged as invisible.');
210
        }
211
212
        return $location;
213
    }
214
215
    /**
216
     * Checks if a user can read a content, or view it as an embed.
217
     *
218
     * @param Content $content
219
     * @param $location
220
     *
221
     * @return bool
222
     */
223
    private function canRead(Content $content, Location $location = null)
224
    {
225
        $limitations = ['valueObject' => $content->contentInfo];
226
        if (isset($location)) {
227
            $limitations['targets'] = $location;
228
        }
229
230
        $readAttribute = new AuthorizationAttribute('content', 'read', $limitations);
231
        $viewEmbedAttribute = new AuthorizationAttribute('content', 'view_embed', $limitations);
232
233
        return
234
            $this->authorizationChecker->isGranted($readAttribute) ||
235
            $this->authorizationChecker->isGranted($viewEmbedAttribute);
236
    }
237
238
    /**
239
     * Checks if the view is an embed one.
240
     * Uses either the controller action (embedAction), or the viewType (embed/embed-inline).
241
     *
242
     * @param array $parameters The ViewBuilder parameters array.
243
     *
244
     * @return bool
245
     */
246
    private function isEmbed($parameters)
247
    {
248
        if ($parameters['_controller'] === 'ez_content:embedAction') {
249
            return true;
250
        }
251
        if (in_array($parameters['viewType'], ['embed', 'embed-inline'])) {
252
            return true;
253
        }
254
255
        return false;
256
    }
257
}
258