7 class File extends \Rsi\Fred\Controller\Widget{
57 $config = array_merge(parent::clientConfig(),$this->
get([self::TYPES]));
58 if(!$this->max) $config[
self::MAX] = \Rsi\Number::shorthandBytes(ini_get(
'post_max_size'));
59 if($this->trailer ===
null) $config[
self::TRAILER] =
'%n (%s %u)';
62 $config[
'fileExists'] = is_file($this->filename) || $this->filenames;
63 if($this->sideloadControllerName && $this->sideloadPath && !$this->_controller->component(
'client')->mobile){
65 foreach($this->types as $type)
if($image = preg_match(
'/^(image|jpe?g)/i',$type))
break;
68 if($this->tusPath && (($this->max > ($limit = min(
69 \
Rsi\Number::shorthandBytes(ini_get(
'upload_max_filesize')),
70 $chunk = \Rsi::memoryLimit() * $this->memoryFactor - $this->memoryMargin
72 $config[
'asyncLimit'] = $limit;
73 $config[
'tusChunkSize'] = $this->tusChunkSize ?: min($chunk,\
Rsi\Number::shorthandBytes(ini_get(
'post_max_size')));
77 if($this->userFiles && $this->_controller->component(
'user')->id) $config[
self::USER_FILES] =
true;
88 if(is_array($value)) $value = array_filter($value,
function($file){
89 return !is_array($file) || (($file[
'error'] ??
null) != 4) || array_filter([
'error' =>
null] + $file);
91 if($value && is_array($value)) $value = $this->multi
92 ? array_column($this->_multiRaw = $value,
'tmp_name')
93 : \Rsi\Record::get($this->_raw = $value,
'tmp_name');
94 elseif(!$value && $this->filename && (is_array($value = $this->filenames) || is_file($value = $this->filename)));
96 return parent::purgeBase($value);
100 return $this->_controller->component(
'event')->trigger(self::EVENT_UPLOAD,$this,$name);
104 return parent::checkMin(filesize($value));
108 return parent::checkMax(filesize($value));
112 return !$this->_raw || (is_uploaded_file($value) && !$this->_raw[
'error']);
116 if($this->_raw) $value = $this->_raw[
'name'];
117 if(!$this->types || in_array(\
Rsi\File::ext($value),$this->types))
return true;
118 $mime = \Rsi\File::mime($value);
119 foreach($this->types as $type)
120 if(preg_match(substr($type,0,1) ==
'/' ? $type :
'/^' . str_replace(
'*',
'.*',$type) .
'$/',$mime))
return true;
124 public function check($value,$index =
null){
125 if(is_array($value) && $this->filename){
128 elseif($this->_multiRaw){
129 foreach($this->_multiRaw as $this->_raw)
if($error = parent::check($this->_raw[
'tmp_name'],$index))
break;
131 else $error = parent::check($value,$index);
132 if(!$error && $value && !is_array($value) && $this->filename && $this->_raw){
133 if(!move_uploaded_file($value,$this->filename))
134 throw new \Exception(
"Can not move uploaded file from '$value' to '{$this->filename}'");
135 else $error = $this->
uploadEvent($this->_raw[
'name']);
144 $image = new \Rsi\Wrapper\Image(file_get_contents(
$filename));
145 $factor = min($this->previewSize / ($width = $image->width),$this->previewSize / ($height = $image->height));
146 $image->_scale(round($factor * $width),round($factor * $height));
149 $preview[] = ob_get_clean();
150 $rotate = function_exists(
'image' . $image->ext(
$filename));
153 $this->log->info($e);
154 if(class_exists(
'Imagick')){
155 $temp = \Rsi\File::tempFile(
'png');
158 foreach($image as $page)
if($count++ < $this->previewMax){
159 $page->setImageBackgroundColor(
'white');
160 $page->setImageFormat(
'png');
161 $page->thumbnailImage($this->previewSize,0);
162 $page->writeImage($temp);
163 $preview[] = file_get_contents($temp);
168 $this->log->info($e);
170 \Rsi\File::unlink($temp);
173 if($preview = $this->services->preview(
$filename,$this->previewSize,$this->previewMax)[
'preview'] ??
null)
174 $preview = array_map(
'base64_decode',$preview);
177 $this->log->info($e);
180 return $preview ? compact(
'preview',
'rotate') :
null;
193 session_write_close();
194 if($rotate = boolval($this->previewSize))
foreach($filenames as
$filename)
if(is_file(
$filename)){
198 ($file = $this->fred->cache->fetch($key = $this->previewKeyPrefix .
$filename .
'@' . $this->previewSize)) &&
203 $preview = array_merge($preview,$file[
'preview']);
204 if(!$file[
'rotate']) $rotate =
false;
205 if($key) $this->fred->cache->save($key,$file + [
'time' => time()],$this->previewTtl);
209 $count = count($preview);
210 return array_slice($preview,0,$this->previewMax);
222 $image = new \Rsi\Wrapper\Image(
$filename);
223 if($angle > 0) $image->_rotate(-$angle,$image->colorAllocate(255,255,255));
225 case -1: $image->flip(IMG_FLIP_HORIZONTAL);
break;
226 case -2: $image->flip(IMG_FLIP_VERTICAL);
break;
231 $this->log->info($e);
242 $size = filesize($temp);
243 if($error = $this->
check($temp)) \Rsi\File::unlink($temp);
244 elseif(!\
Rsi\File::rename($temp,
$filename = str_replace(
'*',microtime(
true),$this->filename)))
245 throw new \Exception(
"Could not rename('$temp','$filename')");
248 foreach($this->
preview(
null,$count,$rotate) as $image) $preview[] = base64_encode($image);
249 $this->request->result = compact(
'error',
'name',
'size',
'preview',
'count',
'rotate') + [
'fileExists' => $error ? false : is_file(
$filename)];
252 \Rsi\File::unlink($temp);
262 return $this->sideloadPath . $token . ($index ===
null ?
'' :
'-' . $index) . $this->sideloadExt;
270 return \Rsi\Http::host(
true) . $this->_controller->component(
'router')->reverse($this->sideloadControllerName,
null,compact(
'token'));
276 if($this->sideloadPath && !rand(0,$this->sideloadGarbageChance))
try{
277 foreach((
new \GlobIterator($this->sideloadPath .
'*' . $this->sideloadExt)) as
$filename => $file)
278 if($file->getMTime() < time() - 2 * $this->sideloadInterval) \Rsi\File::unlink(
$filename);
279 $session = $this->_controller->session;
280 if($tokens = $session->sideloadTokens){
281 foreach($tokens as $token => $time)
if(!is_file($this->
sideloadFilename($token))) unset($tokens[$token]);
282 $session->sideloadTokens = $tokens;
286 $this->log->error($e);
291 http_response_code($code);
292 header(
'Content-Type: text/plain');
294 $this->fred->log->info(
"TUS error ($code): $message");
298 if(($index = $_SERVER[
'HTTP_' . strtoupper(str_replace(
'-',
'_',$header))] ??
null) ===
null) $this->
tusError(400,
"Missing $header header");
299 elseif(($index = filter_var($index,FILTER_VALIDATE_INT,[
'options' => [
'min_range' => 0]])) ===
false) $this->
tusError(400,
"Invalid $header header");
309 if($this->tusPath && !rand(0,$this->tusGarbageChance))
try{
310 foreach((
new \GlobIterator($this->tusPath .
'*' . $this->tusExt)) as
$filename => $file)
311 if($file->getMTime() < time() - $this->tusTimeout){
312 \Rsi\File::unlink(
$filename . $this->tusTempExt);
317 $this->log->error($e);
323 $count = $rotate =
false;
324 if($this->_display >= self::DISPLAY_READABLE)
325 foreach($this->
preview(
null,$count,$rotate) as $image) $preview[] = base64_encode($image);
326 $this->request->result = compact(
'preview',
'count',
'rotate');
330 if($this->_display >= self::DISPLAY_READABLE){
331 if(!is_array($filenames = $this->filenames)) $filenames = [
$this->filename];
332 if((
$filename = $filenames[intval($this->request->index)] ??
null) && is_file(
$filename)){
342 ($this->_display >= self::DISPLAY_WRITEABLE) &&
344 ($temp = \
Rsi\Record::get($this->request->file(
'data'),
'tmp_name'))
349 if($this->_display >= self::DISPLAY_WRITEABLE){
350 session_write_close();
351 $this->
rotate((
int)$this->request->angle);
353 foreach($this->
preview(
null,$count,$rotate) as $image) $preview[] = base64_encode($image);
354 clearstatcache(
false,$this->filename);
355 $this->request->result = compact(
'preview',
'count',
'rotate') + [
'size' => \Rsi\File::size($this->filename)];
360 if(($this->_display >= self::DISPLAY_WRITEABLE) && !$this->required && $this->filename){
361 if(!is_array($filenames = $this->filenames)) $filenames = [
$this->filename];
362 $this->request->result = [
'deleted' => $filenames ? array_sum(array_map(
'\Rsi\File::unlink',$filenames)) :
true];
367 if(($this->_display >= self::DISPLAY_WRITEABLE) && $this->userFiles) $this->request->result = [
368 'files' => $this->_controller->component(
'user')->files->list($this->types),
369 'preview' => (bool)$this->previewSize
374 if($data = $this->_controller->component(
'user')->files->fetch($this->request->name)){
375 session_write_close();
376 $temp = \Rsi\File::tempFile(
'tmp',$data);
378 foreach($this->
preview($temp) as $image) $preview[] = base64_encode($image);
380 $this->request->result = $preview;
385 if(($this->_display >= self::DISPLAY_WRITEABLE) && $this->filename && $this->userFiles){
386 $data = $this->_controller->component(
'user')->files->fetch(
$filename = $this->request->name);
387 if(($data !==
false) && ($data !==
null))
393 if(($this->_display >= self::DISPLAY_WRITEABLE) && $this->filename && $this->sideloadPath && $this->sideloadControllerName){
396 \Rsi\File::serialize(
400 'config' => array_merge($this->_config,[self::FILENAME =>
null]),
401 'time' => $time = time()
405 $session = $this->_controller->session;
406 $tokens = $session->sideloadTokens ?: [];
407 $tokens[$token] = $time;
408 $session->sideloadTokens = $tokens;
410 $this->request->result = [
414 'hint' => $this->trans->str($this->sideloadHint,compact(
'token',
'url'))
420 if($token = $this->sideloadToken){
421 header(
'Content-Type: image/png');
422 imagepng((
new Qr(Qr::ECC_MEDIUM,
null,10))->dataImage($this->
sideloadUrl($token)));
431 while($skip++ < $this->sideloadSkip)
434 $this->
processUpload(\
Rsi\File::tempFile(\
Rsi\File::ext($this->filename),$data[
'data']),$data[
'name']);
438 $this->log->warning(
'Error while processig sideload file: ' . $e->getMessage(),__FILE__,__LINE__);
446 if(($this->_display < self::DISPLAY_WRITEABLE) || !$this->filename || !$this->tusPath) http_response_code(404);
447 elseif(($method = \
Rsi\Http::method()) == \
Rsi\Http::METHOD_OPTIONS){
448 header(
'Tus-Resumable: ' . self::TUS_VERSION);
449 header(
'Tus-Version: ' . self::TUS_VERSION);
450 header(
'Tus-Extension: ' . self::TUS_EXTENSIONS);
451 if($this->max) header(
'Tus-Max-Size: ' . $this->max);
452 header(
'Tus-Checksum-Algorithm: ' . implode(
',',hash_algos()));
453 http_response_code(204);
455 elseif($this->request->server->http->tusResumable != self::TUS_VERSION){
456 header(
'Tus-Version: ' . self::TUS_VERSION);
457 $this->
tusError(412,
'Unsupported Tus-Resumable version. Server supports ' . self::TUS_VERSION);
459 elseif($method == \
Rsi\Http::METHOD_POST){
462 if(($length = $this->
tusIndex(
'Upload-Length')) !==
false){
463 if($this->max && ($length > $this->max)) $this->
tusError(413,
"Upload-Length exceeds maximum allowed size of {$this->max} bytes");
468 foreach(array_filter(explode(
',',$this->request->server->http->uploadMetadata)) as $data){
469 $data = explode(
' ',$data,2);
470 $meta[$data[0]] = count($data) > 1 ? base64_decode($data[1]) :
null;
473 \Rsi\File::serialize(
483 file_put_contents(
$filename . $this->tusTempExt,
null);
484 header(
"Location: {$this->request->server->requestUri}&token=$token");
485 header(
'Tus-Resumable: ' . self::TUS_VERSION);
486 http_response_code(201);
491 ($token = $this->request->token) &&
492 ($data = \
Rsi\File::unserialize(
$filename = $this->tusFilename($token))) &&
493 is_file($temp =
$filename . $this->tusTempExt)
495 case \Rsi\Http::METHOD_HEAD:
496 header(
'Upload-Offset: ' . $data[
'offset']);
497 header(
'Upload-Length: ' . $data[
'length']);
498 header(
'Cache-Control: no-store');
499 header(
'Tus-Resumable: ' . self::TUS_VERSION);
501 foreach($data[
'meta'] as $key => $value) $meta[] = $key . ($value ===
null ? null :
' ' . base64_encode($value));
502 if($meta) header(
'Upload-Metadata: ' . implode(
',',$meta));
503 http_response_code(200);
505 case \Rsi\Http::METHOD_PATCH:
506 if($data[
'complete']) $this->
tusError(400,
'Upload already complete');
507 elseif(($_SERVER[
'CONTENT_TYPE'] ??
null) !=
'application/offset+octet-stream') $this->
tusError(415,
'Content-Type must be application/offset+octet-stream');
508 elseif(($offset = $this->
tusIndex(
'Upload-Offset')) !==
false){
509 if($offset != $data[
'offset']) $this->
tusError(409,
"Offset mismatch: expected {$info['offset']}, got $offset");
510 elseif($offset != filesize($temp)) $this->
tusError(500,
'Upload file wrong size');
511 elseif(($chunk = file_get_contents(
'php://input')) ===
false) $this->
tusError(500,
'Failed to read request body');
514 if($checksum = $this->request->server->http->uploadChecksum){
515 if($error = (count($checksum = explode(
' ',$checksum)) != 2)) $this->
tusError(400,
'Invalid Upload-Checksum header');
516 elseif($error = !in_array($algo = strtolower($checksum[0]),hash_algos())) $this->
tusError(400,
'Checksum algorithm not supported');
517 elseif($error = !hash_equals(base64_encode(hash($algo,$chunk,
true)),$checksum[1])) $this->
tusError(460,
'Checksum mismatch');
520 if(($length = $data[
'offset'] + strlen($chunk)) > $data[
'length']) $tusError(416,
'Resulting file size over total length');
522 file_put_contents($temp,$chunk,FILE_APPEND);
523 if($data[
'complete'] = ($data[
'offset'] = $length) == $data[
'length']){
525 header(
'Upload-Info: ' . json_encode($this->request->result));
528 header(
'Upload-Offset: ' . $length);
529 header(
'Tus-Resumable: ' . self::TUS_VERSION);
530 http_response_code(204);
536 case \Rsi\Http::METHOD_DELETE:
537 \Rsi\File::unlink(
$filename . $this->tusTempExt);
539 header(
'Tus-Resumable: ' . self::TUS_VERSION);
540 http_response_code(204);
543 http_response_code(404);
545 else http_response_code(404);
550 return $this->multi && strpos($this->filename,
'*')
551 ? \Rsi\Record::sort(\
Rsi\File::dir(dirname($this->filename),basename($this->filename)))
556 $session = $this->_controller->session;
558 ($tokens = $session->sideloadTokens) &&
559 array_key_exists($token = $this->request->token,$tokens) &&
560 ($tokens[$token] > ($time = time()) - 2 * $this->sideloadInterval)
562 $tokens[$token] = $time;
563 $session->sideloadTokens = $tokens;