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