dev ldap fixes (#3914)
[openemr.git] / library / edihistory / edih_uploads.php
blob6c3b4cf75c2219d2e2ed4c923af5fe3e29733df6
1 <?PHP
3 /**
4 * edih_uploads.php
6 * @package OpenEMR
7 * @subpackage ediHistory
8 * @link https://www.open-emr.org
9 * @author Kevin McCormick
10 * @copyright Copyright (c) 2017 Kevin McCormick
11 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
15 /**
16 * Rearrange the multi-file upload array
18 * @param array $_files -- the $_FILES array
19 * @param bool $top -- do not change, for internal use in function recursion
20 * @return array
22 function edih_upload_reindex(array $_files, $top = true)
24 // blatantly copied from BigShark666 at gmail dot com 22-Nov-2011 06:51
25 // from php documentation for $_FILES predefined variable
27 $files = array();
28 foreach ($_files as $name => $file) {
29 if ($top) {
30 $sub_name = $file['name'];
31 } else {
32 $sub_name = $name;
35 if (is_array($sub_name)) {
36 foreach (array_keys($sub_name) as $key) {
37 $files[$name][$key] = array(
38 'name' => $file['name'][$key],
39 'type' => $file['type'][$key],
40 'tmp_name' => $file['tmp_name'][$key],
41 'error' => $file['error'][$key],
42 'size' => $file['size'][$key],
44 $files[$name] = edih_upload_reindex($files[$name], false);
46 } else {
47 $files[$name] = $file;
51 return $files;
54 /**
55 * select error message in case of $_FILES error
57 * @param int
58 * @return string
60 function edih_upload_err_message($code)
63 switch ($code) {
64 case UPLOAD_ERR_INI_SIZE:
65 $message = "The uploaded file exceeds the upload_max_filesize directive in php.ini";
66 break;
67 case UPLOAD_ERR_FORM_SIZE:
68 $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form";
69 break;
70 case UPLOAD_ERR_PARTIAL:
71 $message = "The uploaded file was only partially uploaded";
72 break;
73 case UPLOAD_ERR_NO_FILE:
74 $message = "No file was uploaded";
75 break;
76 case UPLOAD_ERR_NO_TMP_DIR:
77 $message = "Missing a temporary folder";
78 break;
79 case UPLOAD_ERR_CANT_WRITE:
80 $message = "Failed to write file to disk";
81 break;
82 case UPLOAD_ERR_EXTENSION:
83 $message = "File upload stopped by extension";
84 break;
86 default:
87 $message = "Unknown upload error";
88 break;
91 return $message;
94 /**
95 * Categorize and check uploaded files
97 * Files are typed and scanned, if they pass scan, then the file is moved
98 * to the edi_history temp dir and an array ['type'] ['name'] is returned
99 * If an error occurs, false is returned
101 * @uses edih_x12_file()
102 * @uses csv_file_type()
103 * @uses csv_edih_tmpdir()
104 * @param array $param_ar -- the csv_parameters("ALL") array (so we don't have to keep creating it)
105 * @param array $fidx -- individual file array from $files[$i] array
107 * @return array|bool
109 function edih_upload_match_file($param_ar, $fidx)
112 // =============
113 $edih_upldir = csv_edih_tmpdir();
114 // =============
115 $ar_fn = array();
116 $ftype = '';
118 if (is_array($fidx) && isset($fidx['name'])) {
119 $fn = basename($fidx['name']);
120 $ftmp = $fidx['tmp_name'];
121 } else {
122 csv_edihist_log('edih_upload_match_file: Error: invalid file argument');
123 return false;
126 //csv_check_x12_obj($filepath, $type='') {
127 $x12obj = new edih_x12_file($ftmp, false);
129 if ($x12obj->edih_hasGS()) {
130 $ftype = csv_file_type($x12obj->edih_type());
131 } elseif ($x12obj->edih_valid()) {
132 if (is_array($param_ar) && count($param_ar)) {
133 // csv_parameters("ALL");
134 foreach ($param_ar as $ky => $par) {
135 if (!isset($param_ar[$ky]['regex'])) {
136 continue;
139 if (preg_match($param_ar[$ky]['regex'], $fn)) {
140 $ftype = $ky;
141 break;
144 } else {
145 csv_edihist_log('edih_upload_match_file: invalid parameters');
146 return false;
148 } else {
149 // failed valdity test: unwanted characters or unmatched mime-type
150 csv_edihist_log('edih_upload_match_file: invalid x12_file ' . strip_tags($x12obj->edih_message()));
151 return false;
155 if (!$ftype) {
156 csv_edihist_log('edih_upload_match_file: unable to classify file ' . $fn);
157 $ar_fn['reject'] = array('name' => $fn, 'comment' => 'unable to classify');
158 return $ar_fn;
162 $newname = $edih_upldir . DS . $fn;
164 if (rename($ftmp, $newname)) {
165 if (chmod($newname, 0400)) {
166 $ar_fn['type'] = $ftype;
167 $ar_fn['name'] = $newname;
168 } else {
169 csv_edihist_log('edih_upload_match_file: failed to set permissions for ' . $fn);
170 $ar_fn['reject'] = array('name' => $fn, 'comment' => 'failed to set permissions');
171 unlink($newname);
172 return false;
174 } else {
175 csv_edihist_log("edih_upload_match_file: unable to move $fn to uploads directory");
176 return false;
180 return $ar_fn;
186 * Function to deal with zip archives of uploaded files
188 * This function examines the zip archive, checks for unwanted files, unpacks
189 * the files to the IBR_UPLOAD_DIR, and creates an array of file paths by the
190 * type of file
192 * @uses ibr_upload_match_file()
193 * @param string $zipfilename -- the $_FILES['file']['tmp_name'];
194 * @param array $param_ar -- the parameters array, so we don't have to create it here
195 * @param string &$html_str -- passed by reference for appending
196 * @return array $f_ar -- paths to unpacked files accepted by this function
198 function edih_ziptoarray($zipfilename, $param_ar, $single = false)
200 // note that this function moves files and set permissions, so platform issues may occur
202 $html_str = '';
203 $edih_upldir = csv_edih_tmpdir();
205 $zip_obj = new ZipArchive();
206 // open archive (ZipArchive::CHECKCONS the ZipArchive::CREATE is supposedly necessary for microsoft)
207 if ($zip_obj->open($zipfilename, ZipArchive::CHECKCONS) !== true) {
208 //$html_str .= "Error: Could not open archive $zipfilename <br />" . PHP_EOL;
209 csv_edihist_log('edih_ziptoarray: Error: Could not open archive ' . $zipfilename);
210 $f_zr['reject'][] = array('name' => $zipfilename, 'comment' => 'Error: Could not open archive ' . $zipfilename);
211 return $f_zr;
214 if ($zip_obj->status != 0) {
215 $err .= "Error code: " . text($zip_obj->status) . " " . text($zip_obj->getStatusString()) . "<br />" . PHP_EOL;
216 csv_edihist_log('edih_ziptoarray: ' . $zipfilename . ' ' . $err);
217 $f_zr['reject'][] = array('name' => $zipfilename, 'comment' => $err);
218 return $f_zr;
221 // initialize output array and counter
222 $f_zr = array();
223 $p_ct = 0;
224 // get number of files
225 $f_ct = $zip_obj->numFiles;
226 if ($single && $f_ct > 1) {
227 csv_edihist_log('edih_ziptoarray: Usage: only single zipped file accepted through this input');
228 $f_zr['reject'][] = array('name' => $zipfilename, 'comment' => 'Usage: only single zipped file accepted through this input');
229 return $f_zr;
232 // get the file names
233 for ($i = 0; $i < $f_ct; $i++) {
235 $isOK = true;
236 $fstr = "";
237 $file = $zip_obj->statIndex($i);
238 $name = $file['name'];
239 $oldCrc = $file['crc'];
240 // get file contents
241 $fstr = stream_get_contents($zip_obj->getStream($name));
242 if ($fstr) {
243 // use only the file name
244 $bnm = basename($name);
245 $newname = tempnam($edih_upldir, 'edi');
247 // extract the file to unzip tmp dir with read/write access
248 $chrs = file_put_contents($newname, $fstr);
249 // test crc
250 $newCrc = hexdec(hash_file("crc32b", $newname));
252 if ($newCrc !== $oldCrc && ($oldCrc + 4294967296) !== $newCrc) {
253 // failure case, mismatched crc file integrity values
254 $html_str .= "CRC error: The files don't match! Removing file " . text($bnm) . " <br />" . PHP_EOL;
255 $isGone = unlink($newname);
256 if ($isGone) {
257 $is_tmpzip = false;
258 $html_str .= "File Removed " . text($bnm) . "<br />" . PHP_EOL;
259 } else {
260 $html_str .= "Failed to removed file " . text($bnm) . "<br />" . PHP_EOL;
262 } else {
263 // passed the CRC test, now type and verify file
264 $fzp['name'] = $bnm;
265 $fzp['tmp_name'] = $newname;
266 // verification checks special to our application
267 $f_uplz = edih_upload_match_file($param_ar, $fzp, $html_str);
269 if (is_array($f_uplz) && count($f_uplz)) {
270 if (isset($f_uplz['reject'])) {
271 $f_zr['reject'][] = $f_uplz['reject'];
272 } elseif (isset($f_uplz['name'])) {
273 $f_zr[$f_uplz['type']][] = $f_uplz['name'];
274 $p_ct++;
276 } else {
277 // verification failed
278 $f_zr['reject'][] = array('name' => $fzp['name'], 'comment' => 'verification failed');
283 } else {
284 csv_edihist_log("Did not get file contents $name");
285 $isOK = false;
287 } // end for ($i=0; $i<$numFiles; $i++)
289 csv_edihist_log("Accepted $p_ct of $f_ct files from $zipfilename");
291 return $f_zr;
295 * Main function that handles the upload files array
297 * The return array has keys 'type' and subarray of file names
298 * relies on global $_POST and $_FILES variables
300 * @uses edih_upload_reindex()
301 * @uses edih_ziptoarray()
302 * @uses ibr_upload_match_file()
303 * @uses csv_parameters()
305 * @param string &$html_str referenced and appended to in this function
306 * @return array array of files that pass the checks and scans
308 function edih_upload_files()
311 $html_str = '';
313 // from php manual ling 03-Nov-2010 08:35
314 if (empty($_FILES) && empty($_POST) && isset($_SERVER['REQUEST_METHOD']) && strtolower($_SERVER['REQUEST_METHOD']) == 'post') {
315 $pmax = ini_get('post_max_size');
317 csv_edihist_log('edih_upload_files: Error: upload too large, max size is ' . $pmax);
318 return false;
321 if (empty($_FILES)) {
322 csv_edihist_log('Error: upload files indicated, but none received.');
323 return false;
326 // only one $_FILES array key is expected
327 $uplkey = '';
328 $uplkey = array_key_exists("fileUplMulti", $_FILES) ? "fileUplMulti" : $uplkey;
329 $uplkey = array_key_exists("fileUplx12", $_FILES) ? "fileUplx12" : $uplkey;
331 if (!$uplkey) {
332 csv_edihist_log('edih_upload_files: Error: file array name error');
333 return false;
337 if ($uplkey != "fileUplMulti") {
338 // Undefined | Multiple Files | $_FILES Corruption Attack
339 // If this request falls under any of them, treat it invalid.
340 if (!isset($_FILES[$uplkey]['error']) || is_array($_FILES[$uplkey]['error'])) {
341 csv_edihist_log('edih_upload_files: Error: file array keys error');
342 return false;
347 // these are the mime-types that we will accept -- however, mime-type is not reliable
348 // for linux, system("file -bi -- ".escapeshellarg($uploadedfile)) gives mime-type and character encoding
349 $m_types = array('application/octet-stream', 'text/plain', 'application/zip', 'application/x-zip-compressed');
351 // some unwanted file extensions that might be accidentally included in upload files
352 $ext_types = 'sh|asp|html|htm|cm|js|xml|jpg|png|tif|xpm|pdf|php|py|pl|tcl|doc|pub|ppt|xls|xla|vsd|rtf|odt|ods|odp';
353 // we get the parameters here to send to ibr_upload_match_file()
354 $param_ar = csv_parameters("ALL");
356 // initialize retained files array and counter
357 $f_ar = array();
358 $p_ct = 0;
360 // here send the $_FILES array to edih_upload_reindex for "fileUplMulti"
361 // instead of $_FILES[$uplkey] ["name"][$i] ["tmp_name"][$i] ["type"][$i] ["error"][$i] ["size"][$i]
362 // we will have $files[$uplkey][$i] ["name"]["tmp_name"]["type"]["error"]["size"]
363 if ($uplkey == "fileUplMulti") {
364 $files = edih_upload_reindex($_FILES);
365 } else {
366 $files[$uplkey][] = $_FILES[$uplkey];
370 $f_ct = count($files[$uplkey]);
371 //begin the check and processing loop
372 foreach ($files[$uplkey] as $idx => $fa) {
373 // basic php verification checks
374 if ($fa['error'] !== UPLOAD_ERR_OK) {
375 //$html_str .= "Error: [{$fa['name']}] " . edih_upload_err_message($fa['error']) . "<br />" . PHP_EOL;
376 $err = edih_upload_err_message($fa['error']);
377 $f_ar['reject'][] = array('name' => $fa['name'],'comment' => $err);
378 csv_edihist_log('edih_upload_files: _FILES error ' . $fa['name'] . ' ' . $err);
379 unset($files[$uplkey][$idx]);
380 continue;
383 if (!is_uploaded_file($fa['tmp_name'])) {
384 //$html_str .= "Error: uploaded_file error for {$fa['name']}<br />". PHP_EOL;
385 $f_ar['reject'][] = array('name' => $fa['name'],'comment' => 'php uploaded file error');
386 csv_edihist_log('edih_upload_files: _FILES error tmp_name ' . $fa['name']);
387 unset($files[$uplkey][$idx]);
388 continue;
391 if (!in_array($fa['type'], $m_types)) {
392 //$html_str .= "Error: mime-type {$fa['type']} not accepted for {$fa['name']} <br />" . PHP_EOL;
393 $f_ar['reject'][] = array('name' => $fa['name'],'comment' => 'mime-type ' . $fa['type']);
394 csv_edihist_log('edih_upload_files: _FILES error mime-type ' . $fa['name'] . ' mime-type ' . $fa['type']);
395 unset($files[$uplkey][$idx]);
396 continue;
399 // verify that we have a usable name
400 $fext = ( strpos($fa['name'], '.') ) ? pathinfo($fa['name'], PATHINFO_EXTENSION) : '';
401 if ($fext && preg_match('/' . $ext_types . '\?/i', $fext)) {
402 //$html_str .= 'Error: uploaded_file error for '.$fa['name'].' extension '.$fext.'<br />'. PHP_EOL;
403 $f_ar['reject'][] = array('name' => $fa['name'],'comment' => 'extension ' . $fext);
404 csv_edihist_log('edih_upload_files: _FILES error name ' . $fa['name'] . ' extension ' . $fext);
405 unset($files[$uplkey][$idx]);
406 continue;
409 if (is_string($fa['name'])) {
410 // check for null byte in file name, linux hidden file, directory
411 if (strpos($fa['name'], '.') === 0 || strpos($fa['name'], "\0") !== false || strpos($fa['name'], "./") !== false) {
412 //$html_str .= "Error: uploaded_file error for " . $fa['name'] . "<br />". PHP_EOL;
413 $fname = preg_replace("/[^a-zA-Z0-9_.-]/", "_", $fa['name']);
414 $f_ar['reject'][] = array('name' => $fname,'comment' => 'null byte, hidden, invalid');
415 csv_edihist_log('edih_upload_files: null byte, hidden, invalid ' . $fname);
416 unset($files[$uplkey][$idx]);
417 continue;
420 // replace spaces in file names -- should not happen, but response files from payers might have spaces
421 // $fname = preg_replace("/[^a-zA-Z0-9_.-]/","_",$fname);
422 $fa['name'] = str_replace(' ', '_', $fa['name']);
423 } else {
424 // name is not a string
425 //$html_str .= "Error: uploaded_file error for " . $fa['tmp_name'] . "<br />". PHP_EOL;
426 $f_ar['reject'][] = array('name' => (string)$fa['name'],'comment' => 'invalid name');
427 unset($files[$uplkey][$idx]);
428 continue;
431 if (!$fa['tmp_name'] || !$fa['size']) {
432 //$html_str .= "Error: file name or size error <br />" . PHP_EOL;
433 $f_ar['reject'][] = array('name' => (string)$fa['name'],'comment' => 'php file upload error');
434 unset($files[$uplkey][$idx]);
435 continue;
438 // verification checks special to our application
440 //////////////////////////////////
441 // check for zip file archive -- sent to edih_ziptoarray
443 if (strpos(strtolower($fa['name']), '.zip') || strpos($fa['type'], 'zip')) {
445 // this is a bit involved since we cannot predict how many files will be returned
446 // get an array of files from the zip unpack function"fileUplx12"
448 //if ($uplkey != "fileUplmulti") {
449 //$f_upl = edih_ziptoarray($fa['tmp_name'], $param_ar, false);
450 //} else {
451 //$f_upl = edih_ziptoarray($fa['tmp_name'], $param_ar, true);
453 $f_upl = edih_ziptoarray($fa['tmp_name'], $param_ar);
455 // put them in the correct type array
456 // expect fupl in form [type] = array(fn1, fn2, fn3, ...)
457 if (is_array($f_upl) && count($f_upl)) {
458 // $tp is file type, fz is file name
459 foreach ($f_upl as $tp => $fz) {
460 if ($tp == 'reject') {
461 if (isset($f_ar['reject']) && is_array($fz)) {
462 array_merge($f_ar['reject'], $fz);
463 } else {
464 $f_ar['reject'] = (is_array($fz)) ? $fz : array();
466 } else {
467 // expect $fz to be an array of file names
468 foreach ($fz as $zf) {
469 $f_ar[$tp][] = $zf;
470 $p_ct++;
474 } else {
475 // nothing good from edih_ziptoarray()
476 // $html_str .= "error with zip file or no files accepted for " . $fa['name'] . "<br />" .PHP_EOL;
477 $f_ar['reject'][] = array('name' => $fa['name'],'comment' => 'error with zip archive');
478 unset($files[$uplkey][$idx]);
481 // continue, since we have done everything that would happen below
482 continue;
485 //////////
486 // at this point, since we have come through all the if statements
487 // then we have:
488 // a single file under "fileUplEra"
489 // a single file under "fileUplx12"
490 // or one of possibly several files under "fileUplMulti"
491 //////////
492 $f_upl = edih_upload_match_file($param_ar, $fa);
494 if (is_array($f_upl) && count($f_upl) > 0) {
495 $f_ar[$f_upl['type']][] = $f_upl['name'];
496 $p_ct++;
497 } else {
498 // verification failed
499 csv_edihist_log('edih_upload_file: verification failed for ' . $fa['name']);
500 $f_ar['reject'][] = array('name' => $fa['name'], 'comment' => 'verification failed');
501 unset($files[$uplkey][$idx]);
503 } // end foreach($files[$uplkey] as $idx=>$fa)
505 $f_ar['remark'][] = "Received $f_ct files, accepted $p_ct" . PHP_EOL;
506 return $f_ar;
510 * Save the uploaded files array to the correct directory
512 * If a matching filename file is already in the directory it is not overwritten.
513 * The uploaded file will just be discarded.
515 * @uses csv_parameters()
516 * @see edih_upload_files()
517 * @param array $files_array files array created by edih_upload_files()
518 * @param bool -- whether to return html output
519 * @param bool -- whether to only report errors (ignored)
520 * @return string html formatted messages
522 function edih_sort_upload($files_array, $html_out = true, $err_only = true)
525 $prc_htm = '';
526 $rmk_htm = '';
527 $dirpath = csv_edih_basedir();
529 if (is_array($files_array) && count($files_array)) {
530 // we have some files
531 $p_ar = csv_parameters($type = "ALL");
533 $prc_htm .= "<p><em>Received Files</em></p>" . PHP_EOL;
534 foreach ($files_array as $key => $val) {
536 $prc_htm .= "<ul class='fupl'>" . PHP_EOL;
537 if (isset($p_ar[$key])) {
538 $tp_dir = $p_ar[$key]['directory'];
539 $tp_base = basename($tp_dir);
540 $idx = 0;
541 $prc_htm .= "<li>type " . text($key) . "</li>" . PHP_EOL;
542 if (!is_array($val) || !count($val)) {
543 $prc_htm .= "<li>no new files</li>" . PHP_EOL;
544 continue;
547 foreach ($val as $idx => $nf) {
548 // check if the file has already been stored
549 // a matching file name will not be replaced
550 $nfb = basename($nf);
551 $testname = $tp_dir . DS . $nfb;
552 $prc_htm .= "<li>" . text($nfb) . "</li>" . PHP_EOL;
553 if (is_file($testname)) {
554 $prc_htm .= "<li> -- file exists</li>" . PHP_EOL;
555 } elseif (rename($nf, $testname)) {
556 $iscm = chmod($testname, 0400);
557 if (!$iscm) {
558 // if we could write, we should be able to set permissions
559 $prc_htm .= "<li> -- file save error</li>" . PHP_EOL;
560 unlink($testname);
562 } else {
563 $prc_htm .= "<li> -- file save error</li>" . PHP_EOL;
566 } elseif ($key == 'reject') {
567 $prc_htm .= "<li><bd>Reject:</bd></li>" . PHP_EOL;
568 foreach ($val as $idx => $nf) {
569 $prc_htm .= "<li>" . text($nf['name']) . "</li>" . PHP_EOL;
570 $prc_htm .= "<li> --" . text($nf['comment']) . "</li>" . PHP_EOL;
572 } elseif ($key == 'remark') {
573 $rmk_htm .= "<p><bd>Remarks:</bd><br />" . PHP_EOL;
574 foreach ($val as $idx => $r) {
575 $rmk_htm .= text($r) . "<br />" . PHP_EOL;
578 $rmk_htm .= "</p>" . PHP_EOL;
579 } else {
580 $prc_htm .= "<li>" . text($key) . " type not stored</li>" . PHP_EOL;
581 foreach ($val as $idx => $nf) {
582 $prc_htm .= "<li>" . text(basename($nf)) . "</li>" . PHP_EOL;
586 $prc_htm .= "</ul>" . PHP_EOL;
587 $prc_htm .= $rmk_htm . PHP_EOL;
589 } else {
590 // should not happen since this function should not be called unless there are new files
591 $prc_htm .= "<ul><li>No files submitted</li></ul>" . PHP_EOL;
595 return $prc_htm;