Merge pull request #6798 from eileenmcnaughton/master
[civicrm-core.git] / CRM / Utils / File.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
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() {
500 static $_path = NULL;
501 if (!$_path) {
502 // Note: Don't rely on $config; that creates a dependency loop.
503 if (!defined('CIVICRM_TEMPLATE_COMPILEDIR')) {
504 throw new RuntimeException("Undefined constant: CIVICRM_TEMPLATE_COMPILEDIR");
505 }
506 $templateCompileDir = CIVICRM_TEMPLATE_COMPILEDIR;
507
508 $path = dirname($templateCompileDir);
509
510 //this fix is to avoid creation of upload dirs inside templates_c directory
511 $checkPath = explode(DIRECTORY_SEPARATOR, $path);
512
513 $cnt = count($checkPath) - 1;
514 if ($checkPath[$cnt] == 'templates_c') {
515 unset($checkPath[$cnt]);
516 $path = implode(DIRECTORY_SEPARATOR, $checkPath);
517 }
518
519 $_path = CRM_Utils_File::addTrailingSlash($path);
520 }
521 return $_path;
522 }
523
524 /**
525 * Determine if a path is absolute.
526 *
527 * @return bool
528 * TRUE if absolute. FALSE if relative.
529 */
530 public static function isAbsolute($path) {
531 if (substr($path, 0, 1) === DIRECTORY_SEPARATOR) {
532 return TRUE;
533 }
534 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
535 if (preg_match('!^[a-zA-Z]:[/\\\\]!', $path)) {
536 return TRUE;
537 }
538 }
539 return FALSE;
540 }
541
542 /**
543 * @param $directory
544 *
545 * @return string
546 */
547 public static function relativeDirectory($directory) {
548 // Do nothing on windows
549 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
550 return $directory;
551 }
552
553 // check if directory is relative, if so return immediately
554 if (!self::isAbsolute($directory)) {
555 return $directory;
556 }
557
558 // make everything relative from the baseFilePath
559 $basePath = self::baseFilePath();
560 // check if basePath is a substr of $directory, if so
561 // return rest of string
562 if (substr($directory, 0, strlen($basePath)) == $basePath) {
563 return substr($directory, strlen($basePath));
564 }
565
566 // return the original value
567 return $directory;
568 }
569
570 /**
571 * @param $directory
572 * @param string|NULL $basePath
573 * The base path when evaluating relative paths. Should include trailing slash.
574 *
575 * @return string
576 */
577 public static function absoluteDirectory($directory, $basePath = NULL) {
578 // check if directory is already absolute, if so return immediately
579 // Note: Windows PHP accepts any mix of "/" or "\", so "C:\htdocs" or "C:/htdocs" would be a valid absolute path
580 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && preg_match(';^[a-zA-Z]:[/\\\\];', $directory)) {
581 return $directory;
582 }
583
584 // check if directory is already absolute, if so return immediately
585 if (substr($directory, 0, 1) == DIRECTORY_SEPARATOR) {
586 return $directory;
587 }
588
589 // make everything absolute from the baseFilePath
590 $basePath = ($basePath === NULL) ? self::baseFilePath() : $basePath;
591
592 return $basePath . $directory;
593 }
594
595 /**
596 * Make a file path relative to some base dir.
597 *
598 * @param $directory
599 * @param $basePath
600 *
601 * @return string
602 */
603 public static function relativize($directory, $basePath) {
604 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
605 $directory = strtr($directory, '\\', '/');
606 $basePath = strtr($basePath, '\\', '/');
607 }
608 if (substr($directory, 0, strlen($basePath)) == $basePath) {
609 return substr($directory, strlen($basePath));
610 }
611 else {
612 return $directory;
613 }
614 }
615
616 /**
617 * Create a path to a temporary file which can endure for multiple requests.
618 *
619 * TODO: Automatic file cleanup using, eg, TTL policy
620 *
621 * @param string $prefix
622 *
623 * @return string, path to an openable/writable file
624 * @see tempnam
625 */
626 public static function tempnam($prefix = 'tmp-') {
627 //$config = CRM_Core_Config::singleton();
628 //$nonce = md5(uniqid() . $config->dsn . $config->userFrameworkResourceURL);
629 //$fileName = "{$config->configAndLogDir}" . $prefix . $nonce . $suffix;
630 $fileName = tempnam(sys_get_temp_dir(), $prefix);
631 return $fileName;
632 }
633
634 /**
635 * Create a path to a temporary directory which can endure for multiple requests.
636 *
637 * TODO: Automatic file cleanup using, eg, TTL policy
638 *
639 * @param string $prefix
640 *
641 * @return string, path to an openable/writable directory; ends with '/'
642 * @see tempnam
643 */
644 public static function tempdir($prefix = 'tmp-') {
645 $fileName = self::tempnam($prefix);
646 unlink($fileName);
647 mkdir($fileName, 0700);
648 return $fileName . '/';
649 }
650
651 /**
652 * Search directory tree for files which match a glob pattern.
653 *
654 * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored.
655 *
656 * @param string $dir
657 * base dir.
658 * @param string $pattern
659 * glob pattern, eg "*.txt".
660 * @param bool $relative
661 * TRUE if paths should be made relative to $dir
662 * @return array(string)
663 */
664 public static function findFiles($dir, $pattern, $relative = FALSE) {
665 $dir = rtrim($dir, '/');
666 $todos = array($dir);
667 $result = array();
668 while (!empty($todos)) {
669 $subdir = array_shift($todos);
670 $matches = glob("$subdir/$pattern");
671 if (is_array($matches)) {
672 foreach ($matches as $match) {
673 if (!is_dir($match)) {
674 $result[] = $relative ? CRM_Utils_File::relativize($match, "$dir/") : $match;
675 }
676 }
677 }
678 if ($dh = opendir($subdir)) {
679 while (FALSE !== ($entry = readdir($dh))) {
680 $path = $subdir . DIRECTORY_SEPARATOR . $entry;
681 if ($entry{0} == '.') {
682 // ignore
683 }
684 elseif (is_dir($path)) {
685 $todos[] = $path;
686 }
687 }
688 closedir($dh);
689 }
690 }
691 return $result;
692 }
693
694 /**
695 * Determine if $child is a sub-directory of $parent
696 *
697 * @param string $parent
698 * @param string $child
699 * @param bool $checkRealPath
700 *
701 * @return bool
702 */
703 public static function isChildPath($parent, $child, $checkRealPath = TRUE) {
704 if ($checkRealPath) {
705 $parent = realpath($parent);
706 $child = realpath($child);
707 }
708 $parentParts = explode('/', rtrim($parent, '/'));
709 $childParts = explode('/', rtrim($child, '/'));
710 while (($parentPart = array_shift($parentParts)) !== NULL) {
711 $childPart = array_shift($childParts);
712 if ($parentPart != $childPart) {
713 return FALSE;
714 }
715 }
716 if (empty($childParts)) {
717 return FALSE; // same directory
718 }
719 else {
720 return TRUE;
721 }
722 }
723
724 /**
725 * Move $fromDir to $toDir, replacing/deleting any
726 * pre-existing content.
727 *
728 * @param string $fromDir
729 * The directory which should be moved.
730 * @param string $toDir
731 * The new location of the directory.
732 * @param bool $verbose
733 *
734 * @return bool
735 * TRUE on success
736 */
737 public static function replaceDir($fromDir, $toDir, $verbose = FALSE) {
738 if (is_dir($toDir)) {
739 if (!self::cleanDir($toDir, TRUE, $verbose)) {
740 return FALSE;
741 }
742 }
743
744 // return rename($fromDir, $toDir); // CRM-11987, https://bugs.php.net/bug.php?id=54097
745
746 CRM_Utils_File::copyDir($fromDir, $toDir);
747 if (!CRM_Utils_File::cleanDir($fromDir, TRUE, FALSE)) {
748 CRM_Core_Session::setStatus(ts('Failed to clean temp dir: %1', array(1 => $fromDir)), '', 'alert');
749 return FALSE;
750 }
751 return TRUE;
752 }
753
754 }