Browse Source

Add a page to update all thumbnails through AJAX requests in both templates

ArthurHoaro 1 year ago
parent
commit
28f2652460

+ 23 - 5
application/PageBuilder.php

@@ -22,10 +22,20 @@ class PageBuilder
     protected $conf;
 
     /**
+     * @var array $_SESSION
+     */
+    protected $session;
+
+    /**
      * @var LinkDB $linkDB instance.
      */
     protected $linkDB;
-    
+
+    /**
+     * @var null|string XSRF token
+     */
+    protected $token;
+
     /** @var bool $isLoggedIn Whether the user is logged in **/
     protected $isLoggedIn = false;
 
@@ -33,14 +43,17 @@ class PageBuilder
      * PageBuilder constructor.
      * $tpl is initialized at false for lazy loading.
      *
-     * @param ConfigManager $conf   Configuration Manager instance (reference).
-     * @param LinkDB        $linkDB instance.
-     * @param string        $token  Session token
+     * @param ConfigManager $conf       Configuration Manager instance (reference).
+     * @param array         $session    $_SESSION array
+     * @param LinkDB        $linkDB     instance.
+     * @param string        $token      Session token
+     * @param bool          $isLoggedIn
      */
-    public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false)
+    public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
     {
         $this->tpl = false;
         $this->conf = $conf;
+        $this->session = $session;
         $this->linkDB = $linkDB;
         $this->token = $token;
         $this->isLoggedIn = $isLoggedIn;
@@ -110,6 +123,11 @@ class PageBuilder
         $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
         $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
 
+        if (! empty($_SESSION['warnings'])) {
+            $this->tpl->assign('global_warnings', $_SESSION['warnings']);
+            unset($_SESSION['warnings']);
+        }
+
         // To be removed with a proper theme configuration.
         $this->tpl->assign('conf', $this->conf);
     }

+ 12 - 0
application/Router.php

@@ -7,6 +7,8 @@
  */
 class Router
 {
+    public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
+
     public static $PAGE_LOGIN = 'login';
 
     public static $PAGE_PICWALL = 'picwall';
@@ -47,6 +49,8 @@ class Router
 
     public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
 
+    public static $PAGE_THUMBS_UPDATE = 'thumbs_update';
+
     public static $GET_TOKEN = 'token';
 
     /**
@@ -101,6 +105,14 @@ class Router
             return self::$PAGE_FEED_RSS;
         }
 
+        if (startsWith($query, 'do='. self::$PAGE_THUMBS_UPDATE)) {
+            return self::$PAGE_THUMBS_UPDATE;
+        }
+
+        if (startsWith($query, 'do='. self::$AJAX_THUMB_UPDATE)) {
+            return self::$AJAX_THUMB_UPDATE;
+        }
+
         // At this point, only loggedin pages.
         if (!$loggedIn) {
             return self::$PAGE_LINKLIST;

+ 23 - 2
application/Updater.php

@@ -31,6 +31,11 @@ class Updater
     protected $isLoggedIn;
 
     /**
+     * @var array $_SESSION
+     */
+    protected $session;
+
+    /**
      * @var ReflectionMethod[] List of current class methods.
      */
     protected $methods;
@@ -42,13 +47,17 @@ class Updater
      * @param LinkDB        $linkDB      LinkDB instance.
      * @param ConfigManager $conf        Configuration Manager instance.
      * @param boolean       $isLoggedIn  True if the user is logged in.
+     * @param array         $session     $_SESSION (by reference)
+     *
+     * @throws ReflectionException
      */
-    public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
+    public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
     {
         $this->doneUpdates = $doneUpdates;
         $this->linkDB = $linkDB;
         $this->conf = $conf;
         $this->isLoggedIn = $isLoggedIn;
+        $this->session = &$session;
 
         // Retrieve all update methods.
         $class = new ReflectionClass($this);
@@ -488,11 +497,23 @@ class Updater
      */
     public function updateMethodWebThumbnailer()
     {
-        $this->conf->set('thumbnails.enabled', $this->conf->get('thumbnail.enable_thumbnails', true));
+        if ($this->conf->exists('thumbnails.enabled')) {
+            return true;
+        }
+
+        $thumbnailsEnabled = $this->conf->get('thumbnail.enable_thumbnails', true);
+        $this->conf->set('thumbnails.enabled', $thumbnailsEnabled);
         $this->conf->set('thumbnails.width', 125);
         $this->conf->set('thumbnails.height', 90);
         $this->conf->remove('thumbnail');
         $this->conf->write(true);
+
+        if ($thumbnailsEnabled) {
+            $this->session['warnings'][] = t(
+                'You have enabled thumbnails. <a href="?do=thumbs_update">Please synchonize them</a>.'
+            );
+        }
+
         return true;
     }
 }

+ 0 - 7
application/config/ConfigManager.php

@@ -367,10 +367,6 @@ class ConfigManager
         $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
         $this->setEmpty('general.default_note_title', 'Note: ');
 
-        $this->setEmpty('thumbnails.enabled', true);
-        $this->setEmpty('thumbnails.width', 120);
-        $this->setEmpty('thumbnails.height', 120);
-
         $this->setEmpty('updates.check_updates', false);
         $this->setEmpty('updates.check_updates_branch', 'stable');
         $this->setEmpty('updates.check_updates_interval', 86400);
@@ -385,9 +381,6 @@ class ConfigManager
         // default state of the 'remember me' checkbox of the login form
         $this->setEmpty('privacy.remember_user_default', true);
 
-        $this->setEmpty('thumbnail.enable_thumbnails', true);
-        $this->setEmpty('thumbnail.enable_localcache', true);
-
         $this->setEmpty('redirector.url', '');
         $this->setEmpty('redirector.encode_url', true);
 

+ 51 - 0
assets/common/js/thumbnails-update.js

@@ -0,0 +1,51 @@
+/**
+ * Script used in the thumbnails update page.
+ *
+ * It retrieves the list of link IDs to update, and execute AJAX requests
+ * to update their thumbnails, while updating the progress bar.
+ */
+
+/**
+ * Update the thumbnail of the link with the current i index in ids.
+ * It contains a recursive call to retrieve the thumb of the next link when it succeed.
+ * It also update the progress bar and other visual feedback elements.
+ *
+ * @param {array}  ids      List of LinkID to update
+ * @param {int}    i        Current index in ids
+ * @param {object} elements List of DOM element to avoid retrieving them at each iteration
+ */
+function updateThumb(ids, i, elements) {
+  const xhr = new XMLHttpRequest();
+  xhr.open('POST', '?do=ajax_thumb_update');
+  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+  xhr.responseType = 'json';
+  xhr.onload = () => {
+    if (xhr.status !== 200) {
+      alert(`An error occurred. Return code: ${xhr.status}`);
+    } else {
+      const { response } = xhr;
+      i += 1;
+      elements.progressBar.style.width = `${(i * 100) / ids.length}%`;
+      elements.current.innerHTML = i;
+      elements.title.innerHTML = response.title;
+      if (response.thumbnail !== false) {
+        elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`;
+      }
+      if (i < ids.length) {
+        updateThumb(ids, i, elements);
+      }
+    }
+  };
+  xhr.send(`id=${ids[i]}`);
+}
+
+(() => {
+  const ids = document.getElementsByName('ids')[0].value.split(',');
+  const elements = {
+    progressBar: document.querySelector('.progressbar > div'),
+    current: document.querySelector('.progress-current'),
+    thumbnail: document.querySelector('.thumbnail-placeholder'),
+    title: document.querySelector('.thumbnail-link-title'),
+  };
+  updateThumb(ids, 0, elements);
+})();

+ 44 - 0
assets/default/scss/shaarli.scss

@@ -146,6 +146,13 @@ body,
   background-color: $main-green;
 }
 
+.pure-alert-warning {
+  a {
+    color: $warning-text;
+    font-weight: bold;
+  }
+}
+
 .page-single-alert {
   margin-top: 100px;
 }
@@ -1547,3 +1554,40 @@ form {
 .pure-button-shaarli {
   background-color: $main-green;
 }
+
+.progressbar {
+  border-radius: 6px;
+  background-color: $main-green;
+  padding: 1px;
+
+  > div {
+    border-radius: 10px;
+    background: repeating-linear-gradient(
+      -45deg,
+      $almost-white,
+      $almost-white 6px,
+      $background-color 6px,
+      $background-color 12px
+    );
+    width: 0%;
+    height: 10px;
+  }
+}
+
+.thumbnails-page-container {
+  .progress-counter {
+    padding: 10px 0 20px;
+  }
+
+  .thumbnail-placeholder {
+    margin: 10px auto;
+    background-color: $light-grey;
+  }
+
+  .thumbnail-link-title {
+    padding-bottom: 20px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}

+ 40 - 0
assets/vintage/css/shaarli.css

@@ -1210,3 +1210,43 @@ ul.errors {
     width: 13px;
     height: 13px;
 }
+
+.thumbnails-update-container {
+    padding: 20px 0;
+    width: 50%;
+    margin: auto;
+}
+
+.thumbnails-update-container .thumbnail-placeholder {
+    background: grey;
+    margin: auto;
+}
+
+.thumbnails-update-container .thumbnail-link-title {
+    width: 75%;
+    margin: auto;
+
+    padding-bottom: 20px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.progressbar {
+    border-radius: 6px;
+    background-color: #111;
+    padding: 1px;
+}
+
+.progressbar > div {
+    border-radius: 10px;
+    background: repeating-linear-gradient(
+        -45deg,
+        #f5f5f5,
+        #f5f5f5 6px,
+        #d0d0d0 6px,
+        #d0d0d0 12px
+    );
+    width: 0%;
+    height: 10px;
+}

+ 100 - 42
composer.lock

@@ -551,12 +551,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/pubsubhubbub/php-publisher.git",
-                "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f"
+                "reference": "5008fc529b057251b48f4d17a10fdb20047ea8f5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f",
-                "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f",
+                "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/5008fc529b057251b48f4d17a10fdb20047ea8f5",
+                "reference": "5008fc529b057251b48f4d17a10fdb20047ea8f5",
                 "shasum": ""
             },
             "require": {
@@ -586,7 +586,7 @@
                 "publishers",
                 "pubsubhubbub"
             ],
-            "time": "2017-10-08T10:59:41+00:00"
+            "time": "2018-05-22T11:56:26+00:00"
         },
         {
             "name": "shaarli/netscape-bookmark-parser",
@@ -2254,21 +2254,22 @@
         },
         {
             "name": "symfony/config",
-            "version": "v3.4.9",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "7c2a9d44f4433863e9bca682e7f03609234657f9"
+                "reference": "73e055cf2e6467715f187724a0347ea32079967c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/7c2a9d44f4433863e9bca682e7f03609234657f9",
-                "reference": "7c2a9d44f4433863e9bca682e7f03609234657f9",
+                "url": "https://api.github.com/repos/symfony/config/zipball/73e055cf2e6467715f187724a0347ea32079967c",
+                "reference": "73e055cf2e6467715f187724a0347ea32079967c",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.5.9|>=7.0.8",
-                "symfony/filesystem": "~2.8|~3.0|~4.0"
+                "symfony/filesystem": "~2.8|~3.0|~4.0",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "conflict": {
                 "symfony/dependency-injection": "<3.3",
@@ -2313,20 +2314,20 @@
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2018-03-19T22:32:39+00:00"
+            "time": "2018-05-14T16:49:53+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.4.9",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "5b1fdfa8eb93464bcc36c34da39cedffef822cdf"
+                "reference": "36f83f642443c46f3cf751d4d2ee5d047d757a27"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/5b1fdfa8eb93464bcc36c34da39cedffef822cdf",
-                "reference": "5b1fdfa8eb93464bcc36c34da39cedffef822cdf",
+                "url": "https://api.github.com/repos/symfony/console/zipball/36f83f642443c46f3cf751d4d2ee5d047d757a27",
+                "reference": "36f83f642443c46f3cf751d4d2ee5d047d757a27",
                 "shasum": ""
             },
             "require": {
@@ -2382,20 +2383,20 @@
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2018-04-30T01:22:56+00:00"
+            "time": "2018-05-16T08:49:21+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.9",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "1b95888cfd996484527cb41e8952d9a5eaf7454f"
+                "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/1b95888cfd996484527cb41e8952d9a5eaf7454f",
-                "reference": "1b95888cfd996484527cb41e8952d9a5eaf7454f",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/b28fd73fefbac341f673f5efd707d539d6a19f68",
+                "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68",
                 "shasum": ""
             },
             "require": {
@@ -2438,20 +2439,20 @@
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2018-04-30T16:53:52+00:00"
+            "time": "2018-05-16T14:03:39+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.4.9",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "54ff9d78b56429f9a1ac12e60bfb6d169c0468e3"
+                "reference": "8a4672aca8db6d807905d695799ea7d83c8e5bba"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54ff9d78b56429f9a1ac12e60bfb6d169c0468e3",
-                "reference": "54ff9d78b56429f9a1ac12e60bfb6d169c0468e3",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8a4672aca8db6d807905d695799ea7d83c8e5bba",
+                "reference": "8a4672aca8db6d807905d695799ea7d83c8e5bba",
                 "shasum": ""
             },
             "require": {
@@ -2509,24 +2510,25 @@
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2018-04-29T14:04:08+00:00"
+            "time": "2018-05-25T11:57:15+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.4.9",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541"
+                "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541",
-                "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
+                "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^5.5.9|>=7.0.8",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "type": "library",
             "extra": {
@@ -2558,20 +2560,20 @@
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-22T10:48:49+00:00"
+            "time": "2018-05-16T08:49:21+00:00"
         },
         {
             "name": "symfony/finder",
-            "version": "v3.4.9",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "bd14efe8b1fabc4de82bf50dce62f05f9a102433"
+                "reference": "472a92f3df8b247b49ae364275fb32943b9656c6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/bd14efe8b1fabc4de82bf50dce62f05f9a102433",
-                "reference": "bd14efe8b1fabc4de82bf50dce62f05f9a102433",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/472a92f3df8b247b49ae364275fb32943b9656c6",
+                "reference": "472a92f3df8b247b49ae364275fb32943b9656c6",
                 "shasum": ""
             },
             "require": {
@@ -2607,7 +2609,62 @@
             ],
             "description": "Symfony Finder Component",
             "homepage": "https://symfony.com",
-            "time": "2018-04-04T05:07:11+00:00"
+            "time": "2018-05-16T08:49:21+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+                "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.8-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                },
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "time": "2018-04-30T19:57:29+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
@@ -2670,20 +2727,21 @@
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.4.9",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "033cfa61ef06ee0847e056e530201842b6e926c3"
+                "reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/033cfa61ef06ee0847e056e530201842b6e926c3",
-                "reference": "033cfa61ef06ee0847e056e530201842b6e926c3",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/c5010cc1692ce1fa328b1fb666961eb3d4a85bb0",
+                "reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^5.5.9|>=7.0.8",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "conflict": {
                 "symfony/console": "<3.4"
@@ -2724,7 +2782,7 @@
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2018-04-08T08:21:29+00:00"
+            "time": "2018-05-03T23:18:14+00:00"
         },
         {
             "name": "theseer/fdomdocument",

+ 51 - 30
index.php

@@ -514,7 +514,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         read_updates_file($conf->get('resource.updates')),
         $LINKSDB,
         $conf,
-        $loginManager->isLoggedIn()
+        $loginManager->isLoggedIn(),
+        $_SESSION
     );
     try {
         $newUpdates = $updater->update();
@@ -529,7 +530,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         die($e->getMessage());
     }
 
-    $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
+    $PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
     $PAGE->assign('linkcount', count($LINKSDB));
     $PAGE->assign('privateLinkcount', count_private($LINKSDB));
     $PAGE->assign('plugin_errors', $pluginManager->getErrors());
@@ -611,38 +612,13 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         $links = $LINKSDB->filterSearch($_GET);
         $linksToDisplay = array();
 
-        $thumbnailer = new Thumbnailer($conf);
-
-
-        $newThumbnailsCpt = 0;
         // Get only links which have a thumbnail.
+        // Note: we do not retrieve thumbnails here, the request is too heavy.
         foreach($links as $key => $link)
         {
-            // Not a note,
-            // and (never retrieved yet or no valid cache file)
-            if ($link['url'][0] != '?'
-                && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail'])))
-            ) {
-                $item = $LINKSDB[$key];
-                $item['thumbnail'] = $thumbnailer->get($link['url']);
-                $LINKSDB[$key] = $item;
-                $newThumbnailsCpt++;
-            }
-
             if (isset($link['thumbnail']) && $link['thumbnail'] !== false) {
                 $linksToDisplay[] = $link; // Add to array.
             }
-
-            // If we retrieved new thumbnails, we update the database every 20 links.
-            // Downloading everything the first time may take a very long time
-            if ($newThumbnailsCpt == 20) {
-                $LINKSDB->save($conf->get('resource.page_cache'));
-                $newThumbnailsCpt = 0;
-            }
-        }
-
-        if ($newThumbnailsCpt > 0) {
-            $LINKSDB->save($conf->get('resource.page_cache'));
         }
 
         $data = array(
@@ -1036,7 +1012,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             $conf->set('api.enabled', !empty($_POST['enableApi']));
             $conf->set('api.secret', escape($_POST['apiSecret']));
             $conf->set('translation.language', escape($_POST['language']));
-            $conf->set('thumbnails.enabled', extension_loaded('gd') && !empty($_POST['enableThumbnails']));
+
+            $thumbnailsEnabled = extension_loaded('gd') && !empty($_POST['enableThumbnails']);
+            $conf->set('thumbnails.enabled', $thumbnailsEnabled);
+
+            if (! $conf->get('thumbnails.enabled') && $thumbnailsEnabled) {
+                $_SESSION['warnings'][] = t(
+                    'You have enabled thumbnails. <a href="?do=thumbs_update">Please synchonize them</a>.'
+                );
+            }
 
             try {
                 $conf->write($loginManager->isLoggedIn());
@@ -1521,6 +1505,43 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         exit;
     }
 
+    // -------- Thumbnails Update
+    if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
+        $ids = [];
+        foreach ($LINKSDB as $link) {
+            // A note or not HTTP(S)
+            if ($link['url'][0] === '?' || ! startsWith(strtolower($link['url']), 'http')) {
+                continue;
+            }
+            $ids[] = $link['id'];
+        }
+        $PAGE->assign('ids', $ids);
+        $PAGE->assign('pagetitle', t('Thumbnail update') .' - '. $conf->get('general.title', 'Shaarli'));
+        $PAGE->renderPage('thumbnails');
+        exit;
+    }
+
+    // -------- Single Thumbnail Update
+    if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
+        if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
+            http_response_code(400);
+            exit;
+        }
+        $id = (int) $_POST['id'];
+        if (empty($LINKSDB[$id])) {
+            http_response_code(404);
+            exit;
+        }
+        $thumbnailer = new Thumbnailer($conf);
+        $link = $LINKSDB[$id];
+        $link['thumbnail'] = $thumbnailer->get($link['url']);
+        $LINKSDB[$id] = $link;
+        $LINKSDB->save($conf->get('resource.page_cache'));
+
+        echo json_encode($link);
+        exit;
+    }
+
     // -------- Otherwise, simply display search form and links:
     showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
     exit;
@@ -1777,7 +1798,7 @@ function install($conf, $sessionManager, $loginManager) {
         exit;
     }
 
-    $PAGE = new PageBuilder($conf, null, $sessionManager->generateToken());
+    $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
     list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
     $PAGE->assign('continents', $continents);
     $PAGE->assign('cities', $cities);

+ 36 - 4
tests/Updater/UpdaterTest.php

@@ -20,7 +20,7 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
     /**
      * @var string Config file path (without extension).
      */
-    protected static $configFile = 'tests/utils/config/configJson';
+    protected static $configFile = 'sandbox/config';
 
     /**
      * @var ConfigManager
@@ -32,6 +32,7 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
      */
     public function setUp()
     {
+        copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
         $this->conf = new ConfigManager(self::$configFile);
     }
 
@@ -686,17 +687,48 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
     }
 
     /**
-     * Test updateMethodAtomDefault with show_atom set to true.
-     * => nothing to do
+     * Test updateMethodWebThumbnailer with thumbnails enabled.
      */
     public function testUpdateMethodWebThumbnailerEnabled()
     {
+        $this->conf->remove('thumbnails');
         $this->conf->set('thumbnail.enable_thumbnails', true);
-        $updater = new Updater([], [], $this->conf, true);
+        $updater = new Updater([], [], $this->conf, true, $_SESSION);
         $this->assertTrue($updater->updateMethodWebThumbnailer());
         $this->assertFalse($this->conf->exists('thumbnail'));
         $this->assertTrue($this->conf->get('thumbnails.enabled'));
         $this->assertEquals(125, $this->conf->get('thumbnails.width'));
         $this->assertEquals(90, $this->conf->get('thumbnails.height'));
+        $this->assertContains('You have enabled thumbnails', $_SESSION['warnings'][0]);
+    }
+
+    /**
+     * Test updateMethodWebThumbnailer with thumbnails disabled.
+     */
+    public function testUpdateMethodWebThumbnailerDisabled()
+    {
+        $this->conf->remove('thumbnails');
+        $this->conf->set('thumbnail.enable_thumbnails', false);
+        $updater = new Updater([], [], $this->conf, true, $_SESSION);
+        $this->assertTrue($updater->updateMethodWebThumbnailer());
+        $this->assertFalse($this->conf->exists('thumbnail'));
+        $this->assertFalse($this->conf->get('thumbnails.enabled'));
+        $this->assertEquals(125, $this->conf->get('thumbnails.width'));
+        $this->assertEquals(90, $this->conf->get('thumbnails.height'));
+        $this->assertTrue(empty($_SESSION['warnings']));
+    }
+
+    /**
+     * Test updateMethodWebThumbnailer with thumbnails disabled.
+     */
+    public function testUpdateMethodWebThumbnailerNothingToDo()
+    {
+        $updater = new Updater([], [], $this->conf, true, $_SESSION);
+        $this->assertTrue($updater->updateMethodWebThumbnailer());
+        $this->assertFalse($this->conf->exists('thumbnail'));
+        $this->assertTrue($this->conf->get('thumbnails.enabled'));
+        $this->assertEquals(90, $this->conf->get('thumbnails.width'));
+        $this->assertEquals(53, $this->conf->get('thumbnails.height'));
+        $this->assertTrue(empty($_SESSION['warnings']));
     }
 }

+ 6 - 6
tests/utils/config/configJson.json.php

@@ -61,11 +61,6 @@
     "dev": {
         "debug": true
     },
-    "thumbnails": {
-        "enabled": true,
-        "width": 125,
-        "height": 90
-    },
     "updates": {
         "check_updates": false,
         "check_updates_branch": "stable",
@@ -79,6 +74,11 @@
         "language": "auto",
         "mode": "php",
         "extensions": []
+    },
+    "thumbnails": {
+        "enabled": true,
+        "width": 90,
+        "height": 53
     }
 }
-*/ ?>
+*/ ?>

+ 3 - 5
tpl/default/configure.html

@@ -248,12 +248,10 @@
             <label for="enableThumbnails">
               <span class="label-name">{'Enable thumbnails'|t}</span><br>
               <span class="label-desc">
-                {'Warning: '|t}
-                {if="$gd_enabled"}
-                  {'It\'s recommended to visit the picture wall after enabling this feature.'|t}
-                  {'If you have a large database, the first retrieval may take a few minutes.'|t}
-                {else}
+                {if="! $gd_enabled"}
                   {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
+                {elseif="$thumbnails_enabled"}
+                  <a href="?do=thumbs_update">{'Synchonize thumbnails'|t}</a>
                 {/if}
               </span>
             </label>

+ 14 - 0
tpl/default/page.header.html

@@ -171,4 +171,18 @@
   </div>
 {/if}
 
+{if="!empty($global_warnings) && $is_logged_in"}
+  <div class="pure-g pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
+    <div class="pure-u-2-24"></div>
+    <div class="pure-u-20-24">
+      {loop="global_warnings"}
+        <p>{$value}</p>
+      {/loop}
+    </div>
+    <div class="pure-u-2-24">
+      <i class="fa fa-times pure-alert-close"></i>
+    </div>
+  </div>
+{/if}
+
   <div class="clear"></div>

+ 48 - 0
tpl/default/thumbnails.html

@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+  {include="includes"}
+</head>
+<body>
+{include="page.header"}
+
+<div class="pure-g thumbnails-page-container">
+  <div class="pure-u-lg-1-3 pure-u-1-24"></div>
+  <div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light">
+    <h2 class="window-title">{'Thumbnails update'|t}</h2>
+
+    <div class="pure-g">
+      <div class="pure-u-lg-1-3 pure-u-1-24"></div>
+      <div class="pure-u-lg-1-3 pure-u-22-24">
+        <div class="thumbnail-placeholder" style="width: {$thumbnails_width}px; height: {$thumbnails_height}px;"></div>
+      </div>
+    </div>
+
+    <div class="pure-g">
+      <div class="pure-u-1-12"></div>
+      <div class="pure-u-5-6">
+        <div class="thumbnail-link-title"></div>
+
+        <div class="progressbar">
+          <div></div>
+        </div>
+      </div>
+    </div>
+
+    <div class="pure-g">
+      <div class="pure-u-lg-1-3 pure-u-1-24"></div>
+      <div class="pure-u-lg-1-3 pure-u-22-24">
+        <div class="progress-counter">
+          <span class="progress-current">0</span> / <span class="progress-total">{$ids|count}</span>
+        </div>
+      </div>
+    </div>
+
+    <input type="hidden" name="ids" value="{function="implode($ids, ',')"}" />
+  </div>
+</div>
+
+{include="page.footer"}
+<script src="js/thumbnails_update.min.js?v={$version_hash}"></script>
+</body>
+</html>

+ 6 - 4
tpl/vintage/configure.html

@@ -132,11 +132,13 @@
         <td valign="top"><b>Enable thumbnails</b></td>
         <td>
           <input type="checkbox" name="enableThumbnails" id="enableThumbnails"
-                 {if="$thumbnails_enabled"}checked{/if}/>
+                 {if="$thumbnails_enabled"}checked{/if} {if="!$gd_enabled"}disabled{/if}>
           <label for="enableThumbnails">
-            &nbsp;<strong>Warning:</strong>
-            If you have a large database, the first retrieval may take a few minutes.
-            It's recommended to visit the picture wall after enabling this feature
+            {if="! $gd_enabled"}
+              {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
+            {elseif="$thumbnails_enabled"}
+              <a href="?do=thumbs_update">{'Synchonize thumbnails'|t}</a>
+            {/if}
           </label>
         </td>
       </tr>

+ 28 - 0
tpl/vintage/thumbnails.html

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>{include="includes"}</head>
+<body>
+<div id="pageheader">
+{include="page.header"}
+</div>
+
+<div class="center thumbnails-update-container">
+  <div class="thumbnail-placeholder" style="width: {$thumbnails_width}px; height: {$thumbnails_height}px;"></div>
+
+  <div class="thumbnail-link-title"></div>
+
+  <div class="progressbar">
+    <div></div>
+  </div>
+
+  <div class="progress-counter">
+    <span class="progress-current">0</span> / <span class="progress-total">{$ids|count}</span>
+  </div>
+</div>
+
+<input type="hidden" name="ids" value="{function="implode($ids, ',')"}" />
+
+{include="page.footer"}
+<script src="js/thumbnails_update.min.js?v={$version_hash}"></script>
+</body>
+</html>

+ 2 - 0
webpack.config.js

@@ -24,6 +24,7 @@ module.exports = [
   {
     entry: {
       thumbnails: './assets/common/js/thumbnails.js',
+      thumbnails_update: './assets/common/js/thumbnails-update.js',
       pluginsadmin: './assets/default/js/plugins-admin.js',
       shaarli: [
         './assets/default/js/base.js',
@@ -97,6 +98,7 @@ module.exports = [
         './assets/vintage/css/shaarli.css',
       ].concat(glob.sync('./assets/vintage/img/*')),
       thumbnails: './assets/common/js/thumbnails.js',
+      thumbnails_update: './assets/common/js/thumbnails-update.js',
     },
     output: {
       filename: '[name].min.js',