Files
IPB/applications/core/sources/Setup/Upgrade.php
T
2025-12-19 05:32:19 -08:00

1240 lines
34 KiB
PHP

<?php
/**
* @brief Upgrader
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @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 ( \Exception $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.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
{
$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;
}
}