Completed
Push — EZP-31644 ( 2e0a1e...93bb44 )
by
unknown
19:12
created

ContentViewBuilder   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 14
dl 0
loc 226
rs 9.76
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A matches() 0 4 1
F buildView() 0 63 17
A loadContent() 0 4 1
A loadEmbeddedContent() 0 25 5
A loadLocation() 0 13 2
A canRead() 0 8 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\Exception\HiddenLocationException;
17
use eZ\Publish\Core\MVC\Symfony\View\Configurator;
18
use eZ\Publish\Core\MVC\Symfony\View\ContentView;
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
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 \eZ\Publish\API\Repository\PermissionResolver */
32
    private $permissionResolver;
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
    /**
47
     * @var \eZ\Publish\Core\Helper\ContentInfoLocationLoader
48
     */
49
    private $locationLoader;
50
51
    public function __construct(
52
        Repository $repository,
53
        Configurator $viewConfigurator,
54
        ParametersInjector $viewParametersInjector,
55
        ContentInfoLocationLoader $locationLoader = null
56
    ) {
57
        $this->repository = $repository;
58
        $this->viewConfigurator = $viewConfigurator;
59
        $this->viewParametersInjector = $viewParametersInjector;
60
        $this->locationLoader = $locationLoader;
61
        $this->permissionResolver = $this->repository->getPermissionResolver();
62
    }
63
64
    public function matches($argument)
65
    {
66
        return strpos($argument, 'ez_content:') !== false;
67
    }
68
69
    /**
70
     * @param array $parameters
71
     *
72
     * @return \eZ\Publish\Core\MVC\Symfony\View\ContentView|\eZ\Publish\Core\MVC\Symfony\View\View
73
     *         If both contentId and locationId parameters are missing
74
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
75
     *         If both contentId and locationId parameters are missing
76
     * @throws \eZ\Publish\Core\Base\Exceptions\UnauthorizedException
77
     */
78
    public function buildView(array $parameters)
79
    {
80
        $view = new ContentView(null, [], $parameters['viewType']);
81
        $view->setIsEmbed($this->isEmbed($parameters));
82
83
        if ($view->isEmbed() && $parameters['viewType'] === null) {
84
            $view->setViewType(EmbedView::DEFAULT_VIEW_TYPE);
85
        }
86
87
        if (isset($parameters['locationId'])) {
88
            $location = $this->loadLocation($parameters['locationId']);
89
        } elseif (isset($parameters['location'])) {
90
            $location = $parameters['location'];
91
        } else {
92
            $location = null;
93
        }
94
95
        if (isset($parameters['content'])) {
96
            $content = $parameters['content'];
97
        } else {
98
            if (isset($parameters['contentId'])) {
99
                $contentId = $parameters['contentId'];
100
            } elseif (isset($location)) {
101
                $contentId = $location->contentId;
102
            } else {
103
                throw new InvalidArgumentException('Content', 'No content could be loaded from parameters');
104
            }
105
106
            $content = $view->isEmbed() ? $this->loadEmbeddedContent($contentId, $location) : $this->loadContent($contentId);
107
        }
108
109
        $view->setContent($content);
110
111
        if (isset($location)) {
112
            if ($location->contentId !== $content->id) {
113
                throw new InvalidArgumentException('Location', 'Provided location does not belong to selected content');
114
            }
115
        } elseif (isset($this->locationLoader)) {
116
            try {
117
                $location = $this->locationLoader->loadLocation($content->contentInfo);
118
            } catch (NotFoundException $e) {
119
                // nothing else to do
120
            }
121
        }
122
123
        if (isset($location)) {
124
            $view->setLocation($location);
125
        }
126
127
        $this->viewParametersInjector->injectViewParameters($view, $parameters);
128
        $this->viewConfigurator->configure($view);
129
130
        // deprecated controller actions are replaced with their new equivalent, viewAction and embedAction
131
        if (!$view->getControllerReference() instanceof ControllerReference) {
132
            if (in_array($parameters['_controller'], ['ez_content:viewLocation', 'ez_content:viewContent'])) {
133
                $view->setControllerReference(new ControllerReference('ez_content:viewAction'));
134
            } elseif (in_array($parameters['_controller'], ['ez_content:embedLocation', 'ez_content:embedContent'])) {
135
                $view->setControllerReference(new ControllerReference('ez_content:embedAction'));
136
            }
137
        }
138
139
        return $view;
140
    }
141
142
    /**
143
     * Loads Content with id $contentId.
144
     *
145
     * @param mixed $contentId
146
     *
147
     * @return \eZ\Publish\API\Repository\Values\Content\Content
148
     *
149
     * @throws \eZ\Publish\Core\Base\Exceptions\UnauthorizedException
150
     */
151
    private function loadContent($contentId)
152
    {
153
        return $this->repository->getContentService()->loadContent($contentId);
154
    }
155
156
    /**
157
     * Loads the embedded content with id $contentId.
158
     * Will load the content with sudo(), and check if the user can view_embed this content, for the given location
159
     * if provided.
160
     *
161
     * @param mixed $contentId
162
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
163
     *
164
     * @return \eZ\Publish\API\Repository\Values\Content\Content
165
     * @throws \eZ\Publish\Core\Base\Exceptions\UnauthorizedException
166
     */
167
    private function loadEmbeddedContent($contentId, Location $location = null)
168
    {
169
        $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...
170
            function (Repository $repository) use ($contentId) {
171
                return $repository->getContentService()->loadContent($contentId);
172
            }
173
        );
174
175
        if (!$this->canRead($content, $location)) {
176
            throw new UnauthorizedException(
177
                'content', 'read|view_embed',
178
                ['contentId' => $contentId, 'locationId' => $location !== null ? $location->id : 'n/a']
179
            );
180
        }
181
182
        // Check that Content is published, since sudo allows loading unpublished content.
183
        if (
184
            $content->getVersionInfo()->status !== VersionInfo::STATUS_PUBLISHED
185
            && !$this->permissionResolver->canUser('content', 'versionread', $content)
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 HiddenLocationException($location, '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
        $targets = isset($location) ? [$location] : [];
226
227
        return
228
            $this->permissionResolver->canUser('content', 'read', $content->contentInfo, $targets) ||
229
            $this->permissionResolver->canUser('content', 'view_embed', $content->contentInfo, $targets);
230
    }
231
232
    /**
233
     * Checks if the view is an embed one.
234
     * Uses either the controller action (embedAction), or the viewType (embed/embed-inline).
235
     *
236
     * @param array $parameters The ViewBuilder parameters array.
237
     *
238
     * @return bool
239
     */
240
    private function isEmbed($parameters)
241
    {
242
        if ($parameters['_controller'] === 'ez_content:embedAction') {
243
            return true;
244
        }
245
        if (in_array($parameters['viewType'], ['embed', 'embed-inline'])) {
246
            return true;
247
        }
248
249
        return false;
250
    }
251
}
252