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

  • AccountsGridViewProfileWidget
  • ActionMenu
  • ActionsGridViewProfileWidget
  • ActionsQuickCreateRelationshipBehavior
  • ActiveDateRangeInput
  • ApplicationConfigBehavior
  • Attachments
  • ChatBox
  • CommonControllerBehavior
  • ContactMapInlineTags
  • ContactsGridViewProfileWidget
  • CronForm
  • CSaveRelationsBehavior
  • DateRangeInputsWidget
  • DocsGridViewProfileWidget
  • DocViewer
  • DocViewerProfileWidget
  • EButtonColumnWithClearFilters
  • EmailDeliveryBehavior
  • EmailProgressControl
  • EncryptedFieldsBehavior
  • EventsChartProfileWidget
  • FileUploader
  • FontPickerInput
  • Formatter
  • FormView
  • GridViewWidget
  • History
  • IframeWidget
  • ImportExportBehavior
  • InlineActionForm
  • InlineEmailAction
  • InlineEmailForm
  • InlineEmailModelBehavior
  • InlineQuotes
  • JSONEmbeddedModelFieldsBehavior
  • JSONFieldsDefaultValuesBehavior
  • LeadRoutingBehavior
  • LeftWidget
  • LoginThemeHelper
  • LoginThemeHelperBase
  • MarketingGridViewProfileWidget
  • MediaBox
  • MessageBox
  • MobileFormatter
  • MobileFormLayoutRenderer
  • MobileLayoutRenderer
  • MobileLoginThemeHelper
  • MobileViewLayoutRenderer
  • ModelFileUploader
  • NewWebLeadsGridViewProfileWidget
  • NormalizedJSONFieldsBehavior
  • NoteBox
  • OnlineUsers
  • OpportunitiesGridViewProfileWidget
  • Panel
  • ProfileDashboardManager
  • ProfileGridViewWidget
  • ProfileLayoutEditor
  • ProfilesGridViewProfileWidget
  • Publisher
  • PublisherActionTab
  • PublisherCalendarEventTab
  • PublisherCallTab
  • PublisherCommentTab
  • PublisherEventTab
  • PublisherSmallCalendarEventTab
  • PublisherTab
  • PublisherTimeTab
  • QuickContact
  • QuickCreateRelationshipBehavior
  • QuotesGridViewProfileWidget
  • RecordAliasesWidget
  • RecordViewLayoutManager
  • RecordViewWidgetManager
  • RememberPagination
  • Reminders
  • ResponseBehavior
  • ResponsiveHtml
  • SearchIndexBehavior
  • ServicesGridViewProfileWidget
  • SmallCalendar
  • SmartActiveDataProvider
  • SmartDataProviderBehavior
  • SmartSort
  • SocialForm
  • SortableWidgetManager
  • SortableWidgets
  • TagBehavior
  • TagCloud
  • TemplatesGridViewProfileWidget
  • TimeZone
  • TopContacts
  • TopSites
  • TransformedFieldStorageBehavior
  • TranslationLogger
  • TwitterFeed
  • TwoColumnSortableWidgetManager
  • UpdaterBehavior
  • UpdatesForm
  • UserIdentity
  • UsersChartProfileWidget
  • WorkflowBehavior
  • X2ActiveGridView
  • X2ActiveGridViewForSortableWidgets
  • X2AssetManager
  • X2AuthManager
  • X2ChangeLogBehavior
  • X2ClientScript
  • X2Color
  • X2DateUtil
  • X2FixtureManager
  • X2FlowFormatter
  • X2GridView
  • X2GridViewBase
  • X2GridViewForSortableWidgets
  • X2GridViewSortableWidgetsBehavior
  • X2LeadsGridViewProfileWidget
  • X2LinkableBehavior
  • X2ListView
  • X2PillBox
  • X2ProgressBar
  • X2SmartSearchModelBehavior
  • X2TimestampBehavior
  • X2TranslationBehavior
  • X2UrlRule
  • X2WebModule
  • X2Widget
  • X2WidgetList
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: 
   3: /*****************************************************************************************
   4:  * X2Engine Open Source Edition is a customer relationship management program developed by
   5:  * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
   6:  * 
   7:  * This program is free software; you can redistribute it and/or modify it under
   8:  * the terms of the GNU Affero General Public License version 3 as published by the
   9:  * Free Software Foundation with the addition of the following permission added
  10:  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
  11:  * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
  12:  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
  13:  * 
  14:  * This program is distributed in the hope that it will be useful, but WITHOUT
  15:  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  16:  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
  17:  * details.
  18:  * 
  19:  * You should have received a copy of the GNU Affero General Public License along with
  20:  * this program; if not, see http://www.gnu.org/licenses or write to the Free
  21:  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  22:  * 02110-1301 USA.
  23:  * 
  24:  * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
  25:  * California 95067, USA. or at email address contact@x2engine.com.
  26:  * 
  27:  * The interactive user interfaces in modified source and object code versions
  28:  * of this program must display Appropriate Legal Notices, as required under
  29:  * Section 5 of the GNU Affero General Public License version 3.
  30:  * 
  31:  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
  32:  * these Appropriate Legal Notices must retain the display of the "Powered by
  33:  * X2Engine" logo. If the display of the logo is not reasonably feasible for
  34:  * technical reasons, the Appropriate Legal Notices must display the words
  35:  * "Powered by X2Engine".
  36:  *****************************************************************************************/
  37: 
  38: Yii::import('application.components.ResponseBehavior');
  39: Yii::import('application.models.Admin');
  40: 
  41: // Extra safeguard, in case automatic creation fails, to maintain that the
  42: // sub-components-directories aliases are valid:
  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:  * Behavior class with application updater/upgrader utilities.
  62:  *
  63:  * Note to all future developers: it is important to bear in mind that if you
  64:  * need to make changes to the updates system or the updater in general, they
  65:  * must be backwards-compatible with all earlier versions of the software (or as
  66:  * far back as possible).
  67:  *
  68:  * @property string $backCompatFile Path to the backwards compatibility flag file.
  69:  * @property array $checksums When running an update, this is a list of all MD5 hashes of files to be applied, with filenames their keys and checksums their values.
  70:  * @property string $checksumsContent The contents of the package contents digest file.
  71:  * @property array $compatibilityStatus An array specifying compatibility issues.
  72:  * @property array $configVars (read-only) variables imported from the configuration
  73:  * @property string $dbBackupCommand (read-only) command to be used for backing up the database
  74:  * @property string $dbBackupPath (read-only) Full path to the database backup file.
  75:  * @property string $dbCommand (read-only) command to be used for running SQL from files
  76:  * @property array $dbParams (read-only) Database information retrieved from {@link CDbConnection}
  77:  * @property string $edition (read-only) The edition of the installation of X2Engine.
  78:  * @property array $files (read-only) A list of files and their statuses (present, missing or corrupt).
  79:  * @property array $filesByStatus (read-only) An array of files in each status category
  80:  * @property array $filesStatus (read-only) A summary (showing counts) of all files' statuses.
  81:  * @property string $latestUpdaterVersion (read-only) The latest version of the updater utility according to the updates server
  82:  * @property string $lockFile Path to the file to use for locking when applying changes
  83:  * @property array $manifest When running an update, this is the change manifest as retrieved from the update package
  84:  * @property boolean $noHalt Whether to terminate the PHP process if errors occur
  85:  * @property PDO $pdo (read-only) The app's PDO instance
  86:  * @property array $requirements (read-only) Requirements script output.
  87:  * @property string $scenario Usage scenario, i.e. update/upgrade
  88:  * @property string $sourceDir (read-only) Absolute path to the base directory of source files to be applied in the update/upgrade
  89:  * @property string $sourceFileRoute (read-only) Route (relative URL on the updates server) from which to download source files in a pinch
  90:  * @property string $thisPath (read-only) Absolute path to the current working directory
  91:  * @property string $uniqueId (read-only) Unique ID of the installation
  92:  * @property string $updateDataRoute (read-only) Relative URL (to the base URL of the update server) from which to get update manifests.
  93:  * @property string $updateDir (read-only) the directory of updates.
  94:  * @property string $updatePackage (read-only) destination path for the update package.
  95:  * @property string $updateServer Base URL of the web server from which to fetch data and files
  96:  * @property string $version Version of X2Engine
  97:  * @property string $webRoot (read-only) Absolute path to the web root, even if not in a web request
  98:  * @property array $webUpdaterActions (read-only) array of actions in the web-based updater utility.
  99:  * @package application.components
 100:  * @author Demitri Morgan <demitri@x2engine.com>
 101:  */
 102: class UpdaterBehavior extends ResponseBehavior {
 103:     ///////////////
 104:     // CONSTANTS //
 105:     ///////////////
 106: 
 107:     /**
 108:      * SQL backup dump file
 109:      */
 110: 
 111:     const BAKFILE = 'update_backup.sql';
 112: 
 113:     /**
 114:      * Defines a file that (for extra security) prevents race conditions in the unlikely event that 
 115:      * multiple requests to the web updater to enact file/database changes are made.
 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:     // Whatever you do, DO NOT change this to a blank string. It WILL result in
 130:     // the obliteration of all files in the app!
 131:     const UPDATE_DIR = 'update';
 132: 
 133:     const SECURITY_IMG = 'cG93ZXJlZF9ieV94MmVuZ2luZS5wbmc=';
 134: 
 135:     // Error codes:
 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:     // File statuses:
 161: 
 162:     const FILE_PRESENT = 0;
 163: 
 164:     const FILE_CORRUPT = 1;
 165: 
 166:     const FILE_MISSING = 2;
 167: 
 168: 
 169:     ///////////////////////
 170:     // STATIC PROPERTIES //
 171:     ///////////////////////
 172: 
 173:     /**
 174:      * Set to true in cases of testing, to avoid having errors end PHP execution.
 175:      * @var boolean
 176:      */
 177:     private static $_noHalt = false;
 178: 
 179:     /**
 180:      * Core configuration file name.
 181:      */
 182:     public static $configFilename = 'X2Config.php';
 183: 
 184:     /**
 185:      * Configuration file variables as [variable name] => [value quote wrap]
 186:      * as can be found in the file protected/config/X2Config.php
 187:      * @var array
 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:      * Explicit override of default {@link ResponseBehavior::$_logCategory}
 204:      * @var string
 205:      */
 206:     public static $_logCategory = 'application.updater';
 207: 
 208:     ///////////////////////////
 209:     // NON-STATIC PROPERTIES //
 210:     ///////////////////////////
 211: 
 212:     private $_canSpawnChildren = false;
 213: 
 214:     /**
 215:      * Holds the value of {@link checksums}
 216:      * @var array
 217:      */
 218:     private $_checksums;
 219: 
 220:     /**
 221:      * Holds the contents of the checksums file.
 222:      * @var type
 223:      */
 224:     private $_checksumsContent;
 225: 
 226:     private $_checksumsAvail = false;
 227: 
 228:     private $_compatibilityStatus;
 229: 
 230:     private $_configVars;
 231: 
 232:     /**
 233:      * True if a backup of the database is available.
 234:      * @var bool
 235:      */
 236:     private $_databaseBackupExists = false;
 237: 
 238:     /**
 239:      * Command to use for backing up the database.
 240:      * @var string
 241:      */
 242:     private $_dbBackupCommand;
 243: 
 244:     /**
 245:      * Full path to the database backup file
 246:      * @var type
 247:      */
 248:     private $_dbBackupPath;
 249: 
 250:     /**
 251:      * Command to use for explicitly running SQL commands from a file:
 252:      * @var string
 253:      */
 254:     private $_dbCommand;
 255: 
 256:     /**
 257:      * DSN parameters taken from {@link CDbConnection}
 258:      * @var array
 259:      */
 260:     private $_dbParams;
 261: 
 262:     /**
 263:      * The application's edition.
 264:      */
 265:     private $_edition;
 266: 
 267:     /**
 268:      * An array showing the status of files to be applied.
 269:      * @var array
 270:      */
 271:     private $_files;
 272: 
 273:     /**
 274:      * An array indexed by status of arrays of files with each status.
 275:      */
 276:     private $_filesByStatus;
 277: 
 278:     /**
 279:      * An array showing file stats based on {@link files}
 280:      * @var type
 281:      */
 282:     private $_filesStatus;
 283: 
 284:     /**
 285:      * Latest version of the updater utility according to the updates server.
 286:      * @var string
 287:      */
 288:     private $_latestUpdaterVersion;
 289: 
 290:     /**
 291:      * Stores the manifest as retrieved from the file.
 292:      * @var array
 293:      */
 294:     private $_manifest;
 295: 
 296:     /**
 297:      * If true, indicates that the manifest file is available.
 298:      * @var bool
 299:      */
 300:     private $_manifestAvail = false;
 301: 
 302:     /**
 303:      * If true, indicates that the package to be applied is actually applicable,
 304:      * i.e. if it's an update from the current version to a later version.
 305:      * @var type
 306:      */
 307:     private $_packageApplies = false;
 308:     
 309:     /**
 310:      * If true, indicates that the update/upgrade package indeed exists on the
 311:      * local filesystem.
 312:      * @var bool
 313:      */
 314:     private $_packageExists = false;
 315: 
 316:     /**
 317:      * Stores output of the requirements check script
 318:      * @var array
 319:      */
 320:     private $_requirements;
 321: 
 322:     /**
 323:      * stores {@link scenario}
 324:      */
 325:     private $_scenario;
 326: 
 327:     /**
 328:      * Stores the application settings object
 329:      * @var Admin
 330:      */
 331:     private $_settings;
 332: 
 333:     /**
 334:      * Stores {@link sourceDir}
 335:      * @var string
 336:      */
 337:     private $_sourceDir;
 338: 
 339:     /**
 340:      * Current working directory.
 341:      * @var string 
 342:      */
 343:     private $_thisPath;
 344: 
 345:     /**
 346:      * Unique ID of the install.
 347:      * @var type
 348:      */
 349:     private $_uniqueId;
 350: 
 351:     /**
 352:      * Version of X2Engine.
 353:      */
 354:     private $_version;
 355: 
 356:     /**
 357:      * Absolute path to the web root
 358:      * @var string
 359:      */
 360:     private $_webRoot;
 361: 
 362:     private $_webUpdaterActions;
 363:     
 364:     /**
 365:      * List of files used by the behavior
 366:      * @var array
 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:      * Converts an array formatted like a behavior or controller actions array
 381:      * entry and returns the path (relative to {@link X2WebApplication.basePath}
 382:      * to the class file. {@link Yii::getPathOfAlias()} is unsafe to use,
 383:      * because in cases where this function is to be used, the files may not
 384:      * exist yet.
 385:      *
 386:      * @param array $classes An array containing a "class" => [Yii path alias] entry
 387:      */
 388:     public static function classAliasPath($alias){
 389:         return preg_replace(':^application/:', '', str_replace('.', '/', $alias)).'.php';
 390:     }
 391: 
 392:     /**
 393:      * In the case of a failed update or other event, restore files from a
 394:      * backup location.
 395:      *
 396:      * @param array $fileList Array of paths relative to webroot to restore from backup.
 397:      * @param string $dir Backup directory
 398:      */
 399:     public function applyFiles($dir=null){
 400:         $success = true;
 401:         $copiedFiles = array();
 402:         
 403:         if(!empty($dir)) // Recursively copy a folder relative to webroot
 404:             $success = $this->copyFile($dir);
 405:         else{ // Copy files individually from source according to the manifest
 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:      * Backwards compatibility hacks - I mean, hooks - to run after self-updating.
 428:      *
 429:      * Sometimes, downloading a copy of itself isn't enough. The updater must do
 430:      * additional work after it self-updates in order to resolve unforeseen
 431:      * post-refresh issues.
 432:      *
 433:      * This works by creating a file in the runtime folder that counts as
 434:      * evidence that it has been run already and thus does not need to be run
 435:      * again (to avoid endless redirect loops in the web updater, for instance).
 436:      *
 437:      * This was added because the decision was made to add ResponseUtil as a
 438:      * dependency, yet because the file already exists as of many versions
 439:      * before, it wouldn't be automatically fetched, because the earlier version
 440:      * of the updater wouldn't have known that it needed to be updated first.
 441:      * 
 442:      * @return bool True or false; true indicates that action has been taken,
 443:      *  whereas false indicates no action needs to be taken nor has been taken.
 444:      */
 445:     public function backCompatHooks($latestUpdaterVersion) {
 446:         $runFlag = $this->backCompatFile;
 447:         if(file_exists($runFlag)) {
 448:             return false;
 449:         }
 450:         // Create the "flag" file as evidence that this function has been run:
 451:         if(@file_put_contents($runFlag,time()) === false)
 452:             return false; // Nothing more that can or should be done past here.
 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:         // This variable indicates that a second self-update should be performed:
 461:         $action = false;
 462: 
 463:         // Missing requirement ResponseUtil before 4.0:
 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:         // Any other problems that arise in future versions to go here
 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:      * Checks for the existence of an unpacked update package folder, and if
 486:      * present, whether all files are present and complete.
 487:      */
 488:     public function checkFiles(){
 489:         // Check integrity of files:
 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:      * Generic dependency and prerequisite checking function.
 506:      *
 507:      * A wrapper for all functions with names beginning with "checkIf"; runs a
 508:      * test if it hasn't been run already and returns the result (or throws an
 509:      * exception). Stores the result of a check so that it isn't necessary to
 510:      * run the check again.
 511:      *
 512:      * All "checkIf" functions must have the rest of their names named after a
 513:      * condition, i.e. "AllClear" to "checkIfAllClear", and have a corresponding 
 514:      * private property named accordingly (i.e. for "checkIfFoo" the property
 515:      * must be named "_foo"). The correspoding property must have a default
 516:      * value of false (boolean).
 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:      * Checks whether it is possible to run system commands using PHP's
 526:      * {@link proc_open()} function.
 527:      * 
 528:      * @param type $throw
 529:      * @return boolean
 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:      * Checks if the package content digests file is present and not empty.
 544:      *
 545:      * Said file (specifically, its content) is necessary for checking whether
 546:      * files were downloaded and extracted properly.
 547:      * 
 548:      * @param type $throw
 549:      * @return boolean
 550:      * @throws CException
 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:      * Checks to see if a file exists and isn't very old..
 574:      * @param type $bakFile
 575:      * @throws Exception
 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{ // Test the timestamp of the backup copy, just to be extra sure it's safe to use
 585:             $backupTime = filemtime($bakFile);
 586:             $currenTime = time();
 587:             if($currenTime - $backupTime > 86400) { // Updating the software should NEVER take a whole day!
 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:      * Checks if the manifest file is present and intact.
 599:      * 
 600:      * @param type $throw If false, returns; if true, throws or returns based on
 601:      *  the success of the check
 602:      * @return bool|string If not throwing: it will be the string representing
 603:      *  the relative path to the manifest if it exists, and false if it doesn't.
 604:      * @throws CException
 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:      * Ensures that the package actually applies to the current version and
 630:      * edition.
 631:      * @throws CException
 632:      */
 633:     public function checkIfPackageApplies($throw = true) {
 634:         if(!$this->checkIf('manifestAvail',$throw))
 635:             return false;
 636:         // Wrong updater version
 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:         // Wrong initial app version
 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:         // Wrong initial app edition
 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:         // Wrong scenario
 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:      * Check to see if there is an update package present in the filesystem.
 675:      *
 676:      * @param bool $throw Whether or not to throw an exception instead of returning false
 677:      * @return boolean True if the update package directory and contents digest
 678:      *  file are present; false otherwise
 679:      * @throws CException
 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:      * Securely obtain the latest version.
 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)  // set request timeout in seconds
 704:                 ));
 705:         $updateCheckUrl = $this->updateServer.'/installs/updates/check?'.http_build_query(compact('i', 'v'), '', '&');
 706:         // Get a "secure" code from the server
 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'){ // if the latest version is newer than our version and updates are enabled
 726:                 Yii::app()->session['versionCheck'] = false;
 727:                 Yii::app()->session['newVersion'] = $newVersion;
 728:             }
 729:         }
 730:         return $newVersion;
 731:     }
 732: 
 733:     /**
 734:      * Deletes the update package folder
 735:      *
 736:      * @param string $dir
 737:      */
 738:     public function cleanUp(){
 739:         FileUtil::rrmdir($this->updateDir);
 740:         FileUtil::rrmdir($this->updatePackage);
 741:     }
 742: 
 743:     /**
 744:      * Copies files out of a folder and into the live installation. 
 745:      * 
 746:      * Wrapper for {@link FileUtil::ccopy} for updates that can operate
 747:      * recursively without requiring a list of files.
 748:      *
 749:      * @param string $path Path relative to the web root to be copied 
 750:      *  (this is the target, unless dir is null, in which case it's the source)
 751:      * @param string $file The path to copy (assumed relative to the webroot)
 752:      * @param string $dir The name of the backup directory; "." means top-level directory
 753:      */
 754:     public function copyFile($path, $dir = null, $ds = DIRECTORY_SEPARATOR){
 755: 
 756:         // Resolve paths
 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:                         // The target shall be the object itself if in the
 771:                         // root level of the backup directory; otherwise,
 772:                         // prepend the path up to the current point (which is
 773:                         // copied in through the recursion levels in the stack)
 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:      * Obtains update/upgrade data package from the server.
 791:      * @param type $version
 792:      * @param type $uniqueId
 793:      * @param type $edition
 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:      * Retrieves a file from the update server. It will be stored in a temporary
 809:      * directory, "tmp", in the web root. To copy it into the live install, use
 810:      * restoreBackup on target "tmp".
 811:      *
 812:      * @param string $route Route relative to the web root of the web root path in the X2Engine source code
 813:      * @param string $file Path relative to the X2Engine web root of the file to be downloaded
 814:      * @param integer $maxAttempts Maximum times to attempt to download the file before giving up and throwing an exception.
 815:      * @return boolean
 816:      * @throws Exception
 817:      */
 818:     public function downloadSourceFile($file, $route = null, $maxAttempts = 5){
 819:         if(empty($route)) // Auto-construct a route based on ID & edition info:
 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:      * Drops all tables in the database.
 837:      *
 838:      * This function is used to eliminate new tables that get created during
 839:      * updates that fail. This allows the next update attempt to be compatible,
 840:      * because any tables that get created in the process won't then be included
 841:      * and thus won't interfere with the update. In other words, when restoring
 842:      * a database backup, tables created since the time of the backup won't get
 843:      * dropped, and that is why this function is actually necessary.
 844:      *
 845:      * This function SHOULD NOT BE CALLED ANYWHERE except in
 846:      * {@link restoreDatabaseBackup}, and the test wrapper function, and only
 847:      * if a backup copy of the database exists.
 848:      */
 849:     private function dropAllTables(){
 850:         if($this->dbParams['server'] == 'mysql'){
 851:             // Generator command for the drop statements:
 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:                 // Generate drop commands:
 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:                 // Open the SQL runner command:
 874:                 $dtRunProc = proc_open($dtRun, $descriptorRun, $pipesRun);
 875:                 // Prevent foreign key constraints from halting progress:
 876:                 fwrite($pipesRun[0], 'SET FOREIGN_KEY_CHECKS=0;');
 877:                 // Loop through output and run the drop commands (which should
 878:                 // each be contained within single lines):
 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:         } // No other DB types supported yet
 890:     }
 891: 
 892:     /**
 893:      * Finalizes an update/upgrade by applying file, database and configuration changes.
 894:      *
 895:      * This method replaces the SQL method as well as finishing copying files over.
 896:      * Both of these happen at once to prevent issues from files depending on SQL
 897:      * changes or vice versa.
 898:      *
 899:      * @param array $params parameters for update or upgrade
 900:      */
 901:     public function enactChanges($autoRestore = false){
 902:         // Check for a lockfile:
 903:         $lockFile = $this->lockFile;
 904:         if(file_exists($lockFile)) {
 905:             $lockTime =  (int) trim(file_get_contents($lockFile));
 906:             if(time()-$lockTime > 3600) // No operation should take longer than an hour
 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:         // One last check: that the package exists and is applicable:
 913:         $this->checkIf('packageApplies');
 914: 
 915:         // Check that all the files in the update package are present and intact:
 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:         // No turning back now. This is it!
 926:         //
 927:         // Create the lockfile:
 928:         FileUtil::createLockfile($lockFile);
 929: 
 930:         // Run the necessary database changes:
 931:         try{
 932:             $this->output(Yii::t('admin','Enacting changes to the database...'));
 933:             $this->enactDatabaseChanges($autoRestore);
 934:         }catch(Exception $e){
 935:             // The operation cannot proceed and is technically finished 
 936:             // executing, so there's no use keeping the lock file around except
 937:             // to frustrate and confuse the end user.
 938:             FileUtil::removeLockfile($lockFile);
 939:             // Toss the Exception back up so it propagates through the stack and
 940:             // the caller can use its message for responding to the user:
 941:             throw $e;
 942:         }
 943: 
 944:         $lastException = null;
 945: 
 946:         try{
 947:             // The hardest part of the update (database changes) is now done. If any
 948:             // errors occurred in the database changes, they should have thrown
 949:             // exceptions with appropriate messages by now.
 950:             //
 951:             // Now, copy the cache of downloaded files into the live install:
 952:             $this->output(Yii::t('admin','Enacting changes to the fileset...'));
 953:             $this->applyFiles();
 954:             // Delete old files:
 955:             $this->removeFiles($this->manifest['deletionList']);
 956:             $this->output(Yii::t('admin','Cleaning up...'));
 957:             if($this->scenario == 'update'){
 958:                 $this->resetAssets();
 959:                 // Apply configuration changes and clear out the assets folder:
 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:                 // Change the edition and product key to reflect the upgrade:
 964:                 $admin = CActiveRecord::model('Admin')->findByPk(1);
 965:                 // refresh admin schema since it may have changed during db changes
 966:                 Yii::app()->db->schema->refresh ();
 967:                 $admin->refreshMetaData ();
 968:                 $admin->edition = $this->manifest['targetEdition'];
 969:                 if(!(empty($this->uniqueId)||$this->uniqueId=='none')) // Set new unique id
 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:         // Remove the lock file
 979:         FileUtil::removeLockfile($lockFile);
 980:         // Remove the backwards compatibility flag since the update is now done
 981:         if(file_exists($bcFile = $this->backCompatFile))
 982:             unlink($bcFile);
 983: 
 984:         // Clear the cache
 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:         // Clear the auth cache
 992:         Yii::app()->db->createCommand('DELETE FROM x2_auth_cache WHERE 1')->execute();
 993:         if($this->scenario == 'update'){
 994:             // Log everyone out; session data may now be obsolete.
 995:             Yii::app()->db->createCommand('DELETE FROM x2_sessions')->execute();
 996:         }
 997: 
 998:         // Done.
 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:      * Runs a list of SQL commands.
1008:      *
1009:      * @param bool $backup Whether to restore the database backup in the case of a database failure
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{ // Run the update SQL.
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){ // A database change failed to apply
1032:                             if($skipOnFail[$delta])
1033:                                 continue;
1034:                             $sqlErr = $e->getMessage();
1035:                             try{
1036:                                 $this->handleSqlFailure ($sql, $sqlRun, $sqlErr, $backup);
1037:                             }catch(Exception $re){ // Database recovery failed. We're SOL
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:      * Handle database backups in the event of failure
1055:      * @param string $error SQL Error
1056:      * @param bool $backup Whether to restore from backup
1057:      */
1058:     public function handleSqlFailure($sql, $sqlRun, $sqlErr, $backup, $throw = true) {
1059:         if ($backup) { // Run the recovery
1060:             $this->restoreDatabaseBackup();
1061:             $dbRestoreMessage = Yii::t('admin', 'The database has been restored to the backup copy.');
1062:         } else { // No recovery available; print messages instead
1063:             if((bool) realpath($this->dbBackupPath)) // Backup available
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 // No backup available
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:      * Notify the server that the update has finished
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:      * Generates a "definition list"
1089:      * @param type $list Array with keys the terms and values their definition entries
1090:      * @param type $web Whether to generate markup (if true) or console output (if false)
1091:      * @return type
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"; // .implode("\n\t\t",$compat['req']['reqMessages'][$level]);
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:      * Returns the path to the backwards-compatibility flag file.
1109:      * @return string
1110:      */
1111:     public function getBackCompatFile() {
1112:         return implode(DIRECTORY_SEPARATOR,array(Yii::app()->basePath,'runtime',self::BCOFILE));
1113:     }
1114: 
1115:     /**
1116:      * Parse output formatted according to that of md5sum into an array of
1117:      * hashes indexed by filename. If no output is specified, the update
1118:      * package's content digest will be used as input.
1119:      *
1120:      * Obtains {@link checksums}.
1121:      * @param string $checksums Optional, to override package digest file
1122:      *  content. If specified, the property won't be altered; the getter will be
1123:      *  used as an auxiliary parsing function.
1124:      * @return array
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:      * Checks the current X2Engine installation for compatibility issues with the
1147:      * current package as defined in the manifest and requirements check script.
1148:      *
1149:      * @return array
1150:      */
1151:     public function getCompatibilityStatus(){
1152:         if(!isset($this->_compatibilityStatus)){
1153:             // A variable which is the catch-all flag for there being messages for
1154:             // the user to heed:
1155:             $allClear = true;
1156: 
1157:             ////////////////////////////////
1158:             // Check system requirements: //
1159:             ////////////////////////////////
1160:             
1161:             $req = $this->requirements;
1162:             $allClear = $allClear && $req['canInstall'];
1163: 
1164:             /////////////////////////////////
1165:             // Check database permissions: //
1166:             /////////////////////////////////
1167:             $databasePermissionError = $this->testDatabasePermissions();
1168:             $allClear = $allClear && !$databasePermissionError;
1169: 
1170:             /////////////////////////////////////////////////////////////////////////
1171:             // Check that user hasn't created any custom modules that are the same //
1172:             // name as new modules added in the update:                            //
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:             // Check fields records for conflicts with custom fields: //
1196:             ////////////////////////////////////////////////////////////
1197:             $Dsql = $this->scenario == 'upgrade' ? 'sqlUpgrade' : 'sqlList';
1198:             $n_p = 0; // Parameter counter
1199:             $params = array(); // Parameters
1200:             $fieldsEntries = array(); // Array of (modelName,fieldName) pairs to test against (as SQL, with parameters)
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:                         // Array representing the column description for the insert statement:
1205:                         $columns = array_map(function($c){
1206:                                     return trim($c, ' `');
1207:                                 }, explode('`,`', $match['columns']));
1208:                         $n_col = count($columns);
1209:                         // Array of arrays, each nested array containing the column values for the fields record:
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; // Ignore records with commas in them
1214:                                 });
1215:                         // Indices of the columns with the unique constraint:
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:                             // Increment parameter counter:
1225:                             $n_p++;
1226:                         }
1227:                     }
1228:                 }
1229:             }
1230:             
1231:             $conflictingFields = array();
1232:             if(!empty($fieldsEntries)){
1233:                 try{
1234:                     // The full query to find conflicting fields:
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:                     // If anything goes wrong... Just ignore it. This was
1244:                     // meant to work with specifically-formatted SQL
1245:                     // statements generated by the update builder.
1246:                 }
1247:             }
1248:             // Special case for Actions.actionDescription, which was deleted
1249:             // from the fields table in 3.0 due to a structural change and and
1250:             // added back in 3.5.5:
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:             // Check updated PHP files for custom analogues: //
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:      * Gets configuration variables from the configuration file(s).
1283:      * @return array
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:      * Magic getter for {@link dbBackupCommand}
1304:      * @return string
1305:      * @throws Exception
1306:      */
1307:     public function getDbBackupCommand(){
1308:         if(!isset($this->_dbBackupCommand)){
1309:             $this->checkIf('canSpawnChildren');
1310:             if($this->dbParams['server'] == 'mysql'){
1311:                 // Test for the availability of mysqldump:
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{ // no other database types supported yet...
1333:                 return null;
1334:             }
1335:         }
1336:         return $this->_dbBackupCommand;
1337:     }
1338: 
1339:     /**
1340:      * Magic getter for {@link dbBackupPath}
1341:      * @return string
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:      * Magic getter for {@link dbCommand}
1351:      * @return string
1352:      * @throws Exception
1353:      */
1354:     public function getDbCommand(){
1355:         if(!isset($this->_dbCommand)){
1356:             $this->checkIf('canSpawnChildren');
1357:             // Test for the availability of mysql command line client/utility:
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{ // no other DB types supported yet..
1379:                 return null;
1380:             }
1381:         }
1382:         return $this->_dbCommand;
1383:     }
1384: 
1385:     /**
1386:      * Magic getter for database parameters from the application's DSN and {@link CDbConnection}
1387:      * @return array
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:                 // No other DBMS's supported yet...
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:      * Backwards-compatible function for obtaining the edition of the
1408:      * installation. Attempts to not fail and return a valid value even if the
1409:      * application singleton doesn't store the information.
1410:      *
1411:      * It uses try/catch blocks because Yii's way of checking if properties
1412:      * exist as of 1.1.x does not include properties "inherited" from behaviors.
1413:      *
1414:      * @return string
1415:      */
1416:     public function getEdition(){
1417:         if(!isset($this->_edition)){
1418:             // Safe default for versions too early to have "admin" in the
1419:             // params, or for the "edition" attribute to exist
1420:             $this->_edition = 'opensource';
1421:             try{
1422:                 // Versions 4.0 and later:
1423:                 $this->_edition = Yii::app()->edition;
1424:             }catch(Exception $e){
1425:                 if(Yii::app()->params->hasProperty('admin')){
1426:                     // Most versions before 4.0:
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:      * Obtains the list of files and their statuses (essentially a wrapper
1439:      * function for {@link checkFiles})
1440:      * 
1441:      * @return array
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:      * Return an array of arrays of files each indexed by the status (present,
1456:      * corrupt or missing) of those sets of files.
1457:      * @return array
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:      * Obtains {@link filesStatus}
1470:      * @return array
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:      * Gets the latest version of the updater utility
1495:      *
1496:      * @return string
1497:      */
1498:     public function getLatestUpdaterVersion(){
1499:         if(!isset($this->_latestUpdaterVersion)){
1500:             $context = stream_context_create(array(
1501:                 'http' => array(
1502:                     'timeout' => 15  // Timeout in seconds
1503:                     )));
1504:             $this->_latestUpdaterVersion = FileUtil::getContents($this->updateServer.'/installs/updates/updateCheck', 0, $context);
1505:         }
1506:         return $this->_latestUpdaterVersion;
1507:     }
1508: 
1509:     /**
1510:      * Magic getter for {@link lockFile}
1511:      */
1512:     public function getLockFile(){
1513:         return implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'runtime', self::LOCKFILE));
1514:     }
1515: 
1516:     /**
1517:      * Obtains {@link manifest}
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:      * Magic getter for {@link noHalt}
1532:      * @return bool
1533:      */
1534:     public function getNoHalt(){
1535:         return self::$_noHalt;
1536:     }
1537: 
1538:     /**
1539:      * Magic getter for {@link requirements}
1540:      * @return type
1541:      * @throws CException
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:             // The following two variables used internally by the requirements
1554:             // checking script:
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:      * Getter for {@link settings}
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:      * Obtains {@link sourceDir}
1591:      * @return string
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:      * Auto-construct a relative base URL on the updates server from which to retrieve
1602:      * source files.
1603:      *
1604:      * @param type $edition
1605:      * @param type $uniqueId
1606:      * @return string
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:      * Magic getter for {@link getThisPath}
1617:      * @return string
1618:      */
1619:     public function getThisPath(){
1620:         if(!isset($this->_thisPath))
1621:             $this->_thisPath = realpath('./');
1622:         return $this->_thisPath;
1623:     }
1624: 
1625:     /**
1626:      * Backwards-compatible function for obtaining the unique id. Very similar
1627:      * to getEdition in regard to its backwards compatibility.
1628:      * @return type
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:      * Retrieves update data from the server. For previewing an update before
1648:      * downloading it; this essentially retrieves the manifest without
1649:      * retrieving the full package.
1650:      * 
1651:      * @param string $version Version updating/upgrading from
1652:      * @param string $uniqueId The identifier/product key for this installation of X2Engine
1653:      * @param string $edition The edition updating/upgrading from
1654:      * @return array
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:      * Gets a relative URL on the update server from which to obtain update data
1670:      *
1671:      * @param string $version
1672:      * @param string $edition
1673:      * @param string $uniqueId
1674:      * @return string
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) // Use current properties as defaults
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:      * Base URL of the web server from which to fetch data and files
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:      * Web root magic getter.
1716:      *
1717:      * Resolves the absolute path to the webroot of the application without using
1718:      * the 'webroot' alias, which only works in web requests. Note, the {@link realpath()}
1719:      * function will strip off the trailing directory separator
1720:      * @return string
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:      * Returns the actions associated with the web-based updater.
1730:      *
1731:      * @param bool $getter If being called as a getter, this method will attempt
1732:      *  to download actions if they don't exist on the server yet. Otherwise, if
1733:      *  this parameter is explicitly set to False, the return value will include
1734:      *  the abstract base action class (in which case it should not be used in
1735:      *  the return value of {@link CController::actions()} ) for purposes of
1736:      *  checking dependencies.
1737:      * @return array An array of actions appropriate for inclusion in the return
1738:      *  value of {@link CController::actions()}.
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:      * Back up the application database.
1759:      *
1760:      * Attempts to perform a database backup using mysqldump or any other tool
1761:      * that might exist.
1762:      * @return bool
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:         // Run the backup!
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:      * Rebuilds the configuration file and performs the final few little update tasks.
1794:      * 
1795:      * @param type $newversion If set, change the version to this value in the resulting config file
1796:      * @param type $newupdaterVersion If set, change the updater version to this value in the resulting config file
1797:      * @param type $newbuildDate If set, change the build date to this value in the resulting config file
1798:      * @param string $newAppName If set, will be used to replace the app name in the config file. 
1799:      * @return bool
1800:      * @throws Exception
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:             // App is using the old config files. New ones will be generated.
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:             // Create a new encryption key if none exists
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:             // Set permissions on encryption
1869:             $this->configPermissions = 100600;
1870:             // Reset config vars property
1871:             if(isset($this->_configVars))
1872:                 unset($this->_configVars);
1873:             
1874:             // Finally done.
1875:             return true;
1876:         }
1877:     }
1878: 
1879:     /**
1880:      * Deletes the database backup file.
1881:      */
1882:     public function removeDatabaseBackup(){
1883:         $dbBackup = realpath($this->dbBackupPath);
1884:         if((bool) $dbBackup)
1885:             unlink($dbBackup);
1886:     }
1887: 
1888:     /**
1889:      * Deletes a list of files.
1890:      * @param array $deletionList
1891:      */
1892:     public function removeFiles($deletionList){
1893:         foreach($deletionList as $file){
1894:             // Use realpath to get platform-dependent path
1895:             $absFile = realpath("{$this->webRoot}/$file");
1896:             if((bool) $absFile){
1897:                 // Get existing file's name to ensure that we're deleting the correct file.
1898:                 // This check is only necessary on case-insensitive file systems
1899:                 $basename = pathinfo ($absFile, PATHINFO_BASENAME);
1900:                 if (basename ($file) === $basename)
1901:                     unlink($absFile);
1902:             }
1903:         }
1904:     }
1905: 
1906:     /**
1907:      * Generates user-friendly messages for letting users know about update
1908:      * compatibility issues.
1909:      *
1910:      * @param string $h Tag in which to wrap "section" titles
1911:      * @param string $htmlOptions Options for the titles of each section
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:         // Section: missing system requirements
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:         // Section: conflicting custom modules
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:         // Section: conflicting custom fields
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:         // Section: files to be updated that have been customized
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:      * Checks whether all dependencies of the updater exist on the server, and
1973:      * downloads any that don't.
1974:      */
1975:     public function requireDependencies(){
1976:         // Check all dependencies:
1977:         $dependencies = $this->updaterFiles;
1978:         // Add web updater actions to the files to be checked
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); // We're downloading/saving new files
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:         // Copy files into the live installation:
1993:         if($refresh)
1994:             $this->applyFiles(self::TMP_DIR);
1995:     }
1996: 
1997:     /**
1998:      * Removes everything in the assets folder.
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:      * Uses a database dump to reinstate the database backup.
2016:      * @return boolean
2017:      * @throws Exception 
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:         // Restore the backup!
2035:         if((bool) $this->dbCommand){
2036:             // A backup copy should exist at this point in the execution,
2037:             // so it should be safe to call the dreaded dropAllTables method:
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:      * Runs a list of migration scripts.
2051:      * 
2052:      * @param type $scripts
2053:      * @param type $ran List of database changes and other scripts that have
2054:      *  already been run
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) { // handle case of '@' error suppression
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:         // add in case this is a version before introduction of this constant
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:                 // To allow the same migration script to be executed twice in testing
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:      * Set the checksum contents to a specific value. Resets _checksumsContent;
2096:      * it no longer is applicable.
2097:      * 
2098:      * @param string $value
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:      * Magic setter that changes the file permissions of sensitive files in
2111:      * protected/config
2112:      * @param type $value
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:      * Sets the update data to a specific value
2129:      * @param array $value
2130:      */
2131:     public function setManifest(array $value) {
2132:         $this->_manifest = $value;
2133:     }
2134: 
2135:     /**
2136:      * Magic setter for {@link noHalt}. Kept here so that console applications
2137:      * can use it to stop more gracefully.
2138:      * @param type $value
2139:      */
2140:     public function setNoHalt($value){
2141:         self::$_noHalt = $value;
2142:     }
2143: 
2144:     public function setScenario($value) {
2145:         // Check for valid scenario:
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:      * Sets the unique ID for the installation.
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:      * Exits, returning SQL error messages
2166:      *
2167:      * @param type $sqlRun
2168:      * @param type $errorMessage
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:         // Test creating a table:
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:         // Test inserting data:
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:         // Test deleting data:
2219:         try{
2220:             $con->exec("DELETE FROM `x2_test_table`");
2221:         }catch(PDOException $e){
2222:             $missingPerms[] = 'delete';
2223:         }
2224: 
2225:         // Test altering tables
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:         // Test removing the table:
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:      * Unzips the package.
2254:      * @throws Exception
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:         // Block direct web access to the extracted folder:
2267:         if(file_exists($htaccess = Yii::app()->basePath.DIRECTORY_SEPARATOR.'.htaccess'))
2268:             copy($htaccess,$this->updateDir.DIRECTORY_SEPARATOR.'.htaccess');
2269:     }
2270: 
2271:     /**
2272:      * In which the updater downloads a new version of itself.
2273:      * 
2274:      * @param type $updaterCheck New version of the update utility
2275:      * @return array
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:         // Retrieve the update package contents' files' digests:
2285:         $md5sums_content = FileUtil::getContents($this->updateServer.'/'.$this->getUpdateDataRoute($this->configVars['updaterVersion']).'/contents.md5');
2286:         // If there's an error on the server end the response will be a JSON
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:             // License key error
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:         // These are the files that need to be downloaded -- only those which have changed:
2310:         $updaterFiles = array_intersect($md5sums,$updaterFiles);
2311: 
2312:         // Try to retrieve the files:
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:                 // Only call it done if it's intact and ready for use:
2326:                 $pass = md5_file($downloadedFile) == $md5;
2327:                 $tries++;
2328:             }
2329:             if(!$pass)
2330:                 $failed2Retrieve[] = "protected/$file";
2331:         }
2332: 
2333:         $failedDownload = (bool) count($failed2Retrieve);
2334:         // Copy the files into the live install
2335:         if(!$failedDownload && (bool) count($updaterFiles)) {
2336:             $this->applyFiles(self::TMP_DIR);
2337:             // Remove the temporary directory:
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:         // Write the new updater version into the configuration; else
2347:         // the app will get stuck in a redirect loop
2348:         if(!$failedDownload) {
2349:             $this->regenerateConfig(Null, $updaterCheck, Null);
2350:         }
2351:         return $failed2Retrieve;
2352:     }
2353: 
2354: }
2355: 
2356: ?>
2357: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0