2018-10-05 22:53:13 +00:00
< ? php
2020-02-09 14:45:36 +00:00
/**
* @ copyright Copyright ( C ) 2020 , Friendica
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
*/
2018-10-05 22:53:13 +00:00
namespace Friendica\Core ;
2019-03-24 21:51:30 +00:00
use Friendica\App ;
2021-01-17 20:32:13 +00:00
use Friendica\App\Mode ;
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 ;
2021-01-17 20:32:13 +00:00
use Friendica\Network\HTTPException\InternalServerErrorException ;
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
/**
2020-01-19 06:05:23 +00:00
* Function to check if the Database structure needs an update .
2018-10-14 11:26:53 +00:00
*
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
*/
2020-11-21 14:58:48 +00:00
public static function check ( string $basePath , bool $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
2020-01-19 20:21:13 +00:00
if ( DI :: config () -> get ( 'system' , 'update' , Update :: SUCCESS , true ) == Update :: FAILED ) {
2019-03-24 21:51:30 +00:00
return ;
}
2020-01-19 20:21:13 +00:00
$build = DI :: config () -> get ( 'system' , 'build' );
2019-03-09 23:28:48 +00:00
if ( empty ( $build )) {
2020-01-19 20:21:53 +00:00
DI :: config () -> set ( 'system' , 'build' , DB_UPDATE_VERSION - 1 );
2019-03-09 23:28:48 +00:00
$build = DB_UPDATE_VERSION - 1 ;
}
2018-10-14 11:26:53 +00:00
// We don't support upgrading from very old versions anymore
2021-01-17 20:32:13 +00:00
if ( $build < NEW_TABLE_STRUCTURE_VERSION ) {
2021-01-21 15:36:52 +00:00
$error = DI :: l10n () -> t ( 'Updates from version %s are not supported. Please update at least to version 2021.01 and wait until the postupdate finished version 1383.' , $build );
2021-01-17 20:32:13 +00:00
if ( DI :: mode () -> getExecutor () == Mode :: INDEX ) {
die ( $error );
} else {
throw new InternalServerErrorException ( $error );
}
}
// The postupdate has to completed version 1281 for the new post views to take over
$postupdate = DI :: config () -> get ( " system " , " post_update_version " );
if ( $postupdate < NEW_TABLE_STRUCTURE_VERSION ) {
2021-01-21 15:36:52 +00:00
$error = DI :: l10n () -> t ( 'Updates from postupdate version %s are not supported. Please update at least to version 2021.01 and wait until the postupdate finished version 1383.' , $postupdate );
2021-01-17 20:32:13 +00:00
if ( DI :: mode () -> getExecutor () == Mode :: INDEX ) {
die ( $error );
} else {
throw new InternalServerErrorException ( $error );
}
2018-10-14 11:26:53 +00:00
}
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
2020-11-21 14:58:48 +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
* @ 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
*/
2020-11-21 14:58:48 +00:00
public static function run ( string $basePath , bool $force = false , bool $override = false , bool $verbose = false , bool $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
}
2020-01-19 20:21:13 +00:00
$build = DI :: 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 ;
2020-01-19 20:21:53 +00:00
DI :: config () -> set ( 'system' , 'build' , $build );
2019-03-09 23:28:48 +00:00
}
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 ) {
2020-01-19 20:20:10 +00:00
DI :: config () -> load ( 'database' );
2018-10-05 22:53:13 +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-11-21 13:17:14 +00:00
if ( DI :: lock () -> acquire ( 'dbupdate' , 0 , Cache\Duration :: INFINITE )) {
2018-10-06 18:38:35 +00:00
2020-11-21 12:28:06 +00:00
Logger :: notice ( 'Update starting.' , [ 'from' => $stored , 'to' => $current ]);
2019-03-10 18:59:20 +00:00
// Checks if the build changed during Lock acquiring (so no double update occurs)
2020-01-19 20:21:13 +00:00
$retryBuild = DI :: config () -> get ( 'system' , 'build' , null , true );
2019-03-10 18:59:20 +00:00
if ( $retryBuild !== $build ) {
2020-08-30 18:57:09 +00:00
Logger :: notice ( '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
2020-11-21 14:58:48 +00:00
for ( $version = $stored + 1 ; $version <= $current ; $version ++ ) {
Logger :: notice ( 'Execute pre update.' , [ 'version' => $version ]);
$r = self :: runUpdateFunction ( $version , 'pre_update' , $sendMail );
2019-03-09 23:28:48 +00:00
if ( ! $r ) {
2020-11-21 14:58:48 +00:00
Logger :: warning ( 'Pre update failed' , [ 'version' => $version ]);
2020-01-19 20:21:53 +00:00
DI :: 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 ;
2020-11-21 12:28:06 +00:00
} else {
2020-11-21 14:58:48 +00:00
Logger :: notice ( 'Pre update executed.' , [ 'version' => $version ]);
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
2020-11-21 12:28:06 +00:00
Logger :: notice ( 'Execute structure update' );
2019-03-09 23:28:48 +00:00
$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 ]);
2020-01-19 20:21:53 +00:00
DI :: 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 {
2020-11-21 12:28:06 +00:00
Logger :: notice ( 'Database structure 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
2020-11-21 14:58:48 +00:00
for ( $version = $stored + 1 ; $version <= $current ; $version ++ ) {
Logger :: notice ( 'Execute post update.' , [ 'version' => $version ]);
$r = self :: runUpdateFunction ( $version , 'update' , $sendMail );
2019-03-09 23:28:48 +00:00
if ( ! $r ) {
2020-11-21 14:58:48 +00:00
Logger :: warning ( 'Post update failed' , [ 'version' => $version ]);
2020-01-19 20:21:53 +00:00
DI :: 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 ;
2020-11-21 12:28:06 +00:00
} else {
2020-11-21 14:58:48 +00:00
DI :: config () -> set ( 'system' , 'build' , $version );
Logger :: notice ( 'Post update executed.' , [ 'version' => $version ]);
2019-03-09 23:28:48 +00:00
}
2018-10-05 22:53:13 +00:00
}
2018-10-06 18:38:35 +00:00
2020-11-21 12:28:06 +00:00
DI :: config () -> set ( 'system' , 'build' , $current );
DI :: config () -> set ( 'system' , 'update' , Update :: SUCCESS );
DI :: lock () -> release ( 'dbupdate' );
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 ) {
2020-11-21 14:58:48 +00:00
self :: updateSuccessful ( $stored , $current );
2018-10-29 09:16:07 +00:00
}
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
*
2020-11-21 14:58:48 +00:00
* @ param int $version the DB version number of the function
2020-07-13 21:22:21 +00:00
* @ param string $prefix the prefix of the function ( update , pre_update )
* @ param bool $sendMail whether to send emails on success / failure
2018-10-05 22:53:13 +00:00
* @ 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
*/
2020-11-21 14:58:48 +00:00
public static function runUpdateFunction ( int $version , string $prefix , bool $sendMail = true )
2018-10-05 22:53:13 +00:00
{
2020-11-21 14:58:48 +00:00
$funcname = $prefix . '_' . $version ;
2018-10-05 22:53:13 +00:00
2020-11-21 12:28:06 +00:00
Logger :: notice ( '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-18 14:41:19 +00:00
if ( DI :: lock () -> acquire ( 'dbupdate_function' , 120 , Cache\Duration :: INFINITE )) {
2018-10-06 18:38:35 +00:00
// call the specific update
2020-11-21 12:28:06 +00:00
Logger :: notice ( 'Pre update function start.' , [ 'function' => $funcname ]);
2018-10-06 18:38:35 +00:00
$retval = $funcname ();
2020-11-21 12:28:06 +00:00
Logger :: notice ( 'Update function done.' , [ 'function' => $funcname ]);
2018-10-05 22:53:13 +00:00
2018-10-06 18:38:35 +00:00
if ( $retval ) {
2020-07-13 21:22:21 +00:00
if ( $sendMail ) {
//send the administrator an e-mail
self :: updateFailed (
2020-11-21 14:58:48 +00:00
$version ,
DI :: l10n () -> t ( 'Update %s failed. See error logs.' , $version )
2020-07-13 21:22:21 +00:00
);
}
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 {
2020-01-06 23:24:10 +00:00
DI :: lock () -> release ( 'dbupdate_function' );
2020-11-21 12:28:06 +00:00
Logger :: notice ( 'Update function finished.' , [ 'function' => $funcname ]);
2018-10-06 18:38:35 +00:00
return true ;
}
2020-08-30 18:57:09 +00:00
} else {
Logger :: error ( 'Locking failed.' , [ 'function' => $funcname ]);
2020-11-21 12:28:06 +00:00
return false ;
2018-10-05 22:53:13 +00:00
}
} else {
2020-11-21 12:28:06 +00:00
Logger :: notice ( 'Update function skipped.' , [ 'function' => $funcname ]);
2018-10-05 22:53:13 +00:00
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
*/
2020-11-21 14:58:48 +00:00
private static function updateFailed ( int $update_id , string $error_message ) {
2018-10-29 09:16:07 +00:00
//send the administrators an e-mail
2020-11-21 14:58:48 +00:00
$condition = [ 'email' => explode ( ',' , str_replace ( ' ' , '' , DI :: config () -> get ( 'config' , 'admin_email' ))), 'parent-uid' => 0 ];
2019-03-02 12:57:47 +00:00
$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' ];
2020-02-02 07:45:10 +00:00
$lang = $admin [ 'language' ] ? ? 'en' ;
2020-01-18 19:54:15 +00:00
$l10n = DI :: 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 ));
2020-11-21 14:58:48 +00:00
$body = $l10n -> t ( 'The error message is\n[pre]%s[/pre]' , $error_message );
2020-02-01 19:08:54 +00:00
$email = DI :: emailer ()
2020-02-04 20:04:08 +00:00
-> newSystemMail ()
2020-02-02 08:22:30 +00:00
-> withMessage ( $l10n -> t ( '[Friendica Notify] Database update' ), $preamble , $body )
2020-02-04 20:04:08 +00:00
-> forUser ( $admin )
2020-02-02 08:22:30 +00:00
-> withRecipient ( $admin [ 'email' ])
-> build ();
2020-02-01 19:08:54 +00:00
DI :: emailer () -> send ( $email );
2018-10-29 09:16:07 +00:00
}
2020-11-21 14:58:48 +00:00
Logger :: alert ( 'Database structure update failed.' , [ 'error' => $error_message ]);
2018-10-29 09:16:07 +00:00
}
2020-11-21 14:58:48 +00:00
/**
* Send a mail to the administrator about the successful update
*
* @ param integer $from_build
* @ param integer $to_build
* @ return void
*/
private static function updateSuccessful ( int $from_build , int $to_build )
2018-10-29 09:16:07 +00:00
{
//send the administrators an e-mail
2020-11-21 14:58:48 +00:00
$condition = [ 'email' => explode ( ',' , str_replace ( ' ' , '' , DI :: config () -> get ( 'config' , 'admin_email' ))), 'parent-uid' => 0 ];
2019-03-02 12:57:47 +00:00
$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' );
2020-01-18 19:54:15 +00:00
$l10n = DI :: l10n () -> withLang ( $lang );
2018-10-29 09:16:07 +00:00
2020-11-21 14:58:48 +00:00
$preamble = Strings :: deindent ( $l10n -> t ( '
The friendica database was successfully updated from % s to % s . ' ,
2018-10-29 09:16:07 +00:00
$from_build , $to_build ));
2020-02-01 19:08:54 +00:00
$email = DI :: emailer ()
2020-02-04 20:04:08 +00:00
-> newSystemMail ()
2020-02-02 08:22:30 +00:00
-> withMessage ( $l10n -> t ( '[Friendica Notify] Database update' ), $preamble )
2020-02-04 20:04:08 +00:00
-> forUser ( $admin )
2020-02-02 08:22:30 +00:00
-> withRecipient ( $admin [ 'email' ])
-> build ();
2020-02-01 19:08:54 +00:00
DI :: emailer () -> send ( $email );
2018-10-29 09:16:07 +00:00
}
}
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
}