From c1221de62c0f7589aa7af7a69cc281a48da1cdb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sun, 30 Mar 2014 15:59:26 -0400 Subject: [PATCH] Add Miniflux auto-update feature --- common.php | 6 + controllers/config.php | 16 +++ data/archive/.gitignore | 4 + data/backup/.gitignore | 4 + data/download/.gitignore | 4 + locales/fr_FR/translations.php | 5 + models/auto_update.php | 229 +++++++++++++++++++++++++++++++++ models/config.php | 54 ++++---- models/schema.php | 5 + templates/config.php | 8 ++ 10 files changed, 310 insertions(+), 25 deletions(-) create mode 100644 data/archive/.gitignore create mode 100644 data/backup/.gitignore create mode 100644 data/download/.gitignore create mode 100644 models/auto_update.php diff --git a/common.php b/common.php index 5a893d3..2a50658 100644 --- a/common.php +++ b/common.php @@ -10,6 +10,7 @@ require __DIR__.'/models/user.php'; require __DIR__.'/models/feed.php'; require __DIR__.'/models/item.php'; require __DIR__.'/models/schema.php'; +require __DIR__.'/models/auto_update.php'; if (file_exists('config.php')) require 'config.php'; @@ -24,6 +25,11 @@ defined('PROXY_HOSTNAME') or define('PROXY_HOSTNAME', ''); defined('PROXY_PORT') or define('PROXY_PORT', 3128); defined('PROXY_USERNAME') or define('PROXY_USERNAME', ''); defined('PROXY_PASSWORD') or define('PROXY_PASSWORD', ''); +defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__); +defined('ENABLE_AUTO_UPDATE') or define('ENABLE_AUTO_UPDATE', true); +defined('AUTO_UPDATE_DOWNLOAD_DIRECTORY') or define('AUTO_UPDATE_DOWNLOAD_DIRECTORY', 'data/download'); +defined('AUTO_UPDATE_ARCHIVE_DIRECTORY') or define('AUTO_UPDATE_ARCHIVE_DIRECTORY', 'data/archive'); +defined('AUTO_UPDATE_BACKUP_DIRECTORY') or define('AUTO_UPDATE_BACKUP_DIRECTORY', 'data/backup'); PicoFeed\Client::proxy(PROXY_HOSTNAME, PROXY_PORT, PROXY_USERNAME, PROXY_PASSWORD); diff --git a/controllers/config.php b/controllers/config.php index 9a10b59..12b81ff 100644 --- a/controllers/config.php +++ b/controllers/config.php @@ -7,6 +7,22 @@ use PicoFarad\Session; use PicoFarad\Template; use PicoDb\Database; +// Auto-update +Router\get_action('auto-update', function() { + + if (ENABLE_AUTO_UPDATE) { + + if (Model\AutoUpdate\execute(Model\Config\get('auto_update_url'))) { + Session\flash(t('Miniflux is updated!')); + } + else { + Session\flash_error(t('Unable to update Miniflux, check the console for errors.')); + } + } + + Response\redirect('?action=config'); +}); + // Re-generate tokens Router\get_action('generate-tokens', function() { diff --git a/data/archive/.gitignore b/data/archive/.gitignore new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/data/archive/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/data/backup/.gitignore b/data/backup/.gitignore new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/data/backup/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/data/download/.gitignore b/data/download/.gitignore new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/data/download/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/locales/fr_FR/translations.php b/locales/fr_FR/translations.php index d9eeda1..6170055 100644 --- a/locales/fr_FR/translations.php +++ b/locales/fr_FR/translations.php @@ -208,4 +208,9 @@ return array( '%d months ago' => 'Il y a %d mois', 'Timezone' => 'Fuseau horaire', 'Update all subscriptions' => 'Mettre à jour tous les abonnements', + 'Auto-Update URL' => 'URL de mise à jour automatique', + 'Update Miniflux' => 'Mettre à jour Miniflux', + 'Miniflux is updated!' => 'Miniflux a été mis à jour avec succès !', + 'Unable to update Miniflux, check the console for errors.' => 'Impossible de mettre à jour Miniflux, allez-voir les erreurs dans la console.', + 'Don\'t forget to backup your database' => 'N\'oubliez pas de sauvegarder votre base de données', ); diff --git a/models/auto_update.php b/models/auto_update.php new file mode 100644 index 0000000..314a447 --- /dev/null +++ b/models/auto_update.php @@ -0,0 +1,229 @@ +valid()) { + + if ($it->isFile() && ! is_excluded_path($it->getSubPathname(), $exclude_list)) { + $files[] = $it->getSubPathname(); + } + + $it->next(); + } + + return $files; +} + +// Check if the given path is excluded +function is_excluded_path($path, array $exclude_list) +{ + foreach ($exclude_list as $excluded_path) { + + if (strpos($path, $excluded_path) === 0) { + return true; + } + } + + return false; +} + +// Synchronize 2 directories (copy/remove files) +function synchronize($source_directory, $destination_directory) +{ + \Model\Config\debug('[SYNCHRONIZE] '.$source_directory.' to '.$destination_directory); + + $src_files = get_files_list($source_directory); + $dst_files = get_files_list($destination_directory); + + // Remove files + $remove_files = array_diff($dst_files, $src_files); + + foreach ($remove_files as $file) { + + $destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file; + \Model\Config\debug('[REMOVE] '.$destination_file); + + if (! @unlink($destination_file)) { + return false; + } + } + + // Overwrite all files + foreach ($src_files as $file) { + + $directory = $destination_directory.DIRECTORY_SEPARATOR.dirname($file); + + if (! is_dir($directory)) { + + \Model\Config\debug('[MKDIR] '.$directory); + + if (! @mkdir($directory, 0755, true)) { + return false; + } + } + + $source_file = $source_directory.DIRECTORY_SEPARATOR.$file; + $destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file; + + \Model\Config\debug('[COPY] '.$source_file.' to '.$destination_file); + + if (! @copy($source_file, $destination_file)) { + return false; + } + } + + return true; +} + +// Download and unzip the archive +function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIRECTORY, $archive_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY) +{ + $archive_file = $download_directory.DIRECTORY_SEPARATOR.'update.zip'; + + \Model\Config\debug('[DOWNLOAD] '.$url); + + if (($data = @file_get_contents($url)) === false) { + return false; + } + + if (@file_put_contents($archive_file, $data) === false) { + return false; + } + + \Model\Config\debug('[UNZIP] '.$archive_file); + + $zip = new ZipArchive; + + if (! $zip->open($archive_file)) { + return false; + } + + $zip->extractTo($archive_directory); + $zip->close(); + + return true; +} + +// Remove all files for a given directory +function cleanup_directory($directory) +{ + \Model\Config\debug('[CLEANUP] '.$directory); + + $dir = new DirectoryIterator($directory); + + foreach ($dir as $fileinfo) { + + if (! $fileinfo->isDot()) { + + $filename = $fileinfo->getRealPath(); + + if ($fileinfo->isFile()) { + \Model\Config\debug('[REMOVE] '.$filename); + @unlink($filename); + } + else { + cleanup_directory($filename); + @rmdir($filename); + } + } + } +} + +// Cleanup all temporary directories +function cleanup_directories() +{ + cleanup_directory(AUTO_UPDATE_DOWNLOAD_DIRECTORY); + cleanup_directory(AUTO_UPDATE_ARCHIVE_DIRECTORY); + cleanup_directory(AUTO_UPDATE_BACKUP_DIRECTORY); +} + +// Find the archive directory name +function find_archive_root($base_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY) +{ + $directory = ''; + $dir = new DirectoryIterator($base_directory); + + foreach ($dir as $fileinfo) { + if (! $fileinfo->isDot() && $fileinfo->isDir()) { + $directory = $fileinfo->getFilename(); + break; + } + } + + if (empty($directory)) { + \Model\Config\debug('[FIND ARCHIVE] No directory found'); + return false; + } + + $path = $base_directory.DIRECTORY_SEPARATOR.$directory; + \Model\Config\debug('[FIND ARCHIVE] '.$path); + + return $path; +} + +// Check if everything is setup correctly +function check_setup() +{ + if (! class_exists('ZipArchive')) die('To use this feature, your PHP installation must be able to uncompress zip files!'); + + if (AUTO_UPDATE_DOWNLOAD_DIRECTORY === '') die('The constant AUTO_UPDATE_DOWNLOAD_DIRECTORY is not set!'); + if (AUTO_UPDATE_ARCHIVE_DIRECTORY === '') die('The constant AUTO_UPDATE_ARCHIVE_DIRECTORY is not set!'); + if (AUTO_UPDATE_DOWNLOAD_DIRECTORY === '') die('The constant AUTO_UPDATE_DOWNLOAD_DIRECTORY is not set!'); + + if (! is_dir(AUTO_UPDATE_DOWNLOAD_DIRECTORY)) @mkdir(AUTO_UPDATE_DOWNLOAD_DIRECTORY, 0755); + if (! is_dir(AUTO_UPDATE_ARCHIVE_DIRECTORY)) @mkdir(AUTO_UPDATE_ARCHIVE_DIRECTORY, 0755); + if (! is_dir(AUTO_UPDATE_BACKUP_DIRECTORY)) @mkdir(AUTO_UPDATE_BACKUP_DIRECTORY, 0755); + + if (! is_writable(AUTO_UPDATE_DOWNLOAD_DIRECTORY)) die('Update directories must be writable by your web server user!'); + if (! is_writable(__DIR__)) die('Source files must be writable by your web server user!'); +} + +// Update the source code +function execute($url) +{ + check_setup(); + cleanup_directories(); + + if (uncompress_archive($url)) { + + $update_directory = find_archive_root(); + + if ($update_directory) { + + // Backup first + if (synchronize(ROOT_DIRECTORY, AUTO_UPDATE_BACKUP_DIRECTORY)) { + + // Update + if (synchronize($update_directory, ROOT_DIRECTORY)) { + cleanup_directories(); + return true; + } + else { + // If update failed, rollback + synchronize(AUTO_UPDATE_BACKUP_DIRECTORY, ROOT_DIRECTORY); + } + } + } + } + + return false; +} diff --git a/models/config.php b/models/config.php index 6bb37f5..e19eb3c 100644 --- a/models/config.php +++ b/models/config.php @@ -16,10 +16,18 @@ use SimpleValidator\Validator; use SimpleValidator\Validators; use PicoDb\Database; -const DB_VERSION = 23; +const DB_VERSION = 24; const HTTP_USERAGENT = 'Miniflux - http://miniflux.net'; const HTTP_FAKE_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36'; + +// Send a debug message to the console +function debug($line) +{ + \PicoFeed\Logging::log($line); + write_debug(); +} + // Write PicoFeed debug output to a file function write_debug() { @@ -211,7 +219,8 @@ function get_all() 'auth_google_token', 'auth_mozilla_token', 'items_sorting_direction', - 'redirect_nothing_to_read' + 'redirect_nothing_to_read', + 'auto_update_url' ) ->findOne(); } @@ -219,32 +228,27 @@ function get_all() // Validation for edit action function validate_modification(array $values) { + $rules = array( + new Validators\Required('username', t('The user name is required')), + new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), + new Validators\Required('autoflush', t('Value required')), + new Validators\Required('items_per_page', t('Value required')), + new Validators\Integer('items_per_page', t('Must be an integer')), + new Validators\Required('theme', t('Value required')), + ); + + if (ENABLE_AUTO_UPDATE) { + $rules[] = new Validators\Required('auto_update_url', t('Value required')); + } + if (! empty($values['password'])) { - - $v = new Validator($values, array( - new Validators\Required('username', t('The user name is required')), - new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), - new Validators\Required('password', t('The password is required')), - new Validators\MinLength('password', t('The minimum length is 6 characters'), 6), - new Validators\Required('confirmation', t('The confirmation is required')), - new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match')), - new Validators\Required('autoflush', t('Value required')), - new Validators\Required('items_per_page', t('Value required')), - new Validators\Integer('items_per_page', t('Must be an integer')), - new Validators\Required('theme', t('Value required')), - )); + $rules[] = new Validators\Required('password', t('The password is required')); + $rules[] = new Validators\MinLength('password', t('The minimum length is 6 characters'), 6); + $rules[] = new Validators\Required('confirmation', t('The confirmation is required')); + $rules[] = new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match')); } - else { - $v = new Validator($values, array( - new Validators\Required('username', t('The user name is required')), - new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), - new Validators\Required('autoflush', t('Value required')), - new Validators\Required('items_per_page', t('Value required')), - new Validators\Integer('items_per_page', t('Must be an integer')), - new Validators\Required('theme', t('Value required')), - )); - } + $v = new Validator($values, $rules); return array( $v->execute(), diff --git a/models/schema.php b/models/schema.php index aaadc24..5d1caa2 100644 --- a/models/schema.php +++ b/models/schema.php @@ -2,6 +2,11 @@ namespace Schema; +function version_24($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'"); +} + function version_23($pdo) { diff --git a/templates/config.php b/templates/config.php index 8bb810f..0b10d18 100644 --- a/templates/config.php +++ b/templates/config.php @@ -36,6 +36,11 @@
+ + +
+ +