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