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 @@
= Helper\form_checkbox('nocontent', t('Do not fetch the content of articles'), 1, isset($values['nocontent']) ? $values['nocontent'] : false) ?>
+
+ = Helper\form_label(t('Auto-Update URL'), 'auto_update_url') ?>
+ = Helper\form_text('auto_update_url', $values, $errors, array('required')) ?>
+
+