<?php
/**
* @brief Upgrader
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 21 May 2014
* @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 Requirements */
$return = array_merge_recursive( $return, static::mysqlRequirements( ) );
$writeablesKey = \IPS\Member::loggedIn()->language()->addToStack('requirements_file_system');
/* Writeables */
if( \IPS\Db::i()->checkForTable('core_file_storage') )
{
$fileSystemItems = array();
foreach ( \IPS\Application::allExtensions( 'core', 'FileStorage', FALSE ) as $k => $v )
{
try
{
try
{
$class = \IPS\File::getClass( $k );
}
catch( \RuntimeException $e )
{
throw new \RuntimeException( 'no_filestorage_class', 115115 );
}
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 ( \Exception $e )
{
if( $e->getCode() !== 115115 )
{
/* We don't want to stop upgrader for this, but we can show the message if the upgrader otherwise cannot continue */
$fileSystemItems[ $class->configurationId ] = array(
'success' => TRUE,
'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;
}
/**
* Check MySQL Requirements
*
* @param \IPS\Db|NULL $db DB Object to use for version check
* @return array
*/
public static function mysqlRequirements( $db=NULL )
{
/* MySQL Version */
$return = array();
$mysqlVersion = $db ? $db->server_info : \IPS\Db::i()->server_info;
$requirements = json_decode( file_get_contents( \IPS\ROOT_PATH . '/applications/core/data/requirements.json' ), TRUE );
if ( version_compare( $mysqlVersion, $requirements['mysql']['required'] ) >= 0 )
{
$return['requirements']['MySQL']['version'] = array(
'success' => TRUE,
'message' => \IPS\Member::loggedIn()->language()->addToStack( 'requirements_mysql_version_success', FALSE, array( 'sprintf' => array( $mysqlVersion ) ) )
);
if ( version_compare( $mysqlVersion, $requirements['mysql']['recommended'] ) == -1 AND !mb_strpos( $mysqlVersion, "MariaDB" ) )
{
$return['advice'][] = \IPS\Member::loggedIn()->language()->addToStack( 'requirements_mysql_version_advice', FALSE, array( 'sprintf' => array( $mysqlVersion, $requirements['mysql']['recommended'] ) ) );
}
}
else
{
$return['requirements']['MySQL']['version'] = array(
'success' => FALSE,
'message' => \IPS\Member::loggedIn()->language()->addToStack( 'requirements_mysql_version_fail', FALSE, array( 'sprintf' => array( $mysqlVersion, $requirements['mysql']['required'], $requirements['mysql']['recommended'] ) ) ),
);
}
/* MySQL timeouts */
$db = $db ?: \IPS\Db::i();
try
{
$query = $db->query( "SHOW VARIABLES LIKE '%wait_timeout%'" );
while( $row = $query->fetch_assoc() )
{
if ( $row['Variable_name'] == 'wait_timeout' AND $row['Value'] < 20 )
{
$return['advice'][] = \IPS\Member::loggedIn()->language()->addToStack( 'requirements_mysql_timeout', FALSE, array( 'sprintf' => array( $row['Variable_name'], $row['Value'] ) ) );
}
}
}
catch( \IPS\Db\Exception $e )
{
$return['advice'][] = $e->getMessage();
}
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 )
{
$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 );
if ( function_exists('opcache_reset') )
{
@opcache_reset();
}
}
/* 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 cache for default theme */
try
{
$theme = \IPS\Theme::load( \IPS\Theme::defaultTheme() );
$theme->buildResourceMap();
$theme->saveSet();
\IPS\Theme::deleteCompiledCss();
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'upgrade_error' );
}
/* Make all disk template caches stale */
\IPS\Theme::resetAllCacheKeys();
/* 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;
}
\IPS\Log::debug( "Step1 Loop: " . json_encode( $data ), 'upgrade' );
/* 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 . ')';
\IPS\Log::debug( "Upgrading database for app: " . $app, 'upgrade' );
$_SESSION['lastSqlError'] = null;
try
{
$fetched = \IPS\Application::load( $app )->installDatabaseUpdates( $extra['_current'], ( \IPS\Request::i()->run_anyway ) ? $lastSqlId - 0.1 : $lastSqlId, 10, !\IPS\Request::i()->run_anyway );
if( isset( \IPS\Request::i()->mr_continue ) AND \IPS\Request::i()->mr_continue )
{
$fetched = array( 'count' => $fetched['count'] );
}
$extra['lastSqlId'] = $_SESSION['lastJsonIndex'];
\IPS\Log::debug( (int) $fetched['count'] . ' queries run, last ID ' . $_SESSION['lastJsonIndex'], 'upgrade' );
}
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;
\IPS\Log::log( "Error: " . $message . ' ' . $queryRun . "\n" . $ex->getTraceAsString(), 'upgrade_error' );
/* Throw so ajax returns 500 and stops upgrader */
throw $ex;
}
/* Queries to run manually */
if( is_array( $fetched['queriesToRun'] ) and count( $fetched['queriesToRun'] ) )
{
\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' => \IPS\Request::i()->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 */
}
}
/* The "run anyway" button uses the same URL as the "I have run, continue" button which increments the upgrade step count, but this means that the run anyway step never actually runs */
if ( isset( \IPS\Request::i()->run_anyway ) and $upgradeStep > 1 )
{
$upgradeStep--;
}
/* 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 ) ) );
\IPS\Log::log( "(Upgrader " . $_methodName . ") " . $message . ' ' . $queryRun . $ex->getTraceAsString(), 'upgrade_error' );
/* 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 ) ) );
\IPS\Log::log( "(Upgrader " . $_methodName . ") " . $message, 'upgrade_error' );
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 )
{
\IPS\Log::debug( "Running finish step for: {$_versionApp} version {$_versionId}", 'upgrade' );
/* 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 )
{
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_2_app'), $app );
$changesToMake = \IPS\Application::load( $app )->databaseCheck();
if ( count( $changesToMake ) )
{
$toRun = \IPS\core\Setup\Upgrade::runManualQueries( $changesToMake );
if ( count( $toRun ) )
{
\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' => \IPS\Request::i()->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.php' ) )
{
@touch( \IPS\ROOT_PATH . '/plugins/hooks.php' );
@chmod( \IPS\ROOT_PATH . '/plugins/hooks.php', 0777 );
}
if( !is_writable( \IPS\ROOT_PATH . '/plugins/hooks.php' ) )
{
throw new \RuntimeException( sprintf( \IPS\Member::loggedIn()->language()->get("hook_file_not_writable"), \IPS\ROOT_PATH . '/plugins/hooks.php' ) );
}
try
{
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_3_app'), $app );
$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 )
{
\IPS\Log::log( $e, 'upgrade_error' );
throw $e;
}
/* Update application data */
if( file_exists( \IPS\ROOT_PATH . '/applications/' . $app . '/data/application.json' ) )
{
\IPS\Log::debug( "Installing application data for " . $app, 'upgrade' );
$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
{
\IPS\Log::log( "Error: Missing app data", 'upgrade_error' );
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 )
{
\IPS\Log::debug( "Installing settings, tasks and keywords for " . $app, 'upgrade' );
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_4_app'), $app );
\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
{
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_5_app'), $app, $data[2] );
$inserted = \IPS\Application::load( $app )->installLanguages( $data[2], 250 );
\IPS\Log::debug( "Inserted language keys for " . $app . ", offset of " . $data[2], 'upgrade' );
}
catch( \Exception $ex )
{
\IPS\Log::log( "Step 5, " . $app . ' ' . $ex->getMessage(), 'upgrade_error' );
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 )
{
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_6_app'), $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
{
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_7_app'), $app );
\IPS\Application::load( $app )->installThemeSettings( TRUE );
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'upgrade_error' );
throw $e;
}
\IPS\Log::debug( "Installed theme settings for " . $app, 'upgrade' );
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 */
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_8_app'), $app );
\IPS\Application::load( $app )->clearTemplates();
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'upgrade_error' );
}
} );
}
/**
* Step 9
* Update templates
*
* @return array Multiple-Redirector Data
*/
protected function step9( $data )
{
\IPS\Settings::i()->clearCache();
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
{
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_9_app'), $app, $data[2] );
$inserted = \IPS\Application::load( $app )->installTemplates( TRUE, $data[2], 150 );
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'upgrade_error' );
throw $e;
}
\IPS\Log::debug( "Installed templates for " . $app . ", offset of " . $data[2], 'upgrade' );
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;
}
$this->customTitle = sprintf( \IPS\Member::loggedIn()->language()->get('upgrade_step_10_app'), $app, $data[2] );
$inserted = \IPS\Application::load( $app )->installJavascript( $data[2], 100 );
\IPS\Log::debug( "Installed javascript for " . $app, 'upgrade' );
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\Settings::i()->changeValues( array( 'setup_in_progress' => intval( $value ) ) );
}
/**
* 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 1051: Can't drop a table because it doesn't exist
* @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( 1007, 1008, 1050, 1060, 1061, 1062, 1091, 1051 ) ) )
{
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
*
* @note We ignore some database errors that shouldn't prevent us from continuing.
* @li 1007: Can't create database because it already exists
* @li 1008: Can't drop database because it does not exist
* @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 1069: MyISAM has maxed out number of allowed indexes per table
* @li 1091: Can't drop key or column because it does not exist
* @param array $queries Array of queries in the following format ( table => x, query = x, db => null ); Supply an \IPS\Db object as db if necessary (i.e. remote archiving)
* @return array
*/
public static function runManualQueries( $queries )
{
if ( isset( \IPS\Request::i()->mr_continue ) AND \IPS\Request::i()->mr_continue )
{
return array();
}
$toReturn = array();
$tableCounts = array();
foreach( $queries as $id => $data )
{
$database = ( isset( $data['db'] ) ) ? $data['db'] : \IPS\Db::i();
if ( ! isset( $tableCounts[ $data['table'] ] ) and $database->checkForTable( $data['table'] ) )
{
$tableCounts[ $data['table'] ] = $database->select( 'count(*)', $data['table'] )->first();
}
}
foreach( $queries as $id => $data )
{
$database = ( isset( $data['db'] ) ) ? $data['db'] : \IPS\Db::i();
if( !\IPS\Request::i()->run_anyway and isset( $tableCounts[ $data['table'] ] ) AND $tableCounts[ $data['table'] ] > \IPS\UPGRADE_MANUAL_THRESHOLD )
{
\IPS\Log::debug( "Big table " . $data['table'] . ", storing query to run manually", 'upgrade' );
$toReturn[] = $data['query'];
continue;
}
else
{
try
{
$database->query( $data['query'] );
}
catch( \IPS\Db\Exception $e )
{
/* If the error isn't important we should ignore it and is consistent with \Application::installDatabaseUpdates() */
if( !in_array( $e->getCode(), array( 1007, 1008, 1050, 1060, 1061, 1062, 1069, 1091 ) ) )
{
throw $e;
}
}
}
}
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 > 0 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;
try
{
\IPS\Task::queue( 'core', 'RepairFileUrls', array( 'storageExtension' => $k, 'count' => $extension->count() ), 1 );
}
catch ( \IPS\Db\Exception $e )
{
/*
* Don't throw an exception if the table or a column doesn't exist, this can happen
* when newer versions add storage configurations that haven't been set up yet.
*/
if( $e->getCode() !== 1146 AND $e->getCode() !== 1054 )
{
throw $e;
}
}
}
}
}
/**
* 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 )
{
$key = 'mr-' . md5( (string) \IPS\Http\Url::internal( 'controller=upgrade' )->setQueryString( 'key', $_SESSION['uniqueKey'] ) );
$mr = isset( $_SESSION[ $key ] ) ? $_SESSION[ $key ] : NULL;
$mr = $mr ? json_decode( $mr, TRUE ) : NULL;
foreach( $changes as $k => $v )
{
if( is_array( $v ) )
{
foreach( $v as $_k => $_v )
{
$mr[ $k ][ $_k ] = $_v;
}
}
else
{
$mr[ $k ] = $v;
}
}
$_SESSION[ $key ] = json_encode( $mr );
return $_SESSION[ $key ];
}
}