2018-10-05 22:53:13 +00:00
< ? php
namespace Friendica\Core ;
2019-03-24 21:51:30 +00:00
use Friendica\App ;
use Friendica\Core\Config\Cache\IConfigCache ;
2018-10-29 09:16:07 +00:00
use Friendica\Database\DBA ;
2018-10-05 22:53:13 +00:00
use Friendica\Database\DBStructure ;
2019-03-25 08:39:33 +00:00
use Friendica\Util\BasePath ;
2019-03-24 21:51:30 +00:00
use Friendica\Util\Config\ConfigFileLoader ;
use Friendica\Util\Config\ConfigFileSaver ;
2018-11-08 15:26:49 +00:00
use Friendica\Util\Strings ;
2018-10-05 22:53:13 +00:00
class Update
{
2018-10-14 11:19:37 +00:00
const SUCCESS = 0 ;
const FAILED = 1 ;
2018-10-14 11:26:53 +00:00
/**
* @ brief Function to check if the Database structure needs an update .
*
2019-03-30 17:54:22 +00:00
* @ param string $basePath The base path of this application
* @ param boolean $via_worker Is the check run via the worker ?
* @ param App\Mode $mode The current app mode
*
2019-01-06 21:06:53 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-14 11:26:53 +00:00
*/
2019-03-30 17:54:22 +00:00
public static function check ( $basePath , $via_worker , App\Mode $mode )
2018-10-14 11:26:53 +00:00
{
2018-12-03 01:57:41 +00:00
if ( ! DBA :: connected ()) {
return ;
}
2019-03-30 17:54:22 +00:00
// Check if the config files are set correctly
self :: checkConfigFile ( $basePath , $mode );
2019-03-24 21:51:30 +00:00
// Don't check the status if the last update was failed
if ( Config :: get ( 'system' , 'update' , Update :: SUCCESS , true ) == Update :: FAILED ) {
return ;
}
2019-03-09 23:28:48 +00:00
$build = Config :: get ( 'system' , 'build' );
if ( empty ( $build )) {
Config :: set ( 'system' , 'build' , DB_UPDATE_VERSION - 1 );
$build = DB_UPDATE_VERSION - 1 ;
}
2018-10-14 11:26:53 +00:00
// We don't support upgrading from very old versions anymore
if ( $build < NEW_UPDATE_ROUTINE_VERSION ) {
die ( 'You try to update from a version prior to database version 1170. The direct upgrade path is not supported. Please update to version 3.5.4 before updating to this version.' );
}
2019-03-09 23:28:48 +00:00
if ( $build < DB_UPDATE_VERSION ) {
2019-02-24 10:52:40 +00:00
if ( $via_worker ) {
// Calling the database update directly via the worker enables us to perform database changes to the workerqueue table itself.
// This is a fallback, since normally the database update will be performed by a worker job.
// This worker job doesn't work for changes to the "workerqueue" table itself.
self :: run ( $basePath );
} else {
Worker :: add ( PRIORITY_CRITICAL , 'DBUpdate' );
}
2018-10-14 11:26:53 +00:00
}
}
2018-10-05 22:53:13 +00:00
/**
* Automatic database updates
2018-10-29 09:16:07 +00:00
*
2019-02-03 21:46:50 +00:00
* @ param string $basePath The base path of this application
2019-02-24 11:24:09 +00:00
* @ param bool $force Force the Update - Check even if the database version doesn ' t match
* @ param bool $override Overrides any running / stuck updates
2019-02-03 21:46:50 +00:00
* @ param bool $verbose Run the Update - Check verbose
* @ param bool $sendMail Sends a Mail to the administrator in case of success / failure
2018-10-29 09:16:07 +00:00
*
* @ return string Empty string if the update is successful , error messages otherwise
2019-01-06 21:06:53 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-05 22:53:13 +00:00
*/
2019-02-24 11:24:09 +00:00
public static function run ( $basePath , $force = false , $override = false , $verbose = false , $sendMail = true )
2018-10-05 22:53:13 +00:00
{
2018-10-29 09:16:07 +00:00
// In force mode, we release the dbupdate lock first
// Necessary in case of an stuck update
2019-02-24 11:24:09 +00:00
if ( $override ) {
2019-02-24 09:08:28 +00:00
Lock :: release ( 'dbupdate' , true );
2018-10-29 09:16:07 +00:00
}
2019-03-10 18:59:20 +00:00
$build = Config :: get ( 'system' , 'build' , null , true );
2019-03-09 23:28:48 +00:00
if ( empty ( $build ) || ( $build > DB_UPDATE_VERSION )) {
$build = DB_UPDATE_VERSION - 1 ;
Config :: set ( 'system' , 'build' , $build );
}
2018-10-05 22:53:13 +00:00
2019-03-09 23:28:48 +00:00
if ( $build != DB_UPDATE_VERSION || $force ) {
2018-10-05 22:53:13 +00:00
require_once 'update.php' ;
2019-03-09 23:28:48 +00:00
$stored = intval ( $build );
$current = intval ( DB_UPDATE_VERSION );
if ( $stored < $current || $force ) {
Config :: load ( 'database' );
2018-10-05 22:53:13 +00:00
2019-03-11 20:36:41 +00:00
Logger :: info ( 'Update starting.' , [ 'from' => $stored , 'to' => $current ]);
2018-10-31 14:22:44 +00:00
2019-03-09 23:28:48 +00:00
// Compare the current structure with the defined structure
// If the Lock is acquired, never release it automatically to avoid double updates
if ( Lock :: acquire ( 'dbupdate' , 120 , Cache :: INFINITE )) {
2018-10-06 18:38:35 +00:00
2019-03-10 18:59:20 +00:00
// Checks if the build changed during Lock acquiring (so no double update occurs)
$retryBuild = Config :: get ( 'system' , 'build' , null , true );
if ( $retryBuild !== $build ) {
2019-03-11 20:36:41 +00:00
Logger :: info ( 'Update already done.' , [ 'from' => $stored , 'to' => $current ]);
2019-03-10 18:59:20 +00:00
Lock :: release ( 'dbupdate' );
return '' ;
}
2019-03-09 23:28:48 +00:00
// run the pre_update_nnnn functions in update.php
for ( $x = $stored + 1 ; $x <= $current ; $x ++ ) {
$r = self :: runUpdateFunction ( $x , 'pre_update' );
if ( ! $r ) {
2019-03-24 21:51:30 +00:00
Config :: set ( 'system' , 'update' , Update :: FAILED );
Lock :: release ( 'dbupdate' );
return $r ;
2019-03-09 23:28:48 +00:00
}
}
2018-10-05 22:53:13 +00:00
2019-03-09 23:28:48 +00:00
// update the structure in one call
$retval = DBStructure :: update ( $basePath , $verbose , true );
if ( ! empty ( $retval )) {
if ( $sendMail ) {
self :: updateFailed (
DB_UPDATE_VERSION ,
$retval
);
}
2019-03-11 20:36:41 +00:00
Logger :: error ( 'Update ERROR.' , [ 'from' => $stored , 'to' => $current , 'retval' => $retval ]);
2019-03-24 21:51:30 +00:00
Config :: set ( 'system' , 'update' , Update :: FAILED );
2019-03-09 23:28:48 +00:00
Lock :: release ( 'dbupdate' );
return $retval ;
} else {
Config :: set ( 'database' , 'last_successful_update' , $current );
Config :: set ( 'database' , 'last_successful_update_time' , time ());
2019-03-11 20:36:41 +00:00
Logger :: info ( 'Update finished.' , [ 'from' => $stored , 'to' => $current ]);
2018-10-06 18:38:35 +00:00
}
2018-10-05 22:53:13 +00:00
2019-03-09 23:28:48 +00:00
// run the update_nnnn functions in update.php
for ( $x = $stored + 1 ; $x <= $current ; $x ++ ) {
$r = self :: runUpdateFunction ( $x , 'update' );
if ( ! $r ) {
2019-03-24 21:51:30 +00:00
Config :: set ( 'system' , 'update' , Update :: FAILED );
Lock :: release ( 'dbupdate' );
return $r ;
2019-03-09 23:28:48 +00:00
}
2018-10-05 22:53:13 +00:00
}
2018-10-06 18:38:35 +00:00
2019-03-11 20:36:41 +00:00
Logger :: notice ( 'Update success.' , [ 'from' => $stored , 'to' => $current ]);
2019-03-09 23:28:48 +00:00
if ( $sendMail ) {
self :: updateSuccessfull ( $stored , $current );
2018-10-29 09:16:07 +00:00
}
2019-03-24 21:51:30 +00:00
Config :: set ( 'system' , 'update' , Update :: SUCCESS );
2019-03-09 23:28:48 +00:00
Lock :: release ( 'dbupdate' );
2018-10-05 22:53:13 +00:00
}
}
}
2018-10-29 09:16:07 +00:00
return '' ;
2018-10-05 22:53:13 +00:00
}
/**
* Executes a specific update function
*
2019-01-06 21:06:53 +00:00
* @ param int $x the DB version number of the function
2018-10-05 22:53:13 +00:00
* @ param string $prefix the prefix of the function ( update , pre_update )
*
* @ return bool true , if the update function worked
2019-01-06 21:06:53 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-05 22:53:13 +00:00
*/
public static function runUpdateFunction ( $x , $prefix )
{
$funcname = $prefix . '_' . $x ;
2019-03-11 20:36:41 +00:00
Logger :: info ( 'Update function start.' , [ 'function' => $funcname ]);
2018-10-31 14:22:44 +00:00
2018-10-05 22:53:13 +00:00
if ( function_exists ( $funcname )) {
// There could be a lot of processes running or about to run.
// We want exactly one process to run the update command.
// So store the fact that we're taking responsibility
// after first checking to see if somebody else already has.
// If the update fails or times-out completely you may need to
// delete the config entry to try again.
2018-10-29 09:21:10 +00:00
if ( Lock :: acquire ( 'dbupdate_function' , 120 , Cache :: INFINITE )) {
2018-10-06 18:38:35 +00:00
// call the specific update
$retval = $funcname ();
2018-10-05 22:53:13 +00:00
2018-10-06 18:38:35 +00:00
if ( $retval ) {
//send the administrator an e-mail
2018-10-29 09:16:07 +00:00
self :: updateFailed (
2018-10-06 18:38:35 +00:00
$x ,
L10n :: t ( 'Update %s failed. See error logs.' , $x )
);
2019-03-11 20:36:41 +00:00
Logger :: error ( 'Update function ERROR.' , [ 'function' => $funcname , 'retval' => $retval ]);
2018-10-06 18:38:35 +00:00
Lock :: release ( 'dbupdate_function' );
return false ;
} else {
Config :: set ( 'database' , 'last_successful_update_function' , $funcname );
Config :: set ( 'database' , 'last_successful_update_function_time' , time ());
if ( $prefix == 'update' ) {
Config :: set ( 'system' , 'build' , $x );
}
Lock :: release ( 'dbupdate_function' );
2019-03-11 20:36:41 +00:00
Logger :: info ( 'Update function finished.' , [ 'function' => $funcname ]);
2018-10-06 18:38:35 +00:00
return true ;
}
2018-10-05 22:53:13 +00:00
}
} else {
2019-03-11 20:36:41 +00:00
Logger :: info ( 'Update function skipped.' , [ 'function' => $funcname ]);
2018-10-06 18:38:35 +00:00
Config :: set ( 'database' , 'last_successful_update_function' , $funcname );
Config :: set ( 'database' , 'last_successful_update_function_time' , time ());
2018-10-05 22:53:13 +00:00
if ( $prefix == 'update' ) {
Config :: set ( 'system' , 'build' , $x );
}
return true ;
}
}
2018-10-29 09:16:07 +00:00
2019-03-24 21:51:30 +00:00
/**
* Checks the config settings and saves given config values into the config file
*
2019-03-30 17:54:22 +00:00
* @ param string $basePath The basepath of Friendica
* @ param App\Mode $$mode The current App mode
2019-03-24 21:51:30 +00:00
*
* @ return bool True , if something has been saved
*/
2019-03-30 17:54:22 +00:00
public static function checkConfigFile ( $basePath , App\Mode $mode )
2019-03-24 21:51:30 +00:00
{
2019-03-30 17:54:22 +00:00
if ( empty ( $basePath )) {
$basePath = BasePath :: create ( dirname ( __DIR__ , 2 ));
}
$config = [
'config' => [
'hostname' => [
'allowEmpty' => false ,
'default' => '' ,
],
],
'system' => [
'basepath' => [
'allowEmpty' => false ,
'default' => $basePath ,
],
]
];
2019-03-24 21:51:30 +00:00
$configFileLoader = new ConfigFileLoader ( $basePath , $mode );
$configCache = new Config\Cache\ConfigCache ();
2019-03-25 08:39:33 +00:00
$configFileLoader -> setupCache ( $configCache , true );
2019-03-30 17:54:22 +00:00
// checks if something is to update, otherwise skip this function at all
$missingConfig = $configCache -> keyDiff ( $config );
if ( empty ( $missingConfig )) {
return true ;
}
// We just want one update process
if ( Lock :: acquire ( 'config_update' )) {
$configFileSaver = new ConfigFileSaver ( $basePath );
$updated = false ;
$toDelete = [];
foreach ( $missingConfig as $category => $keys ) {
foreach ( $keys as $key => $value ) {
if ( self :: updateConfigEntry ( $configCache , $configFileSaver , $category , $key , $value [ 'allowEmpty' ], $value [ 'default' ])) {
$toDelete [] = [ 'cat' => $category , 'key' => $key ];
$updated = true ;
};
}
}
// In case there is nothing to do, skip the update
if ( ! $updated ) {
Lock :: release ( 'config_update' );
return true ;
}
if ( ! $configFileSaver -> saveToConfigFile ()) {
Logger :: alert ( 'Config entry update failed - maybe wrong permission?' );
Lock :: release ( 'config_update' );
return false ;
}
// After the successful save, remove the db values
foreach ( $toDelete as $delete ) {
DBA :: delete ( 'config' , [ 'cat' => $delete [ 'cat' ], 'k' => $delete [ 'key' ]]);
}
Lock :: release ( 'config_update' );
}
return true ;
}
2019-03-24 21:51:30 +00:00
/**
* Adds a value to the ConfigFileSave in case it isn ' t already updated
*
* @ param IConfigCache $configCache The cached config file
* @ param ConfigFileSaver $configFileSaver The config file saver
* @ param string $cat The config category
* @ param string $key The config key
2019-03-30 17:54:22 +00:00
* @ param bool $allowEmpty If true , empty values are valid ( Default there has to be a variable )
2019-03-25 08:39:33 +00:00
* @ param string $default A default value , if none of the settings are valid
2019-03-24 21:51:30 +00:00
*
* @ return boolean True , if a value was updated
*
* @ throws \Exception if DBA or Logger doesn ' t work
*/
2019-03-30 17:54:22 +00:00
private static function updateConfigEntry (
IConfigCache $configCache ,
ConfigFileSaver $configFileSaver ,
$cat ,
$key ,
$allowEmpty = false ,
$default = '' )
2019-03-24 21:51:30 +00:00
{
2019-03-30 17:54:22 +00:00
// check if the config file differs from the whole configuration (= The db contains other values)
$fileValue = $configCache -> get ( $cat , $key );
$dbConfig = DBA :: selectFirst ( 'config' , [ 'v' ], [ 'cat' => $cat , 'k' => $key ]);
2019-03-24 21:51:30 +00:00
2019-03-30 17:54:22 +00:00
if ( DBA :: isResult ( $dbConfig )) {
$dbValue = $dbConfig [ 'v' ];
} else {
$dbValue = null ;
}
2019-03-26 07:13:49 +00:00
2019-03-26 07:00:41 +00:00
// If the db contains a config value, check it
2019-03-30 17:54:22 +00:00
if ((
( $allowEmpty && isset ( $dbValue )) ||
( ! $allowEmpty && ! empty ( $dbValue ))
) &&
$fileValue !== $dbValue ) {
Logger :: info ( 'Difference in config found' , [ 'cat' => $cat , 'key' => $key , 'file' => $fileValue , 'db' => $dbValue ]);
$configFileSaver -> addConfigValue ( $cat , $key , $dbValue );
2019-03-26 07:00:41 +00:00
return true ;
2019-03-26 07:13:49 +00:00
// If both config values are not set, use the default value
2019-03-30 17:54:22 +00:00
} elseif (
( $allowEmpty && ! isset ( $fileValue ) && ! isset ( $dbValue )) ||
( ! $allowEmpty && empty ( $fileValue ) && empty ( $dbValue ) && ! empty ( $default ))) {
2019-03-25 08:39:33 +00:00
Logger :: info ( 'Using default for config' , [ 'cat' => $cat , 'key' => $key , 'value' => $default ]);
$configFileSaver -> addConfigValue ( $cat , $key , $default );
2019-03-26 07:00:41 +00:00
return true ;
// If either the file config value isn't empty or the db value is the same as the
// file config value, skip it
2019-03-24 21:51:30 +00:00
} else {
2019-03-30 17:54:22 +00:00
Logger :: debug ( 'No Difference in config found' , [ 'cat' => $cat , 'key' => $key , 'value' => $fileValue , 'db' => $dbValue ]);
2019-03-25 19:53:46 +00:00
return false ;
2019-03-24 21:51:30 +00:00
}
}
2018-10-29 09:16:07 +00:00
/**
* send the email and do what is needed to do on update fails
*
2019-01-06 21:06:53 +00:00
* @ param int $update_id number of failed update
* @ param string $error_message error message
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-29 09:16:07 +00:00
*/
private static function updateFailed ( $update_id , $error_message ) {
//send the administrators an e-mail
2019-03-02 12:57:47 +00:00
$condition = [ 'email' => explode ( " , " , str_replace ( " " , " " , Config :: get ( 'config' , 'admin_email' ))), 'parent-uid' => 0 ];
$adminlist = DBA :: select ( 'user' , [ 'uid' , 'language' , 'email' ], $condition , [ 'order' => [ 'uid' ]]);
2018-10-29 09:16:07 +00:00
// No valid result?
if ( ! DBA :: isResult ( $adminlist )) {
2019-03-11 20:36:41 +00:00
Logger :: warning ( 'Cannot notify administrators .' , [ 'update' => $update_id , 'message' => $error_message ]);
2018-10-29 09:16:07 +00:00
// Don't continue
return ;
}
2019-03-02 12:57:47 +00:00
$sent = [];
2018-10-29 09:16:07 +00:00
// every admin could had different language
2019-03-02 13:07:29 +00:00
while ( $admin = DBA :: fetch ( $adminlist )) {
2019-03-02 12:57:47 +00:00
if ( in_array ( $admin [ 'email' ], $sent )) {
continue ;
}
$sent [] = $admin [ 'email' ];
2018-10-29 09:16:07 +00:00
$lang = (( $admin [ 'language' ]) ? $admin [ 'language' ] : 'en' );
L10n :: pushLang ( $lang );
2018-11-08 15:26:49 +00:00
$preamble = Strings :: deindent ( L10n :: t ( "
2018-10-29 09:16:07 +00:00
The friendica developers released update % s recently ,
but when I tried to install it , something went terribly wrong .
This needs to be fixed soon and I can ' t do it alone . Please contact a
friendica developer if you can not help me on your own . My database might be invalid . " ,
$update_id ));
$body = L10n :: t ( " The error message is \n [pre]%s[/pre] " , $error_message );
notification ([
'uid' => $admin [ 'uid' ],
'type' => SYSTEM_EMAIL ,
'to_email' => $admin [ 'email' ],
'preamble' => $preamble ,
'body' => $body ,
'language' => $lang ]
);
L10n :: popLang ();
}
//try the logger
2019-03-11 20:36:41 +00:00
Logger :: alert ( 'Database structure update FAILED.' , [ 'error' => $error_message ]);
2018-10-29 09:16:07 +00:00
}
private static function updateSuccessfull ( $from_build , $to_build )
{
//send the administrators an e-mail
2019-03-02 12:57:47 +00:00
$condition = [ 'email' => explode ( " , " , str_replace ( " " , " " , Config :: get ( 'config' , 'admin_email' ))), 'parent-uid' => 0 ];
$adminlist = DBA :: select ( 'user' , [ 'uid' , 'language' , 'email' ], $condition , [ 'order' => [ 'uid' ]]);
2018-10-29 09:16:07 +00:00
if ( DBA :: isResult ( $adminlist )) {
2019-03-02 12:57:47 +00:00
$sent = [];
2018-10-29 09:16:07 +00:00
// every admin could had different language
2019-03-02 13:07:29 +00:00
while ( $admin = DBA :: fetch ( $adminlist )) {
2019-03-02 12:57:47 +00:00
if ( in_array ( $admin [ 'email' ], $sent )) {
continue ;
}
$sent [] = $admin [ 'email' ];
2018-10-29 09:16:07 +00:00
$lang = (( $admin [ 'language' ]) ? $admin [ 'language' ] : 'en' );
L10n :: pushLang ( $lang );
2018-11-08 15:26:49 +00:00
$preamble = Strings :: deindent ( L10n :: t ( "
2018-10-29 09:21:10 +00:00
The friendica database was successfully updated from % s to % s . " ,
2018-10-29 09:16:07 +00:00
$from_build , $to_build ));
notification ([
'uid' => $admin [ 'uid' ],
'type' => SYSTEM_EMAIL ,
'to_email' => $admin [ 'email' ],
'preamble' => $preamble ,
'body' => $preamble ,
'language' => $lang ]
);
L10n :: popLang ();
}
}
//try the logger
2019-03-11 20:36:41 +00:00
Logger :: debug ( 'Database structure update successful.' );
2018-10-29 09:16:07 +00:00
}
2018-10-06 18:38:35 +00:00
}