1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
37:
38: Yii::import('application.components.ResponseBehavior');
39: Yii::import('application.models.Admin');
40:
41:
42:
43: foreach(array('util') as $compDir){
44: $compDirPath = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'components', $compDir));
45: if(!is_dir($compDirPath))
46: @mkdir($compDirPath);
47: if(is_dir($compDirPath))
48: Yii::import("application.components.$compDir.*");
49: }
50:
51:
52:
53: defined('X2_FTP_FILEOPER') or define('X2_FTP_FILEOPER', false);
54: defined('X2_FTP_HOST') or define('X2_FTP_HOST', 'localhost');
55: defined('X2_FTP_USER') or define('X2_FTP_USER', 'root');
56: defined('X2_FTP_PASS') or define('X2_FTP_PASS', '');
57: defined('X2_FTP_CHROOT_DIR') or define('X2_FTP_CHROOT_DIR', false);
58: defined('X2_UPDATE_BETA') or define('X2_UPDATE_BETA',false);
59:
60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101:
102: class UpdaterBehavior extends ResponseBehavior {
103:
104:
105:
106:
107: 108: 109:
110:
111: const BAKFILE = 'update_backup.sql';
112:
113: 114: 115: 116:
117: const LOCKFILE = 'app_update.lock';
118:
119: const PKGFILE = 'update.zip';
120:
121: const TMP_DIR = 'tmp';
122:
123: const ERRFILE = 'update_db_restore.err';
124:
125: const LOGFILE = 'update_db_restore.log';
126:
127: const BCOFILE = 'backcompat.run';
128:
129:
130:
131: const UPDATE_DIR = 'update';
132:
133: const SECURITY_IMG = 'cG93ZXJlZF9ieV94MmVuZ2luZS5wbmc=';
134:
135:
136: const ERR_ISLOCKED = 1;
137:
138: const ERR_CHECKSUM = 2;
139:
140: const ERR_MANIFEST = 3;
141:
142: const ERR_NOUPDATE = 4;
143:
144: const ERR_FILELIST = 5;
145:
146: const ERR_NOTAPPLY = 6;
147:
148: const ERR_UPSERVER = 7;
149:
150: const ERR_DBNOBACK = 8;
151:
152: const ERR_DBOLDBAK = 9;
153:
154: const ERR_SCENARIO = 10;
155:
156: const ERR_NOPROCOP = 11;
157:
158: const ERR_DATABASE = 12;
159:
160:
161:
162: const FILE_PRESENT = 0;
163:
164: const FILE_CORRUPT = 1;
165:
166: const FILE_MISSING = 2;
167:
168:
169:
170:
171:
172:
173: 174: 175: 176:
177: private static $_noHalt = false;
178:
179: 180: 181:
182: public static $configFilename = 'X2Config.php';
183:
184: 185: 186: 187: 188:
189: public static $_configVarNames = array(
190: 'appName',
191: 'email',
192: 'host',
193: 'user',
194: 'pass',
195: 'dbname',
196: 'version',
197: 'buildDate',
198: 'updaterVersion',
199: 'language',
200: );
201:
202: 203: 204: 205:
206: public static $_logCategory = 'application.updater';
207:
208:
209:
210:
211:
212: private $_canSpawnChildren = false;
213:
214: 215: 216: 217:
218: private $_checksums;
219:
220: 221: 222: 223:
224: private $_checksumsContent;
225:
226: private $_checksumsAvail = false;
227:
228: private $_compatibilityStatus;
229:
230: private $_configVars;
231:
232: 233: 234: 235:
236: private $_databaseBackupExists = false;
237:
238: 239: 240: 241:
242: private $_dbBackupCommand;
243:
244: 245: 246: 247:
248: private $_dbBackupPath;
249:
250: 251: 252: 253:
254: private $_dbCommand;
255:
256: 257: 258: 259:
260: private $_dbParams;
261:
262: 263: 264:
265: private $_edition;
266:
267: 268: 269: 270:
271: private $_files;
272:
273: 274: 275:
276: private $_filesByStatus;
277:
278: 279: 280: 281:
282: private $_filesStatus;
283:
284: 285: 286: 287:
288: private $_latestUpdaterVersion;
289:
290: 291: 292: 293:
294: private $_manifest;
295:
296: 297: 298: 299:
300: private $_manifestAvail = false;
301:
302: 303: 304: 305: 306:
307: private $_packageApplies = false;
308:
309: 310: 311: 312: 313:
314: private $_packageExists = false;
315:
316: 317: 318: 319:
320: private $_requirements;
321:
322: 323: 324:
325: private $_scenario;
326:
327: 328: 329: 330:
331: private $_settings;
332:
333: 334: 335: 336:
337: private $_sourceDir;
338:
339: 340: 341: 342:
343: private $_thisPath;
344:
345: 346: 347: 348:
349: private $_uniqueId;
350:
351: 352: 353:
354: private $_version;
355:
356: 357: 358: 359:
360: private $_webRoot;
361:
362: private $_webUpdaterActions;
363:
364: 365: 366: 367:
368: public $updaterFiles = array(
369: "views/admin/updater.php",
370: "components/UpdaterBehavior.php",
371: "components/util/FileUtil.php",
372: "components/util/EncryptUtil.php",
373: "components/util/ResponseUtil.php",
374: "components/ResponseBehavior.php",
375: "components/views/requirements.php",
376: "commands/UpdateCommand.php"
377: );
378:
379: 380: 381: 382: 383: 384: 385: 386: 387:
388: public static function classAliasPath($alias){
389: return preg_replace(':^application/:', '', str_replace('.', '/', $alias)).'.php';
390: }
391:
392: 393: 394: 395: 396: 397: 398:
399: public function applyFiles($dir=null){
400: $success = true;
401: $copiedFiles = array();
402:
403: if(!empty($dir))
404: $success = $this->copyFile($dir);
405: else{
406: $dir = self::UPDATE_DIR.DIRECTORY_SEPARATOR.'source';
407: foreach($this->manifest['fileList'] as $path){
408: $copied = $this->copyFile($path, $dir);
409: $success = $success && $copied;
410: if(!$copied)
411: $copiedFiles[] = $path;
412: }
413: }
414: if($success)
415: $this->cleanUp();
416: else{
417: $message = Yii::t('admin', 'Failed to copy one or more files from {dir} into X2Engine. You may need to copy them manually.', array('{dir}' => $dir));
418: if(!empty($copiedFiles)){
419: $message .= ' '.Yii::t('admin', 'Check that they exist: {fileList}', array('{fileList}' => implode(', ', $copiedFiles)));
420: }
421: throw new CException($message);
422: }
423: return $success;
424: }
425:
426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444:
445: public function backCompatHooks($latestUpdaterVersion) {
446: $runFlag = $this->backCompatFile;
447: if(file_exists($runFlag)) {
448: return false;
449: }
450:
451: if(@file_put_contents($runFlag,time()) === false)
452: return false;
453:
454:
455: $version = $this->configVars['version'];
456: $updaterVersion = $this->configVars['updaterVersion'];
457:
458: $this->output(Yii::t('admin', 'Running backwards compatibility actions for this version.'));
459:
460:
461: $action = false;
462:
463:
464: if (version_compare($version, '4.0') < 0
465: && version_compare($version,'3.4') >= 0) {
466: $action = true;
467: $this->downloadSourceFile("protected/components/util/ResponseUtil.php") ;
468: $this->applyFiles(self::TMP_DIR);
469: }
470:
471:
472:
473: return $action;
474: }
475:
476: public function attach($owner) {
477: if (X2_FTP_FILEOPER && ! $this->isConsole){
478: $dir = str_replace('protected', '', Yii::app()->basePath);
479: FileUtil::ftpInit(X2_FTP_HOST, X2_FTP_USER, X2_FTP_PASS, $dir, X2_FTP_CHROOT_DIR);
480: }
481: parent::attach($owner);
482: }
483:
484: 485: 486: 487:
488: public function checkFiles(){
489:
490: $files = array();
491: foreach($this->checksums as $file => $digest){
492: if(!file_exists($path = $this->updateDir.DIRECTORY_SEPARATOR.FileUtil::rpath($file))){
493: $files[$file] = self::FILE_MISSING;
494: }else if(md5_file($path) != $digest){
495: $files[$file] = self::FILE_CORRUPT;
496: }else{
497: $files[$file] = self::FILE_PRESENT;
498: }
499: }
500: return $files;
501: }
502:
503:
504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517:
518: public function checkIf($name,$throw = true) {
519: if($this->{"_$name"})
520: return true;
521: return $this->{"_$name"} = $this->{"checkIf".ucfirst($name)}($throw);
522: }
523:
524: 525: 526: 527: 528: 529: 530:
531: public function checkIfCanSpawnChildren($throw = true) {
532: if(!@function_exists('proc_open')){
533: if($throw) {
534: throw new CException(Yii::t('admin', 'Unable to spawn child processes on the server because the "proc_open" function is not available.'),self::ERR_NOPROCOP);
535: } else {
536: return false;
537: }
538: }
539: return true;
540: }
541:
542: 543: 544: 545: 546: 547: 548: 549: 550: 551:
552: public function checkIfChecksumsAvail($throw = true) {
553: if(!$this->checkIf('packageExists',$throw))
554: return false;
555: if(!file_exists($checksumsFile = $this->updateDir.DIRECTORY_SEPARATOR.'contents.md5')) {
556: if($throw)
557: throw new CException(Yii::t('admin', 'Cannot verify package contents.').' '.Yii::t('admin', 'Checksum file is missing.'), self::ERR_CHECKSUM);
558: else
559: return false;
560: }
561: $checksums = $this->checksumsContent;
562:
563: if(empty($checksums)) {
564: if($throw)
565: throw new CException(Yii::t('admin', 'Cannot verify package contents.').' '.Yii::t('admin', 'Checksum file is empty.'), self::ERR_CHECKSUM);
566: else
567: return false;
568: }
569: return true;
570: }
571:
572: 573: 574: 575: 576:
577: public function checkIfDatabaseBackupExists($throw = true){
578: $bakFile = $this->dbBackupPath;
579: if(!file_exists($bakFile)) {
580: if($throw)
581: throw new CException(Yii::t('admin', 'Database backup not present.'), self::ERR_DBNOBACK);
582: else
583: return false;
584: }else{
585: $backupTime = filemtime($bakFile);
586: $currenTime = time();
587: if($currenTime - $backupTime > 86400) {
588: if($throw)
589: throw new CException(Yii::t('admin', 'The database backup is over 24 hours old and may thus be unreliable.'), self::ERR_DBOLDBAK);
590: else
591: return false;
592: }
593: }
594: return true;
595: }
596:
597: 598: 599: 600: 601: 602: 603: 604: 605:
606: public function checkIfManifestAvail($throw = true){
607: if(!$this->checkIf('checksumsAvail',$throw))
608: return false;
609: $manifestFile = $this->updateDir.DIRECTORY_SEPARATOR.'manifest.json';
610: if(!file_exists($manifestFile)){
611: if($throw) {
612: throw new CException(Yii::t('admin', 'Manifest file at {file} is missing.', array('{file}' => $manifestFile)), self::ERR_MANIFEST);
613: } else {
614: return false;
615: }
616: }
617:
618: if(md5_file($manifestFile) != $this->checksums['manifest.json']) {
619: if($throw) {
620: throw new CException(Yii::t('admin','Manifest file at {file} is corrupt.', array('{file}' => $manifestFile)),self::ERR_MANIFEST);
621: } else {
622: return false;
623: }
624: }
625: return true;
626: }
627:
628: 629: 630: 631: 632:
633: public function checkIfPackageApplies($throw = true) {
634: if(!$this->checkIf('manifestAvail',$throw))
635: return false;
636:
637: if($this->manifest['updaterVersion'] != $this->configVars['updaterVersion']) {
638: if($throw)
639: throw new CException(Yii::t('admin','The package to be applied is not compatible with the current updater version.'),self::ERR_NOTAPPLY);
640: else
641: return false;
642: }
643:
644: if($this->manifest['fromVersion'] != $this->version) {
645: if($throw)
646: throw new CException(Yii::t('admin','The package to be applied does not correspond to this version of X2Engine; it was meant for version {fv} and this installation is at version {av}.',array(
647: '{fv}'=>$this->manifest['fromVersion'],
648: '{av}'=>$this->version
649: )),self::ERR_NOTAPPLY);
650: else
651: return false;
652: }
653:
654: if($this->manifest['fromEdition'] != $this->edition) {
655: if($throw)
656: throw new CException(Yii::t('admin','The package to be applied does not correspond to this edition of X2Engine; it was meant for edition "{fe}" and this installation is edition "{ae}".',array(
657: '{fe}' => $this->manifest['fromEdition'],
658: '{ae}' => $this->edition,
659: )),self::ERR_NOTAPPLY);
660: else
661: return false;
662: }
663:
664: if($this->manifest['scenario'] != $this->scenario) {
665: if($throw)
666: throw new CException(Yii::t('admin','The package is designated for the scenario "{pscen}" but the updater is being run in the scenario "{bscen}"',array('{pscen}'=>$this->manifest['scenario'],'{bscen}'=>$this->scenario)),self::ERR_NOTAPPLY);
667: else
668: return false;
669: }
670: return true;
671: }
672:
673: 674: 675: 676: 677: 678: 679: 680:
681: public function checkIfPackageExists($throw = true) {
682: if(!is_dir(FileUtil::relpath($this->updateDir, $this->thisPath.DIRECTORY_SEPARATOR))) {
683: if($throw)
684: throw new CException(Yii::t('admin', 'There is no package to apply.'),self::ERR_NOUPDATE);
685: else
686: return false;
687: }
688: return true;
689: }
690:
691:
692:
693:
694: 695: 696:
697: public function checkUpdates($returnOnly = false){
698: $i = empty($this->uniqueId)?'none':$this->uniqueId;
699: $v = $this->version;
700: $e = $this->edition;
701: $secImage = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, '..', 'images', base64_decode(self::SECURITY_IMG)));
702: $context = stream_context_create(array(
703: 'http' => array('timeout' => 4)
704: ));
705: $updateCheckUrl = $this->updateServer.'/installs/updates/check?'.http_build_query(compact('i', 'v'), '', '&');
706:
707: if(($securityKey = FileUtil::getContents($updateCheckUrl, 0, $context)) === false) {
708: if(!$returnOnly)
709: Yii::app()->session['versionCheck'] = true;
710: return Yii::app()->params->version;
711: }
712: $h = hash('sha512', base64_encode(file_exists($secImage) ? file_get_contents($secImage) : null).$securityKey);
713: $n = null;
714: if(!($e == 'opensource' || empty($e)))
715: $n = Yii::app()->db->createCommand()->select('COUNT(*)')->from('x2_users')->queryScalar();
716: $newVersion = FileUtil::getContents($this->updateServer.'/installs/updates/check?'.http_build_query(compact('i', 'v', 'h', 'n'), '', '&'), 0, $context);
717: if(empty($newVersion)) {
718: if(!$returnOnly)
719: Yii::app()->session['versionCheck'] = true;
720: return $this->version;
721: }
722:
723: if(!($this->isConsole || $returnOnly)){
724: Yii::app()->session['versionCheck'] = true;
725: if(version_compare($newVersion, $v) > 0 && $i !== 'none'){
726: Yii::app()->session['versionCheck'] = false;
727: Yii::app()->session['newVersion'] = $newVersion;
728: }
729: }
730: return $newVersion;
731: }
732:
733: 734: 735: 736: 737:
738: public function cleanUp(){
739: FileUtil::rrmdir($this->updateDir);
740: FileUtil::rrmdir($this->updatePackage);
741: }
742:
743: 744: 745: 746: 747: 748: 749: 750: 751: 752: 753:
754: public function copyFile($path, $dir = null, $ds = DIRECTORY_SEPARATOR){
755:
756:
757: $bottomLevel = $dir === null;
758: if($bottomLevel)
759: $dir = $path;
760: $absPath = $bottomLevel ? $this->webRoot.$ds.$path : $this->webRoot.$ds.$dir.$ds.$path;
761: $relPath = FileUtil::relpath($absPath, $this->thisPath.$ds);
762: $absLivePath = $this->webRoot.$ds.$path;
763: $relLivePath = FileUtil::relpath($absLivePath, $this->thisPath.$ds);
764: $success = file_exists($relPath);
765: if($success){
766: if(is_dir($relPath) || $bottomLevel){
767: $objects = scandir($relPath);
768: foreach($objects as $object){
769: if($object != "." && $object != ".."){
770:
771:
772:
773:
774: $copyTarget = $bottomLevel ? $object : $path.$ds.$object;
775: $success == $success && $this->copyFile($copyTarget, $dir);
776: if(!$success)
777: throw new CException(Yii::t('admin', 'Failed to copy from {relPath}; working directory = {cwd}', array('{relPath}' => $relPath, '{cwd}' => $this->$thisPath)));
778: }
779: }
780: } else{
781: return FileUtil::ccopy($relPath, $relLivePath);
782: }
783: }
784: if(!$success)
785: throw new CException(Yii::t('admin', 'Failed to copy from {relPath} (path does not exist); working directory = {cwd}', array('{relPath}' => $relPath, '{cwd}' => $this->thisPath)));
786: return (bool) $success;
787: }
788:
789: 790: 791: 792: 793: 794:
795: public function downloadPackage($version=null,$uniqueId = null, $edition = null) {
796: if(empty($version))
797: $version = $this->configVars['version'];
798: if(empty($uniqueId))
799: $uniqueId = $this->uniqueId;
800: if(empty($edition))
801: $edition = $this->edition;
802: $url = $this->updateServer.'/'.$this->getUpdateDataRoute($version, $uniqueId, $edition);
803: if(!FileUtil::ccopy($url,$this->updatePackage,true))
804: throw new CException(Yii::t('admin','Could not download package; update server error.'),self::ERR_UPSERVER);
805: }
806:
807: 808: 809: 810: 811: 812: 813: 814: 815: 816: 817:
818: public function downloadSourceFile($file, $route = null, $maxAttempts = 5){
819: if(empty($route))
820: $route = $this->sourceFileRoute;
821: $fileUrl = "{$this->updateServer}/{$route}/".strtr($file, array(' ' => '%20'));
822: $i = 0;
823: if($file != ""){
824: $target = FileUtil::relpath(implode(DIRECTORY_SEPARATOR, array($this->webRoot, self::TMP_DIR, FileUtil::rpath($file))), $this->thisPath.DIRECTORY_SEPARATOR);
825: while(!FileUtil::ccopy($fileUrl, $target) && $i < $maxAttempts){
826: $i++;
827: }
828: }
829: if($i >= $maxAttempts){
830: throw new CException(Yii::t('admin', "Failed to download source file {file}. Check that the file is available on the update server at {fileUrl}, and that x2planet.com can be accessed from this web server.", array('{file}' => $file, '{fileUrl}' => $fileUrl)),self::ERR_UPSERVER);
831: }
832: return true;
833: }
834:
835: 836: 837: 838: 839: 840: 841: 842: 843: 844: 845: 846: 847: 848:
849: private function dropAllTables(){
850: if($this->dbParams['server'] == 'mysql'){
851:
852: $dtGen = $this->dbBackupCommand.' --no-data --add-drop-table';
853: $dtRun = $this->dbCommand;
854: $descriptorGen = array(
855: 1 => array('pipe', 'w'),
856: 2 => array('file', implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', self::ERRFILE)), 'a'),
857: );
858: $descriptorRun = array(
859: 0 => array('pipe', 'r'),
860: 1 => array('file', implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', self::LOGFILE)), 'a'),
861: 2 => array('file', implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', self::ERRFILE)), 'a'),
862: );
863: $pipesGen = array();
864: $pipesRun = array();
865: if((bool) $dtGen && (bool) $dtRun){
866:
867: $dtGenProc = proc_open($dtGen, $descriptorGen, $pipesGen);
868: $sqlLines = explode("\n", stream_get_contents($pipesGen[1]));
869: $ret = proc_close($dtGenProc);
870:
871: if($ret == -1)
872: throw new CException(Yii::t('admin', 'Failed to generate drop table statements in the process of restoring the database to a prior state.'));
873:
874: $dtRunProc = proc_open($dtRun, $descriptorRun, $pipesRun);
875:
876: fwrite($pipesRun[0], 'SET FOREIGN_KEY_CHECKS=0;');
877:
878:
879: foreach($sqlLines as $sqlPart){
880: if(preg_match('/^DROP TABLE (IF EXISTS)?/', $sqlPart)){
881: fwrite($pipesRun[0], $sqlPart);
882: }
883: }
884: fwrite($pipesRun[0], 'SET FOREIGN_KEY_CHECKS=1;');
885: $ret = proc_close($dtRunProc);
886: if($ret == -1)
887: throw new CException(Yii::t('admin', 'Failed to run drop table statements in the process of restoring the database to a prior state.'));
888: }
889: }
890: }
891:
892: 893: 894: 895: 896: 897: 898: 899: 900:
901: public function enactChanges($autoRestore = false){
902:
903: $lockFile = $this->lockFile;
904: if(file_exists($lockFile)) {
905: $lockTime = (int) trim(file_get_contents($lockFile));
906: if(time()-$lockTime > 3600)
907: FileUtil::removeLockfile($lockFile);
908: else
909: throw new CException(Yii::t('admin', 'An operation that began {t} is in progress (to apply database and file changes to X2Engine). If you are seeing this message, and the stated time is less than a minute ago, this is most likely because your web browser made a duplicate request to the server. Please stand by while the operation completes. Otherwise, you may delete the lock file {file} and try again.',array('{t}'=>strftime('%h %e, %r',$lockTime),'{file}'=>$this->lockFile)),self::ERR_ISLOCKED);
910: }
911:
912:
913: $this->checkIf('packageApplies');
914:
915:
916: $corrupt = $this->filesStatus[self::FILE_CORRUPT];
917: $missing = $this->filesStatus[self::FILE_MISSING];
918: if($missing || $corrupt){
919: $badFiles = array_merge($this->filesByStatus[self::FILE_CORRUPT], $this->filesByStatus[self::FILE_MISSING]);
920: $msg = Yii::t('admin', 'Unable to apply changes.');
921: $msg .= Yii::t('admin','The following files are corrupt or missing: {list}', array('{list}' => implode(',', $badFiles)));
922: throw new CException($msg, self::ERR_FILELIST);
923: }
924:
925:
926:
927:
928: FileUtil::createLockfile($lockFile);
929:
930:
931: try{
932: $this->output(Yii::t('admin','Enacting changes to the database...'));
933: $this->enactDatabaseChanges($autoRestore);
934: }catch(Exception $e){
935:
936:
937:
938: FileUtil::removeLockfile($lockFile);
939:
940:
941: throw $e;
942: }
943:
944: $lastException = null;
945:
946: try{
947:
948:
949:
950:
951:
952: $this->output(Yii::t('admin','Enacting changes to the fileset...'));
953: $this->applyFiles();
954:
955: $this->removeFiles($this->manifest['deletionList']);
956: $this->output(Yii::t('admin','Cleaning up...'));
957: if($this->scenario == 'update'){
958: $this->resetAssets();
959:
960: $this->regenerateConfig($this->manifest['targetVersion'], $this->manifest['updaterVersion'], $this->manifest['buildDate']);
961: $this->version = $this->manifest['targetVersion'];
962: }else if($this->scenario == 'upgrade'){
963:
964: $admin = CActiveRecord::model('Admin')->findByPk(1);
965:
966: Yii::app()->db->schema->refresh ();
967: $admin->refreshMetaData ();
968: $admin->edition = $this->manifest['targetEdition'];
969: if(!(empty($this->uniqueId)||$this->uniqueId=='none'))
970: $admin->unique_id = $this->uniqueId;
971: $admin->save();
972: $this->edition = $admin->edition;
973: }
974: }catch(Exception $e){
975: $lastException = $e;
976: }
977:
978:
979: FileUtil::removeLockfile($lockFile);
980:
981: if(file_exists($bcFile = $this->backCompatFile))
982: unlink($bcFile);
983:
984:
985: $cache = Yii::app()->cache;
986: if(!empty($cache))
987: $cache->flush();
988: if (isset (Yii::app()->cache2)) {
989: Yii::app()->cache2->flush ();
990: }
991:
992: Yii::app()->db->createCommand('DELETE FROM x2_auth_cache WHERE 1')->execute();
993: if($this->scenario == 'update'){
994:
995: Yii::app()->db->createCommand('DELETE FROM x2_sessions')->execute();
996: }
997:
998:
999: if($lastException instanceof Exception) {
1000: throw new CException(Yii::t('admin','Encountered an issue after applying database changes. The error message given was {msg}.',array('{msg}'=>$lastException->getMessage())));
1001: }else{
1002: return false;
1003: }
1004: }
1005:
1006: 1007: 1008: 1009: 1010:
1011: private function enactDatabaseChanges($backup = false){
1012: $sqlRun = array();
1013: $sqlLists = $this->scenario == 'upgrade' ? array('sqlUpgrade') : array('sqlForce', 'sqlList');
1014: $skipOnFail = array('sqlUpgrade' => 0, 'sqlList' => 0, 'sqlForce' => 1);
1015: $pdo = Yii::app()->db->getPdoInstance();
1016:
1017: foreach($this->manifest['data'] as $part){
1018: foreach($sqlLists as $delta){
1019: foreach($part[$delta] as $sql){
1020: if($sql != ""){
1021: try{
1022: $this->output(Yii::t('admin','Running SQL:').' '.$sql);
1023: $command = $pdo->prepare($sql);
1024: $result = $command->execute();
1025: if($result !== false)
1026: $sqlRun[] = $sql;
1027: else{
1028: $errorInfo = $command->errorInfo();
1029: $this->sqlError($sql, $sqlRun, '('.$errorInfo[0].') '.$errorInfo[2]);
1030: }
1031: }catch(PDOException $e){
1032: if($skipOnFail[$delta])
1033: continue;
1034: $sqlErr = $e->getMessage();
1035: try{
1036: $this->handleSqlFailure ($sql, $sqlRun, $sqlErr, $backup);
1037: }catch(Exception $re){
1038: $dbRestoreMessage = $re->getMessage();
1039: $this->sqlError($sql, $sqlRun, "$sqlErr\n$dbRestoreMessage");
1040: }
1041: }
1042: }
1043: }
1044: }
1045: if(count($part['migrationScripts'])){
1046: $this->output(Yii::t('admin', 'Running migration scripts for version {ver}...', array('{ver}' => $part['version'])));
1047: $sqlRun = $this->runMigrationScripts($part['migrationScripts'], $sqlRun, $backup);
1048: }
1049: }
1050: return true;
1051: }
1052:
1053: 1054: 1055: 1056: 1057:
1058: public function handleSqlFailure($sql, $sqlRun, $sqlErr, $backup, $throw = true) {
1059: if ($backup) {
1060: $this->restoreDatabaseBackup();
1061: $dbRestoreMessage = Yii::t('admin', 'The database has been restored to the backup copy.');
1062: } else {
1063: if((bool) realpath($this->dbBackupPath))
1064: $dbRestoreMessage = Yii::t('admin', 'To restore the database to its previous state, use the database dump file {file} stored in {dir}', array('{file}' => self::BAKFILE, '{dir}' => 'protected/data'));
1065: else
1066: $dbRestoreMessage = Yii::t('admin', 'If you made a backup of the database before running the updater, you will need to apply it manually.');
1067: }
1068: $this->sqlError($sql, $sqlRun, "$sqlErr\n$dbRestoreMessage", $throw);
1069: }
1070:
1071: 1072: 1073:
1074: public function finalizeUpdate($scenario, $unique_id, $version, $edition) {
1075: if ($scenario !== 'update')
1076: return;
1077: $params = array(
1078: 'unique_id' => $unique_id,
1079: 'version' => $version,
1080: 'edition' => $edition,
1081: );
1082: return FileUtil::getContents (
1083: $this->updateServer . '/installs/updates/finalizeUpdate?' .
1084: http_build_query ($params, '', '&'));
1085: }
1086:
1087: 1088: 1089: 1090: 1091: 1092:
1093: public function formatDefinitionList($list,$web) {
1094: $messages = $web ? '<dl>' : "\n";
1095: foreach($list as $term => $definition) {
1096: $messages .= $web ? '<dt>'.$term.'</dt>': "\n$term";
1097: if(is_array($definition)){
1098: $messages .= $web ? '<dd><ul><li>'.implode('</li><li>', $definition).'</li></ul></dd>' : "\n\t".implode("\n\t", $definition);
1099: } else {
1100: $messages .= $web ? "<dd>$definition</dd>" : "\n\t$definition";
1101: }
1102: }
1103: $messages .= $web ? "</dl>" : "\n";
1104: return $messages;
1105: }
1106:
1107: 1108: 1109: 1110:
1111: public function getBackCompatFile() {
1112: return implode(DIRECTORY_SEPARATOR,array(Yii::app()->basePath,'runtime',self::BCOFILE));
1113: }
1114:
1115: 1116: 1117: 1118: 1119: 1120: 1121: 1122: 1123: 1124: 1125:
1126: public function getChecksums(){
1127: if(empty($this->_checksums)){
1128: $this->checkIf('checksumsAvail');
1129: preg_match_all('/^(?<md5sum>[a-f0-9]{32})\s+(?<filename>\S.*)$/m', $this->checksumsContent, $cs);
1130: $checksums = array();
1131: for($i = 0; $i < count($cs[0]); $i++){
1132: $checksums[trim($cs['filename'][$i])] = $cs['md5sum'][$i];
1133: }
1134: $this->_checksums = $checksums;
1135: }
1136: return $this->_checksums;
1137: }
1138:
1139: public function getChecksumsContent() {
1140: if(empty($this->_checksumsContent))
1141: $this->_checksumsContent = trim(file_get_contents($this->updateDir.DIRECTORY_SEPARATOR.'contents.md5'));
1142: return $this->_checksumsContent;
1143: }
1144:
1145: 1146: 1147: 1148: 1149: 1150:
1151: public function getCompatibilityStatus(){
1152: if(!isset($this->_compatibilityStatus)){
1153:
1154:
1155: $allClear = true;
1156:
1157:
1158:
1159:
1160:
1161: $req = $this->requirements;
1162: $allClear = $allClear && $req['canInstall'];
1163:
1164:
1165:
1166:
1167: $databasePermissionError = $this->testDatabasePermissions();
1168: $allClear = $allClear && !$databasePermissionError;
1169:
1170:
1171:
1172:
1173:
1174: $modulesInUpdate = array();
1175: foreach($this->manifest['fileList'] as $file){
1176: if(preg_match(':protected/modules/([a-zA-Z0-9]+)/.*:', $file, $match)){
1177: $modulesInUpdate[$match[1]] = 1;
1178: }
1179: }
1180: $modulesInUpdate = array_keys($modulesInUpdate);
1181: $crit = new CDbCriteria();
1182: $crit->addInCondition('name', $modulesInUpdate);
1183: $crit->addColumnCondition(array('custom' => 1));
1184: $modRec = Modules::model()->findAll($crit);
1185: if(!empty($modRec)){
1186: $allClear = false;
1187: $modules = array_map(function($m){
1188: return $m->name;
1189: }, $modRec);
1190: }else{
1191: $modules = array();
1192: }
1193:
1194:
1195:
1196:
1197: $Dsql = $this->scenario == 'upgrade' ? 'sqlUpgrade' : 'sqlList';
1198: $n_p = 0;
1199: $params = array();
1200: $fieldsEntries = array();
1201: foreach($this->manifest['data'] as $version){
1202: foreach($version[$Dsql] as $sql){
1203: if(preg_match('/INSERT INTO `?x2_fields`?\s+\((?<columns>[a-zA-Z0-9_,`\s]+)\)\s+VALUES\s+(?<records>.+);?$/im', $sql, $match)){
1204:
1205: $columns = array_map(function($c){
1206: return trim($c, ' `');
1207: }, explode('`,`', $match['columns']));
1208: $n_col = count($columns);
1209:
1210: $records = array_filter(array_map(function($r){
1211: return explode(',', trim($r, ' \'"()'));
1212: }, explode('),(', $match['records'])), function($r) use($n_col){
1213: return count($r) == $n_col;
1214: });
1215:
1216: $i_mn = array_search('modelName', $columns);
1217: $i_fn = array_search('fieldName', $columns);
1218: foreach($records as $record){
1219: $p_mn = ":modelName$n_p";
1220: $p_fn = ":fieldName$n_p";
1221: $params[$p_fn] = trim($record[$i_fn],'"\'');
1222: $params[$p_mn] = trim($record[$i_mn],'"\'');
1223: $fieldsEntries[] = "($p_mn,$p_fn)";
1224:
1225: $n_p++;
1226: }
1227: }
1228: }
1229: }
1230:
1231: $conflictingFields = array();
1232: if(!empty($fieldsEntries)){
1233: try{
1234:
1235: $fields = Yii::app()->db->createCommand('SELECT `modelName`,`fieldName` FROM x2_fields WHERE (`modelName`,`fieldName`) IN ('.implode(',', $fieldsEntries).');')->queryAll(true, $params);
1236: $conflictingFields = array_fill_keys(array_unique(array_map(function($f){
1237: return $f['modelName'];
1238: }, $fields)), array());
1239: foreach($fields as $f){
1240: $conflictingFields[$f['modelName']][] = $f['fieldName'];
1241: }
1242: } catch(Exception $e){
1243:
1244:
1245:
1246: }
1247: }
1248:
1249:
1250:
1251: if(version_compare($this->version,'3.0') < 0 && isset($conflictingFields['Actions']['actionDescription'])) {
1252: unset($conflictingFields['Actions']['actionDescription']);
1253: if(count($conflictingFields['Actions']) == 0) {
1254: unset($conflictingFields['Actions']);
1255: }
1256: }
1257:
1258:
1259: $allClear = $allClear && empty($conflictingFields);
1260:
1261:
1262:
1263:
1264: $customFiles = array();
1265: foreach($this->manifest['fileList'] as $file){
1266: if(preg_match('/^.+\.php$/', $file)){
1267: $localFile = preg_match(':/controllers/:', $file) ? preg_replace('/(\w+)Controller\.php$/', 'My${1}Controller.php', $file) : $file;
1268: $customFile = implode(DIRECTORY_SEPARATOR, array($this->webRoot, 'custom', FileUtil::rpath($localFile)));
1269: if(file_exists($customFile)){
1270: $customFiles[] = $file;
1271: $allClear = false;
1272: }
1273: }
1274: }
1275:
1276: $this->_compatibilityStatus = compact('req','databasePermissionError', 'modules', 'conflictingFields', 'customFiles', 'allClear');
1277: }
1278: return $this->_compatibilityStatus;
1279: }
1280:
1281: 1282: 1283: 1284:
1285: public function getConfigVars(){
1286: if(!isset($this->_configVars)){
1287: $configPath = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'config', self::$configFilename));
1288: if(!file_exists($configPath))
1289: $this->regenerateConfig();
1290: $populateVars = function($path) {
1291: include($path);
1292: $vars = compact(array_keys(get_defined_vars()));
1293: unset($vars['path']);
1294: return $vars;
1295: };
1296: $this->_configVars = $populateVars($configPath);
1297: $this->version = $this->_configVars['version'];
1298: }
1299: return $this->_configVars;
1300: }
1301:
1302: 1303: 1304: 1305: 1306:
1307: public function getDbBackupCommand(){
1308: if(!isset($this->_dbBackupCommand)){
1309: $this->checkIf('canSpawnChildren');
1310: if($this->dbParams['server'] == 'mysql'){
1311:
1312: $descriptor = array(
1313: 0 => array('pipe', 'r'),
1314: 1 => array('pipe', 'w'),
1315: 2 => array('pipe', 'w'),
1316: );
1317: $testProc = proc_open('mysqldump --help', $descriptor, $pipes);
1318: $ret = proc_close($testProc);
1319: $prog = 'mysqldump';
1320: unset($pipes);
1321:
1322: if($ret !== 0){
1323: $testProc = proc_open('mysqldump.exe --help', $descriptor, $pipes);
1324: $ret = proc_close($testProc);
1325: if($ret !== 0)
1326: throw new CException(Yii::t('admin', 'Unable to perform database backup; the "mysqldump" utility is not available on this system.'));
1327: else
1328: $prog = 'mysqldump.exe';
1329: }
1330: $quotedPass = escapeshellarg($this->dbParams['dbpass']);
1331: $this->_dbBackupCommand = $prog." -h{$this->dbParams['dbhost']} -u{$this->dbParams['dbuser']} -p{$quotedPass} {$this->dbParams['dbname']}";
1332: } else{
1333: return null;
1334: }
1335: }
1336: return $this->_dbBackupCommand;
1337: }
1338:
1339: 1340: 1341: 1342:
1343: public function getDbBackupPath(){
1344: if(!isset($this->_dbBackupPath))
1345: $this->_dbBackupPath = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', self::BAKFILE));
1346: return $this->_dbBackupPath;
1347: }
1348:
1349: 1350: 1351: 1352: 1353:
1354: public function getDbCommand(){
1355: if(!isset($this->_dbCommand)){
1356: $this->checkIf('canSpawnChildren');
1357:
1358: if($this->dbParams['server'] == 'mysql'){
1359: $descriptor = array(
1360: 0 => array('pipe', 'r'),
1361: 1 => array('pipe', 'w'),
1362: 2 => array('pipe', 'w'),
1363: );
1364: $testProc = proc_open('mysql --help', $descriptor, $pipes);
1365: $ret = proc_close($testProc);
1366: $prog = 'mysql';
1367: unset($pipes);
1368:
1369: if($ret !== 0){
1370: $testProc = proc_open('mysql.exe --help', $descriptor, $pipes);
1371: $ret = proc_close($testProc);
1372: if($ret !== 0)
1373: throw new CException(Yii::t('admin', 'Cannot restore database; the MySQL command line client is not available on this system.'));
1374: else
1375: $prog = 'mysql.exe';
1376: }
1377: $this->_dbCommand = $prog." -h{$this->dbParams['dbhost']} -u{$this->dbParams['dbuser']} -p{$this->dbParams['dbpass']} {$this->dbParams['dbname']}";
1378: } else{
1379: return null;
1380: }
1381: }
1382: return $this->_dbCommand;
1383: }
1384:
1385: 1386: 1387: 1388:
1389: public function getDbParams(){
1390: if(!isset($this->_dbParams)){
1391: $this->_dbParams = array();
1392: if(preg_match('/mysql:host=([^;]+);dbname=([^;]+)/', Yii::app()->db->connectionString, $param)){
1393: $this->_dbParams['dbhost'] = $param[1];
1394: $this->_dbParams['dbname'] = $param[2];
1395: $this->_dbParams['server'] = 'mysql';
1396: }else{
1397:
1398: return false;
1399: }
1400: $this->_dbParams['dbuser'] = Yii::app()->db->username;
1401: $this->_dbParams['dbpass'] = Yii::app()->db->password;
1402: }
1403: return $this->_dbParams;
1404: }
1405:
1406: 1407: 1408: 1409: 1410: 1411: 1412: 1413: 1414: 1415:
1416: public function getEdition(){
1417: if(!isset($this->_edition)){
1418:
1419:
1420: $this->_edition = 'opensource';
1421: try{
1422:
1423: $this->_edition = Yii::app()->edition;
1424: }catch(Exception $e){
1425: if(Yii::app()->params->hasProperty('admin')){
1426:
1427: $admin = Yii::app()->params->admin;
1428: if($admin->hasAttribute('edition')){
1429: $this->_edition = $admin->edition == null ? 'opensource' : $admin->edition;
1430: }
1431: }
1432: }
1433: }
1434: return $this->_edition;
1435: }
1436:
1437: 1438: 1439: 1440: 1441: 1442:
1443: public function getFiles(){
1444: if(empty($this->_files)){
1445: $files = $this->checkFiles();
1446: if(empty($files)){
1447: return $files;
1448: }
1449: $this->_files = $files;
1450: }
1451: return $this->_files;
1452: }
1453:
1454: 1455: 1456: 1457: 1458:
1459: public function getFilesByStatus() {
1460: if(!isset($this->_filesByStatus)) {
1461: if(isset($this->_filesStatus))
1462: $this->_filesStatus = null;
1463: $this->getFilesStatus();
1464: }
1465: return $this->_filesByStatus;
1466: }
1467:
1468: 1469: 1470: 1471:
1472: public function getFilesStatus(){
1473: if(empty($this->_filesStatus)){
1474: $files = $this->files;
1475: $statusCodes = array(self::FILE_PRESENT, self::FILE_CORRUPT, self::FILE_MISSING);
1476: $filesByStatus = array_fill_keys($statusCodes,array());
1477: $fss = array_fill_keys($statusCodes, 0);
1478: if(is_array($files)){
1479: foreach($files as $file => $status) {
1480: $filesByStatus[$status][] = $file;
1481: $fss[$status]++;
1482: }
1483: $this->_filesByStatus = $filesByStatus;
1484: $this->_filesStatus = $fss;
1485: }else{
1486: $this->_filesByStatus = false;
1487: return $files;
1488: }
1489: }
1490: return $this->_filesStatus;
1491: }
1492:
1493: 1494: 1495: 1496: 1497:
1498: public function getLatestUpdaterVersion(){
1499: if(!isset($this->_latestUpdaterVersion)){
1500: $context = stream_context_create(array(
1501: 'http' => array(
1502: 'timeout' => 15
1503: )));
1504: $this->_latestUpdaterVersion = FileUtil::getContents($this->updateServer.'/installs/updates/updateCheck', 0, $context);
1505: }
1506: return $this->_latestUpdaterVersion;
1507: }
1508:
1509: 1510: 1511:
1512: public function getLockFile(){
1513: return implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'runtime', self::LOCKFILE));
1514: }
1515:
1516: 1517: 1518:
1519: public function getManifest(){
1520: if(!isset($this->_manifest)){
1521: $this->checkIf('manifestAvail');
1522: $manifestFile = $this->updateDir.DIRECTORY_SEPARATOR.'manifest.json';
1523: $this->_manifest = json_decode(file_get_contents($manifestFile),1);
1524: if(empty($this->_manifest))
1525: throw new CException(Yii::t('admin', 'Manifest file at {file} contains malformed JSON.', array('{file}' => $manifestFile)), self::ERR_MANIFEST);
1526: }
1527: return $this->_manifest;
1528: }
1529:
1530: 1531: 1532: 1533:
1534: public function getNoHalt(){
1535: return self::$_noHalt;
1536: }
1537:
1538: 1539: 1540: 1541: 1542:
1543: public function getRequirements() {
1544: if(!isset($this->_requirements)){
1545: $reqScript = implode(DIRECTORY_SEPARATOR, array(
1546: Yii::app()->basePath,
1547: 'components',
1548: 'views',
1549: 'requirements.php'
1550: ));
1551: if(!is_readable($reqScript))
1552: throw new CException(Yii::t('admin', "Requirements check script at {path} is missing or not readable.",array('{path}'=>$reqScript)));
1553:
1554:
1555: $returnArray = true;
1556: $thisFile = Yii::app()->request->scriptFile;
1557: $this->_requirements = @require_once($reqScript);
1558: if(!$this->_requirements) {
1559: CException(Yii::t('admin', "Requirements check script encountered an internal error."));
1560: }
1561: }
1562: return $this->_requirements;
1563: }
1564:
1565: public function getScenario() {
1566: if(!isset($this->_scenario)) {
1567: throw new CException(Yii::t('admin','Scenario not set.'),self::ERR_SCENARIO);
1568: }
1569: return $this->_scenario;
1570: }
1571:
1572: 1573: 1574:
1575: public function getSettings() {
1576: if(!isset($this->_settings)){
1577: if(Yii::app()->hasProperty('settings')){
1578: $this->_settings = Yii::app()->settings;
1579: } else if(Yii::app()->params->hasProperty('admin')) {
1580: $this->_settings = Yii::app()->params->admin;
1581: } else {
1582: $this->_settings = CActiveRecord::model('Admin')->findByPk(1);
1583: }
1584: }
1585: return $this->_settings;
1586:
1587: }
1588:
1589: 1590: 1591: 1592:
1593: public function getSourceDir(){
1594: if(!isset($this->_sourceDir)){
1595: $this->_sourceDir = implode(DIRECTORY_SEPARATOR, array($this->updateDir, 'source'));
1596: }
1597: return $this->_sourceDir;
1598: }
1599:
1600: 1601: 1602: 1603: 1604: 1605: 1606: 1607:
1608: public function getSourceFileRoute($edition = null, $uniqueId = null){
1609: foreach(array('edition', 'uniqueId') as $attr)
1610: if(empty(${$attr}))
1611: ${$attr} = $this->$attr;
1612: return "installs/update/$edition/$uniqueId";
1613: }
1614:
1615: 1616: 1617: 1618:
1619: public function getThisPath(){
1620: if(!isset($this->_thisPath))
1621: $this->_thisPath = realpath('./');
1622: return $this->_thisPath;
1623: }
1624:
1625: 1626: 1627: 1628: 1629:
1630: public function getUniqueId(){
1631: if(!isset($this->_uniqueId)){
1632: try {
1633: $this->_uniqueId = Yii::app()->settings->unique_id;
1634: } catch(Exception $e) {
1635: $admin = Yii::app()->params->admin;
1636: if($admin->hasAttribute('unique_id')){
1637: $this->_uniqueId = empty($admin->unique_id) ? 'none' : $admin->unique_id;
1638: }else{
1639: $this->_uniqueId = 'none';
1640: }
1641: }
1642: }
1643: return $this->_uniqueId;
1644: }
1645:
1646: 1647: 1648: 1649: 1650: 1651: 1652: 1653: 1654: 1655:
1656: public function getUpdateData($version = null, $uniqueId = null, $edition = null){
1657: $updateData = FileUtil::getContents($this->updateServer.'/'.$this->getUpdateDataRoute($version,$uniqueId,$edition).'/manifest.json');
1658: if(!$updateData) {
1659: throw new CException(Yii::t('admin','Update server error.'),self::ERR_UPSERVER);
1660: }
1661: $updateData = json_decode($updateData,1);
1662: if(!(bool) $updateData || !is_array($updateData)) {
1663: throw new CException(Yii::t('admin','Malformed data in updates server response; invalid JSON.'));
1664: }
1665: return $updateData;
1666: }
1667:
1668: 1669: 1670: 1671: 1672: 1673: 1674: 1675:
1676: public function getUpdateDataRoute($version = null, $uniqueId = null, $edition = null){
1677: $route = $this->scenario == 'upgrade' ? 'installs/upgrades/{unique_id}/{edition}_{n_users}' : 'installs/updates/{version}/{unique_id}';
1678: $configVars = $this->configVars;
1679: if(!isset($this->version) && empty($version))
1680: extract($configVars);
1681: foreach(array('version', 'uniqueId', 'edition') as $attr)
1682: if(empty(${$attr}))
1683: ${$attr} = $this->$attr;
1684: $params = array('{version}' => $version, '{unique_id}' => $uniqueId, '{scenario}'=>$this->scenario);
1685: if($edition != 'opensource' || $this->scenario == 'upgrade'){
1686: $route .= $this->scenario == 'upgrade' ? '': '_{edition}_{n_users}';
1687: $params['{edition}'] = $edition;
1688: $params['{n_users}'] = Yii::app()->db->createCommand()->select('COUNT(*)')->from('x2_users')->queryScalar();
1689: }
1690: return strtr($route, $params);
1691: }
1692:
1693: public function getUpdateDir(){
1694: return $this->webRoot.DIRECTORY_SEPARATOR.self::UPDATE_DIR;
1695: }
1696:
1697: public function getUpdatePackage() {
1698: return $this->webRoot.DIRECTORY_SEPARATOR.self::PKGFILE;
1699: }
1700:
1701: 1702: 1703:
1704: public function getUpdateServer() {
1705: return X2_UPDATE_BETA ? 'http://beta.x2planet.com' : 'https://x2planet.com';
1706: }
1707:
1708: public function getVersion() {
1709: if(!isset($this->_version))
1710: $this->_version = Yii::app()->params->version;
1711: return $this->_version;
1712: }
1713:
1714: 1715: 1716: 1717: 1718: 1719: 1720: 1721:
1722: public function getWebRoot(){
1723: if(!isset($this->_webRoot))
1724: $this->_webRoot = realpath(implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, '..','')));
1725: return $this->_webRoot;
1726: }
1727:
1728: 1729: 1730: 1731: 1732: 1733: 1734: 1735: 1736: 1737: 1738: 1739:
1740: public function getWebUpdaterActions($getter = true){
1741: if(!isset($this->_webUpdaterActions) || !$getter){
1742: $this->_webUpdaterActions = array(
1743: 'backup' => array('class' => 'application.components.webupdater.DatabaseBackupAction'),
1744: 'updateStage' => array('class' => 'application.components.webupdater.UpdateStageAction'),
1745: 'updater' => array('class' => 'application.components.webupdater.UpdaterAction'),
1746: );
1747: $allClasses = array_merge($this->_webUpdaterActions, array('base' => array('class' => 'application.components.webupdater.WebUpdaterAction')));
1748: if($getter){
1749: $this->requireDependencies();
1750: }else{
1751: return $allClasses;
1752: }
1753: }
1754: return $this->_webUpdaterActions;
1755: }
1756:
1757: 1758: 1759: 1760: 1761: 1762: 1763:
1764: public function makeDatabaseBackup(){
1765: try{
1766: $this->checkIf('canSpawnChildren');
1767: }catch(Exception $e){
1768: throw new CException(Yii::t('admin', 'Could not perform database backup. {reason}', array('{reason}' => $e->getMessage())));
1769: }
1770: $dataDir = Yii::app()->basePath.DIRECTORY_SEPARATOR.'data';
1771: if(!is_dir($dataDir))
1772: mkdir($dataDir);
1773: $errFile = self::ERRFILE;
1774: $descriptor = array(
1775: 1 => array('file', $this->dbBackupPath, 'w'),
1776: 2 => array('file', implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', $errFile)), 'w'),
1777: );
1778: $pipes = array();
1779:
1780:
1781: $prog = $this->dbBackupCommand;
1782: if((bool) $prog){
1783: $backup = proc_open($this->dbBackupCommand, $descriptor, $pipes, $this->webRoot);
1784: $return = proc_close($backup);
1785: if($return !== 0)
1786: throw new CException(Yii::t('admin', "Database backup process did not exit cleanly. See the file {file} for error output details.", array('{file}' => "protected/data/$errFile")));
1787: else
1788: return True;
1789: }
1790: }
1791:
1792: 1793: 1794: 1795: 1796: 1797: 1798: 1799: 1800: 1801:
1802: public function regenerateConfig($newversion = Null, $newupdaterVersion = Null, $newbuildDate = null, $newAppName=null){
1803:
1804: $newbuildDate = $newbuildDate == null ? time() : $newbuildDate;
1805: $basePath = Yii::app()->basePath;
1806: $configPath = implode(DIRECTORY_SEPARATOR, array($basePath, 'config', self::$configFilename));
1807: if(!file_exists($configPath)){
1808:
1809: include(implode(DIRECTORY_SEPARATOR, array($basePath, 'config', 'emailConfig.php')));
1810: include(implode(DIRECTORY_SEPARATOR, array($basePath, 'config', 'dbConfig.php')));
1811: }else{
1812: include($configPath);
1813: }
1814:
1815: if(!isset($appName)){
1816: if(!empty(Yii::app()->name))
1817: $appName = Yii::app()->name;
1818: else
1819: $appName = "X2Engine";
1820: }
1821: if ($newAppName) {
1822: $appName = $newAppName;
1823: }
1824: if(!isset($email)){
1825: if(!empty($this->settings->emailFromAddr))
1826: $email = $this->settings->emailFromAddr;
1827: else
1828: $email = 'contact@'.$_SERVER['SERVER_NAME'];
1829: }
1830: if(!isset($language)){
1831: if(!empty(Yii::app()->language))
1832: $language = Yii::app()->language;
1833: else
1834: $language = 'en';
1835: }
1836:
1837: $config = "<?php\n";
1838: if(!isset($buildDate))
1839: $buildDate = $newbuildDate;
1840: if(!isset($updaterVersion))
1841: $updaterVersion = '';
1842: foreach(array('version', 'updaterVersion', 'buildDate') as $var)
1843: if(${'new'.$var} !== null)
1844: ${$var} = ${'new'.$var};
1845: foreach(self::$_configVarNames as $var) {
1846: if(!empty(${"new$var"}))
1847: ${$var} = ${"new$var"};
1848: $config .= "\$$var=".var_export(${$var},1).";\n";
1849: }
1850: $config .= "?>";
1851:
1852:
1853: if(file_put_contents($configPath, $config) === false){
1854: $contents = $this->isConsole ? "\n$config" : "<br /><pre>\n$config\n</pre>";
1855: throw new CException(Yii::t('admin', "Failed to set version info in the configuration. To fix this issue, edit {file} and ensure its contents are as follows: {contents}", array('{file}' => $configPath, '{contents}' => $contents)));
1856: }else{
1857:
1858: $key = implode(DIRECTORY_SEPARATOR,array(Yii::app()->basePath,'config','encryption.key'));
1859: $iv = implode(DIRECTORY_SEPARATOR,array(Yii::app()->basePath,'config','encryption.iv'));
1860: if(!file_exists($key) || !file_exists($iv)){
1861: try{
1862: $encryption = new EncryptUtil($key, $iv);
1863: $encryption->saveNew();
1864: }catch(Exception $e){
1865: throw new CException(Yii::t('admin', "Succeeded in setting the version info in the configuration, but failed to create a secure encryption key. The error message was: {message}", array('{message}' => $e->getMessage())));
1866: }
1867: }
1868:
1869: $this->configPermissions = 100600;
1870:
1871: if(isset($this->_configVars))
1872: unset($this->_configVars);
1873:
1874:
1875: return true;
1876: }
1877: }
1878:
1879: 1880: 1881:
1882: public function removeDatabaseBackup(){
1883: $dbBackup = realpath($this->dbBackupPath);
1884: if((bool) $dbBackup)
1885: unlink($dbBackup);
1886: }
1887:
1888: 1889: 1890: 1891:
1892: public function removeFiles($deletionList){
1893: foreach($deletionList as $file){
1894:
1895: $absFile = realpath("{$this->webRoot}/$file");
1896: if((bool) $absFile){
1897:
1898:
1899: $basename = pathinfo ($absFile, PATHINFO_BASENAME);
1900: if (basename ($file) === $basename)
1901: unlink($absFile);
1902: }
1903: }
1904: }
1905:
1906: 1907: 1908: 1909: 1910: 1911: 1912:
1913: public function renderCompatibilityMessages($h="h3",$htmlOptions=array()) {
1914: $compat = $this->getCompatibilityStatus();
1915: $web = !$this->isConsole;
1916: if($compat['allClear']) {
1917: return Yii::t('admin','No potential compatibility issues could be found.');
1918: }
1919: $messages = '';
1920:
1921:
1922: if($compat['req']['hasMessages']) {
1923: $reqLevels = array(
1924: 1 => Yii::t('admin', 'Minor'),
1925: 2 => Yii::t('admin', 'Major'),
1926: 3 => Yii::t('admin', 'Critical')
1927: );
1928: $messages .= $web ? '<dl>' : "\n";
1929: $definitions = array();
1930: foreach($reqLevels as $level => $label) {
1931: if(!empty($compat['req']['reqMessages'][$level])) {
1932: $definitions[$label] = $compat['req']['reqMessages'][$level];
1933: }
1934: }
1935: if(!empty($definitions)) {
1936: $header = Yii::t('admin','Some requirements for running X2Engine at the latest version are not met on this server:');
1937: $messages .= $web ? CHtml::tag($h,$htmlOptions,$header) : "$header";
1938: $messages .= $this->formatDefinitionList($definitions,$web);
1939: }
1940: }
1941:
1942: if($compat['databasePermissionError']) {
1943: $header = $compat['databasePermissionError'];
1944: $messages .= $web ? CHtml::tag($h,$htmlOptions,$header) : "$header\n";
1945: }
1946:
1947:
1948: if(count($compat['modules']) > 0) {
1949: $header = Yii::t('admin','The following custom modules conflict with new modules to be added:');
1950: $messages .= $web ? CHtml::tag($h,$htmlOptions,$header) : $header;
1951: $messages .= $web ? "<ul><li>".implode('</li><li>',$compat['modules'])."</li></ul>" : "\n\t".implode("\n\t",$compat['modules']);
1952: }
1953:
1954:
1955: if(count($compat['conflictingFields']) > 0) {
1956: $header = Yii::t('admin','The following preexisting fields conflict with fields to be added:');
1957: $messages .= $web ? CHtml::tag($h,$htmlOptions,$header) : $header;
1958: $messages .= $this->formatDefinitionList($compat['conflictingFields'],$web);
1959: }
1960:
1961:
1962: if(count($compat['customFiles']) > 0) {
1963: $header = Yii::t('admin','Note that the following files, of which there are local custom derivatives, will be changed:');
1964: $messages .= $web ? CHtml::tag($h,$htmlOptions,$header) : $header;
1965: $messages .= $web ? "<ul><li>".implode('</li><li>',$compat['customFiles'])."</li></ul>" : "\n\t".implode("\n\t",$compat['customFiles']);
1966: }
1967:
1968: return $messages;
1969: }
1970:
1971: 1972: 1973: 1974:
1975: public function requireDependencies(){
1976:
1977: $dependencies = $this->updaterFiles;
1978:
1979: $webUpdaterActions = $this->getWebUpdaterActions(false);
1980: foreach($webUpdaterActions as $name => $properties)
1981: $dependencies[] = self::classAliasPath($properties['class']);
1982: $actionsDir = Yii::app()->basePath.'/components/webupdater/';
1983: $utilDir = Yii::app()->basePath.'/components/util/';
1984: $refresh = !is_dir($actionsDir) || !is_dir($utilDir);
1985: foreach($dependencies as $relPath){
1986: $absPath = Yii::app()->basePath."/$relPath";
1987: if(!file_exists($absPath)){
1988: $refresh = true;
1989: $this->downloadSourceFile("protected/$relPath");
1990: }
1991: }
1992:
1993: if($refresh)
1994: $this->applyFiles(self::TMP_DIR);
1995: }
1996:
1997: 1998: 1999:
2000: public function resetAssets(){
2001: $assetsDir = realpath($this->webRoot.DIRECTORY_SEPARATOR.'assets');
2002: if(!(bool) $assetsDir)
2003: throw new CException(Yii::t('admin', 'Assets folder does not exist.'));
2004: $assets = array();
2005: foreach(scandir($assetsDir) as $n) {
2006: if (!in_array($n, array('..', '.'))) {
2007: $assets[] = $n;
2008: }
2009: }
2010: foreach($assets as $crcDir)
2011: FileUtil::rrmdir($assetsDir.DIRECTORY_SEPARATOR.$crcDir);
2012: }
2013:
2014: 2015: 2016: 2017: 2018:
2019: public function restoreDatabaseBackup(){
2020: try{
2021: $this->checkIf('canSpawnChildren');
2022: }catch(Exception $e){
2023: throw new CException(Yii::t('admin', 'Cannot restore database. {reason}', array('{reason}' => $e->getMessage())));
2024: }
2025: $bakFile = $this->dbBackupPath;
2026: $logFile = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', self::ERRFILE));
2027: $errFile = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', self::LOGFILE));
2028: $this->checkIfDatabaseBackupExists($bakFile);
2029: $descriptor = array(
2030: 0 => array('file', $bakFile, 'r'),
2031: 1 => array('file', $logFile, 'a'),
2032: 2 => array('file', $errFile, 'a'),
2033: );
2034:
2035: if((bool) $this->dbCommand){
2036:
2037:
2038: $this->dropAllTables();
2039: $backup = proc_open($this->dbCommand, $descriptor, $pipes, $this->webRoot);
2040: $ret = proc_close($backup);
2041: if($ret == -1)
2042: throw new CException(Yii::t('admin', "Database restore process did not exit cleanly. See the files {err} and {res} for output details.", array('{err}' => "protected/data/$errFile", '{res}' => "protected/data/$logFile")));
2043: else{
2044: return True;
2045: }
2046: }
2047: }
2048:
2049: 2050: 2051: 2052: 2053: 2054: 2055:
2056: public function runMigrationScripts($scripts, $ran, $backup){
2057: $that = $this;
2058: $script = '';
2059: $scriptExc = function($e) use(&$ran, &$script, $that, $backup){
2060: $that->handleSqlFailure ($script, $ran, $e->getMessage(), $backup, false);
2061: };
2062: $scriptErr = function($errno, $errstr, $errfile, $errline, $errcontext) use(&$ran, &$script, $that, $backup) {
2063: if (error_reporting () === 0) {
2064: return false;
2065: }
2066: $unrecoverable = array(
2067: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING
2068: );
2069: if (!in_array($errno, $unrecoverable)) {
2070: $that->handleSqlFailure ($script, $ran,
2071: "$errstr [$errno] : $errfile L$errline;", $backup, false);
2072: }
2073: };
2074: set_error_handler($scriptErr);
2075: set_exception_handler($scriptExc);
2076: sort($scripts);
2077:
2078: defined('YII_UNIT_TESTING') or define('YII_UNIT_TESTING',false);
2079: foreach($scripts as $script){
2080: $this->output(Yii::t('admin', 'Running migration script: {script}', array('{script}' => $script)));
2081: if (YII_UNIT_TESTING) {
2082:
2083: require($this->sourceDir.DIRECTORY_SEPARATOR.FileUtil::rpath($script));
2084: } else {
2085: require_once($this->sourceDir.DIRECTORY_SEPARATOR.FileUtil::rpath($script));
2086: }
2087: $ran[] = Yii::t('admin', 'migration script {file}', array('{file}' => $script));
2088: }
2089: restore_exception_handler();
2090: restore_error_handler();
2091: return $ran;
2092: }
2093:
2094: 2095: 2096: 2097: 2098: 2099:
2100: public function setChecksums($value) {
2101: $this->_checksums = $value;
2102: $this->_checksumsContent = null;
2103: }
2104:
2105: public function setChecksumsContent($value) {
2106: $this->_checksumsContent = $value;
2107: }
2108:
2109: 2110: 2111: 2112: 2113:
2114: public function setConfigPermissions($value){
2115: $mode = is_int($value) ? octdec($value) : octdec((int) "100$value");
2116: foreach(array('encryption.key', 'encryption.iv') as $file){
2117: $path = implode(DIRECTORY_SEPARATOR,array(Yii::app()->basePath,"config",$file));
2118: if(file_exists($path))
2119: chmod($path, $mode);
2120: }
2121: }
2122:
2123: public function setEdition($value) {
2124: $this->_edition = $value;
2125: }
2126:
2127: 2128: 2129: 2130:
2131: public function setManifest(array $value) {
2132: $this->_manifest = $value;
2133: }
2134:
2135: 2136: 2137: 2138: 2139:
2140: public function setNoHalt($value){
2141: self::$_noHalt = $value;
2142: }
2143:
2144: public function setScenario($value) {
2145:
2146: if(!in_array($value, array('update', 'upgrade'))) {
2147: throw new CException(Yii::t('admin','Invalid scenario: "{scenario}"',array('{scenario}'=>$this->_scenario)),self::ERR_SCENARIO);
2148: }
2149: $this->_scenario = $value;
2150: }
2151:
2152: 2153: 2154:
2155: public function setUniqueId($value) {
2156: $this->_uniqueId = $value;
2157: $this->settings->unique_id = $value;
2158: }
2159:
2160: public function setVersion($value) {
2161: $this->_version = $value;
2162: }
2163:
2164: 2165: 2166: 2167: 2168: 2169:
2170: public function sqlError($sqlFail, $sqlRun = array(), $errorMessage = null, $throw = true){
2171: if(!$this->isConsole)
2172: $errorMessage = CHtml::encode($errorMessage);
2173: $message = Yii::t('admin', 'A database change failed to apply: {sql}.', array('{sql}' => $sqlFail)).' ';
2174: if(count($sqlRun)){
2175: $message .= Yii::t('admin', '{n} changes were applied prior to this failure:', array('{n}' => count($sqlRun)));
2176:
2177: $sqlList = '';
2178: foreach($sqlRun as $sqlStatemt)
2179: $sqlList .= ($this->isConsole ? "\n$sqlStatemt" : '<li>'.CHtml::encode($sqlStatemt).'</li>');
2180: $message .= $this->isConsole ? $sqlList : "<ol>$sqlList</ol>";
2181: $message .= "\n".Yii::t('admin', "Please save the above list.")." \n\n";
2182: }
2183: if($errorMessage !== null){
2184: $message .= Yii::t('admin', "The error message given was:")." $errorMessage";
2185: }
2186:
2187: $message .= "\n\n".Yii::t('admin', "Update failed.");
2188: if(!$this->isConsole)
2189: $message = str_replace("\n", "<br />", $message);
2190: if($throw) {
2191: throw new CException($message,self::ERR_DATABASE);
2192: } else {
2193: $this->respond($message,1,1);
2194: }
2195:
2196: }
2197:
2198: public function testDatabasePermissions(){
2199: $missingPerms = array();
2200: $con = Yii::app()->db->pdoInstance;
2201:
2202: try{
2203: $con->exec("CREATE TABLE IF NOT EXISTS `x2_test_table` (
2204: `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
2205: `a` varchar(10) NOT NULL,
2206: PRIMARY KEY (`id`))");
2207: }catch(PDOException $e){
2208: $missingPerms[] = 'create';
2209: }
2210:
2211:
2212: try{
2213: $con->exec("INSERT INTO `x2_test_table` (`id`,`a`) VALUES (1,'a')");
2214: }catch(PDOException $e){
2215: $missingPerms[] = 'insert';
2216: }
2217:
2218:
2219: try{
2220: $con->exec("DELETE FROM `x2_test_table`");
2221: }catch(PDOException $e){
2222: $missingPerms[] = 'delete';
2223: }
2224:
2225:
2226: try{
2227: $con->exec("ALTER TABLE `x2_test_table` ADD COLUMN `b` varchar(10) NULL;");
2228: }catch(PDOException $e){
2229: $missingPerms[] = 'alter';
2230: }
2231:
2232:
2233: try{
2234: $con->exec("DROP TABLE `x2_test_table`");
2235: }catch(PDOException $e){
2236: $missingPerms[] = 'drop';
2237: }
2238:
2239: if(empty($missingPerms)){
2240: return false;
2241: }else{
2242: return Yii::t('admin', 'Database user {u} does not have adequate permisions on database {db} to perform updates; it does not have the following permissions: {perms}', array(
2243: '{u}' => $this->dbParams['dbuser'],
2244: '{db}' => $this->dbParams['dbname'],
2245: '{perms}' => implode(',', array_map(function($m){
2246: return Yii::t('app', $m);
2247: }, $missingPerms))
2248: ));
2249: }
2250: }
2251:
2252: 2253: 2254: 2255:
2256: public function unpack() {
2257: $package = $this->updatePackage;
2258: if(!file_exists($package))
2259: throw new Exception(Yii::t('admin','No update package could be found.'),self::ERR_NOUPDATE);
2260: if(file_exists($this->updateDir))
2261: throw new Exception(Yii::t('admin','Could not extract package; destination path {path} already exists.',array('{path}'=>$this->updateDir)),self::ERR_ISLOCKED);
2262: mkdir($this->updateDir);
2263: $zip = new ZipArchive;
2264: $zip->open($package);
2265: $zip->extractTo($this->updateDir);
2266:
2267: if(file_exists($htaccess = Yii::app()->basePath.DIRECTORY_SEPARATOR.'.htaccess'))
2268: copy($htaccess,$this->updateDir.DIRECTORY_SEPARATOR.'.htaccess');
2269: }
2270:
2271: 2272: 2273: 2274: 2275: 2276:
2277: public function updateUpdater($updaterCheck){
2278:
2279: if(version_compare($this->configVars['updaterVersion'], $updaterCheck) >= 0)
2280: return array();
2281:
2282: $updaterFiles = $this->updaterFiles;
2283:
2284:
2285: $md5sums_content = FileUtil::getContents($this->updateServer.'/'.$this->getUpdateDataRoute($this->configVars['updaterVersion']).'/contents.md5');
2286:
2287: $tryJson = json_decode($md5sums_content,1);
2288: if(!(bool) $md5sums_content) {
2289: $admin = CActiveRecord::model('Admin')->findByPk(1);
2290: if ($this->scenario === 'upgrade' && isset($admin) && empty($admin->unique_key)) {
2291: $updaterSettingsLink = CHtml::link(Yii::t('admin', 'Updater Settings page'), array('admin/updaterSettings'));
2292: throw new CException(Yii::t('admin','You must first set a product key on the '.$updaterSettingsLink));
2293: } else {
2294: throw new CException(Yii::t('admin','Unknown update server error.'),self::ERR_UPSERVER);
2295: }
2296: } else if(is_array($tryJson)) {
2297:
2298: if(isset($tryJson['errors'])) {
2299: throw new CException($tryJson['errors']);
2300: } else {
2301: throw new CException(Yii::t('admin','Unknown update server error.').' '.$md5sums_content);
2302: }
2303: }
2304: preg_match_all(':^(?<md5sum>[a-f0-9]{32})\s+source/protected/(?<filename>\S.*)$:m',$md5sums_content,$md5s);
2305: $md5sums = array();
2306: for($i=0;$i<count($md5s[0]);$i++) {
2307: $md5sums[$md5s['md5sum'][$i]] = $md5s['filename'][$i];
2308: }
2309:
2310: $updaterFiles = array_intersect($md5sums,$updaterFiles);
2311:
2312:
2313: $failed2Retrieve = array();
2314: foreach($updaterFiles as $md5 => $file){
2315: $pass = 0;
2316: $tries = 0;
2317: $downloadedFile = FileUtil::relpath(implode(DIRECTORY_SEPARATOR, array($this->webRoot,self::TMP_DIR,'protected',FileUtil::rpath($file))), $this->thisPath.DIRECTORY_SEPARATOR);
2318: while(!$pass && $tries < 2){
2319: $remoteFile = $this->updateServer.'/'.$this->sourceFileRoute."/protected/$file";
2320: try{
2321: $this->downloadSourceFile("protected/$file");
2322: }catch(Exception $e){
2323: break;
2324: }
2325:
2326: $pass = md5_file($downloadedFile) == $md5;
2327: $tries++;
2328: }
2329: if(!$pass)
2330: $failed2Retrieve[] = "protected/$file";
2331: }
2332:
2333: $failedDownload = (bool) count($failed2Retrieve);
2334:
2335: if(!$failedDownload && (bool) count($updaterFiles)) {
2336: $this->applyFiles(self::TMP_DIR);
2337:
2338: FileUtil::rrmdir($this->webRoot.DIRECTORY_SEPARATOR.self::TMP_DIR);
2339: } else {
2340: $errorResponse = json_decode($md5sums_content,1);
2341: if(isset($errorResponse['errors'])) {
2342: throw new CException($errorResponse['errors']);
2343: }
2344: }
2345:
2346:
2347:
2348: if(!$failedDownload) {
2349: $this->regenerateConfig(Null, $updaterCheck, Null);
2350: }
2351: return $failed2Retrieve;
2352: }
2353:
2354: }
2355:
2356: ?>
2357: