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