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