Fix spelling and capitalization of Contact ID and External ID
[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
5bc392e6
EM
180 /**
181 * @param $source
182 * @param $destination
183 */
6a488035
TO
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 trailing slashed to paths
248 *
f4aaa82a
EM
249 * @param $name
250 * @param null $separator
251 *
6a488035
TO
252 * @return string
253 * @access public
254 * @static
255 */
256 static function addTrailingSlash($name, $separator = NULL) {
257 if (!$separator) {
258 $separator = DIRECTORY_SEPARATOR;
259 }
260
261 if (substr($name, -1, 1) != $separator) {
262 $name .= $separator;
263 }
264 return $name;
265 }
266
5bc392e6
EM
267 /**
268 * @param $dsn
269 * @param $fileName
270 * @param null $prefix
271 * @param bool $isQueryString
272 * @param bool $dieOnErrors
273 */
6a488035
TO
274 static function sourceSQLFile($dsn, $fileName, $prefix = NULL, $isQueryString = FALSE, $dieOnErrors = TRUE) {
275 require_once 'DB.php';
276
277 $db = DB::connect($dsn);
278 if (PEAR::isError($db)) {
279 die("Cannot open $dsn: " . $db->getMessage());
280 }
281 if (CRM_Utils_Constant::value('CIVICRM_MYSQL_STRICT', CRM_Utils_System::isDevelopment())) {
282 $db->query('SET SESSION sql_mode = STRICT_TRANS_TABLES');
283 }
284
285 if (!$isQueryString) {
286 $string = $prefix . file_get_contents($fileName);
287 }
288 else {
289 // use filename as query string
290 $string = $prefix . $fileName;
291 }
292
293 //get rid of comments starting with # and --
294
295 $string = preg_replace("/^#[^\n]*$/m", "\n", $string);
296 $string = preg_replace("/^(--[^-]).*/m", "\n", $string);
297
298 $queries = preg_split('/;\s*$/m', $string);
299 foreach ($queries as $query) {
300 $query = trim($query);
301 if (!empty($query)) {
302 CRM_Core_Error::debug_query($query);
303 $res = &$db->query($query);
304 if (PEAR::isError($res)) {
305 if ($dieOnErrors) {
306 die("Cannot execute $query: " . $res->getMessage());
307 }
308 else {
309 echo "Cannot execute $query: " . $res->getMessage() . "<p>";
310 }
311 }
312 }
313 }
314 }
315
5bc392e6
EM
316 /**
317 * @param $ext
318 *
319 * @return bool
320 */
6a488035
TO
321 static function isExtensionSafe($ext) {
322 static $extensions = NULL;
323 if (!$extensions) {
324 $extensions = CRM_Core_OptionGroup::values('safe_file_extension', TRUE);
325
326 //make extensions to lowercase
327 $extensions = array_change_key_case($extensions, CASE_LOWER);
328 // allow html/htm extension ONLY if the user is admin
329 // and/or has access CiviMail
330 if (!(CRM_Core_Permission::check('access CiviMail') ||
331 CRM_Core_Permission::check('administer CiviCRM') ||
332 (CRM_Mailing_Info::workflowEnabled() &&
333 CRM_Core_Permission::check('create mailings')
334 )
335 )) {
336 unset($extensions['html']);
337 unset($extensions['htm']);
338 }
339 }
340 //support lower and uppercase file extensions
341 return isset($extensions[strtolower($ext)]) ? TRUE : FALSE;
342 }
343
344 /**
345 * Determine whether a given file is listed in the PHP include path
346 *
347 * @param string $name name of file
348 *
349 * @return boolean whether the file can be include()d or require()d
350 */
351 static function isIncludable($name) {
352 $x = @fopen($name, 'r', TRUE);
353 if ($x) {
354 fclose($x);
355 return TRUE;
356 }
357 else {
358 return FALSE;
359 }
360 }
361
362 /**
363 * remove the 32 bit md5 we add to the fileName
364 * also remove the unknown tag if we added it
365 */
366 static function cleanFileName($name) {
367 // replace the last 33 character before the '.' with null
368 $name = preg_replace('/(_[\w]{32})\./', '.', $name);
369 return $name;
370 }
371
5bc392e6
EM
372 /**
373 * @param $name
374 *
375 * @return string
376 */
6a488035
TO
377 static function makeFileName($name) {
378 $uniqID = md5(uniqid(rand(), TRUE));
379 $info = pathinfo($name);
380 $basename = substr($info['basename'],
381 0, -(strlen(CRM_Utils_Array::value('extension', $info)) + (CRM_Utils_Array::value('extension', $info) == '' ? 0 : 1))
382 );
383 if (!self::isExtensionSafe(CRM_Utils_Array::value('extension', $info))) {
384 // munge extension so it cannot have an embbeded dot in it
385 // The maximum length of a filename for most filesystems is 255 chars.
386 // We'll truncate at 240 to give some room for the extension.
387 return CRM_Utils_String::munge("{$basename}_" . CRM_Utils_Array::value('extension', $info) . "_{$uniqID}", '_', 240) . ".unknown";
388 }
389 else {
390 return CRM_Utils_String::munge("{$basename}_{$uniqID}", '_', 240) . "." . CRM_Utils_Array::value('extension', $info);
391 }
392 }
393
5bc392e6
EM
394 /**
395 * @param $path
396 * @param $ext
397 *
398 * @return array
399 */
6a488035
TO
400 static function getFilesByExtension($path, $ext) {
401 $path = self::addTrailingSlash($path);
402 $dh = opendir($path);
403 $files = array();
404 while (FALSE !== ($elem = readdir($dh))) {
405 if (substr($elem, -(strlen($ext) + 1)) == '.' . $ext) {
406 $files[] .= $path . $elem;
407 }
408 }
409 closedir($dh);
410 return $files;
411 }
412
413 /**
414 * Restrict access to a given directory (by planting there a restrictive .htaccess file)
415 *
f4aaa82a
EM
416 * @param string $dir the directory to be secured
417 * @param bool $overwrite
6a488035 418 */
ea3b22b5 419 static function restrictAccess($dir, $overwrite = FALSE) {
6a488035
TO
420 // note: empty value for $dir can play havoc, since that might result in putting '.htaccess' to root dir
421 // of site, causing site to stop functioning.
422 // FIXME: we should do more checks here -
ea3b22b5 423 if (!empty($dir) && is_dir($dir)) {
6a488035
TO
424 $htaccess = <<<HTACCESS
425<Files "*">
426 Order allow,deny
427 Deny from all
428</Files>
429
430HTACCESS;
431 $file = $dir . '.htaccess';
ea3b22b5
TO
432 if ($overwrite || !file_exists($file)) {
433 if (file_put_contents($file, $htaccess) === FALSE) {
434 CRM_Core_Error::movedSiteError($file);
435 }
6a488035
TO
436 }
437 }
438 }
439
af5201d4
TO
440 /**
441 * Restrict remote users from browsing the given directory.
442 *
443 * @param $publicDir
444 */
445 static function restrictBrowsing($publicDir) {
9404eeac
TO
446 if (!is_dir($publicDir) || !is_writable($publicDir)) {
447 return;
448 }
449
af5201d4
TO
450 // base dir
451 $nobrowse = realpath($publicDir) . '/index.html';
452 if (!file_exists($nobrowse)) {
453 @file_put_contents($nobrowse, '');
454 }
455
456 // child dirs
457 $dir = new RecursiveDirectoryIterator($publicDir);
458 foreach ($dir as $name => $object) {
459 if (is_dir($name) && $name != '..') {
460 $nobrowse = realpath($name) . '/index.html';
461 if (!file_exists($nobrowse)) {
462 @file_put_contents($nobrowse, '');
463 }
464 }
465 }
466 }
467
6a488035
TO
468 /**
469 * Create the base file path from which all our internal directories are
470 * offset. This is derived from the template compile directory set
471 */
472 static function baseFilePath($templateCompileDir = NULL) {
473 static $_path = NULL;
474 if (!$_path) {
475 if ($templateCompileDir == NULL) {
476 $config = CRM_Core_Config::singleton();
477 $templateCompileDir = $config->templateCompileDir;
478 }
479
480 $path = dirname($templateCompileDir);
481
482 //this fix is to avoid creation of upload dirs inside templates_c directory
483 $checkPath = explode(DIRECTORY_SEPARATOR, $path);
484
485 $cnt = count($checkPath) - 1;
486 if ($checkPath[$cnt] == 'templates_c') {
487 unset($checkPath[$cnt]);
488 $path = implode(DIRECTORY_SEPARATOR, $checkPath);
489 }
490
491 $_path = CRM_Utils_File::addTrailingSlash($path);
492 }
493 return $_path;
494 }
495
5bc392e6
EM
496 /**
497 * @param $directory
498 *
499 * @return string
500 */
6a488035
TO
501 static function relativeDirectory($directory) {
502 // Do nothing on windows
503 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
504 return $directory;
505 }
506
507 // check if directory is relative, if so return immediately
508 if (substr($directory, 0, 1) != DIRECTORY_SEPARATOR) {
509 return $directory;
510 }
511
512 // make everything relative from the baseFilePath
513 $basePath = self::baseFilePath();
514 // check if basePath is a substr of $directory, if so
515 // return rest of string
516 if (substr($directory, 0, strlen($basePath)) == $basePath) {
517 return substr($directory, strlen($basePath));
518 }
519
520 // return the original value
521 return $directory;
522 }
523
5bc392e6
EM
524 /**
525 * @param $directory
526 *
527 * @return string
528 */
6a488035
TO
529 static function absoluteDirectory($directory) {
530 // Do nothing on windows - config will need to specify absolute path
531 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
532 return $directory;
533 }
534
535 // check if directory is already absolute, if so return immediately
536 if (substr($directory, 0, 1) == DIRECTORY_SEPARATOR) {
537 return $directory;
538 }
539
540 // make everything absolute from the baseFilePath
541 $basePath = self::baseFilePath();
542
543 return $basePath . $directory;
544 }
545
546 /**
547 * Make a file path relative to some base dir
548 *
f4aaa82a
EM
549 * @param $directory
550 * @param $basePath
551 *
6a488035
TO
552 * @return string
553 */
554 static function relativize($directory, $basePath) {
555 if (substr($directory, 0, strlen($basePath)) == $basePath) {
556 return substr($directory, strlen($basePath));
557 } else {
558 return $directory;
559 }
560 }
561
562 /**
563 * Create a path to a temporary file which can endure for multiple requests
564 *
565 * TODO: Automatic file cleanup using, eg, TTL policy
566 *
567 * @param $prefix string
568 *
569 * @return string, path to an openable/writable file
570 * @see tempnam
571 */
572 static function tempnam($prefix = 'tmp-') {
573 //$config = CRM_Core_Config::singleton();
574 //$nonce = md5(uniqid() . $config->dsn . $config->userFrameworkResourceURL);
575 //$fileName = "{$config->configAndLogDir}" . $prefix . $nonce . $suffix;
576 $fileName = tempnam(sys_get_temp_dir(), $prefix);
577 return $fileName;
578 }
579
580 /**
581 * Create a path to a temporary directory which can endure for multiple requests
582 *
583 * TODO: Automatic file cleanup using, eg, TTL policy
584 *
585 * @param $prefix string
586 *
587 * @return string, path to an openable/writable directory; ends with '/'
588 * @see tempnam
589 */
590 static function tempdir($prefix = 'tmp-') {
591 $fileName = self::tempnam($prefix);
592 unlink($fileName);
593 mkdir($fileName, 0700);
594 return $fileName . '/';
595 }
596
597 /**
d7166b43
TO
598 * Search directory tree for files which match a glob pattern.
599 *
600 * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored.
6a488035
TO
601 *
602 * @param $dir string, base dir
603 * @param $pattern string, glob pattern, eg "*.txt"
604 * @return array(string)
605 */
606 static function findFiles($dir, $pattern) {
607 $todos = array($dir);
608 $result = array();
609 while (!empty($todos)) {
610 $subdir = array_shift($todos);
0b72a00f
TO
611 $matches = glob("$subdir/$pattern");
612 if (is_array($matches)) {
613 foreach ($matches as $match) {
002f1716
TO
614 if (!is_dir($match)) {
615 $result[] = $match;
616 }
6a488035
TO
617 }
618 }
619 $dh = opendir($subdir);
620 if ($dh) {
621 while (FALSE !== ($entry = readdir($dh))) {
622 $path = $subdir . DIRECTORY_SEPARATOR . $entry;
d7166b43
TO
623 if ($entry{0} == '.') {
624 // ignore
6a488035
TO
625 } elseif (is_dir($path)) {
626 $todos[] = $path;
627 }
628 }
629 closedir($dh);
630 }
631 }
632 return $result;
633 }
634
635 /**
636 * Determine if $child is a sub-directory of $parent
637 *
638 * @param string $parent
639 * @param string $child
f4aaa82a
EM
640 * @param bool $checkRealPath
641 *
6a488035
TO
642 * @return bool
643 */
644 static function isChildPath($parent, $child, $checkRealPath = TRUE) {
645 if ($checkRealPath) {
646 $parent = realpath($parent);
647 $child = realpath($child);
648 }
649 $parentParts = explode('/', rtrim($parent, '/'));
650 $childParts = explode('/', rtrim($child, '/'));
651 while (($parentPart = array_shift($parentParts)) !== NULL) {
652 $childPart = array_shift($childParts);
653 if ($parentPart != $childPart) {
654 return FALSE;
655 }
656 }
657 if (empty($childParts)) {
658 return FALSE; // same directory
659 } else {
660 return TRUE;
661 }
662 }
663
664 /**
665 * Move $fromDir to $toDir, replacing/deleting any
666 * pre-existing content.
667 *
668 * @param string $fromDir the directory which should be moved
669 * @param string $toDir the new location of the directory
f4aaa82a
EM
670 * @param bool $verbose
671 *
6a488035
TO
672 * @return bool TRUE on success
673 */
674 static function replaceDir($fromDir, $toDir, $verbose = FALSE) {
675 if (is_dir($toDir)) {
676 if (!self::cleanDir($toDir, TRUE, $verbose)) {
677 return FALSE;
678 }
679 }
680
681 // return rename($fromDir, $toDir); // CRM-11987, https://bugs.php.net/bug.php?id=54097
682
683 CRM_Utils_File::copyDir($fromDir, $toDir);
684 if (!CRM_Utils_File::cleanDir($fromDir, TRUE, FALSE)) {
685 CRM_Core_Session::setStatus(ts('Failed to clean temp dir: %1', array(1 => $fromDir)), '', 'alert');
686 return FALSE;
687 }
688 return TRUE;
689 }
19f7e35e
CW
690
691 /**
692 * Create a static file (e.g. css or js) in the dynamic resource directory
693 * Note: if the file already exists it will be overwritten
694 * @param string $fileName
695 * @param string $contents
696 */
697 static function addDynamicResource($fileName, $contents) {
698 // First ensure the directory exists
4a19b166 699 $path = self::dynamicResourcePath();
19f7e35e
CW
700 if (!is_dir($path)) {
701 self::createDir($path);
702 self::restrictBrowsing($path);
703 }
4a19b166 704 file_put_contents("$path/$fileName", $contents);
19f7e35e
CW
705 }
706
707 /**
708 * Get the path of a dynamic resource file
4a19b166 709 * With no fileName supplied, returns the path of the directory
19f7e35e
CW
710 * @param string $fileName
711 * @return string
712 */
4a19b166
CW
713 static function dynamicResourcePath($fileName = NULL) {
714 $config = CRM_Core_Config::singleton();
715 // FIXME: Use self::baseFilePath once url issue has been resolved
3121b188 716 $path = self::addTrailingSlash(str_replace(array('/persist/contribute', '\persist\contribute'), '', $config->imageUploadDir)) . 'dynamic';
4a19b166
CW
717 if ($fileName !== NULL) {
718 $path .= "/$fileName";
719 }
720 return $path;
19f7e35e
CW
721 }
722
723 /**
724 * Get the URL of a dynamic resource file
725 * @param string $fileName
726 * @return string
727 */
728 static function dynamicResourceUrl($fileName) {
729 $config = CRM_Core_Config::singleton();
4a19b166 730 // FIXME: Need a better way of getting the url of the baseFilePath
8ce76a56 731 return self::addTrailingSlash(str_replace('/persist/contribute', '', $config->imageUploadURL), '/') . 'dynamic/' . $fileName;
19f7e35e
CW
732 }
733
734 /**
76dca235 735 * Delete all files from the dynamic resource directory
19f7e35e
CW
736 */
737 static function flushDynamicResources() {
738 $files = glob(self::dynamicResourcePath('*'));
739 foreach ($files ? $files : array() as $file) {
740 if (is_file($file)) {
741 unlink($file);
742 }
743 }
744 }
6a488035
TO
745}
746