dolibarr 23.0.3
document.controller.class.php
Go to the documentation of this file.
1<?php
2/*
3 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
26require_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
27
28
33{
37 public $action;
38
42 public $attachment;
43
47 public $encoding;
48
52 public $entity;
53
57 public $filename;
58
62 public $fullpath_original_file;
63
67 public $fullpath_original_file_osencoded;
68
72 public $modulepart;
73
77 public $original_file;
78
82 public $type;
83
84
90 public function init()
91 {
92 global $conf, $hookmanager;
93
94 define('MAIN_SECURITY_FORCECSP', "default-src 'none'");
95
96 if (!defined('NOTOKENRENEWAL')) {
97 define('NOTOKENRENEWAL', '1');
98 }
99 if (!defined('NOREQUIREMENU')) {
100 define('NOREQUIREMENU', '1');
101 }
102 if (!defined('NOREQUIREHTML')) {
103 define('NOREQUIREHTML', '1');
104 }
105 if (!defined('NOREQUIREAJAX')) {
106 define('NOREQUIREAJAX', '1');
107 }
108
110
111 $encoding = '';
112 $action = GETPOST('action', 'aZ09');
113 $original_file = GETPOST('file', 'alphanohtml'); // Do not use urldecode here ($_GET are already decoded by PHP).
114 $modulepart = GETPOST('modulepart', 'alpha');
115 $entity = GETPOSTINT('entity') ? GETPOSTINT('entity') : $conf->entity;
116 $socId = GETPOSTINT('soc_id');
117
118 // Security check
119 if (empty($modulepart)) {
120 httponly_accessforbidden('Bad link. Bad value for parameter modulepart', 400);
121 }
122 if (empty($original_file)) {
123 httponly_accessforbidden('Bad link. Missing identification to find file (original_file)', 400);
124 }
125
126 // get original file
127 $ecmfile = '';
128
129 // Define attachment (attachment=true to force choice popup 'open'/'save as')
130 $attachment = true;
131 if (preg_match('/\.(html|htm)$/i', $original_file)) {
132 $attachment = false;
133 }
134 if (GETPOSTISSET("attachment")) {
135 $attachment = GETPOST("attachment", 'alpha') ? true : false;
136 }
137 if (getDolGlobalString('MAIN_DISABLE_FORCE_SAVEAS')) {
138 $attachment = false;
139 }
140
141 // Define mime type
142 if (GETPOST('type', 'alpha')) {
143 $type = GETPOST('type', 'alpha');
144 } else {
145 $type = dol_mimetype($original_file);
146 }
147 // Security: Force to octet-stream if file is a dangerous file. For example when it is a .noexe file
148 // We do not force if file is a javascript to be able to get js from website module with <script src="
149 // Note: Force whatever is $modulepart seems ok.
150 if (!in_array($type, array('text/x-javascript')) && !dolIsAllowedForPreview($original_file)) {
151 $type = 'application/octet-stream';
152 }
153
154 // Security: Delete string ../ or ..\ into $original_file
155 $original_file = preg_replace('/\.\.+/', '..', $original_file); // Replace '... or more' with '..'
156 $original_file = str_replace('../', '/', $original_file);
157 $original_file = str_replace('..\\', '/', $original_file);
158
159 // Check security and set return info with full path of file
160 $accessallowed = 0; // not allowed by default
161 $moduleName = $modulepart;
162 $moduleNameEn = $moduleName;
163 if ($moduleName == 'commande') {
164 $moduleNameEn = 'order';
165 } elseif ($moduleName == 'facture') {
166 $moduleNameEn = 'invoice';
167 }
168 $moduleNameUpperEn = strtoupper($moduleNameEn);
169 // Hooks
170 $hookmanager->initHooks(array('document'));
171 $parameters = array('ecmfile' => $ecmfile, 'modulepart' => $modulepart, 'original_file' => &$original_file, 'socId' => $socId,
172 'entity' => $entity, 'accessallowed' => &$accessallowed);
173 $object = new stdClass();
174 $reshook = $hookmanager->executeHooks('accessDownloadDocument', $parameters, $object, $action); // Note that $action and $object may have been
175 if ($reshook < 0) {
176 $errors = $hookmanager->error . (is_array($hookmanager->errors) ? (!empty($hookmanager->error) ? ', ' : '') . implode(', ', $hookmanager->errors) : '');
177 dol_syslog("document.php - Errors when executing the hook 'accessDownloadDocument' : " . $errors);
178 print "ErrorDownloadDocumentHooks: " . $errors;
179 exit;
180 } elseif (empty($reshook)) {
181 // check config access
182 // and file mime type (only PDF)
183 // and check login access
184 if (getDolGlobalInt('WEBPORTAL_' . $moduleNameUpperEn . '_LIST_ACCESS')
185 && in_array($type, array('application/pdf'))
186 && ($context->logged_thirdparty && $context->logged_thirdparty->id > 0)
187 && $context->logged_thirdparty->id == $socId
188 ) {
189 if (isModEnabled($moduleName) && isset($conf->{$moduleName}->multidir_output[$entity])) {
190 $original_file = $conf->{$moduleName}->multidir_output[$entity] . '/' . $original_file;
191 $accessallowed = 1;
192 }
193 }
194 }
195 $fullpath_original_file = $original_file; // $fullpath_original_file is now a full path name
196
197 // Security:
198 // Limit access if permissions are wrong
199 if (!$accessallowed) {
201 }
202
203 // Security:
204 // We refuse directory transversal change and pipes in file names
205 if (preg_match('/\.\./', $fullpath_original_file) || preg_match('/[<>|]/', $fullpath_original_file)) {
206 dol_syslog("Refused to deliver file " . $fullpath_original_file);
207 print "ErrorFileNameInvalid: " . dol_escape_htmltag($original_file);
208 exit;
209 }
210
211 // Find the subdirectory name as the reference
212 $refname = basename(dirname($original_file) . "/");
213
214 $filename = basename($fullpath_original_file);
215 $filename = preg_replace('/\.noexe$/i', '', $filename);
216
217 // Output file on browser
218 dol_syslog("document controller download $fullpath_original_file filename=$filename content-type=$type");
219 $fullpath_original_file_osencoded = dol_osencode($fullpath_original_file); // New file name encoded in OS encoding charset
220
221 // This test if file exists should be useless. We keep it to find bug more easily
222 if (!file_exists($fullpath_original_file_osencoded)) {
223 dol_syslog("ErrorFileDoesNotExists: " . $fullpath_original_file);
224 print "ErrorFileDoesNotExists: " . $original_file;
225 exit;
226 }
227
228 $fileSize = dol_filesize($fullpath_original_file);
229 $fileSizeMax = getDolGlobalInt('MAIN_SECURITY_MAXFILESIZE_DOWNLOADED');
230 if ($fileSizeMax && $fileSize > ($fileSizeMax * 1024)) {
231 // FIX: Convert limit from Ko to bytes for proper comparison
232 $fileSizeKb = round($fileSize / 1024, 2);
233 dol_syslog('ErrorFileSizeTooLarge: ' . $fileSize . ' bytes (' . $fileSizeKb . ' Kb) - max allowed: ' . $fileSizeMax . ' Kb');
234 print 'ErrorFileSizeTooLarge: ' . $fileSizeKb . ' Kb (max ' . $fileSizeMax . ' Kb)';
235 exit;
236 }
237
238 // Hooks
239 $hookmanager->initHooks(array('document'));
240 $parameters = array('ecmfile' => $ecmfile, 'modulepart' => $modulepart, 'original_file' => $original_file,
241 'entity' => $entity, 'refname' => $refname, 'fullpath_original_file' => $fullpath_original_file,
242 'filename' => $filename, 'fullpath_original_file_osencoded' => $fullpath_original_file_osencoded);
243 $object = new stdClass();
244 $reshook = $hookmanager->executeHooks('downloadDocument', $parameters, $object, $action); // Note that $action and $object may have been
245 if ($reshook < 0) {
246 $errors = $hookmanager->error . (is_array($hookmanager->errors) ? (!empty($hookmanager->error) ? ', ' : '') . implode(', ', $hookmanager->errors) : '');
247 dol_syslog("document.php - Errors when executing the hook 'downloadDocument' : " . $errors);
248 print "ErrorDownloadDocumentHooks: " . $errors;
249 exit;
250 }
251
252 $this->action = $action;
253 $this->attachment = $attachment;
254 $this->encoding = $encoding;
255 $this->entity = $entity;
256 $this->filename = $filename;
257 $this->fullpath_original_file = $fullpath_original_file;
258 $this->fullpath_original_file_osencoded = $fullpath_original_file_osencoded;
259 $this->modulepart = $modulepart;
260 $this->original_file = $original_file;
261 $this->type = $type;
262 }
263
269 public function checkAccess()
270 {
271 $this->accessRight = true;
272
273 return parent::checkAccess();
274 }
275
282 public function action()
283 {
285 if (!$context->controllerInstance->checkAccess()) {
286 return -1;
287 }
288
289 //$context = Context::getInstance();
290 //$context->title = $langs->trans('WebPortalDocumentTitle');
291 //$context->desc = $langs->trans('WebPortalDocumentDesc');
292 //$context->doNotDisplayHeaderBar=1;// hide default header
293
294 $this->init();
295
296 return 1;
297 }
298
304 public function display()
305 {
307 if (!$context->controllerInstance->checkAccess()) {
308 $this->display404();
309 return;
310 }
311
312 // initialize
313 $attachment = $this->attachment;
314 $encoding = $this->encoding;
315 $filename = $this->filename;
316 $fullpath_original_file = $this->fullpath_original_file;
317 $fullpath_original_file_osencoded = $this->fullpath_original_file_osencoded;
318 $type = $this->type;
319
320 clearstatcache();
321
322 // Permissions are ok and file found, so we return it
323 top_httphead($type);
324 header('Content-Description: File Transfer');
325 if ($encoding) {
326 header('Content-Encoding: ' . $encoding);
327 }
328 // Add MIME Content-Disposition from RFC 2183 (inline=automatically displayed, attachment=need user action to open)
329 if ($attachment) {
330 header('Content-Disposition: attachment; filename="' . $filename . '"');
331 } else {
332 header('Content-Disposition: inline; filename="' . $filename . '"');
333 }
334 header('Cache-Control: Public, must-revalidate');
335 header('Pragma: public');
336
337 // Send file now
338 header('Content-Length: ' . dol_filesize($fullpath_original_file));
339 readfileLowMemory($fullpath_original_file_osencoded);
340 }
341}
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
static getInstance()
Singleton method to create one instance of this object.
Class to manage pages.
Class for DocumentController.
action()
Action method is called before html output can be used to manage security and change context.
checkAccess()
Check current access to controller.
dol_filesize($pathoffile)
Return size of a file.
dol_mimetype($file, $default='application/octet-stream', $mode=0)
Return MIME type of a file from its name with extension.
GETPOSTINT($paramname, $method=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
dol_osencode($str)
Return a string encoded into OS filesystem encoding.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0)
Return value of a param into GET or POST supervariable.
readfileLowMemory($fullpath_original_file_osencoded, $method=-1)
Return a file on output using a low memory.
dolIsAllowedForPreview($file)
Return if a file is qualified for preview.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
if(!defined( 'NOREQUIREMENU')) if(!empty(GETPOST('seteventmessages', 'alpha'))) if(!function_exists("llxHeader")) top_httphead($contenttype='text/html', $forcenocache=0)
Show HTTP header.
$context
@method int call_trigger(string $triggerName, ?User $user)
Definition logout.php:42
if(preg_match('/(crypted|dolcrypt):/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
'integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter[:Sortfield]]]',...
Definition repair.php:125
httponly_accessforbidden($message='1', $http_response_code=403, $stringalreadysanitized=0)
Show a message to say access is forbidden and stop program.
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.