Passed
Push — 2.3.3 ( ...6e0367 )
by steve
19:55
created

FileController::getName()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 8
ccs 0
cts 8
cp 0
crap 12
rs 10
1
<?php
2
/**
3
 * @link http://www.newicon.net/neon
4
 * @copyright Copyright (c) 28/11/2016 Newicon Ltd
5
 * @license http://www.newicon.net/neon/license/
6
 */
7
8
namespace neon\firefly\controllers;
9
10
use Carbon\Carbon;
11
use neon\core\helpers\Arr;
12
use neon\core\helpers\File;
13
use neon\core\helpers\Hash;
14
use neon\core\helpers\Str;
15
use neon\firefly\services\plupload\Receiver;
16
use neon\core\web\Response;
17
use yii\helpers\FileHelper;
18
use yii\web\HttpException;
19
20
class FileController extends \yii\web\Controller
21
{
22
	public function actionDestroyDeleted()
23
	{
24
		return neon()->firefly->mediaManager->destroyDeleted();
25
	}
26
27
	/**
28
	 * Show a file manager managed file
29
	 *
30
	 * /firefly/file/get?id=[fileuuid]
31
	 * /firefly/[fileuuid]
32
	 * @return \neon\core\web\Response
33
	 */
34
	public function actionGet()
35
	{
36
		$request = neon()->request;
37
		$firefly = neon()->firefly;
38
		$id = $request->get('id');
39
		if (!$firefly->fileManager->exists($id)) {
40
			// show placeholder image - should be handled by image manager - controllers should not know
41
			return neon()->response->sendFile(dirname(__DIR__) . '/assets/placeholder.jpg', 'No File', ['inline' => true]);
42
		}
43
44
		// as the url is unique for a file we can be aggressive about caching
45
		// see if we already have this in cache and the browser already has it too
46
		$this->handleNotModified();
47
48
		// set expiry information on the file
49
		$meta = $firefly->fileManager->getMeta($id);
50
		$this->sendCacheHeaders($meta['mime_type'], $meta['size'], $meta['updated_at'], md5($request->absoluteUrl), $meta['name']);
51
		$download = $request->get($meta['name'], $request->get('dl', false));
52
		try {
53
			// this can throw an error if the path is unknown
54
			return $firefly->fileManager->sendFile($id, (bool) $download);
55
		} catch (\Exception $e) {
56
			// if path unknown respond with a blank placeholder
57
			return neon()->response->sendFile(dirname(__DIR__) . '/assets/placeholder.jpg', 'No File', ['inline' => true]);
58
		}
59
	}
60
61
	/**
62
	 * Alternative action url for getting images for more consistent interface
63
	 *
64
	 * @return \neon\core\web\Response
65
	 * @throws HttpException
66
	 */
67
	public function actionImage()
68
	{
69
		return $this->actionImg();
70
	}
71
72
	/**
73
	 * Show a file manager managed image
74
	 * /mydomain.com/firefly/file/img?id=[fileuuid]
75
	 * @throws HttpException if id request parameter is not defined
76
	 * @return Response
77
	 */
78
	public function actionImg()
79
	{
80
		$id = neon()->request->getRequired('id');
81
82
		// create a unique request key - add v2 to force use of new cached version
83
		$requestKey = neon()->request->absoluteUrl . 'v6';
84
85
		// see if we already have this in cache and the browser already has it too
86
		// We want to remove the need to process images when ever possible
87
		// Send not modified header if already cached
88
		$this->handleNotModified($requestKey);
89
90
		try {
91
			$image = $this->handleImageProcessing($requestKey, $id);
92
			// This caches this whole request.... by generating the image file
93
			// note we only have the id when originally asking for the image via `getImage()`
94
			// Only images should ever be made public - these have been resaved as images by this point
95
			// $url = $this->generatePublicFile($id, $image['image']);
96
			// return $this->redirect($url);
97
			neon()->response->format = Response::FORMAT_RAW;
98
			$this->sendCacheHeaders($image['mime_type'], $image['size'], $image['updated_at'], $image['eTag'], $image["name"]);
99
			neon()->response->content = $image['image'];
100
			return neon()->response;
101
102
		} catch (\Intervention\Image\Exception\NotReadableException $e) {
103
			return neon()->firefly->fileManager->sendFile($id, true);
104
		}
105
	}
106
107
108
	public function handleImageProcessing($requestKey, $idOrUrl)
109
	{
110
		// retrieve (or generate) from cache and return with appropriate caching headers
111
		return neon()->firefly->getCache()->getOrSet($requestKey, function() use ($idOrUrl, $requestKey) {
112
			$processedImage = neon()->firefly->imageManager->process($idOrUrl, neon()->request->get());
113
			$mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $processedImage);
114
			$name = $this->getName($idOrUrl);
115
			$extensions = File::getExtensionsByMimeType($mime);
116
			$extension = empty($extensions) ? '' : collect($extensions)->last();
117
			return [
118
				'name' => "$name.$extension",
119
				// recalculate the mime type in case was spoofed
120
				'size' => strlen($processedImage),
121
				'mime_type' => $mime,
122
				'updated_at' => date("Y-m-d H:i:s"),
123
				'image' => $processedImage,
124
				'eTag' => md5($requestKey),
125
			];
126
		});
127
	}
128
129
	public function getName($idOrUrl)
130
	{
131
		if (Hash::isUuid64($idOrUrl)) {
132
			if (!neon()->firefly->fileManager->exists($idOrUrl)) throw new HttpException(404);
133
			$meta = neon()->firefly->fileManager->getMeta($idOrUrl);
134
			return $meta['name'];
135
		}
136
		return basename($idOrUrl);
137
	}
138
139
140
	/**
141
	 * Another potential cache strategy to generate a static asset image file in a publicly accessible folder
142
	 * @param string $id - fileManager file id
143
	 * @param mixed $imageContent - the contents of the image
144
	 * @throws \yii\base\Exception
145
	 * @return string - the public url path
146
	 */
147
	public function generatePublicFile($id, $imageContent)
148
	{
149
		$dir = substr($id,0, 3);
150
		$publicDir = neon()->getAlias("@webroot/assets/firefly/$dir");
151
		if (!file_exists($publicDir)) {
152
			FileHelper::createDirectory($publicDir);
153
			file_put_contents("$publicDir/$id", $imageContent);
154
		}
155
		return neon()->getAlias("@web/assets/firefly/$dir/$id");
156
	}
157
158
	/**
159
	 * Send appropriate cache headers
160
	 * @param string $mime - mime type of file
161
	 * @param string|int $size - size in bytes of response
162
 	 * @param string $modified - MySQL Datetime
163
	 * @param string $eTag - eTag token header to use
164
	 * @param string $name - filename string to use in content disposition header
165
	 */
166
	public function sendCacheHeaders($mime, $size, $modified, $eTag, $name)
167
	{
168
		$headers = neon()->response->headers;
169
		$headers->add('Accept-Ranges', 'bytes');
170
		$headers->add('Content-Type', $mime);
171
		$headers->add('Content-Length', $size);
172
		$headers->add('Access-Control-Allow-Origin', '*');
173
		$headers->add('Last-Modified', Carbon::createFromFormat('Y-m-d H:i:s', $modified)->toRfc1123String());
174
		$headers->add('Cache-Control', 'private, max-age=30000000, immutable, only-if-cached');
175
		$headers->add('Expires', Carbon::now()->addSeconds(30000000)->toRfc1123String());
176
		$headers->add('ETag', $eTag);
177
		$headers->add('Content-Disposition', 'inline; filename="'.$name.'"');
178
	}
179
180
	/**
181
	 * Handle a request that can return a 304 not modified header
182
	 * @param bool|string $cacheKey - default false - if cache key is specified
183
	 */
184
	public function handleNotModified($cacheKey=false)
185
	{
186
		$requestHeaders = neon()->request->headers;
187
		$browserHasCache = (isset($requestHeaders['If-Modified-Since']) || isset($requestHeaders['If-None-Match']));
188
		if ($browserHasCache && (!$cacheKey || neon()->firefly->getCache()->exists($cacheKey))) {
189
			$this->sendNotModifiedHeaders();
190
		}
191
	}
192
193
	/**
194
	 * Send appropriate headers for 304 Not Modified header
195
	 */
196
	public function sendNotModifiedHeaders()
197
	{
198
		header($_SERVER['SERVER_PROTOCOL'].' 304 Not modified');
199
		header('Cache-Control: private, max-age=30000000, immutable, only-if-cached');
200
		header('Expires: ' . Carbon::now()->addYear(10)->toRfc1123String());
0 ignored issues
show
Unused Code introduced by
The call to Carbon\Carbon::addYear() has too many arguments starting with 10. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

200
		header('Expires: ' . Carbon::now()->/** @scrutinizer ignore-call */ addYear(10)->toRfc1123String());

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
201
		exit;
202
	}
203
204
	/**
205
	 * API endpoint for plupload
206
	 * @return array
207
	 */
208
	public function actionUpload()
209
	{
210
		$r = new Receiver(neon()->request);
211
		return $r->receiveToFirefly('file');
212
	}
213
}
214