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