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