Invision Power Services, Inc. * @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc. * @license http://www.invisionpower.com/legal/standards/ * @package IPS Social Suite * @since 21 May 2014 * @version SVN_VERSION_NUMBER * @todo MAKE DATABASE CHECK RUN BEFORE EVERYTHING ELSE */ namespace IPS\core\Setup; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Upgrader */ class _Upgrade { /** * System Requirements * * @return array */ public static function systemRequirements() { $return = Install::systemRequirements(); /* MySQL Version */ $mysqlVersion = \IPS\Db::i()->server_info; if ( version_compare( $mysqlVersion, '5.1.3' ) >= 0 ) { $return['requirements']['MySQL'][] = array( 'success' => TRUE, 'message' => \IPS\Member::loggedIn()->language()->addToStack( 'requirements_mysql_version_success', FALSE, array( 'sprintf' => array( $mysqlVersion ) ) ) ); if ( version_compare( $mysqlVersion, '5.6.2' ) == -1 AND !mb_strpos( $mysqlVersion, "MariaDB" ) ) { $return['advice'][] = \IPS\Member::loggedIn()->language()->addToStack( 'requirements_mysql_version_advice', FALSE, array( 'sprintf' => array( $mysqlVersion ) ) ); } } else { $return['requirements']['MySQL'][] = array( 'success' => FALSE, 'message' => \IPS\Member::loggedIn()->language()->addToStack( 'requirements_mysql_version_fail', FALSE, array( 'sprintf' => array( $mysqlVersion ) ) ), ); } /* Writeables */ if( \IPS\Db::i()->checkForTable('core_file_storage') ) { $writeablesKey = \IPS\Member::loggedIn()->language()->addToStack('requirements_file_system'); $fileSystemItems = array(); foreach ( \IPS\Application::allExtensions( 'core', 'FileStorage', FALSE ) as $k => $v ) { try { $class = \IPS\File::getClass( $k ); if ( !isset( $fileSystemItems[ $class->configurationId ] ) ) { if ( method_exists( $class, 'testSettings' ) ) { $class->testSettings( $class->configuration ); } $fileSystemItems[ $class->configurationId ] = array( 'success' => TRUE, 'message' => ( $class instanceof \IPS\File\FileSystem ) ? \IPS\Member::loggedIn()->language()->addToStack( 'requirements_file_writable', FALSE, array( 'sprintf' => array( str_replace( '{root}', \IPS\ROOT_PATH, $class->configuration['dir'] ) ) ) ) : $class->displayName( $class->configuration ) ); } } catch ( \LogicException $e ) { $fileSystemItems[ $class->configurationId ] = array( 'success' => FALSE, 'message' => $e->getMessage() ); } } array_splice( $return['requirements'][ $writeablesKey ], array_search( 'uploads', array_keys( $return['requirements'][ $writeablesKey ] ) ), 1, $fileSystemItems ); } if ( \IPS\Data\Store::i() instanceof \IPS\Data\Store\FileSystem ) { $success = ( is_dir( \IPS\Data\Store::i()->_path ) and is_writeable( \IPS\Data\Store::i()->_path ) ); $return['requirements'][ $writeablesKey ]['datastore'] = array( 'success' => $success, 'message' => $success ? \IPS\Member::loggedIn()->language()->addToStack( 'requirements_file_writable', FALSE, array( 'sprintf' => array( \IPS\Data\Store::i()->_path ) ) ) : \IPS\Member::loggedIn()->language()->addToStack('bad_datastore_configuration', FALSE, array( 'sprintf' => array( \IPS\Data\Store::i()->_path ) ) ) ); } else { unset( $return['requirements'][ $writeablesKey ]['datastore'] ); } return $return; } /** * @brief Percentage of *this step* completed (used for the progress bar) */ protected $stepProgress = 0; /** * @brief Custom title to use for refresh/progress bar */ protected $customTitle = NULL; /** * Constructor * * @param array $apps Application keys of apps to upgrade * @return void * @throws \InvalidArgumentException */ public function __construct( $apps ) { /* Store data */ $this->apps = $apps; } /** * Process * * @param array Multiple-Redirector Data * @return array|null Multiple-Redirector Data or NULL indicates done */ public function process( $data ) { /* Start */ if ( $data === 0 ) { $data = array( 0 => 1 ); } /* Clear the last SQL error if we are continuing upgrade to prevent it erroneously showing up again later */ if( isset( \IPS\Request::i()->mr_continue ) ) { unset( $_SESSION['lastSqlError'] ); } /* Run the step */ $step = intval( $data[0] ); if ( $step === 1 ) { /* Set the we're setting up flag */ static::setUpgradingFlag( TRUE ); } /* Write the log */ @file_put_contents( \IPS\ROOT_PATH . '/uploads/logs/upgrader_data.cgi', json_encode( array( 'session' => $_SESSION, 'step' => $step, 'data' => $data ) ) ); @chmod( \IPS\ROOT_PATH . '/uploads/logs/upgrader_data.cgi', \IPS\IPS_FILE_PERMISSION ); if ( $step == 11 ) { /* Clear member menu cache */ \IPS\Member::clearCreateMenu(); /* Clear widget caches */ \IPS\Widget::deleteCaches(); /* Clear store */ \IPS\Data\Cache::i()->clearAll(); \IPS\Data\Store::i()->clearAll(); /* Clear theme caches */ try { foreach( \IPS\Theme::themes() as $id => $theme ) { $theme->buildResourceMap(); $theme->saveSet(); } \IPS\Theme::deleteCompiledCss(); } catch( \Exception $e ) { try { \IPS\Log::i( LOG_ERR )->write( "Error : " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'upgrade_error' ); } catch( \RuntimeException $e ){} } /* Remove the flag */ static::setUpgradingFlag( FALSE ); return NULL; } elseif ( !method_exists( $this, "step{$step}" ) ) { throw new \BadMethodCallException( 'NO_STEP' ); } $response = call_user_func( array( $this, "step{$step}" ), $data ); return array( $response, \IPS\Member::loggedIn()->language()->addToStack( ( $this->customTitle ) ? $this->customTitle : 'upgrade_step_' . $step ), ( ( ( 100/10 ) * $data[0] + ( ( 100/10 ) / 100 * $this->stepProgress ) ) ) ?: 1 ); } /** * Fetch the next update ID * * @param string $app Application key * @param int $current Current upgrading ID * @return int|null Upgrade or ID if none */ protected function getNextUpgradeId( $app, $current=0 ) { $currentVersion = \IPS\Application::load( $app )->long_version; $upgradeSteps = \IPS\Application::load( $app )->getUpgradeSteps( $current ? $current : $currentVersion ); /* Grab next upgrade step to run */ if ( ! count( $upgradeSteps ) ) { return NULL; } $next = array_shift( $upgradeSteps ); $next = intval( $next ); if( !isset( $_SESSION['upgrade_steps'] ) ) { $_SESSION['upgrade_steps'] = array( $next => array( $app => $app ) ); } elseif( !isset( $_SESSION['upgrade_steps'][ $next ] ) ) { $_SESSION['upgrade_steps'][ $next ] = array( $app => $app ); } else { $_SESSION['upgrade_steps'][ $next ][ $app ] = $app; } return $next; } /** * App Looper * * @param array data Multiple-Redirector Data * @param callback $code Code to execute for each app * @return array Data to Multiple-Redirector Data */ protected function appLoop( $data, $code ) { $this->stepProgress = 0; $returnNext = FALSE; foreach ( $this->apps as $app ) { $this->stepProgress += ( 100 / count( $this->apps ) ); if ( !isset( $data[1] ) ) { return array( $data[0], $app ); } elseif ( $data[1] == $app ) { $val = call_user_func( $code, $app ); if ( is_array( $val ) OR is_string( $val ) ) { return $val; } else { $returnNext = true; } } elseif ( $returnNext ) { return array( $data[0], $app ); } } return array( ( $data[0] + 1 ) ); } /** * Step 1 * Upgrade database * * @return array Multiple-Redirector Data */ protected function step1( $data ) { $this->stepProgress = 0; $perAppProgress = floor( 100 / count( $this->apps ) ); $returnNext = FALSE; $extra = array(); $lastAppToRun = end( $this->apps ); reset( $this->apps ); foreach ( $this->apps as $app ) { $this->stepProgress += $perAppProgress; if ( !isset( $data[1] ) ) { $this->customTitle = "Preparing to upgrade database"; return array( $data[0], $app ); } /* If we finished with the last app, $returnNext would get set to true and we'd want to run this again */ elseif ( $data[1] == $app OR $returnNext ) { if ( isset( $_SESSION['updatedData'] ) ) { unset( $_SESSION['updatedData'] ); } if ( $returnNext ) { /* Start next app */ $data['extra'] = array(); $data[1] = $app; $_SESSION['updatedData'] = $data; $_SESSION['lastJsonIndex'] = 0; } try { \IPS\Log::i( LOG_INFO )->write( "Step1 Loop: " . json_encode( $data ), 'upgrade' ); } catch( \RuntimeException $e ){} /* Re-initialize $extra variable */ $extra = array(); $extra['lastSqlId'] = isset( $data['extra']['lastSqlId'] ) ? $data['extra']['lastSqlId'] : 0; /* Currently running version */ if ( isset( $data['extra'] ) and array_key_exists( '_current', $data['extra'] ) ) # Can be null which isset() ignores { $extra['_current'] = $data['extra']['_current']; } else { $extra['_current'] = $this->getNextUpgradeId( $app ); $extra['lastSqlId'] = 0; } /* Did we find any? */ if( $extra['_current'] ) { /* We need to populate \IPS\Request with the extra data returned from the last upgrader step call */ if( isset( $data['extra']['_upgradeData'] ) ) { \IPS\Request::i()->extra = $data['extra']['_upgradeData']; } /* What step in the upgrader file are we on? */ $upgradeStep = ( isset($data['extra']['_upgradeStep']) ) ? intval($data['extra']['_upgradeStep']) : 1; /* We're on the first step of the current version's upgrade.php, so run the raw queries yet, do so */ if( $upgradeStep == 1 AND ! isset( $data['extra']['_upgradeData'] ) and ( ! ( isset( $_SESSION['sqlFinished'][ $app ][ $extra['_current'] ] ) AND $_SESSION['sqlFinished'][ $app ][ $extra['_current'] ] ) ) ) { $lastSqlId = ( isset( $extra['lastSqlId'] ) ? $extra['lastSqlId'] : 0 ); $this->customTitle = "Upgrading database (" . ucfirst( $app ) . ': Upgrade ID ' . $extra['_current'] . '-' . $lastSqlId . ')'; try { \IPS\Log::i( LOG_INFO )->write( "Upgrading database for app: " . $app, 'upgrade' ); } catch( \RuntimeException $e ){} $_SESSION['lastSqlError'] = null; try { $fetched = \IPS\Application::load( $app )->installDatabaseUpdates( $extra['_current'], $lastSqlId, 10, !\IPS\Request::i()->run_anyway ); $extra['lastSqlId'] = $_SESSION['lastJsonIndex']; try { \IPS\Log::i( LOG_INFO )->write( (int) $fetched['count'] . ' queries run, last ID ' . $_SESSION['lastJsonIndex'], 'upgrade' ); } catch( \RuntimeException $e ){} } catch( \IPS\Db\Exception $ex ) { $trace = $ex->getTrace(); $queryRun = ''; $message = $ex->getMessage(); if ( isset( $trace[0]['args'][0] ) ) { $queryRun = $trace[0]['args'][0]; } $_SESSION['lastSqlError'] = $message . ' ' . $queryRun; try { \IPS\Log::i( LOG_ERR )->write( "Error: " . $message . ' ' . $queryRun . "\n" . $ex->getTraceAsString(), 'upgrade_error' ); } catch( \RuntimeException $e ){} /* Throw so ajax returns 500 and stops upgrader */ throw $ex; } /* Queries to run manually */ if( is_array( $fetched['queriesToRun'] ) and count( $fetched['queriesToRun'] ) ) { $mr = \IPS\core\Setup\Upgrade::adjustMultipleRedirect( array( 1 => $app, 'extra' => array( 'lastSqlId' => $_SESSION['lastJsonIndex'], '_current' => $extra['_current'] ) ) ); return \IPS\Theme::i()->getTemplate( 'forms' )->queries( $fetched['queriesToRun'], \IPS\Http\Url::internal( 'controller=upgrade' )->setQueryString( array( 'key' => $_SESSION['uniqueKey'], 'mr_continue' => 1, 'mr' => $mr ) ) ); } else { /* Got more? */ if ( $fetched['count'] > 0 AND $_SESSION['lastJsonIndex'] ) { return array( $data[0], $app, 'extra' => $extra ); } $extra['lastSqlId'] = 0; $_SESSION['sqlFinished'][ $app ][ $extra['_current'] ] = 1; $_SESSION['lastJsonIndex'] = 0; $_SESSION['lastSqlError'] = null; /* All done, allow code to finish this foreach and process below */ } } /* Get the object */ $_className = "\\IPS\\{$app}\\setup\\upg_{$extra['_current']}\\Upgrade"; $_methodName = "step{$upgradeStep}"; if( class_exists( $_className ) ) { $upgrader = new $_className; /* If the next step exists, run it */ if( method_exists( $upgrader, $_methodName ) ) { $this->customTitle = "Running upgrade step " . $upgradeStep . " (" . ucfirst( $app ) . ': Upgrade ID ' . $extra['_current'] . ')'; try { /* Get custom title first as the step may unset session variables that are being referenced */ $customTitleMethod = 'step' . $upgradeStep . 'CustomTitle'; if ( method_exists( $upgrader, $customTitleMethod ) ) { $this->customTitle = $upgrader->$customTitleMethod(); } $result = $upgrader->$_methodName(); } catch( \IPS\Db\Exception $ex ) { $trace = $ex->getTrace(); $queryRun = ''; $message = $ex->getMessage(); if ( isset( $trace[0]['args'][0] ) ) { $queryRun = $trace[0]['args'][0]; } $_SESSION['lastSqlError'] = $message . ' ' . $queryRun; $_SESSION['updatedData'] = array_merge( $data, array( 'extra' => $extra ), array( 'extra' => array( '_current' => $extra['_current'], '_upgradeStep' => $upgradeStep ) ) ); try { \IPS\Log::i( LOG_ERR )->write( "(Upgrader " . $_methodName . ") " . $message . ' ' . $queryRun . $ex->getTraceAsString(), 'upgrade_error' ); } catch( \RuntimeException $e ){} /* Throw so ajax returns 500 and stops upgrader */ throw $ex; } catch( \UnderflowException $ex ) { $trace = $ex->getTrace(); $message = $ex->getMessage(); $_SESSION['lastSqlError'] = $message; $_SESSION['updatedData'] = array_merge( $data, array( 'extra' => $extra ), array( 'extra' => array( '_current' => $extra['_current'], '_upgradeStep' => $upgradeStep ) ) ); try { \IPS\Log::i( LOG_ERR )->write( "(Upgrader " . $_methodName . ") " . $message, 'upgrade_error' ); } catch( \RuntimeException $e ){} throw $ex; } /* If the result is 'true' we move on to the next step */ if( $result === TRUE ) { /* Reset this for future version IDs */ $extra['lastSqlId'] = 0; $_SESSION['lastJsonIndex'] = 0; $_SESSION['lastSqlError'] = null; $_SESSION['updatedData'] = null; $_nextMethodStep = "step" . ( $upgradeStep + 1 ); if( method_exists( $upgrader, $_nextMethodStep ) ) { /* We have another step to run - set the data and move along */ $extra['_upgradeStep'] = $upgradeStep + 1; return array( $data[0], $app, 'extra' => $extra ); } else { /* Done with this current step, see if there are any more */ $extra['_current'] = $this->getNextUpgradeId( $app, $extra['_current'] ); if( $extra['_current'] ) { unset( $extra['_upgradeStep'], $extra['_upgradeData'] ); return array( $data[0], $app, 'extra' => $extra ); } } } /* If the result is an array with 'html' key, we show that */ else if( is_array( $result ) AND isset( $result['html'] ) ) { return $result['html']; } /* Otherwise we need to run the same step again and store the data returned */ else { /* Store the data returned, set the step to the same/current one, and re-run */ $extra['_upgradeData'] = $result; $extra['_upgradeStep'] = $upgradeStep; return array( $data[0], $app, 'extra' => $extra ); } } else { /* Step doesn't exist so move on to next version */ $extra['_current'] = $this->getNextUpgradeId( $app, $extra['_current'] ); $extra['lastSqlId'] = 0; unset( $extra['_upgradeStep'], $extra['_upgradeData'] ); return array( $data[0], $app, 'extra' => $extra ); } } # If has upg_xxxxx/Upgrade.php else { /* SQL done, no upgrade steps, lets jog on */ $extra['_current'] = $this->getNextUpgradeId( $app, $extra['_current'] ); $extra['lastSqlId'] = 0; unset( $extra['_upgradeStep'], $extra['_upgradeData'] ); return array( $data[0], $app, 'extra' => $extra ); } $returnNext = TRUE; } # If current step else { $_SESSION['lastJsonIndex'] = 0; $_SESSION['lastSqlError'] = null; $_SESSION['updatedData'] = null; /* Get the next app to upgrade */ $returnNext = TRUE; } } # If current app } # Foreach /* We're done, look for finish methods for this upgrade ID */ if ( $lastAppToRun === $app AND isset( $_SESSION['upgrade_steps'] ) AND count( $_SESSION['upgrade_steps'] ) ) { $this->customTitle = "Running finish step (" . ucfirst( $app ) . ': Upgrade ID ' . $extra['_current'] . ')'; foreach( $_SESSION['upgrade_steps'] as $_versionId => $_versionApps ) { foreach( $_versionApps as $_versionApp ) { try { \IPS\Log::i( LOG_INFO )->write( "Running finish step for: {$_versionApp} version {$_versionId}", 'upgrade' ); } catch( \RuntimeException $e ){} /* Get the object */ $_className = "\\IPS\\{$_versionApp}\\setup\\upg_{$_versionId}\\Upgrade"; if( class_exists( $_className ) ) { $finisher = new $_className; if ( method_exists( $finisher, 'finish' ) ) { try { $finisher->finish(); } catch( \Exception $ex ) { $_SESSION['lastSqlError'] = $ex->getMessage() . ' (' . $_className . '->finish() )'; throw $ex; } } } } } } /* Move on to next step */ return array( ( $data[0] + 1 ), 'extra' => $extra ); } /** * Step 2 * Run a database check * * @return array Multiple-Redirector Data */ protected function step2( $data ) { return $this->appLoop( $data, function( $app ) use( $data ) { $changesToMake = \IPS\Application::load( $app )->databaseCheck(); if ( count( $changesToMake ) ) { $toRun = \IPS\core\Setup\Upgrade::runManualQueries( $changesToMake ); if ( count( $toRun ) ) { $mr = \IPS\core\Setup\Upgrade::adjustMultipleRedirect( array( 1 => $app, 'extra' => array( '_upgradeStep' => 2 ) ) ); return \IPS\Theme::i()->getTemplate( 'forms' )->queries( $toRun, \IPS\Http\Url::internal( 'controller=upgrade' )->setQueryString( array( 'key' => $_SESSION['uniqueKey'], 'mr_continue' => 1, 'mr' => $mr ) ) ); } } return NULL; } ); } /** * Step 3 * Insert app data * * @return array Multiple-Redirector Data */ protected function step3( $data ) { return $this->appLoop( $data, function( $app ) { if( !file_exists( \IPS\ROOT_PATH . '/plugins/hooks.txt' ) ) { @touch( \IPS\ROOT_PATH . '/plugins/hooks.txt' ); @chmod( \IPS\ROOT_PATH . '/plugins/hooks.txt', 0777 ); } if( !is_writable( \IPS\ROOT_PATH . '/plugins/hooks.txt' ) ) { throw new \RuntimeException( sprintf( \IPS\Member::loggedIn()->language()->get("hook_file_not_writable"), \IPS\ROOT_PATH . '/plugins/hooks.txt' ) ); } try { $application = \IPS\Application::load( $app ); $application->installJsonData( TRUE ); /* Upgrade history */ $versions = $application->getAllVersions(); $longVersions = array_keys( $versions ); $humanVersions = array_values( $versions ); if( count($versions) ) { $latestLVersion = array_pop( $longVersions ); $latestHVersion = array_pop( $humanVersions ); \IPS\Db::i()->insert( 'core_upgrade_history', array( 'upgrade_version_human' => $latestHVersion, 'upgrade_version_id' => $latestLVersion, 'upgrade_date' => time(), 'upgrade_mid' => (int) \IPS\Member::loggedIn()->member_id, 'upgrade_app' => $app ) ); } } catch( \Exception $e ) { try { \IPS\Log::i( LOG_ERR )->write( $e, 'upgrade_error' ); } catch( \RuntimeException $ex ){} throw $e; } /* Update application data */ if( file_exists( \IPS\ROOT_PATH . '/applications/' . $app . '/data/application.json' ) ) { try { \IPS\Log::i( LOG_INFO )->write( "Installing application data for " . $app, 'upgrade' ); } catch( \RuntimeException $e ){} $application = json_decode( file_get_contents( \IPS\ROOT_PATH . '/applications/' . $app . '/data/application.json' ), TRUE ); //\IPS\Lang::saveCustom( $app, "__app_{$app}", $application['application_title'] ); unset( $application['app_directory'], $application['app_protected'], $application['application_title'] ); $app = \IPS\Application::load( $app ); foreach( $application as $column => $value ) { $column = str_replace( 'app_', '', $column ); $app->$column = $value; } $app->save( TRUE ); } else { try { \IPS\Log::i( LOG_ERR )->write( "Error: Missing app data", 'upgrade_error' ); } catch( \RuntimeException $e ){} throw new \LogicException( \IPS\Member::loggedIn()->language()->addToStack( 'err_missing_app_data', FALSE, array( 'sprintf' => array( $app ) ) ) ); } } ); } /** * Step 4 * Update settings, tasks, etc. * * @return array Multiple-Redirector Data */ protected function step4( $data ) { return $this->appLoop( $data, function( $app ) { try { \IPS\Log::i( LOG_INFO )->write( "Installing settings, tasks and keywords for " . $app, 'upgrade' ); } catch( \RuntimeException $e ){} \IPS\Application::load( $app )->installSettings(); \IPS\Application::load( $app )->installTasks(); \IPS\Application::load( $app )->installSearchKeywords(); } ); } /** * Step 5 * Update Languages * * @return array Multiple-Redirector Data */ protected function step5( $data ) { return $this->appLoop( $data, function( $app ) use ($data) { if ( !isset( $data[2] ) ) { $data[2] = 0; } try { $inserted = \IPS\Application::load( $app )->installLanguages( $data[2], 250 ); try { \IPS\Log::i( LOG_INFO )->write( "Inserted language keys for " . $app . ", offset of " . $data[2], 'upgrade' ); } catch( \RuntimeException $e ){} } catch( \Exception $ex ) { try { \IPS\Log::i( LOG_ERR )->write( "Step 5, " . $app . ' ' . $ex->getMessage(), 'upgrade_error' ); } catch( \RuntimeException $e ){} throw $ex; } if ( $inserted ) { $data[2] += $inserted; return $data; } else { return null; } } ); } /** * Step 6 * Update Email Templates * * @return array Multiple-Redirector Data */ protected function step6( $data ) { return $this->appLoop( $data, function( $app ) { \IPS\Application::load( $app )->installEmailTemplates(); } ); } /** * Step 7 * Update theme settings * * @return array Multiple-Redirector Data */ protected function step7( $data ) { return $this->appLoop( $data, function( $app ) { try { \IPS\Application::load( $app )->installThemeSettings( TRUE ); } catch( \Exception $e ) { try { \IPS\Log::i( LOG_ERR )->write( "Error : " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'upgrade_error' ); } catch( \RuntimeException $e ){} throw $e; } try { \IPS\Log::i( LOG_INFO )->write( "Installed theme settings for " . $app, 'upgrade' ); } catch( \RuntimeException $e ){} return null; } ); } /** * Step 8 * Clear existing templates * * @return array Multiple-Redirector Data */ protected function step8( $data ) { /* Clear old caches */ \IPS\Data\Cache::i()->clearAll(); \IPS\Data\Store::i()->clearAll(); \IPS\Theme::clearFiles( \IPS\Theme::IMAGES ); return $this->appLoop( $data, function( $app ) use( $data ) { try { /* Deletes old data from the database */ \IPS\Application::load( $app )->clearTemplates(); } catch( \Exception $e ) { try { \IPS\Log::i( LOG_ERR )->write( "Error : " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'upgrade_error' ); } catch( \RuntimeException $e ){} } } ); } /** * Step 9 * Update templates * * @return array Multiple-Redirector Data */ protected function step9( $data ) { if( isset( \IPS\Data\Store::i()->settings ) ) { unset( \IPS\Data\Store::i()->settings ); } if( isset( \IPS\Data\Store::i()->storageConfigurations ) ) { unset( \IPS\Data\Store::i()->storageConfigurations ); } return $this->appLoop( $data, function( $app ) use( $data ) { if ( !isset( $data[2] ) ) { $data[2] = 0; } try { $inserted = \IPS\Application::load( $app )->installTemplates( TRUE, $data[2], 150 ); } catch( \Exception $e ) { try { \IPS\Log::i( LOG_ERR )->write( "Error : " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'upgrade_error' ); } catch( \RuntimeException $e ){} throw $e; } try { \IPS\Log::i( LOG_INFO )->write( "Installed templates for " . $app . ", offset of " . $data[2], 'upgrade' ); } catch( \RuntimeException $e ){} if ( $inserted ) { $data[2] += $inserted; return $data; } else { return null; } } ); } /** * Step 10 * Update Javascript * * @return array Multiple-Redirector Data */ protected function step10( $data ) { return $this->appLoop( $data, function( $app ) use( $data ) { if ( !isset( $data[2] ) ) { $data[2] = 0; } $inserted = \IPS\Application::load( $app )->installJavascript( $data[2], 150 ); try { \IPS\Log::i( LOG_INFO )->write( "Installed javascript for " . $app, 'upgrade' ); } catch( \RuntimeException $e ){} if ( $inserted ) { $data[2] += $inserted; return $data; } else { return null; } } ); } /** * Class static methods * */ /** * Set the upgrading flag to prevent compilation of themes/js elsewhere until we're ready * * @param boolean $value Value of flag to save * @return void */ public static function setUpgradingFlag( $value=TRUE ) { try { \IPS\Db::i()->select( 'conf_value', 'core_sys_conf_settings', array( 'conf_key=?', 'setup_in_progress' ) )->first(); } catch( \UnderflowException $ex ) { if ( \IPS\Application::load('core')->long_version >= 40000 ) { $insert = array( 'conf_key' => 'setup_in_progress', 'conf_value' => 0, 'conf_default' => 0, 'conf_keywords' => '', 'conf_app' => 'core' ); } else { $insert = array( 'conf_key' => 'setup_in_progress', 'conf_value' => 0, 'conf_default' => 0, 'conf_keywords' => '', 'conf_type' => 'yes_no' ); } /* This key was added in 4.0.8 so it may not exist */ \IPS\Db::i()->insert( 'core_sys_conf_settings', $insert ); } \IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => intval( $value ) ), array( 'conf_key=?', 'setup_in_progress' ) ); unset( \IPS\Data\Store::i()->settings ); } /** * Runs a bunch of legacy SQL queries * * @param string $app App key to run * @param int $upgradeId Current upgrade ID * @param string $file File holding $SQL array * @return TRUE|\IPS\Db\Exception * @note We ignore some database errors that shouldn't prevent us from continuing. * @li 1050: Can't rename a table as it already exists * @li 1060: Can't add a column as it already exists * @li 1062: Can't add an index as index already exists * @li 1062: Can't add a row as PKEY already exists * @li 1091: Can't drop key or column because it does not exist */ public static function runLegacySql( $app, $upgradeId, $file='queries.php' ) { $queryFile = \IPS\ROOT_PATH . '/applications/' . $app . '/setup/upg_' . $upgradeId . '/' . $file; $lastIndex = ( isset( $_SESSION['lastJsonIndex'] ) ) ? $_SESSION['lastJsonIndex'] : 0; if ( file_exists( $queryFile ) ) { require( $queryFile ); if ( is_array( $SQL ) ) { foreach( $SQL as $k => $query ) { if ( $lastIndex AND $lastIndex >= $k ) { continue; } $_SESSION['lastJsonIndex'] = $k; try { \IPS\Db::i()->query( static::addPrefixToQuery( $query ) ); } catch( \IPS\Db\Exception $e ) { if ( ! in_array( $e->getCode(), array( 1050, 1060, 1061, 1062, 1091 ) ) ) { throw $e; } } } } } } /** * Add SQL Prefix to Query * * @param string SQL Query * @return string */ public static function addPrefixToQuery( $query ) { if ( \IPS\Db::i()->prefix ) { $query = preg_replace( "#^CREATE TABLE(?:\s+?)?(\S+)#is" , "CREATE TABLE " . \IPS\Db::i()->prefix . "\\1 ", $query ); $query = preg_replace( "#^RENAME TABLE(?:\s+?)?(\S+)\s+?TO\s+?(\S+?)(\s|$)#i" , "RENAME TABLE " . \IPS\Db::i()->prefix . "\\1 TO " . \IPS\Db::i()->prefix ."\\2", $query ); $query = preg_replace( "#^DROP TABLE( IF EXISTS)?(?:\s+?)?(\S+)(\s+?)?#i" , "DROP TABLE \\1 " . \IPS\Db::i()->prefix . "\\2 ", $query ); $query = preg_replace( "#^TRUNCATE TABLE(?:\s+?)?(\S+)(\s+?)?#i", "TRUNCATE TABLE ". \IPS\Db::i()->prefix . "\\1 ", $query ); $query = preg_replace( "#^DELETE FROM(?:\s+?)?(\S+)(\s+?)?#i" , "DELETE FROM " . \IPS\Db::i()->prefix . "\\1 ", $query ); $query = preg_replace( "#^INSERT INTO(?:\s+?)?(\S+)\s+?#i" , "INSERT INTO " . \IPS\Db::i()->prefix . "\\1 ", $query ); //$query = preg_replace( "#^INSERT IGNORE INTO(?:\s+?)?(\S+)\s+?#i", "INSERT IGNORE INTO " . \IPS\Db::i()->prefix . "\\1 ", $query ); $query = preg_replace( "#^UPDATE(?:\s+?)?(\S+)\s+?#i" , "UPDATE " . \IPS\Db::i()->prefix . "\\1 ", $query ); $query = preg_replace( "#^REPLACE INTO(?:\s+?)?(\S+)\s+?#i" , "REPLACE INTO " . \IPS\Db::i()->prefix . "\\1 ", $query ); $query = preg_replace( "#^ALTER TABLE(?:\s+?)?(\S+)\s+?#i" , "ALTER TABLE " . \IPS\Db::i()->prefix . "\\1 ", $query ); $query = preg_replace( "#^ALTER IGNORE TABLE(?:\s+?)?(\S+)\s+?#i" , "ALTER IGNORE TABLE " . \IPS\Db::i()->prefix . "\\1 ", $query ); $query = preg_replace( "#^CREATE INDEX (\S+) ON (\S+) #", "CREATE INDEX \\1 ON " . \IPS\Db::i()->prefix . "\\2 " , $query ); $query = preg_replace( "#^CREATE UNIQUE INDEX (\S+) ON (\S+) #", "CREATE UNIQUE INDEX \\1 ON " . \IPS\Db::i()->prefix . "\\2 " , $query ); } return $query; } /** * Run manual queries, that may be rather large * * @param array $queries Array of queries in the following format ( table => x, query = x ); * @return array */ public static function runManualQueries( $queries ) { $toReturn = array(); $tableCounts = array(); foreach( $queries as $id => $data ) { if ( ! isset( $tableCounts[ $data['table'] ] ) and \IPS\Db::i()->checkForTable( $data['table'] ) ) { $tableCounts[ $data['table'] ] = \IPS\Db::i()->select( 'count(*)', $data['table'] )->first(); } } foreach( $queries as $id => $data ) { if( !\IPS\Request::i()->run_anyway and isset( $tableCounts[ $data['table'] ] ) AND $tableCounts[ $data['table'] ] > \IPS\UPGRADE_MANUAL_THRESHOLD ) { try { \IPS\Log::i( LOG_INFO )->write( "Big table " . $data['table'] . ", storing query to run manually", 'upgrade' ); } catch( \RuntimeException $e ){} $toReturn[] = $data['query']; continue; } else { \IPS\Db::i()->query( $data['query'] ); } } return $toReturn; } /** * Determine what our cutoff should be for long running queries * * @return null|int */ public static function determineCutoff() { $cutOff = 30; if( $maxExecution = @ini_get( 'max_execution_time' ) AND $maxExecution < $cutOff ) { $cutOff = $maxExecution; } return time() + ( $cutOff * .6 ); } /** * Repair File URLs * * @param string $application Application key * @return void */ public static function repairFileUrls( $application ) { $settings = json_decode( \IPS\Settings::i()->upload_settings, TRUE ); foreach ( $settings as $k => $v ) { $exploded = explode( '_', $k ); $classname = "IPS\\{$exploded[2]}\\extensions\\core\\FileStorage\\{$exploded[3]}"; if ( $exploded[2] != $application ) { continue; } if( class_exists( $classname ) ) { $extension = new $classname; \IPS\Task::queue( 'core', 'RepairFileUrls', array( 'storageExtension' => $k, 'count' => $extension->count() ), 1 ); } } } /** * Determine what our cutoff should be for long running queries * * @param array $changes The changes to make to the mr data * @return string */ public static function adjustMultipleRedirect( $changes ) { $mr = json_decode( urldecode( base64_decode( \IPS\Request::i()->mr ) ), TRUE ); foreach( $changes as $k => $v ) { if( is_array( $v ) ) { foreach( $v as $_k => $_v ) { $mr[ $k ][ $_k ] = $_v; } } else { $mr[ $k ] = $v; } } \IPS\Request::i()->mr = base64_encode( urlencode( json_encode( $mr ) ) ); return \IPS\Request::i()->mr; } }