FRED™  3.0
FRED™: Framework for Rapid and Easy Development
File.php
Go to the documentation of this file.
1 <?php
2 
4 
5 use \Rsi\Qr;
6 
7 class File extends \Rsi\Fred\Controller\Widget{
8 
9  const TYPES = 'types'; //!< Allowed file types (mime or extension; mime can have asterisk wildcards or even be a complete
10  // regex; all lower case).
11  const FILENAME = 'filename'; //!< Fixed filename for the upload. If everything is OK, file will be overwitten directly, and
12  // can also be deleted (if not required).
13  const MULTI = 'multi'; //!< Allow multiple files to be uploaded.
14  const PREVIEW_SIZE = 'previewSize'; //!< Size (width) of preview image (false = no preview).
15  const USER_FILES = 'userFiles'; //!< Show user files.
16 
17  const EVENT_UPLOAD = 'controller:widget:file:upload';
18 
19  public $types = [];
20  public $filename = null;
21  public $multi = false;
22  public $previewSize = false;
23 
24  public $previewMax = 3; //!< Maximum number of preview images.
25 
26  public $sideloadControllerName = null; //!< Controller that provides the sideloading.
27  public $sideloadPath = null; //!< Path to temporary store sideload files.
28  public $sideloadExt = '.side'; //!< Extension for sideload files.
29  public $sideloadMode = 0666; //!< File mode for sideload files.
30  public $sideloadTokenLength = 16; //!< Length of the sideload token.
31  public $sideloadHint = null; //!< Hint to show during sideloading (translated).
32  public $sideloadInterval = 5; //!< Poll interval for sideloading check (seconds).
33  public $sideloadGarbageChance = 100; //!< Clear sideloading path once every ... times.
34  public $sideloadSkip = 5; //!< Stop scanning for new files after this number of misses.
35 
36  protected $_raw = null;
37  protected $_multiRaw = null;
38 
39  public function clientConfig(){
40  $config = array_merge(parent::clientConfig(),$this->get([self::TYPES]));
41  if(!$this->max) $config[self::MAX] = \Rsi\Number::shorthandBytes(ini_get('post_max_size'));
42  if($this->trailer === null) $config[self::TRAILER] = '%n (%s %u)';
43  if($this->filename){
44  $config[self::FILENAME] = \Rsi\File::basename($this->filename);
45  $config['fileExists'] = is_file($this->filename);
46  if($this->sideloadControllerName && $this->sideloadPath && !$this->_controller->component('client')->mobile){
47  $image = !$this->types;
48  foreach($this->types as $type) if($image = preg_match('/^(image|jpe?g)/i',$type)) break;
49  if($image) $config['sideloadInterval'] = $this->sideloadInterval;
50  }
51 //TODO: user files werkt nu alleen met een direct lokale filename (dus niet "classic" POST - via een hidden met name ofzo)
52  if($this->userFiles && $this->_controller->component('user')->id) $config[self::USER_FILES] = true;
53  }
54  if($this->multi) $config[self::MULTI] = true;
55  return $config;
56  }
57  /**
58  * Purge uploaded filename from possible array construction.
59  * If all details are needed you can call fred->request->file(['widget','id']) (where \$id can also be multiple levels
60  * deep).
61  */
62  protected function purgeBase($value){
63  if($value && is_array($value)) $value = $this->multi //classic upload
64  ? array_column($this->_multiRaw = $value,'tmp_name')
65  : \Rsi\Record::get($this->_raw = $value,'tmp_name');
66  elseif(!$value && $this->filename && is_file($this->filename)) $value = $this->filename; //earlier async upload
67  return parent::purgeBase($value);
68  }
69 
70  protected function uploadEvent($name){
71  return $this->_controller->component('event')->trigger(self::EVENT_UPLOAD,$this,$name);
72  }
73 
74  protected function checkMin($value,$index = null){
75  return parent::checkMin(filesize($value));
76  }
77 
78  protected function checkMax($value,$index = null){
79  return parent::checkMax(filesize($value));
80  }
81 
82  protected function checkUpload($value,$index = null){
83  return !$this->_raw || (is_uploaded_file($value) && !$this->_raw['error']); //http://php.net/manual/en/features.file-upload.errors.php
84  }
85 
86  protected function checkTypes($value,$index = null){
87  if($this->_raw) $value = $this->_raw['name'];
88  if(!$this->types || in_array(\Rsi\File::ext($value),$this->types)) return true;
89  $mime = \Rsi\File::mime($value);
90  foreach($this->types as $type)
91  if(preg_match(substr($type,0,1) == '/' ? $type : '/^' . str_replace('*','.*',$type) . '$/',$mime)) return true;
92  return false;
93  }
94 
95  public function check($value,$index = null){
96  if(!$this->_multiRaw) $error = parent::check($value,$index);
97  else foreach($this->_multiRaw as $this->_raw) if($error = parent::check($this->_raw['tmp_name'],$index)) break;
98  if(!$error && $value && !is_array($value) && $this->filename && $this->_raw){
99  if(!move_uploaded_file($value,$this->filename))
100  throw new \Exception("Can not move uploaded file from '$value' to '{$this->filename}'");
101  else $error = $this->uploadEvent($this->_raw['name']);
102  }
103  return $error;
104  }
105  /**
106  * Generate a preview.
107  * @param string $filename File to generate a preview for (empty = fixed upload filename).
108  * @param int $count Will be set to the number of pages in the file.
109  * @param bool $rotate Will be set to true if the image can be rotated.
110  * @return array Preview PNG images.
111  */
112  public function preview($filename = null,&$count = null,&$rotate = null){
114  $count = $rotate = false;
115  $preview = [];
116  if($this->previewSize && $filename && is_file($filename)) try{
117  $image = imagecreatefromstring(file_get_contents($filename));
118  $factor = min($this->previewSize / ($width = imagesx($image)),$this->previewSize / ($height = imagesy($image)));
119  $scaled = imagescale($image,round($factor * $width),round($factor * $height));
120  imagedestroy($image);
121  ob_start();
122  imagepng($scaled);
123  $preview[] = ob_get_clean();
124  imagedestroy($scaled);
125  $rotate = in_array(\Rsi\File::ext($filename),['gif','jpg','jpeg','png']);
126  }
127  catch(\Exception $e){
128  $this->log->info($e);
129  $temp = \Rsi\File::tempFile('png');
130  if(class_exists('Imagick')) try{
131  $image = new \Imagick($filename);
132  $count = $image->getNumberImages();
133  foreach($image as $index => $page){
134  $page->setImageBackgroundColor('white');
135  $page->setImageFormat('png');
136  $page->thumbnailImage($this->previewSize,0);
137  $page->writeImage($temp);
138  $preview[] = file_get_contents($temp);
139  if(++$index >= $this->previewMax) break;
140  }
141  $image->destroy();
142  }
143  catch(\Exception $e){
144  $this->log->info($e);
145  }
146  finally{
147  \Rsi\File::unlink($temp);
148  }
149  else try{
150  extract($this->services->preview($filename,$this->previewSize,$this->previewMax));
151  }
152  catch(\Exception $e){
153  $this->log->info($e);
154  }
155  }
156  if(!$count) $count = count($preview);
157  return $preview;
158  }
159  /**
160  * Rotate a file.
161  * @param int $angle Rotation angle (positive: degrees clockwise; -1: flip horizontal, -2: flip vertical).
162  * @param string $filename File to rotate (empty = fixed upload filename).
163  * @return bool True if the operation was successful (file will be replaced).
164  */
165  public function rotate($angle = 90,$filename = null){
167  $result = !$angle;
168  if($angle && $filename && is_file($filename)) try{
169  $image = null;
170  switch($ext = \Rsi\File::ext($filename)){
171  case 'gif':
172  $image = imagecreatefromgif($filename);
173  break;
174  case 'jpg':
175  $ext = 'jpeg';
176  case 'jpeg':
177  $image = imagecreatefromjpeg($filename);
178  break;
179  case 'png':
180  $image = imagecreatefrompng($filename);
181  break;
182  }
183  if($image){
184  if($angle > 0) $image = imagerotate($image,-$angle,imagecolorallocate($image,255,255,255));
185  else switch($angle){
186  case -1: imageflip($image,IMG_FLIP_HORIZONTAL); break;
187  case -2: imageflip($image,IMG_FLIP_VERTICAL); break;
188  }
189  $result = call_user_func('image' . $ext,$image,$filename);
190  imagedestroy($image);
191  }
192  }
193  catch(\Exception $e){
194  $this->log->info($e);
195  }
196  return $result;
197  }
198  /**
199  * Process upload data.
200  * @param mixed $data Uploaded data (binary string).
201  * @param string $name Original filename.
202  */
203  protected function processUpload($data,$name){
204  try{
205  $temp = \Rsi\File::tempFile(\Rsi\File::ext($this->filename),$data);
206  if($error = $this->check($temp)) \Rsi\File::unlink($temp);
207  elseif(!rename($temp,$this->filename)) throw new \Exception("Could not rename('$temp','{$this->filename}')");
208  else $error = $this->uploadEvent($name);
209  $preview = [];
210  foreach($this->preview(null,$count,$rotate) as $image) $preview[] = base64_encode($image);
211  $this->request->result = compact('error','name','preview','count','rotate') + ['size' => strlen($data),'fileExists' => is_file($this->filename)];
212  }
213  finally{
214  \Rsi\File::unlink($temp);
215  }
216  }
217  /**
218  * Filename for a sideload file.
219  * @param string $token Sideload token.
220  * @param int $index Upload index.
221  * @return string
222  */
223  protected function sideloadFilename($token,$index = null){
224  return $this->sideloadPath . $token . ($index === null ? '' : '-' . $index) . $this->sideloadExt;
225  }
226  /**
227  * Sideload URL.
228  * @param string $token Sideload token.
229  * @return string Full URL.
230  */
231  protected function sideloadUrl($token){
232  return \Rsi\Http::host(true) . $this->_controller->component('router')->reverse($this->sideloadControllerName,null,compact('token'));
233  }
234  /**
235  * Clear old sideload tokens.
236  */
237  protected function sideloadGarbage(){
238  if($this->sideloadPath && !rand(0,$this->sideloadGarbageChance)) try{
239  foreach((new \GlobIterator($this->sideloadPath . '*' . $this->sideloadExt)) as $filename => $file)
240  if($file->getMTime() < time() - 2 * $this->sideloadInterval) \Rsi\File::unlink($filename);
241  $session = $this->_controller->session;
242  if($tokens = $session->sideloadTokens){
243  foreach($tokens as $token => $time) if(!is_file($this->sideloadFilename($token))) unset($tokens[$token]);
244  $session->sideloadTokens = $tokens;
245  }
246  }
247  catch(\Exception $e){
248  $this->log->error($e);
249  }
250  }
251 
252  protected function actionRotate(){
253  if($this->_display >= self::DISPLAY_WRITEABLE){
254  session_write_close();
255  $this->rotate((int)$this->request->angle);
256  $preview = [];
257  foreach($this->preview(null,$count,$rotate) as $image) $preview[] = base64_encode($image);
258  clearstatcache(false,$this->filename);
259  $this->request->result = compact('preview','count','rotate') + ['size' => filesize($this->filename)];
260  }
261  }
262 
263  protected function actionDownload(){
264  if(($this->_display >= self::DISPLAY_READABLE) && $this->filename && is_file($this->filename)){
265  \Rsi\Http::downloadHeaders($this->filename,null,filesize($this->filename));
266  readfile($this->filename);
267  $this->fred->halt();
268  }
269  }
270 
271  protected function actionUpload(){
272  if(
273  ($this->_display >= self::DISPLAY_WRITEABLE) &&
274  $this->filename &&
275  ($value = \Rsi\Record::get($this->request->file('data'),'tmp_name'))
276  ) $this->processUpload(file_get_contents($value),$this->request->name);
277  }
278 
279  protected function actionDelete(){
280  if(($this->_display >= self::DISPLAY_WRITEABLE) && !$this->required && $this->filename)
281  $this->request->result = ['deleted' => \Rsi\File::unlink($this->filename)];
282  }
283 
284  protected function actionUserFiles(){
285  if(($this->_display >= self::DISPLAY_WRITEABLE) && $this->userFiles) $this->request->result = [
286  'files' => $this->_controller->component('user')->files->list($this->types),
287  'preview' => (bool)$this->previewSize
288  ];
289  }
290 
291  protected function actionUserFilePreview(){
292  if($data = $this->_controller->component('user')->files->fetch($this->request->name)){
293  session_write_close();
294  $temp = \Rsi\File::tempFile('tmp',$data);
295  $preview = [];
296  foreach($this->preview($temp) as $image) $preview[] = base64_encode($image);
297  unlink($temp);
298  $this->request->result = $preview;
299  }
300  }
301 
302  protected function actionUserFile(){
303  if(($this->_display >= self::DISPLAY_WRITEABLE) && $this->filename && $this->userFiles){
304  $data = $this->_controller->component('user')->files->fetch($filename = $this->request->name);
305  if(($data !== false) && ($data !== null)) $this->processUpload($data,basename($filename));
306  }
307  }
308 
309  protected function actionSideload(){
310  if(($this->_display >= self::DISPLAY_WRITEABLE) && $this->filename && $this->sideloadPath && $this->sideloadControllerName){
311  $this->sideloadGarbage();
312  while(file_exists($filename = $this->sideloadFilename($token = \Rsi\Str::random($this->sideloadTokenLength))));
313  \Rsi\File::serialize(
314  $filename,
315  [
316  'token' => $token,
317  'config' => array_merge($this->_config,[self::FILENAME => null]),
318  'time' => $time = time()
319  ],
320  $this->sideloadMode
321  );
322  $session = $this->_controller->session;
323  $tokens = $session->sideloadTokens ?: [];
324  $tokens[$token] = $time;
325  $session->sideloadTokens = $tokens;
326  $url = $this->sideloadUrl($token);
327  $this->request->result = [
328  'token' => $token,
329  'controller' => $this->sideloadControllerName,
330  'url' => $url,
331  'hint' => $this->trans->str($this->sideloadHint,compact('token','url'))
332  ];
333  }
334  }
335 
336  protected function actionSideloadImage(){
337  if($token = $this->sideloadToken){
338  header('Content-Type: image/png');
339  imagepng((new Qr(Qr::ECC_MEDIUM,null,10))->dataImage($this->sideloadUrl($token)));
340  $this->fred->halt();
341  }
342  }
343 
344  protected function actionSideloadCheck(){
345  if(($token = $this->sideloadToken) && is_file($filename = $this->sideloadFilename($token))){
346  touch($filename);
347  $index = $skip = 0;
348  while($skip++ < $this->sideloadSkip)
349  if(($data = \Rsi\File::unserialize($filename = $this->sideloadFilename($token,$index++))) !== false) try{
350  \Rsi\File::unlink($filename);
351  $this->processUpload($data['data'],$data['name']);
352  $skip = 0;
353  }
354  catch(\Exception $e){
355  $this->log->warning('Error while processig sideload file: ' . $e->getMessage(),__FILE__,__LINE__);
356  \Rsi\File::unlink($filename);
357  }
358  }
359  if(!$this->request->result) $this->sideloadGarbage();
360  }
361 
362  protected function getSideloadToken(){
363  $session = $this->_controller->session;
364  if(
365  ($tokens = $session->sideloadTokens) &&
366  array_key_exists($token = $this->request->token,$tokens) &&
367  ($tokens[$token] > ($time = time()) - 2 * $this->sideloadInterval)
368  ){
369  $tokens[$token] = $time;
370  $session->sideloadTokens = $tokens;
371  return $token;
372  }
373  }
374 
375 }
sideloadUrl($token)
Sideload URL.
Definition: File.php:231
$sideloadMode
File mode for sideload files.
Definition: File.php:29
rotate($angle=90, $filename=null)
Rotate a file.
Definition: File.php:165
$sideloadSkip
Stop scanning for new files after this number of misses.
Definition: File.php:34
$sideloadControllerName
Controller that provides the sideloading.
Definition: File.php:26
checkMax($value, $index=null)
Definition: File.php:78
purgeBase($value)
Purge uploaded filename from possible array construction.
Definition: File.php:62
$sideloadExt
Extension for sideload files.
Definition: File.php:28
const TYPES
Allowed file types (mime or extension; mime can have asterisk wildcards or even be a complete...
Definition: File.php:9
$sideloadPath
Path to temporary store sideload files.
Definition: File.php:27
const PREVIEW_SIZE
Size (width) of preview image (false = no preview).
Definition: File.php:14
const MULTI
Allow multiple files to be uploaded.
Definition: File.php:13
processUpload($data, $name)
Process upload data.
Definition: File.php:203
sideloadGarbage()
Clear old sideload tokens.
Definition: File.php:237
preview($filename=null, &$count=null, &$rotate=null)
Generate a preview.
Definition: File.php:112
$sideloadHint
Hint to show during sideloading (translated).
Definition: File.php:31
$sideloadTokenLength
Length of the sideload token.
Definition: File.php:30
$sideloadInterval
Poll interval for sideloading check (seconds).
Definition: File.php:32
sideloadFilename($token, $index=null)
Filename for a sideload file.
Definition: File.php:223
const USER_FILES
Show user files.
Definition: File.php:15
const FILENAME
Fixed filename for the upload. If everything is OK, file will be overwitten directly, and.
Definition: File.php:11
$sideloadGarbageChance
Clear sideloading path once every ... times.
Definition: File.php:33
$previewMax
Maximum number of preview images.
Definition: File.php:24
checkMin($value, $index=null)
Definition: File.php:74
check($value, $index=null)
Definition: File.php:95
checkTypes($value, $index=null)
Definition: File.php:86
checkUpload($value, $index=null)
Definition: File.php:82