]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge branch 'v0.11' into stable stable
authorArthurHoaro <arthur@hoa.ro>
Tue, 13 Oct 2020 10:07:13 +0000 (12:07 +0200)
committerArthurHoaro <arthur@hoa.ro>
Tue, 13 Oct 2020 10:07:13 +0000 (12:07 +0200)
1  2 
CHANGELOG.md
application/bookmark/LinkDB.php
application/updater/Updater.php
index.php
tests/updater/UpdaterTest.php

diff --combined CHANGELOG.md
index 2b2ee62edcfe3b683255733677eec1c16f8ced6d,abf802ead6c89a2663f843860b124d4226e49135..e4991c4d7ce9c5d3d59229fd4793290ead11dd23
@@@ -4,8 -4,65 +4,65 @@@ All notable changes to this project wil
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
  and this project adheres to [Semantic Versioning](http://semver.org/).
  
- ## [v0.10.4](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4) - 2019-04-16
+ ## [v0.11.1](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) - 2019-08-03
+ Release to fix broken Docker build on the latest version.
+ ### Fixed
+ - Fixed Docker build
+ - Fixed a few documentation broken links
+ - Fixed broken label in configuration page
+ ### Added
+ - More accessibility improvements
+ ## [v0.11.0](https://github.com/shaarli/Shaarli/releases/tag/v0.11.0) - 2019-07-27
+ **Shaarli no longer officially support PHP 5.6 and PHP 7.0 as they've reached end of life.**
+ **Shaarli classes now use namespace, third party plugins need to update.**
  
+ ### Added
+ - Add optional PHP extension to composer suggestions.
+ - composer: enforce PHP security advisories
+ - phpDocumentor configuration and make target
+ - Run unit tests against PHP 7.3
+ - Bunch of accessibility improvements to the default template, thanks to @llune
+ - Bulk actions: set visibility
+ - Display sticky label in linklist
+ - Add print CSS rules to the default template
+ - New setting to automatically retrieve description for new bookmarks
+ - Plugin to override default template colors
+ ### Changed
+ - Shaarli now uses namespaces for its classes.
+ - Rewrite IP ban management
+ - Default template: slightly lighten visited link color
+ - Hide select all button on mobile view
+ - Switch from FontAwesome v4.x to ForkAwesome
+ - Daily - display the current day instead of the previous one
+ ### Fixed
+ - Do not check the IP address with session protection disabled
+ - API: update test regexes to comply with PCRE2
+ - Optimize and cleanup imports
+ - ensure HTML tags are stripped from OpenGraph description
+ - Documentation invalid links
+ - Thumbnails disabling if PHP GD is not installed
+ - Warning if links sticky status isn't set
+ - Fix button overlapping on mobile in linklist
+ - Do not try to retrieve thumbnails for internal link
+ - Update node-sass to fix a vulnerability in node tar dependency
+ - armhf Dockerfile
+ - Default template: Responsive issue with delete button fix
+ - Persist sticky status on bookmark update
+ ### Removed
+ - Doxygen configuration
+ - redirector setting
+ - QRCode link to an external service
+ ## [v0.10.4](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4) - 2019-04-16
  ### Fixed
  - Fix thumbnails disabling if PHP GD is not installed
  - Fix a warning if links sticky status isn't set
@@@ -282,7 -339,7 +339,7 @@@ configuration to enable URL rewriting, 
          - `/api/v1/info`: get general information on the Shaarli instance
          - `/api/v1/links`: get a list of shaared links
          - `/api/v1/history`: get a list of latest actions
--Theming:
++          Theming:
      - Introduce a new theme
      - Allow selecting themes/templates from the configuration page
      - New/Edit link form can be submitted using CTRL+Enter in the textarea
@@@ -580,12 -637,12 +637,12 @@@ Please use our release archives, or fol
  - Cleanup: introduce an `ApplicationUtils` class
  
  ### Removed
-- - Cleanup: remove `json_encode()` function (built-in since PHP 5.2)
++- Cleanup: remove `json_encode()` function (built-in since PHP 5.2)
  
  ### Fixed
-- - Auto-complete more than one tag
-- - Bookmarklet: support titles containing quotes
-- - URL encode links when setting a redirector
++- Auto-complete more than one tag
++- Bookmarklet: support titles containing quotes
++- URL encode links when setting a redirector
  
  
  ## [v0.6.0](https://github.com/shaarli/Shaarli/releases/tag/v0.6.0) - 2015-11-18
@@@ -1222,8 -1279,8 +1279,8 @@@ Initial release on GitHub
  - In tag autocomplete, tags are presented in use order
    (most used tags first, instead of alphabetical order)
  - RSS Feed can now be filtered by tags or fulltext search. Just add to the feed url:
--  - `&searchtags=minecraft+video` for tag filtering
--  - `&searchterm=portal` for fulltext search to the feed url
++    - `&searchtags=minecraft+video` for tag filtering
++    - `&searchterm=portal` for fulltext search to the feed url
  
  
  ## [v0.0.12beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
index 803757cae5c02c1fffed2f5257b57ff798ef5a07,efde8468fca97e896f1da161e444993bdd711b4d..76ba95f0dbd7a5a309ddb2f832f86fe6b5c81273
@@@ -1,4 -1,15 +1,15 @@@
  <?php
+ namespace Shaarli\Bookmark;
+ use ArrayAccess;
+ use Countable;
+ use DateTime;
+ use Iterator;
+ use Shaarli\Bookmark\Exception\LinkNotFoundException;
+ use Shaarli\Exceptions\IOException;
+ use Shaarli\FileUtils;
  /**
   * Data storage for links.
   *
   *  - private:  Is this link private? 0=no, other value=yes
   *  - tags:     tags attached to this entry (separated by spaces)
   *  - title     Title of the link
-  *  - url       URL of the link. Used for displayable links (no redirector, relative, etc.).
-  *              Can be absolute or relative.
-  *              Relative URLs are permalinks (e.g.'?m-ukcw')
-  *  - real_url  Absolute processed URL.
+  *  - url       URL of the link. Used for displayable links.
+  *              Can be absolute or relative in the database but the relative links
+  *              will be converted to absolute ones in templates.
+  *  - real_url  Raw URL in stored in the DB (absolute or relative).
   *  - shorturl  Permalink smallhash
   *
   * Implements 3 interfaces:
@@@ -77,19 -88,6 +88,6 @@@ class LinkDB implements Iterator, Count
      // Hide public links
      private $hidePublicLinks;
  
-     // link redirector set in user settings.
-     private $redirector;
-     /**
-      * Set this to `true` to urlencode link behind redirector link, `false` to leave it untouched.
-      *
-      * Example:
-      *   anonym.to needs clean URL while dereferer.org needs urlencoded URL.
-      *
-      * @var boolean $redirectorEncode parameter: true or false
-      */
-     private $redirectorEncode;
      /**
       * Creates a new LinkDB
       *
       * @param string  $datastore        datastore file path.
       * @param boolean $isLoggedIn       is the user logged in?
       * @param boolean $hidePublicLinks  if true all links are private.
-      * @param string  $redirector       link redirector set in user settings.
-      * @param boolean $redirectorEncode Enable urlencode on redirected urls (default: true).
       */
      public function __construct(
          $datastore,
          $isLoggedIn,
-         $hidePublicLinks,
-         $redirector = '',
-         $redirectorEncode = true
+         $hidePublicLinks
      ) {
 -    
++
          $this->datastore = $datastore;
          $this->loggedIn = $isLoggedIn;
          $this->hidePublicLinks = $hidePublicLinks;
-         $this->redirector = $redirector;
-         $this->redirectorEncode = $redirectorEncode === true;
          $this->check();
          $this->read();
      }
          if (!isset($value['id']) || empty($value['url'])) {
              die(t('Internal Error: A link should always have an id and URL.'));
          }
-         if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
+         if (($offset !== null && !is_int($offset)) || !is_int($value['id'])) {
              die(t('You must specify an integer as a key.'));
          }
          if ($offset !== null && $offset !== $value['id']) {
          $this->links = array();
          $link = array(
              'id' => 1,
-             'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
-             'url'=>'https://shaarli.readthedocs.io',
-             'description'=>t(
+             'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'),
+             'url' => 'https://shaarli.readthedocs.io',
+             'description' => t(
                  'Welcome to Shaarli! This is your first public bookmark. '
-                 .'To edit or delete me, you must first login.
+                 . 'To edit or delete me, you must first login.
  
  To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
  
  You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
              ),
-             'private'=>0,
-             'created'=> new DateTime(),
-             'tags'=>'opensource software',
+             'private' => 0,
+             'created' => new DateTime(),
+             'tags' => 'opensource software',
              'sticky' => false,
          );
          $link['shorturl'] = link_small_hash($link['created'], $link['id']);
  
          $link = array(
              'id' => 0,
-             'title'=> t('My secret stuff... - Pastebin.com'),
-             'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
-             'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
-             'private'=>1,
-             'created'=> new DateTime('1 minute ago'),
-             'tags'=>'secretstuff',
+             'title' => t('My secret stuff... - Pastebin.com'),
+             'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
+             'description' => t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
+             'private' => 1,
+             'created' => new DateTime('1 minute ago'),
+             'tags' => 'secretstuff',
              'sticky' => false,
          );
          $link['shorturl'] = link_small_hash($link['created'], $link['id']);
  
          $toremove = array();
          foreach ($this->links as $key => &$link) {
-             if (! $this->loggedIn && $link['private'] != 0) {
+             if (!$this->loggedIn && $link['private'] != 0) {
                  // Transition for not upgraded databases.
                  unset($this->links[$key]);
                  continue;
              sanitizeLink($link);
  
              // Remove private tags if the user is not logged in.
-             if (! $this->loggedIn) {
+             if (!$this->loggedIn) {
                  $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
              }
  
-             // Do not use the redirector for internal links (Shaarli note URL starting with a '?').
-             if (!empty($this->redirector) && !startsWith($link['url'], '?')) {
-                 $link['real_url'] = $this->redirector;
-                 if ($this->redirectorEncode) {
-                     $link['real_url'] .= urlencode(unescape($link['url']));
-                 } else {
-                     $link['real_url'] .= $link['url'];
-                 }
-             } else {
-                 $link['real_url'] = $link['url'];
-             }
+             $link['real_url'] = $link['url'];
  
              $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
  
              // To be able to load links before running the update, and prepare the update
-             if (! isset($link['created'])) {
+             if (!isset($link['created'])) {
                  $link['id'] = $link['linkdate'];
                  $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
-                 if (! empty($link['updated'])) {
+                 if (!empty($link['updated'])) {
                      $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']);
                  }
                  $link['shorturl'] = smallHash($link['linkdate']);
      /**
       * Filter links according to search parameters.
       *
-      * @param array  $filterRequest Search request content. Supported keys:
+      * @param array  $filterRequest  Search request content. Supported keys:
       *                                - searchtags: list of tags
       *                                - searchterm: term search
-      * @param bool   $casesensitive Optional: Perform case sensitive filter
-      * @param string $visibility    return only all/private/public links
-      * @param string $untaggedonly  return only untagged links
+      * @param bool   $casesensitive  Optional: Perform case sensitive filter
+      * @param string $visibility     return only all/private/public links
+      * @param bool   $untaggedonly   return only untagged links
       *
       * @return array filtered links, all links if no suitable filter was provided.
       */
          $visibility = 'all',
          $untaggedonly = false
      ) {
 -    
++
          // Filter link database according to parameters.
          $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
          $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
      /**
       * Returns the list tags appearing in the links with the given tags
       *
-      * @param array $filteringTags tags selecting the links to consider
-      * @param string $visibility   process only all/private/public links
+      * @param array  $filteringTags tags selecting the links to consider
+      * @param string $visibility    process only all/private/public links
       *
       * @return array tag => linksCount
       */
index 86a21fc39b6c701cd33d9ea3b4649f30ef020f50,beb9ea9b7517613cd9c2ca01c884ba239f250355..30e5247bc43dab93c7a5c4afb42a96541d04404a
@@@ -1,11 -1,24 +1,24 @@@
  <?php
+ namespace Shaarli\Updater;
+ use Exception;
+ use RainTPL;
+ use ReflectionClass;
+ use ReflectionException;
+ use ReflectionMethod;
+ use Shaarli\ApplicationUtils;
+ use Shaarli\Bookmark\LinkDB;
+ use Shaarli\Bookmark\LinkFilter;
  use Shaarli\Config\ConfigJson;
- use Shaarli\Config\ConfigPhp;
  use Shaarli\Config\ConfigManager;
+ use Shaarli\Config\ConfigPhp;
+ use Shaarli\Exceptions\IOException;
  use Shaarli\Thumbnailer;
+ use Shaarli\Updater\Exception\UpdaterException;
  
  /**
-  * Class Updater.
+  * Class updater.
   * Used to update stuff when a new Shaarli's version is reached.
   * Update methods are ran only once, and the stored in a JSON file.
   */
@@@ -83,12 -96,12 +96,12 @@@ class Update
          }
  
          if ($this->methods === null) {
-             throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
+             throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
          }
  
          foreach ($this->methods as $method) {
              // Not an update method or already done, pass.
-             if (! startsWith($method->getName(), 'updateMethod')
+             if (!startsWith($method->getName(), 'updateMethod')
                  || in_array($method->getName(), $this->doneUpdates)
              ) {
                  continue;
                  }
              }
              $this->conf->write($this->isLoggedIn);
-             unlink($this->conf->get('resource.data_dir').'/options.php');
+             unlink($this->conf->get('resource.data_dir') . '/options.php');
          }
  
          return true;
          $subConfig = array('config', 'plugins');
          foreach ($subConfig as $sub) {
              foreach ($oldConfig[$sub] as $key => $value) {
-                 if (isset($legacyMap[$sub .'.'. $key])) {
-                     $configKey = $legacyMap[$sub .'.'. $key];
+                 if (isset($legacyMap[$sub . '.' . $key])) {
+                     $configKey = $legacyMap[$sub . '.' . $key];
                  } else {
-                     $configKey = $sub .'.'. $key;
+                     $configKey = $sub . '.' . $key;
                  }
                  $this->conf->set($configKey, $value);
              }
          try {
              $this->conf->set('general.title', escape($this->conf->get('general.title')));
              $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
-             $this->conf->set('redirector.url', escape($this->conf->get('redirector.url')));
              $this->conf->write($this->isLoggedIn);
          } catch (Exception $e) {
              error_log($e->getMessage());
              return true;
          }
  
-         $save = $this->conf->get('resource.data_dir') .'/datastore.'. date('YmdHis') .'.php';
+         $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
          copy($this->conf->get('resource.datastore'), $save);
  
          $links = array();
      }
  
      /**
 +<<<<<<< HEAD
 +=======
       * Rename tags starting with a '-' to work with tag exclusion search.
       */
      public function updateMethodRenameDashTags()
          // We run the update only if this folder still contains the template files.
          $tplDir = $this->conf->get('resource.raintpl_tpl');
          $tplFile = $tplDir . '/linklist.html';
-         if (! file_exists($tplFile)) {
+         if (!file_exists($tplFile)) {
              return true;
          }
  
       */
      public function updateMethodMoveUserCss()
      {
-         if (! is_file('inc/user.css')) {
+         if (!is_file('inc/user.css')) {
              return true;
          }
  
       */
      public function updateMethodPiwikUrl()
      {
-         if (! $this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
+         if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
              return true;
          }
  
-         $this->conf->set('plugins.PIWIK_URL', 'http://'. $this->conf->get('plugins.PIWIK_URL'));
+         $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
          $this->conf->write($this->isLoggedIn);
  
          return true;
              return true;
          }
  
-         if (! $this->conf->exists('general.download_max_size')) {
-             $this->conf->set('general.download_max_size', 1024*1024*4);
+         if (!$this->conf->exists('general.download_max_size')) {
+             $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
          }
  
-         if (! $this->conf->exists('general.download_timeout')) {
+         if (!$this->conf->exists('general.download_timeout')) {
              $this->conf->set('general.download_timeout', 30);
          }
  
  
          return true;
      }
- }
- /**
-  * Class UpdaterException.
-  */
- class UpdaterException extends Exception
- {
-     /**
-      * @var string Method where the error occurred.
-      */
-     protected $method;
  
      /**
-      * @var Exception The parent exception.
+      * Remove redirector settings.
       */
-     protected $previous;
-     /**
-      * Constructor.
-      *
-      * @param string         $message  Force the error message if set.
-      * @param string         $method   Method where the error occurred.
-      * @param Exception|bool $previous Parent exception.
-      */
-     public function __construct($message = '', $method = '', $previous = false)
+     public function updateMethodRemoveRedirector()
      {
-         $this->method = $method;
-         $this->previous = $previous;
-         $this->message = $this->buildMessage($message);
-     }
-     /**
-      * Build the exception error message.
-      *
-      * @param string $message Optional given error message.
-      *
-      * @return string The built error message.
-      */
-     private function buildMessage($message)
-     {
-         $out = '';
-         if (! empty($message)) {
-             $out .= $message . PHP_EOL;
-         }
-         if (! empty($this->method)) {
-             $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
-         }
-         if (! empty($this->previous)) {
-             $out .= '  '. $this->previous->getMessage();
-         }
-         return $out;
-     }
- }
- /**
-  * Read the updates file, and return already done updates.
-  *
-  * @param string $updatesFilepath Updates file path.
-  *
-  * @return array Already done update methods.
-  */
- function read_updates_file($updatesFilepath)
- {
-     if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
-         $content = file_get_contents($updatesFilepath);
-         if (! empty($content)) {
-             return explode(';', $content);
-         }
-     }
-     return array();
- }
- /**
-  * Write updates file.
-  *
-  * @param string $updatesFilepath Updates file path.
-  * @param array  $updates         Updates array to write.
-  *
-  * @throws Exception Couldn't write version number.
-  */
- function write_updates_file($updatesFilepath, $updates)
- {
-     if (empty($updatesFilepath)) {
-         throw new Exception(t('Updates file path is not set, can\'t write updates.'));
-     }
-     $res = file_put_contents($updatesFilepath, implode(';', $updates));
-     if ($res === false) {
-         throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
+         $this->conf->remove('redirector');
+         $this->conf->write(true);
+         return true;
      }
  }
diff --combined index.php
index 27c67ce1a7f16935ba1916442fe12614dde1933b,9e473936ab0ab76cc3d5d23dfb6cffc6c13c2468..3a5c35c480067caefe56e352b15fd110c6e20266
+++ b/index.php
@@@ -1,12 -1,6 +1,12 @@@
  <?php
  /**
 +<<<<<<< HEAD
 + * Shaarli v0.8.7 - Shaare your links...
 + *
 + * The personal, minimalist, super-fast, database free, bookmarking service.
 +=======
   * Shaarli - The personal, minimalist, super-fast, database free, bookmarking service.
 +>>>>>>> v0.9.7
   *
   * Friendly fork by the Shaarli community:
   *  - https://github.com/shaarli/Shaarli
@@@ -62,31 -56,33 +62,33 @@@ require_once 'inc/rain.tpl.class.php'
  require_once __DIR__ . '/vendor/autoload.php';
  
  // Shaarli library
- require_once 'application/ApplicationUtils.php';
- require_once 'application/Cache.php';
- require_once 'application/CachedPage.php';
+ require_once 'application/bookmark/LinkUtils.php';
  require_once 'application/config/ConfigPlugin.php';
- require_once 'application/FeedBuilder.php';
+ require_once 'application/feed/Cache.php';
+ require_once 'application/http/HttpUtils.php';
+ require_once 'application/http/UrlUtils.php';
+ require_once 'application/updater/UpdaterUtils.php';
  require_once 'application/FileUtils.php';
- require_once 'application/History.php';
- require_once 'application/HttpUtils.php';
- require_once 'application/LinkDB.php';
- require_once 'application/LinkFilter.php';
- require_once 'application/LinkUtils.php';
- require_once 'application/NetscapeBookmarkUtils.php';
- require_once 'application/PageBuilder.php';
  require_once 'application/TimeZone.php';
- require_once 'application/Url.php';
  require_once 'application/Utils.php';
- require_once 'application/PluginManager.php';
- require_once 'application/Router.php';
- require_once 'application/Updater.php';
+ use \Shaarli\ApplicationUtils;
+ use \Shaarli\Bookmark\Exception\LinkNotFoundException;
+ use \Shaarli\Bookmark\LinkDB;
  use \Shaarli\Config\ConfigManager;
+ use \Shaarli\Feed\CachedPage;
+ use \Shaarli\Feed\FeedBuilder;
+ use \Shaarli\History;
  use \Shaarli\Languages;
+ use \Shaarli\Netscape\NetscapeBookmarkUtils;
+ use \Shaarli\Plugin\PluginManager;
+ use \Shaarli\Render\PageBuilder;
+ use \Shaarli\Render\ThemeUtils;
+ use \Shaarli\Router;
  use \Shaarli\Security\LoginManager;
  use \Shaarli\Security\SessionManager;
- use \Shaarli\ThemeUtils;
  use \Shaarli\Thumbnailer;
+ use \Shaarli\Updater\Updater;
  
  // Ensure the PHP version is supported
  try {
@@@ -129,7 -125,7 +131,7 @@@ if (isset($_COOKIE['shaarli']) && !Sess
  
  $conf = new ConfigManager();
  $sessionManager = new SessionManager($_SESSION, $conf);
- $loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
+ $loginManager = new LoginManager($conf, $sessionManager);
  $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
  $clientIpId = client_ip_id($_SERVER);
  
@@@ -316,9 -312,7 +318,7 @@@ function showDailyRSS($conf, $loginMana
      $LINKSDB = new LinkDB(
          $conf->get('resource.datastore'),
          $loginManager->isLoggedIn(),
-         $conf->get('privacy.hide_public_links'),
-         $conf->get('redirector.url'),
-         $conf->get('redirector.encode_url')
+         $conf->get('privacy.hide_public_links')
      );
  
      /* Some Shaarlies may have very few links, so we need to look
  
          // We pre-format some fields for proper output.
          foreach ($links as &$link) {
-             $link['formatedDescription'] = format_description(
-                 $link['description'],
-                 $conf->get('redirector.url'),
-                 $conf->get('redirector.encode_url')
-             );
+             $link['formatedDescription'] = format_description($link['description']);
              $link['timestamp'] = $link['created']->getTimestamp();
-             if (startsWith($link['url'], '?')) {
+             if (is_note($link['url'])) {
                  $link['url'] = index_url($_SERVER) . $link['url'];  // make permalink URL absolute
              }
          }
   */
  function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
  {
-     $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
      if (isset($_GET['day'])) {
          $day = $_GET['day'];
+         if ($day === date('Ymd', strtotime('now'))) {
+             $pageBuilder->assign('dayDesc', t('Today'));
+         } elseif ($day === date('Ymd', strtotime('-1 days'))) {
+             $pageBuilder->assign('dayDesc', t('Yesterday'));
+         }
+     } else {
+         $day = date('Ymd', strtotime('now')); // Today, in format YYYYMMDD.
+         $pageBuilder->assign('dayDesc', t('Today'));
      }
  
      $days = $LINKSDB->days();
          $taglist = explode(' ', $link['tags']);
          uasort($taglist, 'strcasecmp');
          $linksToDisplay[$key]['taglist']=$taglist;
-         $linksToDisplay[$key]['formatedDescription'] = format_description(
-             $link['description'],
-             $conf->get('redirector.url'),
-             $conf->get('redirector.encode_url')
-         );
+         $linksToDisplay[$key]['formatedDescription'] = format_description($link['description']);
          $linksToDisplay[$key]['timestamp'] =  $link['created']->getTimestamp();
      }
  
@@@ -1022,6 -1015,7 +1021,7 @@@ function renderPage($conf, $pluginManag
              $conf->set('general.timezone', $tz);
              $conf->set('general.title', escape($_POST['title']));
              $conf->set('general.header_link', escape($_POST['titleLink']));
+             $conf->set('general.retrieve_description', !empty($_POST['retrieveDescription']));
              $conf->set('resource.theme', escape($_POST['theme']));
              $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
              $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
              );
              $PAGE->assign('continents', $continents);
              $PAGE->assign('cities', $cities);
+             $PAGE->assign('retrieve_description', $conf->get('general.retrieve_description'));
              $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
              $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
              $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
              $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
              $PAGE->assign('api_secret', $conf->get('api.secret'));
              $PAGE->assign('languages', Languages::getAvailableLanguages());
-             $PAGE->assign('language', $conf->get('translation.language'));
              $PAGE->assign('gd_enabled', extension_loaded('gd'));
              $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
              $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
  
          // lf_id should only be present if the link exists.
          $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
+         $link['id'] = $id;
          // Linkdate is kept here to:
          //   - use the same permalink for notes as they're displayed when creating them
          //   - let users hack creation date of their posts
          //     See: https://shaarli.readthedocs.io/en/master/guides/various-hacks/#changing-the-timestamp-for-a-shaare
          $linkdate = escape($_POST['lf_linkdate']);
+         $link['created'] = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
          if (isset($LINKSDB[$id])) {
              // Edit
-             $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
-             $updated = new DateTime();
-             $shortUrl = $LINKSDB[$id]['shorturl'];
+             $link['updated'] = new DateTime();
+             $link['shorturl'] = $LINKSDB[$id]['shorturl'];
+             $link['sticky'] = isset($LINKSDB[$id]['sticky']) ? $LINKSDB[$id]['sticky'] : false;
              $new = false;
          } else {
              // New link
-             $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
-             $updated = null;
-             $shortUrl = link_small_hash($created, $id);
+             $link['updated'] = null;
+             $link['shorturl'] = link_small_hash($link['created'], $id);
+             $link['sticky'] = false;
              $new = true;
          }
  
          }
          $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols'));
  
-         $link = array(
-             'id' => $id,
+         $link = array_merge($link, [
              'title' => trim($_POST['lf_title']),
              'url' => $url,
              'description' => $_POST['lf_description'],
              'private' => (isset($_POST['lf_private']) ? 1 : 0),
-             'created' => $created,
-             'updated' => $updated,
              'tags' => str_replace(',', ' ', $tags),
-             'shorturl' => $shortUrl,
-         );
+         ]);
  
          // If title is empty, use the URL as title.
          if ($link['title'] == '') {
              $link['title'] = $link['url'];
          }
  
-         if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE) {
+         if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
+             && ! is_note($link['url'])
+         ) {
              $thumbnailer = new Thumbnailer($conf);
              $link['thumbnail'] = $thumbnailer->get($url);
          }
  
 +        $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
 +
          $pluginManager->executeHooks('save_link', $link);
  
          $LINKSDB[$id] = $link;
          exit;
      }
  
+     // -------- User clicked either "Set public" or "Set private" bulk operation
+     if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) {
+         if (! $sessionManager->checkToken($_GET['token'])) {
+             die(t('Wrong token.'));
+         }
+         $ids = trim($_GET['ids']);
+         if (strpos($ids, ' ') !== false) {
+             // multiple, space-separated ids provided
+             $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
+         } else {
+             // only a single id provided
+             $ids = [$ids];
+         }
+         // assert at least one id is given
+         if (!count($ids)) {
+             die('no id provided');
+         }
+         // assert that the visibility is valid
+         if (!isset($_GET['newVisibility']) || !in_array($_GET['newVisibility'], ['public', 'private'])) {
+             die('invalid visibility');
+         } else {
+             $private = $_GET['newVisibility'] === 'private';
+         }
+         foreach ($ids as $id) {
+             $id = (int) escape($id);
+             $link = $LINKSDB[$id];
+             $link['private'] = $private;
+             $pluginManager->executeHooks('save_link', $link);
+             $LINKSDB[$id] = $link;
+         }
+         $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
+         $location = '?';
+         if (isset($_SERVER['HTTP_REFERER'])) {
+             $location = generateLocation(
+                 $_SERVER['HTTP_REFERER'],
+                 $_SERVER['HTTP_HOST']
+             );
+         }
+         header('Location: ' . $location); // After deleting the link, redirect to appropriate location
+         exit;
+     }
      // -------- User clicked the "EDIT" button on a link: Display link edit form.
      if (isset($_GET['edit_link'])) {
          $id = (int) escape($_GET['edit_link']);
              // If this is an HTTP(S) link, we try go get the page to extract
              // the title (otherwise we will to straight to the edit form.)
              if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
+                 $retrieveDescription = $conf->get('general.retrieve_description');
                  // Short timeout to keep the application responsive
                  // The callback will fill $charset and $title with data from the downloaded page.
                  get_http_response(
                      $url,
                      $conf->get('general.download_timeout', 30),
                      $conf->get('general.download_max_size', 4194304),
-                     get_curl_download_callback($charset, $title)
+                     get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
                  );
                  if (! empty($title) && strtolower($charset) != 'utf-8') {
                      $title = mb_convert_encoding($title, 'utf-8', $charset);
      if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
          try {
              if (isset($_POST['parameters_form'])) {
+                 $pluginManager->executeHooks('save_plugin_parameters', $_POST);
                  unset($_POST['parameters_form']);
                  foreach ($_POST as $param => $value) {
                      $conf->set('plugins.'. $param, escape($value));
          $ids = [];
          foreach ($LINKSDB as $link) {
              // A note or not HTTP(S)
-             if ($link['url'][0] === '?' || ! startsWith(strtolower($link['url']), 'http')) {
+             if (is_note($link['url']) || ! startsWith(strtolower($link['url']), 'http')) {
                  continue;
              }
              $ids[] = $link['id'];
@@@ -1668,11 -1707,7 +1715,7 @@@ function buildLinkList($PAGE, $LINKSDB
      $linkDisp = array();
      while ($i<$end && $i<count($keys)) {
          $link = $linksToDisplay[$keys[$i]];
-         $link['description'] = format_description(
-             $link['description'],
-             $conf->get('redirector.url'),
-             $conf->get('redirector.encode_url')
-         );
+         $link['description'] = format_description($link['description']);
          $classLi =  ($i % 2) != 0 ? '' : 'publicLinkHightLight';
          $link['class'] = $link['private'] == 0 ? $classLi : 'private';
          $link['timestamp'] = $link['created']->getTimestamp();
          'search_term' => $searchterm,
          'search_tags' => $searchtags,
          'visibility' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '',
-         'redirector' => $conf->get('redirector.url'),  // Optional redirector URL.
          'links' => $linkDisp,
      );
  
@@@ -1883,9 -1917,7 +1925,7 @@@ try 
  $linkDb = new LinkDB(
      $conf->get('resource.datastore'),
      $loginManager->isLoggedIn(),
-     $conf->get('privacy.hide_public_links'),
-     $conf->get('redirector.url'),
-     $conf->get('redirector.encode_url')
+     $conf->get('privacy.hide_public_links')
  );
  
  $container = new \Slim\Container();
@@@ -1908,7 -1940,7 +1948,7 @@@ $app->group('/api/v1', function () 
      $this->put('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:putTag')->setName('putTag');
      $this->delete('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:deleteTag')->setName('deleteTag');
  
-     $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory');
+     $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
  })->add('\Shaarli\Api\ApiMiddleware');
  
  $response = $app->run(true);
index 608e331dac04a4c12da14fa920338c79d4f95110,93bc86c1b72a1ae5457fbd89847bbcf2f4730803..738b7361553e7869be792cbb0fdd623f57380244
@@@ -1,17 -1,24 +1,24 @@@
  <?php
+ namespace Shaarli\Updater;
+ use DateTime;
+ use Exception;
+ use Shaarli\Bookmark\LinkDB;
  use Shaarli\Config\ConfigJson;
  use Shaarli\Config\ConfigManager;
  use Shaarli\Config\ConfigPhp;
  use Shaarli\Thumbnailer;
  
- require_once 'tests/Updater/DummyUpdater.php';
+ require_once 'application/updater/UpdaterUtils.php';
+ require_once 'tests/updater/DummyUpdater.php';
+ require_once 'tests/utils/ReferenceLinkDB.php';
  require_once 'inc/rain.tpl.class.php';
  
  /**
   * Class UpdaterTest.
-  * Runs unit tests against the Updater class.
+  * Runs unit tests against the updater class.
   */
- class UpdaterTest extends PHPUnit_Framework_TestCase
+ class UpdaterTest extends \PHPUnit\Framework\TestCase
  {
      /**
       * @var string Path to test datastore.
      /**
       * Test Update failed.
       *
-      * @expectedException UpdaterException
+      * @expectedException \Exception
       */
      public function testUpdateFailed()
      {
          $this->conf->setConfigFile('tests/utils/config/configPhp');
          $this->conf->reset();
  
-         $optionsFile = 'tests/Updater/options.php';
+         $optionsFile = 'tests/updater/options.php';
          $options = '<?php
  $GLOBALS[\'privateLinkByDefault\'] = true;';
          file_put_contents($optionsFile, $options);
  
          // tmp config file.
-         $this->conf->setConfigFile('tests/Updater/config');
+         $this->conf->setConfigFile('tests/updater/config');
  
          // merge configs
          $updater = new Updater(array(), array(), $this->conf, true);
-         // This writes a new config file in tests/Updater/config.php
+         // This writes a new config file in tests/updater/config.php
          $updater->updateMethodMergeDeprecatedConfigFile();
  
          // make sure updated field is changed
       */
      public function testRenameDashTags()
      {
-         $refDB = new ReferenceLinkDB();
+         $refDB = new \ReferenceLinkDB();
          $refDB->write(self::$testDatastore);
          $linkDB = new LinkDB(self::$testDatastore, true, false);
  
          $this->conf = new ConfigManager($sandbox);
          $title = '<script>alert("title");</script>';
          $headerLink = '<script>alert("header_link");</script>';
-         $redirectorUrl = '<script>alert("redirector");</script>';
          $this->conf->set('general.title', $title);
          $this->conf->set('general.header_link', $headerLink);
-         $this->conf->set('redirector.url', $redirectorUrl);
          $updater = new Updater(array(), array(), $this->conf, true);
          $done = $updater->updateMethodEscapeUnescapedConfig();
          $this->assertTrue($done);
          $this->conf->reload();
          $this->assertEquals(escape($title), $this->conf->get('general.title'));
          $this->assertEquals(escape($headerLink), $this->conf->get('general.header_link'));
-         $this->assertEquals(escape($redirectorUrl), $this->conf->get('redirector.url'));
          unlink($sandbox . '.json.php');
      }
  
                  'private' => true,
              ),
          );
-         $refDB = new ReferenceLinkDB();
+         $refDB = new \ReferenceLinkDB();
          $refDB->setLinks($links);
          $refDB->write(self::$testDatastore);
          $linkDB = new LinkDB(self::$testDatastore, true, false);
       */
      public function testDatastoreIdsNothingToDo()
      {
-         $refDB = new ReferenceLinkDB();
+         $refDB = new \ReferenceLinkDB();
          $refDB->write(self::$testDatastore);
          $linkDB = new LinkDB(self::$testDatastore, true, false);
  
          $this->assertFalse($this->conf->get('security.markdown_escape'));
      }
  
 -
      /**
       * Test updateMethodEscapeMarkdown with markdown plugin disabled
       * => setting markdown_escape set to true.
      }
  
      /**
- <<<<<<< HEAD
       * Test updateMethodWebThumbnailer with thumbnails enabled.
       */
      public function testUpdateMethodWebThumbnailerEnabled()
              1 => ['id' => 1] + $blank,
              2 => ['id' => 2] + $blank,
          ];
-         $refDB = new ReferenceLinkDB();
+         $refDB = new \ReferenceLinkDB();
          $refDB->setLinks($links);
          $refDB->write(self::$testDatastore);
          $linkDB = new LinkDB(self::$testDatastore, true, false);
              1 => ['id' => 1, 'sticky' => true] + $blank,
              2 => ['id' => 2] + $blank,
          ];
-         $refDB = new ReferenceLinkDB();
+         $refDB = new \ReferenceLinkDB();
          $refDB->setLinks($links);
          $refDB->write(self::$testDatastore);
          $linkDB = new LinkDB(self::$testDatastore, true, false);
          $linkDB = new LinkDB(self::$testDatastore, true, false);
          $this->assertTrue($linkDB[1]['sticky']);
      }
+     /**
+      * Test updateMethodRemoveRedirector().
+      */
+     public function testUpdateRemoveRedirector()
+     {
+         $sandboxConf = 'sandbox/config';
+         copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
+         $this->conf = new ConfigManager($sandboxConf);
+         $updater = new Updater([], null, $this->conf, true);
+         $this->assertTrue($updater->updateMethodRemoveRedirector());
+         $this->assertFalse($this->conf->exists('redirector'));
+         $this->conf = new ConfigManager($sandboxConf);
+         $this->assertFalse($this->conf->exists('redirector'));
+     }
  }