Merge pull request #5901 from colemanw/CRM-16557
[civicrm-core.git] / CRM / Utils / File.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2015 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2015
32 * $Id: $
33 *
34 */
35
36 /**
37 * class to provide simple static functions for file objects
38 */
39 class CRM_Utils_File {
40
41 /**
42 * Given a file name, determine if the file contents make it an ascii file
43 *
44 * @param string $name
45 * Name of file.
46 *
47 * @return bool
48 * true if file is ascii
49 */
50 public static function isAscii($name) {
51 $fd = fopen($name, "r");
52 if (!$fd) {
53 return FALSE;
54 }
55
56 $ascii = TRUE;
57 while (!feof($fd)) {
58 $line = fgets($fd, 8192);
59 if (!CRM_Utils_String::isAscii($line)) {
60 $ascii = FALSE;
61 break;
62 }
63 }
64
65 fclose($fd);
66 return $ascii;
67 }
68
69 /**
70 * Given a file name, determine if the file contents make it an html file
71 *
72 * @param string $name
73 * Name of file.
74 *
75 * @return bool
76 * true if file is html
77 */
78 public static function isHtml($name) {
79 $fd = fopen($name, "r");
80 if (!$fd) {
81 return FALSE;
82 }
83
84 $html = FALSE;
85 $lineCount = 0;
86 while (!feof($fd) & $lineCount <= 5) {
87 $lineCount++;
88 $line = fgets($fd, 8192);
89 if (!CRM_Utils_String::isHtml($line)) {
90 $html = TRUE;
91 break;
92 }
93 }
94
95 fclose($fd);
96 return $html;
97 }
98
99 /**
100 * Create a directory given a path name, creates parent directories
101 * if needed
102 *
103 * @param string $path
104 * The path name.
105 * @param bool $abort
106 * Should we abort or just return an invalid code.
107 *
108 * @return void
109 */
110 public static function createDir($path, $abort = TRUE) {
111 if (is_dir($path) || empty($path)) {
112 return;
113 }
114
115 CRM_Utils_File::createDir(dirname($path), $abort);
116 if (@mkdir($path, 0777) == FALSE) {
117 if ($abort) {
118 $docLink = CRM_Utils_System::docURL2('Moving an Existing Installation to a New Server or Location', NULL, NULL, NULL, NULL, "wiki");
119 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>";
120
121 CRM_Utils_System::civiExit();
122 }
123 else {
124 return FALSE;
125 }
126 }
127 return TRUE;
128 }
129
130 /**
131 * Delete a directory given a path name, delete children directories
132 * and files if needed
133 *
134 * @param string $target
135 * The path name.
136 * @param bool $rmdir
137 * @param bool $verbose
138 *
139 * @throws Exception
140 * @return void
141 */
142 public static function cleanDir($target, $rmdir = TRUE, $verbose = TRUE) {
143 static $exceptions = array('.', '..');
144 if ($target == '' || $target == '/') {
145 throw new Exception("Overly broad deletion");
146 }
147
148 if ($dh = @opendir($target)) {
149 while (FALSE !== ($sibling = readdir($dh))) {
150 if (!in_array($sibling, $exceptions)) {
151 $object = $target . DIRECTORY_SEPARATOR . $sibling;
152
153 if (is_dir($object)) {
154 CRM_Utils_File::cleanDir($object, $rmdir, $verbose);
155 }
156 elseif (is_file($object)) {
157 if (!unlink($object)) {
158 CRM_Core_Session::setStatus(ts('Unable to remove file %1', array(1 => $object)), ts('Warning'), 'error');
159 }
160 }
161 }
162 }
163 closedir($dh);
164
165 if ($rmdir) {
166 if (rmdir($target)) {
167 if ($verbose) {
168 CRM_Core_Session::setStatus(ts('Removed directory %1', array(1 => $target)), '', 'success');
169 }
170 return TRUE;
171 }
172 else {
173 CRM_Core_Session::setStatus(ts('Unable to remove directory %1', array(1 => $target)), ts('Warning'), 'error');
174 }
175 }
176 }
177 }
178
179 /**
180 * Concatenate several files.
181 *
182 * @param array $files
183 * List of file names.
184 * @param string $delim
185 * An optional delimiter to put between files.
186 * @return string
187 */
188 public static function concat($files, $delim = '') {
189 $buf = '';
190 $first = TRUE;
191 foreach ($files as $file) {
192 if (!$first) {
193 $buf .= $delim;
194 }
195 $buf .= file_get_contents($file);
196 $first = FALSE;
197 }
198 return $buf;
199 }
200
201 /**
202 * @param string $source
203 * @param string $destination
204 */
205 public static function copyDir($source, $destination) {
206 if ($dh = opendir($source)) {
207 @mkdir($destination);
208 while (FALSE !== ($file = readdir($dh))) {
209 if (($file != '.') && ($file != '..')) {
210 if (is_dir($source . DIRECTORY_SEPARATOR . $file)) {
211 CRM_Utils_File::copyDir($source . DIRECTORY_SEPARATOR . $file, $destination . DIRECTORY_SEPARATOR . $file);
212 }
213 else {
214 copy($source . DIRECTORY_SEPARATOR . $file, $destination . DIRECTORY_SEPARATOR . $file);
215 }
216 }
217 }
218 closedir($dh);
219 }
220 }
221
222 /**
223 * Given a file name, recode it (in place!) to UTF-8
224 *
225 * @param string $name
226 * Name of file.
227 *
228 * @return bool
229 * whether the file was recoded properly
230 */
231 public static function toUtf8($name) {
232 static $config = NULL;
233 static $legacyEncoding = NULL;
234 if ($config == NULL) {
235 $config = CRM_Core_Config::singleton();
236 $legacyEncoding = $config->legacyEncoding;
237 }
238
239 if (!function_exists('iconv')) {
240
241 return FALSE;
242
243 }
244
245 $contents = file_get_contents($name);
246 if ($contents === FALSE) {
247 return FALSE;
248 }
249
250 $contents = iconv($legacyEncoding, 'UTF-8', $contents);
251 if ($contents === FALSE) {
252 return FALSE;
253 }
254
255 $file = fopen($name, 'w');
256 if ($file === FALSE) {
257 return FALSE;
258 }
259
260 $written = fwrite($file, $contents);
261 $closed = fclose($file);
262 if ($written === FALSE or !$closed) {
263 return FALSE;
264 }
265
266 return TRUE;
267 }
268
269 /**
270 * Appends a slash to the end of a string if it doesn't already end with one
271 *
272 * @param string $path
273 * @param string $slash
274 *
275 * @return string
276 */
277 public static function addTrailingSlash($path, $slash = NULL) {
278 if (!$slash) {
279 // FIXME: Defaulting to backslash on windows systems can produce unexpected results, esp for URL strings which should always use forward-slashes.
280 // I think this fn should default to forward-slash instead.
281 $slash = DIRECTORY_SEPARATOR;
282 }
283 if (!in_array(substr($path, -1, 1), array('/', '\\'))) {
284 $path .= $slash;
285 }
286 return $path;
287 }
288
289 /**
290 * @param $dsn
291 * @param string $fileName
292 * @param null $prefix
293 * @param bool $isQueryString
294 * @param bool $dieOnErrors
295 */
296 public static function sourceSQLFile($dsn, $fileName, $prefix = NULL, $isQueryString = FALSE, $dieOnErrors = TRUE) {
297 require_once 'DB.php';
298
299 $db = DB::connect($dsn);
300 if (PEAR::isError($db)) {
301 die("Cannot open $dsn: " . $db->getMessage());
302 }
303 if (CRM_Utils_Constant::value('CIVICRM_MYSQL_STRICT', CRM_Utils_System::isDevelopment())) {
304 $db->query('SET SESSION sql_mode = STRICT_TRANS_TABLES');
305 }
306
307 if (!$isQueryString) {
308 $string = $prefix . file_get_contents($fileName);
309 }
310 else {
311 // use filename as query string
312 $string = $prefix . $fileName;
313 }
314
315 //get rid of comments starting with # and --
316
317 $string = preg_replace("/^#[^\n]*$/m", "\n", $string);
318 $string = preg_replace("/^(--[^-]).*/m", "\n", $string);
319
320 $queries = preg_split('/;\s*$/m', $string);
321 foreach ($queries as $query) {
322 $query = trim($query);
323 if (!empty($query)) {
324 CRM_Core_Error::debug_query($query);
325 $res = &$db->query($query);
326 if (PEAR::isError($res)) {
327 if ($dieOnErrors) {
328 die("Cannot execute $query: " . $res->getMessage());
329 }
330 else {
331 echo "Cannot execute $query: " . $res->getMessage() . "<p>";
332 }
333 }
334 }
335 }
336 }
337
338 /**
339 * @param $ext
340 *
341 * @return bool
342 */
343 public static function isExtensionSafe($ext) {
344 static $extensions = NULL;
345 if (!$extensions) {
346 $extensions = CRM_Core_OptionGroup::values('safe_file_extension', TRUE);
347
348 //make extensions to lowercase
349 $extensions = array_change_key_case($extensions, CASE_LOWER);
350 // allow html/htm extension ONLY if the user is admin
351 // and/or has access CiviMail
352 if (!(CRM_Core_Permission::check('access CiviMail') ||
353 CRM_Core_Permission::check('administer CiviCRM') ||
354 (CRM_Mailing_Info::workflowEnabled() &&
355 CRM_Core_Permission::check('create mailings')
356 )
357 )
358 ) {
359 unset($extensions['html']);
360 unset($extensions['htm']);
361 }
362 }
363 //support lower and uppercase file extensions
364 return isset($extensions[strtolower($ext)]) ? TRUE : FALSE;
365 }
366
367 /**
368 * Determine whether a given file is listed in the PHP include path.
369 *
370 * @param string $name
371 * Name of file.
372 *
373 * @return bool
374 * whether the file can be include()d or require()d
375 */
376 public static function isIncludable($name) {
377 $x = @fopen($name, 'r', TRUE);
378 if ($x) {
379 fclose($x);
380 return TRUE;
381 }
382 else {
383 return FALSE;
384 }
385 }
386
387 /**
388 * Remove the 32 bit md5 we add to the fileName
389 * also remove the unknown tag if we added it
390 */
391 public static function cleanFileName($name) {
392 // replace the last 33 character before the '.' with null
393 $name = preg_replace('/(_[\w]{32})\./', '.', $name);
394 return $name;
395 }
396
397 /**
398 * @param string $name
399 *
400 * @return string
401 */
402 public static function makeFileName($name) {
403 $uniqID = md5(uniqid(rand(), TRUE));
404 $info = pathinfo($name);
405 $basename = substr($info['basename'],
406 0, -(strlen(CRM_Utils_Array::value('extension', $info)) + (CRM_Utils_Array::value('extension', $info) == '' ? 0 : 1))
407 );
408 if (!self::isExtensionSafe(CRM_Utils_Array::value('extension', $info))) {
409 // munge extension so it cannot have an embbeded dot in it
410 // The maximum length of a filename for most filesystems is 255 chars.
411 // We'll truncate at 240 to give some room for the extension.
412 return CRM_Utils_String::munge("{$basename}_" . CRM_Utils_Array::value('extension', $info) . "_{$uniqID}", '_', 240) . ".unknown";
413 }
414 else {
415 return CRM_Utils_String::munge("{$basename}_{$uniqID}", '_', 240) . "." . CRM_Utils_Array::value('extension', $info);
416 }
417 }
418
419 /**
420 * @param $path
421 * @param $ext
422 *
423 * @return array
424 */
425 public static function getFilesByExtension($path, $ext) {
426 $path = self::addTrailingSlash($path);
427 $files = array();
428 if ($dh = opendir($path)) {
429 while (FALSE !== ($elem = readdir($dh))) {
430 if (substr($elem, -(strlen($ext) + 1)) == '.' . $ext) {
431 $files[] .= $path . $elem;
432 }
433 }
434 closedir($dh);
435 }
436 return $files;
437 }
438
439 /**
440 * Restrict access to a given directory (by planting there a restrictive .htaccess file)
441 *
442 * @param string $dir
443 * The directory to be secured.
444 * @param bool $overwrite
445 */
446 public static function restrictAccess($dir, $overwrite = FALSE) {
447 // note: empty value for $dir can play havoc, since that might result in putting '.htaccess' to root dir
448 // of site, causing site to stop functioning.
449 // FIXME: we should do more checks here -
450 if (!empty($dir) && is_dir($dir)) {
451 $htaccess = <<<HTACCESS
452 <Files "*">
453 Order allow,deny
454 Deny from all
455 </Files>
456
457 HTACCESS;
458 $file = $dir . '.htaccess';
459 if ($overwrite || !file_exists($file)) {
460 if (file_put_contents($file, $htaccess) === FALSE) {
461 CRM_Core_Error::movedSiteError($file);
462 }
463 }
464 }
465 }
466
467 /**
468 * Restrict remote users from browsing the given directory.
469 *
470 * @param $publicDir
471 */
472 public static function restrictBrowsing($publicDir) {
473 if (!is_dir($publicDir) || !is_writable($publicDir)) {
474 return;
475 }
476
477 // base dir
478 $nobrowse = realpath($publicDir) . '/index.html';
479 if (!file_exists($nobrowse)) {
480 @file_put_contents($nobrowse, '');
481 }
482
483 // child dirs
484 $dir = new RecursiveDirectoryIterator($publicDir);
485 foreach ($dir as $name => $object) {
486 if (is_dir($name) && $name != '..') {
487 $nobrowse = realpath($name) . '/index.html';
488 if (!file_exists($nobrowse)) {
489 @file_put_contents($nobrowse, '');
490 }
491 }
492 }
493 }
494
495 /**
496 * Create the base file path from which all our internal directories are
497 * offset. This is derived from the template compile directory set
498 */
499 public static function baseFilePath($templateCompileDir = NULL) {
500 static $_path = NULL;
501 if (!$_path) {
502 if ($templateCompileDir == NULL) {
503 $config = CRM_Core_Config::singleton();
504 $templateCompileDir = $config->templateCompileDir;
505 }
506
507 $path = dirname($templateCompileDir);
508
509 //this fix is to avoid creation of upload dirs inside templates_c directory
510 $checkPath = explode(DIRECTORY_SEPARATOR, $path);
511
512 $cnt = count($checkPath) - 1;
513 if ($checkPath[$cnt] == 'templates_c') {
514 unset($checkPath[$cnt]);
515 $path = implode(DIRECTORY_SEPARATOR, $checkPath);
516 }
517
518 $_path = CRM_Utils_File::addTrailingSlash($path);
519 }
520 return $_path;
521 }
522
523 /**
524 * Determine if a path is absolute.
525 *
526 * @return bool
527 * TRUE if absolute. FALSE if relative.
528 */
529 public static function isAbsolute($path) {
530 if (substr($path, 0, 1) === DIRECTORY_SEPARATOR) {
531 return TRUE;
532 }
533 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
534 if (preg_match('!^[a-zA-Z]:[/\\\\]!', $path)) {
535 return TRUE;
536 }
537 }
538 return FALSE;
539 }
540
541 /**
542 * @param $directory
543 *
544 * @return string
545 */
546 public static function relativeDirectory($directory) {
547 // Do nothing on windows
548 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
549 return $directory;
550 }
551
552 // check if directory is relative, if so return immediately
553 if (!self::isAbsolute($directory)) {
554 return $directory;
555 }
556
557 // make everything relative from the baseFilePath
558 $basePath = self::baseFilePath();
559 // check if basePath is a substr of $directory, if so
560 // return rest of string
561 if (substr($directory, 0, strlen($basePath)) == $basePath) {
562 return substr($directory, strlen($basePath));
563 }
564
565 // return the original value
566 return $directory;
567 }
568
569 /**
570 * @param $directory
571 *
572 * @return string
573 */
574 public static function absoluteDirectory($directory) {
575 // check if directory is already absolute, if so return immediately
576 // Note: Windows PHP accepts any mix of "/" or "\", so "C:\htdocs" or "C:/htdocs" would be a valid absolute path
577 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && preg_match(';^[a-zA-Z]:[/\\\\];', $directory)) {
578 return $directory;
579 }
580
581 // check if directory is already absolute, if so return immediately
582 if (substr($directory, 0, 1) == DIRECTORY_SEPARATOR) {
583 return $directory;
584 }
585
586 // make everything absolute from the baseFilePath
587 $basePath = self::baseFilePath();
588
589 return $basePath . $directory;
590 }
591
592 /**
593 * Make a file path relative to some base dir.
594 *
595 * @param $directory
596 * @param $basePath
597 *
598 * @return string
599 */
600 public static function relativize($directory, $basePath) {
601 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
602 $directory = strtr($directory, '\\', '/');
603 $basePath = strtr($basePath, '\\', '/');
604 }
605 if (substr($directory, 0, strlen($basePath)) == $basePath) {
606 return substr($directory, strlen($basePath));
607 }
608 else {
609 return $directory;
610 }
611 }
612
613 /**
614 * Create a path to a temporary file which can endure for multiple requests.
615 *
616 * TODO: Automatic file cleanup using, eg, TTL policy
617 *
618 * @param string $prefix
619 *
620 * @return string, path to an openable/writable file
621 * @see tempnam
622 */
623 public static function tempnam($prefix = 'tmp-') {
624 //$config = CRM_Core_Config::singleton();
625 //$nonce = md5(uniqid() . $config->dsn . $config->userFrameworkResourceURL);
626 //$fileName = "{$config->configAndLogDir}" . $prefix . $nonce . $suffix;
627 $fileName = tempnam(sys_get_temp_dir(), $prefix);
628 return $fileName;
629 }
630
631 /**
632 * Create a path to a temporary directory which can endure for multiple requests.
633 *
634 * TODO: Automatic file cleanup using, eg, TTL policy
635 *
636 * @param string $prefix
637 *
638 * @return string, path to an openable/writable directory; ends with '/'
639 * @see tempnam
640 */
641 public static function tempdir($prefix = 'tmp-') {
642 $fileName = self::tempnam($prefix);
643 unlink($fileName);
644 mkdir($fileName, 0700);
645 return $fileName . '/';
646 }
647
648 /**
649 * Search directory tree for files which match a glob pattern.
650 *
651 * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored.
652 *
653 * @param string $dir
654 * base dir.
655 * @param string $pattern
656 * glob pattern, eg "*.txt".
657 * @param bool $relative
658 * TRUE if paths should be made relative to $dir
659 * @return array(string)
660 */
661 public static function findFiles($dir, $pattern, $relative = FALSE) {
662 $dir = rtrim($dir, '/');
663 $todos = array($dir);
664 $result = array();
665 while (!empty($todos)) {
666 $subdir = array_shift($todos);
667 $matches = glob("$subdir/$pattern");
668 if (is_array($matches)) {
669 foreach ($matches as $match) {
670 if (!is_dir($match)) {
671 $result[] = $relative ? CRM_Utils_File::relativize($match, "$dir/") : $match;
672 }
673 }
674 }
675 if ($dh = opendir($subdir)) {
676 while (FALSE !== ($entry = readdir($dh))) {
677 $path = $subdir . DIRECTORY_SEPARATOR . $entry;
678 if ($entry{0} == '.') {
679 // ignore
680 }
681 elseif (is_dir($path)) {
682 $todos[] = $path;
683 }
684 }
685 closedir($dh);
686 }
687 }
688 return $result;
689 }
690
691 /**
692 * Determine if $child is a sub-directory of $parent
693 *
694 * @param string $parent
695 * @param string $child
696 * @param bool $checkRealPath
697 *
698 * @return bool
699 */
700 public static function isChildPath($parent, $child, $checkRealPath = TRUE) {
701 if ($checkRealPath) {
702 $parent = realpath($parent);
703 $child = realpath($child);
704 }
705 $parentParts = explode('/', rtrim($parent, '/'));
706 $childParts = explode('/', rtrim($child, '/'));
707 while (($parentPart = array_shift($parentParts)) !== NULL) {
708 $childPart = array_shift($childParts);
709 if ($parentPart != $childPart) {
710 return FALSE;
711 }
712 }
713 if (empty($childParts)) {
714 return FALSE; // same directory
715 }
716 else {
717 return TRUE;
718 }
719 }
720
721 /**
722 * Move $fromDir to $toDir, replacing/deleting any
723 * pre-existing content.
724 *
725 * @param string $fromDir
726 * The directory which should be moved.
727 * @param string $toDir
728 * The new location of the directory.
729 * @param bool $verbose
730 *
731 * @return bool
732 * TRUE on success
733 */
734 public static function replaceDir($fromDir, $toDir, $verbose = FALSE) {
735 if (is_dir($toDir)) {
736 if (!self::cleanDir($toDir, TRUE, $verbose)) {
737 return FALSE;
738 }
739 }
740
741 // return rename($fromDir, $toDir); // CRM-11987, https://bugs.php.net/bug.php?id=54097
742
743 CRM_Utils_File::copyDir($fromDir, $toDir);
744 if (!CRM_Utils_File::cleanDir($fromDir, TRUE, FALSE)) {
745 CRM_Core_Session::setStatus(ts('Failed to clean temp dir: %1', array(1 => $fromDir)), '', 'alert');
746 return FALSE;
747 }
748 return TRUE;
749 }
750
751 }