Passed
Push — develop ( 84f32e...989890 )
by
unknown
15:42 queued 41s
created

FileController::init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
dl 0
loc 8
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 0
ccs 0
cts 6
cp 0
crap 2
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\Hash;
13
use neon\core\helpers\Str;
14
use neon\firefly\services\plupload\Receiver;
15
use neon\core\web\Response;
16
use yii\helpers\FileHelper;
17
use yii\web\HttpException;
18
19
class FileController extends \yii\web\Controller
20
{
21
	public function actionDestroyDeleted()
22
	{
23
		return neon()->firefly->mediaManager->destroyDeleted();
24
	}
25
26
	/**
27
	 * @inheritdoc
28
	 */
29
	public function init()
30
	{
31
		parent::init();
32
		// before doing anything we must turn off the session
33
		// this controller is for stateless non session APIs
34
		neon()->user->enableSession = false;
35
		neon()->user->loginUrl = null;
36
		neon()->getRequest()->enableCsrfValidation = false;
37
	}
38
39
	/**
40
	 * Show a file manager managed file
41
	 *
42
	 * /firefly/file/get?id=[fileuuid]
43
	 * /firefly/[fileuuid]
44
	 * @return \neon\core\web\Response
45
	 */
46
	public function actionGet()
47
	{
48
		$request = neon()->request;
49
		$firefly = neon()->firefly;
50
		$id = $request->get('id');
51
		if (!$firefly->fileManager->exists($id)) {
52
			// show placeholder image - should be handled by image manager - controllers should not know
53
			return neon()->response->sendFile(dirname(__DIR__) . '/assets/placeholder.jpg', 'No File', ['inline' => true]);
54
		}
55
56
		// as the url is unique for a file we can be aggressive about caching
57
		// see if we already have this in cache and the browser already has it too
58
		$this->handleNotModified();
59
60
		// set expiry information on the file
61
		$meta = $firefly->fileManager->getMeta($id);
62
		if ($meta['mime_type'] == 'image/svg')
63
			$meta['mime_type'] = 'image/svg+xml';
64
		$this->sendCacheHeaders($meta['mime_type'], $meta['size'], $meta['updated_at'], md5($request->absoluteUrl), $meta['name']);
65
		$download = $request->get('download', $request->get('dl', false));
66
		try {
67
			// this can throw an error if the path is unknown
68
			return $firefly->fileManager->sendFile($id, (bool) $download);
69
		} catch (\Exception $e) {
70
			// if path unknown respond with a blank placeholder
71
			return neon()->response->sendFile(dirname(__DIR__) . '/assets/placeholder.jpg', 'No File', ['inline' => true]);
72
		}
73
	}
74
75
	/**
76
	 * Alternative action url for getting images for more consistent interface
77
	 *
78
	 * @return \neon\core\web\Response
79
	 * @throws HttpException
80
	 */
81
	public function actionImage()
82
	{
83
		return $this->actionImg();
84
	}
85
86
	/**
87
	 * Show a file manager managed image
88
	 * /mydomain.com/firefly/file/img?id=[fileuuid]
89
	 * @throws HttpException if id request parameter is not defined
90
	 * @return Response
91
	 */
92
	public function actionImg()
93
	{
94
		$id = neon()->request->getRequired('id');
95
96
		// create a unique request key - add v2 to force use of new cached version
97
		$requestKey = neon()->request->absoluteUrl . 'v10';
98
99
		// see if we already have this in cache and the browser already has it too
100
		// We want to remove the need to process images when ever possible
101
		// Send not modified header if already cached
102
		$this->handleNotModified($requestKey);
103
104
		$image = $this->handleImageProcessing($requestKey, $id);
105
106
		// This caches this whole request.... by generating the image file
107
		// note we only have the id when originally asking for the image via `getImage()`
108
		// Only images should ever be made public - these have been resaved as images by this point
109
		// $url = $this->generatePublicFile($id, $image['image']);
110
		// return $this->redirect($url);
111
		neon()->response->format = Response::FORMAT_RAW;
112
		$this->sendCacheHeaders($image['mime_type'], $image['size'], $image['updated_at'], $image['eTag'], basename($id));
113
		neon()->response->content = $image['image'];
114
115
		return neon()->response;
116
	}
117
118
119
	public function handleImageProcessing($requestKey, $idOrUrl)
120
	{
121
		// retrieve (or generate) from cache and return with appropriate caching headers
122
		return neon()->firefly->getCache()->getOrSet($requestKey, function() use ($idOrUrl, $requestKey) {
123
			$processedImage = neon()->firefly->imageManager->process($idOrUrl, neon()->request->get());
124
			$mimeType = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $processedImage);
0 ignored issues
show
Bug introduced by
It seems like $processedImage can also be of type false; however, parameter $string of finfo_buffer() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

124
			$mimeType = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), /** @scrutinizer ignore-type */ $processedImage);
Loading history...
125
			// svgs don't seem display in browsers without specifically the svg+xml mime type
126
			if ($mimeType === 'image/svg') $mimeType = 'image/svg+xml';
127
			return [
128
				// recalculate the mime type in case was spoofed
129
				'size' => strlen($processedImage),
0 ignored issues
show
Bug introduced by
It seems like $processedImage can also be of type false; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

129
				'size' => strlen(/** @scrutinizer ignore-type */ $processedImage),
Loading history...
130
				'mime_type' => $mimeType,
131
				'updated_at' => date("Y-m-d H:i:s"),
132
				'image' => $processedImage,
133
				'eTag' => md5($requestKey)
134
			];
135
		});
136
	}
137
138
139
	/**
140
	 * Another potential cache strategy to generate a static asset image file in a publicly accessible folder
141
	 * @param string $id - fileManager file id
142
	 * @param mixed $imageContent - the contents of the image
143
	 * @throws \yii\base\Exception
144
	 * @return string - the public url path
145
	 */
146
	public function generatePublicFile($id, $imageContent)
147
	{
148
		$dir = substr($id,0, 3);
149
		$publicDir = neon()->getAlias("@webroot/assets/firefly/$dir");
150
		if (!file_exists($publicDir)) {
151
			FileHelper::createDirectory($publicDir);
152
			file_put_contents("$publicDir/$id", $imageContent);
153
		}
154
		return neon()->getAlias("@web/assets/firefly/$dir/$id");
155
	}
156
157
	/**
158
	 * Send appropriate cache headers
159
	 * @param string $mime - mime type of file
160
	 * @param string|int $size - size in bytes of response
161
 	 * @param string $modified - MySQL Datetime
162
	 * @param string $eTag - eTag token header to use
163
	 * @param string $name - filename string to use in content disposition header
164
	 */
165
	public function sendCacheHeaders($mime, $size, $modified, $eTag, $name)
166
	{
167
		$headers = neon()->response->headers;
168
		$headers->add('Accept-Ranges', 'bytes');
169
		$headers->add('Content-Type', $mime);
170
		$headers->add('Content-Length', $size);
171
		$headers->add('Access-Control-Allow-Origin', '*');
172
		$headers->add('Last-Modified', Carbon::createFromFormat('Y-m-d H:i:s', $modified)->toRfc1123String());
173
		$headers->add('Cache-Control', 'private, max-age=30000000, immutable, only-if-cached');
174
		$headers->add('Expires', Carbon::now()->addSeconds(30000000)->toRfc1123String());
175
		$headers->add('ETag', $eTag);
176
		$headers->add('Content-Disposition', 'inline; filename="'.$name.'"');
177
	}
178
179
	/**
180
	 * Handle a request that can return a 304 not modified header
181
	 * @param bool|string $cacheKey - default false - if cache key is specified
182
	 */
183
	public function handleNotModified($cacheKey=false)
184
	{
185
		$requestHeaders = neon()->request->headers;
186
		$browserHasCache = (isset($requestHeaders['If-Modified-Since']) || isset($requestHeaders['If-None-Match']));
187
		if ($browserHasCache && (!$cacheKey || neon()->firefly->getCache()->exists($cacheKey))) {
188
			$this->sendNotModifiedHeaders();
189
		}
190
	}
191
192
	/**
193
	 * Send appropriate headers for 304 Not Modified header
194
	 */
195
	public function sendNotModifiedHeaders()
196
	{
197
		header($_SERVER['SERVER_PROTOCOL'].' 304 Not modified');
198
		header('Cache-Control: private, max-age=30000000, immutable, only-if-cached');
199
		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

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