Completed
Push — master ( 949e77...a1f0e2 )
by
unknown
120:09 queued 86:45
created

ContentViewBuilder   B

Complexity

Total Complexity 33

Size/Duplication

Total Lines 235
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 0
loc 235
rs 8.6166
c 0
b 0
f 0
wmc 33
lcom 1
cbo 15

8 Methods

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