Overview

Packages

  • application
    • commands
    • components
      • actions
      • filters
      • leftWidget
      • permissions
      • sortableWidget
      • util
      • webupdater
      • x2flow
        • actions
        • triggers
      • X2GridView
      • X2Settings
    • controllers
    • models
      • embedded
    • modules
      • accounts
        • controllers
        • models
      • actions
        • controllers
        • models
      • calendar
        • controllers
        • models
      • charts
        • models
      • contacts
        • controllers
        • models
      • docs
        • components
        • controllers
        • models
      • groups
        • controllers
        • models
      • marketing
        • components
        • controllers
        • models
      • media
        • controllers
        • models
      • mobile
        • components
      • opportunities
        • controllers
        • models
      • products
        • controllers
        • models
      • quotes
        • controllers
        • models
      • services
        • controllers
        • models
      • template
        • models
      • users
        • controllers
        • models
      • workflow
        • controllers
        • models
      • x2Leads
        • controllers
        • models
  • None
  • system
    • base
    • caching
    • console
    • db
      • ar
      • schema
    • validators
    • web
      • actions
      • auth
      • helpers
      • widgets
        • captcha
        • pagers
  • zii
    • widgets
      • grid

Classes

  • AppFileUtil
  • CommandUtil
  • CrontabUtil
  • EncryptUtil
  • FileUtil
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /*****************************************************************************************
  3:  * X2Engine Open Source Edition is a customer relationship management program developed by
  4:  * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
  5:  * 
  6:  * This program is free software; you can redistribute it and/or modify it under
  7:  * the terms of the GNU Affero General Public License version 3 as published by the
  8:  * Free Software Foundation with the addition of the following permission added
  9:  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 10:  * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
 11:  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 12:  * 
 13:  * This program is distributed in the hope that it will be useful, but WITHOUT
 14:  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 15:  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 16:  * details.
 17:  * 
 18:  * You should have received a copy of the GNU Affero General Public License along with
 19:  * this program; if not, see http://www.gnu.org/licenses or write to the Free
 20:  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 21:  * 02110-1301 USA.
 22:  * 
 23:  * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
 24:  * California 95067, USA. or at email address contact@x2engine.com.
 25:  * 
 26:  * The interactive user interfaces in modified source and object code versions
 27:  * of this program must display Appropriate Legal Notices, as required under
 28:  * Section 5 of the GNU Affero General Public License version 3.
 29:  * 
 30:  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 31:  * these Appropriate Legal Notices must retain the display of the "Powered by
 32:  * X2Engine" logo. If the display of the logo is not reasonably feasible for
 33:  * technical reasons, the Appropriate Legal Notices must display the words
 34:  * "Powered by X2Engine".
 35:  *****************************************************************************************/
 36: 
 37: /**
 38:  * Standalone file manipulation class.
 39:  *
 40:  * Miscellaneous file system utilities. It is not a child class of CComponent or
 41:  * the like in order to be portable/stand-alone (so it can be used outside the
 42:  * app, i.e. by the installer).
 43:  *
 44:  * @package application.components.util
 45:  * @author Demitri Morgan <demitri@x2engine.com>
 46:  */
 47: class FileUtil {
 48: 
 49:     const ERR_FTPOPER = 100;
 50: 
 51:     public static $_finfo;
 52: 
 53:     public static $alwaysCurl = false;
 54: 
 55:     public static $fileOper = "php";
 56:     public static $ftpChroot = false;
 57:     private static $_ftpStream;
 58: 
 59:     /**
 60:      * Copies a file or directory recursively.
 61:      *
 62:      * If the local filesystem directory to where the file will be copied does
 63:      * not exist yet, it will be created automatically. Furthermore, if a remote
 64:      * URL is being accessed and allow_url_fopen isn't set, it will attempt to
 65:      * use CURL instead.
 66:      *
 67:      * @param string $source The source file.
 68:      * @param string $target The destination path.
 69:      * @param boolean $relTarget Transform the target to a relative path. This
 70:      *  option must be false unless the target is an absolute path.
 71:      * @param boolean $contents If true, and the source is a directory, its
 72:      *  contents will be copied to the destination; otherwise, the whole directory
 73:      *  will be copied into the destination directory.
 74:      * @return boolean
 75:      */
 76:     public static function ccopy($source, $target, $relTarget = false, $contents = true){
 77:         $ds = DIRECTORY_SEPARATOR;
 78:         $remote = (bool) preg_match('%^https?://%', $source);
 79:         // Normalize target to the form where if it's a directory it doesn't have
 80:         // a trailing slash, and is platform-agnostic:
 81:         $target = rtrim(self::rpath($target), $ds);
 82:         // Make the target into a relative path:
 83:         if($relTarget && self::$fileOper !== 'ftp')
 84:             $target = self::relpath($target);
 85:         // Safeguard against overwriting files:
 86:         if(!$remote && is_dir($source) && !is_dir($target) && is_file($target))
 87:             throw new Exception("Cannot copy a directory ($source) into a file ($target).");
 88: 
 89:         // Create parent directories if they don't exist already.
 90:         // 
 91:         // If a file is being copied: the path to examine for directory creation
 92:         //  is one lower than the target (the bottom-level parent).
 93:         // If a directory is being copied: the same is true (to not create the
 94:         //  top level node) even though it's a directory, because the target
 95:         //  directory will be created anyway if necessary
 96:         // If a directory is being copied and $contents is false: it's assumed
 97:         //  that the target is a destination directory and not part of the tree
 98:         //  to be copied.
 99:         $pathNodes = explode($ds, self::ftpStripChroot($target));
100:         if($contents)
101:             array_pop($pathNodes);
102:         for($i = 0; $i <= count($pathNodes); $i++){
103:             $parent = implode($ds, array_slice($pathNodes, 0, $i));
104:             // If we are using an FTP chroot, prepend the $parent path with the chroot dir
105:             // so that is_dir() is accurate.
106:             if (self::$fileOper === 'ftp' && self::$ftpChroot !== false && !self::isRelative($parent))
107:                 $verifyDir = self::$ftpChroot.$parent;
108:             else
109:                 $verifyDir = $parent;
110:             if($parent != '' && !is_dir($verifyDir)){
111:                 switch (self::$fileOper) {
112:                     case 'ftp':
113:                         if (!@ftp_mkdir(self::$_ftpStream, self::ftpStripChroot($parent)))
114:                             throw new Exception("Failed to create directory $parent", self::ERR_FTPOPER);
115:                         break;
116:                     case 'php':
117:                     default:
118:                         if(!@mkdir($parent))
119:                             throw new Exception("Failed to create directory $parent");
120:                 }
121:             }
122:         }
123: 
124:         if($remote){
125:             if(self::tryCurl($source)){
126:                 // Fall back on the getContents method, which will try using CURL
127:                 $ch = self::curlInit($source);
128:                 $contents = curl_exec($ch);
129:                 if((bool) $contents)
130:                     return @file_put_contents($target, $contents) !== false;
131:                 else
132:                     return false;
133:             } else{
134:                 $context = stream_context_create(array(
135:                     'http' => array(
136:                         'timeout' => 15  // Timeout in seconds
137:                         )));
138:                 return @copy($source, $target, $context) !== false;
139:             }
140:         }else{
141:             // Recursively copy a whole folder
142:             $source = self::rpath($source);
143:             if(!is_dir($source) && !file_exists($source))
144:                 throw new Exception("Source file/directory to be copied ($source) not found.");
145: 
146:             if(is_dir($source)){
147:                 if(!$contents){
148:                     // Append the bottom level node in the source path to the
149:                     // target path.
150:                     //
151:                     // This ensures that we copy in the aptly-named target
152:                     // directory instead of dumping the contents of the source
153:                     // into the designated target.
154:                     $source = rtrim($source, $ds);
155:                     $sourceNodes = explode($ds, $source);
156:                     $target = $target.$ds.array_pop($sourceNodes);
157:                 }
158:                 if(!is_dir($target)){
159:                     switch (self::$fileOper) {
160:                         case 'ftp':
161:                             if (!@ftp_mkdir(self::$_ftpStream, self::ftpStripChroot($target)))
162:                                 throw new Exception("Unable to create directory $target", self::ERR_FTPOPER);
163:                             break;
164:                         case 'php':
165:                         default:
166:                             mkdir($target);
167:                     }
168:                 }
169:                 $return = true;
170:                 $files = scandir($source);
171:                 foreach($files as $file){
172:                     if($file != '.' && $file != '..'){
173:                         // Must be recursively called with $relTarget = false
174:                         // because if ccopy is called with $relTarget = true,
175:                         // then at this stage "$target" is already relative,
176:                         // and the argument passed to relpath must be absolute.
177:                         // It also must be called with contents=true because
178:                         // that option, if enabled at lower levels, will create
179:                         // the parent directory twice.
180:                         $return = $return && FileUtil::ccopy($source.$ds.$file, $target.$ds.$file);
181:                     }
182:                 }
183:                 return $return;
184:             }else{
185:                 switch (self::$fileOper) {
186:                     case 'ftp':
187:                         return @ftp_put(self::$_ftpStream, self::ftpStripChroot($target), $source, FTP_BINARY);
188:                     case 'php':
189:                     default:
190:                         $retVal = @copy($source, $target) !== false;
191:                         self::caseInsensitiveCopyFix ($source, $target);
192:                         return $retVal;
193:                 }
194:             }
195:         }
196:     }
197: 
198:     /**
199:      * To be called after copying a file. If it's the case that source and target basenames differ 
200:      * by case, target will be renamed so that its basename matches the source's. Allows case
201:      * of source filename to be preserved in case insensitive file systems.
202:      * @return bool false if rename wasn't called, true otherwise (value used for testing purposes)
203:      */
204:     private static function caseInsensitiveCopyFix ($source, $target) {
205:         $sourceBasename = basename ($source);
206:         $targetBasename = basename ($target);
207:         // if basename of source and target params aren't the same, it means that case was changed
208:         // explicitly
209:         if ($sourceBasename !== $targetBasename) return false;
210: 
211:         // get path to file corresponding to target, so that we can get the basename of the actual
212:         // file
213:         $target = realpath ($target); 
214:         if (!$target) return false;
215: 
216:         $targetBasename = basename ($target);
217: 
218:         // source and target have the same case so renaming won't be necessary
219:         if ($targetBasename === $sourceBasename ||
220:             // or source and target base name differ by something other than case
221:             strtolower ($targetBasename) !== strtolower ($sourceBasename)) {
222: 
223:             return false;
224:         }
225: 
226:         // replace target basename with source basename
227:         $newTargetName = preg_replace (
228:             '/'.preg_quote ($targetBasename).'$/', $sourceBasename, $target); 
229:         if ($newTargetName !== $target) {
230:             @rename ($target, $newTargetName);
231:             return true;
232:         }
233:         return false;
234:     }
235: 
236:     /**
237:      * Change to a given directory relative to the FTP stream's
238:      * current working directory
239:      * @param string $target
240:      */
241:     public static function cdftp($target) {
242:         $target = self::ftpStripChroot($target);
243:         $src = ftp_pwd(self::$_ftpStream);
244:         if ($src === '/')
245:             $cd = $target;
246:         else {
247:             $cd = self::relpath($target, $src . DIRECTORY_SEPARATOR);
248:             if (empty($cd))
249:                 return;
250:         }
251:         if (!@ftp_chdir(self::$_ftpStream, $cd))
252:             throw new Exception("Unable to change to directory '$cd' from '$src'", self::ERR_FTPOPER);
253:     }
254: 
255:     /**
256:      * Removes DOS-related junk from an absolute path.
257:      *
258:      * Returns the path as an array of nodes.
259:      */
260:     public static function cleanDosPath($path){
261:         $a_dirty = explode('\\', $path);
262:         $a = array();
263:         foreach($a_dirty as $node){
264:             $a[] = $node;
265:         }
266:         $lastNode = array_pop($a);
267:         if(preg_match('%/%', $lastNode)){
268:             // The final part of the path might contain a relative path put
269:             // together with forward slashes (for the lazy developer)
270:             foreach(explode('/', $lastNode) as $node)
271:                 $a[] = $node;
272:         }else{
273:             $a[] = $lastNode;
274:         }
275:         return $a;
276:     }
277: 
278: 
279:     /**
280:      * Create a lockfile using the appropriate functionality, depending
281:      * on the selected file operation.
282:      * @param String $lockfile
283:      */
284:     public static function createLockfile($lockfile) {
285:         switch (self::$fileOper) {
286:             case 'ftp':
287:                 $stream = fopen('data://text/plain,'.time(), 'r');
288:                 if (!@ftp_fput(self::$_ftpStream, self::ftpStripChroot($lockfile), $stream, FTP_BINARY))
289:                     throw new Exception("Unable to create lockfile $lockfile", self::ERR_FTPOPER);
290:                 fclose($stream);
291:                 break;
292:             case 'php':
293:             default:
294:                 file_put_contents($lockfile, time());
295:         }
296:     }
297: 
298:     /**
299:      * Initializes and returns a CURL resource handle
300:      * @param string $url
301:      * @return resource
302:      */
303:     public static function curlInit($url){
304:         $ch = curl_init($url);
305:         $curlopt = array(
306:             CURLOPT_RETURNTRANSFER => 1,
307:             CURLOPT_RETURNTRANSFER => 1,
308:             CURLOPT_BINARYTRANSFER => 1,
309:             CURLOPT_POST => 0,
310:             CURLOPT_TIMEOUT => 15
311:         );
312:         curl_setopt_array($ch, $curlopt);
313:         return $ch;
314:     }
315: 
316:     /**
317:      * Closes the current FTP stream and resets the file
318:      * operation method to 'php'
319:      */
320:     public static function ftpClose() {
321:         ftp_close(self::$_ftpStream);
322:         self::$ftpChroot = false;
323:         self::$fileOper = 'php';
324:     }
325: 
326:     /**
327:      * Initializes the FTP functionality. This connects to
328:      * the FTP server, logs in, and sets PASV mode.
329:      * Optionally, you can specify the chroot directory and a directory to
330:      * change to, e.g.: the web root or the test directory. This is recommended
331:      * if working with relative paths.
332:      * @param String $host The FTP server to connect to
333:      * @param String $user The FTP user.
334:      * @param String $pass Specified FTP user's password.
335:      * @param String $dir Initial directory to change to, or null by default
336:      * to disable.
337:      * @param String $chroot The chosen chroot directory for the user.
338:      */
339:     public static function ftpInit($host, $user, $pass, $dir = null, $chroot = null) {
340:         if (!self::$_ftpStream = ftp_connect($host))
341:             throw new Exception("The updater is unable to connect to $host. Please check your FTP connection settings.", self::ERR_FTPOPER);
342:         if (!@ftp_login(self::$_ftpStream, $user, $pass))
343:             throw new Exception("Unable to login as user $user", self::ERR_FTPOPER);
344:         ftp_pasv(self::$_ftpStream, true);
345:         if ($chroot !== null)
346:             self::$ftpChroot = $chroot;
347:         if ($dir !== null)
348:             self::cdftp($dir);
349:         self::$fileOper = "ftp";
350:     }
351: 
352:     public static function ftpStripChroot($dir) {
353:         if (self::$ftpChroot === false || self::isRelative($dir)) // Don't modify a relative path
354:             return $dir;
355:         else {
356:             $replaced = str_replace(self::$ftpChroot, '', $dir);
357:             // Add a leading slash if missing
358:             if (!preg_match('/^(\/|\\\)/', $replaced))
359:                     $replaced = DIRECTORY_SEPARATOR.$replaced;
360:             return $replaced;
361:         }
362:     }
363: 
364:     /**
365:      * Wrapper for file_get_contents that attempts to use CURL if allow_url_fopen is disabled.
366:      *
367:      * @param type $source
368:      * @param type $url
369:      * @return type
370:      * @throws Exception
371:      */
372:     public static function getContents($source, $use_include_path = false, $context = null){
373:         if(self::tryCurl($source)){
374:             $ch = self::curlInit($source);
375:             return @curl_exec($ch);
376:         }else{
377:             // Use the usual copy method
378:             return @file_get_contents($source, $use_include_path, $context);
379:         }
380:     }
381: 
382:     /**
383:      * Returns whether the given parameter is a relative path
384:      * @param string $path
385:      * @return boolean Whether the path is relative
386:      */
387:     public static function isRelative($path) {
388:         // Paths that start with .. or a word character, but not a Windows
389:         // drive specification (C:\).
390:         return preg_match('/^\.\./', $path) || preg_match('/^\w[^:]/', $path);
391:     }
392: 
393:     /**
394:      * Removes redundant up-one-level directory traversal from a path.
395:      *
396:      * Returns an array corresponding to each node in the path, with redundant
397:      *  directory traversal removed. For example, "items/files/stuff/../things"
398:      *  will be returned as array("items","files","things"). Note, however, that
399:      *  "../../stuff/things" will be returned as array("stuff","things"), which
400:      *  does not accurately reflect the actual path from the original working
401:      *  directory. The intention of this function was to clean up absolute paths.
402:      * @param array $path Path to clean
403:      */
404:     public static function noTraversal($path){
405:         $p2 = array();
406:         foreach($path as $node){
407:             if($node == '..'){
408:                 if(count($p2) > 0)
409:                     array_pop($p2);
410:             } else{
411:                 $p2[] = $node;
412:             }
413:         }
414:         return $p2;
415:     }
416: 
417:     /**
418:      * Remove a lockfile using the appropriate functionality, depending
419:      * on the selected file operation.
420:      * @param String $lockfile
421:      */
422:     public static function removeLockfile($lockfile) {
423:         switch (self::$fileOper) {
424:             case 'ftp':
425:                 $lockfile = self::ftpStripChroot($lockfile);
426:                 if (!@ftp_delete(self::$_ftpStream, $lockfile))
427:                     throw new Exception("Unable to delete the lockfile $lockfile", self::ERR_FTPOPER);
428:                 break;
429:             case 'php':
430:             default:
431:                 unlink($lockfile);
432:         }
433:     }
434: 
435:     /**
436:      * Format a path so that it is platform-independent. Doesn't return false
437:      * if the path doesn't exist (so unlike realpath() it can be used to create
438:      * new files).
439:      *
440:      * @param string $path
441:      * @return string
442:      */
443:     public static function rpath($path){
444:         return implode(DIRECTORY_SEPARATOR, explode('/', $path));
445:     }
446: 
447:     /**
448:      * Calculates a relative path between two absolute paths.
449:      *
450:      * @param string $path The path to which the absolute path should be calculated.
451:      * @param string $start The starting path. Must be absolute, if specified, and
452:      *  must be specified if the path argument is not platform-agnostic.
453:      * @param string $ds Directory separator. If the two paths (path and start)
454:      *  use the (almost) ubiquitous convention of forward slashes, but the
455:      *  calculation is to take place on a Windows machine, this must be set to
456:      *  forward slash, so that
457:      */
458:     public static function relpath($path, $start = null, $ds = DIRECTORY_SEPARATOR){
459:         $thisPath = $start === null ? realpath('.').$ds : $start;
460:         // Get node arrays for each path:
461:         if(preg_match('/^([A-Z]):\\\\/', $thisPath, $match0)){ // Windows environment
462:             if(preg_match('/([A-Z]):\\\\/', $path, $match1)){ // Target path is absolute
463:                 if($match0[1] != $match1[1]) // Source and destination are on different drives. Regurgitate the absolute path.
464:                     return $path;
465:                 else{ // Source/destination on same drive.
466:                     $a1 = self::cleanDosPath($path);
467:                     array_shift($a1);
468:                     $a1 = self::noTraversal($a1);
469:                 }
470:             }else{ // Target path is relative
471:                 $a1 = self::noTraversal(explode($ds, $path));
472:             }
473:             $a0 = self::cleanDosPath($thisPath);
474:             array_shift($a0);
475:             $a0 = self::noTraversal($a0);
476:             array_pop($a0);
477:         }else{ // Unix environment. SO MUCH EASIER.
478:             $a0 = self::noTraversal(explode($ds, $thisPath));
479:             array_pop($a0);
480:             $a1 = self::noTraversal(explode($ds, $path));
481:         }
482:         // Find out what the paths have in common and calculate the number of levels to traverse up:
483:         $l = 0;
484:         while($l < count($a0) && $l < count($a1)){
485:             if($a0[$l] != $a1[$l])
486:                 break;
487:             $l++;
488:         }
489:         $lUp = count($a0) - $l;
490:         return str_repeat('..'.$ds, $lUp).implode($ds, array_slice($a1, $l));
491:     }
492: 
493:     /**
494:      * Recursively remove a directory and all its subdirectories.
495:      *
496:      * Walks a directory structure, removing files recursively. An optional
497:      * exclusion pattern can be included. If a directory contains a file that
498:      * matches the exclusion pattern, the directory and its ancestors will not
499:      * be deleted.
500:      *
501:      * @param string $path
502:      * @param string $noDelPat PCRE pattern for excluding files in deletion.
503:      */
504:     public static function rrmdir($path, $noDelPat = null){
505:         $useExclude = $noDelPat != null;
506:         $special = '/.*\/?\.+\/?$/';
507:         $excluded = false;
508:         if(!realpath($path))
509:             return false;
510:         $path = realpath($path);
511:         if(filetype($path) == 'dir'){
512:             $objects = scandir($path);
513:             foreach($objects as $object){
514:                 if(!preg_match($special, $object)){
515:                     if($useExclude){
516:                         if(!preg_match($noDelPat, $object)){
517:                             $excludeThis = self::rrmdir($path.DIRECTORY_SEPARATOR.$object, $noDelPat);
518:                             $excluded = $excluded || $excludeThis;
519:                         }else{
520:                             $excluded = true;
521:                         }
522:                     } else
523:                         self::rrmdir($path.DIRECTORY_SEPARATOR.$object, $noDelPat);
524:                 }
525:             }
526:             reset($objects);
527:             if(!$excluded)
528:                 if(!preg_match($special, $path))
529:                     switch (self::$fileOper) {
530:                         case 'ftp':
531:                             $path = self::ftpStripChroot($path);
532:                             ftp_rmdir(self::$_ftpStream, $path);
533:                             break;
534:                         case 'php':
535:                         default:
536:                             rmdir($path);
537:                     }
538:         } else
539:             switch (self::$fileOper) {
540:                 case 'ftp':
541:                     $path = self::ftpStripChroot($path);
542:                     ftp_delete(self::$_ftpStream, $path);
543:                     break;
544:                 case 'php':
545:                 default:
546:                     unlink($path);
547:             }
548:         return $excluded;
549:     }
550: 
551:     /**
552:      * Create/return finfo resource handle
553:      *
554:      * @return resource
555:      */
556:     public static function finfo(){
557:         if(!isset(self::$_finfo))
558:             if(extension_loaded('fileinfo'))
559:                 self::$_finfo = finfo_open();
560:             else
561:                 self::$_finfo = false;
562:         return self::$_finfo;
563:     }
564: 
565:     /**
566:      * Create human-readable size string
567:      *
568:      * @param type $bytes
569:      * @return type
570:      */
571:     public static function formatSize($bytes, $places = 0){
572:         $sz = array('', 'K', 'M', 'G', 'T', 'P');
573:         $factor = floor((strlen($bytes) - 1) / 3);
574:         return sprintf("%.{$places}f ", $bytes / pow(1024, $factor)).@$sz[$factor]."B";
575:     }
576: 
577:     /**
578:      * The criteria for which CURL should be used.
579:      * @return type
580:      */
581:     public static function tryCurl($source){
582:         $try = preg_match('%^https?://%', $source) && (ini_get('allow_url_fopen') == 0 || self::$alwaysCurl);
583:         if($try)
584:             if(!extension_loaded('curl'))
585:                 throw new Exception('No HTTP methods available. Tried accessing a remote object, but allow_url_fopen is not enabled and the CURL extension is missing.', 500);
586:         return $try;
587:     }
588: 
589: }
590: 
591: ?>
592: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0