Completed
Push — ezp26381-content_view_default_... ( 148f87...a8621d )
by
unknown
35:11
created

ContentViewBuilder::loadContent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
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
        $view->setLocation($location);
122
123
        $this->viewParametersInjector->injectViewParameters($view, $parameters);
124
        $this->viewConfigurator->configure($view);
125
126
        // deprecated controller actions are replaced with their new equivalent, viewAction and embedAction
127
        if (!$view->getControllerReference() instanceof ControllerReference) {
128
            if (in_array($parameters['_controller'], ['ez_content:viewLocation', 'ez_content:viewContent'])) {
129
                $view->setControllerReference(new ControllerReference('ez_content:viewAction'));
130
            } elseif (in_array($parameters['_controller'], ['ez_content:embedLocation', 'ez_content:embedContent'])) {
131
                $view->setControllerReference(new ControllerReference('ez_content:embedAction'));
132
            }
133
        }
134
135
        return $view;
136
    }
137
138
    /**
139
     * Loads Content with id $contentId.
140
     *
141
     * @param mixed $contentId
142
     *
143
     * @return \eZ\Publish\API\Repository\Values\Content\Content
144
     *
145
     * @throws \eZ\Publish\Core\Base\Exceptions\UnauthorizedException
146
     */
147
    private function loadContent($contentId)
148
    {
149
        return $this->repository->getContentService()->loadContent($contentId);
150
    }
151
152
    /**
153
     * Loads the embedded content with id $contentId.
154
     * Will load the content with sudo(), and check if the user can view_embed this content, for the given location
155
     * if provided.
156
     *
157
     * @param mixed $contentId
158
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
159
     *
160
     * @return \eZ\Publish\API\Repository\Values\Content\Content
161
     * @throws \eZ\Publish\Core\Base\Exceptions\UnauthorizedException
162
     */
163
    private function loadEmbeddedContent($contentId, Location $location = null)
164
    {
165
        $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...
166
            function (Repository $repository) use ($contentId) {
167
                return $repository->getContentService()->loadContent($contentId);
168
            }
169
        );
170
171
        if (!$this->canRead($content, $location)) {
172
            throw new UnauthorizedException(
173
                'content', 'read|view_embed',
174
                ['contentId' => $contentId, 'locationId' => $location !== null ? $location->id : 'n/a']
175
            );
176
        }
177
178
        // Check that Content is published, since sudo allows loading unpublished content.
179
        if (
180
            $content->getVersionInfo()->status !== VersionInfo::STATUS_PUBLISHED
181
            && !$this->authorizationChecker->isGranted(
182
                new AuthorizationAttribute('content', 'versionread', array('valueObject' => $content))
183
            )
184
        ) {
185
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId]);
186
        }
187
188
        return $content;
189
    }
190
191
    /**
192
     * Loads a visible Location.
193
     * @todo Do we need to handle permissions here ?
194
     *
195
     * @param $locationId
196
     *
197
     * @return \eZ\Publish\API\Repository\Values\Content\Location
198
     */
199
    private function loadLocation($locationId)
200
    {
201
        $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...
202
            function (Repository $repository) use ($locationId) {
203
                return $repository->getLocationService()->loadLocation($locationId);
204
            }
205
        );
206
        if ($location->invisible) {
207
            throw new NotFoundHttpException('Location cannot be displayed as it is flagged as invisible.');
208
        }
209
210
        return $location;
211
    }
212
213
    /**
214
     * Checks if a user can read a content, or view it as an embed.
215
     *
216
     * @param Content $content
217
     * @param $location
218
     *
219
     * @return bool
220
     */
221
    private function canRead(Content $content, Location $location = null)
222
    {
223
        $limitations = ['valueObject' => $content->contentInfo];
224
        if (isset($location)) {
225
            $limitations['targets'] = $location;
226
        }
227
228
        $readAttribute = new AuthorizationAttribute('content', 'read', $limitations);
229
        $viewEmbedAttribute = new AuthorizationAttribute('content', 'view_embed', $limitations);
230
231
        return
232
            $this->authorizationChecker->isGranted($readAttribute) ||
233
            $this->authorizationChecker->isGranted($viewEmbedAttribute);
234
    }
235
236
    /**
237
     * Checks if the view is an embed one.
238
     * Uses either the controller action (embedAction), or the viewType (embed/embed-inline).
239
     *
240
     * @param array $parameters The ViewBuilder parameters array.
241
     *
242
     * @return bool
243
     */
244
    private function isEmbed($parameters)
245
    {
246
        if ($parameters['_controller'] === 'ez_content:embedAction') {
247
            return true;
248
        }
249
        if (in_array($parameters['viewType'], ['embed', 'embed-inline'])) {
250
            return true;
251
        }
252
253
        return false;
254
    }
255
}
256