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