2018-10-05 22:53:13 +00:00
< ? php
namespace Friendica\Core ;
2019-03-24 21:51:30 +00:00
use Friendica\App ;
2018-10-29 09:16:07 +00:00
use Friendica\Database\DBA ;
2018-10-05 22:53:13 +00:00
use Friendica\Database\DBStructure ;
2020-01-06 23:20:31 +00:00
use Friendica\DI ;
2018-11-08 15:26:49 +00:00
use Friendica\Util\Strings ;
2020-01-06 23:51:02 +00:00
use Friendica\Core\Cache\Cache ;
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 ?
2019-03-30 18:08:47 +00:00
* @ 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-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 ) {
2020-01-06 23:24:10 +00:00
DI :: 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
2020-01-06 23:20:31 +00:00
if ( DI :: 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 ]);
2020-01-06 23:24:10 +00:00
DI :: lock () -> release ( 'dbupdate' );
2019-03-10 18:59:20 +00:00
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 );
2020-01-06 23:24:10 +00:00
DI :: lock () -> release ( 'dbupdate' );
2019-03-24 21:51:30 +00:00
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 );
2020-01-06 23:24:10 +00:00
DI :: lock () -> release ( 'dbupdate' );
2019-03-09 23:28:48 +00:00
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 );
2020-01-06 23:24:10 +00:00
DI :: lock () -> release ( 'dbupdate' );
2019-03-24 21:51:30 +00:00
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 );
2020-01-06 23:24:10 +00:00
DI :: 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.
2020-01-06 23:20:31 +00:00
if ( DI :: 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 ]);
2020-01-06 23:24:10 +00:00
DI :: lock () -> release ( 'dbupdate_function' );
2018-10-06 18:38:35 +00:00
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 );
}
2020-01-06 23:24:10 +00:00
DI :: 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
/**
* 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' );
2019-12-28 22:12:01 +00:00
$l10n = L10n :: withLang ( $lang );
2018-10-29 09:16:07 +00:00
2019-12-28 22:12:01 +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 ));
2019-12-28 22:12:01 +00:00
$body = $l10n -> t ( " The error message is \n [pre]%s[/pre] " , $error_message );
2018-10-29 09:16:07 +00:00
notification ([
'uid' => $admin [ 'uid' ],
'type' => SYSTEM_EMAIL ,
'to_email' => $admin [ 'email' ],
2019-12-28 22:12:01 +00:00
'subject' => $l10n -> t ( '[Friendica Notify] Database update' ),
2018-10-29 09:16:07 +00:00
'preamble' => $preamble ,
'body' => $body ,
'language' => $lang ]
);
}
//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' );
2019-12-28 22:12:01 +00:00
$l10n = L10n :: withLang ( $lang );
2018-10-29 09:16:07 +00:00
2019-12-28 22:12:01 +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' ],
2019-05-06 09:13:38 +00:00
'subject' => l10n :: t ( '[Friendica Notify] Database update' ),
2018-10-29 09:16:07 +00:00
'preamble' => $preamble ,
'body' => $preamble ,
'language' => $lang ]
);
}
}
//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
}