dolibarr/htdocs/core/lib/files.lib.php
VIAL-GOUTEYRON Quentin ea54342891
NEW Drag and drop a file on the 23 remaining cards (#39473)
* FIX Drag and drop of a file reports a wrong error, or none at all

The error handler of dragAndDropFileUpload() had three defects that all end on
the user believing the file was attached when it was not.

 - The first assignment of the message was dead code, there was no return after
   the test on the http code 403, so a refusal was reported as a generic error.
 - The key ErrorUploadPermissionDenied it used exists in no language file, so
   the raw key was shown.
 - JSON.parse() was called with no try/catch on an answer that is not always a
   json, a fatal error of the endpoint or a request over post_max_size for
   example. The exception left the user on a page with no message at all.
 - An empty list of files was treated as a success, while it means the endpoint
   stored nothing.

The value of PHP_SELF is also escaped before it is written into the 6 generated
javascript strings. It holds the path info of the request on a server that
accepts it, so it is a user input. dol_escape_js() is called with the mode that
escapes a double quote by a double quote, the strings being delimited by double
quotes, and the sequence '</' is escaped too because the function does not do it
and the path info could otherwise close the script tag and open one of its own.

Adds the key ErrorOnAtLeastOneFileUpload to en_US, used when some files of a
batch failed and some did not.

* FIX getMultidirOutput returns a directory the Documents tab does not read

Four defects of the same function, all ending on a file stored where the user
will never see it, or written outside the documents directory.

 - The ref of the project of a task was only passed through dol_sanitizePathName(),
   which keeps a slash, a colon and the accented chars, while projet/tasks/document.php
   sanitizes it with dol_sanitizeFileName(). A project ref holding a slash even
   created an extra level of directory. Measured on real databases: 304 projects
   over 330 hold a slash on one of them, carrying 701 tasks over 824.
 - The same case calls $object->fetchProject() with no guard, while the signature
   of the function accepts an object that is not a CommonObject, and even a null
   when a module is given. Such a caller gets a fatal error where it expects the
   error string. No core caller is in that case today, an external module or a
   hook can be.
 - The entity of the object may have no declared directory, an object shared by
   another entity for example. The undefined index returned a relative path, so
   the caller read or wrote under the web root. The current entity is used
   instead, and the fallback is logged because the directory is then not the one
   of the entity of the object, which matters for a caller that deletes files.
 - When the current entity has no declared directory either, the fallback
   returned the sub directory alone, again a relative path. The same error than
   for a module that declares no directory at all is now returned.

Adds the sub directory of a partnership and of a stock transfer, which their own
document tabs already read.

* FIX getElementProperties answers wrong properties for 9 elements

A customer payment, a supplier payment, a various payment, a stock transfer and
the 4 objects of the hrm module had no properties at all, or wrong ones, so any
caller that resolves a class, a table or a document directory from an element
failed on them.

 - payment, payment_supplier and payment_various had no branch. The branch of a
   customer payment tests $elementType and not $element, because the rule on the
   elements named myobject_mysubobject rewrites $element to 'payment' for
   'payment_salary' too, which is stored somewhere else.
 - job, position, skill and evaluation answered a wrong table (hrm_job_user for
   a position, and so on) and no sub directory, while their document tabs read
   one named after the element.
 - stocktransfer answered an empty classname, because it is not the ucfirst() of
   the element, so a caller doing new $classname($db) ended on a fatal error.
 - The sub directory was concatenated even when the module is disabled and the
   directory is empty, which answered a path at the root of the file system.
 - A contact and a conference are stored into a sub directory their tab reads.

isModEnabled('invoice') is tested for a customer payment: there is no module
named 'compta', so testing it was always false, while $conf->compta->payment is
set unconditionally by Conf::setValues() and could not be used as a proxy.

* FIX Access refused to everyone on 11 objects of the core

restrictedArea() and checkUserAccessToObject() refuse the access to objects that
no permission and no rule can match, whatever the user, an administrator
included.

 - The hrm module declares no permission at its first level, only 'all', and the
   stocktransfer module only 'stocktransfer'. A check on the module itself
   therefore tests a permission that does not exist. The mapping is the same one
   as into User::hasRight().
 - The module of an event organization declares no permission at all, its whole
   permission block being commented out on purpose, and its cards check the
   parent project instead. The feature is mapped onto that project, with the two
   guards the card has: an external user is refused, and so is a conference with
   no parent project, whose id of 0 would otherwise grant an access with no check
   on the record at all.
 - The default rule of checkUserAccessToObject() builds its sql on the columns
   entity and fk_soc of the table. llx_asset, llx_paiement, llx_paiementfourn and
   llx_workstation_workstation have no fk_soc, and llx_hrm_job, llx_hrm_job_user
   and llx_hrm_skill have neither. The sql failed, so the access was refused to
   every user this rule applies to. These tables are now checked on their entity
   only, which is what the $check rule already does for the same class of tables,
   and the 3 tables of hrm can be checked on nothing at all. The rule is selected
   on the table and not on the element, because $object is an id and not an
   object for most of the callers, the cards of an asset and of a workstation
   included, which are broken today for any user without the permission to see
   all third parties.
 - An external user is refused explicitly on those tables: none of these objects
   is linked to a third party, so the default rule refused him through a link
   that does not exist, and the rules that replace it do not look at the third
   party of the user at all.

Measured on a vanilla instance with 5 profiles, an administrator, an internal
user with every right, one without the permission to see all third parties, one
that is not a sales representative of the third party of the object, and an
external user: the 11 objects go from refused to granted for the internal users
and stay refused for the external one, and the 26 other elements answer exactly
the same for the 5 profiles.

* FIX A file dropped on a card is lost, or reported as refused when it was stored

FileUpload stores the file into a directory that the "Attached files" tab of the
object never reads, so the user attaches a file that no screen will ever show,
and nothing is indexed in database to find it back. Measured on real databases:
216487 thirdparties over 216887 and 157852 products over 280319 are in that case
on the cards that already enable the drag and drop.

 - The directory of the object is now forged with get_exdir(), the way the tabs
   do: it always uses the id for a thirdparty, whose ref is a company name and is
   not unique, and it falls back on the id when the ref is empty. The sub
   directory of the module is read with getMultidirOutput(), which knows the
   elements that store their documents into one. That function does not return
   an empty string when it fails but a string starting with 'error-', so only an
   absolute path is accepted: writing into that string would create the files
   under the web root.
 - fetchObjectByElement() returns an object even when fetch() returned 0. The
   object was then not loaded, and the file was stored at the root of the
   directory of the module, out of any object. The constructor now throws, and
   the endpoint answers the error with the same json contract than a successful
   call so that the caller can show it, instead of a fatal error and an http 500.
 - An attachment of the same name was silently overwritten, dol_move_uploaded_file()
   being called with $allowoverwrite = 1 while the name was checked before the
   ref of the object was added as a prefix. The check is done again on the final
   name, and on the .noexe suffixed name too, which that function appends to an
   executable file.
 - An executable file was renamed with that .noexe suffix and then reported as an
   error, while it was correctly stored.
 - The endpoint called restrictedArea() with an empty feature when the element is
   unknown, and the loop of that function then takes no branch at all and grants
   the access with no check. It refuses before, with the same http code and the
   same message than a refusal, so that a user cannot tell an object that exists
   but is not allowed from an object that does not exist. The refusal of an
   external user on an object of another third party answered a message of its
   own, which allowed the same enumeration.

Comes with the tests of the path resolution, of the fallback of get_exdir(), of
the rejection of the error string of getMultidirOutput(), and of the file name
deduplication.

* FIX Remove the drop area from 13 cards where dropping a file is harmful

Two distinct groups, both of them removing a drop area that only produces a
result the user does not want.

7 pages have no "Attached files" tab at all, so a file dropped on them can be
reached by no screen: a fiscal year and its info page, an accounting model, a
webhook trigger history, an intracomm report, a bookcal calendar and its booking
list. Measured: 4 of them accepted the upload and wrote an orphan file, the
accounting model wrote it at the root of the directory of the module, out of any
object, and the 2 bookcal ones answered an http 500.

6 cards print their tabs inside their edit form, so the drop area covered that
form: a product, an expense report, a VAT payment, a social contribution, a loan
and a salary. Dropping a file there reloads the page, which discards what the
user is typing. A product and an expense report have a second call for the read
only view, so the drop area is only removed from the call of the edit branch;
the 4 others share a single call between both views, so the parameter is now
conditional.

* NEW Drag and drop a file on the 23 remaining cards

Every card of the core that owns an "Attached files" tab can now receive a file
by drag and drop, which was the case of 32 of them only. The 23 added here are
an asset, an event, a various payment, a customer payment, a contact, a donation,
a conference or booth, a shipment, a supplier payment, a leave request, an
evaluation, a job, a position, a skill, a knowledge record, a manufacturing
order, a partnership, a lot, a stock transfer, a task, a reception, a resource
and a workstation.

The tab bar of a leave request and of a resource is printed inside their edit
form, so the drop area is not enabled there: dropping a file reloads the page,
which would discard what the user is typing. The card of a supplier payment
prints its tab bar even when the object was not loaded, so the drop area is only
enabled when it is.

* FIX getMultidirOutput refuses an entity with no directory instead of falling back [skip-claudemd]

The previous revision of this PR fell back on the directory of the current
entity when the entity of the object had none, with a LOG_WARNING. On a
multicompany install that made a caller read, write and above all delete
files in the directory of another entity. Refuse instead: the function
already answers 'error-diroutput-not-defined-for-this-object' when the
module declares no directory at all, so the caller has one behaviour to
handle, not two.

The entity is cast to int, which is what the array index needs and what
silences the four PhanTypeMismatchDimFetchNullable this function reported.

---------

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-08-19 02:46:21 +02:00

4371 lines
177 KiB
PHP

<?php
/* Copyright (C) 2008-2012 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2012-2021 Regis Houssin <regis.houssin@inodbox.com>
* Copyright (C) 2012-2016 Juanjo Menent <jmenent@2byte.es>
* Copyright (C) 2015 Marcos García <marcosgdf@gmail.com>
* Copyright (C) 2016 Raphaël Doursenaud <rdoursenaud@gpcsolutions.fr>
* Copyright (C) 2019-2026 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2023 Lenin Rivas <lenin.rivas777@gmail.com>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2025 William Mead <william@m34d.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* or see https://www.gnu.org/
*/
/**
* \file htdocs/core/lib/files.lib.php
* \brief Library for file managing functions
*/
/**
* Make a basename working with all page code (default PHP basenamed fails with cyrillic).
* We suppose dir separator for input is '/'.
*
* @param string $pathfile String to find basename.
* @return string Basename of input
*/
function dol_basename($pathfile)
{
return preg_replace('/^.*\/([^\/]+)$/', '$1', rtrim($pathfile, '/'));
}
/**
* Scan a directory and return a list of files/directories.
* Content for string is UTF8 and dir separator is "/".
*
* @param string $utf8_path Starting path from which to search. This is a full path.
* @param string $types Can be "directories", "files", or "all"
* @param int $recursive Determines whether subdirectories are searched
* @param string|string[]|null $filter Regex or Array of Regex filter to restrict list. The regex value must be escaped for '/' by doing preg_quote($var,'/'), since this char is used for preg_match function,
* but must NOT contains the start and end '/'. Filter is checked into basename only.
* @param string|string[]|null $excludefilter Array of Regex for exclude filter (example: array('(\.meta|_preview.*\.png)$','^\.')). Exclude is checked both into fullpath and into basename (So '^xxx' may exclude 'xxx/dirscanned/...' and dirscanned/xxx').
* @param string $sortcriteria Sort criteria ('','fullname','relativename','name','date','size' or 'type,fullname')
* @param int $sortorder Sort order (SORT_ASC, SORT_DESC)
* @param int $mode 0=Return array minimum keys loaded (faster), 1=Force all keys like date and size to be loaded (slower), 2=Force load of date only, 3=Force load of size only, 4=Force load of perm
* @param int $nohook Disable all hooks
* @param string $relativename For recursive purpose only. Must be "" at first call.
* @param int $donotfollowsymlinks Do not follow symbolic links
* @param int $nbsecondsold Only files older than $nbsecondsold
* @return array<array{name:string,path:string,level1name:string,relativename:string,fullname:string,date:string,size:int,perm:int,type:string}> Array of array('name'=>'xxx','fullname'=>'/abc/xxx','date'=>'yyy','size'=>99,'type'=>'dir|file',...)>
* @see dol_dir_list_in_database()
*/
function dol_dir_list($utf8_path, $types = "all", $recursive = 0, $filter = "", $excludefilter = null, $sortcriteria = "name", $sortorder = SORT_ASC, $mode = 0, $nohook = 0, $relativename = "", $donotfollowsymlinks = 0, $nbsecondsold = 0)
{
global $hookmanager;
global $object;
if ($recursive <= 1) { // Avoid too verbose log
$error_info = "";
// Verify filters (only on the first call of the function)
$filter_ok = true;
if (!empty($filter) && !is_array($filter)) {
if (strlen($filter) > 25000) { // Note that limit depends on syntax of filter
dol_syslog("Value for filter is too large", LOG_ERR);
$filter_ok = false;
} else {
// Check that all '/' are escaped.
if ((int) preg_match('/(?:^|[^\\\\])\//', $filter) > 0) {
$excludefilter_ok = false;
$error_info .= " error='filter_has_unescaped_slash'";
dol_syslog("'$filter' has unescaped '/'", LOG_ERR);
}
}
}
// Ensure we have an array for the exclusions
$excludefilter_ok = true;
$exclude_array = ($excludefilter === null || $excludefilter === '') ? array() : (is_array($excludefilter) ? $excludefilter : array($excludefilter));
foreach ($exclude_array as $f) {
// Check that all '/' are escaped.
if ((int) preg_match('/(?:^|[^\\\\])\//', $f) > 0) {
$excludefilter_ok = false;
$error_info .= " error='excludefilter_has_unescaped_slash'";
dol_syslog("'$f' has unescaped '/'", LOG_ERR);
}
}
dol_syslog("files.lib.php::dol_dir_list path=".$utf8_path." types=".$types." recursive=".$recursive." filter=".json_encode($filter)." excludefilter=".json_encode($excludefilter).$error_info);
// print 'xxx'."files.lib.php::dol_dir_list path=".$utf8_path." types=".$types." recursive=".$recursive." filter=".json_encode($filter)." excludefilter=".json_encode($exclude_array);
if (!$filter_ok || !$excludefilter_ok) {
// Return empty array when filters are invalid
return array();
}
} else {
// Already computed before
$exclude_array = ($excludefilter === null || $excludefilter === '') ? array() : (is_array($excludefilter) ? $excludefilter : array($excludefilter));
}
// Define excludefilterarray (before while, for speed)
$excludefilterarray = array_merge(array('^\.'), $exclude_array);
$loaddate = ($mode == 1 || $mode == 2 || $nbsecondsold != 0 || $sortcriteria == 'date');
$loadsize = ($mode == 1 || $mode == 3 || $sortcriteria == 'size');
$loadperm = ($mode == 1 || $mode == 4 || $sortcriteria == 'perm');
$now = dol_now();
$reshook = 0;
$file_list = array();
// Clean parameters
$utf8_path = preg_replace('/([\\/]+)$/', '', $utf8_path);
if (preg_match('/\*/', $utf8_path)) {
$utf8_path_array = glob($utf8_path, GLOB_ONLYDIR); // This scan dir for files. If file does not exists, return empty.
//$os_path_array = dol_dir_list($utf8_path);
} else {
$utf8_path_array = array($utf8_path);
}
foreach ($utf8_path_array as $utf8_path_cursor) {
$os_path = dol_osencode($utf8_path_cursor);
if (!$nohook && $hookmanager instanceof HookManager) {
$hookmanager->resArray = array();
$hookmanager->initHooks(array('fileslib'));
$parameters = array(
'path' => $os_path,
'types' => $types,
'recursive' => $recursive,
'filter' => $filter,
'excludefilter' => $exclude_array, // Already converted to array.
'sortcriteria' => $sortcriteria,
'sortorder' => $sortorder,
'loaddate' => $loaddate,
'loadsize' => $loadsize,
'mode' => $mode
);
$reshook = $hookmanager->executeHooks('getDirList', $parameters, $object);
}
// $hookmanager->resArray may contain array stacked by other modules
if (empty($reshook)) {
if (!is_dir($os_path)) {
continue;
}
if (($dir = opendir($os_path)) === false) {
continue;
}
$filedate = '';
$filesize = '';
$fileperm = '';
while (false !== ($os_file = readdir($dir))) { // $utf8_file is always a basename (in directory $os_path)
$os_fullpathfile = ($os_path ? $os_path.'/' : '').$os_file;
if (!utf8_check($os_file)) {
$utf8_file = mb_convert_encoding($os_file, 'UTF-8', 'ISO-8859-1'); // Make sure data is stored in utf8 in memory
} else {
$utf8_file = $os_file;
}
$utf8_fullpathfile = $utf8_path_cursor."/".$utf8_file; // Temp variable for speed
// Check if file is qualified
$qualified = 1;
foreach ($excludefilterarray as $filt) {
if (preg_match('/'.$filt.'/i', $utf8_file) || preg_match('/'.$filt.'/i', $utf8_fullpathfile)) {
$qualified = 0;
break;
}
}
//print $utf8_fullpathfile.' '.$utf8_file.' '.$qualified.'<br>';
if ($qualified) {
$isdir = is_dir($os_fullpathfile);
// Check whether this is a file or directory and whether we're interested in that type
if ($isdir) {
// Add entry into file_list array
if (($types == "directories") || ($types == "all")) {
if ($loaddate || $sortcriteria == 'date') {
$filedate = dol_filemtime($utf8_fullpathfile);
}
if ($loadsize || $sortcriteria == 'size') {
$filesize = dol_filesize($utf8_fullpathfile);
}
if ($loadperm || $sortcriteria == 'perm') {
$fileperm = dol_fileperm($utf8_fullpathfile);
}
$qualifiedforfilter = 0;
if (empty($filter)) {
$qualifiedforfilter = 1;
} else {
$testpregmatch = false;
if (is_array($filter)) {
$chunks = array_chunk($filter, 500);
foreach ($chunks as $chunk) {
$testpregmatch = preg_match('/'.implode('|', $chunk).'/i', $utf8_file); // May failed if $filter too large
if ($testpregmatch) {
break;
}
}
} else {
$testpregmatch = preg_match('/'.$filter.'/i', $utf8_file); // May failed if $filter too large
}
if ($testpregmatch) {
$qualifiedforfilter = 1;
}
}
if ($qualifiedforfilter) { // We do not search key $filter into all $path, only into $file part
$reg = array();
preg_match('/([^\/]+)\/[^\/]+$/', $utf8_fullpathfile, $reg);
$level1name = (isset($reg[1]) ? $reg[1] : '');
$file_list[] = array(
"name" => $utf8_file,
"path" => $utf8_path,
"level1name" => $level1name,
"relativename" => ($relativename ? $relativename.'/' : '').$utf8_file,
"fullname" => $utf8_fullpathfile,
"date" => $filedate,
"size" => $filesize,
"perm" => $fileperm,
"type" => 'dir'
);
}
}
// if we're in a directory and we want recursive behavior, call this function again
if ($recursive > 0) {
if (empty($donotfollowsymlinks) || !is_link($os_fullpathfile)) {
//var_dump('eee '. $utf8_fullpathfile. ' '.is_dir($utf8_fullpathfile).' '.is_link($utf8_fullpathfile));
$file_list = array_merge($file_list, dol_dir_list($utf8_fullpathfile, $types, $recursive + 1, $filter, $exclude_array, $sortcriteria, $sortorder, $mode, $nohook, ($relativename != '' ? $relativename.'/' : '').$utf8_file, $donotfollowsymlinks, $nbsecondsold));
}
}
} elseif (in_array($types, array("files", "all"))) {
// Add file into file_list array
if ($loaddate || $sortcriteria == 'date') {
$filedate = dol_filemtime($utf8_fullpathfile);
}
if ($loadsize || $sortcriteria == 'size') {
$filesize = dol_filesize($utf8_fullpathfile);
}
$qualifiedforfilter = 0;
if (empty($filter)) {
$qualifiedforfilter = 1;
} else {
$testpregmatch = false;
if (is_array($filter)) {
$chunks = array_chunk($filter, 500);
foreach ($chunks as $chunk) {
$testpregmatch = preg_match('/'.implode('|', $chunk).'/i', $utf8_file); // May failed if $filter too large
if ($testpregmatch) {
break;
}
}
} else {
$testpregmatch = preg_match('/'.$filter.'/i', $utf8_file); // May failed if $filter too large
}
if ($testpregmatch) {
$qualifiedforfilter = 1;
}
}
if ($qualifiedforfilter) { // We do not search key $filter into all $path, only into $file part
if (empty($nbsecondsold) || $filedate <= ($now - $nbsecondsold)) {
preg_match('/([^\/]+)\/[^\/]+$/', $utf8_fullpathfile, $reg);
$level1name = (isset($reg[1]) ? $reg[1] : '');
$file_list[] = array(
"name" => $utf8_file,
"path" => $utf8_path,
"level1name" => $level1name,
"relativename" => ($relativename ? $relativename.'/' : '').$utf8_file,
"fullname" => $utf8_fullpathfile,
"date" => $filedate,
"size" => $filesize,
"type" => 'file'
);
}
}
}
}
}
closedir($dir);
}
}
// Obtain a list of columns
if (!empty($sortcriteria) && $sortorder) {
$file_list = dol_sort_array($file_list, $sortcriteria, ($sortorder == SORT_ASC ? 'asc' : 'desc'));
}
if ($hookmanager instanceof HookManager && is_array($hookmanager->resArray)) {
$file_list = array_merge($file_list, $hookmanager->resArray);
}
return $file_list;
}
/**
* Scan a directory and return a list of files/directories.
* Content for string is UTF8 and dir separator is "/".
*
* @param string $path Starting path from which to search. Example: 'produit/MYPROD' or 'produit/%'. Must not end with "/".
* @param string $filter Regex filter to restrict list. This regex value must be escaped for '/', since this char is used for preg_match function
* @param string[]|null $excludefilter Array of Regex for exclude filter (example: array('(\.meta|_preview.*\.png)$','^\.'))
* @param string $sortcriteria Sort criteria ("","fullname","name","date","size")
* @param int $sortorder Sort order (SORT_ASC, SORT_DESC)
* @param int $mode 0=Return array minimum keys loaded (faster), 1=Force all keys like description
* @param string $sqlfilters Filter as an Universal Search string.
* Example: '((client:=:1) OR ((client:>=:2) AND (client:<=:3))) AND (client:!=:8) AND (nom:like:'a%')'
* @param ?Object $object Object used
* @return array<array{rowid:string,label:string,name:string,path:string,level1name:string,fullname:string,fullpath_orig:string,date_c:string,date_m:string,type:string,keywords:string,cover:string,position:int,acl:string,share:string,description:string}> Array of array('name'=>'xxx','fullname'=>'/abc/xxx','date'=>'yyy','size'=>99,'type'=>'dir|file',...)
* @see dol_dir_list()
*/
function dol_dir_list_in_database($path, $filter = "", $excludefilter = null, $sortcriteria = "name", $sortorder = SORT_ASC, $mode = 0, $sqlfilters = "", $object = null)
{
global $conf, $db;
if (is_null($object)) {
$object = new stdClass();
}
$sql = "SELECT rowid, label, entity, filename, filepath, fullpath_orig, keywords, cover, gen_or_uploaded, extraparams,";
$sql .= " date_c, tms as date_m, fk_user_c, fk_user_m, acl, position, share";
if ($mode) {
$sql .= ", description";
}
$sql .= " FROM ".MAIN_DB_PREFIX."ecm_files";
if (!empty($object->entity)) {
$sql .= " WHERE entity = ".((int) $object->entity);
} else {
$sql .= " WHERE entity = ".((int) $conf->entity);
}
if (preg_match('/%$/', $path)) {
$sql .= " AND (filepath LIKE '".$db->escape($path)."' OR filepath = '".$db->escape(preg_replace('/\/%$/', '', $path))."')";
} else {
$sql .= " AND filepath = '".$db->escape($path)."'";
}
// Manage filter
$errormessage = '';
$sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
if ($errormessage) {
dol_print_error(null, $errormessage);
return array();
}
$resql = $db->query($sql);
if ($resql) {
$file_list = array();
$num = $db->num_rows($resql);
$i = 0;
while ($i < $num) {
$obj = $db->fetch_object($resql);
if ($obj) {
$reg = array();
preg_match('/([^\/]+)\/[^\/]+$/', DOL_DATA_ROOT.'/'.$obj->filepath.'/'.$obj->filename, $reg);
$level1name = (isset($reg[1]) ? $reg[1] : '');
$file_list[] = array(
"rowid" => $obj->rowid,
"label" => $obj->label, // md5
"name" => $obj->filename,
"path" => DOL_DATA_ROOT.'/'.$obj->filepath,
"level1name" => $level1name,
"fullname" => DOL_DATA_ROOT.'/'.$obj->filepath.'/'.$obj->filename,
"fullpath_orig" => $obj->fullpath_orig,
"date_c" => $db->jdate($obj->date_c),
"date_m" => $db->jdate($obj->date_m),
"type" => 'file',
"keywords" => $obj->keywords,
"cover" => $obj->cover,
"position" => (int) $obj->position,
"acl" => $obj->acl,
"share" => $obj->share,
"description" => ($mode ? $obj->description : '')
// TODO Add 'content' with $mode == 2 ?
);
}
$i++;
}
// Obtain a list of columns
if (!empty($sortcriteria)) {
$myarray = array();
foreach ($file_list as $key => $row) {
$myarray[$key] = (isset($row[$sortcriteria]) ? $row[$sortcriteria] : '');
}
// Sort the data
if ($sortorder) {
array_multisort($myarray, $sortorder, SORT_REGULAR, $file_list);
}
}
return $file_list;
} else {
dol_print_error($db);
return array();
}
}
/**
* Complete $filearray with data from database.
* This will call doldir_list_indatabase to complete filearray.
*
* @param array<array{name:string,path:string,level1name:string,relativename:string,fullname:string,date:string,size:int,perm:int,type:string}> $filearray Array of array('name'=>'xxx','fullname'=>'/abc/xxx','date'=>'yyy','size'=>99,'type'=>'dir|file',...) Array of files obtained using dol_dir_list
* @param string $relativedir Relative dir from DOL_DATA_ROOT
* @param ?Object $object Object used
* @return void
*/
function completeFileArrayWithDatabaseInfo(&$filearray, $relativedir, $object = null)
{
global $conf, $db, $user;
if (is_null($object)) {
$object = new stdClass();
$object->id = null;
$object->element = null;
}
$filearrayindatabase = dol_dir_list_in_database(rtrim($relativedir, "/\\"), '', null, 'name', SORT_ASC, 0, '', $object);
global $modulepart;
// Note: $modulepart is 'product' when set by product/document.php, but 'produit' in some other contexts, so we accept both.
if (in_array($modulepart, array('produit', 'product')) && getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
// TODO Remove this when PRODUCT_USE_OLD_PATH_FOR_PHOTO will be removed
global $object;
if (!empty($object->id)) {
if (isModEnabled("product")) {
$upload_dirold = $conf->product->multidir_output[$object->entity ?? $conf->entity].'/'.substr(substr("000".$object->id, -2), 1, 1).'/'.substr(substr("000".$object->id, -2), 0, 1).'/'.$object->id."/photos";
} else {
$upload_dirold = $conf->service->multidir_output[$object->entity ?? $conf->entity].'/'.substr(substr("000".$object->id, -2), 1, 1).'/'.substr(substr("000".$object->id, -2), 0, 1).'/'.$object->id."/photos";
}
$relativedirold = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $upload_dirold);
$relativedirold = ltrim($relativedirold, "/\\");
// Note: $object must be provided so the entity filter matches the one used to forge $upload_dirold (multicompany)
$filearrayindatabase = array_merge($filearrayindatabase, dol_dir_list_in_database($relativedirold, '', null, 'name', SORT_ASC, 0, '', $object));
}
} elseif ($modulepart == 'ticket') {
foreach ($filearray as $key => $val) {
$rel_dir = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $filearray[$key]['path']);
$rel_dir = trim($rel_dir, "/\\");
if ($rel_dir != $relativedir) {
$filearrayindatabase = array_merge($filearrayindatabase, dol_dir_list_in_database($rel_dir, '', null, 'name', SORT_ASC));
}
}
}
// Complete filearray with properties found into $filearrayindatabase
foreach ($filearray as $key => $val) {
$tmpfilename = preg_replace('/\.noexe$/', '', $filearray[$key]['name']);
$found = 0;
// Search if it exists into $filearrayindatabase
foreach ($filearrayindatabase as $key2 => $val2) {
if (($filearrayindatabase[$key2]['path'] == $filearray[$key]['path']) && ($filearrayindatabase[$key2]['name'] == $tmpfilename)) {
$filearray[$key]['position_name'] = ($filearrayindatabase[$key2]['position'] ? $filearrayindatabase[$key2]['position'] : '0').'_'.$filearrayindatabase[$key2]['name'];
$filearray[$key]['position'] = $filearrayindatabase[$key2]['position'];
$filearray[$key]['cover'] = $filearrayindatabase[$key2]['cover'];
$filearray[$key]['keywords'] = $filearrayindatabase[$key2]['keywords'];
$filearray[$key]['acl'] = $filearrayindatabase[$key2]['acl'];
$filearray[$key]['rowid'] = $filearrayindatabase[$key2]['rowid'];
$filearray[$key]['label'] = $filearrayindatabase[$key2]['label'];
$filearray[$key]['share'] = $filearrayindatabase[$key2]['share'];
$found = 1;
break;
}
}
if (!$found) { // This happen in transition toward version 6, or if files were added manually into os dir.
$filearray[$key]['position'] = '999999'; // File not indexed are at end. So if we add a file, it will not replace an existing position
$filearray[$key]['cover'] = 0;
$filearray[$key]['acl'] = '';
$filearray[$key]['share'] = 0;
$rel_filename = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $filearray[$key]['fullname']);
if (!preg_match('/([\\/]temp[\\/]|[\\/]thumbs|\.meta$)/', $rel_filename)) { // If not a tmp file
dol_syslog("list_of_documents We found a file called '".$filearray[$key]['name']."' not indexed into database. We add it");
include_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
$ecmfile = new EcmFiles($db);
// Add entry into database
$filename = basename($rel_filename);
$rel_dir = dirname($rel_filename);
$rel_dir = preg_replace('/[\\/]$/', '', $rel_dir);
$rel_dir = preg_replace('/^[\\/]/', '', $rel_dir);
$ecmfile->filepath = $rel_dir;
$ecmfile->filename = $filename;
$ecmfile->label = md5_file(dol_osencode($filearray[$key]['fullname'])); // $destfile is a full path to file
$ecmfile->fullpath_orig = $filearray[$key]['fullname'];
$ecmfile->gen_or_uploaded = 'unknown';
if (is_object($object)) {
$ecmfile->src_object_type = $object->element;
$ecmfile->src_object_id = $object->id;
}
$ecmfile->description = ''; // indexed content
$ecmfile->keywords = ''; // keyword content
// When you scan file with dol_dir_list_in_database, you scan for files in entity of object (like with projects), even if you
// are connected into another entity. So we must also create record that was not found into the entity scan, so the one of the object).
$ecmfile->entity = empty($object->entity) ? $conf->entity : $object->entity;
$result = $ecmfile->create($user);
if ($result < 0) {
setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
} else {
$filearray[$key]['rowid'] = $result;
}
} else {
$filearray[$key]['rowid'] = 0; // Should not happened
}
}
}
//var_dump($filearray); var_dump($relativedir.' - tmpfilename='.$tmpfilename.' - found='.$found);
}
/**
* Fast compare of 2 files identified by their properties ->name, ->date and ->size
*
* @param object $a File 1
* @param object $b File 2
* @return int 1, 0, 1
*/
function dol_compare_file($a, $b)
{
global $sortorder, $sortfield;
$sortorder = strtoupper($sortorder);
if ($sortorder == 'ASC') {
$retup = -1;
$retdown = 1;
} else {
$retup = 1;
$retdown = -1;
}
if ($sortfield == 'name') {
if ($a->name == $b->name) {
return 0;
}
return ($a->name < $b->name) ? $retup : $retdown;
}
if ($sortfield == 'date') {
if ($a->date == $b->date) {
return 0;
}
return ($a->date < $b->date) ? $retup : $retdown;
}
if ($sortfield == 'size') {
if ($a->size == $b->size) {
return 0;
}
return ($a->size < $b->size) ? $retup : $retdown;
}
return 0;
}
/**
* Test if filename is a directory
*
* @param string $folder Name of folder
* @return boolean True if it's a directory, False if not found
*/
function dol_is_dir($folder)
{
$newfolder = dol_osencode($folder);
if (is_dir($newfolder)) {
return true;
} else {
return false;
}
}
/**
* Return if path is empty
*
* @param string $dir Path of Directory
* @return boolean True or false
*/
function dol_is_dir_empty($dir)
{
if (!is_readable($dir)) {
return false;
}
return (count(scandir($dir)) == 2);
}
/**
* Return if path is a file
*
* @param string $pathoffile Path of file
* @return boolean True or false
*/
function dol_is_file($pathoffile)
{
$newpathoffile = dol_osencode($pathoffile);
return is_file($newpathoffile);
}
/**
* Return if path is a symbolic link
*
* @param string $pathoffile Path of file
* @return boolean True or false
*/
function dol_is_link($pathoffile)
{
$newpathoffile = dol_osencode($pathoffile);
return is_link($newpathoffile);
}
/**
* Test if directory or filename is writable
*
* @param string $folderorfile Name of folder or filename
* @return boolean True if it's writable, False if not found
*/
function dol_is_writable($folderorfile)
{
$newfolderorfile = dol_osencode($folderorfile);
return is_writable($newfolderorfile);
}
/**
* Return if path is an URI (the name of the method is misleading).
*
* URLs are addresses for websites, URI refer to online resources.
*
* @param string $uri URI to test
* @return boolean True if the path looks like a URI, else false.
*/
function dol_is_url($uri)
{
$prots = array('file', 'http', 'https', 'ftp', 'zlib', 'data', 'ssh', 'ssh2', 'ogg', 'expect');
return false !== preg_match('/^('.implode('|', $prots).'):/i', $uri);
}
/**
* Test if a folder is empty
*
* @param string $folder Name of folder
* @return boolean True if dir is empty or non-existing, False if it contains files
*/
function dol_dir_is_emtpy($folder)
{
$newfolder = dol_osencode($folder);
if (is_dir($newfolder)) {
$handle = opendir($newfolder);
$folder_content = '';
$name_array = [];
while ((gettype($name = readdir($handle)) != "boolean")) {
$name_array[] = $name;
}
foreach ($name_array as $temp) {
$folder_content .= $temp;
}
closedir($handle);
if ($folder_content == "...") {
return true;
} else {
return false;
}
} else {
return true; // Dir does not exists
}
}
/**
* Count number of lines in a file
*
* @param string $file Filename
* @return int Return integer <0 if KO, Number of lines in files if OK
* @see dol_nboflines()
*/
function dol_count_nb_of_line($file)
{
$nb = 0;
$newfile = dol_osencode($file);
//print 'x'.$file;
$fp = fopen($newfile, 'r');
if ($fp) {
while (!feof($fp)) {
$line = fgets($fp);
// Increase count only if read was success.
// Test needed because feof returns true only after fgets
// so we do n+1 fgets for a file with n lines.
if ($line !== false) {
$nb++;
}
}
fclose($fp);
} else {
$nb = -1;
}
return $nb;
}
/**
* Return size of a file
*
* @param string $pathoffile Path of file
* @return integer File size
* @see dol_print_size()
*/
function dol_filesize($pathoffile)
{
$newpathoffile = dol_osencode($pathoffile);
return filesize($newpathoffile);
}
/**
* Return time of a file
*
* @param string $pathoffile Path of file
* @return int Time of file
*/
function dol_filemtime($pathoffile)
{
$newpathoffile = dol_osencode($pathoffile);
return @filemtime($newpathoffile); // @Is to avoid errors if files does not exists
}
/**
* Return permissions of a file
*
* @param string $pathoffile Path of file
* @return integer File permissions
*/
function dol_fileperm($pathoffile)
{
$newpathoffile = dol_osencode($pathoffile);
return fileperms($newpathoffile);
}
/**
* Make replacement of strings into a file.
*
* @param string $srcfile Source file (can't be a directory)
* @param array<string,null|string|int> $arrayreplacement Array with strings to replace. Example: array('valuebefore'=>'valueafter', ...)
* @param string $destfile Destination file (can't be a directory). If empty, will be same than source file.
* @param string $newmask Mask for new file. '0' by default means getDolGlobalString('MAIN_UMASK'). Example: '0666'.
* @param int $indexdatabase 1=index new file into database.
* @param int $arrayreplacementisregex 1=Array of replacement is already an array with key that is a regex. Warning: the key must be escaped with preg_quote for '/' and include the starting/ending '/' in string.
* @return int Return integer <0 if error, 0 if nothing done (dest file already exists), >0 if OK
* @see dol_copy(), dolCopyDir()
*/
function dolReplaceInFile($srcfile, $arrayreplacement, $destfile = '', $newmask = '0', $indexdatabase = 0, $arrayreplacementisregex = 0)
{
dol_syslog("files.lib.php::dolReplaceInFile srcfile=".$srcfile." destfile=".$destfile." newmask=".$newmask." indexdatabase=".$indexdatabase." arrayreplacementisregex=".$arrayreplacementisregex);
if (empty($srcfile)) {
return -1;
}
if (empty($destfile)) {
$destfile = $srcfile;
}
// Clean the aa/bb/../cc into aa/cc
$srcfile = preg_replace('/\.\.\/?/', '', $srcfile);
$destfile = preg_replace('/\.\.\/?/', '', $destfile);
$destexists = dol_is_file($destfile);
if (($destfile != $srcfile) && $destexists) {
return 0;
}
$srcexists = dol_is_file($srcfile);
if (!$srcexists) {
dol_syslog("files.lib.php::dolReplaceInFile failed to read src file", LOG_WARNING);
return -3;
}
$tmpdestfile = $destfile.'.tmp';
$newpathofsrcfile = dol_osencode($srcfile);
$newpathoftmpdestfile = dol_osencode($tmpdestfile);
$newpathofdestfile = dol_osencode($destfile);
$newdirdestfile = dirname($newpathofdestfile);
if ($destexists && !is_writable($newpathofdestfile)) {
dol_syslog("files.lib.php::dolReplaceInFile failed Permission denied to overwrite target file", LOG_WARNING);
return -1;
}
if (!is_writable($newdirdestfile)) {
dol_syslog("files.lib.php::dolReplaceInFile failed Permission denied to write into target directory ".$newdirdestfile, LOG_WARNING);
return -2;
}
dol_delete_file($tmpdestfile);
// Create $newpathoftmpdestfile from $newpathofsrcfile
$content = file_get_contents($newpathofsrcfile);
if (empty($arrayreplacementisregex)) {
$content = make_substitutions($content, $arrayreplacement, null);
} else {
foreach ($arrayreplacement as $key => $value) {
$content = preg_replace($key, (string) $value, $content);
}
}
file_put_contents($newpathoftmpdestfile, $content);
dolChmod($newpathoftmpdestfile, $newmask);
// Rename
$moreinfo = array('gen_or_uploaded' => 'unknown');
$result = dol_move($newpathoftmpdestfile, $newpathofdestfile, $newmask, (($destfile == $srcfile) ? 1 : 0), 0, $indexdatabase, $moreinfo);
if (!$result) {
dol_syslog("files.lib.php::dolReplaceInFile failed to move tmp file to final dest", LOG_WARNING);
return -3;
}
if (empty($newmask) && getDolGlobalString('MAIN_UMASK')) {
$newmask = getDolGlobalString('MAIN_UMASK');
}
if (empty($newmask)) { // This should no happen
dol_syslog("Warning: dolReplaceInFile called with empty value for newmask and no default value defined", LOG_WARNING);
$newmask = '0664';
}
dolChmod($newpathofdestfile, $newmask);
return 1;
}
/**
* Removes content from a file that matches a given pattern.
*
* @param string $filePath Path to the file to be processed.
* @param string $pattern Regular expression pattern to identify the content to remove.
* @return bool Returns true if the operation was successful, false otherwise.
*/
function removePatternFromFile(string $filePath, string $pattern): bool
{
// Check if the file exists
if (! file_exists($filePath)) {
dol_syslog("files.lib.php::removePatternFromFile: File $filePath does not exist", LOG_WARNING);
return false;
}
// Read the file content
$content = file_get_contents($filePath);
if ($content === false) {
dol_syslog("files.lib.php::removePatternFromFile: Unable to read the file $filePath", LOG_WARNING);
return false;
}
// Remove content matching the pattern
$updatedContent = preg_replace($pattern, '', $content);
if ($updatedContent === null) {
dol_syslog("files.lib.php::removePatternFromFile: Error while processing the file $filePath", LOG_WARNING);
return false;
}
// Write the updated content back to the file
$result = file_put_contents($filePath, $updatedContent);
if ($result === false) {
dol_syslog("files.lib.php::removePatternFromFile: Permission denied to overwrite the target file $filePath", LOG_WARNING);
return false;
}
dol_syslog("files.lib.php::removePatternFromFile: Content successfully removed in the file $filePath", LOG_INFO);
return true;
}
/**
* Copy a file to another file.
*
* @param string $srcfile Source file (can't be a directory)
* @param string $destfile Destination file (can't be a directory)
* @param string $newmask Mask for new file (0 by default means $conf->global->MAIN_UMASK). Example: '0666'
* @param int $overwriteifexists Overwrite file if exists (1 by default)
* @param int $testvirus Do an antivirus test. Move is canceled if a virus is found.
* @param int $indexdatabase Index new file into database.
* @return int Return integer <0 if error, 0 if nothing done (dest file already exists and overwriteifexists=0), >0 if OK
* @see dol_delete_file(), dolCopyDir()
*/
function dol_copy($srcfile, $destfile, $newmask = '0', $overwriteifexists = 1, $testvirus = 0, $indexdatabase = 0)
{
global $db, $user;
dol_syslog("files.lib.php::dol_copy srcfile=".$srcfile." destfile=".$destfile." newmask=".$newmask." overwriteifexists=".$overwriteifexists);
if (empty($srcfile) || empty($destfile)) {
return -1;
}
$destexists = dol_is_file($destfile);
if (!$overwriteifexists && $destexists) {
return 0;
}
$newpathofsrcfile = dol_osencode($srcfile);
$newpathofdestfile = dol_osencode($destfile);
$newdirdestfile = dirname($newpathofdestfile);
if ($destexists && !is_writable($newpathofdestfile)) {
dol_syslog("files.lib.php::dol_copy failed Permission denied to overwrite target file", LOG_WARNING);
return -1;
}
if (!is_writable($newdirdestfile)) {
dol_syslog("files.lib.php::dol_copy failed Permission denied to write into target directory ".$newdirdestfile, LOG_WARNING);
return -2;
}
// Check virus
$testvirusarray = array();
if ($testvirus) {
$testvirusarray = dolCheckVirus($srcfile, $destfile);
if (count($testvirusarray)) {
dol_syslog("files.lib.php::dol_copy canceled because a virus was found into source file. we ignore the copy request.", LOG_WARNING);
return -3;
}
}
// Copy with overwriting if exists
$result = @copy($newpathofsrcfile, $newpathofdestfile);
//$result=copy($newpathofsrcfile, $newpathofdestfile); // To see errors, remove @
if (!$result) {
dol_syslog("files.lib.php::dol_copy failed to copy", LOG_WARNING);
return -3;
}
if (empty($newmask) && getDolGlobalString('MAIN_UMASK')) {
$newmask = getDolGlobalString('MAIN_UMASK');
}
if (empty($newmask)) { // This should no happen
dol_syslog("Warning: dol_copy called with empty value for newmask and no default value defined", LOG_WARNING);
$newmask = '0664';
}
dolChmod($newpathofdestfile, $newmask);
if ($result && $indexdatabase) {
// Add entry into ecm database
$rel_filetocopyafter = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $newpathofdestfile);
if (!preg_match('/([\\/]temp[\\/]|[\\/]thumbs|\.meta$)/', $rel_filetocopyafter)) { // If not a tmp file
$rel_filetocopyafter = preg_replace('/^[\\/]/', '', $rel_filetocopyafter);
//var_dump($rel_filetorenamebefore.' - '.$rel_filetocopyafter);exit;
dol_syslog("Try to copy also entries in database for: ".$rel_filetocopyafter, LOG_DEBUG);
include_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
$ecmfiletarget = new EcmFiles($db);
$resultecmtarget = $ecmfiletarget->fetch(0, '', $rel_filetocopyafter);
if ($resultecmtarget > 0) { // An entry for target name already exists for target, we delete it, a new one will be created.
dol_syslog("ECM dest file found, remove it", LOG_DEBUG);
$ecmfiletarget->delete($user);
} else {
dol_syslog("ECM dest file not found, create it", LOG_DEBUG);
}
$ecmSrcfile = new EcmFiles($db);
$resultecm = $ecmSrcfile->fetch(0, '', $srcfile);
if ($resultecm) {
dol_syslog("Fetch src file ok", LOG_DEBUG);
} else {
dol_syslog("Fetch src file error", LOG_DEBUG);
}
$ecmfile = new EcmFiles($db);
$filename = basename($rel_filetocopyafter);
$rel_dir = dirname($rel_filetocopyafter);
$rel_dir = preg_replace('/[\\/]$/', '', $rel_dir);
$rel_dir = preg_replace('/^[\\/]/', '', $rel_dir);
$ecmfile->filepath = $rel_dir;
$ecmfile->filename = $filename;
$ecmfile->label = md5_file(dol_osencode($destfile)); // $destfile is a full path to file
$ecmfile->fullpath_orig = $srcfile;
$ecmfile->gen_or_uploaded = 'copy';
$ecmfile->description = $ecmSrcfile->description;
$ecmfile->keywords = $ecmSrcfile->keywords;
$resultecm = $ecmfile->create($user);
if ($resultecm < 0) {
dol_syslog("Create ECM file ok", LOG_DEBUG);
setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
} else {
dol_syslog("Create ECM file error", LOG_DEBUG);
setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
}
if ($resultecm > 0) {
$result = 1;
} else {
$result = -1;
}
}
}
return (int) $result;
}
/**
* Copy a dir to another dir. This include recursive subdirectories.
*
* @param string $srcfile Source file (a directory)
* @param string $destfile Destination file (a directory)
* @param string $newmask Mask for new file ('0' by default means getDolGlobalString('MAIN_UMASK')). Example: '0666'
* @param int $overwriteifexists Overwrite file if exists (1 by default)
* @param ?array<string,string> $arrayreplacement Array to use to replace filenames with another one during the copy (works only on file names, not on directory names).
* @param int $excludesubdir 0=Do not exclude subdirectories, 1=Exclude subdirectories, 2=Exclude subdirectories if name is not a 2 chars (used for country codes subdirectories).
* @param ?string[] $excludefileext Exclude some file extensions
* @param int $excludearchivefiles Exclude archive files that start with v+timestamp or d+timestamp (0 by default)
* @return int Return integer <0 if error, 0 if nothing done (all files already exists and overwriteifexists=0), >0 if OK
* @see dol_copy()
*/
function dolCopyDir($srcfile, $destfile, $newmask, $overwriteifexists, $arrayreplacement = null, $excludesubdir = 0, $excludefileext = null, $excludearchivefiles = 0)
{
$result = 0;
dol_syslog("files.lib.php::dolCopyDir srcfile=".$srcfile." destfile=".$destfile." newmask=".$newmask." overwriteifexists=".$overwriteifexists);
if (empty($srcfile) || empty($destfile)) {
return -1;
}
$destexists = dol_is_dir($destfile);
//if (! $overwriteifexists && $destexists) return 0; // The overwriteifexists is for files only, so propagated to dol_copy only.
if (!$destexists) {
// We must set mask just before creating dir, because it can be set differently by dol_copy
umask(0);
$dirmaskdec = octdec($newmask);
if (empty($newmask) && getDolGlobalString('MAIN_UMASK')) {
$dirmaskdec = octdec(getDolGlobalString('MAIN_UMASK'));
}
$dirmaskdec |= octdec('0200'); // Set w bit required to be able to create content for recursive subdirs files
$result = dol_mkdir($destfile, '', decoct($dirmaskdec));
if (!dol_is_dir($destfile)) {
// The output directory does not exists and we failed to create it. So we stop here.
return -1;
}
}
$ossrcfile = dol_osencode($srcfile);
$osdestfile = dol_osencode($destfile);
// Recursive function to copy all subdirectories and contents:
if (is_dir($ossrcfile)) {
$dir_handle = opendir($ossrcfile);
$tmpresult = 0; // Initialised before loop to keep old behavior, may be needed inside loop
while ($file = readdir($dir_handle)) {
if ($file != "." && $file != ".." && !is_link($ossrcfile."/".$file)) {
if (is_dir($ossrcfile."/".$file)) {
if (empty($excludesubdir) || ($excludesubdir == 2 && strlen($file) == 2)) {
$newfile = $file;
// Replace destination filename with a new one
if (is_array($arrayreplacement)) {
foreach ($arrayreplacement as $key => $val) {
$newfile = str_replace($key, $val, $newfile);
}
}
//var_dump("xxx dolCopyDir $srcfile/$file, $destfile/$file, $newmask, $overwriteifexists");
$tmpresult = dolCopyDir($srcfile."/".$file, $destfile."/".$newfile, $newmask, $overwriteifexists, $arrayreplacement, $excludesubdir, $excludefileext, $excludearchivefiles);
}
} else {
$newfile = $file;
if (is_array($excludefileext)) {
$extension = pathinfo($file, PATHINFO_EXTENSION);
if (in_array($extension, $excludefileext)) {
//print "We exclude the file ".$file." because its extension is inside list ".join(', ', $excludefileext); exit;
continue;
}
}
if ($excludearchivefiles == 1) {
$extension = pathinfo($file, PATHINFO_EXTENSION);
if (preg_match('/^[v|d]\d+$/', $extension)) {
continue;
}
}
// Replace destination filename with a new one
if (is_array($arrayreplacement)) {
foreach ($arrayreplacement as $key => $val) {
$newfile = str_replace($key, $val, $newfile);
}
}
$tmpresult = dol_copy($srcfile."/".$file, $destfile."/".$newfile, $newmask, $overwriteifexists);
}
// Set result
if ($result > 0 && $tmpresult >= 0) {
// Do nothing, so we don't set result to 0 if tmpresult is 0 and result was success in a previous pass
} else {
$result = $tmpresult;
}
if ($result < 0) {
break;
}
}
}
closedir($dir_handle);
} else {
// Source directory does not exists
$result = -2;
}
return (int) $result;
}
/**
* Move a file into another name.
* Note:
* - This function differs from dol_move_uploaded_file, because it can be called in any context.
* - Database indexes for files are updated.
* - Test on virus is done only if param testvirus is provided and an antivirus was set.
*
* @param string $srcfile Source file (can't be a directory. use native php @rename() to move a directory)
* @param string $destfile Destination file (can't be a directory. use native php @rename() to move a directory)
* @param string $newmask Mask in octal string for new file ('0' by default means $conf->global->MAIN_UMASK)
* @param int<0,1> $overwriteifexists Overwrite file if exists (1 by default)
* @param int<0,1> $testvirus Do an antivirus test. Move is canceled if a virus is found.
* @param int<0,1> $indexdatabase Index new file into database.
* @param array<string,mixed> $moreinfo Array with more information to set in index table
* @param int|null $entity Entity (it's null by default to avoid problem with entity = 0)
* @return boolean True if OK, false if KO
* @see dol_move_uploaded_file()
*/
function dol_move($srcfile, $destfile, $newmask = '0', $overwriteifexists = 1, $testvirus = 0, $indexdatabase = 1, $moreinfo = array(), $entity = null)
{
global $user, $db;
$result = false;
dol_syslog("files.lib.php::dol_move srcfile=".$srcfile." destfile=".$destfile." newmask=".$newmask." overwritifexists=".$overwriteifexists);
$srcexists = dol_is_file($srcfile);
$destexists = dol_is_file($destfile);
if (!$srcexists) {
dol_syslog("files.lib.php::dol_move srcfile does not exists. we ignore the move request.");
return false;
}
if ($overwriteifexists || !$destexists) {
$newpathofsrcfile = dol_osencode($srcfile);
$newpathofdestfile = dol_osencode($destfile);
// Check on virus
$testvirusarray = array();
if ($testvirus) {
// Check using filename + antivirus
$testvirusarray = dolCheckVirus($newpathofsrcfile, $newpathofdestfile);
if (count($testvirusarray)) {
dol_syslog("files.lib.php::dol_move canceled because a virus was found into source file. We ignore the move request.", LOG_WARNING);
return false;
}
} else {
// Check using filename only
$testvirusarray = dolCheckOnFileName($newpathofsrcfile, $newpathofdestfile);
if (count($testvirusarray)) {
dol_syslog("files.lib.php::dol_move canceled because a virus was found into source file. We ignore the move request.", LOG_WARNING);
return false;
}
}
global $dolibarr_main_restrict_os_commands;
if (!empty($dolibarr_main_restrict_os_commands)) {
$arrayofallowedcommand = explode(',', $dolibarr_main_restrict_os_commands);
$arrayofallowedcommand = array_map('trim', $arrayofallowedcommand);
if (in_array(basename($destfile), $arrayofallowedcommand)) {
//$langs->load("errors"); // key must be loaded because we can't rely on loading during output, we need var substitution to be done now.
//setEventMessages($langs->trans("ErrorFilenameReserved", basename($destfile)), null, 'errors');
dol_syslog("files.lib.php::dol_move canceled because target filename ".basename($destfile)." is using a reserved command name. we ignore the move request.", LOG_WARNING);
return false;
}
}
$result = @rename($newpathofsrcfile, $newpathofdestfile); // To see errors, remove @
if (!$result) {
if ($destexists) {
dol_syslog("files.lib.php::dol_move Failed. We try to delete target first and move after.", LOG_WARNING);
// We force delete and try again. Rename function sometimes fails to replace dest file with some windows NTFS partitions.
dol_delete_file($destfile);
$result = @rename($newpathofsrcfile, $newpathofdestfile); // To see errors, remove @
} else {
dol_syslog("files.lib.php::dol_move Failed.", LOG_WARNING);
}
}
// Move ok
if ($result && $indexdatabase) {
// Rename entry into ecm database
$rel_filetorenamebefore = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $srcfile);
$rel_filetorenameafter = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $destfile);
if (!preg_match('/([\\/]temp[\\/]|[\\/]thumbs|\.meta$)/', $rel_filetorenameafter)) { // If not a tmp file
$rel_filetorenamebefore = preg_replace('/^[\\/]/', '', $rel_filetorenamebefore);
$rel_filetorenameafter = preg_replace('/^[\\/]/', '', $rel_filetorenameafter);
//var_dump($rel_filetorenamebefore.' - '.$rel_filetorenameafter);exit;
dol_syslog("Try to rename also entries in database for full relative path before = ".$rel_filetorenamebefore." after = ".$rel_filetorenameafter, LOG_DEBUG);
include_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
$ecmfiletarget = new EcmFiles($db);
$resultecmtarget = $ecmfiletarget->fetch(0, '', $rel_filetorenameafter, '', '', '', 0, $entity);
if ($resultecmtarget > 0) { // An entry for target name already exists for target, we delete it, a new one will be created.
$ecmfiletarget->delete($user);
}
$ecmfile = new EcmFiles($db);
$resultecm = $ecmfile->fetch(0, '', $rel_filetorenamebefore, '', '', '', 0, $entity);
if ($resultecm > 0) { // If an entry was found for src file, we use it to move entry
$filename = basename($rel_filetorenameafter);
$rel_dir = dirname($rel_filetorenameafter);
$rel_dir = preg_replace('/[\\/]$/', '', $rel_dir);
$rel_dir = preg_replace('/^[\\/]/', '', $rel_dir);
$ecmfile->filepath = $rel_dir;
$ecmfile->filename = $filename;
$resultecm = $ecmfile->update($user);
} elseif ($resultecm == 0) { // If no entry were found for src files, create/update target file
$filename = basename($rel_filetorenameafter);
$rel_dir = dirname($rel_filetorenameafter);
$rel_dir = preg_replace('/[\\/]$/', '', $rel_dir);
$rel_dir = preg_replace('/^[\\/]/', '', $rel_dir);
$ecmfile->filepath = $rel_dir;
$ecmfile->filename = $filename;
$ecmfile->label = md5_file(dol_osencode($destfile)); // $destfile is a full path to file
$ecmfile->fullpath_orig = basename($srcfile);
if (!empty($moreinfo) && !empty($moreinfo['gen_or_uploaded'])) {
$ecmfile->gen_or_uploaded = $moreinfo['gen_or_uploaded'];
} else {
$ecmfile->gen_or_uploaded = 'unknown'; // 'generated', 'uploaded', 'api'
}
if (!empty($moreinfo) && !empty($moreinfo['description'])) {
$ecmfile->description = $moreinfo['description']; // indexed content
} else {
$ecmfile->description = ''; // indexed content
}
if (!empty($moreinfo) && !empty($moreinfo['keywords'])) {
$ecmfile->keywords = $moreinfo['keywords']; // indexed content
} else {
$ecmfile->keywords = ''; // keyword content
}
if (!empty($moreinfo) && !empty($moreinfo['note_private'])) {
$ecmfile->note_private = $moreinfo['note_private'];
}
if (!empty($moreinfo) && !empty($moreinfo['note_public'])) {
$ecmfile->note_public = $moreinfo['note_public'];
}
if (!empty($moreinfo) && !empty($moreinfo['src_object_type'])) {
$ecmfile->src_object_type = $moreinfo['src_object_type'];
}
if (!empty($moreinfo) && !empty($moreinfo['src_object_id'])) {
$ecmfile->src_object_id = $moreinfo['src_object_id'];
}
if (!empty($moreinfo) && !empty($moreinfo['position'])) {
$ecmfile->position = $moreinfo['position'];
}
if (!empty($moreinfo) && !empty($moreinfo['cover'])) {
$ecmfile->cover = $moreinfo['cover'];
}
if (! empty($entity)) {
$ecmfile->entity = $entity;
}
$resultecm = $ecmfile->create($user);
if ($resultecm < 0) {
setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
} else {
if (!empty($moreinfo) && !empty($moreinfo['array_options']) && is_array($moreinfo['array_options'])) {
$ecmfile->array_options = $moreinfo['array_options'];
$resultecm = $ecmfile->insertExtraFields();
if ($resultecm < 0) {
setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
}
}
}
} elseif ($resultecm < 0) {
setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
}
if ($resultecm > 0) {
$result = true;
} else {
$result = false;
}
}
}
if (empty($newmask)) {
$newmask = getDolGlobalString('MAIN_UMASK', '0755');
}
// Currently method is restricted to files (dol_delete_files previously used is for files, and mask usage if for files too)
// to allow mask usage for dir, we should introduce a new param "isdir" to 1 to complete newmask like this
// if ($isdir) $newmaskdec |= octdec('0111'); // Set x bit required for directories
dolChmod($newpathofdestfile, $newmask);
}
return $result;
}
/**
* Move a directory into another name.
*
* @param string $srcdir Source directory
* @param string $destdir Destination directory
* @param int $overwriteifexists Overwrite directory if it already exists (1 by default)
* @param int $indexdatabase Index new name of files into database.
* @param int $renamedircontent Also rename contents inside srcdir after the move to match new destination name.
* @return boolean True if OK, false if KO
*/
function dol_move_dir($srcdir, $destdir, $overwriteifexists = 1, $indexdatabase = 1, $renamedircontent = 1)
{
$result = false;
dol_syslog("files.lib.php::dol_move_dir srcdir=".$srcdir." destdir=".$destdir." overwritifexists=".$overwriteifexists." indexdatabase=".$indexdatabase." renamedircontent=".$renamedircontent);
$srcexists = dol_is_dir($srcdir);
$srcbasename = basename($srcdir);
$destexists = dol_is_dir($destdir);
if (!$srcexists) {
dol_syslog("files.lib.php::dol_move_dir srcdir does not exists. Move fails");
return false;
}
if ($overwriteifexists || !$destexists) {
$newpathofsrcdir = dol_osencode($srcdir);
$newpathofdestdir = dol_osencode($destdir);
// On windows, if destination directory exists and is empty, command fails. So if overwrite is on, we first remove destination directory.
// On linux, if destination directory exists and is empty, command succeed. So no need to delete di destination directory first.
// Note: If dir exists and is not empty, it will and must fail on both linux and windows even, if option $overwriteifexists is on.
if ($overwriteifexists) {
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
if (is_dir($newpathofdestdir)) {
@rmdir($newpathofdestdir);
}
}
}
$result = @rename($newpathofsrcdir, $newpathofdestdir);
// Now rename contents in the directory after the move to match the new destination
if ($result && $renamedircontent) {
if (file_exists($newpathofdestdir)) {
$destbasename = basename($newpathofdestdir);
$files = dol_dir_list($newpathofdestdir);
if (!empty($files) && is_array($files)) {
foreach ($files as $key => $file) {
if (!file_exists($file["fullname"])) {
continue;
}
$filepath = $file["path"];
$oldname = $file["name"];
$newname = str_replace($srcbasename, $destbasename, $oldname);
if (!empty($newname) && $newname !== $oldname) {
if ($file["type"] == "dir") {
$res = dol_move_dir($filepath.'/'.$oldname, $filepath.'/'.$newname, $overwriteifexists, $indexdatabase, $renamedircontent);
} else {
$moreinfo = array('gen_or_uploaded' => 'unknown');
$res = dol_move($filepath.'/'.$oldname, $filepath.'/'.$newname, '0', $overwriteifexists, 0, $indexdatabase, $moreinfo);
}
if (!$res) {
return $result;
}
}
}
$result = true;
}
}
}
}
return $result;
}
/**
* Unescape a file submitted by upload.
* PHP escape char " (%22) or char ' (%27) into $FILES.
*
* @param string $filename Filename
* @return string Filename sanitized
*/
function dol_unescapefile($filename)
{
// Remove path information and dots around the filename, to prevent uploading
// into different directories or replacing hidden system files.
// Also remove control characters and spaces (\x00..\x20) around the filename:
return trim(basename($filename), ".\x00..\x20");
}
/**
* Check virus into a file
*
* @param string $src_file Source file to check
* @param string $dest_file Destination file name (to know the expected type)
* @return string[] Array of errors, or empty array if not virus found
*/
function dolCheckVirus($src_file, $dest_file = '')
{
global $db;
$reterrors = dolCheckOnFileName($src_file, $dest_file);
if (!empty($reterrors)) {
return $reterrors;
}
if (getDolGlobalString('MAIN_ANTIVIRUS_UPLOAD_ON')) {
if (!class_exists('AntiVir')) {
require_once DOL_DOCUMENT_ROOT.'/core/class/antivir.class.php';
}
$antivir = new AntiVir($db);
$result = $antivir->dol_avscan_file($src_file);
if ($result < 0) { // If virus or error, we stop here
$reterrors = $antivir->errors;
return $reterrors;
}
}
return array();
}
/**
* Check virus into a file
*
* @param string $src_file Source file to check
* @param string $dest_file Destination file name (to know the expected type)
* @return string[] Array of errors, or empty array if not virus found
*/
function dolCheckOnFileName($src_file, $dest_file = '')
{
if (preg_match('/\.pdf$/i', $dest_file)) {
if (!getDolGlobalString('MAIN_ANTIVIRUS_ALLOW_JS_IN_PDF')) {
dol_syslog("dolCheckOnFileName Check that pdf does not contains js code");
$tmp = file_get_contents(trim($src_file));
if (preg_match('/[\n\s]+\/JavaScript[\n\s]+/m', $tmp)) {
return array('File is a PDF with javascript inside');
}
} else {
dol_syslog("dolCheckOnFileName Check js into pdf disabled");
}
}
return array();
}
/**
* Check validity of a file upload from an GUI page, and move it to its final destination.
* If there is errors (virus found, antivir in error, bad filename), file is not moved.
* Note:
* - This function can be used only into a HTML page context. Use dol_move if you are outside.
* - Test on antivirus is always done (if antivirus set).
* - Database of files is NOT updated (this is done by dol_add_file_process() that calls this function).
* - Extension .noexe may be added if file is executable and MAIN_DOCUMENT_IS_OUTSIDE_WEBROOT_SO_NOEXE_NOT_REQUIRED is not set.
*
* @param string $src_file Source full path filename ($_FILES['field']['tmp_name'])
* @param string $dest_file Target full path filename ($_FILES['field']['name'])
* @param int $allowoverwrite 1=Overwrite target file if it already exists
* @param int $disablevirusscan 1=Disable virus scan
* @param integer $uploaderrorcode Value of PHP upload error code ($_FILES['field']['error'])
* @param int $nohook Disable all hooks
* @param string $keyforsourcefile Key for source file in _FILES (not used)
* @param string $upload_dir For information. Already included into $dest_file.
* @param int $mode 0=Default mode use to move a file from the default system upload dir (/tmp/...) into $upload_dir. 1=Mode to move an uploaded file with a simple rename (when uploaded dir is in same filesystem than $upload_dir).
* @return int|string 1 if OK, 2 if OK and .noexe appended, <0 or string if KO
* @see dol_move()
*/
function dol_move_uploaded_file($src_file, $dest_file, $allowoverwrite, $disablevirusscan = 0, $uploaderrorcode = 0, $nohook = 0, $keyforsourcefile = 'addedfile', $upload_dir = '', $mode = 0)
{
global $conf;
global $object, $hookmanager;
$reshook = 0;
$file_name = $dest_file;
$successcode = 1;
if (empty($nohook)) {
$reshook = $hookmanager->initHooks(array('fileslib'));
$parameters = array('dest_file' => $dest_file, 'src_file' => $src_file, 'file_name' => $file_name, 'varfiles' => $keyforsourcefile, 'allowoverwrite' => $allowoverwrite);
$reshook = $hookmanager->executeHooks('moveUploadedFile', $parameters, $object);
}
if (empty($reshook)) {
// If an upload error has been reported
if ($uploaderrorcode) {
switch ($uploaderrorcode) {
case UPLOAD_ERR_INI_SIZE: // 1
return 'ErrorFileSizeTooLarge';
case UPLOAD_ERR_FORM_SIZE: // 2 - Exceed the MAX_FILE_SIZE specified into a field in form
return 'ErrorFileSizeTooLarge';
case UPLOAD_ERR_PARTIAL: // 3
return 'ErrorPartialFile';
case UPLOAD_ERR_NO_TMP_DIR: //
return 'ErrorNoTmpDir';
case UPLOAD_ERR_CANT_WRITE:
return 'ErrorFailedToWriteInDir';
case UPLOAD_ERR_EXTENSION:
return 'ErrorUploadBlockedByAddon';
default:
break;
}
}
// Security:
// If we need to make a virus scan
if (empty($disablevirusscan) && file_exists($src_file)) {
$checkvirusarray = dolCheckVirus($src_file, $dest_file);
if (count($checkvirusarray)) {
dol_syslog('Files.lib::dol_move_uploaded_file File "'.$src_file.'" (target name "'.$dest_file.'") KO with antivirus: errors='.implode(',', $checkvirusarray), LOG_WARNING);
return 'ErrorFileIsInfectedWithAVirus: '.implode(',', $checkvirusarray);
}
}
// Security:
// Disallow file with some extensions. We rename them.
// Because if we put the documents directory into a directory inside web root (very bad), this allows to execute on demand arbitrary code.
if (isAFileWithExecutableContent($dest_file) && !getDolGlobalString('MAIN_DOCUMENT_IS_OUTSIDE_WEBROOT_SO_NOEXE_NOT_REQUIRED')) {
// $upload_dir ends with a slash, so be must be sure the medias dir to compare to ends with slash too.
$publicmediasdirwithslash = $conf->medias->multidir_output[$conf->entity];
if (!preg_match('/\/$/', $publicmediasdirwithslash)) {
$publicmediasdirwithslash .= '/';
}
if (strpos($upload_dir, $publicmediasdirwithslash) !== 0 || !getDolGlobalInt("MAIN_DOCUMENT_DISABLE_NOEXE_IN_MEDIAS_DIR")) { // We never add .noexe on files into media directory
$file_name .= '.noexe';
$successcode = 2;
}
}
// Security:
// We refuse cache files/dirs, upload using .. and pipes into filenames.
if (preg_match('/^\./', basename($src_file)) || preg_match('/\.\./', $src_file) || preg_match('/[<>|]/', $src_file)) {
dol_syslog("Refused to deliver file ".$src_file, LOG_WARNING);
return -1;
}
// Security:
// We refuse cache files/dirs, upload using .. and pipes into filenames.
if (preg_match('/^\./', basename($dest_file)) || preg_match('/\.\./', $dest_file) || preg_match('/[<>|]/', $dest_file)) {
dol_syslog("Refused to deliver file ".$dest_file, LOG_WARNING);
return -2;
}
}
if ($reshook < 0) { // At least one blocking error returned by one hook
$errmsg = implode(',', $hookmanager->errors);
if (empty($errmsg)) {
$errmsg = 'ErrorReturnedBySomeHooks'; // Should not occurs. Added if hook is bugged and does not set ->errors when there is error.
}
return $errmsg;
} elseif (empty($reshook)) {
// The file functions must be in OS filesystem encoding.
$src_file_osencoded = dol_osencode($src_file);
$file_name_osencoded = dol_osencode($file_name);
// Check if destination dir is writable
if (!is_writable(dirname($file_name_osencoded))) {
dol_syslog("Files.lib::dol_move_uploaded_file Dir ".dirname($file_name_osencoded)." is not writable. Return 'ErrorDirNotWritable'", LOG_WARNING);
return 'ErrorDirNotWritable';
}
// Check if destination file already exists
if (!$allowoverwrite) {
if (file_exists($file_name_osencoded)) {
dol_syslog("Files.lib::dol_move_uploaded_file File ".$file_name." already exists. Return 'ErrorFileAlreadyExists'", LOG_WARNING);
return 'ErrorFileAlreadyExists';
}
} else { // We are allowed to erase
if (is_dir($file_name_osencoded)) { // If there is a directory with name of file to create
dol_syslog("Files.lib::dol_move_uploaded_file A directory with name ".$file_name." already exists. Return 'ErrorDirWithFileNameAlreadyExists'", LOG_WARNING);
return 'ErrorDirWithFileNameAlreadyExists';
}
}
// Move file using a simple system function
if ($mode == 0) {
$return = move_uploaded_file($src_file_osencoded, $file_name_osencoded);
} else {
$return = rename($src_file_osencoded, $file_name_osencoded);
}
if ($return) {
dolChmod($file_name_osencoded);
dol_syslog("Files.lib::dol_move_uploaded_file Success to move ".$src_file." to ".$file_name." - Umask=" . getDolGlobalString('MAIN_UMASK'), LOG_DEBUG);
return $successcode; // Success
} else {
dol_syslog("Files.lib::dol_move_uploaded_file Failed to move ".$src_file." to ".$file_name, LOG_ERR);
return -3; // Unknown error
}
}
return $successcode; // Success
}
/**
* Remove a file or several files with a mask.
* This delete file physically but also database indexes.
*
* @param string $file File to delete or mask of files to delete
* @param int $disableglob Disable usage of glob like * so function is an exact delete function that will return error if no file found
* @param int $nophperrors Disable all PHP output errors
* @param int $nohook Disable all hooks
* @param object|null $object Current object in use
* @param boolean $allowdotdot Allow to delete file path with .. inside. Never use this, it is reserved for migration purpose.
* @param int $indexdatabase Try to remove also index entries.
* @param int $nolog Disable log file
* @return boolean True if no error (file is deleted or if glob is used and there's nothing to delete), False if error
* @see dol_delete_dir()
*/
function dol_delete_file($file, $disableglob = 0, $nophperrors = 0, $nohook = 0, $object = null, $allowdotdot = false, $indexdatabase = 1, $nolog = 0)
{
global $db, $user;
global $hookmanager;
if (empty($nolog)) {
dol_syslog("dol_delete_file file=".$file." disableglob=".$disableglob." nophperrors=".$nophperrors." nohook=".$nohook);
}
// Security:
// We refuse transversal using .. and pipes into filenames.
if ((!$allowdotdot && preg_match('/\.\./', $file)) || preg_match('/[<>|]/', $file)) {
dol_syslog("Refused to delete file ".$file, LOG_WARNING);
return false;
}
$reshook = 0;
if (empty($nohook) && !empty($hookmanager)) {
$hookmanager->initHooks(array('fileslib'));
$parameters = array(
'file' => $file,
'disableglob' => $disableglob,
'nophperrors' => $nophperrors
);
$reshook = $hookmanager->executeHooks('deleteFile', $parameters, $object);
}
if (empty($nohook) && $reshook != 0) { // reshook = 0 to do standard actions, 1 = ok and replace, -1 = ko
dol_syslog("reshook=".$reshook);
if ($reshook < 0) {
return false;
}
return true;
} else {
$file_osencoded = dol_osencode($file); // New filename encoded in OS filesystem encoding charset
if (empty($disableglob) && !empty($file_osencoded)) {
$ok = true;
$globencoded = str_replace('[', '\[', $file_osencoded);
$globencoded = str_replace(']', '\]', $globencoded);
$listoffiles = glob($globencoded); // This scan dir for files. If file does not exists, return empty.
if (!empty($listoffiles) && is_array($listoffiles)) {
foreach ($listoffiles as $filename) {
if ($nophperrors) {
$ok = @unlink($filename);
} else {
$ok = unlink($filename);
}
// If it fails and it is because of the missing write permission on parent dir
if (!$ok && file_exists(dirname($filename)) && !(fileperms(dirname($filename)) & 0200)) {
dol_syslog("Error in deletion, but parent directory exists with no permission to write, we try to change permission on parent directory and retry...", LOG_DEBUG);
dolChmod(dirname($filename), decoct(fileperms(dirname($filename)) | 0200));
// Now we retry deletion
if ($nophperrors) {
$ok = @unlink($filename);
} else {
$ok = unlink($filename);
}
}
if ($ok) {
if (empty($nolog)) {
dol_syslog("Removed file ".$filename, LOG_DEBUG);
}
// Delete entry into ecm database
$rel_filetodelete = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $filename);
if (!preg_match('/(\/temp\/|\/thumbs\/|\.meta$)/', $rel_filetodelete)) { // If not a tmp file
if (is_object($db) && $indexdatabase) { // $db may not be defined when lib is in a context with define('NOREQUIREDB',1)
$rel_filetodelete = preg_replace('/^[\\/]/', '', $rel_filetodelete);
$rel_filetodelete = preg_replace('/\.noexe$/', '', $rel_filetodelete);
dol_syslog("Try to remove also entries in database for full relative path = ".$rel_filetodelete, LOG_DEBUG);
include_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
$ecmfile = new EcmFiles($db);
$entity = (isset($object->entity) ? $object->entity : null);
$result = $ecmfile->fetch(0, '', $rel_filetodelete, '', '', '', 0, $entity);
if ($result >= 0 && $ecmfile->id > 0) {
$result = $ecmfile->delete($user);
}
if ($result < 0) {
setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
}
}
}
} else {
dol_syslog("Failed to remove file ".$filename, LOG_WARNING);
// TODO Failure to remove can be because file was already removed or because of permission
// If error because it does not exists, we should return true, and we should return false if this is a permission problem
}
}
} else {
$ok = true; // nothing to delete when glob is on must return ok
dol_syslog("No files to delete found", LOG_DEBUG);
}
} else {
$ok = false;
if ($nophperrors) {
$ok = @unlink($file_osencoded);
} else {
$ok = unlink($file_osencoded);
}
$filename = $file_osencoded;
// If it fails and it is because of the missing write permission on parent dir
if (!$ok && file_exists(dirname($filename)) && !(fileperms(dirname($filename)) & 0200)) {
dol_syslog("Error in deletion, but parent directory exists with no permission to write, we try to change permission on parent directory and retry...", LOG_DEBUG);
dolChmod(dirname($filename), decoct(fileperms(dirname($filename)) | 0200));
// Now we retry deletion
if ($nophperrors) {
$ok = @unlink($filename);
} else {
$ok = unlink($filename);
}
}
if ($ok) {
if (empty($nolog)) {
dol_syslog("Removed file ".$filename, LOG_DEBUG);
}
// Delete entry into ecm database
$rel_filetodelete = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $filename);
if (!preg_match('/(\/temp\/|\/thumbs\/|\.meta$)/', $rel_filetodelete)) { // If not a tmp file
if (is_object($db) && $indexdatabase) { // $db may not be defined when lib is in a context with define('NOREQUIREDB',1)
$rel_filetodelete = preg_replace('/^[\\/]/', '', $rel_filetodelete);
$rel_filetodelete = preg_replace('/\.noexe$/', '', $rel_filetodelete);
dol_syslog("Try to remove also entries in database for full relative path = ".$rel_filetodelete, LOG_DEBUG);
include_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
$ecmfile = new EcmFiles($db);
$entity = (isset($object->entity) ? $object->entity : null);
$result = $ecmfile->fetch(0, '', $rel_filetodelete, '', '', '', 0, $entity);
if ($result >= 0 && $ecmfile->id > 0) {
$result = $ecmfile->delete($user);
}
if ($result < 0) {
setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
}
}
}
} else {
dol_syslog("Failed to remove file ".$filename, LOG_WARNING);
}
}
return $ok;
}
}
/**
* Remove a directory (not recursive, so content must be empty).
* If directory is not empty, return false
*
* @param string $dir Directory to delete
* @param int $nophperrors Disable all PHP output errors
* @return boolean True if success, false if error
* @see dol_delete_file() dolCopyDir()
*/
function dol_delete_dir($dir, $nophperrors = 0)
{
// Security:
// We refuse transversal using .. and pipes into filenames.
if (preg_match('/\.\./', $dir) || preg_match('/[<>|]/', $dir)) {
dol_syslog("Refused to delete dir ".$dir.' (contains invalid char sequence)', LOG_WARNING);
return false;
}
$dir_osencoded = dol_osencode($dir);
return ($nophperrors ? @rmdir($dir_osencoded) : rmdir($dir_osencoded));
}
/**
* Remove a directory $dir and its subdirectories (or only files and subdirectories)
*
* @param string $dir Dir to delete
* @param int $count Counter to count nb of elements found to delete
* @param int $nophperrors Disable all PHP output errors
* @param int $onlysub Delete only files and subdir, not main directory
* @param int $countdeleted Counter to count nb of elements found really deleted
* @param int $indexdatabase Try to remove also index entries.
* @param int $nolog 1=Disable log for files and subdirectories (too verbose when making recursive directories)
* @param int $level 0 = level or recursivity
* @return int Number of files and directory we try to remove. NB really removed is returned into var by reference $countdeleted.
*/
function dol_delete_dir_recursive($dir, $count = 0, $nophperrors = 0, $onlysub = 0, &$countdeleted = 0, $indexdatabase = 1, $nolog = 0, $level = 0)
{
if (empty($nolog) || empty($level)) {
dol_syslog("functions.lib:dol_delete_dir_recursive ".$dir, LOG_DEBUG);
}
if ($level > 1000) {
dol_syslog("functions.lib:dol_delete_dir_recursive too many depth", LOG_WARNING);
}
if (dol_is_dir($dir)) {
$dir_osencoded = dol_osencode($dir);
if ($handle = opendir("$dir_osencoded")) {
while (false !== ($item = readdir($handle))) {
if (!utf8_check($item)) {
$item = mb_convert_encoding($item, 'UTF-8', 'ISO-8859-1'); // should be useless
}
if ($item != "." && $item != "..") {
if (is_dir(dol_osencode("$dir/$item")) && !is_link(dol_osencode("$dir/$item"))) {
$count = dol_delete_dir_recursive("$dir/$item", $count, $nophperrors, 0, $countdeleted, $indexdatabase, $nolog, ($level + 1));
} else {
chmod(dol_osencode("$dir/$item"), 0755);
$result = dol_delete_file("$dir/$item", 1, $nophperrors, 0, null, false, $indexdatabase, $nolog);
$count++;
if ($result) {
$countdeleted++;
}
//else print 'Error on '.$item."\n";
}
}
}
closedir($handle);
// Delete also the main directory
if (empty($onlysub)) {
$result = dol_delete_dir($dir, $nophperrors);
$count++;
if ($result) {
$countdeleted++;
}
//else print 'Error on '.$dir."\n";
}
}
}
return $count;
}
/**
* Delete all preview files linked to object instance.
* Note that preview image of PDF files is generated when required, by dol_banner_tab() for example.
*
* @param object $object Object to clean
* @return int 0 if error, 1 if OK
* @see dol_convert_file()
*/
function dol_delete_preview($object)
{
global $langs, $conf;
// Define parent dir of elements
$element = $object->element;
if ($object->element == 'order_supplier') {
$dir = $conf->fournisseur->commande->dir_output;
} elseif ($object->element == 'invoice_supplier') {
$dir = $conf->fournisseur->facture->dir_output;
} elseif ($object->element == 'project') {
$dir = $conf->project->dir_output;
} elseif ($object->element == 'shipping') {
$dir = $conf->expedition->dir_output.'/sending';
} elseif ($object->element == 'delivery') {
$dir = $conf->expedition->dir_output.'/receipt';
} elseif ($object->element == 'fichinter') {
$dir = $conf->ficheinter->dir_output;
} else {
$dir = empty($conf->$element->dir_output) ? '' : $conf->$element->dir_output;
}
if (empty($dir)) {
$object->error = $langs->trans('ErrorObjectNoSupportedByFunction');
return 0;
}
$refsan = dol_sanitizeFileName($object->ref);
$dir = $dir."/".$refsan;
$filepreviewnew = $dir."/".$refsan.".pdf_preview.png";
$filepreviewnewbis = $dir."/".$refsan.".pdf_preview-0.png";
$filepreviewold = $dir."/".$refsan.".pdf.png";
// For new preview files
if (file_exists($filepreviewnew) && is_writable($filepreviewnew)) {
if (!dol_delete_file($filepreviewnew, 1)) {
$object->error = $langs->trans("ErrorFailedToDeleteFile", $filepreviewnew);
return 0;
}
}
if (file_exists($filepreviewnewbis) && is_writable($filepreviewnewbis)) {
if (!dol_delete_file($filepreviewnewbis, 1)) {
$object->error = $langs->trans("ErrorFailedToDeleteFile", $filepreviewnewbis);
return 0;
}
}
// For old preview files
if (file_exists($filepreviewold) && is_writable($filepreviewold)) {
if (!dol_delete_file($filepreviewold, 1)) {
$object->error = $langs->trans("ErrorFailedToDeleteFile", $filepreviewold);
return 0;
}
} else {
$multiple = $filepreviewold.".";
for ($i = 0; $i < 20; $i++) {
$preview = $multiple.$i;
if (file_exists($preview) && is_writable($preview)) {
if (!dol_delete_file($preview, 1)) {
$object->error = $langs->trans("ErrorFailedToOpenFile", $preview);
return 0;
}
}
}
}
return 1;
}
/**
* Create a meta file with document file into same directory.
* This make "grep" search possible.
* This feature to generate the meta file is enabled only if option MAIN_DOC_CREATE_METAFILE is set.
*
* @param CommonObject $object Object
* @return int 0 if do nothing, >0 if we update meta file too, <0 if KO
*/
function dol_meta_create($object)
{
global $conf;
// Create meta file
if (!getDolGlobalString('MAIN_DOC_CREATE_METAFILE')) {
return 0; // By default, no metafile.
}
// Define parent dir of elements
$element = $object->element;
if ($object->element == 'order_supplier') {
$dir = $conf->fournisseur->dir_output.'/commande';
} elseif ($object->element == 'invoice_supplier') {
$dir = $conf->fournisseur->dir_output.'/facture';
} elseif ($object->element == 'project') {
$dir = $conf->project->dir_output;
} elseif ($object->element == 'shipping') {
$dir = $conf->expedition->dir_output.'/sending';
} elseif ($object->element == 'delivery') {
$dir = $conf->expedition->dir_output.'/receipt';
} elseif ($object->element == 'fichinter') {
$dir = $conf->ficheinter->dir_output;
} else {
$dir = empty($conf->$element->dir_output) ? '' : $conf->$element->dir_output;
}
if ($dir) {
$object->fetch_thirdparty();
$objectref = dol_sanitizeFileName((string) $object->ref);
$dir = $dir."/".$objectref;
$file = $dir."/".$objectref.".meta";
if (!is_dir($dir)) {
dol_mkdir($dir);
}
$meta = '';
if (is_dir($dir)) {
if (is_countable($object->lines) && count($object->lines) > 0) {
$nblines = count($object->lines);
} else {
$nblines = 0;
}
$client = $object->thirdparty->name." ".$object->thirdparty->address." ".$object->thirdparty->zip." ".$object->thirdparty->town;
$meta = "REFERENCE=\"".$object->ref."\"
DATE=\"" . dol_print_date($object->date, '')."\"
NB_ITEMS=\"" . $nblines."\"
CLIENT=\"" . $client."\"
AMOUNT_EXCL_TAX=\"" . $object->total_ht."\"
AMOUNT=\"" . $object->total_ttc."\"\n";
for ($i = 0; $i < $nblines; $i++) {
//Pour les articles
$meta .= "ITEM_".$i."_QUANTITY=\"".$object->lines[$i]->qty."\"
ITEM_" . $i."_AMOUNT_WO_TAX=\"".$object->lines[$i]->total_ht."\"
ITEM_" . $i."_VAT=\"".$object->lines[$i]->tva_tx."\"
ITEM_" . $i."_DESCRIPTION=\"".str_replace("\r\n", "", nl2br($object->lines[$i]->desc))."\"
";
}
}
$fp = fopen($file, "w");
fwrite($fp, $meta);
fclose($fp);
dolChmod($file);
return 1;
} else {
dol_syslog('FailedToDetectDirInDolMetaCreateFor'.$object->element, LOG_WARNING);
}
return 0;
}
/**
* Scan a directory and init $_SESSION to manage uploaded files with list of all found files.
* Note: Only email module seems to use this. Other feature initialize the $_SESSION doing $formmail->clear_attached_files(); $formmail->add_attached_files()
*
* @param string $pathtoscan Path to scan
* @param string $trackid Track id (used to prefix name of session vars to avoid conflict)
* @return void
*/
function dol_init_file_process($pathtoscan = '', $trackid = '')
{
$listofpaths = array();
$listofnames = array();
$listofmimes = array();
if ($pathtoscan) {
$listoffiles = dol_dir_list($pathtoscan, 'files');
foreach ($listoffiles as $key => $val) {
$listofpaths[] = $val['fullname'];
$listofnames[] = $val['name'];
$listofmimes[] = dol_mimetype($val['name']);
}
}
$keytoavoidconflict = empty($trackid) ? '' : '-'.$trackid;
$_SESSION["listofpaths".$keytoavoidconflict] = implode(';', $listofpaths);
$_SESSION["listofnames".$keytoavoidconflict] = implode(';', $listofnames);
$_SESSION["listofmimes".$keytoavoidconflict] = implode(';', $listofmimes);
}
/**
* Get and save an upload file (for example after submitting a new file in a mail form).
* The database index of the file is also updated if $updatesessionordb is set to 1.
* Function can work in 2 mode, one to get file from $_FILES, one to get file from its full path.
* Note: This function can be used only into a HTML page context.
*
* @param string $upload_dir If mode=0, directory where to store uploaded file (note: used to forge $destpath = $upload_dir + filename). If $mode=1, the full destination path.
* @param int<0,1> $allowoverwrite 1=Allow overwrite existing file
* @param int<-1,1> $updatesessionordb 1=Do no edit _SESSION variable but update database index. 0=Update _SESSION and not database index. -1=Do not update SESSION neither db.
* @param string $keyforsourcefile If $mode=0, the key into _FILES with information of the file to move. If $mode=1, the source file name on disk (To Implement).
* @param string $savingdocmask Mask to use to define output filename. For example 'XXXXX-__YYYYMMDD__-__file__'
* @param ?string $link Link to add (to add a link instead of a file)
* @param string $trackid Track id (used to prefix name of session vars to avoid conflict, when $updatesessionordb is 0)
* @param int<0,1> $generatethumbs 1=Generate also thumbs for uploaded image files
* @param ?Object $object Object used to set the fields src_object_type and src_object_id.
* @param string $forceFullTextIndexation '1'=Force full text storage in database even if global option not set (consume a high level of data)
* @param int $mode 0=Default mode use to move a file from default system upload dir to $upload_dir. 1=Mode to move an uploaded file from $keyforsourcefile into $upload_dir.
* @return int Return integer <=0 if KO, nb of success if OK (>0)
* @see dol_remove_file_process(), FileUpload::handleFileUpload()
*/
function dol_add_file_process($upload_dir, $allowoverwrite = 0, $updatesessionordb = 0, $keyforsourcefile = 'addedfile', $savingdocmask = '', $link = null, $trackid = '', $generatethumbs = 1, $object = null, $forceFullTextIndexation = '', $mode = 0)
{
global $db, $user, $conf, $langs;
$res = 0;
// If mode 1, prepare environment to be compatible with mode 0
if ($mode == 1) {
$_FILES = array($keyforsourcefile => array());
$_FILES[$keyforsourcefile]['tmp_name'] = $keyforsourcefile;
$_FILES[$keyforsourcefile]['name'] = $keyforsourcefile;
$mode = 0;
}
if (!empty($_FILES[$keyforsourcefile])) { // For view $_FILES[$keyforsourcefile]['error']
dol_syslog('dol_add_file_process varfiles = '.$keyforsourcefile.' upload_dir='.$upload_dir.' allowoverwrite='.$allowoverwrite.' updatesessionordb='.$updatesessionordb.' savingdocmask='.$savingdocmask, LOG_DEBUG);
$maxfilesinform = getDolGlobalInt("MAIN_SECURITY_MAX_ATTACHMENT_ON_FORMS", 10);
if (is_array($_FILES[$keyforsourcefile]["name"]) && count($_FILES[$keyforsourcefile]["name"]) > $maxfilesinform) {
$langs->load("errors"); // key must be loaded because we can't rely on loading during output, we need var substitution to be done now.
setEventMessages($langs->trans("ErrorTooMuchFileInForm", $maxfilesinform), null, "errors");
return -1;
}
$result = dol_mkdir($upload_dir);
//var_dump($result);exit;
if ($result >= 0) {
$TFile = $_FILES[$keyforsourcefile];
// Convert value of $TFile
if (!is_array($TFile['name'])) {
foreach ($TFile as $key => &$val) {
$val = array($val);
}
}
$nbfile = count($TFile['name']);
$nbok = 0;
for ($i = 0; $i < $nbfile; $i++) {
if (empty($TFile['name'][$i])) {
continue; // For example, when submitting a form with no file name
}
// Define $destfull (path to file including filename) and $destfile (only filename)
$destfile = trim($TFile['name'][$i]);
$destfull = $upload_dir."/".$destfile;
$destfilewithoutext = preg_replace('/\.[^\.]+$/', '', $destfile);
if ($savingdocmask && strpos($savingdocmask, $destfilewithoutext) !== 0) {
$destfile = trim(preg_replace('/__file__/', $TFile['name'][$i], $savingdocmask));
$destfull = $upload_dir."/".$destfile;
}
$filenameto = basename($destfile);
if (preg_match('/^\./', $filenameto)) {
$langs->load("errors"); // key must be loaded because we can't rely on loading during output, we need var substitution to be done now.
setEventMessages($langs->trans("ErrorFilenameCantStartWithDot", $filenameto), null, 'errors');
break;
}
// dol_sanitizeFileName the file name and lowercase extension
$info = pathinfo($destfull);
$destfull = $info['dirname'].'/'.dol_sanitizeFileName($info['filename'].($info['extension'] != '' ? ('.'.strtolower($info['extension'])) : ''));
$info = pathinfo($destfile);
$destfile = dol_sanitizeFileName($info['filename'].($info['extension'] != '' ? ('.'.strtolower($info['extension'])) : ''));
// Check extension is allowed for upload.
// Guard against partial upgrades where files.lib.php has been refreshed
// but functions.lib.php has not been reloaded with getExecutableContent() yet.
$defaultexecutableextensions = function_exists('getExecutableContent') ? implode(',', getExecutableContent()) : 'htm,html,shtml,js,phar,php,php3,php4,php5,phtml,pht,pl,py,cgi,ksh,sh,bash,bat,cmd,wpk,exe';
$fileextensionrestriction = getDolGlobalString("MAIN_FILE_EXTENSION_UPLOAD_RESTRICTION", $defaultexecutableextensions);
if (!empty($fileextensionrestriction)) {
$arrayofregexextension = explode(",", $fileextensionrestriction);
foreach ($arrayofregexextension as $fileextension) {
if (preg_match('/\.'.preg_quote(trim($fileextension), '/').'$/i', $destfull)) {
$langs->load("errors"); // key must be loaded because we can't rely on loading during output, we need var substitution to be done now.
setEventMessages($langs->trans("ErrorFilenameExtensionNotAllowed", $filenameto), null, 'errors');
return -1;
}
}
}
// We apply dol_string_nohtmltag also to clean file names (this remove duplicate spaces) because
// this function is also applied when we rename and when we make try to download file (by the GETPOST(filename, 'alphanohtml') call).
$destfile = dol_string_nohtmltag($destfile);
$destfull = dol_string_nohtmltag($destfull);
// Check that filename is not the one of a reserved allowed CLI command
global $dolibarr_main_restrict_os_commands;
if (!empty($dolibarr_main_restrict_os_commands)) {
$arrayofallowedcommand = explode(',', $dolibarr_main_restrict_os_commands);
$arrayofallowedcommand = array_map('trim', $arrayofallowedcommand);
if (in_array($destfile, $arrayofallowedcommand)) {
$langs->load("errors"); // key must be loaded because we can't rely on loading during output, we need var substitution to be done now.
setEventMessages($langs->trans("ErrorFilenameReserved", $destfile), null, 'errors');
return -1;
}
}
// Move file from source directory to final destination. Check for virus is also embedded and a .noexe may also be appended on file name.
$resupload = dol_move_uploaded_file($TFile['tmp_name'][$i], $destfull, $allowoverwrite, 0, $TFile['error'][$i], 0, $keyforsourcefile, $upload_dir, $mode);
if (is_numeric($resupload) && $resupload > 0) { // $resupload can be 'ErrorFileAlreadyExists', 'ErrorFileIsInfectedWithAVirus...'
include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
$tmparraysize = getDefaultImageSizes();
$maxwidthsmall = $tmparraysize['maxwidthsmall'];
$maxheightsmall = $tmparraysize['maxheightsmall'];
$maxwidthmini = $tmparraysize['maxwidthmini'];
$maxheightmini = $tmparraysize['maxheightmini'];
//$quality = $tmparraysize['quality'];
$quality = 50; // For thumbs, we force quality to 50
// Generate thumbs.
if ($generatethumbs) {
if (image_format_supported($destfull) == 1) {
// Create thumbs
// We can't use $object->addThumbs here because there is no $object known
// Used on logon for example
$imgThumbSmall = vignette($destfull, $maxwidthsmall, $maxheightsmall, '_small', $quality, "thumbs");
// Create mini thumbs for image (Ratio is near 16/9)
// Used on menu or for setup page for example
$imgThumbMini = vignette($destfull, $maxwidthmini, $maxheightmini, '_mini', $quality, "thumbs");
}
}
// Update session
if (empty($updatesessionordb)) {
include_once DOL_DOCUMENT_ROOT.'/core/class/html.formmail.class.php';
$formmail = new FormMail($db);
$formmail->trackid = $trackid;
$formmail->add_attached_files($destfull, $destfile, $TFile['type'][$i]);
}
// Update index table of files (llx_ecm_files)
if ($updatesessionordb == 1) {
$sharefile = 0;
if ($TFile['type'][$i] == 'application/pdf' && strpos($_SERVER["REQUEST_URI"], 'product') !== false && getDolGlobalString('PRODUCT_ALLOW_EXTERNAL_DOWNLOAD')) {
$sharefile = 1;
}
// If we allow overwrite, we may need to also overwrite index, so we delete index first so insert can work
if ($allowoverwrite) {
deleteFilesIntoDatabaseIndex($upload_dir, basename($destfile).($resupload == 2 ? '.noexe' : ''), '', $object);
}
$result = addFileIntoDatabaseIndex($upload_dir, basename($destfile).($resupload == 2 ? '.noexe' : ''), $TFile['name'][$i], 'uploaded', $sharefile, $object, $forceFullTextIndexation);
if ($result < 0) {
if ($allowoverwrite) {
// Do not show error message. We can have an error due to DB_ERROR_RECORD_ALREADY_EXISTS
} else {
setEventMessages('WarningFailedToAddFileIntoDatabaseIndex', null, 'warnings');
}
}
}
$nbok++;
} else {
$langs->load("errors");
if (is_numeric($resupload) && $resupload < 0) { // Unknown error
setEventMessages($langs->trans("ErrorFileNotUploaded"), null, 'errors');
} elseif (preg_match('/ErrorFileIsInfectedWithAVirus/', $resupload)) { // Files infected by a virus
if (preg_match('/File is a PDF with javascript inside/', $resupload)) {
setEventMessages($langs->trans("ErrorFileIsAnInfectedPDFWithJSInside"), null, 'errors');
} else {
setEventMessages($langs->trans("ErrorFileIsInfectedWithAVirus").'<br>'.dolGetFirstLineOfText($resupload), null, 'errors');
}
} else { // Known error
setEventMessages($langs->trans($resupload), null, 'errors');
}
}
}
if ($nbok > 0) {
$res = $nbok;
setEventMessages($langs->trans("FileTransferComplete"), null, 'mesgs');
}
} else {
setEventMessages($langs->trans("ErrorFailedToCreateDir", $upload_dir), null, 'errors');
}
} elseif ($link) {
require_once DOL_DOCUMENT_ROOT.'/core/class/link.class.php';
$linkObject = new Link($db);
$linkObject->entity = $conf->entity;
$linkObject->url = $link;
$linkObject->objecttype = GETPOST('objecttype', 'alpha');
$linkObject->objectid = GETPOSTINT('objectid');
$linkObject->label = GETPOST('label', 'alpha');
$res = $linkObject->create($user);
if ($res > 0) {
setEventMessages($langs->trans("LinkComplete"), null, 'mesgs');
} else {
setEventMessages($langs->trans("ErrorFileNotLinked"), null, 'errors');
}
} else {
$langs->load("errors");
setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentities("File")), null, 'errors');
}
return $res;
}
/**
* Remove an uploaded file (for example after submitting a new file a mail form).
* All information used are in db, conf, langs, user and _FILES.
*
* @param int $filenb File nb to delete
* @param int $donotupdatesession -1 or 1 = Do not update _SESSION variable
* @param int $donotdeletefile 1=Do not delete physically file
* @param string $trackid Track id (used to prefix name of session vars to avoid conflict)
* @return void
* @see dol_add_file_process()
*/
function dol_remove_file_process($filenb, $donotupdatesession = 0, $donotdeletefile = 1, $trackid = '')
{
global $db, $langs;
$keytodelete = $filenb;
$keytodelete--;
$listofpaths = array();
$listofnames = array();
$listofmimes = array();
$keytoavoidconflict = empty($trackid) ? '' : '-'.$trackid;
if (!empty($_SESSION["listofpaths".$keytoavoidconflict])) {
$listofpaths = explode(';', $_SESSION["listofpaths".$keytoavoidconflict]);
}
if (!empty($_SESSION["listofnames".$keytoavoidconflict])) {
$listofnames = explode(';', $_SESSION["listofnames".$keytoavoidconflict]);
}
if (!empty($_SESSION["listofmimes".$keytoavoidconflict])) {
$listofmimes = explode(';', $_SESSION["listofmimes".$keytoavoidconflict]);
}
if ($keytodelete >= 0) {
$pathtodelete = $listofpaths[$keytodelete];
$filetodelete = $listofnames[$keytodelete];
if (empty($donotdeletefile)) {
$result = dol_delete_file($pathtodelete, 1); // The delete of ecm database is inside the function dol_delete_file
} else {
$result = 0;
}
if ($result >= 0) {
if (empty($donotdeletefile)) {
$langs->load("other");
setEventMessages($langs->trans("FileWasRemoved", $filetodelete), null, 'mesgs');
}
if (empty($donotupdatesession)) {
include_once DOL_DOCUMENT_ROOT.'/core/class/html.formmail.class.php';
$formmail = new FormMail($db);
$formmail->trackid = $trackid;
$formmail->remove_attached_files($keytodelete);
}
}
}
}
/**
* Add a file into database index.
* Called by dol_add_file_process when uploading a file and on other cases.
* See also commonGenerateDocument that also add/update database index when a file is generated.
*
* @param string $dir Directory name (full real path without ending /)
* @param string $file File name (May end with '.noexe')
* @param string $fullpathorig Full path of origin for file (can be '')
* @param string $mode How file was created ('uploaded', 'generated', ...)
* @param int $setsharekey Set also the share key
* @param ?Object $object Object used to set 'src_object_*' fields
* @param string $forceFullTextIndexation '1'=Force full text indexation even if global option not set
* @return int Return integer <0 if KO, 0 if nothing done, >0 if OK
*/
function addFileIntoDatabaseIndex($dir, $file, $fullpathorig = '', $mode = 'uploaded', $setsharekey = 0, $object = null, $forceFullTextIndexation = '')
{
global $db, $user;
$result = 0;
$error = 0;
dol_syslog("addFileIntoDatabaseIndex dir=".$dir." file=".$file, LOG_DEBUG);
$rel_dir = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $dir);
if (!preg_match('/[\\/]temp[\\/]|[\\/]thumbs|\.meta$/', $rel_dir)) { // If not a temporary directory. TODO Does this test work ?
$filename = basename(preg_replace('/\.noexe$/', '', $file));
$rel_dir = preg_replace('/[\\/]$/', '', $rel_dir);
$rel_dir = preg_replace('/^[\\/]/', '', $rel_dir);
include_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
$ecmfile = new EcmFiles($db);
$ecmfile->filepath = $rel_dir;
$ecmfile->filename = $filename;
$ecmfile->label = md5_file(dol_osencode($dir.'/'.$file)); // MD5 of file content
$ecmfile->fullpath_orig = $fullpathorig;
$ecmfile->gen_or_uploaded = $mode;
$ecmfile->description = ''; // indexed content
$ecmfile->keywords = ''; // keyword content
if (is_object($object) && $object->id > 0) {
$ecmfile->src_object_id = $object->id;
if (isset($object->table_element)) {
$ecmfile->src_object_type = $object->table_element;
} else {
dol_syslog('Error: object ' . get_class($object) . ' has no table_element attribute.');
return -1;
}
if (isset($object->src_object_description)) {
$ecmfile->description = $object->src_object_description;
}
if (isset($object->src_object_keywords)) {
$ecmfile->keywords = $object->src_object_keywords;
}
if (isset($object->entity)) {
$ecmfile->entity = $object->entity;
}
}
if (getDolGlobalString('MAIN_FORCE_SHARING_ON_ANY_UPLOADED_FILE')) {
$setsharekey = 1;
}
if ($setsharekey) {
require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php';
$ecmfile->share = getRandomPassword(true);
}
// Use a convert tool for Doc to Text
$useFullTextIndexation = getDolGlobalString('MAIN_SAVE_FILE_CONTENT_AS_TEXT'); // Can be '', 'pdftotext' or 'docling'
if (empty($useFullTextIndexation) && $forceFullTextIndexation == '1') {
if (getDolGlobalString('MAIN_SAVE_FILE_CONTENT_AS_TEXT_PDFTOTEXT')) { // Command line for pdftotext
$useFullTextIndexation = 'pdftotext';
} elseif (getDolGlobalString('MAIN_SAVE_FILE_CONTENT_AS_TEXT_DOCLING')) { // Command line for docling
$useFullTextIndexation = 'docling';
}
}
//$useFullTextIndexation = 1;
if ($useFullTextIndexation) {
$ecmfile->filepath = $rel_dir;
$ecmfile->filename = $filename;
$filetoprocess = $dir.'/'.$ecmfile->filename;
$textforfulltextindex = '';
$keywords = '';
$cmd = '';
if (preg_match('/\.pdf/i', $filename)) {
// Convertfile into text
$result = dolDocToText($filetoprocess);
if (empty($result['error'])) {
$textforfulltextindex = $result['content'];
$filetoprocess = $result['keywords'];
$cmd = $result['cmd'];
} else {
$error++;
}
}
if ($cmd) {
$ecmfile->description = 'File content generated by '.$cmd;
}
$ecmfile->content = $textforfulltextindex;
$ecmfile->keywords = $keywords;
}
if (!$error) {
$result = $ecmfile->create($user);
if ($result < 0) {
dol_syslog($ecmfile->error);
}
}
}
return $result;
}
/**
* Delete files into database index using search criteria.
*
* @param string $dir Directory name (full real path without ending /)
* @param string $file File name
* @param string $mode How file was created ('uploaded', 'generated', ...)
* @param ?CommonObject $object object used for entity
* @return int Return integer <0 if KO, 0 if nothing done, >0 if OK
*/
function deleteFilesIntoDatabaseIndex($dir, $file, $mode = 'uploaded', $object = null)
{
global $conf, $db;
$error = 0;
if (empty($dir)) {
dol_syslog("deleteFilesIntoDatabaseIndex: dir parameter can't be empty", LOG_ERR);
return -1;
}
dol_syslog("deleteFilesIntoDatabaseIndex dir=".$dir." file=".$file, LOG_DEBUG);
$db->begin();
$rel_dir = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $dir);
if (!preg_match('/[\\/]temp[\\/]|[\\/]thumbs|\.meta$/', $rel_dir)) { // If not a temporary directory. TODO Does this test work ?
//$filename = basename($file);
$rel_dir = preg_replace('/[\\/]$/', '', $rel_dir);
$rel_dir = preg_replace('/^[\\/]/', '', $rel_dir);
if (!$error) {
$sql = 'DELETE FROM '.MAIN_DB_PREFIX.'ecm_files';
if (isset($object->entity)) {
$sql .= ' WHERE entity = ' . ((int) $object->entity);
} else {
$sql .= ' WHERE entity = ' . ((int) $conf->entity);
}
$sql .= " AND filepath = '".$db->escape($rel_dir)."'";
if ($file) {
$sql .= " AND filename = '".$db->escape($file)."'";
}
if ($mode) {
$sql .= " AND gen_or_uploaded = '".$db->escape($mode)."'";
}
$resql = $db->query($sql);
if (!$resql) {
$error++;
dol_syslog(__FUNCTION__.' '.$db->lasterror(), LOG_ERR);
}
}
}
// Commit or rollback
if ($error) {
$db->rollback();
return -1 * $error;
} else {
$db->commit();
return 1;
}
}
/**
* Check if a file is a real PDF file by checking its signature and its MIME type.
* Testing first char may be enough. No need to use the finfo_file().
*
* @param string $filePath File path to check
* @return bool True if the file is a real PDF, false otherwise.
*/
function isRealPdf(string $filePath)
{
if (!is_file($filePath) || !is_readable($filePath)) {
return false;
}
// Open file
$handle = fopen($filePath, 'rb');
if (!$handle) {
return false;
}
$header = fread($handle, 5);
fclose($handle);
if ($header !== '%PDF-') {
return false;
}
// Check using finfo_file
/*
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $filePath);
finfo_close($finfo);
if ($mime !== 'application/pdf') {
return false;
}
*/
return true;
}
/**
* Convert a PDF file into another image format.
* This need Imagick php extension. You can use dol_imageResizeOrCrop() for a function that need GD.
*
* @param string $fileinput Input full path name
* @param string $ext Format of target file (It is also extension added to file if fileoutput is not provided).
* @param string $fileoutput Output filename
* @param string $page Page number if we convert a PDF into png
* @return int Return integer <0 if KO, 0=Nothing done, >0 if OK
* @see dol_imageResizeOrCrop()
*/
function dol_convert_file($fileinput, $ext = 'png', $fileoutput = '', $page = '')
{
if (class_exists('Imagick')) {
$image = new Imagick();
try {
// Imagick may have a support for Magick Scripting Language (MSL) that allows to run execution code with some files like SVG. So we need to check
// that file is really a PDF file.
// Note: The Imagick policy options can be disabled into /etc/ImageMagick*/policy.xml.
if (!isRealPdf($fileinput)) {
dol_syslog("We try to convert a PDF file with name ".$fileinput." but it is not a real PDF file (hack attempt ?).", LOG_WARNING);
return -4;
}
$filetoconvert = $fileinput.(($page != '') ? '['.$page.']' : '');
//var_dump($filetoconvert);
$ret = $image->readImage($filetoconvert);
} catch (Exception $e) {
$ext = pathinfo($fileinput, PATHINFO_EXTENSION);
dol_syslog("Failed to read image using Imagick (Try to install package 'apt-get install php-imagick ghostscript' and check there is no policy to disable ".$ext." conversion in /etc/ImageMagick*/policy.xml): ".$e->getMessage(), LOG_WARNING);
return 0;
}
if ($ret) {
$ret = $image->setImageFormat($ext);
if ($ret) {
if (empty($fileoutput)) {
$fileoutput = $fileinput.".".$ext;
}
$count = $image->getNumberImages();
if (!dol_is_file($fileoutput) || is_writable($fileoutput)) {
try {
$ret = $image->writeImages($fileoutput, true);
} catch (Exception $e) {
dol_syslog($e->getMessage(), LOG_WARNING);
}
} else {
dol_syslog("Warning: Failed to write cache preview file '.$fileoutput.'. Check permission on file/dir", LOG_ERR);
}
if ($ret) {
return $count;
} else {
return -3;
}
} else {
return -2;
}
} else {
return -1;
}
} else {
return 0;
}
}
/**
* Compress a file.
* An error string may be returned into parameters.
*
* @param string $inputfile Source file name
* @param string $outputfile Target file name
* @param string $mode 'gz' or 'bz' or 'zip'
* @param string $errorstring Error string
* @return int Return integer <0 if KO, >0 if OK
* @see dol_uncompress(), dol_compress_dir()
*/
function dol_compress_file($inputfile, $outputfile, $mode = "gz", &$errorstring = null)
{
$foundhandler = 0;
//var_dump(basename($inputfile)); exit;
try {
dol_syslog("dol_compress_file mode=".$mode." inputfile=".$inputfile." outputfile=".$outputfile);
$data = implode("", file(dol_osencode($inputfile)));
$compressdata = null;
if ($mode == 'gz' && function_exists('gzencode')) {
$foundhandler = 1;
$compressdata = gzencode($data, 9);
} elseif ($mode == 'bz' && function_exists('bzcompress')) {
$foundhandler = 1;
$compressdata = bzcompress($data, 9);
} elseif ($mode == 'zstd' && function_exists('zstd_compress')) {
$foundhandler = 1;
$compressdata = zstd_compress($data, 9);
} elseif ($mode == 'zip') {
if (class_exists('ZipArchive') && getDolGlobalString('MAIN_USE_ZIPARCHIVE_FOR_ZIP_COMPRESS')) {
$foundhandler = 1;
$rootPath = realpath($inputfile);
dol_syslog("Class ZipArchive is set so we zip using ZipArchive to zip into ".$outputfile.' rootPath='.$rootPath);
$zip = new ZipArchive();
if ($zip->open($outputfile, ZipArchive::CREATE) !== true) {
$errorstring = "dol_compress_file failure - Failed to open file ".$outputfile."\n";
dol_syslog($errorstring, LOG_ERR);
global $errormsg;
$errormsg = $errorstring;
return -6;
}
// Create recursive directory iterator
/** @var SplFileInfo[] $files */
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($rootPath, FilesystemIterator::UNIX_PATHS),
RecursiveIteratorIterator::LEAVES_ONLY
);
'@phan-var-force SplFileInfo[] $files';
foreach ($files as $name => $file) {
// Skip directories (they would be added automatically)
if (!$file->isDir()) {
// Get real and relative path for current file
$filePath = $file->getPath(); // the full path with filename using the $inputdir root.
$fileName = $file->getFilename();
$fileFullRealPath = $file->getRealPath(); // the full path with name and transformed to use real path directory.
//$relativePath = substr($fileFullRealPath, strlen($rootPath) + 1);
$relativePath = substr(($filePath ? $filePath.'/' : '').$fileName, strlen($rootPath) + 1);
// Add current file to archive
$zip->addFile($fileFullRealPath, $relativePath);
}
}
// Zip archive will be created only after closing object
$zip->close();
dol_syslog("dol_compress_file success - ".$zip->numFiles." files");
return 1;
}
if (defined('ODTPHP_PATHTOPCLZIP')) {
$foundhandler = 1;
include_once ODTPHP_PATHTOPCLZIP.'pclzip.lib.php';
$archive = new PclZip($outputfile);
$result = $archive->add($inputfile, PCLZIP_OPT_REMOVE_PATH, dirname($inputfile));
if ($result === 0) {
global $errormsg;
$errormsg = $archive->errorInfo(true);
if ($archive->errorCode() == PCLZIP_ERR_WRITE_OPEN_FAIL) {
$errorstring = "PCLZIP_ERR_WRITE_OPEN_FAIL";
dol_syslog("dol_compress_file error - archive->errorCode() = PCLZIP_ERR_WRITE_OPEN_FAIL", LOG_ERR);
return -4;
}
$errorstring = "dol_compress_file error archive->errorCode = ".$archive->errorCode()." errormsg=".$errormsg;
dol_syslog("dol_compress_file failure - ".$errormsg, LOG_ERR);
return -3;
} else {
dol_syslog("dol_compress_file success - ".count($result)." files");
return 1;
}
}
}
if ($foundhandler && is_string($compressdata)) {
$fp = fopen($outputfile, "w");
fwrite($fp, $compressdata);
fclose($fp);
return 1;
} else {
$errorstring = "Try to zip with format ".$mode." with no handler for this format";
dol_syslog($errorstring, LOG_ERR);
global $errormsg;
$errormsg = $errorstring;
return -2;
}
} catch (Exception $e) {
global $langs, $errormsg;
$langs->load("errors");
$errormsg = $langs->trans("ErrorFailedToWriteInDir");
$errorstring = "Failed to open file ".$outputfile;
dol_syslog($errorstring, LOG_ERR);
return -1;
}
}
/**
* Uncompress a file
*
* @param string $inputfile File (.zip, .gz, .bz2, .zst, ...) to uncompress
* @param string $outputdir Target dir name
* @return array{error?:string} array('error'=>'Error code') or array() if no error
* @see dol_compress_file(), dol_compress_dir()
*/
function dol_uncompress($inputfile, $outputdir)
{
global $langs, $db;
$fileinfo = pathinfo($inputfile);
$fileinfo["extension"] = strtolower($fileinfo["extension"]);
if ($fileinfo["extension"] == "zip") {
if (defined('ODTPHP_PATHTOPCLZIP') && !getDolGlobalString('MAIN_USE_ZIPARCHIVE_FOR_ZIP_UNCOMPRESS')) {
dol_syslog("Constant ODTPHP_PATHTOPCLZIP for pclzip library is set to ".ODTPHP_PATHTOPCLZIP.", so we use Pclzip to unzip into ".$outputdir);
include_once ODTPHP_PATHTOPCLZIP.'pclzip.lib.php';
$archive = new PclZip($inputfile);
// We create output dir manually, so it uses the correct permission (When created by the archive->extract, dir is rwx for everybody).
dol_mkdir(dol_sanitizePathName($outputdir));
try {
// Extract into outputdir, but only files that match the regex '/^((?!\.\.).)*$/' that means "does not include .."
$result = $archive->extract(PCLZIP_OPT_PATH, $outputdir, PCLZIP_OPT_BY_PREG, '/^((?!\.\.).)*$/');
} catch (Exception $e) {
return array('error' => $e->getMessage());
}
if (!is_array($result) && $result <= 0) {
return array('error' => $archive->errorInfo(true));
} else {
$ok = 1;
$errmsg = '';
// Loop on each file to check result for unzipping file
foreach ($result as $key => $val) {
if ($val['status'] == 'path_creation_fail') {
$langs->load("errors");
$ok = 0;
$errmsg = $langs->trans("ErrorFailToCreateDir", $val['filename']);
break;
}
if ($val['status'] == 'write_protected') {
$langs->load("errors");
$ok = 0;
$errmsg = $langs->trans("ErrorFailToCreateFile", $val['filename']);
break;
}
}
if ($ok) {
return array();
} else {
return array('error' => $errmsg);
}
}
}
if (class_exists('ZipArchive')) { // Must install php-zip to have it
dol_syslog("Class ZipArchive is set so we unzip using ZipArchive to unzip into ".$outputdir);
$zip = new ZipArchive();
$res = $zip->open($inputfile);
if ($res === true) {
//$zip->extractTo($outputdir.'/');
// We must extract one file at time so we can check that file name does not contain '..' to avoid transversal path of zip built for example using
// python3 path_traversal_archiver.py <Created_file_name> test.zip -l 10 -p tmp/
// with -l is the range of dot to go back in path.
// and path_traversal_archiver.py found at https://github.com/Alamot/code-snippets/blob/master/path_traversal/path_traversal_archiver.py
for ($i = 0; $i < $zip->numFiles; $i++) {
if (preg_match('/\.\./', $zip->getNameIndex($i))) {
dol_syslog("Warning: Try to unzip a file with a transversal path ".$zip->getNameIndex($i), LOG_WARNING);
continue; // Discard the file
}
$zip->extractTo($outputdir.'/', array($zip->getNameIndex($i)));
}
$zip->close();
return array();
} else {
return array('error' => 'ErrUnzipFails');
}
}
return array('error' => 'ErrNoZipEngine');
} elseif (in_array($fileinfo["extension"], array('gz', 'bz2', 'zst'))) {
include_once DOL_DOCUMENT_ROOT."/core/class/utils.class.php";
$utils = new Utils($db);
dol_mkdir(dol_sanitizePathName($outputdir));
$outputfilename = escapeshellcmd(dol_sanitizePathName($outputdir).'/'.dol_sanitizeFileName($fileinfo["filename"]));
dol_delete_file($outputfilename.'.tmp');
dol_delete_file($outputfilename.'.err');
$extension = strtolower(pathinfo($fileinfo["filename"], PATHINFO_EXTENSION));
if ($extension == "tar") {
$cmd = 'tar -C '.escapeshellcmd(dol_sanitizePathName($outputdir)).' -xvf '.escapeshellcmd(dol_sanitizePathName($fileinfo["dirname"]).'/'.dol_sanitizeFileName($fileinfo["basename"]));
$resarray = $utils->executeCLI($cmd, $outputfilename.'.tmp', 0, $outputfilename.'.err', 0);
if ($resarray["result"] != 0) {
$resarray["error"] .= file_get_contents($outputfilename.'.err');
}
} else {
$program = "";
if ($fileinfo["extension"] == "gz") {
$program = 'gzip';
} elseif ($fileinfo["extension"] == "bz2") {
$program = 'bzip2';
} elseif ($fileinfo["extension"] == "zst") {
$program = 'zstd';
} else {
return array('error' => 'ErrorBadFileExtension');
}
$cmd = $program.' -dc '.escapeshellcmd(dol_sanitizePathName($fileinfo["dirname"]).'/'.dol_sanitizeFileName($fileinfo["basename"]));
$cmd .= ' > '.$outputfilename;
$resarray = $utils->executeCLI($cmd, $outputfilename.'.tmp', 0, null, 1, $outputfilename.'.err');
if ($resarray["result"] != 0) {
$errfilecontent = @file_get_contents($outputfilename.'.err');
if ($errfilecontent) {
$resarray["error"] .= " - ".$errfilecontent;
}
}
}
return $resarray["result"] != 0 ? array('error' => $resarray["error"]) : array();
}
return array('error' => 'ErrorBadFileExtension');
}
/**
* Compress a directory and subdirectories into a package file.
*
* @param string $inputdir Source dir name
* @param string $outputfile Target file name (output directory must exists and be writable)
* @param string $mode 'zip'
* @param string $excludefiles A regex pattern to exclude files. For example: '/\.log$|\/temp\//'
* @param string $rootdirinzip Add a root dir level in zip file
* @param string $newmask Mask for new file ('0' by default means getDolGlobalString('MAIN_UMASK')). Example: '0666'
* @return int Return integer <0 if KO, >0 if OK
* @see dol_uncompress(), dol_compress_file()
*/
function dol_compress_dir($inputdir, $outputfile, $mode = "zip", $excludefiles = '', $rootdirinzip = '', $newmask = '0')
{
$foundhandler = 0;
dol_syslog("Try to zip dir ".$inputdir." into ".$outputfile." mode=".$mode);
if (!dol_is_dir(dirname($outputfile)) || !is_writable(dirname($outputfile))) {
global $langs, $errormsg;
$langs->load("errors");
$errormsg = $langs->trans("ErrorFailedToWriteInDir", $outputfile);
return -3;
}
try {
if ($mode == 'gz') {
$foundhandler = 0;
} elseif ($mode == 'bz') {
$foundhandler = 0;
} elseif ($mode == 'zip') {
/*if (defined('ODTPHP_PATHTOPCLZIP'))
{
$foundhandler=0; // TODO implement this
include_once ODTPHP_PATHTOPCLZIP.'/pclzip.lib.php';
$archive = new PclZip($outputfile);
$archive->add($inputfile, PCLZIP_OPT_REMOVE_PATH, dirname($inputfile));
//$archive->add($inputfile);
return 1;
}
else*/
//if (class_exists('ZipArchive') && !empty($conf->global->MAIN_USE_ZIPARCHIVE_FOR_ZIP_COMPRESS))
if (class_exists('ZipArchive')) {
$foundhandler = 1;
// Initialize archive object
$zip = new ZipArchive();
$result = $zip->open($outputfile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
if ($result !== true) {
global $langs, $errormsg;
$langs->load("errors");
$errormsg = $langs->trans("ErrorFailedToBuildArchive", $outputfile);
return -4;
}
// Create recursive directory iterator
// This does not return symbolic links
/** @var SplFileInfo[] $files */
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($inputdir, FilesystemIterator::UNIX_PATHS),
RecursiveIteratorIterator::LEAVES_ONLY
);
'@phan-var-force SplFileInfo[] $files';
//var_dump($inputdir);
foreach ($files as $name => $file) {
// Skip directories (they would be added automatically)
if (!$file->isDir()) {
// Get real and relative path for current file
$filePath = $file->getPath(); // the full path with filename using the $inputdir root.
$fileName = $file->getFilename();
$fileFullRealPath = $file->getRealPath(); // the full path with name and transformed to use real path directory.
//$relativePath = ($rootdirinzip ? $rootdirinzip.'/' : '').substr($fileFullRealPath, strlen($inputdir) + 1);
$relativePath = ($rootdirinzip ? $rootdirinzip.'/' : '').substr(($filePath ? $filePath.'/' : '').$fileName, strlen($inputdir) + 1);
//var_dump($filePath);var_dump($fileFullRealPath);var_dump($relativePath);
if (empty($excludefiles) || !preg_match($excludefiles, $fileFullRealPath)) {
// Add current file to archive
$zip->addFile($fileFullRealPath, $relativePath);
}
}
}
// Zip archive will be created only after closing object
$zip->close();
if (empty($newmask) && getDolGlobalString('MAIN_UMASK')) {
$newmask = getDolGlobalString('MAIN_UMASK');
}
if (empty($newmask)) { // This should no happen
dol_syslog("Warning: dol_compress_dir called with empty value for newmask and no default value defined", LOG_WARNING);
$newmask = '0664';
}
dolChmod($outputfile, $newmask);
return 1;
}
}
if (!$foundhandler) {
dol_syslog("Try to zip with format ".$mode." with no handler for this format", LOG_ERR);
return -2;
} else {
return 0;
}
} catch (Exception $e) {
global $langs, $errormsg;
$langs->load("errors");
dol_syslog("Failed to open file ".$outputfile, LOG_ERR);
dol_syslog($e->getMessage(), LOG_ERR);
$errormsg = $langs->trans("ErrorFailedToBuildArchive", $outputfile).' - '.$e->getMessage();
return -1;
}
}
/**
* Return file(s) into a directory (by default most recent)
*
* @param string $dir Directory to scan
* @param string $regexfilter Regex filter to restrict list. This regex value must be escaped for '/', since this char is used for preg_match function
* @param string[] $excludefilter Array of Regex for exclude filter (example: array('(\.meta|_preview.*\.png)$','^\.')). This regex value must be escaped for '/', since this char is used for preg_match function
* @param int<0,1> $nohook Disable all hooks
* @param int<0,3> $mode 0=Return array minimum keys loaded (faster), 1=Force all keys like date and size to be loaded (slower), 2=Force load of date only, 3=Force load of size only
* @return null|array{name:string,path:string,level1name:string,relativename:string,fullname:string,date:string,size:int,perm:int,type:string} null if none or Array with properties (full path, date, ...) of the most recent file
*/
function dol_most_recent_file($dir, $regexfilter = '', $excludefilter = array('(\.meta|_preview.*\.png)$', '^\.'), $nohook = 0, $mode = 0)
{
$tmparray = dol_dir_list($dir, 'files', 0, $regexfilter, $excludefilter, 'date', SORT_DESC, $mode, $nohook);
return isset($tmparray[0]) ? $tmparray[0] : null;
}
/**
* Security check when accessing to a document (used by document.php, viewimage.php and webservices to get documents).
* TODO Replace code that set $accessallowed by a call to restrictedArea()
*
* @param string $modulepart Module of document ('module', 'module_user_temp', 'module_user' or 'module_temp'). Example: 'medias', 'invoice', 'logs', 'tax-vat', ...
* @param string $original_file Relative path with filename, relative to modulepart.
* @param int $entity Restrict objects stored into entity (0=no restriction). Note that permissions tested are the one the user is logged in, so permissions for $conf->entity that may be different than $entity.
* @param User|null $fuser User object (forced)
* @param string $refname Ref of object to check permission for external users (autodetect if not provided by taking the dirname of $original_file) or for hierarchy
* @param string $mode Check permission for 'read' or 'write'
* @return mixed Array with access information : 'accessallowed' & 'sqlprotectagainstexternals' & 'original_file' (as a full path name)
* @see restrictedArea()
*/
function dol_check_secure_access_document($modulepart, $original_file, $entity, $fuser = null, $refname = '', $mode = 'read')
{
global $conf, $db, $user, $hookmanager;
global $dolibarr_main_data_root, $dolibarr_main_document_root_alt;
global $object;
if (!is_object($fuser)) {
$fuser = $user;
}
if (empty($modulepart)) {
return 'ErrorBadParameter';
}
if (empty($entity)) {
if (!isModEnabled('multicompany')) {
$entity = 1;
} else {
$entity = 0;
}
} else {
// TODO Test that the user in session of conf->entity can see objects of the target $entity
// ...
}
// Fix modulepart for backward compatibility
if ($modulepart == 'facture') {
$modulepart = 'invoice';
} elseif ($modulepart == 'users') {
$modulepart = 'user';
} elseif ($modulepart == 'tva') {
$modulepart = 'tax-vat';
} elseif ($modulepart == 'expedition' && strpos($original_file, 'receipt/') === 0) {
// Fix modulepart delivery
$modulepart = 'delivery';
} elseif ($modulepart == 'propale') {
$modulepart = 'propal';
}
//print 'dol_check_secure_access_document modulepart='.$modulepart.' original_file='.$original_file.' entity='.$entity;
dol_syslog('dol_check_secure_access_document modulepart='.$modulepart.' original_file='.$original_file.' entity='.$entity);
// We define $accessallowed and $sqlprotectagainstexternals
$accessallowed = 0;
$sqlprotectagainstexternals = '';
$ret = array();
// Find the subdirectory name as the reference. For example original_file='10/myfile.pdf' -> refname='10'
if (empty($refname)) {
$refname = basename(dirname($original_file)."/");
if ($refname == 'thumbs' || $refname == 'temp') {
// If we get the thumbs directory, we must go one step higher. For example original_file='10/thumbs/myfile_small.jpg' -> refname='10'
$refname = basename(dirname(dirname($original_file))."/");
}
}
// Define possible keys to use for permission check
$lire = 'lire';
$read = 'read';
$download = 'download';
if ($mode == 'write') {
$lire = 'creer';
$read = 'write';
$download = 'upload';
}
// Wrapping for miscellaneous medias files
if ($modulepart == 'common') {
// Wrapping for some images
$accessallowed = 1;
$original_file = DOL_DOCUMENT_ROOT.'/public/theme/common/'.$original_file;
} elseif ($modulepart == 'medias' && !empty($dolibarr_main_data_root)) {
/* the medias directory is by default a public directory accessible online for everybody, so test on permission per entity is not done, it has no sense */
if (empty($entity)) {
$entity = 1;
}
$accessallowed = 0;
if ($mode == 'write') {
if ($fuser->hasRight('website', 'write')) {
$accessallowed = 1;
}
} else {
$accessallowed = 1; // As dir is public, we allow read access to all files in medias directory
}
$original_file = (empty($conf->medias->multidir_output[$entity]) ? (empty($conf->medias->dir_output) ? DOL_DATA_ROOT.'/medias' : $conf->medias->dir_output) : $conf->medias->multidir_output[$entity]).'/'.$original_file;
} elseif ($modulepart == 'logs' && !empty($dolibarr_main_data_root)) {
// Wrapping for *.log files, like when used with url http://.../document.php?modulepart=logs&file=dolibarr.log
$accessallowed = ($user->admin && basename($original_file) == $original_file && preg_match('/^dolibarr.*\.(log|json)$/', basename($original_file)));
$original_file = $dolibarr_main_data_root.'/'.$original_file;
} elseif ($modulepart == 'doctemplates' && !empty($dolibarr_main_data_root)) {
$accessallowed = $user->admin;
$relative_file = $original_file;
$ent = ($entity > 0 ? $entity : $conf->entity);
$path_with_entity = $dolibarr_main_data_root . '/' . $ent . '/doctemplates/' . $relative_file;
if ($ent > 1 && file_exists(dol_osencode($path_with_entity))) {
$original_file = $path_with_entity;
} else {
$original_file = $dolibarr_main_data_root . '/doctemplates/' . $relative_file;
}
} elseif ($modulepart == 'doctemplateswebsite' && !empty($dolibarr_main_data_root)) {
// Wrapping for doctemplates of websites
$accessallowed = ($fuser->hasRight('website', 'write') && preg_match('/\.jpg$/i', basename($original_file)));
$original_file = $dolibarr_main_data_root.'/doctemplates/websites/'.$original_file;
} elseif ($modulepart == 'packages' && !empty($dolibarr_main_data_root)) { // To download zip of modules
// Wrapping for *.zip package files, like when used with url http://.../document.php?modulepart=packages&file=module_myfile.zip
// Dir for custom dirs
$tmp = explode(',', $dolibarr_main_document_root_alt);
$dirins = $tmp[0];
$accessallowed = ($user->admin && preg_match('/^module_.*\.zip$/', basename($original_file)));
$original_file = $dirins.'/'.$original_file;
} elseif ($modulepart == 'mycompany' && !empty($conf->mycompany->dir_output)) {
// Wrapping for some images
$accessallowed = 1;
$original_file = $conf->mycompany->dir_output.'/'.$original_file;
} elseif ($modulepart == 'userphoto' && !empty($conf->user->dir_output)) {
// Wrapping for users photos (user photos are allowed to any connected users)
$accessallowed = 0;
if (preg_match('/^\d+\/photos\//', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->user->dir_output.'/'.$original_file;
} elseif ($modulepart == 'userphotopublic' && !empty($conf->user->dir_output)) {
// Wrapping for users photos that were set to public (for virtual credit card) by their owner (public user photos can be read
// with the public link and securekey)
$accessok = false;
$reg = array();
if (preg_match('/^(\d+)\/photos\//', $original_file, $reg)) {
if ((int) $reg[1]) {
$tmpobject = new User($db);
$tmpobject->fetch((int) $reg[1], '', '', 1);
if (getDolUserInt('USER_ENABLE_PUBLIC', 0, $tmpobject)) {
$securekey = GETPOST('securekey', 'alpha', 1);
// Security check
global $dolibarr_main_cookie_cryptkey, $dolibarr_main_instance_unique_id;
$valuetouse = $dolibarr_main_instance_unique_id ? $dolibarr_main_instance_unique_id : $dolibarr_main_cookie_cryptkey; // Use $dolibarr_main_instance_unique_id first then $dolibarr_main_cookie_cryptkey
$encodedsecurekey = dol_hash($valuetouse.'uservirtualcard'.$tmpobject->id.'-'.$tmpobject->login, 'md5');
if ($encodedsecurekey == $securekey) {
$accessok = true;
}
}
}
}
if ($accessok) {
$accessallowed = 1;
}
$original_file = $conf->user->dir_output.'/'.$original_file;
} elseif (($modulepart == 'companylogo') && !empty($conf->mycompany->dir_output)) {
// Wrapping for company logos (company logos are allowed to anyboby, they are public)
$accessallowed = 1;
$original_file = $conf->mycompany->dir_output.'/logos/'.$original_file;
} elseif ($modulepart == 'memberphoto' && !empty($conf->member->dir_output)) {
// Wrapping for members photos
$accessallowed = 0;
// Simple chosen for automatic generation of member codes
if (preg_match('/^\d+\/photos\//', $original_file)) {
$accessallowed = 1;
}
// Advanced chosen for automatic generation of member codes
if (preg_match('/^MEM\d\d\d\d-\d\d\d\d\/photos\//', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->member->dir_output.'/'.$original_file;
} elseif ($modulepart == 'apercufacture' && !empty($conf->invoice->multidir_output[$entity])) {
// Wrapping for invoices (user need permission to read invoices)
if ($fuser->hasRight('facture', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->invoice->multidir_output[$entity].'/'.$original_file;
} elseif ($modulepart == 'apercupropal' && !empty($conf->propal->multidir_output[$entity])) {
// Wrapping for preview of proposals
if ($fuser->hasRight('propal', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->propal->multidir_output[$entity].'/'.$original_file;
} elseif ($modulepart == 'apercucommande' && !empty($conf->order->multidir_output[$entity])) {
// Wrapping for preview of orders
if ($fuser->hasRight('commande', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->order->multidir_output[$entity].'/'.$original_file;
} elseif (($modulepart == 'apercufichinter' || $modulepart == 'apercuficheinter') && !empty($conf->ficheinter->multidir_output[$entity])) {
// Wrapping for preview of intervention
if ($fuser->hasRight('ficheinter', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->ficheinter->multidir_output[$entity].'/'.$original_file;
} elseif (($modulepart == 'apercucontract') && !empty($conf->contract->multidir_output[$entity])) {
// Wrapping for preview of contracts
if ($fuser->hasRight('contrat', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->contract->multidir_output[$entity].'/'.$original_file;
} elseif (($modulepart == 'apercusupplier_proposal') && !empty($conf->supplier_proposal->dir_output)) {
// Wrapping for preview of vendor proposals
if ($fuser->hasRight('supplier_proposal', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->supplier_proposal->dir_output.'/'.$original_file;
} elseif (($modulepart == 'apercusupplier_order') && !empty($conf->fournisseur->commande->dir_output)) {
// Wrapping for preview of purchase orders
if ($fuser->hasRight('fournisseur', 'commande', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->fournisseur->commande->dir_output.'/'.$original_file;
} elseif (($modulepart == 'apercusupplier_invoice') && !empty($conf->fournisseur->facture->dir_output)) {
// Wrapping for preview of supplier invoices
if ($fuser->hasRight('fournisseur', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->fournisseur->facture->dir_output.'/'.$original_file;
} elseif (($modulepart == 'holiday') && !empty($conf->holiday->dir_output)) {
if ($fuser->hasRight('holiday', $read) || $fuser->hasRight('holiday', 'readall') || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
// If we known $id of holiday, call checkUserAccessToObject to check permission on properties and hierarchy of leave request
if ($refname && !$fuser->hasRight('holiday', 'readall') && !preg_match('/^specimen/i', $original_file)) {
include_once DOL_DOCUMENT_ROOT.'/holiday/class/holiday.class.php';
$tmpholiday = new Holiday($db);
$tmpholiday->fetch(0, $refname);
$accessallowed = checkUserAccessToObject($user, array('holiday'), $tmpholiday, 'holiday', '', '', 'rowid', '');
}
}
$original_file = $conf->holiday->dir_output.'/'.$original_file;
} elseif (($modulepart == 'expensereport') && !empty($conf->expensereport->dir_output)) {
if ($fuser->hasRight('expensereport', $lire) || $fuser->hasRight('expensereport', 'readall') || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
// If we known $id of expensereport, call checkUserAccessToObject to check permission on properties and hierarchy of expense report
if ($refname && !$fuser->hasRight('expensereport', 'readall') && !preg_match('/^specimen/i', $original_file)) {
include_once DOL_DOCUMENT_ROOT.'/expensereport/class/expensereport.class.php';
$tmpexpensereport = new ExpenseReport($db);
$tmpexpensereport->fetch(0, $refname);
$accessallowed = checkUserAccessToObject($user, array('expensereport'), $tmpexpensereport, 'expensereport', '', '', 'rowid', '');
}
}
$original_file = $conf->expensereport->dir_output.'/'.$original_file;
} elseif (($modulepart == 'apercuexpensereport') && !empty($conf->expensereport->dir_output)) {
// Wrapping for preview of expense report
if ($fuser->hasRight('expensereport', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->expensereport->dir_output.'/'.$original_file;
} elseif ($modulepart == 'propalstats' && !empty($conf->propal->multidir_temp[$entity])) {
// Wrapping for statistics images of proposal
if ($fuser->hasRight('propal', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->propal->multidir_temp[$entity].'/'.$original_file;
} elseif ($modulepart == 'orderstats' && !empty($conf->order->dir_temp)) {
// Wrapping for statistics images of orders
if ($fuser->hasRight('commande', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->order->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'orderstatssupplier' && !empty($conf->fournisseur->dir_output)) {
if ($fuser->hasRight('fournisseur', 'commande', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->fournisseur->commande->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'billstats' && !empty($conf->invoice->dir_temp)) {
// Wrapping for statistics images of purchase orders
if ($fuser->hasRight('facture', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->invoice->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'billstatssupplier' && !empty($conf->fournisseur->dir_output)) {
if ($fuser->hasRight('fournisseur', 'facture', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->fournisseur->facture->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'expeditionstats' && !empty($conf->expedition->dir_temp)) {
// Wrapping for statistics images of shipments
if ($fuser->hasRight('expedition', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->expedition->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'tripsexpensesstats' && !empty($conf->deplacement->dir_temp)) {
// Wrapping pour les images des stats expeditions
if ($fuser->hasRight('deplacement', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->deplacement->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'memberstats' && !empty($conf->member->dir_temp)) {
// Wrapping for statistics images of memberships
if ($fuser->hasRight('adherent', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->member->dir_temp.'/'.$original_file;
} elseif (preg_match('/^productstats_/i', $modulepart) && !empty($conf->product->dir_temp)) {
// Wrapping for statistics images of products
if ($fuser->hasRight('produit', $lire) || $fuser->hasRight('service', $lire)) {
$accessallowed = 1;
}
$original_file = (!empty($conf->product->multidir_temp[$entity]) ? $conf->product->multidir_temp[$entity] : $conf->service->multidir_temp[$entity]).'/'.$original_file;
} elseif (in_array($modulepart, array('tax', 'tax-vat', 'tva')) && !empty($conf->tax->dir_output)) {
// Wrapping for taxes
if ($fuser->hasRight('tax', 'charges', $lire)) {
$accessallowed = 1;
}
$modulepartsuffix = str_replace('tax-', '', $modulepart);
$original_file = $conf->tax->dir_output.'/'.($modulepartsuffix != 'tax' ? $modulepartsuffix.'/' : '').$original_file;
} elseif (($modulepart == 'actions' || $modulepart == 'actioncomm') && !empty($conf->agenda->dir_output)) {
// Wrapping for events
if ($fuser->hasRight('agenda', 'myactions', $read)) {
$accessallowed = 1;
// If we known $id of project, call checkUserAccessToObject to check permission on the given agenda event on properties and assigned users
if ($refname && !preg_match('/^specimen/i', $original_file)) {
include_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php';
$tmpobject = new ActionComm($db);
$tmpobject->fetch((int) $refname);
$accessallowed = checkUserAccessToObject($user, array('agenda'), $tmpobject->id, 'actioncomm&societe', 'myactions|allactions', 'fk_soc', 'id', '');
if ($user->socid && $tmpobject->socid) {
$accessallowed = checkUserAccessToObject($user, array('societe'), $tmpobject->socid);
}
}
}
$original_file = $conf->agenda->dir_output.'/'.$original_file;
} elseif ($modulepart == 'category' && !empty($conf->categorie->multidir_output[$entity])) {
// Wrapping for categories (categories are allowed if user has permission to read categories or to work on TakePos)
if (empty($entity) || empty($conf->categorie->multidir_output[$entity])) {
return array('accessallowed' => 0, 'error' => 'Value entity must be provided');
}
if ($fuser->hasRight("categorie", $lire) || $fuser->hasRight("takepos", "run")) {
$accessallowed = 1;
}
$original_file = $conf->categorie->multidir_output[$entity].'/'.$original_file;
} elseif ($modulepart == 'prelevement' && !empty($conf->prelevement->dir_output)) {
// Wrapping pour les prelevements
if ($fuser->hasRight('prelevement', 'bons', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->prelevement->dir_output.'/'.$original_file;
} elseif ($modulepart == 'graph_stock' && !empty($conf->stock->dir_temp)) {
// Wrapping pour les graph energie
$accessallowed = 1;
$original_file = $conf->stock->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'graph_fourn' && !empty($conf->fournisseur->dir_temp)) {
// Wrapping pour les graph fournisseurs
$accessallowed = 1;
$original_file = $conf->fournisseur->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'graph_product' && !empty($conf->product->dir_temp)) {
// Wrapping pour les graph des produits
$accessallowed = 1;
$original_file = $conf->product->multidir_temp[$entity].'/'.$original_file;
} elseif ($modulepart == 'barcode') {
// Wrapping pour les code barre
$accessallowed = 1;
// If viewimage is called for barcode, we try to output an image on the fly, with no build of file on disk.
//$original_file=$conf->barcode->dir_temp.'/'.$original_file;
$original_file = '';
} elseif ($modulepart == 'iconmailing' && !empty($conf->mailing->dir_temp)) {
// Wrapping for icon of background of mailings
$accessallowed = 1;
$original_file = $conf->mailing->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'scanner_user_temp' && !empty($conf->scanner->dir_temp)) {
// Wrapping pour le scanner
$accessallowed = 1;
$original_file = $conf->scanner->dir_temp.'/'.$fuser->id.'/'.$original_file;
} elseif ($modulepart == 'fckeditor' && !empty($conf->fckeditor->dir_output)) {
// Wrapping pour les images fckeditor
$accessallowed = 1;
$original_file = $conf->fckeditor->dir_output.'/'.$original_file;
} elseif ($modulepart == 'user' && !empty($conf->user->dir_output)) {
// Wrapping for users
$canreaduser = (!empty($fuser->admin) || $fuser->hasRight('user', 'user', $lire));
if ($fuser->id == (int) $refname) {
$canreaduser = 1;
} // A user can always read its own card
if ($canreaduser || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->user->dir_output.'/'.$original_file;
} elseif (($modulepart == 'company' || $modulepart == 'societe' || $modulepart == 'thirdparty') && !empty($conf->societe->multidir_output[$entity])) {
// Wrapping for third parties
if (empty($entity) || empty($conf->societe->multidir_output[$entity])) {
return array('accessallowed' => 0, 'error' => 'Value entity must be provided');
}
if ($fuser->hasRight('societe', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->societe->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT rowid as fk_soc FROM ".MAIN_DB_PREFIX."societe WHERE rowid = ".((int) $refname)." AND entity IN (".getEntity('societe').")";
} elseif (($modulepart == 'contact' || $modulepart == 'socpeople') && !empty($conf->societe->multidir_output[$entity])) {
// Wrapping for contact
if (empty($entity) || empty($conf->societe->multidir_output[$entity])) {
return array('accessallowed' => 0, 'error' => 'Value entity must be provided');
}
if ($fuser->hasRight('societe', 'contact', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->societe->multidir_output[$entity].'/contact/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc FROM ".MAIN_DB_PREFIX."socpeople WHERE rowid = ".((int) $refname)." AND entity IN (".getEntity('contact').")";
} elseif (($modulepart == 'facture' || $modulepart == 'invoice') && !empty($conf->invoice->multidir_output[$entity])) {
// Wrapping for invoices
if ($fuser->hasRight('facture', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->invoice->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."facture WHERE ref='".$db->escape($refname)."' AND entity IN (".getEntity('invoice').")";
} elseif ($modulepart == 'massfilesarea_proposals' && !empty($conf->propal->multidir_output[$entity])) {
// Wrapping for mass actions
if ($fuser->hasRight('propal', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->propal->multidir_output[$entity].'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_orders') {
if ($fuser->hasRight('commande', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->order->multidir_output[$entity].'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_sendings') {
if ($fuser->hasRight('expedition', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->expedition->dir_output.'/sending/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_receipts') {
if ($fuser->hasRight('reception', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->reception->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_invoices') {
if ($fuser->hasRight('facture', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->invoice->multidir_output[$entity].'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_expensereport') {
if ($fuser->hasRight('facture', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->expensereport->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_interventions') {
if ($fuser->hasRight('ficheinter', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->ficheinter->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_supplier_proposal' && !empty($conf->supplier_proposal->dir_output)) {
if ($fuser->hasRight('supplier_proposal', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->supplier_proposal->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_supplier_order') {
if ($fuser->hasRight('fournisseur', 'commande', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->fournisseur->commande->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_supplier_invoice') {
if ($fuser->hasRight('fournisseur', 'facture', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->fournisseur->facture->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_contract' && !empty($conf->contract->dir_output)) {
if ($fuser->hasRight('contrat', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->contract->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif ($modulepart == 'massfilesarea_stock' && !empty($conf->stock->dir_output)) {
if ($fuser->hasRight('stock', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->stock->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} elseif (($modulepart == 'fichinter' || $modulepart == 'ficheinter') && !empty($conf->ficheinter->multidir_output[$entity])) {
// Wrapping for interventions
if ($fuser->hasRight('ficheinter', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->ficheinter->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."fichinter WHERE ref='".$db->escape($refname)."' AND entity=".((int) $conf->entity);
} elseif ($modulepart == 'deplacement' && !empty($conf->deplacement->dir_output)) {
// Wrapping pour les deplacements et notes de frais
if ($fuser->hasRight('deplacement', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->deplacement->dir_output.'/'.$original_file;
//$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."fichinter WHERE ref='".$db->escape($refname)."' AND entity=".((int) $conf->entity);
} elseif (($modulepart == 'propal' || $modulepart == 'propale') && isset($conf->propal->multidir_output[$entity])) {
// Wrapping pour les propales
if ($fuser->hasRight('propal', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->propal->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."propal WHERE ref='".$db->escape($refname)."' AND entity IN (".getEntity('propal').")";
} elseif (($modulepart == 'commande' || $modulepart == 'order') && !empty($conf->order->multidir_output[$entity])) {
// Wrapping pour les commandes
if ($fuser->hasRight('commande', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->order->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."commande WHERE ref='".$db->escape($refname)."' AND entity IN (".getEntity('order').")";
} elseif ($modulepart == 'project' && !empty($conf->project->multidir_output[$entity])) {
// Wrapping pour les projects
if ($fuser->hasRight('projet', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
// If we known $id of project, call checkUserAccessToObject to check permission on properties and contact of project
if ($refname && !preg_match('/^specimen/i', $original_file)) {
include_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
$tmpproject = new Project($db);
$tmpproject->fetch(0, $refname);
$accessallowed = checkUserAccessToObject($user, array('projet'), $tmpproject->id, 'projet&project', '', '', 'rowid', '');
}
}
$original_file = $conf->project->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."projet WHERE ref='".$db->escape($refname)."' AND entity IN (".getEntity('project').")";
} elseif ($modulepart == 'project_task' && !empty($conf->project->multidir_output[$entity])) {
if ($fuser->hasRight('projet', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
// If we known $id of project, call checkUserAccessToObject to check permission on properties and contact of project
if ($refname && !preg_match('/^specimen/i', $original_file)) {
include_once DOL_DOCUMENT_ROOT.'/projet/class/task.class.php';
$tmptask = new Task($db);
$tmptask->fetch(0, $refname);
$accessallowed = checkUserAccessToObject($user, array('projet_task'), $tmptask->id, 'projet_task&project', '', '', 'rowid', '');
}
}
$original_file = $conf->project->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."projet WHERE ref='".$db->escape($refname)."' AND entity IN (".getEntity('project').")";
} elseif (($modulepart == 'commande_fournisseur' || $modulepart == 'order_supplier') && !empty($conf->fournisseur->commande->dir_output)) {
// Wrapping for purchase orders
if ($fuser->hasRight('fournisseur', 'commande', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->fournisseur->commande->dir_output.'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."commande_fournisseur WHERE ref='".$db->escape($refname)."' AND entity=".((int) $conf->entity);
} elseif (($modulepart == 'facture_fournisseur' || $modulepart == 'invoice_supplier') && !empty($conf->fournisseur->facture->dir_output)) {
// Wrapping for supplier invoices
if ($fuser->hasRight('fournisseur', 'facture', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->fournisseur->facture->dir_output.'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."facture_fourn WHERE ref='".$db->escape($refname)."' AND entity=".((int) $conf->entity);
} elseif ($modulepart == 'supplier_payment') {
// Wrapping for supplier payments
if ($fuser->hasRight('fournisseur', 'facture', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = preg_replace("/payment\//", "", $original_file); // Because the $conf->fournisseur->payment->dir_output already contains the "payment/"
$original_file = $conf->fournisseur->payment->dir_output.'/'.$original_file;
$sqlprotectagainstexternals = "SELECT f.fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."paiementfourn as p";
$sqlprotectagainstexternals .= " INNER JOIN ".MAIN_DB_PREFIX."paiementfourn_facturefourn as pf ON pf.fk_paiementfourn = p.rowid";
$sqlprotectagainstexternals .= " INNER JOIN ".MAIN_DB_PREFIX."facture_fourn as f ON pf.fk_facturefourn = p.rowid";
$sqlprotectagainstexternals .= " WHERE p.ref = '".$db->escape($refname)."' AND p.entity=".((int) $conf->entity);
} elseif ($modulepart == 'payment') {
// Wrapping for report of payments
if ($fuser->hasRight('facture', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->compta->payment->dir_output.'/'.$original_file;
} elseif ($modulepart == 'facture_paiement' && !empty($conf->invoice->dir_output)) {
// Wrapping for report of payments
if ($fuser->hasRight('facture', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
if ($fuser->socid > 0) {
$original_file = $conf->invoice->dir_output.'/payments/private/'.$fuser->id.'/'.$original_file;
} else {
$original_file = $conf->invoice->dir_output.'/payments/'.$original_file;
}
/* $sqlprotectagainstexternals = "SELECT f.fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."paiement as p";
$sqlprotectagainstexternals .= " INNER JOIN ".MAIN_DB_PREFIX."paiement_facture as pf ON pf.fk_paiement = p.rowid";
$sqlprotectagainstexternals .= " INNER JOIN ".MAIN_DB_PREFIX."facture as f ON pf.fk_facture = p.rowid";
$sqlprotectagainstexternals .= " WHERE p.ref = '".$db->escape($refname)."' AND p.entity=".((int) $conf->entity);
var_dump($sqlprotectagainstexternals);exit;*/
} elseif ($modulepart == 'export_compta' && !empty($conf->accounting->dir_output)) {
// Wrapping for accounting exports
if ($fuser->hasRight('accounting', 'bind', 'write') || $fuser->hasRight('accounting', 'mouvements', 'export') || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->accounting->dir_output.'/'.$original_file;
} elseif (($modulepart == 'expedition' || $modulepart == 'shipment' || $modulepart == 'shipping') && !empty($conf->expedition->dir_output)) {
// Wrapping pour les expedition
if ($fuser->hasRight('expedition', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->expedition->dir_output."/".(strpos($original_file, 'sending/') === 0 ? '' : 'sending/').$original_file;
//$original_file = $conf->expedition->dir_output."/".$original_file;
} elseif (($modulepart == 'livraison' || $modulepart == 'delivery') && !empty($conf->expedition->dir_output)) {
// Delivery Note Wrapping
if ($fuser->hasRight('expedition', 'delivery', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->expedition->dir_output."/".(strpos($original_file, 'receipt/') === 0 ? '' : 'receipt/').$original_file;
} elseif ($modulepart == 'actionsreport' && !empty($conf->agenda->dir_temp)) {
// Wrapping for actions
if ($fuser->hasRight('agenda', 'allactions', $read) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->agenda->dir_temp."/".$original_file;
} elseif ($modulepart == 'product' || $modulepart == 'produit' || $modulepart == 'service' || $modulepart == 'produit|service') {
// Wrapping for products and services
if (empty($entity) || (empty($conf->product->multidir_output[$entity]) && empty($conf->service->multidir_output[$entity]))) {
return array('accessallowed' => 0, 'error' => 'Value entity must be provided');
}
if (($fuser->hasRight('produit', $lire) || $fuser->hasRight('service', $lire)) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
if (isModEnabled("product")) {
$original_file = $conf->product->multidir_output[$entity].'/'.$original_file;
} elseif (isModEnabled("service")) {
$original_file = $conf->service->multidir_output[$entity].'/'.$original_file;
}
} elseif ($modulepart == 'product_batch' || $modulepart == 'produitlot') {
// Wrapping for product lots
if (empty($entity) || (empty($conf->productbatch->multidir_output[$entity]))) {
return array('accessallowed' => 0, 'error' => 'Value entity must be provided');
}
if (($fuser->hasRight('produit', $lire)) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
if (isModEnabled('productbatch')) {
$original_file = $conf->productbatch->multidir_output[$entity].'/'.$original_file;
}
} elseif ($modulepart == 'movement' || $modulepart == 'mouvement') {
// Wrapping for stock movements
if (empty($entity) || empty($conf->stock->multidir_output[$entity])) {
return array('accessallowed' => 0, 'error' => 'Value entity must be provided');
}
if (($fuser->hasRight('stock', $lire) || $fuser->hasRight('stock', 'movement', $lire) || $fuser->hasRight('stock', 'mouvement', $lire)) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
if (isModEnabled('stock')) {
$original_file = $conf->stock->multidir_output[$entity].'/movement/'.$original_file;
}
} elseif ($modulepart == 'entrepot') {
// Wrapping for stock warehouse
if (empty($entity) || empty($conf->stock->multidir_output[$entity])) {
return array('accessallowed' => 0, 'error' => 'Value entity must be provided');
}
if (($fuser->hasRight('stock', $lire) || $fuser->hasRight('stock', 'movement', $lire) || $fuser->hasRight('stock', 'mouvement', $lire)) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
if (isModEnabled('stock')) {
$original_file = $conf->stock->multidir_output[$entity].'/'.$original_file;
}
} elseif ($modulepart == 'contract' && !empty($conf->contract->multidir_output[$entity])) {
// Wrapping for contracts
if ($fuser->hasRight('contrat', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->contract->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."contrat WHERE ref='".$db->escape($refname)."' AND entity IN (".getEntity('contract').")";
} elseif ($modulepart == 'donation' && !empty($conf->don->dir_output)) {
// Wrapping for donation
if ($fuser->hasRight('don', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->don->dir_output.'/'.$original_file;
} elseif ($modulepart == 'dolresource' && !empty($conf->resource->dir_output)) {
// Wrapping for resources
if ($fuser->hasRight('resource', $read) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->resource->dir_output.'/'.$original_file;
} elseif (($modulepart == 'remisecheque' || $modulepart == 'chequereceipt') && !empty($conf->bank->dir_output)) {
// Wrapping pour les remises de cheques
if ($fuser->hasRight('banque', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->bank->dir_output.'/checkdeposits/'.$original_file; // original_file should contains relative path so include the get_exdir result
} elseif (($modulepart == 'banque' || $modulepart == 'bank') && !empty($conf->bank->dir_output)) {
// Wrapping for bank
if ($fuser->hasRight('banque', $lire)) {
$accessallowed = 1;
}
$original_file = $conf->bank->dir_output.'/'.$original_file;
} elseif ($modulepart == 'export' && !empty($conf->export->dir_temp)) {
// Wrapping for export module
// Note that a test may not be required because we force the dir of download on the directory of the user that export
$accessallowed = $user->hasRight('export', 'lire');
$original_file = $conf->export->dir_temp.'/'.$fuser->id.'/'.$original_file;
} elseif ($modulepart == 'import' && !empty($conf->import->dir_temp)) {
// Wrapping for import module
$accessallowed = $user->hasRight('import', 'run');
$original_file = $conf->import->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'recruitment' && !empty($conf->recruitment->dir_output)) {
// Wrapping for recruitment module
$accessallowed = $user->hasRight('recruitment', 'recruitmentjobposition', 'read');
$original_file = $conf->recruitment->dir_output.'/'.$original_file;
} elseif ($modulepart == 'hrm' && !empty($conf->hrm->dir_output)) {
// Wrapping for hrm module
$accessallowed = $user->hasRight('hrm', 'all', 'read');
$original_file = $conf->hrm->dir_output.'/'.$original_file;
} elseif ($modulepart == 'editor' && !empty($conf->fckeditor->dir_output)) {
// Wrapping for wysiwyg editor
$accessallowed = 1;
$original_file = $conf->fckeditor->dir_output.'/'.$original_file;
} elseif ($modulepart == 'systemtools' && !empty($conf->admin->dir_output)) {
// Wrapping for backups
if ($fuser->admin) {
$accessallowed = 1;
}
$original_file = $conf->admin->dir_output.'/'.$original_file;
} elseif ($modulepart == 'admin_temp' && !empty($conf->admin->dir_temp)) {
// Wrapping for upload file test
if ($fuser->admin) {
$accessallowed = 1;
}
$original_file = $conf->admin->dir_temp.'/'.$original_file;
} elseif ($modulepart == 'bittorrent' && !empty($conf->bittorrent->dir_output)) {
// Wrapping pour BitTorrent
$accessallowed = 1;
$dir = 'files';
if (dol_mimetype($original_file) == 'application/x-bittorrent') {
$dir = 'torrents';
}
$original_file = $conf->bittorrent->dir_output.'/'.$dir.'/'.$original_file;
} elseif ($modulepart == 'member' && !empty($conf->member->dir_output)) {
// Wrapping pour Foundation module
if ($fuser->hasRight('adherent', $lire) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->member->dir_output.'/'.$original_file;
} elseif ($modulepart == 'ticket' && !empty($conf->ticket->multidir_output[$entity])) {
// Wrapping for events
if ($fuser->hasRight('ticket', $read)) {
$accessallowed = 1;
}
if (!isset($_SESSION['email_customer'])) {
$sqlprotectagainstexternals = '';
} else {
$email_split = explode('@', $_SESSION['email_customer']);
$sqlprotectagainstexternals = 'SELECT t.rowid, t.fk_soc FROM '.MAIN_DB_PREFIX.'ticket t';
$sqlprotectagainstexternals .= ' LEFT JOIN '.MAIN_DB_PREFIX.'element_contact ec ON ec.element_id = t.rowid';
$sqlprotectagainstexternals .= ' LEFT JOIN '.MAIN_DB_PREFIX.'socpeople c ON c.rowid = ec.fk_socpeople';
$sqlprotectagainstexternals .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_type_contact tc ON tc.element = "ticket" AND tc.rowid = ec.fk_c_type_contact';
$sqlprotectagainstexternals .= ' WHERE t.ref LIKE "'.$db->sanitize($refname).'"';
$sqlprotectagainstexternals .= ' AND (';
$sqlprotectagainstexternals .= ' (';
$sqlprotectagainstexternals .= ' tc.rowid IS NOT NULL';
$sqlprotectagainstexternals .= ' AND c.email = "'.$db->sanitize($email_split[0]).'@'.$db->sanitize($email_split[1]).'"';
$sqlprotectagainstexternals .= ' )';
$sqlprotectagainstexternals .= ' OR t.origin_email = "'.$db->sanitize($email_split[0]).'@'.$db->sanitize($email_split[1]).'"';
$sqlprotectagainstexternals .= ' )';
}
$original_file = $conf->ticket->multidir_output[$entity].'/'.$original_file;
// If modulepart=module_user_temp Allows any module to open a file if file is in directory called DOL_DATA_ROOT/modulepart/temp/iduser
// If modulepart=module_temp Allows any module to open a file if file is in directory called DOL_DATA_ROOT/modulepart/temp
// If modulepart=module_user Allows any module to open a file if file is in directory called DOL_DATA_ROOT/modulepart/iduser
// If modulepart=module Allows any module to open a file if file is in directory called DOL_DATA_ROOT/modulepart
// If modulepart=module-abc Allows any module to open a file if file is in directory called DOL_DATA_ROOT/modulepart
} else {
// GENERIC Wrapping
//var_dump($modulepart);
//var_dump($original_file);
if (preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1; // If link to a file called specimen. Test must be done before changing $original_file int full path.
}
if ($fuser->admin) {
$accessallowed = 1; // If user is admin
}
$tmpmodulepart = explode('-', $modulepart);
if (!empty($tmpmodulepart[1])) {
$modulepart = $tmpmodulepart[0];
$original_file = $tmpmodulepart[1].'/'.$original_file;
}
// Define $accessallowed
$reg = array();
if (preg_match('/^([a-z]+)_user_temp$/i', $modulepart, $reg)) {
$tmpmodule = $reg[1];
if (empty($conf->$tmpmodule->dir_temp)) { // modulepart not supported
dol_print_error(null, 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
exit;
}
if ($fuser->hasRight($tmpmodule, $lire) || $fuser->hasRight($tmpmodule, $read) || $fuser->hasRight($tmpmodule, $download)) {
$accessallowed = 1;
}
$original_file = $conf->{$reg[1]}->dir_temp.'/'.$fuser->id.'/'.$original_file;
} elseif (preg_match('/^([a-z]+)_temp$/i', $modulepart, $reg)) {
$tmpmodule = $reg[1];
if (empty($conf->$tmpmodule->dir_temp)) { // modulepart not supported
dol_print_error(null, 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
exit;
}
if ($fuser->hasRight($tmpmodule, $lire) || $fuser->hasRight($tmpmodule, $read) || $fuser->hasRight($tmpmodule, $download)) {
$accessallowed = 1;
}
$original_file = $conf->$tmpmodule->dir_temp.'/'.$original_file;
} elseif (preg_match('/^([a-z]+)_user$/i', $modulepart, $reg)) {
$tmpmodule = $reg[1];
if (empty($conf->$tmpmodule->dir_output)) { // modulepart not supported
dol_print_error(null, 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
exit;
}
if ($fuser->hasRight($tmpmodule, $lire) || $fuser->hasRight($tmpmodule, $read) || $fuser->hasRight($tmpmodule, $download)) {
$accessallowed = 1;
}
$original_file = $conf->$tmpmodule->dir_output.'/'.$fuser->id.'/'.$original_file;
} elseif (preg_match('/^massfilesarea_([a-z]+)$/i', $modulepart, $reg)) {
$tmpmodule = $reg[1];
if (empty($conf->$tmpmodule->dir_output)) { // modulepart not supported
dol_print_error(null, 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
exit;
}
// Check fuser->rights->modulepart->myobject->read and fuser->rights->modulepart->read
$partsofdirinoriginalfile = explode('/', $original_file);
if (!empty($partsofdirinoriginalfile[1])) { // If original_file is xxx/filename (xxx is a part we will use)
$partofdirinoriginalfile = $partsofdirinoriginalfile[0];
if (($partofdirinoriginalfile && $fuser->hasRight($tmpmodule, $partofdirinoriginalfile, 'read')) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
}
if ($fuser->hasRight($tmpmodule, $read) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;
}
$original_file = $conf->$tmpmodule->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
} else {
if (empty($conf->$modulepart->dir_output)) { // modulepart not supported
dol_print_error(null, 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.'). The module for this modulepart value may not be activated.');
exit;
}
// Check fuser->hasRight('modulepart', 'myobject', 'read') and fuser->hasRight('modulepart', 'read')
$partsofdirinoriginalfile = explode('/', $original_file);
if (!empty($partsofdirinoriginalfile[1])) { // If original_file is xxx/filename (xxx is a part we will use)
$partofdirinoriginalfile = $partsofdirinoriginalfile[0];
if ($partofdirinoriginalfile && ($fuser->hasRight($modulepart, $partofdirinoriginalfile, 'lire') || $fuser->hasRight($modulepart, $partofdirinoriginalfile, 'read'))) {
$accessallowed = 1;
}
}
if (($fuser->hasRight($modulepart, $lire) || $fuser->hasRight($modulepart, $read)) || ($fuser->hasRight($modulepart, 'all', $lire) || $fuser->hasRight($modulepart, 'all', $read))) {
$accessallowed = 1;
}
if (is_array($conf->$modulepart->multidir_output) && !empty($conf->$modulepart->multidir_output[$entity])) {
$original_file = $conf->$modulepart->multidir_output[$entity].'/'.$original_file;
} else {
$original_file = $conf->$modulepart->dir_output.'/'.$original_file;
}
}
$parameters = array(
'modulepart' => $modulepart,
'original_file' => $original_file,
'entity' => $entity,
'fuser' => $fuser,
'refname' => '',
'mode' => $mode
);
$reshook = $hookmanager->executeHooks('checkSecureAccess', $parameters, $object);
if ($reshook > 0) {
if (!empty($hookmanager->resArray['original_file'])) {
$original_file = $hookmanager->resArray['original_file'];
}
if (!empty($hookmanager->resArray['accessallowed'])) {
$accessallowed = $hookmanager->resArray['accessallowed'];
}
if (!empty($hookmanager->resArray['sqlprotectagainstexternals'])) {
$sqlprotectagainstexternals = $hookmanager->resArray['sqlprotectagainstexternals'];
}
}
}
$ret = array(
'accessallowed' => ($accessallowed ? 1 : 0),
'sqlprotectagainstexternals' => $sqlprotectagainstexternals,
'original_file' => $original_file
);
return $ret;
}
/**
* Store object in file.
*
* @param string $directory Directory of cache
* @param string $filename Name of filecache
* @param string $object String to store in cachefile
* @return void
*/
function dol_filecache($directory, $filename, $object)
{
if (!dol_is_dir($directory)) {
$result = dol_mkdir($directory);
if ($result < -1) {
dol_syslog("Failed to create the cache directory ".$directory, LOG_WARNING);
}
}
$cachefile = $directory.$filename;
file_put_contents($cachefile, json_encode($object), LOCK_EX);
dolChmod($cachefile);
}
/**
* Test if Refresh needed.
*
* @param string $directory Directory of cache
* @param string $filename Name of filecache
* @param int $cachetime Cachetime delay
* @return boolean 0 no refresh 1 if refresh needed
*/
function dol_cache_refresh($directory, $filename, $cachetime)
{
$now = dol_now();
$cachefile = $directory.$filename;
$refresh = !file_exists($cachefile) || ($now - $cachetime) > dol_filemtime($cachefile);
return $refresh;
}
/**
* Read object from cachefile.
*
* @param string $directory Directory of cache
* @param string $filename Name of filecache
* @return string Unserialise from file
*/
function dol_readcachefile($directory, $filename)
{
$cachefile = $directory.$filename;
$object = json_decode(file_get_contents($cachefile));
return $object;
}
/**
* Return the relative dirname (relative to DOL_DATA_ROOT) of a full path string.
*
* @param string $pathfile Full path of a file
* @return string Path of file relative to DOL_DATA_ROOT
*/
function dirbasename($pathfile)
{
return preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'\//', '', $pathfile);
}
/**
* Function to get list of updated or modified files.
* $file_list is used as global variable
*
* @param array{}|array{insignature:string[],missing?:array<array{filename:string,expectedhash:string,expectedsize:string,algo:string}>,updated:array<array{filename:string,expectedhash:string,expectedsize:string,hash:string,algo:string}>} $file_list Array for response
* @param SimpleXMLElement $dir SimpleXMLElement of files to test
* @param string $path Path of files relative to $pathref. We start with ''. Used by recursive calls.
* @param string $pathref Path ref (DOL_DOCUMENT_ROOT)
* @param string[] $checksumconcat Array of checksum
* @return array{insignature:string[],missing?:array<array{filename:string,expectedhash:string,expectedsize:string,algo:string}>,updated:array<array{filename:string,expectedhash:string,expectedsize:string,hash:string,algo:string}>} $file_list Array of filenames
*/
function getFilesUpdated(&$file_list, SimpleXMLElement $dir, $path = '', $pathref = '', &$checksumconcat = array())
{
global $conffile;
//$exclude = 'install';
$entry = array();
$algo = '';
if (!empty($dir->md5file)) {
$entry = $dir->md5file;
$algo = 'md5';
} elseif (!empty($dir->sha256file)) {
$entry = $dir->sha256file;
$algo = 'sha256';
}
foreach ($entry as $file) { // $file is a simpleXMLElement
$filename = $path.$file['name'];
$file_list['insignature'][] = $filename;
$expectedsize = (empty($file['size']) ? '' : $file['size']);
$expectedhash = (string) $file;
if (!file_exists($pathref.'/'.$filename)) {
$file_list['missing'][] = array('filename' => $filename, 'expectedhash' => $expectedhash, 'expectedsize' => $expectedsize, 'algo' => (string) $algo);
} else {
$hash_local = hash_file($algo, $pathref.'/'.$filename);
if ($conffile == '/etc/dolibarr/conf.php' && $filename == '/filefunc.inc.php') { // For install with deb or rpm, we ignore test on filefunc.inc.php that was modified by package
$checksumconcat[] = $expectedhash;
} else {
if ($hash_local != $expectedhash) {
$file_list['updated'][] = array('filename' => $filename, 'expectedhash' => $expectedhash, 'expectedsize' => $expectedsize, 'hash' => (string) $hash_local, 'algo' => (string) $algo);
}
$checksumconcat[] = $hash_local;
}
}
}
foreach ($dir->dir as $subdir) { // $subdir['name'] is '' or '/accountancy/admin' for example
getFilesUpdated($file_list, $subdir, $path.$subdir['name'].'/', $pathref, $checksumconcat);
}
return $file_list;
}
/**
* Function to manage the drag and drop of a file.
* We use global variable $object
*
* @param string $htmlname The id of the component where we need to drag and drop
* @return string Js script to display
*/
function dragAndDropFileUpload($htmlname)
{
global $object, $langs;
// Every generated javascript string that carries an interpolated value is delimited by a single quote, so
// dol_escape_js() is called with the mode 1 everywhere below: it escapes a single quote and leaves a double
// quote alone. The default mode would rewrite a double quote into an escaped single quote, which is safe
// inside a '...' string but silently alters the value.
// dol_escape_js() escapes the quotes but not '</', and PHP_SELF holds the path info of the request on a
// server that accepts it, so a request could close the script tag below and open one of its own.
$pageurl = str_replace('</', '<\\/', dol_escape_js($_SERVER["PHP_SELF"], 1));
$out = "";
$out .= '<div id="'.$htmlname.'Message" class="dragDropAreaMessage hidden"><span>'.img_picto("", 'download').'<br>'.$langs->trans("DropFileToAddItToObject").'</span></div>';
$out .= "\n<!-- JS CODE TO ENABLE DRAG AND DROP OF FILE -->\n";
$out .= '<script nonce="'.getNonce().'">';
$out .= '
jQuery(document).ready(function() {
var enterTargetDragDrop = null;
$(\'#'.$htmlname.'\').addClass(\'cssDragDropArea\');
$(".cssDragDropArea").on("dragenter", function(ev, ui) {
var dataTransfer = ev.originalEvent.dataTransfer;
var dataTypes = dataTransfer.types;
//console.log(dataTransfer);
//console.log(dataTypes);
if (!dataTypes || ($.inArray(\'Files\', dataTypes) === -1)) {
// The element dragged is not a file, so we avoid the "dragenter"
ev.preventDefault();
return false;
}
// Entering drop area. Highlight area
console.log("dragAndDropFileUpload: We add class highlightDragDropArea")
enterTargetDragDrop = ev.target;
$(this).addClass("highlightDragDropArea");
$(\'#'.$htmlname.'Message\').removeClass(\'hidden\');
ev.preventDefault();
});
$(".cssDragDropArea").on("dragleave", function(ev) {
// Going out of drop area. Remove Highlight
if (enterTargetDragDrop == ev.target){
console.log("dragAndDropFileUpload: We remove class highlightDragDropArea")
$(\'#'.$htmlname.'Message\').addClass(\'hidden\');
$(this).removeClass("highlightDragDropArea");
}
});
$(".cssDragDropArea").on("dragover", function(ev) {
ev.preventDefault();
return false;
});
$(".cssDragDropArea").on("drop", function(e) {
console.log(\'Trigger event file dropped. fk_element='.dol_escape_js((string) $object->id, 1).' element='.dol_escape_js($object->element, 1).'\');
e.preventDefault();
fd = new FormData();
fd.append(\'fk_element\', \''.dol_escape_js((string) $object->id, 1).'\');
fd.append(\'element\', \''.dol_escape_js($object->element, 1).'\');
fd.append(\'token\', \''.currentToken().'\');
fd.append("action", "linkit");
var dataTransfer = e.originalEvent.dataTransfer;
if (dataTransfer.files && dataTransfer.files.length){
var droppedFiles = e.originalEvent.dataTransfer.files;
$.each(droppedFiles, function(index,file){
fd.append("files[]", file,file.name)
});
}
$(".cssDragDropArea").removeClass("highlightDragDropArea");
counterdragdrop = 0;
$.ajax({
url: \''.DOL_URL_ROOT.'/core/ajax/fileupload.php\',
type: "POST",
processData: false,
contentType: false,
data: fd,
success:function() {
console.log("Uploaded.", arguments);
/* arguments[0] is the json string of files */
/* arguments[1] is the value for variable "success", can be 0 or 1 */
let listoffiles = [];
/* The answer is not the expected json when php stopped before answering, for example when
post_max_size was reached. Without this, the exception of JSON.parse() would leave the
user on a page with no message at all, thinking the file was added. */
try {
listoffiles = JSON.parse(arguments[0]);
} catch (e) {
window.location.href = \''.$pageurl.'?id='.dol_escape_js((string) $object->id, 1).'&seteventmessages=ErrorUploadFileDragDrop:errors\';
return;
}
console.log(listoffiles);
let nboferror = 0;
for (let i = 0; i < listoffiles.length; i++) {
console.log(listoffiles[i].error);
if (listoffiles[i].error) {
nboferror++;
}
}
console.log(nboferror);
/* An empty list means no file was stored at all, so it is an error and not a success:
php empties $_FILES when post_max_size is reached. */
if (listoffiles.length == 0) {
window.location.href = \''.$pageurl.'?id='.dol_escape_js((string) $object->id, 1).'&seteventmessages=ErrorUploadFileDragDrop:errors\';
} else if (nboferror > 0) {
window.location.href = \''.$pageurl.'?id='.dol_escape_js((string) $object->id, 1).'&seteventmessages=ErrorOnAtLeastOneFileUpload:warnings\';
} else {
window.location.href = \''.$pageurl.'?id='.dol_escape_js((string) $object->id, 1).'&seteventmessages=UploadFileDragDropSuccess:mesgs\';
}
},
error:function(jqXHR) {
console.log("Error Uploading.", arguments)
if (jqXHR.status == 403) {
window.location.href = \''.$pageurl.'?id='.dol_escape_js((string) $object->id, 1).'&seteventmessages=ErrorUploadFileDragDropPermissionDenied:errors\';
} else {
window.location.href = \''.$pageurl.'?id='.dol_escape_js((string) $object->id, 1).'&seteventmessages=ErrorUploadFileDragDrop:errors\';
}
},
})
});
});
';
$out .= "</script>\n";
return $out;
}
/**
* Manage backup versions for a given file, ensuring only a maximum number of versions are kept.
*
* @param string $srcfile Full path of the source filename for the backups. Example /mydir/mydocuments/mymodule/filename.ext
* @param int $max_versions The maximum number of backup versions to keep.
* @param string $archivedir Target directory of backups (without ending /). Keep empty to save into the same directory than source file.
* @param string $suffix 'v' (versioned files) or 'd' (archived files after deletion)
* @param string $moveorcopy 'move' or 'copy'
* @return bool Returns true if successful, false otherwise.
*/
function archiveOrBackupFile($srcfile, $max_versions = 5, $archivedir = '', $suffix = "v", $moveorcopy = 'move')
{
$base_file_pattern = ($archivedir ? $archivedir : dirname($srcfile)).'/'.basename($srcfile).".".$suffix;
$files_in_directory = glob($base_file_pattern . "*");
// Extract the modification timestamps for each file
$files_with_timestamps = [];
foreach ($files_in_directory as $file) {
$files_with_timestamps[] = [
'file' => $file,
'timestamp' => filemtime($file)
];
}
// Sort the files by modification date
$sorted_files = [];
while (count($files_with_timestamps) > 0) {
$latest_file = null;
$latest_index = null;
// Find the latest file by timestamp
foreach ($files_with_timestamps as $index => $file_info) {
if ($latest_file === null || (is_array($latest_file) && $file_info['timestamp'] > $latest_file['timestamp'])) {
$latest_file = $file_info;
$latest_index = $index;
}
}
// Add the latest file to the sorted list and remove it from the original list
if ($latest_file !== null) {
$sorted_files[] = $latest_file['file'];
unset($files_with_timestamps[$latest_index]);
}
}
// Delete the oldest files to keep only the allowed number of versions
if (count($sorted_files) >= $max_versions) {
$oldest_files = array_slice($sorted_files, $max_versions - 1);
foreach ($oldest_files as $oldest_file) {
dol_delete_file($oldest_file, 0, 0, 0, null, false, 0);
}
}
$timestamp = dol_now('gmt');
$new_backup = $srcfile . ".v" . $timestamp;
// Move or copy the original file to the new backup with the timestamp
if ($moveorcopy == 'move') {
$result = dol_move($srcfile, $new_backup, '0', 1, 0, 0);
} else {
$result = dol_copy($srcfile, $new_backup, '0', 1, 0, 0);
}
if (!$result) {
return false;
}
return true;
}
/**
* @param string $filetoprocess File name to process
* @param string $useFullTextIndexation Method for txt conversion
* @param string $options Output format ('html', 'fulltext');
* @return array<string,mixed> Array of result ('error'=>, 'content'=> , 'keywords'=>, 'cmd'=> )
*/
function dolDocToText($filetoprocess, $useFullTextIndexation = 'pdftotext', $options = 'html')
{
global $conf, $db, $user;
$error = 0;
$keywords = array();
$textforfulltextindex = '';
$cmd = '';
if (empty($useFullTextIndexation)) {
$useFullTextIndexation = 'pdftotext';
}
// TODO Move this into external submodule files
// TODO Develop a native PHP parser using sample code in https://github.com/adeel/php-pdf-parser or https://github.com/smalot/pdfparser
// Use the method pdftotext to generate a HTML
if (preg_match('/pdftotext/i', $useFullTextIndexation)) {
include_once DOL_DOCUMENT_ROOT.'/core/class/utils.class.php';
$utils = new Utils($db);
$outputfile = $conf->admin->dir_temp.'/tmppdftotext.'.$user->id.'.out'; // File used with popen method
// We also exclude '/temp/' dir and 'documents/admin/documents'
// We make escapement here and call executeCLI without escapement because we don't want to have the '*.log' escaped.
if ($options == 'fulltext') {
$params = '-nodiag -layout';
} else {
$params = '-htmlmeta';
}
$cmd = getDolGlobalString('MAIN_SAVE_FILE_CONTENT_AS_TEXT_PDFTOTEXT', 'pdftotext') . " " . $params ." '".escapeshellcmd($filetoprocess)."' - ";
$resultexec = $utils->executeCLI($cmd, $outputfile, 0, null, 1);
if (empty($resultexec['error'])) {
$matches = array();
if ($options == 'fulltext') {
$textforfulltextindex = $resultexec['output'];
}
if ($options == 'html') {
$txt = $resultexec['output'];
if (preg_match('/<meta name="keywords" content="([^\/]+)"\s*\/>/i', $txt, $matches)) {
$keywords = $matches[1];
}
if (preg_match('/<pre>(.*)<\/pre>/si', $txt, $matches)) {
$textforfulltextindex = dol_string_nounprintableascii($matches[1], 0);
}
}
} else {
dol_syslog($resultexec['error']);
$error++;
}
}
// Use the method docling to generate a .md (https://ds4sd.github.io/docling/)
if (preg_match('/docling/i', $useFullTextIndexation)) {
include_once DOL_DOCUMENT_ROOT.'/core/class/utils.class.php';
$utils = new Utils($db);
$outputfile = $conf->admin->dir_temp.'/tmpdocling.'.$user->id.'.out'; // File used with popen method
// We also exclude '/temp/' dir and 'documents/admin/documents'
// We make escapement here and call executeCLI without escapement because we don't want to have the '*.log' escaped.
$cmd = getDolGlobalString('MAIN_SAVE_FILE_CONTENT_AS_TEXT_DOCLING', 'docling')." --from pdf --to text '".escapeshellcmd($filetoprocess)."'";
$resultexec = $utils->executeCLI($cmd, $outputfile, 0, null, 1);
if (!$resultexec['error']) {
$txt = $resultexec['output'];
//$matches = array();
//if (preg_match('/<meta name="Keywords" content="([^\/]+)"\s*\/>/i', $txt, $matches)) {
// $keywords = $matches[1];
//}
//if (preg_match('/<pre>(.*)<\/pre>/si', $txt, $matches)) {
// $textforfulltextindex = dol_string_nounprintableascii($matches[1], 0);
//}
$textforfulltextindex = $txt;
} else {
dol_syslog($resultexec['error']);
$error++;
}
}
return array('error' => $error, 'keywords' => $keywords, 'content' => $textforfulltextindex, 'cmd' => $cmd);
}
/**
* Remove the last line of a text file
*
* @param string $fullpath Full path of file
* @return int Return <0 if error, >0 if OK
*/
function removeLastLine($fullpath)
{
// Generate tmp file content without the last line
$fp = fopen($fullpath, "r");
fseek($fp, -1, SEEK_END);
$pos = -1;
$char = fgetc($fp);
while ($char === "\n" || $char === "\r") { // Go to last real char of last line
fseek($fp, $pos--, SEEK_END);
$char = fgetc($fp);
}
while ($char !== "\n" && $char !== false) {
fseek($fp, $pos--, SEEK_END);
$char = fgetc($fp);
}
/*
while ($char === "\n" || $char === "\r") { // Go to last real char of last-1 line
fseek($fp, $pos--, SEEK_END);
$char = fgetc($fp);
}
*/
$truncatePos = ftell($fp);
fclose($fp);
// Truncate the tmp file to remove the last line
$fp = fopen($fullpath, "c+");
ftruncate($fp, $truncatePos);
fclose($fp);
return 1;
}