dolibarr  21.0.0-alpha
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-2023 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 
26 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
27 require_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
28 
29 
34 {
35  public $options;
36  protected $fk_element;
37  protected $element;
38 
47  public function __construct($options = null, $fk_element = null, $element = null)
48  {
49  global $db;
50  global $hookmanager;
51 
52  $hookmanager->initHooks(array('fileupload'));
53 
54  $element_prop = getElementProperties($element);
55  //var_dump($element_prop);
56 
57  $this->fk_element = $fk_element;
58  $this->element = $element;
59 
60  $pathname = str_replace('/class', '', $element_prop['classpath']);
61  $filename = dol_sanitizeFileName($element_prop['classfile']);
62  $dir_output = dol_sanitizePathName($element_prop['dir_output']);
63 
64  //print 'fileupload.class.php: element='.$element.' pathname='.$pathname.' filename='.$filename.' dir_output='.$dir_output."\n";
65 
66  if (empty($dir_output)) {
67  setEventMessage('The element '.$element.' is not supported for uploading file. dir_output is unknown.', 'errors');
68  throw new Exception('The element '.$element.' is not supported for uploading file. dir_output is unknown.');
69  }
70 
71  // If pathname and filename are null then we can still upload files if we have specified upload_dir on $options
72  if ($pathname !== null && $filename !== null) {
73  // Get object from its id and type
74  $object = fetchObjectByElement($fk_element, $element);
75 
76  $object_ref = dol_sanitizeFileName($object->ref);
77 
78  // Special cases to forge $object_ref used to forge $upload_dir
79  if ($element == 'invoice_supplier') {
80  $object_ref = get_exdir($object->id, 2, 0, 0, $object, 'invoice_supplier').$object_ref;
81  } elseif ($element == 'project_task') {
82  $parentForeignKey = 'fk_project';
83  $parentClass = 'Project';
84  $parentElement = 'projet';
85  $parentObject = 'project';
86 
87  dol_include_once('/'.$parentElement.'/class/'.$parentObject.'.class.php');
88  $parent = new $parentClass($db);
89  $parent->fetch($object->$parentForeignKey);
90  if (!empty($parent->socid)) {
91  $parent->fetch_thirdparty();
92  }
93  $object->$parentObject = clone $parent;
94 
95  $object_ref = dol_sanitizeFileName($object->project->ref).'/'.$object_ref;
96  }
97  }
98 
99  $this->options = array(
100  'script_url' => $_SERVER['PHP_SELF'],
101  'upload_dir' => $dir_output.'/'.$object_ref.'/',
102  'upload_url' => DOL_URL_ROOT.'/document.php?modulepart='.$element.'&attachment=1&file=/'.$object_ref.'/',
103  'param_name' => 'files',
104  // Set the following option to 'POST', if your server does not support
105  // DELETE requests. This is a parameter sent to the client:
106  'delete_type' => 'DELETE',
107  // The php.ini settings upload_max_filesize and post_max_size
108  // take precedence over the following max_file_size setting:
109  'max_file_size' => null,
110  'min_file_size' => 1,
111  'accept_file_types' => '/.+$/i',
112  // The maximum number of files for the upload directory:
113  'max_number_of_files' => null,
114  // Image resolution restrictions:
115  'max_width' => null,
116  'max_height' => null,
117  'min_width' => 1,
118  'min_height' => 1,
119  // Set the following option to false to enable resumable uploads:
120  'discard_aborted_uploads' => true,
121  'image_versions' => array(
122  // Uncomment the following version to restrict the size of
123  // uploaded images. You can also add additional versions with
124  // their own upload directories:
125  /*
126  'large' => array(
127  'upload_dir' => dirname($_SERVER['SCRIPT_FILENAME']).'/files/',
128  'upload_url' => $this->getFullUrl().'/files/',
129  'max_width' => 1920,
130  'max_height' => 1200,
131  'jpeg_quality' => 95
132  ),
133  */
134  'thumbnail' => array(
135  'upload_dir' => $dir_output.'/'.$object_ref.'/thumbs/',
136  'upload_url' => DOL_URL_ROOT.'/document.php?modulepart='.urlencode($element).'&attachment=1&file='.urlencode('/'.$object_ref.'/thumbs/'),
137  'max_width' => 80,
138  'max_height' => 80
139  )
140  )
141  );
142 
143  global $action;
144 
145  $hookmanager->executeHooks(
146  'overrideUploadOptions',
147  array(
148  'options' => &$options,
149  'element' => $element
150  ),
151  $object,
152  $action
153  );
154 
155  if ($options) {
156  $this->options = array_replace_recursive($this->options, $options);
157  }
158 
159  // At this point we should have a valid upload_dir in options
160  //if ($pathname === null && $filename === null) { // OR or AND???
161  if ($pathname === null || $filename === null) {
162  if (!array_key_exists("upload_dir", $this->options)) {
163  setEventMessage('If $fk_element = null or $element = null you must specify upload_dir on $options', 'errors');
164  throw new Exception('If $fk_element = null or $element = null you must specify upload_dir on $options');
165  } elseif (!is_dir($this->options['upload_dir'])) {
166  setEventMessage('The directory '.$this->options['upload_dir'].' doesn\'t exists', 'errors');
167  throw new Exception('The directory '.$this->options['upload_dir'].' doesn\'t exists');
168  } elseif (!is_writable($this->options['upload_dir'])) {
169  setEventMessage('The directory '.$this->options['upload_dir'].' is not writable', 'errors');
170  throw new Exception('The directory '.$this->options['upload_dir'].' is not writable');
171  }
172  }
173  }
174 
180  protected function getFullUrl()
181  {
182  $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
183  return
184  ($https ? 'https://' : 'http://').
185  (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
186  (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
187  ($https && $_SERVER['SERVER_PORT'] === 443 ||
188  $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
189  substr($_SERVER['SCRIPT_NAME'], 0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
190  }
191 
198  protected function setFileDeleteUrl($file)
199  {
200  $file->delete_url = $this->options['script_url'].'?file='.urlencode((string) ($file->name)).'&fk_element='.urlencode((string) ($this->fk_element)).'&element='.urlencode((string) ($this->element));
201  $file->delete_type = $this->options['delete_type'];
202  if ($file->delete_type !== 'DELETE') {
203  $file->delete_url .= '&_method=DELETE';
204  }
205  }
206 
213  protected function getFileObject($file_name)
214  {
215  $file_path = $this->options['upload_dir'].dol_sanitizeFileName($file_name);
216 
217  if (dol_is_file($file_path) && $file_name[0] !== '.') {
218  $file = new stdClass();
219  $file->name = $file_name;
220  $file->mime = dol_mimetype($file_name, '', 2);
221  $file->size = filesize($file_path);
222  $file->url = $this->options['upload_url'].urlencode($file->name);
223 
224  foreach ($this->options['image_versions'] as $version => $options) {
225  if (dol_is_file($options['upload_dir'].$file_name)) {
226  $tmp = explode('.', $file->name);
227 
228  // We save the path of mini file into file->... (seems not used)
229  $keyforfile = $version.'_url';
230  $file->$keyforfile = $options['upload_url'].urlencode($tmp[0].'_mini.'.$tmp[1]);
231  }
232  }
233  $this->setFileDeleteUrl($file);
234  return $file;
235  }
236  return null;
237  }
238 
244  protected function getFileObjects()
245  {
246  return array_values(array_filter(array_map(array($this, 'getFileObject'), scandir($this->options['upload_dir']))));
247  }
248 
256  protected function createScaledImage($file_name, $options)
257  {
258  global $maxwidthmini, $maxheightmini, $maxwidthsmall, $maxheightsmall;
259 
260  $file_path = $this->options['upload_dir'].$file_name;
261  $new_file_path = $options['upload_dir'].$file_name;
262 
263  if (dol_mkdir($options['upload_dir']) >= 0) {
264  list($img_width, $img_height) = @getimagesize($file_path);
265  if (!$img_width || !$img_height) {
266  return false;
267  }
268 
269  $res = vignette($file_path, $maxwidthmini, $maxheightmini, '_mini'); // We don't use ->addThumbs here because there is no object
270  if (preg_match('/error/i', $res)) {
271  return false;
272  }
273 
274  $res = vignette($file_path, $maxwidthsmall, $maxheightsmall, '_small'); // We don't use ->addThumbs here because there is no object
275  if (preg_match('/error/i', $res)) {
276  return false;
277  }
278 
279  return true;
280  } else {
281  return false;
282  }
283  }
284 
294  protected function validate($uploaded_file, $file, $error, $index)
295  {
296  if ($error) {
297  $file->error = $error;
298  return false;
299  }
300  if (!$file->name) {
301  $file->error = 'missingFileName';
302  return false;
303  }
304  if (!preg_match($this->options['accept_file_types'], $file->name)) {
305  $file->error = 'acceptFileTypes';
306  return false;
307  }
308  if ($uploaded_file && is_uploaded_file($uploaded_file)) {
309  $file_size = dol_filesize($uploaded_file);
310  } else {
311  $file_size = $_SERVER['CONTENT_LENGTH'];
312  }
313  if ($this->options['max_file_size'] && (
314  $file_size > $this->options['max_file_size'] ||
315  $file->size > $this->options['max_file_size']
316  )
317  ) {
318  $file->error = 'maxFileSize';
319  return false;
320  }
321  if ($this->options['min_file_size'] &&
322  $file_size < $this->options['min_file_size']) {
323  $file->error = 'minFileSize';
324  return false;
325  }
326  if (is_numeric($this->options['max_number_of_files']) && (
327  count($this->getFileObjects()) >= $this->options['max_number_of_files']
328  )
329  ) {
330  $file->error = 'maxNumberOfFiles';
331  return false;
332  }
333  list($img_width, $img_height) = @getimagesize($uploaded_file);
334  if (is_numeric($img_width)) {
335  if ($this->options['max_width'] && $img_width > $this->options['max_width'] ||
336  $this->options['max_height'] && $img_height > $this->options['max_height']) {
337  $file->error = 'maxResolution';
338  return false;
339  }
340  if ($this->options['min_width'] && $img_width < $this->options['min_width'] ||
341  $this->options['min_height'] && $img_height < $this->options['min_height']) {
342  $file->error = 'minResolution';
343  return false;
344  }
345  }
346  return true;
347  }
348 
355  protected function upcountNameCallback($matches)
356  {
357  $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
358  $ext = isset($matches[2]) ? $matches[2] : '';
359  return ' ('.$index.')'.$ext;
360  }
361 
368  protected function upcountName($name)
369  {
370  return preg_replace_callback('/(?:(?: \‍(([\d]+)\‍))?(\.[^.]+))?$/', array($this, 'upcountNameCallback'), $name, 1);
371  }
372 
381  protected function trimFileName($name, $type, $index)
382  {
383  // Remove path information and dots around the filename, to prevent uploading
384  // into different directories or replacing hidden system files.
385  $file_name = basename(dol_sanitizeFileName($name));
386  // Add missing file extension for known image types:
387  $matches = array();
388  if (strpos($file_name, '.') === false && preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
389  $file_name .= '.'.$matches[1];
390  }
391  if ($this->options['discard_aborted_uploads']) {
392  while (dol_is_file($this->options['upload_dir'].$file_name)) {
393  $file_name = $this->upcountName($file_name);
394  }
395  }
396  return $file_name;
397  }
398 
411  protected function handleFileUpload($uploaded_file, $name, $size, $type, $error, $index)
412  {
413  $file = new stdClass();
414  $file->name = $this->trimFileName($name, $type, $index);
415  $file->mime = dol_mimetype($file->name, '', 2);
416  $file->size = intval($size);
417  $file->type = $type;
418 
419  // Sanitize to avoid stream execution when calling file_size(). Not that this is a second security because
420  // most streams are already disabled by stream_wrapper_unregister() in filefunc.inc.php
421  $uploaded_file = preg_replace('/\s*(http|ftp)s?:/i', '', $uploaded_file);
422  $uploaded_file = realpath($uploaded_file); // A hack to be sure the file point to an existing file on disk (and is not a SSRF attack)
423 
424  $validate = $this->validate($uploaded_file, $file, $error, $index);
425 
426  if ($validate) {
427  if (dol_mkdir($this->options['upload_dir']) >= 0) {
428  $file_path = dol_sanitizePathName($this->options['upload_dir']).dol_sanitizeFileName($file->name);
429  $append_file = !$this->options['discard_aborted_uploads'] && dol_is_file($file_path) && $file->size > dol_filesize($file_path);
430 
431  clearstatcache();
432 
433  if ($uploaded_file && is_uploaded_file($uploaded_file)) {
434  // multipart/formdata uploads (POST method uploads)
435  if ($append_file) {
436  file_put_contents($file_path, fopen($uploaded_file, 'r'), FILE_APPEND);
437  } else {
438  $result = dol_move_uploaded_file($uploaded_file, $file_path, 1, 0, 0, 0, 'userfile');
439  }
440  } else {
441  // Non-multipart uploads (PUT method support)
442  file_put_contents($file_path, fopen('php://input', 'r'), $append_file ? FILE_APPEND : 0);
443  }
444  $file_size = dol_filesize($file_path);
445  if ($file_size === $file->size) {
446  $file->url = $this->options['upload_url'].urlencode($file->name);
447  foreach ($this->options['image_versions'] as $version => $options) {
448  if ($this->createScaledImage($file->name, $options)) { // Creation of thumbs mini and small is ok
449  $tmp = explode('.', $file->name);
450 
451  // We save the path of mini file into file->... (seems not used)
452  $keyforfile = $version.'_url';
453  $file->$keyforfile = $options['upload_url'].urlencode($tmp[0].'_mini.'.$tmp[1]);
454  }
455  }
456  } elseif ($this->options['discard_aborted_uploads']) {
457  unlink($file_path);
458  $file->error = 'abort';
459  }
460  $file->size = $file_size;
461  $this->setFileDeleteUrl($file);
462  } else {
463  $file->error = 'failedtocreatedestdir';
464  }
465  } else {
466  // should not happen
467  }
468 
469  return $file;
470  }
471 
477  /*public function get()
478  {
479  $file_name = isset($_REQUEST['file']) ? basename(stripslashes($_REQUEST['file'])) : null;
480  if ($file_name) {
481  $info = $this->getFileObject($file_name);
482  } else {
483  $info = $this->getFileObjects();
484  }
485 
486  header('Content-type: application/json');
487  echo json_encode($info);
488  }
489  */
490 
496  public function post()
497  {
498  $error = 0;
499 
500  $upload = isset($_FILES[$this->options['param_name']]) ? $_FILES[$this->options['param_name']] : null;
501 
502  $info = array();
503  if ($upload && is_array($upload['tmp_name'])) {
504  // param_name is an array identifier like "files[]",
505  // $_FILES is a multi-dimensional array:
506  foreach ($upload['tmp_name'] as $index => $value) {
507  $tmpres = $this->handleFileUpload(
508  $upload['tmp_name'][$index],
509  isset($_SERVER['HTTP_X_FILE_NAME']) ? $_SERVER['HTTP_X_FILE_NAME'] : $upload['name'][$index],
510  isset($_SERVER['HTTP_X_FILE_SIZE']) ? $_SERVER['HTTP_X_FILE_SIZE'] : $upload['size'][$index],
511  isset($_SERVER['HTTP_X_FILE_TYPE']) ? $_SERVER['HTTP_X_FILE_TYPE'] : $upload['type'][$index],
512  $upload['error'][$index],
513  $index
514  );
515  if (!empty($tmpres->error)) {
516  $error++;
517  }
518  $info[] = $tmpres;
519  }
520  } elseif ($upload || isset($_SERVER['HTTP_X_FILE_NAME'])) {
521  // param_name is a single object identifier like "file",
522  // $_FILES is a one-dimensional array:
523  $tmpres = $this->handleFileUpload(
524  isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
525  isset($_SERVER['HTTP_X_FILE_NAME']) ? $_SERVER['HTTP_X_FILE_NAME'] : (isset($upload['name']) ? $upload['name'] : null),
526  isset($_SERVER['HTTP_X_FILE_SIZE']) ? $_SERVER['HTTP_X_FILE_SIZE'] : (isset($upload['size']) ? $upload['size'] : null),
527  isset($_SERVER['HTTP_X_FILE_TYPE']) ? $_SERVER['HTTP_X_FILE_TYPE'] : (isset($upload['type']) ? $upload['type'] : null),
528  isset($upload['error']) ? $upload['error'] : null,
529  0
530  );
531  if (!empty($tmpres->error)) {
532  $error++;
533  }
534  $info[] = $tmpres;
535  }
536 
537  header('Vary: Accept');
538  $json = json_encode($info);
539 
540  /* disabled. Param redirect seems not used
541  $redirect = isset($_REQUEST['redirect']) ? stripslashes($_REQUEST['redirect']) : null;
542  if ($redirect) {
543  header('Location: '.sprintf($redirect, urlencode($json)));
544  return;
545  }
546  */
547 
548  if (isset($_SERVER['HTTP_ACCEPT']) && (strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)) {
549  header('Content-type: application/json');
550  } else {
551  header('Content-type: text/plain');
552  }
553  echo $json;
554 
555  return $error;
556  }
557 
564  /*
565  public function delete($file)
566  {
567  $file_name = $file ? basename($file) : null;
568  $file_path = $this->options['upload_dir'].dol_sanitizeFileName($file_name);
569  $success = dol_is_file($file_path) && $file_name[0] !== '.' && unlink($file_path);
570  if ($success) {
571  foreach ($this->options['image_versions'] as $version => $options) {
572  $file = $options['upload_dir'].$file_name;
573  if (dol_is_file($file)) {
574  unlink($file);
575  }
576  }
577  }
578  // Return result in json format
579  header('Content-type: application/json');
580  echo json_encode($success);
581 
582  return 0;
583  }
584  */
585 }
if($user->socid > 0) if(! $user->hasRight('accounting', 'chartofaccount')) $object
Definition: card.php:58
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)
Make validation on an uploaded file.
dol_filesize($pathoffile)
Return size of a file.
Definition: files.lib.php:635
dol_move_uploaded_file($src_file, $dest_file, $allowoverwrite, $disablevirusscan=0, $uploaderrorcode=0, $nohook=0, $varfiles='addedfile', $upload_dir='')
Check validity of a file upload from an GUI page, and move it to its final destination.
Definition: files.lib.php:1331
dol_is_file($pathoffile)
Return if path is a file.
Definition: files.lib.php:519
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($elementType)
Get an array with properties of an element.
fetchObjectByElement($element_id, $element_type, $element_ref='', $useCache=0, $maxCacheByType=10)
Fetch an object from its id and element_type Inclusion of classes is automatic.
dol_sanitizeFileName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a file name.
get_exdir($num, $level, $alpha, $withoutslash, $object, $modulepart='')
Return a path to have a the directory according to object where files are stored.
dol_sanitizePathName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a path name.
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:514