Merge pull request #4314 from cividesk/CRM-13227
[civicrm-core.git] / CRM / Utils / File.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
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-2014
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 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 *
132 * @param $target
133 * @param bool $rmdir
134 * @param bool $verbose
135 *
136 * @throws Exception
137 * @internal param string $path the path name
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) {
169 CRM_Core_Session::setStatus(ts('Removed directory %1', array(1 => $target)), '', 'success');
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
180 /**
181 * @param $source
182 * @param $destination
183 */
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 /**
247 * Appends a slash to the end of a string if it doesn't already end with one
248 *
249 * @param string $path
250 * @param string $slash
251 *
252 * @return string
253 * @access public
254 * @static
255 */
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;
261 }
262 if (!in_array(substr($path, -1, 1), array('/', '\\'))) {
263 $path .= $slash;
264 }
265 return $path;
266 }
267
268 /**
269 * @param $dsn
270 * @param $fileName
271 * @param null $prefix
272 * @param bool $isQueryString
273 * @param bool $dieOnErrors
274 */
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
317 /**
318 * @param $ext
319 *
320 * @return bool
321 */
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
373 /**
374 * @param $name
375 *
376 * @return string
377 */
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
395 /**
396 * @param $path
397 * @param $ext
398 *
399 * @return array
400 */
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 *
417 * @param string $dir the directory to be secured
418 * @param bool $overwrite
419 */
420 static function restrictAccess($dir, $overwrite = FALSE) {
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 -
424 if (!empty($dir) && is_dir($dir)) {
425 $htaccess = <<<HTACCESS
426 <Files "*">
427 Order allow,deny
428 Deny from all
429 </Files>
430
431 HTACCESS;
432 $file = $dir . '.htaccess';
433 if ($overwrite || !file_exists($file)) {
434 if (file_put_contents($file, $htaccess) === FALSE) {
435 CRM_Core_Error::movedSiteError($file);
436 }
437 }
438 }
439 }
440
441 /**
442 * Restrict remote users from browsing the given directory.
443 *
444 * @param $publicDir
445 */
446 static function restrictBrowsing($publicDir) {
447 if (!is_dir($publicDir) || !is_writable($publicDir)) {
448 return;
449 }
450
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
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
497 /**
498 * @param $directory
499 *
500 * @return string
501 */
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
525 /**
526 * @param $directory
527 *
528 * @return string
529 */
530 static function absoluteDirectory($directory) {
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)) {
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 *
551 * @param $directory
552 * @param $basePath
553 *
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 /**
600 * Search directory tree for files which match a glob pattern.
601 *
602 * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored.
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);
613 $matches = glob("$subdir/$pattern");
614 if (is_array($matches)) {
615 foreach ($matches as $match) {
616 if (!is_dir($match)) {
617 $result[] = $match;
618 }
619 }
620 }
621 $dh = opendir($subdir);
622 if ($dh) {
623 while (FALSE !== ($entry = readdir($dh))) {
624 $path = $subdir . DIRECTORY_SEPARATOR . $entry;
625 if ($entry{0} == '.') {
626 // ignore
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
642 * @param bool $checkRealPath
643 *
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
672 * @param bool $verbose
673 *
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