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