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