Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 128
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
taoItems_actions_ItemContent
0.00% covered (danger)
0.00%
0 / 128
0.00% covered (danger)
0.00%
0 / 12
1640
0.00% covered (danger)
0.00%
0 / 1
 files
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 fileExists
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 upload
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
132
 download
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 delete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getMediaAsset
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
 resolveAsset
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getRequiredQueryParams
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 buildFilters
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 getAssetTreeBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMediaSourceFileStream
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPermissionChecker
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2014-2021 (original work) Open Assessment Technologies SA;
19 *
20 */
21
22use oat\generis\model\OntologyAwareTrait;
23use oat\tao\helpers\FileUploadException;
24use oat\tao\model\accessControl\Context;
25use oat\tao\model\accessControl\data\PermissionException;
26use oat\tao\model\accessControl\PermissionChecker;
27use oat\tao\model\accessControl\PermissionCheckerInterface;
28use oat\tao\model\http\HttpJsonResponseTrait;
29use oat\tao\model\media\MediaAsset;
30use oat\tao\model\media\MediaBrowser;
31use oat\tao\model\media\mediaSource\DirectorySearchQuery;
32use oat\tao\model\media\ProcessedFileStreamAware;
33use oat\tao\model\media\TaoMediaException;
34use oat\tao\model\resources\ResourceAccessDeniedException;
35use oat\taoItems\model\media\AssetTreeBuilder;
36use oat\taoItems\model\media\AssetTreeBuilderInterface;
37use oat\taoItems\model\media\ItemMediaResolver;
38use oat\taoItems\model\media\LocalItemSource;
39use Psr\Http\Message\StreamInterface;
40use common_exception_MissingParameter as MissingParameterException;
41use tao_models_classes_FileNotFoundException as FileNotFoundException;
42
43/**
44 * Items Content Controller provide access to the files of an item
45 *
46 * @author Joel Bout, <joel@taotesting.com>
47 */
48class taoItems_actions_ItemContent extends tao_actions_CommonModule
49{
50    use HttpJsonResponseTrait;
51    use OntologyAwareTrait;
52
53    /**
54     * @throws MissingParameterException|TaoMediaException
55     */
56    public function files(): void
57    {
58        $params = $this->getRequiredQueryParams('uri', 'lang', 'path');
59        ['uri' => $uri, 'lang' => $lang, 'path' => $path] = $params;
60
61        $depth = (int)($params['depth'] ?? 1);
62        $childrenOffset = (int)($params['childrenOffset'] ?? AssetTreeBuilder::DEFAULT_PAGINATION_OFFSET);
63
64        $filters = $this->buildFilters($params);
65
66        $searchQuery = new DirectorySearchQuery(
67            $this->resolveAsset($uri, $path, $lang),
68            $uri,
69            $lang,
70            $filters,
71            $depth,
72            $childrenOffset
73        );
74
75        $this->setSuccessJsonResponse($this->getAssetTreeBuilder()->build($searchQuery));
76    }
77
78    /**
79     * Returns whenever or not a file exists at the indicated path
80     * @throws MissingParameterException|TaoMediaException
81     */
82    public function fileExists(): void
83    {
84        try {
85            $params = $this->getRequiredQueryParams('uri', 'lang', 'path');
86            $asset = $this->resolveAsset($params['uri'], $params['path'], $params['lang']);
87
88            $asset->getMediaSource()->getFileInfo($asset->getMediaIdentifier());
89            $found = true;
90        } catch (FileNotFoundException $exception) {
91            $found = false;
92        }
93
94        $formatter = $this->getResponseFormatter()
95            ->withJsonHeader()
96            ->withBody(['exists' => $found]);
97        $this->setResponse($formatter->format($this->getPsrResponse()));
98    }
99
100    /**
101     * Upload a file to the item directory
102     */
103    public function upload(): void
104    {
105        $formatter = $this->getResponseFormatter()
106            ->withJsonHeader();
107
108        //as upload may be called multiple times, we remove the session lock as soon as possible
109        try {
110            session_write_close();
111
112            $params = $this->getRequiredQueryParams('uri', 'lang', 'relPath', 'filters');
113            ['filters' => $filters] = $params;
114            $asset = $this->getMediaAsset('uploadAsset');
115
116            $file = tao_helpers_Http::getUploadedFile('content');
117            $fileTmpName = $file['tmp_name'] . '_' . $file['name'];
118
119            if (!tao_helpers_File::copy($file['tmp_name'], $fileTmpName)) {
120                throw new common_exception_Error('impossible to copy ' . $file['tmp_name'] . ' to ' . $fileTmpName);
121            }
122
123            $mime = tao_helpers_File::getMimeType($fileTmpName);
124            if (is_string($filters)) {
125                // the mime type is part of the $filters
126                $filters = explode(',', $filters);
127                if ((in_array($mime, $filters))) {
128                    $fileData = $asset->getMediaSource()->add(
129                        $fileTmpName,
130                        $file['name'],
131                        $asset->getMediaIdentifier()
132                    );
133                } else {
134                    throw new FileUploadException(__('The file you tried to upload is not valid'));
135                }
136            } else {
137                $valid = false;
138                // OR the extension is part of the filter and it correspond to the mime type
139                $fileExtension = tao_helpers_File::getFileExtention($fileTmpName);
140                foreach ($filters as $filter) {
141                    if (
142                        $filter['mime'] === $mime &&
143                        (!isset($filter['extension']) || $filter['extension'] === $fileExtension)
144                    ) {
145                        $valid = true;
146                    }
147                }
148                if ($valid) {
149                    $fileData = $asset->getMediaSource()->add(
150                        $fileTmpName,
151                        $file['name'],
152                        $asset->getMediaIdentifier()
153                    );
154                } else {
155                    throw new FileUploadException(__('The file you tried to upload is not valid'));
156                }
157            }
158
159            $formatter->withBody($fileData);
160        } catch (PermissionException | FileUploadException $e) {
161            $formatter->withBody(['error' => $e->getMessage()]);
162        } catch (common_Exception $e) {
163            $this->logWarning($e->getMessage());
164            $formatter->withBody(['error' => __('Unable to upload file')]);
165        }
166
167        $this->setResponse($formatter->format($this->getPsrResponse()));
168    }
169
170    /**
171     * @throws MissingParameterException|FileNotFoundException|TaoMediaException
172     */
173    public function download(): void
174    {
175        $asset = $this->getMediaAsset('previewAsset');
176
177        $mediaSource = $asset->getMediaSource();
178        $stream = $this->getMediaSourceFileStream($mediaSource, $asset);
179
180        $info = $mediaSource->getFileInfo($asset->getMediaIdentifier());
181        $mime = $info['mime'] !== 'application/qti+xml' ? $info['mime'] : null;
182
183        tao_helpers_Http::returnStream($stream, $mime, $this->getPsrRequest());
184    }
185
186    /**
187     * Delete a file from the item directory
188     *
189     * @throws MissingParameterException|TaoMediaException
190     */
191    public function delete(): void
192    {
193        $asset = $this->getMediaAsset('deleteAsset');
194
195        $deleted = $asset->getMediaSource()->delete($asset->getMediaIdentifier());
196
197        $formatter = $this->getResponseFormatter()
198            ->withJsonHeader()
199            ->withBody(['deleted' => $deleted]);
200        $this->setResponse($formatter->format($this->getPsrResponse()));
201    }
202
203    private function getMediaAsset(string $action): ?MediaAsset
204    {
205        $isWrite = in_array($action, ['deleteAsset', 'uploadAsset'], true);
206        $params = $this->getRequiredQueryParams('uri', 'lang');
207        $queryParams = $this->getPsrRequest()->getQueryParams();
208        $path = $queryParams['relPath'] ?? $queryParams['path'] ?? null;
209        $asset = $this->resolveAsset($params['uri'], $path, $params['lang']);
210        $resourceUri = $params['uri'];
211        $hasAccess = true;
212
213        // We do not want to validate access for item gallery
214        if (!$asset->getMediaSource() instanceof LocalItemSource) {
215            $resourceUri = tao_helpers_Uri::decode($asset->getMediaIdentifier());
216            $context = new Context(
217                [
218                    Context::PARAM_CONTROLLER => taoItems_actions_ItemContent::class,
219                    Context::PARAM_ACTION => $action,
220                ]
221            );
222            $hasAccess = $isWrite ? $this->hasWriteAccessByContext($context) : $this->hasReadAccessByContext($context);
223        }
224
225        if (!$isWrite) {
226            $hasAccess = $hasAccess && $this->getPermissionChecker()->hasReadAccess($resourceUri);
227        }
228
229        if ($isWrite) {
230            $hasAccess = $hasAccess && $this->getPermissionChecker()->hasWriteAccess($resourceUri);
231        }
232
233        if (!$hasAccess) {
234            throw new ResourceAccessDeniedException($resourceUri);
235        }
236
237        return $asset;
238    }
239
240    /**
241     * @throws TaoMediaException
242     */
243    private function resolveAsset(string $url, string $path, string $lang): MediaAsset
244    {
245        $item = $this->getResource($url);
246        $resolver = new ItemMediaResolver($item, $lang);
247
248        return $resolver->resolve($path);
249    }
250
251    /**
252     * Returns Query params if mandatory params not empty
253     *
254     * @throws MissingParameterException
255     */
256    private function getRequiredQueryParams(string ...$requiredKeys): array
257    {
258        $params = $this->getPsrRequest()->getQueryParams();
259        foreach ($requiredKeys as $key) {
260            if (!array_key_exists($key, $params) || empty($params[$key])) {
261                throw new MissingParameterException($key, __METHOD__);
262            }
263        }
264
265        return $params;
266    }
267
268    private function buildFilters(array $params): array
269    {
270        $filters = [];
271        if (isset($params['filters'])) {
272            $filterParameter = $params['filters'];
273            if (is_array($filterParameter)) {
274                foreach ($filterParameter as $filter) {
275                    if (preg_match('/\/\*/', $filter['mime'])) {
276                        $this->logWarning(
277                            'Stars mime type are not yet supported, filter "' . $filter['mime'] . '" will fail'
278                        );
279                    }
280                    $filters[] = $filter['mime'];
281                }
282            } else {
283                if (preg_match('/\/\*/', $filterParameter)) {
284                    $this->logWarning(
285                        'Stars mime type are not yet supported, filter "' . $filterParameter . '" will fail'
286                    );
287                }
288                $filters = array_map('trim', explode(',', $filterParameter));
289            }
290        }
291        return $filters;
292    }
293
294    private function getAssetTreeBuilder(): AssetTreeBuilderInterface
295    {
296        return $this->getServiceLocator()->get(AssetTreeBuilder::SERVICE_ID);
297    }
298
299    /**
300     * @throws FileNotFoundException
301     */
302    private function getMediaSourceFileStream(MediaBrowser $mediaSource, MediaAsset $asset): StreamInterface
303    {
304        if ($mediaSource instanceof ProcessedFileStreamAware) {
305            return $mediaSource->getProcessedFileStream($asset->getMediaIdentifier());
306        }
307
308        return $mediaSource->getFileStream($asset->getMediaIdentifier());
309    }
310
311    private function getPermissionChecker(): PermissionCheckerInterface
312    {
313        return $this->getServiceLocator()->get(PermissionChecker::class);
314    }
315}