dolibarr  19.0.0-dev
fileupload.class.php
Go to the documentation of this file.
1 <?php
2 /* Copyright (C) 2011-2022 Regis Houssin <regis.houssin@inodbox.com>
3  * Copyright (C) 2011-2012 Laurent Destailleur <eldy@users.sourceforge.net>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <https://www.gnu.org/licenses/>.
17  */
18 
24 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
25 require_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
26 
27 
32 {
33  public $options;
34  protected $fk_element;
35  protected $element;
36 
45  public function __construct($options = null, $fk_element = null, $element = null)
46  {
47  global $db, $conf;
48  global $hookmanager;
49 
50  $hookmanager->initHooks(array('fileupload'));
51 
52  $element_prop = getElementProperties($element);
53  //var_dump($element_prop);
54 
55  $this->fk_element = $fk_element;
56  $this->element = $element;
57 
58  $pathname = str_replace('/class', '', $element_prop['classpath']);
59  $filename = $element_prop['classfile'];
60  $dir_output = $element_prop['dir_output'];
61 
62  //print 'fileupload.class.php: element='.$element.' pathname='.$pathname.' filename='.$filename.' dir_output='.$dir_output."\n";
63 
64  if (empty($dir_output)) {
65  setEventMessage('The element '.$element.' is not supported for uploading file. dir_output is unknow.', 'errors');
66  throw new Exception('The element '.$element.' is not supported for uploading file. dir_output is unknow.');
67  }
68 
69  // If pathname and filename are null then we can still upload files if we have specified upload_dir on $options
70  if ($pathname !== null && $filename !== null) {
71  // Get object from its id and type
72  $object = fetchObjectByElement($fk_element, $element);
73 
74  $object_ref = dol_sanitizeFileName($object->ref);
75 
76  // Special cases to forge $object_ref used to forge $upload_dir
77  if ($element == 'invoice_supplier') {
78  $object_ref = get_exdir($object->id, 2, 0, 0, $object, 'invoice_supplier').$object_ref;
79  } elseif ($element == 'project_task') {
80  $parentForeignKey = 'fk_project';
81  $parentClass = 'Project';
82  $parentElement = 'projet';
83  $parentObject = 'project';
84 
85  dol_include_once('/'.$parentElement.'/class/'.$parentObject.'.class.php');
86  $parent = new $parentClass($db);
87  $parent->fetch($object->$parentForeignKey);
88  if (!empty($parent->socid)) {
89  $parent->fetch_thirdparty();
90  }
91  $object->$parentObject = clone $parent;
92 
93  $object_ref = dol_sanitizeFileName($object->project->ref).'/'.$object_ref;
94  }
95  }
96 
97  $this->options = array(
98  'script_url' => $_SERVER['PHP_SELF'],
99  'upload_dir' => $dir_output.'/'.$object_ref.'/',
100  'upload_url' => DOL_URL_ROOT.'/document.php?modulepart='.$element.'&attachment=1&file=/'.$object_ref.'/',
101  'param_name' => 'files',
102  // Set the following option to 'POST', if your server does not support
103  // DELETE requests. This is a parameter sent to the client:
104  'delete_type' => 'DELETE',
105  // The php.ini settings upload_max_filesize and post_max_size
106  // take precedence over the following max_file_size setting:
107  'max_file_size' => null,
108  'min_file_size' => 1,
109  'accept_file_types' => '/.+$/i',
110  // The maximum number of files for the upload directory:
111  'max_number_of_files' => null,
112  // Image resolution restrictions:
113  'max_width' => null,
114  'max_height' => null,
115  'min_width' => 1,
116  'min_height' => 1,
117  // Set the following option to false to enable resumable uploads:
118  'discard_aborted_uploads' => true,
119  'image_versions' => array(
120  // Uncomment the following version to restrict the size of
121  // uploaded images. You can also add additional versions with
122  // their own upload directories:
123  /*
124  'large' => array(
125  'upload_dir' => dirname($_SERVER['SCRIPT_FILENAME']).'/files/',
126  'upload_url' => $this->getFullUrl().'/files/',
127  'max_width' => 1920,
128  'max_height' => 1200,
129  'jpeg_quality' => 95
130  ),
131  */
132  'thumbnail' => array(
133  'upload_dir' => $dir_output.'/'.$object_ref.'/thumbs/',
134  'upload_url' => DOL_URL_ROOT.'/document.php?modulepart='.urlencode($element).'&attachment=1&file=/'.$object_ref.'/thumbs/',
135  'max_width' => 80,
136  'max_height' => 80
137  )
138  )
139  );
140 
141  global $action;
142 
143  $hookmanager->executeHooks(
144  'overrideUploadOptions',
145  array(
146  'options' => &$options,
147  'element' => $element
148  ),
149  $object,
150  $action
151  );
152 
153  if ($options) {
154  $this->options = array_replace_recursive($this->options, $options);
155  }
156 
157  // At this point we should have a valid upload_dir in options
158  //if ($pathname === null && $filename === null) { // OR or AND???
159  if ($pathname === null || $filename === null) {
160  if (!key_exists("upload_dir", $this->options)) {
161  setEventMessage('If $fk_element = null or $element = null you must specify upload_dir on $options', 'errors');
162  throw new Exception('If $fk_element = null or $element = null you must specify upload_dir on $options');
163  } elseif (!is_dir($this->options['upload_dir'])) {
164  setEventMessage('The directory '.$this->options['upload_dir'].' doesn\'t exists', 'errors');
165  throw new Exception('The directory '.$this->options['upload_dir'].' doesn\'t exists');
166  } elseif (!is_writable($this->options['upload_dir'])) {
167  setEventMessage('The directory '.$this->options['upload_dir'].' is not writable', 'errors');
168  throw new Exception('The directory '.$this->options['upload_dir'].' is not writable');
169  }
170  }
171  }
172 
178  protected function getFullUrl()
179  {
180  $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
181  return
182  ($https ? 'https://' : 'http://').
183  (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
184  (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
185  ($https && $_SERVER['SERVER_PORT'] === 443 ||
186  $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
187  substr($_SERVER['SCRIPT_NAME'], 0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
188  }
189 
196  protected function setFileDeleteUrl($file)
197  {
198  $file->delete_url = $this->options['script_url']
199  .'?file='.urlencode($file->name).'&fk_element='.urlencode($this->fk_element).'&element='.urlencode($this->element);
200  $file->delete_type = $this->options['delete_type'];
201  if ($file->delete_type !== 'DELETE') {
202  $file->delete_url .= '&_method=DELETE';
203  }
204  }
205 
212  protected function getFileObject($file_name)
213  {
214 
215  $file_path = $this->options['upload_dir'].$file_name;
216  if (is_file($file_path) && $file_name[0] !== '.') {
217  $file = new stdClass();
218  $file->name = $file_name;
219  $file->mime = dol_mimetype($file_name, '', 2);
220  $file->size = filesize($file_path);
221  $file->url = $this->options['upload_url'].rawurlencode($file->name);
222  foreach ($this->options['image_versions'] as $version => $options) {
223  if (is_file($options['upload_dir'].$file_name)) {
224  $tmp = explode('.', $file->name);
225 
226  // We save the path of mini file into file->... (seems not used)
227  $keyforfile = $version.'_url';
228  $file->$keyforfile = $options['upload_url'].rawurlencode($tmp[0].'_mini.'.$tmp[1]);
229  }
230  }
231  $this->setFileDeleteUrl($file);
232  return $file;
233  }
234  return null;
235  }
236 
242  protected function getFileObjects()
243  {
244  return array_values(array_filter(array_map(array($this, 'getFileObject'), scandir($this->options['upload_dir']))));
245  }
246 
254  protected function createScaledImage($file_name, $options)
255  {
256  global $maxwidthmini, $maxheightmini, $maxwidthsmall, $maxheightsmall;
257 
258  $file_path = $this->options['upload_dir'].$file_name;
259  $new_file_path = $options['upload_dir'].$file_name;
260 
261  if (dol_mkdir($options['upload_dir']) >= 0) {
262  list($img_width, $img_height) = @getimagesize($file_path);
263  if (!$img_width || !$img_height) {
264  return false;
265  }
266 
267  $res = vignette($file_path, $maxwidthmini, $maxheightmini, '_mini'); // We don't use ->addThumbs here because there is no object
268  if (preg_match('/error/i', $res)) {
269  return false;
270  }
271 
272  $res = vignette($file_path, $maxwidthsmall, $maxheightsmall, '_small'); // We don't use ->addThumbs here because there is no object
273  if (preg_match('/error/i', $res)) {
274  return false;
275  }
276 
277  return true;
278  } else {
279  return false;
280  }
281  }
282 
292  protected function validate($uploaded_file, $file, $error, $index)
293  {
294  if ($error) {
295  $file->error = $error;
296  return false;
297  }
298  if (!$file->name) {
299  $file->error = 'missingFileName';
300  return false;
301  }
302  if (!preg_match($this->options['accept_file_types'], $file->name)) {
303  $file->error = 'acceptFileTypes';
304  return false;
305  }
306  if ($uploaded_file && is_uploaded_file($uploaded_file)) {
307  $file_size = filesize($uploaded_file);
308  } else {
309  $file_size = $_SERVER['CONTENT_LENGTH'];
310  }
311  if ($this->options['max_file_size'] && (
312  $file_size > $this->options['max_file_size'] ||
313  $file->size > $this->options['max_file_size'])
314  ) {
315  $file->error = 'maxFileSize';
316  return false;
317  }
318  if ($this->options['min_file_size'] &&
319  $file_size < $this->options['min_file_size']) {
320  $file->error = 'minFileSize';
321  return false;
322  }
323  if (is_numeric($this->options['max_number_of_files']) && (
324  count($this->getFileObjects()) >= $this->options['max_number_of_files'])
325  ) {
326  $file->error = 'maxNumberOfFiles';
327  return false;
328  }
329  list($img_width, $img_height) = @getimagesize($uploaded_file);
330  if (is_numeric($img_width)) {
331  if ($this->options['max_width'] && $img_width > $this->options['max_width'] ||
332  $this->options['max_height'] && $img_height > $this->options['max_height']) {
333  $file->error = 'maxResolution';
334  return false;
335  }
336  if ($this->options['min_width'] && $img_width < $this->options['min_width'] ||
337  $this->options['min_height'] && $img_height < $this->options['min_height']) {
338  $file->error = 'minResolution';
339  return false;
340  }
341  }
342  return true;
343  }
344 
351  protected function upcountNameCallback($matches)
352  {
353  $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
354  $ext = isset($matches[2]) ? $matches[2] : '';
355  return ' ('.$index.')'.$ext;
356  }
357 
364  protected function upcountName($name)
365  {
366  return preg_replace_callback('/(?:(?: \‍(([\d]+)\‍))?(\.[^.]+))?$/', array($this, 'upcountNameCallback'), $name, 1);
367  }
368 
377  protected function trimFileName($name, $type, $index)
378  {
379  // Remove path information and dots around the filename, to prevent uploading
380  // into different directories or replacing hidden system files.
381  // Also remove control characters and spaces (\x00..\x20) around the filename:
382  $file_name = trim(basename(stripslashes($name)), ".\x00..\x20");
383  // Add missing file extension for known image types:
384  $matches = array();
385  if (strpos($file_name, '.') === false && preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
386  $file_name .= '.'.$matches[1];
387  }
388  if ($this->options['discard_aborted_uploads']) {
389  while (is_file($this->options['upload_dir'].$file_name)) {
390  $file_name = $this->upcountName($file_name);
391  }
392  }
393  return $file_name;
394  }
395 
408  protected function handleFileUpload($uploaded_file, $name, $size, $type, $error, $index)
409  {
410  $file = new stdClass();
411  $file->name = $this->trimFileName($name, $type, $index);
412  $file->mime = dol_mimetype($file->name, '', 2);
413  $file->size = intval($size);
414  $file->type = $type;
415 
416  $validate = $this->validate($uploaded_file, $file, $error, $index);
417 
418  if ($validate) {
419  if (dol_mkdir($this->options['upload_dir']) >= 0) {
420  $file_path = $this->options['upload_dir'].$file->name;
421  $append_file = !$this->options['discard_aborted_uploads'] && is_file($file_path) && $file->size > filesize($file_path);
422 
423  clearstatcache();
424 
425  if ($uploaded_file && is_uploaded_file($uploaded_file)) {
426  // multipart/formdata uploads (POST method uploads)
427  if ($append_file) {
428  file_put_contents($file_path, fopen($uploaded_file, 'r'), FILE_APPEND);
429  } else {
430  $result = dol_move_uploaded_file($uploaded_file, $file_path, 1, 0, 0, 0, 'userfile');
431  }
432  } else {
433  // Non-multipart uploads (PUT method support)
434  file_put_contents($file_path, fopen('php://input', 'r'), $append_file ? FILE_APPEND : 0);
435  }
436  $file_size = filesize($file_path);
437  if ($file_size === $file->size) {
438  $file->url = $this->options['upload_url'].rawurlencode($file->name);
439  foreach ($this->options['image_versions'] as $version => $options) {
440  if ($this->createScaledImage($file->name, $options)) { // Creation of thumbs mini and small is ok
441  $tmp = explode('.', $file->name);
442 
443  // We save the path of mini file into file->... (seems not used)
444  $keyforfile = $version.'_url';
445  $file->$keyforfile = $options['upload_url'].rawurlencode($tmp[0].'_mini.'.$tmp[1]);
446  }
447  }
448  } elseif ($this->options['discard_aborted_uploads']) {
449  unlink($file_path);
450  $file->error = 'abort';
451  }
452  $file->size = $file_size;
453  $this->setFileDeleteUrl($file);
454  } else {
455  $file->error = 'failedtocreatedestdir';
456  }
457  } else {
458  // should not happen
459  }
460 
461  return $file;
462  }
463 
469  public function get()
470  {
471  $file_name = isset($_REQUEST['file']) ?
472  basename(stripslashes($_REQUEST['file'])) : null;
473  if ($file_name) {
474  $info = $this->getFileObject($file_name);
475  } else {
476  $info = $this->getFileObjects();
477  }
478  header('Content-type: application/json');
479  echo json_encode($info);
480  }
481 
487  public function post()
488  {
489  $error = 0;
490 
491  if (isset($_REQUEST['_method']) && $_REQUEST['_method'] === 'DELETE') {
492  return $this->delete();
493  }
494  //var_dump($_FILES);
495 
496  $upload = isset($_FILES[$this->options['param_name']]) ?
497  $_FILES[$this->options['param_name']] : null;
498  $info = array();
499  if ($upload && is_array($upload['tmp_name'])) {
500  // param_name is an array identifier like "files[]",
501  // $_FILES is a multi-dimensional array:
502  foreach ($upload['tmp_name'] as $index => $value) {
503  $tmpres = $this->handleFileUpload(
504  $upload['tmp_name'][$index],
505  isset($_SERVER['HTTP_X_FILE_NAME']) ? $_SERVER['HTTP_X_FILE_NAME'] : $upload['name'][$index],
506  isset($_SERVER['HTTP_X_FILE_SIZE']) ? $_SERVER['HTTP_X_FILE_SIZE'] : $upload['size'][$index],
507  isset($_SERVER['HTTP_X_FILE_TYPE']) ? $_SERVER['HTTP_X_FILE_TYPE'] : $upload['type'][$index],
508  $upload['error'][$index],
509  $index
510  );
511  if (!empty($tmpres->error)) {
512  $error++;
513  }
514  $info[] = $tmpres;
515  }
516  } elseif ($upload || isset($_SERVER['HTTP_X_FILE_NAME'])) {
517  // param_name is a single object identifier like "file",
518  // $_FILES is a one-dimensional array:
519  $tmpres = $this->handleFileUpload(
520  isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
521  isset($_SERVER['HTTP_X_FILE_NAME']) ? $_SERVER['HTTP_X_FILE_NAME'] : (isset($upload['name']) ? $upload['name'] : null),
522  isset($_SERVER['HTTP_X_FILE_SIZE']) ? $_SERVER['HTTP_X_FILE_SIZE'] : (isset($upload['size']) ? $upload['size'] : null),
523  isset($_SERVER['HTTP_X_FILE_TYPE']) ? $_SERVER['HTTP_X_FILE_TYPE'] : (isset($upload['type']) ? $upload['type'] : null),
524  isset($upload['error']) ? $upload['error'] : null,
525  0
526  );
527  if (!empty($tmpres->error)) {
528  $error++;
529  }
530  $info[] = $tmpres;
531  }
532 
533  header('Vary: Accept');
534  $json = json_encode($info);
535 
536  /* disabled. Param redirect seems not used
537  $redirect = isset($_REQUEST['redirect']) ? stripslashes($_REQUEST['redirect']) : null;
538  if ($redirect) {
539  header('Location: '.sprintf($redirect, rawurlencode($json)));
540  return;
541  }
542  */
543 
544  if (isset($_SERVER['HTTP_ACCEPT']) && (strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)) {
545  header('Content-type: application/json');
546  } else {
547  header('Content-type: text/plain');
548  }
549  echo $json;
550 
551  return $error;
552  }
553 
559  public function delete()
560  {
561  $file_name = isset($_REQUEST['file']) ?
562  basename(stripslashes($_REQUEST['file'])) : null;
563  $file_path = $this->options['upload_dir'].$file_name;
564  $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path);
565  if ($success) {
566  foreach ($this->options['image_versions'] as $version => $options) {
567  $file = $options['upload_dir'].$file_name;
568  if (is_file($file)) {
569  unlink($file);
570  }
571  }
572  }
573  // Return result in json format
574  header('Content-type: application/json');
575  echo json_encode($success);
576 
577  return 0;
578  }
579 }
This class is used to manage file upload using ajax.
getFileObjects()
getFileObjects
setFileDeleteUrl($file)
Set delete url.
__construct($options=null, $fk_element=null, $element=null)
Constructor.
post()
Output data.
handleFileUpload($uploaded_file, $name, $size, $type, $error, $index)
handleFileUpload.
upcountName($name)
Enter description here ...
getFileObject($file_name)
getFileObject
upcountNameCallback($matches)
Enter description here ...
createScaledImage($file_name, $options)
Create thumbs of a file uploaded.
getFullUrl()
Return full URL.
trimFileName($name, $type, $index)
trimFileName
validate($uploaded_file, $file, $error, $index)
Enter description here ...
dol_move_uploaded_file($src_file, $dest_file, $allowoverwrite, $disablevirusscan=0, $uploaderrorcode=0, $nohook=0, $varfiles='addedfile', $upload_dir='')
Make control on an uploaded file from an GUI page and move it to final destination.
Definition: files.lib.php:1196
dol_mimetype($file, $default='application/octet-stream', $mode=0)
Return MIME type of a file from its name with extension.
setEventMessage($mesgs, $style='mesgs', $noduplicate=0)
Set event message in dol_events session object.
if(!function_exists('dol_getprefix')) dol_include_once($relpath, $classname='')
Make an include_once using default root and alternate root if it fails.
getElementProperties($element_type)
Get an array with properties of an element.
dol_sanitizeFileName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a file name.
fetchObjectByElement($element_id, $element_type, $element_ref='')
Fetch an object from its id and element_type Inclusion of classes is automatic.
get_exdir($num, $level, $alpha, $withoutslash, $object, $modulepart='')
Return a path to have a the directory according to object where files are stored.
dol_mkdir($dir, $dataroot='', $newmask='')
Creation of a directory (this can create recursive subdir)
vignette($file, $maxWidth=160, $maxHeight=120, $extName='_small', $quality=50, $outdir='thumbs', $targetformat=0)
Create a thumbnail from an image file (Supported extensions are gif, jpg, png and bmp).
Definition: images.lib.php:509