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