/** * This test ensures that the updater will recover from failed migrations if * specified, and won't otherwise */ public function testRecoveryFromFailedMigration() { // Prepare for the test $ube = $this->instantiateUBe(); $this->prereq($ube, 'sourceDir'); $nodes = array('protected', 'tests', 'data', 'updatemigration'); $allNodes = ''; foreach ($nodes as $node) { if (!is_dir($absNode = $ube->sourceDir . $allNodes . DIRECTORY_SEPARATOR . $node)) { mkdir($absNode); } $allNodes .= DIRECTORY_SEPARATOR . $node; } $migdir = implode(DIRECTORY_SEPARATOR, array_merge(array($ube->sourceDir), $nodes)); // Copy the migration scripts $exceptionScript = 'protected/tests/data/updatemigration/failure-except.php'; $errorScript = 'protected/tests/data/updatemigration/failure-error.php'; copy($ube->webRoot . DIRECTORY_SEPARATOR . FileUtil::rpath($exceptionScript), $ube->sourceDir . DIRECTORY_SEPARATOR . FileUtil::rpath($exceptionScript)); copy($ube->webRoot . DIRECTORY_SEPARATOR . FileUtil::rpath($errorScript), $ube->sourceDir . DIRECTORY_SEPARATOR . FileUtil::rpath($errorScript)); // Ensure that backups can be restored when an exception is raised Yii::app()->db->createCommand("DROP TABLE IF EXISTS some_new_table;")->execute(); $ube->makeDatabaseBackup(); $scripts = array($exceptionScript); $ran = array(); ob_start(); try { $ube->runMigrationScripts($scripts, $ran, true); ob_end_clean(); } catch (Exception $e) { ob_end_clean(); $this->assertEquals(42000, $e->getCode(), 'Incorrect exception raised. This test ' . 'expects a PDO syntax error'); $ube->restoreDatabaseBackup(); } // Should have reverted $this->assertTableNotExists('some_new_table'); // Ensure that backups can be restored when an error is raised $ran = array(); $scripts = array($errorScript); ob_start(); $ube->runMigrationScripts($scripts, $ran, true); // Retrieve error code from output $output = ob_get_clean(); preg_match('/\\[(\\d+)\\]/', $output, $matches); $this->assertArrayHasKey(1, $matches); $this->assertEquals(8, $matches[1], 'Incorrect error occured. This test ' . 'expects an E_NOTICE: undefined variable.'); // Should have reverted $this->assertTableNotExists('some_new_table'); // Ensure that the database is in an 'unexpected' state when no recovery is performed $ran = array(); $scripts = array($exceptionScript); ob_start(); try { $ube->runMigrationScripts($scripts, $ran, false); ob_end_clean(); } catch (Exception $e) { ob_end_clean(); $this->assertEquals(42000, $e->getCode(), 'Incorrect exception raised. This test ' . 'expects a PDO syntax error'); } $this->assertTableExists('some_new_table'); Yii::app()->db->createCommand("DROP TABLE IF EXISTS some_new_table;")->execute(); }
/** * Getter for {@link edition} * * @return string */ public function getEdition() { if (!isset($this->_edition)) { if (YII_DEBUG) { switch (PRO_VERSION) { case 1: $this->_edition = 'pro'; break; case 2: $this->_edition = 'pla'; break; default: $this->_edition = 'opensource'; } } else { $this->_edition = 'opensource'; foreach (array('pla', 'pro') as $ed) { $logo = "images/x2engine_crm_{$ed}.png"; $logoPath = implode(DIRECTORY_SEPARATOR, array($this->owner->basePath, '..', FileUtil::rpath($logo))); if (file_exists($logoPath)) { if (md5_file($logoPath) == self::$_logoHashes[$ed]) { $this->_edition = $ed; break; } } } } } return $this->_edition; }
/** * Remove all the test files and directories. * @param bool $emptyFileList Whether to reset the properties that reference the file set */ public function removeTestDirs($emptyFileList = true) { foreach (array_merge($this->files, $this->exclFiles) as $file) { $path = $this->baseDir . FileUtil::rpath("/{$file}"); if (file_exists($path)) { unlink($path); } } foreach (array_merge($this->subDirs, $this->exclSubDirs) as $dir) { $path = $this->baseDir . FileUtil::rpath("/{$dir}"); if (file_exists($path)) { rmdir($path); } } if (is_dir($this->baseDir)) { rmdir($this->baseDir); } if ($emptyFileList) { // Reset everything $this->resetTestDirs(); } }
public function testRelpath() { // Specifying both paths $startPoint = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'config', 'main.php')); $file = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, '..', 'framework', 'YiiBase.php')); $relpath = FileUtil::relpath($file, $startPoint); $this->assertEquals(str_replace('/', DIRECTORY_SEPARATOR, '../../framework/YiiBase.php'), $relpath); // Specifying only one path. The return value should originate from // index.php's directory! $relpath = FileUtil::relpath($file); $this->assertEquals('../../framework/YiiBase.php', $relpath); // Test on Windows! $startPoint = 'C:\\Program Files (x86)\\Something\\SomethingElse\\..\\something.exe'; $endPoint = 'C:\\Windows\\Something\\..\\Something\\SomethingMore/library.dll'; $relpath = FileUtil::relpath($endPoint, $startPoint, DIRECTORY_SEPARATOR); $this->assertEquals(FileUtil::rpath('../../Windows/Something/SomethingMore/library.dll'), $relpath); // Two ordinary points that don't require upward traversal... $startPoint = '/home/joeschmoe/public_html/'; $endPoint = '/home/joeschmoe/public_html/protected/controllers/FatController.php'; $relpath = FileUtil::relpath($endPoint, $startPoint); $this->assertEquals(FileUtil::rpath('protected/controllers/FatController.php'), $relpath); // Two points, one in a backup dir $startPoint = '/home/joeschmoe/public_html/protected/controllers/FatController.php'; $endPoint = '/home/joeschmoe/public_html/backup/protected/controllers/FatController.php'; $relpath = FileUtil::relpath($endPoint, $startPoint); $this->assertEquals(FileUtil::rpath('../../backup/protected/controllers/FatController.php'), $relpath); }
/** * Non-interactive fixture export with option to specify aliases as command line args */ public function actionExport($tableName, $type = 'f', $range = 1, $columns = '*', $writeCond = 's', array $aliases = array()) { $fileName = $tableName . ($type == 'i' ? '.init' : '') . '.php'; $filePath = $this->fixtureDir . '/' . $tableName . ($type == 'i' ? '.init' : '') . '.php'; if (file_exists(FileUtil::rpath($filePath))) { switch ($writeCond) { case 'r': $i = 0; $backup = $filePath; while (file_exists(FileUtil::rpath($backup))) { $backup = "{$filePath}.{$i}"; $i++; } $this->copyFiles(array("backup of existing: {$fileName}" => array('source' => $filePath, 'target' => $backup))); break; case 'o': echo "\nOverwriting existing file {$fileName}\n"; break; case 's': break; default: // filename echo "\nWriting to file {$writeCond}\n"; } } $aliasPrompt = false; if ($type == 'f' && $this->_mode === 'interactive') { $aliasPrompt = $this->confirm('Prompt for row aliases?'); } $records = Yii::app()->db->createCommand()->select($columns)->from($tableName)->where($range)->queryAll(); $fileCont = "<?php\nreturn array(\n"; foreach ($records as $index => $record) { $alias = null; if ($type == 'f') { if (!$aliasPrompt && isset($aliases[$index])) { $alias = $aliases[$index]; } else { $alias = $index; } if ($aliasPrompt) { var_dump($record); $alias = $this->prompt("Alias for this record (enter for \"{$index}\"):"); if (empty($alias)) { $alias = $index; } while (in_array($alias, $aliases)) { $alias = $this->prompt("Alias in use already. Enter another:"); if (empty($alias)) { $alias = $index; } } $aliases[] = $alias; } else { } } $fileCont .= $this->formatRecord($record, $alias); } $fileCont .= ");\n?>"; if (!in_array($writeCond, array('s', 'r', 'o'))) { file_put_contents($writeCond, $fileCont); } elseif ($writeCond !== 's') { file_put_contents($filePath, $fileCont); } else { /**/ print $fileCont; } echo "\nExport complete.\n"; }
/** * In which the updater downloads a new version of itself. * * @param type $updaterCheck New version of the update utility * @return array */ public function updateUpdater($updaterCheck) { if (version_compare($this->configVars['updaterVersion'], $updaterCheck) >= 0) { return array(); } $updaterFiles = $this->updaterFiles; // Retrieve the update package contents' files' digests: $md5sums_content = FileUtil::getContents($this->updateServer . '/' . $this->getUpdateDataRoute($this->configVars['updaterVersion']) . '/contents.md5'); // If there's an error on the server end the response will be a JSON $tryJson = json_decode($md5sums_content, 1); if (!(bool) $md5sums_content) { $admin = CActiveRecord::model('Admin')->findByPk(1); if ($this->scenario === 'upgrade' && isset($admin) && empty($admin->unique_key)) { $updaterSettingsLink = CHtml::link(Yii::t('admin', 'Updater Settings page'), array('admin/updaterSettings')); throw new CException(Yii::t('admin', 'You must first set a product key on the ' . $updaterSettingsLink)); } else { throw new CException(Yii::t('admin', 'Unknown update server error.'), self::ERR_UPSERVER); } } else { if (is_array($tryJson)) { // License key error if (isset($tryJson['errors'])) { throw new CException($tryJson['errors']); } else { throw new CException(Yii::t('admin', 'Unknown update server error.') . ' ' . $md5sums_content); } } } preg_match_all(':^(?<md5sum>[a-f0-9]{32})\\s+source/protected/(?<filename>\\S.*)$:m', $md5sums_content, $md5s); $md5sums = array(); for ($i = 0; $i < count($md5s[0]); $i++) { $md5sums[$md5s['md5sum'][$i]] = $md5s['filename'][$i]; } // These are the files that need to be downloaded -- only those which have changed: $updaterFiles = array_intersect($md5sums, $updaterFiles); // Try to retrieve the files: $failed2Retrieve = array(); foreach ($updaterFiles as $md5 => $file) { $pass = 0; $tries = 0; $downloadedFile = FileUtil::relpath(implode(DIRECTORY_SEPARATOR, array($this->webRoot, self::TMP_DIR, 'protected', FileUtil::rpath($file))), $this->thisPath . DIRECTORY_SEPARATOR); while (!$pass && $tries < 2) { $remoteFile = $this->updateServer . '/' . $this->sourceFileRoute . "/protected/{$file}"; try { $this->downloadSourceFile("protected/{$file}"); } catch (Exception $e) { break; } // Only call it done if it's intact and ready for use: $pass = md5_file($downloadedFile) == $md5; $tries++; } if (!$pass) { $failed2Retrieve[] = "protected/{$file}"; } } $failedDownload = (bool) count($failed2Retrieve); // Copy the files into the live install if (!$failedDownload && (bool) count($updaterFiles)) { $this->applyFiles(self::TMP_DIR); // Remove the temporary directory: FileUtil::rrmdir($this->webRoot . DIRECTORY_SEPARATOR . self::TMP_DIR); } else { $errorResponse = json_decode($md5sums_content, 1); if (isset($errorResponse['errors'])) { throw new CException($errorResponse['errors']); } } // Write the new updater version into the configuration; else // the app will get stuck in a redirect loop if (!$failedDownload) { $this->regenerateConfig(Null, $updaterCheck, Null); } return $failed2Retrieve; }
/** * Export the contents of a table in the live database as a fixture or init script. * * Usage: * <tt>./yiic exportfixture [table name] [f|i] [range] [columns] [o|r]</tt> * * @param array $args */ public function run($args) { $this->fixtureDir = Yii::app()->basePath . '/tests/fixtures'; foreach ($this->args as $pos => $spec) { $valid = false; while (!$valid) { if (array_key_exists($pos, $args)) { ${$spec[0]} = $args[$pos]; $valid = $this->validInput($args[$pos], $spec[3]); if (!$valid) { echo $this->errorMessage($spec, ${$spec[4]}); echo $this->getHelp(); Yii::app()->end(); } } else { ${$spec[0]} = $this->prompt("{$spec[0]} ({$spec[1]})", $spec[2]); $valid = $this->validInput(${$spec[0]}, $spec[3]); if (!$valid) { echo $this->errorMessage($spec, ${$spec[0]}); } } } } if (!$valid) { echo $this->getHelp(); Yii::app()->end(); } $fileName = $tableName . ($type == 'i' ? '.init' : '') . '.php'; $filePath = $this->fixtureDir . '/' . $tableName . ($type == 'i' ? '.init' : '') . '.php'; if (file_exists(FileUtil::rpath($filePath))) { switch ($writeCond) { case 'r': $i = 0; $backup = $filePath; while (file_exists(FileUtil::rpath($backup))) { $backup = "{$filePath}.{$i}"; $i++; } $this->copyFiles(array("backup of existing: {$fileName}" => array('source' => $filePath, 'target' => $backup))); break; case 'o': echo "\nOverwriting existing file {$fileName}\n"; break; case 's': break; default: // filename echo "\nWriting to file {$writeCond}\n"; } } $aliasPrompt = false; if ($type == 'f') { $aliasPrompt = $this->confirm('Prompt for row aliases?'); } $records = Yii::app()->db->createCommand()->select($columns)->from($tableName)->where($range)->queryAll(); $fileCont = "<?php\nreturn array(\n"; $aliases = array(); foreach ($records as $index => $record) { $alias = null; if ($type == 'f') { $alias = $index; if ($aliasPrompt) { var_dump($record); $alias = $this->prompt("Alias for this record (enter for \"{$index}\"):"); if (empty($alias)) { $alias = $index; } while (in_array($alias, $aliases)) { $alias = $this->prompt("Alias in use already. Enter another:"); if (empty($alias)) { $alias = $index; } } $aliases[] = $alias; } else { } } $fileCont .= $this->formatRecord($record, $alias); } $fileCont .= ");\n?>"; if (!in_array($writeCond, array('s', 'r', 'o'))) { file_put_contents($writeCond, $fileCont); } elseif ($writeCond !== 's') { file_put_contents($filePath, $fileCont); } else { /**/ print $fileCont; } echo "\nExport complete.\n"; }
/** * Exports the database content into dummy data files * * @param array $args * @param PDOException $e * @return type */ public function actionExport($args) { if (!copy("./data/install_timestamp", "./data/dummy_data_date")) { die("Error: actionExport: failed to copy install_timestamp to dummy_data_date"); } // [edition] => [array of table names] $tblEditions = (require realpath(Yii::app()->basePath . '/data/nonFreeTables.php')); $allEditions = array_keys($tblEditions); $nonFreeEditions = array_diff($allEditions, array('opensource')); $specTemplate = array_fill_keys($allEditions, array()); $this->pdo = Yii::app()->db->pdoInstance; $conf = realpath(Yii::app()->basePath . '/config/X2Config.php'); if ($conf) { if ((include $conf) !== 1) { die('Configuration import failed.'); } } else { die("Configuration file not found. This script must be run in protected/data.\n"); } $getTbls = $this->pdo->prepare("SHOW TABLES IN `{$dbname}`"); $getTbls->execute(); try { $allTbls = array_map(function ($tr) use($dbname) { return $tr["Tables_in_{$dbname}"]; }, $getTbls->fetchAll(PDO::FETCH_ASSOC)); } catch (PDOException $e) { die("Database error: " . $e->getMessage() . "\n"); } /** * The command for exporting data: */ $command = "mysqldump -tc -u {$user} -p{$pass} {$dbname} "; // Ignore pattern for lines in output of mysqldump: $lPat = '/^(\\/\\*|\\-\\-|\\s*$'; // Export current app's data as "dummy" (usage example) data $lPat .= '|(?:UN)?LOCK TABLES)/'; $out = FileUtil::rpath(Yii::app()->basePath . '/data/dummy_data%s.sql'); /** * Update the list of tables for each edition with the default tables: */ $nonFreeTbls = array_reduce($allEditions, function ($a, $e) use($tblEditions) { return array_merge($tblEditions[$e], $a); }, array()); $tblEditions['opensource'] = array_diff($allTbls, $nonFreeTbls); /** * Declare the export specification arrays * * Here it's specified what data will be exported and how. * Each of these arrays follows the basic pattern of $specTemplate: * [edition] => [array of table names or ([table name] =>[spec])] */ /** * These will be excluded from data export altogether */ $tblsExclude = $specTemplate; // These will be excluded for open source and above: $tblsExclude['opensource'] = array_merge(array('x2_admin', 'x2_auth_assignment', 'x2_auth_item', 'x2_auth_item_child', 'x2_modules', 'x2_sessions', 'x2_temp_files', 'x2_timezones', 'x2_timezone_points', 'x2_tips'), $tblEditions['pro'], $tblEditions['pla']); // These for professional edition: $tblsExclude['pro'] = array_merge(array('x2_forwarded_email_patterns'), $tblEditions['pla']); // These for platform/platinum edition: $tblsExclude['pla'] = array('x2_forwarded_email_patterns'); /** * These will be included, but with specific criteria */ $tblsWhere = $specTemplate; $tblsWhere['opensource'] = array('x2_dropdowns' => 'id>=1000', 'x2_fields' => 'custom=1', 'x2_form_layouts' => 'id>=1000', 'x2_media' => '(id>11 AND id<1000) OR (id>1006 AND id<2000) OR id>2002', 'x2_profile' => 'id>2', 'x2_users' => 'id>2', 'x2_social' => 'id>1', 'x2_docs' => 'id>52 OR id<52'); /** * Update statements will be generated for these tables on which there's no way * of inserting it at install time without running into duplicate primary key * errors (because it's a record inserted by the installer itself). In each table: * 'pk' => primary key (string for single-column or array for multi-column) * 'fields' => array of fields to update or "*" to update all fields. Must include primary key. * 'where' => records for which to generate update statements */ $tblsChangeDefault = $specTemplate; $tblsChangeDefault['opensource'] = array('x2_profile' => array('pk' => 'id', 'fields' => '*', 'where' => '`id`=1'), 'x2_users' => array('pk' => 'id', 'fields' => array('id', 'firstName', 'lastName', 'officePhone', 'cellPhone', 'showCalendars', 'calendarViewPermission', 'calendarEditPermission', 'calendarFilter', 'setCalendarPermissions', 'recentItems', 'topContacts'), 'where' => '`id`=1')); /** * Switch the order of output generation so that foreign key constraints don't * fail during insertion. List dependencies here. */ $insertFirst = $specTemplate; $insertFirst['opensource'] = array('x2_list_criteria' => array('x2_lists'), 'x2_list_items' => array('x2_lists'), 'x2_role_to_workflow' => array('x2_workflow_stages', 'x2_roles', 'x2_workflows'), 'x2_workflow_stages' => array('x2_workflows'), 'x2_action_text' => array('x2_actions')); /** * This array stores tables to be executed "next" */ $insertNext = $specTemplate; /** * The resulting SQL to be written to files */ $allSql = $specTemplate; /** * Assemble the array of combined export specs. * * Note that since the "where" conditions are put in the array last, they'll * take precedence (so if it's listed in both $tblsExclude and $tblsWhere, * only $tblsWhere will apply). */ $allTbls = array(); foreach ($allEditions as $edition) { $allTbls[$edition] = array_fill_keys($tblEditions[$edition], true); foreach ($tblsExclude[$edition] as $tbl) { $allTbls[$edition][$tbl] = false; } foreach ($tblsWhere[$edition] as $tbl => $where) { $allTbls[$edition][$tbl] = $where; } } // The update statement that will be used for updating records post-insertion: $updateStatement = "UPDATE `%s` SET %s WHERE %s;"; foreach ($nonFreeEditions as $edition) { $allSql[$edition][] = "/* @edition:{$edition} */"; } /** * Generate SQL for the data: */ foreach ($allTbls as $edition => $tbls) { /** * Generate insertion statements */ $eTbls = $tbls; while (count($eTbls) > 0) { $tblsTmp = $eTbls; foreach ($tblsTmp as $tbl => $where) { if ($where != false) { // This table is to be included in the data export if (array_key_exists($tbl, $insertFirst[$edition])) { // This table depends on other tables being ready with data $skip = False; foreach ($insertFirst[$edition][$tbl] as $tblFirst) { // Check to see if the table has been accounted for already if (array_key_exists($tblFirst, $eTbls)) { $skip = True; break; } } if ($skip) { // Not all dependencies of this table have been resolved yet. continue; } } $output = array(); $tblCommand = "{$command} {$tbl}" . ($where !== true ? " --where='" . $where . "' " : ' '); exec($tblCommand, $output); foreach ($output as $line) { if (!preg_match($lPat, $line)) { $allSql[$edition][] = $line; } } } unset($eTbls[$tbl]); } } /** * Generate update statements */ foreach ($tblsChangeDefault[$edition] as $tbl => $how) { $colSel = $how['fields']; if (is_array($how['fields'])) { $colSel = '`' . implode('`,`', $how['fields']) . '`'; } $query = $this->pdo->prepare("SELECT {$colSel} FROM `{$tbl}` WHERE {$how['where']}"); $query->execute(); $recs = $query->fetchAll(PDO::FETCH_ASSOC); $pk = $how['pk']; if (!is_array($pk)) { $pk = array($pk); } foreach ($recs as $rec) { // Generate a "where" clause criterion to refer to this record by its primary key $whereSelector = array(); foreach ($pk as $c) { $whereSelector[] = "`{$c}`=" . $this->sqlValue($rec[$c]); } // Exclude the primary key from the columns to be updated: foreach ($pk as $col) { unset($rec[$col]); } $fieldsSet = array(); foreach ($rec as $col => $val) { $fieldsSet[] = "`{$col}`=" . $this->sqlValue($val); } $allSql[$edition][] = sprintf($updateStatement, $tbl, implode(',', $fieldsSet), implode(' AND ', $whereSelector)); } } } // Create dummy data files foreach ($allSql as $edition => $sqls) { file_put_contents(sprintf($out, $edition == 'opensource' ? '' : "-{$edition}"), implode("\n/*&*/\n", $sqls)); } }