psr fix to prior commit
[openemr.git] / library / edihistory / edih_uploads.php
blob04174c845c545d4ea2cd402289e86f37472a3758
1 <?PHP
2 /**
3 * edih_uploads.php
4 * Copyright 2012 Kevin McCormick
6 * This program is distributed in the hope that it will be useful,
7 * but WITHOUT ANY WARRANTY; without even the implied warranty of
8 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 * GNU General Public License for more details.
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; version 3 or later. You should have
14 * received a copy of the GNU General Public License along with this program;
15 * if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * <http://opensource.org/licenses/gpl-license.php>
21 * @author Kevin McCormick
22 * @link: http://www.open-emr.org
23 * @package OpenEMR
24 * @subpackage ediHistory
28 /**
29 * Rearrange the multi-file upload array
31 * @param array $_files -- the $_FILES array
32 * @param bool $top -- do not change, for internal use in function recursion
33 * @return array
35 function edih_upload_reindex(array $_files, $top = true)
37 // blatantly copied from BigShark666 at gmail dot com 22-Nov-2011 06:51
38 // from php documentation for $_FILES predefined variable
40 $files = array();
41 foreach ($_files as $name => $file) {
42 if ($top) {
43 $sub_name = $file['name'];
44 } else {
45 $sub_name = $name;
48 if (is_array($sub_name)) {
49 foreach (array_keys($sub_name) as $key) {
50 $files[$name][$key] = array(
51 'name' => $file['name'][$key],
52 'type' => $file['type'][$key],
53 'tmp_name' => $file['tmp_name'][$key],
54 'error' => $file['error'][$key],
55 'size' => $file['size'][$key],
57 $files[$name] = edih_upload_reindex($files[$name], false);
59 } else {
60 $files[$name] = $file;
64 return $files;
67 /**
68 * select error message in case of $_FILES error
70 * @param int
71 * @return string
73 function edih_upload_err_message($code)
76 switch ($code) {
77 case UPLOAD_ERR_INI_SIZE:
78 $message = "The uploaded file exceeds the upload_max_filesize directive in php.ini";
79 break;
80 case UPLOAD_ERR_FORM_SIZE:
81 $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form";
82 break;
83 case UPLOAD_ERR_PARTIAL:
84 $message = "The uploaded file was only partially uploaded";
85 break;
86 case UPLOAD_ERR_NO_FILE:
87 $message = "No file was uploaded";
88 break;
89 case UPLOAD_ERR_NO_TMP_DIR:
90 $message = "Missing a temporary folder";
91 break;
92 case UPLOAD_ERR_CANT_WRITE:
93 $message = "Failed to write file to disk";
94 break;
95 case UPLOAD_ERR_EXTENSION:
96 $message = "File upload stopped by extension";
97 break;
99 default:
100 $message = "Unknown upload error";
101 break;
104 return $message;
108 * Categorize and check uploaded files
110 * Files are typed and scanned, if they pass scan, then the file is moved
111 * to the edi_history temp dir and an array ['type'] ['name'] is returned
112 * If an error occurs, false is returned
114 * @uses edih_x12_file()
115 * @uses csv_file_type()
116 * @uses csv_edih_tmpdir()
117 * @param array $param_ar -- the csv_parameters("ALL") array (so we don't have to keep creating it)
118 * @param array $fidx -- individual file array from $files[$i] array
120 * @return array|bool
122 function edih_upload_match_file($param_ar, $fidx)
125 // =============
126 $edih_upldir = csv_edih_tmpdir();
127 // =============
128 $ar_fn = array();
129 $ftype = '';
131 if (is_array($fidx) && isset($fidx['name'])) {
132 $fn = basename($fidx['name']);
133 $ftmp = $fidx['tmp_name'];
134 } else {
135 csv_edihist_log('edih_upload_match_file: Error: invalid file argument');
136 return false;
139 //csv_check_x12_obj($filepath, $type='') {
140 $x12obj = new edih_x12_file($ftmp, false);
142 if ($x12obj->edih_hasGS()) {
143 $ftype = csv_file_type($x12obj->edih_type());
144 } elseif ($x12obj->edih_valid()) {
145 if (is_array($param_ar) && count($param_ar)) {
146 // csv_parameters("ALL");
147 foreach ($param_ar as $ky => $par) {
148 if (!isset($param_ar[$ky]['regex'])) {
149 continue;
152 if (preg_match($param_ar[$ky]['regex'], $fn)) {
153 $ftype = $ky;
154 break;
157 } else {
158 csv_edihist_log('edih_upload_match_file: invalid parameters');
159 return false;
161 } else {
162 // failed valdity test: unwanted characters or unmatched mime-type
163 csv_edihist_log('edih_upload_match_file: invalid x12_file '.strip_tags($x12obj->edih_message()));
164 return false;
168 if (!$ftype) {
169 csv_edihist_log('edih_upload_match_file: unable to classify file '.$fn);
170 $ar_fn['reject'] = array('name'=>$fn, 'comment'=>'unable to classify');
171 return $ar_fn;
175 $newname = $edih_upldir.DS.$fn;
177 if (rename($ftmp, $newname)) {
178 if (chmod($newname, 0400)) {
179 $ar_fn['type'] = $ftype;
180 $ar_fn['name'] = $newname;
181 } else {
182 csv_edihist_log('edih_upload_match_file: failed to set permissions for '.$fn);
183 $ar_fn['reject'] = array('name'=>$fn, 'comment'=>'failed to set permissions');
184 unlink($newname);
185 return false;
187 } else {
188 csv_edihist_log("edih_upload_match_file: unable to move $fn to uploads directory");
189 return false;
193 return $ar_fn;
199 * Function to deal with zip archives of uploaded files
201 * This function examines the zip archive, checks for unwanted files, unpacks
202 * the files to the IBR_UPLOAD_DIR, and creates an array of file paths by the
203 * type of file
205 * @uses ibr_upload_match_file()
206 * @param string $zipfilename -- the $_FILES['file']['tmp_name'];
207 * @param array $param_ar -- the parameters array, so we don't have to create it here
208 * @param string &$html_str -- passed by reference for appending
209 * @return array $f_ar -- paths to unpacked files accepted by this function
211 function edih_ziptoarray($zipfilename, $param_ar, $single = false)
213 // note that this function moves files and set permissions, so platform issues may occur
215 $html_str = '';
216 $edih_upldir = csv_edih_tmpdir();
218 $zip_obj = new ZipArchive();
219 // open archive (ZIPARCHIVE::CHECKCONS the ZIPARCHIVE::CREATE is supposedly necessary for microsoft)
220 if ($zip_obj->open($zipfilename, ZIPARCHIVE::CHECKCONS) !== true) {
221 //$html_str .= "Error: Could not open archive $zipfilename <br />" . PHP_EOL;
222 csv_edihist_log('edih_ziptoarray: Error: Could not open archive '.$zipfilename);
223 $f_zr['reject'][] = array('name'=>$zipfilename, 'comment'=>'Error: Could not open archive '.$zipfilename);
224 return $f_zr;
227 if ($zip_obj->status != 0) {
228 $err .= "Error code: " . $zip_obj->status ." ". $zip_obj->getStatusString() . "<br />" . PHP_EOL;
229 csv_edihist_log('edih_ziptoarray: '.$zipfilename.' '.$err);
230 $f_zr['reject'][] = array('name'=>$zipfilename, 'comment'=>$err);
231 return $f_zr;
234 // initialize output array and counter
235 $f_zr = array();
236 $p_ct = 0;
237 // get number of files
238 $f_ct = $zip_obj->numFiles;
239 if ($single && $f_ct > 1) {
240 csv_edihist_log('edih_ziptoarray: Usage: only single zipped file accepted through this input');
241 $f_zr['reject'][] = array('name'=>$zipfilename, 'comment'=>'Usage: only single zipped file accepted through this input');
242 return $f_zr;
245 // get the file names
246 for ($i=0; $i<$f_ct; $i++) {
248 $isOK = true;
249 $fstr = "";
250 $file = $zip_obj->statIndex($i);
251 $name = $file['name'];
252 $oldCrc = $file['crc'];
253 // get file contents
254 $fstr = stream_get_contents($zip_obj->getStream($name));
255 if ($fstr) {
256 // use only the file name
257 $bnm = basename($name);
258 $newname = tempnam($edih_upldir, 'edi');
260 // extract the file to unzip tmp dir with read/write access
261 $chrs = file_put_contents($newname, $fstr);
262 // test crc
263 $newCrc = hexdec(hash_file("crc32b", $newname));
265 if ($newCrc !== $oldCrc && ($oldCrc + 4294967296) !== $newCrc) {
266 // failure case, mismatched crc file integrity values
267 $html_str .= "CRC error: The files don't match! Removing file $bnm <br />" . PHP_EOL;
268 $isGone = unlink($newname);
269 if ($isGone) {
270 $is_tmpzip = false;
271 $html_str .= "File Removed $bnm<br />".PHP_EOL;
272 } else {
273 $html_str .= "Failed to removed file $bnm<br />".PHP_EOL;
275 } else {
276 // passed the CRC test, now type and verify file
277 $fzp['name'] = $bnm;
278 $fzp['tmp_name'] = $newname;
279 // verification checks special to our application
280 $f_uplz = edih_upload_match_file($param_ar, $fzp, $html_str);
282 if (is_array($f_uplz) && count($f_uplz)) {
283 if (isset($f_uplz['reject'])) {
284 $f_zr['reject'][] = $f_uplz['reject'];
285 } elseif (isset($f_uplz['name'])) {
286 $f_zr[$f_uplz['type']][] = $f_uplz['name'];
287 $p_ct++;
289 } else {
290 // verification failed
291 $f_zr['reject'][] = array('name'=>$fzp['name'], 'comment'=>'verification failed');
296 } else {
297 csv_edihist_log("Did not get file contents $name");
298 $isOK = false;
300 } // end for ($i=0; $i<$numFiles; $i++)
302 csv_edihist_log("Accepted $p_ct of $f_ct files from $zipfilename");
304 return $f_zr;
308 * Main function that handles the upload files array
310 * The return array has keys 'type' and subarray of file names
311 * relies on global $_POST and $_FILES variables
313 * @uses edih_upload_reindex()
314 * @uses edih_ziptoarray()
315 * @uses ibr_upload_match_file()
316 * @uses csv_parameters()
318 * @param string &$html_str referenced and appended to in this function
319 * @return array array of files that pass the checks and scans
321 function edih_upload_files()
324 $html_str = '';
326 // from php manual ling 03-Nov-2010 08:35
327 if (empty($_FILES) && empty($_POST) && isset($_SERVER['REQUEST_METHOD']) && strtolower($_SERVER['REQUEST_METHOD']) == 'post') {
328 $pmax = ini_get('post_max_size');
330 csv_edihist_log('edih_upload_files: Error: upload too large, max size is '.$pmax);
331 return false;
334 if (empty($_FILES)) {
335 csv_edihist_log('Error: upload files indicated, but none received.');
336 return false;
339 // only one $_FILES array key is expected
340 $uplkey = '';
341 $uplkey = array_key_exists("fileUplMulti", $_FILES) ? "fileUplMulti" : $uplkey;
342 $uplkey = array_key_exists("fileUplx12", $_FILES) ? "fileUplx12" : $uplkey;
344 if (!$uplkey) {
345 csv_edihist_log('edih_upload_files: Error: file array name error');
346 return false;
350 if ($uplkey != "fileUplMulti") {
351 // Undefined | Multiple Files | $_FILES Corruption Attack
352 // If this request falls under any of them, treat it invalid.
353 if (!isset($_FILES[$uplkey]['error']) || is_array($_FILES[$uplkey]['error'])) {
354 csv_edihist_log('edih_upload_files: Error: file array keys error');
355 return false;
360 // these are the mime-types that we will accept -- however, mime-type is not reliable
361 // for linux, system("file -bi -- ".escapeshellarg($uploadedfile)) gives mime-type and character encoding
362 $m_types = array('application/octet-stream', 'text/plain', 'application/zip', 'application/x-zip-compressed');
364 // some unwanted file extensions that might be accidentally included in upload files
365 $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';
366 // we get the parameters here to send to ibr_upload_match_file()
367 $param_ar = csv_parameters("ALL");
369 // initialize retained files array and counter
370 $f_ar = array();
371 $p_ct = 0;
373 // here send the $_FILES array to edih_upload_reindex for "fileUplMulti"
374 // instead of $_FILES[$uplkey] ["name"][$i] ["tmp_name"][$i] ["type"][$i] ["error"][$i] ["size"][$i]
375 // we will have $files[$uplkey][$i] ["name"]["tmp_name"]["type"]["error"]["size"]
376 if ($uplkey == "fileUplMulti") {
377 $files = edih_upload_reindex($_FILES);
378 } else {
379 $files[$uplkey][] = $_FILES[$uplkey];
383 $f_ct = count($files[$uplkey]);
384 //begin the check and processing loop
385 foreach ($files[$uplkey] as $idx => $fa) {
386 // basic php verification checks
387 if ($fa['error'] !== UPLOAD_ERR_OK) {
388 //$html_str .= "Error: [{$fa['name']}] " . edih_upload_err_message($fa['error']) . "<br />" . PHP_EOL;
389 $err = edih_upload_err_message($fa['error']);
390 $f_ar['reject'][] = array('name'=>$fa['name'],'comment'=>$err);
391 csv_edihist_log('edih_upload_files: _FILES error '.$fa['name'].' '.$err);
392 unset($files[$uplkey][$idx]);
393 continue;
396 if (!is_uploaded_file($fa['tmp_name'])) {
397 //$html_str .= "Error: uploaded_file error for {$fa['name']}<br />". PHP_EOL;
398 $f_ar['reject'][] = array('name'=>$fa['name'],'comment'=>'php uploaded file error');
399 csv_edihist_log('edih_upload_files: _FILES error tmp_name '.$fa['name']);
400 unset($files[$uplkey][$idx]);
401 continue;
404 if (!in_array($fa['type'], $m_types)) {
405 //$html_str .= "Error: mime-type {$fa['type']} not accepted for {$fa['name']} <br />" . PHP_EOL;
406 $f_ar['reject'][] = array('name'=>$fa['name'],'comment'=>'mime-type '.$fa['type']);
407 csv_edihist_log('edih_upload_files: _FILES error mime-type '.$fa['name'].' mime-type '.$fa['type']);
408 unset($files[$uplkey][$idx]);
409 continue;
412 // verify that we have a usable name
413 $fext = ( strpos($fa['name'], '.') ) ? pathinfo($fa['name'], PATHINFO_EXTENSION) : '';
414 if ($fext && preg_match('/'.$ext_types.'\?/i', $fext)) {
415 //$html_str .= 'Error: uploaded_file error for '.$fa['name'].' extension '.$fext.'<br />'. PHP_EOL;
416 $f_ar['reject'][] = array('name'=>$fa['name'],'comment'=>'extension '.$fext);
417 csv_edihist_log('edih_upload_files: _FILES error name '.$fa['name'].' extension '.$fext);
418 unset($files[$uplkey][$idx]);
419 continue;
422 if (is_string($fa['name'])) {
423 // check for null byte in file name, linux hidden file, directory
424 if (strpos($fa['name'], '.') === 0 || strpos($fa['name'], "\0") !== false || strpos($fa['name'], "./") !== false) {
425 //$html_str .= "Error: uploaded_file error for " . $fa['name'] . "<br />". PHP_EOL;
426 $fname = preg_replace("/[^a-zA-Z0-9_.-]/", "_", $fa['name']);
427 $f_ar['reject'][] = array('name'=>$fname,'comment'=>'null byte, hidden, invalid');
428 csv_edihist_log('edih_upload_files: null byte, hidden, invalid '.$fname);
429 unset($files[$uplkey][$idx]);
430 continue;
433 // replace spaces in file names -- should not happen, but response files from payers might have spaces
434 // $fname = preg_replace("/[^a-zA-Z0-9_.-]/","_",$fname);
435 $fa['name'] = str_replace(' ', '_', $fa['name']);
436 } else {
437 // name is not a string
438 //$html_str .= "Error: uploaded_file error for " . $fa['tmp_name'] . "<br />". PHP_EOL;
439 $f_ar['reject'][] = array('name'=>(string)$fa['name'],'comment'=>'invalid name');
440 unset($files[$uplkey][$idx]);
441 continue;
444 if (!$fa['tmp_name'] || !$fa['size']) {
445 //$html_str .= "Error: file name or size error <br />" . PHP_EOL;
446 $f_ar['reject'][] = array('name'=>(string)$fa['name'],'comment'=>'php file upload error');
447 unset($files[$uplkey][$idx]);
448 continue;
451 // verification checks special to our application
453 //////////////////////////////////
454 // check for zip file archive -- sent to edih_ziptoarray
456 if (strpos(strtolower($fa['name']), '.zip') || strpos($fa['type'], 'zip')) {
458 // this is a bit involved since we cannot predict how many files will be returned
459 // get an array of files from the zip unpack function"fileUplx12"
461 //if ($uplkey != "fileUplmulti") {
462 //$f_upl = edih_ziptoarray($fa['tmp_name'], $param_ar, false);
463 //} else {
464 //$f_upl = edih_ziptoarray($fa['tmp_name'], $param_ar, true);
466 $f_upl = edih_ziptoarray($fa['tmp_name'], $param_ar);
468 // put them in the correct type array
469 // expect fupl in form [type] = array(fn1, fn2, fn3, ...)
470 if (is_array($f_upl) && count($f_upl)) {
471 // $tp is file type, fz is file name
472 foreach ($f_upl as $tp => $fz) {
473 if ($tp == 'reject') {
474 if (isset($f_ar['reject']) && is_array($fz)) {
475 array_merge($f_ar['reject'], $fz);
476 } else {
477 $f_ar['reject'] = (is_array($fz)) ? $fz : array();
479 } else {
480 // expect $fz to be an array of file names
481 foreach ($fz as $zf) {
482 $f_ar[$tp][] = $zf;
483 $p_ct ++;
487 } else {
488 // nothing good from edih_ziptoarray()
489 // $html_str .= "error with zip file or no files accepted for " . $fa['name'] . "<br />" .PHP_EOL;
490 $f_ar['reject'][] = array('name'=>$fa['name'],'comment'=>'error with zip archive');
491 unset($files[$uplkey][$idx]);
494 // continue, since we have done everything that would happen below
495 continue;
498 //////////
499 // at this point, since we have come through all the if statements
500 // then we have:
501 // a single file under "fileUplEra"
502 // a single file under "fileUplx12"
503 // or one of possibly several files under "fileUplMulti"
504 //////////
505 $f_upl = edih_upload_match_file($param_ar, $fa);
507 if (is_array($f_upl) && count($f_upl) > 0) {
508 $f_ar[$f_upl['type']][] = $f_upl['name'];
509 $p_ct++;
510 } else {
511 // verification failed
512 csv_edihist_log('edih_upload_file: verification failed for '. $fa['name']);
513 $f_ar['reject'][] = array('name'=>$fa['name'], 'comment'=>'verification failed');
514 unset($files[$uplkey][$idx]);
516 } // end foreach($files[$uplkey] as $idx=>$fa)
518 $f_ar['remark'][] = "Received $f_ct files, accepted $p_ct" . PHP_EOL;
519 return $f_ar;
523 * Save the uploaded files array to the correct directory
525 * If a matching filename file is already in the directory it is not overwritten.
526 * The uploaded file will just be discarded.
528 * @uses csv_parameters()
529 * @see edih_upload_files()
530 * @param array $files_array files array created by edih_upload_files()
531 * @param bool -- whether to return html output
532 * @param bool -- whether to only report errors (ignored)
533 * @return string html formatted messages
535 function edih_sort_upload($files_array, $html_out = true, $err_only = true)
538 $prc_htm = '';
539 $rmk_htm = '';
540 $dirpath = csv_edih_basedir();
542 if (is_array($files_array) && count($files_array)) {
543 // we have some files
544 $p_ar = csv_parameters($type = "ALL");
546 $prc_htm .= "<p><em>Received Files</em></p>".PHP_EOL;
547 foreach ($files_array as $key => $val) {
549 $prc_htm .= "<ul class='fupl'>".PHP_EOL;
550 if (isset($p_ar[$key])) {
551 $tp_dir = $p_ar[$key]['directory'];
552 $tp_base = basename($tp_dir);
553 $idx = 0;
554 $prc_htm .= "<li>type $key</li>".PHP_EOL;
555 if (!is_array($val) || !count($val)) {
556 $prc_htm .= "<li>no new files</li>" . PHP_EOL;
557 continue;
560 foreach ($val as $idx => $nf) {
561 // check if the file has already been stored
562 // a matching file name will not be replaced
563 $nfb = basename($nf);
564 $testname = $tp_dir.DS.$nfb;
565 $prc_htm .= "<li>$nfb</li>".PHP_EOL;
566 if (is_file($testname)) {
567 $prc_htm .= "<li> -- file exists</li>" .PHP_EOL;
568 } elseif (rename($nf, $testname)) {
569 $iscm = chmod($testname, 0400);
570 if (!$iscm) {
571 // if we could write, we should be able to set permissions
572 $prc_htm .= "<li> -- file save error</li>" .PHP_EOL;
573 unlink($testname);
575 } else {
576 $prc_htm .= "<li> -- file save error</li>" .PHP_EOL;
579 } elseif ($key == 'reject') {
580 $prc_htm .= "<li><bd>Reject:</bd></li>".PHP_EOL;
581 foreach ($val as $idx => $nf) {
582 $prc_htm .= "<li>".$nf['name']."</li>".PHP_EOL;
583 $prc_htm .= "<li> --".$nf['comment']."</li>".PHP_EOL;
585 } elseif ($key == 'remark') {
586 $rmk_htm .= "<p><bd>Remarks:</bd><br>".PHP_EOL;
587 foreach ($val as $idx => $r) {
588 $rmk_htm .= $r."<br>".PHP_EOL;
591 $rmk_htm .= "</p>".PHP_EOL;
592 } else {
593 $prc_htm .= "<li>$key type not stored</li>".PHP_EOL;
594 foreach ($val as $idx => $nf) {
595 $prc_htm .= "<li>".basename($nf)."</li>".PHP_EOL;
599 $prc_htm .= "</ul>".PHP_EOL;
600 $prc_htm .= $rmk_htm.PHP_EOL;
602 } else {
603 // should not happen since this function should not be called unless there are new files
604 $prc_htm .= "<ul><li>No files submitted</li></ul>" . PHP_EOL;
608 return $prc_htm;