dolibarr 24.0.0-beta
viewimage.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 $entity;
43
47 public $filename;
48
52 public $fullpath_original_file;
53
57 public $fullpath_original_file_osencoded;
58
62 public $modulepart;
63
67 public $original_file;
68
72 public $type;
73
74
80 public function init()
81 {
82 global $conf, $hookmanager, $user;
83
84 define('MAIN_SECURITY_FORCECSP', "default-src 'none'");
85
86 if (!defined('NOREQUIRESOC')) {
87 define('NOREQUIRESOC', '1');
88 }
89 if (!defined('NOREQUIRETRAN')) {
90 define('NOREQUIRETRAN', '1');
91 }
92 if (!defined('NOCSRFCHECK')) {
93 define('NOCSRFCHECK', '1');
94 }
95 if (!defined('NOTOKENRENEWAL')) {
96 define('NOTOKENRENEWAL', '1');
97 }
98 if (!defined('NOREQUIREMENU')) {
99 define('NOREQUIREMENU', '1');
100 }
101 if (!defined('NOREQUIREHTML')) {
102 define('NOREQUIREHTML', '1');
103 }
104 if (!defined('NOREQUIREAJAX')) {
105 define('NOREQUIREAJAX', '1');
106 }
107
108 // For MultiCompany module.
109 // Do not use GETPOST here, function is not defined and define must be done before including main.inc.php
110 // Because 2 entities can have the same ref.
111 $entity = (!empty($_GET['entity']) ? (int) $_GET['entity'] : (!empty($_POST['entity']) ? (int) $_POST['entity'] : 1));
112 define("DOLENTITY", $entity);
113
115
116 $action = GETPOST('action', 'aZ09');
117 $original_file = GETPOST('file', 'alphanohtml');
118 $hashp = GETPOST('hashp', 'aZ09', 1);
119 $extname = GETPOST('extname', 'alpha', 1);
120 $modulepart = GETPOST('modulepart', 'alpha', 1);
121 $urlsource = GETPOST('urlsource', 'alpha');
122 $entity = (GETPOSTINT('entity') ? GETPOSTINT('entity') : $conf->entity);
123
124 // Security check
125 if (empty($modulepart) && empty($hashp)) {
126 httponly_accessforbidden('Bad link. Bad value for parameter modulepart', 400);
127 }
128 if (empty($original_file) && empty($hashp) && $modulepart != 'barcode') {
129 httponly_accessforbidden('Bad link. Missing identification to find file (param file or hashp)', 400);
130 }
131 if ($modulepart == 'fckeditor') {
132 $modulepart = 'medias'; // For backward compatibility
133 }
134
135 $cachestring = GETPOST("cache", 'aZ09'); // May be 1, or an int, or a hash
136 if ($cachestring) {
137 // Important: The following code is to avoid a page request by the browser and PHP CPU at each Dolibarr page access.
138 // We are here when param cache=xxx to force a cache policy:
139 // xxx=1 means cache of 3600s
140 // xxx=abcdef or 123456789 means a cache of 1 week (the key will be modified to get break cache use)
141 $delaycache = ((is_numeric($cachestring) && (int) $cachestring > 1 && (int) $cachestring < 999999) ? $cachestring : '3600');
142 header('Cache-Control: max-age=' . $delaycache . ', public, must-revalidate');
143 header('Pragma: cache'); // This is to avoid to have Pragma: no-cache set by proxy or web server
144 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (int) $delaycache) . ' GMT'); // This is to avoid to have Expires set by proxy or web server
145 }
146
147 // Define mime type
148 $type = 'application/octet-stream';
149 if (GETPOST('type', 'alpha')) {
150 $type = GETPOST('type', 'alpha');
151 } else {
152 $type = dol_mimetype($original_file);
153 }
154
155 // Security: This wrapper is for images. We do not allow type/html
156 if (preg_match('/html/i', $type)) {
157 httponly_accessforbidden('Error: Using the image wrapper to output a file with a mime type HTML is not possible.');
158 }
159 // Security: This wrapper is for images. We do not allow files ending with .noexe
160 if (preg_match('/\.noexe$/i', $original_file)) {
161 httponly_accessforbidden('Error: Using the image wrapper to output a file ending with .noexe is not allowed.');
162 }
163
164 // Security: Delete string ../ or ..\ into $original_file
165 $original_file = preg_replace('/\.\.+/', '..', $original_file); // Replace '... or more' with '..'
166 $original_file = str_replace('../', '/', $original_file);
167 $original_file = str_replace('..\\', '/', $original_file);
168
169 // Find the subdirectory name as the reference
170 $refname = basename(dirname($original_file) . "/");
171 if ($refname == 'thumbs') {
172 // If we get the thumbs directory, we must go one step higher. For example original_file='10/thumbs/myfile_small.jpg' -> refname='10'
173 $refname = basename(dirname(dirname($original_file)) . "/");
174 }
175
176 // Check that file is allowed for view with viewimage.php
177 if (!empty($original_file) && !dolIsAllowedForPreview($original_file)) {
178 httponly_accessforbidden('This file extension is not qualified for preview', 403);
179 }
180
181 // Security check
182 if (empty($modulepart)) {
183 httponly_accessforbidden('Bad value for parameter modulepart', 400);
184 }
185
186 // When logged in a different entity, medias cannot be accessed because $conf->$module->multidir_output
187 // is not set on the requested entity, but they are public documents, so reset entity
188 if ($modulepart === 'medias' && $entity != $conf->entity) {
189 $conf->entity = $entity;
190 $conf->setValues($this->db);
191 }
192
193 $sqlprotectagainstexternals = '';
194 $fullpath_original_file = '';
195 $accessallowed = 0;
196
197 // Hooks
198 $hookmanager->initHooks(array('viewimage'));
199 $parameters = array('modulepart' => $modulepart, 'original_file' => &$original_file,
200 'sqlprotectagainstexternals' => &$sqlprotectagainstexternals, 'fullpath_original_file' => &$fullpath_original_file,
201 'entity' => $entity, 'accessallowed' => &$accessallowed);
202 $object = new stdClass();
203 $reshook = $hookmanager->executeHooks('accessViewImage', $parameters, $object, $action); // Note that $action and $object may have been
204 if ($reshook < 0) {
205 $errors = $hookmanager->error . (is_array($hookmanager->errors) ? (!empty($hookmanager->error) ? ', ' : '') . implode(', ', $hookmanager->errors) : '');
206 dol_syslog("document.php - Errors when executing the hook 'accessViewImage' : " . $errors);
207 print "ErrorViewImageHooks: " . $errors;
208 exit;
209 } elseif (empty($reshook)) {
210 $check_access = dol_check_secure_access_document($modulepart, $original_file, $entity, $user, $refname);
211 $accessallowed = $check_access['accessallowed'];
212 $sqlprotectagainstexternals = $check_access['sqlprotectagainstexternals'];
213 $fullpath_original_file = $check_access['original_file']; // $fullpath_original_file is now a full path name
214 }
215
216 if (!empty($hashp)) {
217 $accessallowed = 1; // When using hashp, link is public so we force $accessallowed
218 $sqlprotectagainstexternals = '';
219 } else {
220 // Basic protection (against external users only)
221 if ($user->socid > 0) {
222 if ($sqlprotectagainstexternals) {
223 $resql = $this->db->query($sqlprotectagainstexternals);
224 if ($resql) {
225 $num = $this->db->num_rows($resql);
226 $i = 0;
227 while ($i < $num) {
228 $obj = $this->db->fetch_object($resql);
229 if ($user->socid != $obj->fk_soc) {
230 $accessallowed = 0;
231 break;
232 }
233 $i++;
234 }
235 }
236 }
237 }
238 }
239
240 // Security:
241 // Limit access if permissions are wrong
242 if (!$accessallowed) {
244 }
245
246 // Security:
247 // On interdit les remontees de repertoire ainsi que les pipe dans les noms de fichiers.
248 if (preg_match('/\.\./', $fullpath_original_file) || preg_match('/[<>|]/', $fullpath_original_file)) {
249 dol_syslog("Refused to deliver file " . $fullpath_original_file);
250 print "ErrorFileNameInvalid: " . dol_escape_htmltag($original_file);
251 exit;
252 }
253
254 // Find the subdirectory name as the reference
255 $refname = basename(dirname($original_file) . "/");
256
257 $filename = basename($fullpath_original_file);
258 $filename = preg_replace('/\.noexe$/i', '', $filename);
259
260 // Output file on browser
261 dol_syslog("document controller download $fullpath_original_file filename=$filename content-type=$type");
262 $fullpath_original_file_osencoded = dol_osencode($fullpath_original_file); // New file name encoded in OS encoding charset
263
264 // This test if file exists should be useless. We keep it to find bug more easily
265 if (!file_exists($fullpath_original_file_osencoded)) {
266 dol_syslog("ErrorFileDoesNotExists: " . $fullpath_original_file);
267 print "ErrorFileDoesNotExists: " . $original_file;
268 exit;
269 }
270
271 $fileSize = dol_filesize($fullpath_original_file);
272 $fileSizeMax = getDolGlobalInt('MAIN_SECURITY_MAXFILESIZE_DOWNLOADED');
273 if ($fileSizeMax && $fileSize > $fileSizeMax) {
274 dol_syslog('ErrorFileSizeTooLarge: ' . $fileSize);
275 print 'ErrorFileSizeTooLarge: ' . $fileSize . ' (max ' . $fileSizeMax . ' Kb)';
276 exit;
277 }
278
279 // Hooks
280 $hookmanager->initHooks(array('document'));
281 $parameters = array('modulepart' => $modulepart, 'original_file' => $original_file,
282 'entity' => $entity, 'refname' => $refname, 'fullpath_original_file' => $fullpath_original_file,
283 'filename' => $filename, 'fullpath_original_file_osencoded' => $fullpath_original_file_osencoded);
284 $object = new stdClass();
285 $reshook = $hookmanager->executeHooks('viewImage', $parameters, $object, $action); // Note that $action and $object may have been
286 if ($reshook < 0) {
287 $errors = $hookmanager->error . (is_array($hookmanager->errors) ? (!empty($hookmanager->error) ? ', ' : '') . implode(', ', $hookmanager->errors) : '');
288 dol_syslog("document.php - Errors when executing the hook 'viewImage' : " . $errors);
289 print "ErrorViewImageHooks: " . $errors;
290 exit;
291 }
292
293 $this->action = $action;
294 $this->entity = $entity;
295 $this->filename = $filename;
296 $this->fullpath_original_file = $fullpath_original_file;
297 $this->fullpath_original_file_osencoded = $fullpath_original_file_osencoded;
298 $this->modulepart = $modulepart;
299 $this->original_file = $original_file;
300 $this->type = $type;
301 }
302
308 public function checkAccess()
309 {
310 $this->accessRight = true;
311
312 return parent::checkAccess();
313 }
314
321 public function action()
322 {
324 if (!$context->controllerInstance->checkAccess()) {
325 return -1;
326 }
327
328 //$context = Context::getInstance();
329 //$context->title = $langs->trans('WebPortalDocumentTitle');
330 //$context->desc = $langs->trans('WebPortalDocumentDesc');
331 //$context->doNotDisplayHeaderBar=1;// hide default header
332
333 $this->init();
334
335 return 1;
336 }
337
343 public function display()
344 {
345 global $conf;
346
348 if (!$context->controllerInstance->checkAccess()) {
349 $this->display404();
350 return;
351 }
352
353 // initialize
354 $modulepart = $this->modulepart;
355 $fullpath_original_file = $this->fullpath_original_file;
356 $fullpath_original_file_osencoded = $this->fullpath_original_file_osencoded;
357 $type = $this->type;
358
359 if ($modulepart == 'barcode') {
360 $generator = GETPOST("generator", "aZ09");
361 $encoding = GETPOST("encoding", "aZ09");
362 $readable = GETPOST("readable", 'aZ09') ? GETPOST("readable", "aZ09") : "Y";
363 if (in_array($encoding, array('EAN8', 'EAN13'))) {
364 $code = GETPOST("code", 'alphanohtml');
365 } else {
366 $code = GETPOST("code", 'restricthtml'); // This can be rich content (qrcode, datamatrix, ...)
367 }
368
369 // If $code is virtualcard_xxx_999.vcf, it is a file to read to get code
370 $reg = array();
371 if (preg_match('/^virtualcard_([^_]+)_(\d+)\.vcf$/', $code, $reg)) {
372 $vcffile = '';
373 $id = 0;
374 $login = '';
375 if ($reg[1] == 'user' && (int) $reg[2] > 0) {
376 $vcffile = $conf->user->dir_temp . '/' . $code;
377 $id = (int) $reg[2];
378 $tmpuser = new User($this->db);
379 $tmpuser->fetch($id);
380 $login = $tmpuser->login;
381 } elseif ($reg[1] == 'contact' && (int) $reg[2] > 0) {
382 $vcffile = $conf->contact->dir_temp . '/' . $code;
383 $id = (int) $reg[2];
384 }
385
386 $code = '';
387 if ($vcffile && $id) {
388 // Case of use of viewimage to get the barcode for user pubic profile,
389 // we must check the securekey that protet against forging url
390 if ($reg[1] == 'user' && (int) $reg[2] > 0) {
391 $encodedsecurekey = dol_hash($conf->file->instance_unique_id . 'uservirtualcard' . $id . '-' . $login, 'md5');
392 if ($encodedsecurekey != GETPOST('securekey')) {
393 $code = 'badvalueforsecurekey';
394 }
395 }
396 if (empty($code)) {
397 $code = file_get_contents($vcffile);
398 }
399 }
400 }
401
402
403 if (empty($generator) || empty($encoding)) {
404 print 'Error: Parameter "generator" or "encoding" not defined';
405 exit;
406 }
407
408 $dirbarcode = array_merge(array("/core/modules/barcode/doc/"), $conf->modules_parts['barcode']);
409
410 $result = 0;
411
412 foreach ($dirbarcode as $reldir) {
413 $dir = dol_buildpath($reldir, 0);
414 $newdir = dol_osencode($dir);
415
416 // Check if directory exists (we do not use dol_is_dir to avoid loading files.lib.php)
417 if (!is_dir($newdir)) {
418 continue;
419 }
420
421 $result = @include_once $newdir . $generator . '.modules.php';
422 if ($result) {
423 break;
424 }
425 }
426
427 // Load barcode class
428 $classname = "mod" . ucfirst($generator);
429
430 $module = new $classname($this->db);
431 '@phan-var-force ModeleBarCode $module';
433 if ($module->encodingIsSupported($encoding)) {
434 $result = $module->buildBarCode($code, $encoding, $readable);
435 }
436 } else {
437 // Open and return file
438 clearstatcache();
439
440 $filename = basename($fullpath_original_file);
441
442 // Output files on browser
443 dol_syslog("viewimage.php return file $fullpath_original_file filename=$filename content-type=$type");
444
445 if (!dol_is_file($fullpath_original_file) && !GETPOSTINT("noalt", 1)) {
446 // This test is to replace error images with a nice "notfound image" when image is not available (for example when thumbs not yet generated).
447 $fullpath_original_file = Context::getRootConfigUrl() . 'img/nophoto.png';
448 /*$error='Error: File '.$_GET["file"].' does not exists or filesystems permissions are not allowed';
449 print $error;
450 exit;*/
451 }
452
453 // Permissions are ok and file found, so we return it
454 if ($type) {
455 top_httphead($type);
456 header('Content-Disposition: inline; filename="' . basename($fullpath_original_file) . '"');
457 } else {
458 top_httphead('image/png');
459 header('Content-Disposition: inline; filename="' . basename($fullpath_original_file) . '"');
460 }
461
462 $fullpath_original_file_osencoded = dol_osencode($fullpath_original_file);
463
464 readfile($fullpath_original_file_osencoded);
465 }
466 }
467}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
static getInstance()
Singleton method to create one instance of this object.
static getRootConfigUrl()
Get WebPortal root url.
Class to manage pages.
display()
Display.
display404()
Display error template.
Class to manage Dolibarr users.
Class for ViewImageController.
checkAccess()
Check current access to controller.
action()
Action method is called before html output can be used to manage security and change context.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
dol_filesize($pathoffile)
Return size of a file.
dol_check_secure_access_document($modulepart, $original_file, $entity, $fuser=null, $refname='', $mode='read')
Security check when accessing to a document (used by document.php, viewimage.php and webservices to g...
dol_is_file($pathoffile)
Return if path is a file.
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.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
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:130
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.
dol_hash($chain, $type='0', $nosalt=0, $mode=0)
Returns a hash (non reversible encryption) of a string.