Merge pull request #19225 from colemanw/select2Tweak
[civicrm-core.git] / CRM / Utils / File.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * class to provide simple static functions for file objects
20 */
21 class CRM_Utils_File {
22
23 /**
24 * Given a file name, determine if the file contents make it an ascii file
25 *
26 * @param string $name
27 * Name of file.
28 *
29 * @return bool
30 * true if file is ascii
31 */
32 public static function isAscii($name) {
33 $fd = fopen($name, "r");
34 if (!$fd) {
35 return FALSE;
36 }
37
38 $ascii = TRUE;
39 while (!feof($fd)) {
40 $line = fgets($fd, 8192);
41 if (!CRM_Utils_String::isAscii($line)) {
42 $ascii = FALSE;
43 break;
44 }
45 }
46
47 fclose($fd);
48 return $ascii;
49 }
50
51 /**
52 * Given a file name, determine if the file contents make it an html file
53 *
54 * @param string $name
55 * Name of file.
56 *
57 * @return bool
58 * true if file is html
59 */
60 public static function isHtml($name) {
61 $fd = fopen($name, "r");
62 if (!$fd) {
63 return FALSE;
64 }
65
66 $html = FALSE;
67 $lineCount = 0;
68 while (!feof($fd) & $lineCount <= 5) {
69 $lineCount++;
70 $line = fgets($fd, 8192);
71 if (!CRM_Utils_String::isHtml($line)) {
72 $html = TRUE;
73 break;
74 }
75 }
76
77 fclose($fd);
78 return $html;
79 }
80
81 /**
82 * Create a directory given a path name, creates parent directories
83 * if needed
84 *
85 * @param string $path
86 * The path name.
87 * @param bool $abort
88 * Should we abort or just return an invalid code.
89 * @return bool|NULL
90 * NULL: Folder already exists or was not specified.
91 * TRUE: Creation succeeded.
92 * FALSE: Creation failed.
93 */
94 public static function createDir($path, $abort = TRUE) {
95 if (is_dir($path) || empty($path)) {
96 return NULL;
97 }
98
99 CRM_Utils_File::createDir(dirname($path), $abort);
100 if (@mkdir($path, 0777) == FALSE) {
101 if ($abort) {
102 $docLink = CRM_Utils_System::docURL2('Moving an Existing Installation to a New Server or Location', NULL, NULL, NULL, NULL, "wiki");
103 echo "Error: Could not create directory: $path.<p>If you have moved an existing CiviCRM installation from one location or server to another there are several steps you will need to follow. They are detailed on this CiviCRM wiki page - {$docLink}. A fix for the specific problem that caused this error message to be displayed is to set the value of the config_backend column in the civicrm_domain table to NULL. However we strongly recommend that you review and follow all the steps in that document.</p>";
104
105 CRM_Utils_System::civiExit();
106 }
107 else {
108 return FALSE;
109 }
110 }
111 return TRUE;
112 }
113
114 /**
115 * Delete a directory given a path name, delete children directories
116 * and files if needed
117 *
118 * @param string $target
119 * The path name.
120 * @param bool $rmdir
121 * @param bool $verbose
122 *
123 * @throws Exception
124 */
125 public static function cleanDir($target, $rmdir = TRUE, $verbose = TRUE) {
126 static $exceptions = ['.', '..'];
127 if ($target == '' || $target == '/' || !$target) {
128 throw new Exception("Overly broad deletion");
129 }
130
131 if ($dh = @opendir($target)) {
132 while (FALSE !== ($sibling = readdir($dh))) {
133 if (!in_array($sibling, $exceptions)) {
134 $object = $target . DIRECTORY_SEPARATOR . $sibling;
135
136 if (is_dir($object)) {
137 CRM_Utils_File::cleanDir($object, $rmdir, $verbose);
138 }
139 elseif (is_file($object)) {
140 if (!unlink($object)) {
141 CRM_Core_Session::setStatus(ts('Unable to remove file %1', [1 => $object]), ts('Warning'), 'error');
142 }
143 }
144 }
145 }
146 closedir($dh);
147
148 if ($rmdir) {
149 if (rmdir($target)) {
150 if ($verbose) {
151 CRM_Core_Session::setStatus(ts('Removed directory %1', [1 => $target]), '', 'success');
152 }
153 return TRUE;
154 }
155 else {
156 CRM_Core_Session::setStatus(ts('Unable to remove directory %1', [1 => $target]), ts('Warning'), 'error');
157 }
158 }
159 }
160 }
161
162 /**
163 * Concatenate several files.
164 *
165 * @param array $files
166 * List of file names.
167 * @param string $delim
168 * An optional delimiter to put between files.
169 * @return string
170 */
171 public static function concat($files, $delim = '') {
172 $buf = '';
173 $first = TRUE;
174 foreach ($files as $file) {
175 if (!$first) {
176 $buf .= $delim;
177 }
178 $buf .= file_get_contents($file);
179 $first = FALSE;
180 }
181 return $buf;
182 }
183
184 /**
185 * @param string $source
186 * @param string $destination
187 */
188 public static function copyDir($source, $destination) {
189 if ($dh = opendir($source)) {
190 @mkdir($destination);
191 while (FALSE !== ($file = readdir($dh))) {
192 if (($file != '.') && ($file != '..')) {
193 if (is_dir($source . DIRECTORY_SEPARATOR . $file)) {
194 CRM_Utils_File::copyDir($source . DIRECTORY_SEPARATOR . $file, $destination . DIRECTORY_SEPARATOR . $file);
195 }
196 else {
197 copy($source . DIRECTORY_SEPARATOR . $file, $destination . DIRECTORY_SEPARATOR . $file);
198 }
199 }
200 }
201 closedir($dh);
202 }
203 }
204
205 /**
206 * Given a file name, recode it (in place!) to UTF-8
207 *
208 * @param string $name
209 * Name of file.
210 *
211 * @return bool
212 * whether the file was recoded properly
213 */
214 public static function toUtf8($name) {
215 static $config = NULL;
216 static $legacyEncoding = NULL;
217 if ($config == NULL) {
218 $config = CRM_Core_Config::singleton();
219 $legacyEncoding = $config->legacyEncoding;
220 }
221
222 if (!function_exists('iconv')) {
223
224 return FALSE;
225
226 }
227
228 $contents = file_get_contents($name);
229 if ($contents === FALSE) {
230 return FALSE;
231 }
232
233 $contents = iconv($legacyEncoding, 'UTF-8', $contents);
234 if ($contents === FALSE) {
235 return FALSE;
236 }
237
238 $file = fopen($name, 'w');
239 if ($file === FALSE) {
240 return FALSE;
241 }
242
243 $written = fwrite($file, $contents);
244 $closed = fclose($file);
245 if ($written === FALSE or !$closed) {
246 return FALSE;
247 }
248
249 return TRUE;
250 }
251
252 /**
253 * Appends a slash to the end of a string if it doesn't already end with one
254 *
255 * @param string $path
256 * @param string $slash
257 *
258 * @return string
259 */
260 public static function addTrailingSlash($path, $slash = NULL) {
261 if (!$slash) {
262 // FIXME: Defaulting to backslash on windows systems can produce
263 // unexpected results, esp for URL strings which should always use forward-slashes.
264 // I think this fn should default to forward-slash instead.
265 $slash = DIRECTORY_SEPARATOR;
266 }
267 if (!in_array(substr($path, -1, 1), ['/', '\\'])) {
268 $path .= $slash;
269 }
270 return $path;
271 }
272
273 /**
274 * Save a fake file somewhere
275 *
276 * @param string $dir
277 * The directory where the file should be saved.
278 * @param string $contents
279 * Optional: the contents of the file.
280 * @param string $fileName
281 *
282 * @return string
283 * The filename saved, or FALSE on failure.
284 */
285 public static function createFakeFile($dir, $contents = 'delete me', $fileName = NULL) {
286 $dir = self::addTrailingSlash($dir);
287 if (!$fileName) {
288 $fileName = 'delete-this-' . CRM_Utils_String::createRandom(10, CRM_Utils_String::ALPHANUMERIC);
289 }
290 $success = @file_put_contents($dir . $fileName, $contents);
291
292 return ($success === FALSE) ? FALSE : $fileName;
293 }
294
295 /**
296 * @param string|NULL $dsn
297 * Use NULL to load the default/active connection from CRM_Core_DAO.
298 * Otherwise, give a full DSN string.
299 * @param string $fileName
300 * @param string $prefix
301 * @param bool $dieOnErrors
302 */
303 public static function sourceSQLFile($dsn, $fileName, $prefix = NULL, $dieOnErrors = TRUE) {
304 if (FALSE === file_get_contents($fileName)) {
305 // Our file cannot be found.
306 // Using 'die' here breaks this on extension upgrade.
307 throw new CRM_Core_Exception('Could not find the SQL file.');
308 }
309
310 self::runSqlQuery($dsn, file_get_contents($fileName), $prefix, $dieOnErrors);
311 }
312
313 /**
314 *
315 * @param string|NULL $dsn
316 * @param string $queryString
317 * @param string $prefix
318 * @param bool $dieOnErrors
319 */
320 public static function runSqlQuery($dsn, $queryString, $prefix = NULL, $dieOnErrors = TRUE) {
321 $string = $prefix . $queryString;
322
323 if ($dsn === NULL) {
324 $db = CRM_Core_DAO::getConnection();
325 }
326 else {
327 require_once 'DB.php';
328 $dsn = CRM_Utils_SQL::autoSwitchDSN($dsn);
329 $db = DB::connect($dsn);
330 }
331
332 if (PEAR::isError($db)) {
333 die("Cannot open $dsn: " . $db->getMessage());
334 }
335 $db->query('SET NAMES utf8mb4');
336 $transactionId = CRM_Utils_Type::escape(CRM_Utils_Request::id(), 'String');
337 $db->query('SET @uniqueID = ' . "'$transactionId'");
338
339 // get rid of comments starting with # and --
340
341 $string = self::stripComments($string);
342
343 $queries = preg_split('/;\s*$/m', $string);
344 foreach ($queries as $query) {
345 $query = trim($query);
346 if (!empty($query)) {
347 CRM_Core_Error::debug_query($query);
348 $res = &$db->query($query);
349 if (PEAR::isError($res)) {
350 if ($dieOnErrors) {
351 die("Cannot execute $query: " . $res->getMessage());
352 }
353 else {
354 echo "Cannot execute $query: " . $res->getMessage() . "<p>";
355 }
356 }
357 }
358 }
359 }
360
361 /**
362 *
363 * Strips comment from a possibly multiline SQL string
364 *
365 * @param string $string
366 *
367 * @return string
368 * stripped string
369 */
370 public static function stripComments($string) {
371 return preg_replace("/^(#|--).*\R*/m", "", $string);
372 }
373
374 /**
375 * @param $ext
376 *
377 * @return bool
378 */
379 public static function isExtensionSafe($ext) {
380 static $extensions = NULL;
381 if (!$extensions) {
382 $extensions = CRM_Core_OptionGroup::values('safe_file_extension', TRUE);
383
384 // make extensions to lowercase
385 $extensions = array_change_key_case($extensions, CASE_LOWER);
386 // allow html/htm extension ONLY if the user is admin
387 // and/or has access CiviMail
388 if (!(CRM_Core_Permission::check('access CiviMail') ||
389 CRM_Core_Permission::check('administer CiviCRM') ||
390 (CRM_Mailing_Info::workflowEnabled() &&
391 CRM_Core_Permission::check('create mailings')
392 )
393 )
394 ) {
395 unset($extensions['html']);
396 unset($extensions['htm']);
397 }
398 }
399 // support lower and uppercase file extensions
400 return (bool) isset($extensions[strtolower($ext)]);
401 }
402
403 /**
404 * Determine whether a given file is listed in the PHP include path.
405 *
406 * @param string $name
407 * Name of file.
408 *
409 * @return bool
410 * whether the file can be include()d or require()d
411 */
412 public static function isIncludable($name) {
413 $x = @fopen($name, 'r', TRUE);
414 if ($x) {
415 fclose($x);
416 return TRUE;
417 }
418 else {
419 return FALSE;
420 }
421 }
422
423 /**
424 * Remove the 32 bit md5 we add to the fileName also remove the unknown tag if we added it.
425 *
426 * @param $name
427 *
428 * @return mixed
429 */
430 public static function cleanFileName($name) {
431 // replace the last 33 character before the '.' with null
432 $name = preg_replace('/(_[\w]{32})\./', '.', $name);
433 return $name;
434 }
435
436 /**
437 * Make a valid file name.
438 *
439 * @param string $name
440 *
441 * @return string
442 */
443 public static function makeFileName($name) {
444 $uniqID = md5(uniqid(rand(), TRUE));
445 $info = pathinfo($name);
446 $basename = substr($info['basename'],
447 0, -(strlen(CRM_Utils_Array::value('extension', $info)) + (CRM_Utils_Array::value('extension', $info) == '' ? 0 : 1))
448 );
449 if (!self::isExtensionSafe(CRM_Utils_Array::value('extension', $info))) {
450 // munge extension so it cannot have an embbeded dot in it
451 // The maximum length of a filename for most filesystems is 255 chars.
452 // We'll truncate at 240 to give some room for the extension.
453 return CRM_Utils_String::munge("{$basename}_" . CRM_Utils_Array::value('extension', $info) . "_{$uniqID}", '_', 240) . ".unknown";
454 }
455 else {
456 return CRM_Utils_String::munge("{$basename}_{$uniqID}", '_', 240) . "." . CRM_Utils_Array::value('extension', $info);
457 }
458 }
459
460 /**
461 * Copies a file
462 *
463 * @param $filePath
464 * @return mixed
465 */
466 public static function duplicate($filePath) {
467 $oldName = pathinfo($filePath, PATHINFO_FILENAME);
468 $uniqID = md5(uniqid(rand(), TRUE));
469 $newName = preg_replace('/(_[\w]{32})$/', '', $oldName) . '_' . $uniqID;
470 $newPath = str_replace($oldName, $newName, $filePath);
471 copy($filePath, $newPath);
472 return $newPath;
473 }
474
475 /**
476 * Get files for the extension.
477 *
478 * @param string $path
479 * @param string $ext
480 *
481 * @return array
482 */
483 public static function getFilesByExtension($path, $ext) {
484 $path = self::addTrailingSlash($path);
485 $files = [];
486 if ($dh = opendir($path)) {
487 while (FALSE !== ($elem = readdir($dh))) {
488 if (substr($elem, -(strlen($ext) + 1)) == '.' . $ext) {
489 $files[] .= $path . $elem;
490 }
491 }
492 closedir($dh);
493 }
494 return $files;
495 }
496
497 /**
498 * Restrict access to a given directory (by planting there a restrictive .htaccess file)
499 *
500 * @param string $dir
501 * The directory to be secured.
502 * @param bool $overwrite
503 */
504 public static function restrictAccess($dir, $overwrite = FALSE) {
505 // note: empty value for $dir can play havoc, since that might result in putting '.htaccess' to root dir
506 // of site, causing site to stop functioning.
507 // FIXME: we should do more checks here -
508 if (!empty($dir) && is_dir($dir)) {
509 $htaccess = <<<HTACCESS
510 <Files "*">
511 # Apache 2.2
512 <IfModule !authz_core_module>
513 Order allow,deny
514 Deny from all
515 </IfModule>
516
517 # Apache 2.4+
518 <IfModule authz_core_module>
519 Require all denied
520 </IfModule>
521 </Files>
522
523 HTACCESS;
524 $file = $dir . '.htaccess';
525 if ($overwrite || !file_exists($file)) {
526 if (file_put_contents($file, $htaccess) === FALSE) {
527 CRM_Core_Error::movedSiteError($file);
528 }
529 }
530 }
531 }
532
533 /**
534 * Restrict remote users from browsing the given directory.
535 *
536 * @param $publicDir
537 */
538 public static function restrictBrowsing($publicDir) {
539 if (!is_dir($publicDir) || !is_writable($publicDir)) {
540 return;
541 }
542
543 // base dir
544 $nobrowse = realpath($publicDir) . '/index.html';
545 if (!file_exists($nobrowse)) {
546 @file_put_contents($nobrowse, '');
547 }
548
549 // child dirs
550 $dir = new RecursiveDirectoryIterator($publicDir);
551 foreach ($dir as $name => $object) {
552 if (is_dir($name) && $name != '..') {
553 $nobrowse = realpath($name) . '/index.html';
554 if (!file_exists($nobrowse)) {
555 @file_put_contents($nobrowse, '');
556 }
557 }
558 }
559 }
560
561 /**
562 * (Deprecated) Create the file-path from which all other internal paths are
563 * computed. This implementation determines it as `dirname(CIVICRM_TEMPLATE_COMPILEDIR)`.
564 *
565 * This approach is problematic - e.g. it prevents one from authentically
566 * splitting the CIVICRM_TEMPLATE_COMPILEDIR away from other dirs. The implementation
567 * is preserved for backwards compatibility (and should only be called by
568 * CMS-adapters and by Civi\Core\Paths).
569 *
570 * Do not use it for new path construction logic. Instead, use Civi::paths().
571 *
572 * @deprecated
573 * @see \Civi::paths()
574 * @see \Civi\Core\Paths
575 */
576 public static function baseFilePath() {
577 static $_path = NULL;
578 if (!$_path) {
579 // Note: Don't rely on $config; that creates a dependency loop.
580 if (!defined('CIVICRM_TEMPLATE_COMPILEDIR')) {
581 throw new RuntimeException("Undefined constant: CIVICRM_TEMPLATE_COMPILEDIR");
582 }
583 $templateCompileDir = CIVICRM_TEMPLATE_COMPILEDIR;
584
585 $path = dirname($templateCompileDir);
586
587 //this fix is to avoid creation of upload dirs inside templates_c directory
588 $checkPath = explode(DIRECTORY_SEPARATOR, $path);
589
590 $cnt = count($checkPath) - 1;
591 if ($checkPath[$cnt] == 'templates_c') {
592 unset($checkPath[$cnt]);
593 $path = implode(DIRECTORY_SEPARATOR, $checkPath);
594 }
595
596 $_path = CRM_Utils_File::addTrailingSlash($path);
597 }
598 return $_path;
599 }
600
601 /**
602 * Determine if a path is absolute.
603 *
604 * @param string $path
605 *
606 * @return bool
607 * TRUE if absolute. FALSE if relative.
608 */
609 public static function isAbsolute($path) {
610 if (substr($path, 0, 1) === DIRECTORY_SEPARATOR) {
611 return TRUE;
612 }
613 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
614 if (preg_match('!^[a-zA-Z]:[/\\\\]!', $path)) {
615 return TRUE;
616 }
617 }
618 return FALSE;
619 }
620
621 /**
622 * @param $directory
623 *
624 * @return string
625 * @deprecated
626 * Computation of a relative path requires some base.
627 * This implementation is problematic because it relies on an
628 * implicit base which was constructed problematically.
629 */
630 public static function relativeDirectory($directory) {
631 // Do nothing on windows
632 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
633 return $directory;
634 }
635
636 // check if directory is relative, if so return immediately
637 if (!self::isAbsolute($directory)) {
638 return $directory;
639 }
640
641 // make everything relative from the baseFilePath
642 $basePath = self::baseFilePath();
643 // check if basePath is a substr of $directory, if so
644 // return rest of string
645 if (substr($directory, 0, strlen($basePath)) == $basePath) {
646 return substr($directory, strlen($basePath));
647 }
648
649 // return the original value
650 return $directory;
651 }
652
653 /**
654 * @param $directory
655 * @param string $basePath
656 * The base path when evaluating relative paths. Should include trailing slash.
657 *
658 * @return string
659 */
660 public static function absoluteDirectory($directory, $basePath) {
661 // check if directory is already absolute, if so return immediately
662 // Note: Windows PHP accepts any mix of "/" or "\", so "C:\htdocs" or "C:/htdocs" would be a valid absolute path
663 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && preg_match(';^[a-zA-Z]:[/\\\\];', $directory)) {
664 return $directory;
665 }
666
667 // check if directory is already absolute, if so return immediately
668 if (substr($directory, 0, 1) == DIRECTORY_SEPARATOR) {
669 return $directory;
670 }
671
672 if ($basePath === NULL) {
673 // Previous versions interpreted `NULL` to mean "default to `self::baseFilePath()`".
674 // However, no code in the known `universe` relies on this interpretation, and
675 // the `baseFilePath()` function is problematic/deprecated.
676 throw new \RuntimeException("absoluteDirectory() requires specifying a basePath");
677 }
678
679 // ensure that $basePath has a trailing slash
680 $basePath = self::addTrailingSlash($basePath);
681 return $basePath . $directory;
682 }
683
684 /**
685 * Make a file path relative to some base dir.
686 *
687 * @param $directory
688 * @param $basePath
689 *
690 * @return string
691 */
692 public static function relativize($directory, $basePath) {
693 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
694 $directory = strtr($directory, '\\', '/');
695 $basePath = strtr($basePath, '\\', '/');
696 }
697 if (substr($directory, 0, strlen($basePath)) == $basePath) {
698 return substr($directory, strlen($basePath));
699 }
700 else {
701 return $directory;
702 }
703 }
704
705 /**
706 * Create a path to a temporary file which can endure for multiple requests.
707 *
708 * @todo Automatic file cleanup using, eg, TTL policy
709 *
710 * @param string $prefix
711 *
712 * @return string, path to an openable/writable file
713 * @see tempnam
714 */
715 public static function tempnam($prefix = 'tmp-') {
716 // $config = CRM_Core_Config::singleton();
717 // $nonce = md5(uniqid() . $config->dsn . $config->userFrameworkResourceURL);
718 // $fileName = "{$config->configAndLogDir}" . $prefix . $nonce . $suffix;
719 $fileName = tempnam(sys_get_temp_dir(), $prefix);
720 return $fileName;
721 }
722
723 /**
724 * Create a path to a temporary directory which can endure for multiple requests.
725 *
726 * @todo Automatic file cleanup using, eg, TTL policy
727 *
728 * @param string $prefix
729 *
730 * @return string, path to an openable/writable directory; ends with '/'
731 * @see tempnam
732 */
733 public static function tempdir($prefix = 'tmp-') {
734 $fileName = self::tempnam($prefix);
735 unlink($fileName);
736 mkdir($fileName, 0700);
737 return $fileName . '/';
738 }
739
740 /**
741 * Search directory tree for files which match a glob pattern.
742 *
743 * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored.
744 *
745 * @param string $dir
746 * base dir.
747 * @param string $pattern
748 * glob pattern, eg "*.txt".
749 * @param bool $relative
750 * TRUE if paths should be made relative to $dir
751 * @return array(string)
752 */
753 public static function findFiles($dir, $pattern, $relative = FALSE) {
754 if (!is_dir($dir) || !is_readable($dir)) {
755 return [];
756 }
757 // Which dirs should we exclude from our searches?
758 // If not defined, we default to excluding any dirname that begins
759 // with a . which is the old behaviour and therefore excludes .git/
760 $excludeDirsPattern = defined('CIVICRM_EXCLUDE_DIRS_PATTERN')
761 ? constant('CIVICRM_EXCLUDE_DIRS_PATTERN')
762 : '@' . preg_quote(DIRECTORY_SEPARATOR) . '\.@';
763
764 $dir = rtrim($dir, '/');
765 $todos = [$dir];
766 $result = [];
767 while (!empty($todos)) {
768 $subdir = array_shift($todos);
769 $matches = glob("$subdir/$pattern");
770 if (is_array($matches)) {
771 foreach ($matches as $match) {
772 if (!is_dir($match)) {
773 $result[] = $relative ? CRM_Utils_File::relativize($match, "$dir/") : $match;
774 }
775 }
776 }
777 // Find subdirs to recurse into.
778 if ($dh = opendir($subdir)) {
779 while (FALSE !== ($entry = readdir($dh))) {
780 $path = $subdir . DIRECTORY_SEPARATOR . $entry;
781 // Exclude . (self) and .. (parent) to avoid infinite loop.
782 // Exclude configured exclude dirs.
783 // Exclude dirs we can't read.
784 // Exclude anything that's not a dir.
785 if (
786 $entry !== '.'
787 && $entry !== '..'
788 && (empty($excludeDirsPattern) || !preg_match($excludeDirsPattern, $path))
789 && is_dir($path)
790 && is_readable($path)
791 ) {
792 $todos[] = $path;
793 }
794 }
795 closedir($dh);
796 }
797 }
798 return $result;
799 }
800
801 /**
802 * Determine if $child is a sub-directory of $parent
803 *
804 * @param string $parent
805 * @param string $child
806 * @param bool $checkRealPath
807 *
808 * @return bool
809 */
810 public static function isChildPath($parent, $child, $checkRealPath = TRUE) {
811 if ($checkRealPath) {
812 $parent = realpath($parent);
813 $child = realpath($child);
814 }
815 $parentParts = explode('/', rtrim($parent, '/'));
816 $childParts = explode('/', rtrim($child, '/'));
817 while (($parentPart = array_shift($parentParts)) !== NULL) {
818 $childPart = array_shift($childParts);
819 if ($parentPart != $childPart) {
820 return FALSE;
821 }
822 }
823 if (empty($childParts)) {
824 // same directory
825 return FALSE;
826 }
827 else {
828 return TRUE;
829 }
830 }
831
832 /**
833 * Move $fromDir to $toDir, replacing/deleting any
834 * pre-existing content.
835 *
836 * @param string $fromDir
837 * The directory which should be moved.
838 * @param string $toDir
839 * The new location of the directory.
840 * @param bool $verbose
841 *
842 * @return bool
843 * TRUE on success
844 */
845 public static function replaceDir($fromDir, $toDir, $verbose = FALSE) {
846 if (is_dir($toDir)) {
847 if (!self::cleanDir($toDir, TRUE, $verbose)) {
848 return FALSE;
849 }
850 }
851
852 // return rename($fromDir, $toDir); CRM-11987, https://bugs.php.net/bug.php?id=54097
853
854 CRM_Utils_File::copyDir($fromDir, $toDir);
855 if (!CRM_Utils_File::cleanDir($fromDir, TRUE, FALSE)) {
856 CRM_Core_Session::setStatus(ts('Failed to clean temp dir: %1', [1 => $fromDir]), '', 'alert');
857 return FALSE;
858 }
859 return TRUE;
860 }
861
862 /**
863 * Format file.
864 *
865 * @param array $param
866 * @param string $fileName
867 * @param array $extraParams
868 */
869 public static function formatFile(&$param, $fileName, $extraParams = []) {
870 if (empty($param[$fileName])) {
871 return;
872 }
873
874 $fileParams = [
875 'uri' => $param[$fileName]['name'],
876 'type' => $param[$fileName]['type'],
877 'location' => $param[$fileName]['name'],
878 'upload_date' => date('YmdHis'),
879 ] + $extraParams;
880
881 $param[$fileName] = $fileParams;
882 }
883
884 /**
885 * Return formatted file URL, like for image file return image url with image icon
886 *
887 * @param string $path
888 * Absoulte file path
889 * @param string $fileType
890 * @param string $url
891 * File preview link e.g. https://example.com/civicrm/file?reset=1&filename=image.png&mime-type=image/png
892 *
893 * @return string $url
894 */
895 public static function getFileURL($path, $fileType, $url = NULL) {
896 if (empty($path) || empty($fileType)) {
897 return '';
898 }
899 elseif (empty($url)) {
900 $fileName = basename($path);
901 $url = CRM_Utils_System::url('civicrm/file', "reset=1&filename={$fileName}&mime-type={$fileType}");
902 }
903 switch ($fileType) {
904 case 'image/jpeg':
905 case 'image/pjpeg':
906 case 'image/gif':
907 case 'image/x-png':
908 case 'image/png':
909 case 'image/jpg':
910 list($imageWidth, $imageHeight) = getimagesize($path);
911 list($imageThumbWidth, $imageThumbHeight) = CRM_Contact_BAO_Contact::getThumbSize($imageWidth, $imageHeight);
912 $url = "<a href=\"$url\" class='crm-image-popup'>
913 <img src=\"$url\" width=$imageThumbWidth height=$imageThumbHeight/>
914 </a>";
915 break;
916
917 default:
918 $url = sprintf('<a href="%s">%s</a>', $url, self::cleanFileName(basename($path)));
919 break;
920 }
921
922 return $url;
923 }
924
925 /**
926 * Return formatted image icon
927 *
928 * @param string $imageURL
929 * Contact's image url
930 *
931 * @return string $url
932 */
933 public static function getImageURL($imageURL) {
934 // retrieve image name from $imageURL
935 $imageURL = CRM_Utils_String::unstupifyUrl($imageURL);
936 parse_str(parse_url($imageURL, PHP_URL_QUERY), $query);
937
938 $url = NULL;
939 if (!empty($query['photo'])) {
940 $path = CRM_Core_Config::singleton()->customFileUploadDir . $query['photo'];
941 }
942 else {
943 $path = $url = $imageURL;
944 }
945 $fileExtension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
946 //According to (https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types),
947 // there are some extensions that would need translating.:
948 $translateMimeTypes = [
949 'tif' => 'tiff',
950 'jpg' => 'jpeg',
951 'svg' => 'svg+xml',
952 ];
953 $mimeType = 'image/' . CRM_Utils_Array::value(
954 $fileExtension,
955 $translateMimeTypes,
956 $fileExtension
957 );
958
959 return self::getFileURL($path, $mimeType, $url);
960 }
961
962 /**
963 * Resize an image.
964 *
965 * @param string $sourceFile
966 * Filesystem path to existing image on server
967 * @param int $targetWidth
968 * New width desired, in pixels
969 * @param int $targetHeight
970 * New height desired, in pixels
971 * @param string $suffix = ""
972 * If supplied, the image will be renamed to include this suffix. For
973 * example if the original file name is "foo.png" and $suffix = "_bar",
974 * then the final file name will be "foo_bar.png".
975 * @param bool $preserveAspect = TRUE
976 * When TRUE $width and $height will be used as a bounding box, outside of
977 * which the resized image will not extend.
978 * When FALSE, the image will be resized exactly to $width and $height, even
979 * if it means stretching it.
980 *
981 * @return string
982 * Path to image
983 * @throws \CRM_Core_Exception
984 * Under the following conditions
985 * - When GD is not available.
986 * - When the source file is not an image.
987 */
988 public static function resizeImage($sourceFile, $targetWidth, $targetHeight, $suffix = "", $preserveAspect = TRUE) {
989
990 // Check if GD is installed
991 $gdSupport = CRM_Utils_System::getModuleSetting('gd', 'GD Support');
992 if (!$gdSupport) {
993 throw new CRM_Core_Exception(ts('Unable to resize image because the GD image library is not currently compiled in your PHP installation.'));
994 }
995
996 $sourceMime = mime_content_type($sourceFile);
997 if ($sourceMime == 'image/gif') {
998 $sourceData = imagecreatefromgif($sourceFile);
999 }
1000 elseif ($sourceMime == 'image/png') {
1001 $sourceData = imagecreatefrompng($sourceFile);
1002 }
1003 elseif ($sourceMime == 'image/jpeg') {
1004 $sourceData = imagecreatefromjpeg($sourceFile);
1005 }
1006 else {
1007 throw new CRM_Core_Exception(ts('Unable to resize image because the file supplied was not an image.'));
1008 }
1009
1010 // get image about original image
1011 $sourceInfo = getimagesize($sourceFile);
1012 $sourceWidth = $sourceInfo[0];
1013 $sourceHeight = $sourceInfo[1];
1014
1015 // Adjust target width/height if preserving aspect ratio
1016 if ($preserveAspect) {
1017 $sourceAspect = $sourceWidth / $sourceHeight;
1018 $targetAspect = $targetWidth / $targetHeight;
1019 if ($sourceAspect > $targetAspect) {
1020 $targetHeight = $targetWidth / $sourceAspect;
1021 }
1022 if ($sourceAspect < $targetAspect) {
1023 $targetWidth = $targetHeight * $sourceAspect;
1024 }
1025 }
1026
1027 // figure out the new filename
1028 $pathParts = pathinfo($sourceFile);
1029 $targetFile = $pathParts['dirname'] . DIRECTORY_SEPARATOR
1030 . $pathParts['filename'] . $suffix . "." . $pathParts['extension'];
1031
1032 $targetData = imagecreatetruecolor($targetWidth, $targetHeight);
1033
1034 // resize
1035 imagecopyresized($targetData, $sourceData,
1036 0, 0, 0, 0,
1037 $targetWidth, $targetHeight, $sourceWidth, $sourceHeight);
1038
1039 // save the resized image
1040 $fp = fopen($targetFile, 'w+');
1041 ob_start();
1042 imagejpeg($targetData);
1043 $image_buffer = ob_get_contents();
1044 ob_end_clean();
1045 imagedestroy($targetData);
1046 fwrite($fp, $image_buffer);
1047 rewind($fp);
1048 fclose($fp);
1049
1050 // return the URL to link to
1051 $config = CRM_Core_Config::singleton();
1052 return $config->imageUploadURL . basename($targetFile);
1053 }
1054
1055 /**
1056 * Get file icon class for specific MIME Type
1057 *
1058 * @param string $mimeType
1059 * @return string
1060 */
1061 public static function getIconFromMimeType($mimeType) {
1062 if (!isset(Civi::$statics[__CLASS__]['mimeIcons'])) {
1063 Civi::$statics[__CLASS__]['mimeIcons'] = json_decode(file_get_contents(__DIR__ . '/File/mimeIcons.json'), TRUE);
1064 }
1065 $iconClasses = Civi::$statics[__CLASS__]['mimeIcons'];
1066 foreach ($iconClasses as $text => $icon) {
1067 if (strpos($mimeType, $text) === 0) {
1068 return $icon;
1069 }
1070 }
1071 return $iconClasses['*'];
1072 }
1073
1074 /**
1075 * Is the filename a safe and valid filename passed in from URL
1076 *
1077 * @param string $fileName
1078 * @return bool
1079 */
1080 public static function isValidFileName($fileName = NULL) {
1081 if ($fileName) {
1082 $check = ($fileName === basename($fileName));
1083 if ($check) {
1084 if (substr($fileName, 0, 1) == '/' || substr($fileName, 0, 1) == '.' || substr($fileName, 0, 1) == DIRECTORY_SEPARATOR) {
1085 $check = FALSE;
1086 }
1087 }
1088 return $check;
1089 }
1090 return FALSE;
1091 }
1092
1093 /**
1094 * Get the extensions that this MimeTpe is for
1095 * @param string $mimeType the mime-type we want extensions for
1096 * @return array
1097 */
1098 public static function getAcceptableExtensionsForMimeType($mimeType = []) {
1099 $mimeRepostory = new \MimeTyper\Repository\ExtendedRepository();
1100 return $mimeRepostory->findExtensions($mimeType);
1101 }
1102
1103 /**
1104 * Get the extension of a file based on its path
1105 * @param string $path path of the file to query
1106 * @return string
1107 */
1108 public static function getExtensionFromPath($path) {
1109 return pathinfo($path, PATHINFO_EXTENSION);
1110 }
1111
1112 }