]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Apply the new system (Bookmark + Service) to the whole code base
authorArthurHoaro <arthur@hoa.ro>
Sat, 25 May 2019 13:52:27 +0000 (15:52 +0200)
committerArthurHoaro <arthur@hoa.ro>
Sat, 18 Jan 2020 08:55:59 +0000 (09:55 +0100)
See https://github.com/shaarli/Shaarli/issues/1307

31 files changed:
application/History.php
application/Utils.php
application/api/ApiMiddleware.php
application/api/ApiUtils.php
application/api/controllers/ApiController.php
application/api/controllers/HistoryController.php
application/api/controllers/Info.php
application/api/controllers/Links.php
application/api/controllers/Tags.php
application/bookmark/BookmarkArray.php
application/config/ConfigManager.php
application/feed/FeedBuilder.php
application/formatter/BookmarkMarkdownFormatter.php
application/netscape/NetscapeBookmarkUtils.php
application/render/PageBuilder.php
application/updater/Updater.php
application/updater/UpdaterUtils.php
assets/common/css/markdown.css [moved from plugins/markdown/markdown.css with 98% similarity]
doc/md/guides/various-hacks.md
index.php
plugins/markdown/README.md [deleted file]
plugins/markdown/help.html [deleted file]
plugins/markdown/markdown.meta [deleted file]
plugins/markdown/markdown.php [deleted file]
tpl/default/configure.html
tpl/default/editlink.html
tpl/default/includes.html
tpl/vintage/configure.html
tpl/vintage/editlink.html
tpl/vintage/includes.html
webpack.config.js

index a58466528050e2efb4aee4dbccb59f3629e42041..4fd2f29444ea8a6122740a25f77fc97847e868d4 100644 (file)
@@ -3,6 +3,7 @@ namespace Shaarli;
 
 use DateTime;
 use Exception;
+use Shaarli\Bookmark\Bookmark;
 
 /**
  * Class History
@@ -20,7 +21,7 @@ use Exception;
  *   - UPDATED: link updated
  *   - DELETED: link deleted
  *   - SETTINGS: the settings have been updated through the UI.
- *   - IMPORT: bulk links import
+ *   - IMPORT: bulk bookmarks import
  *
  * Note: new events are put at the beginning of the file and history array.
  */
@@ -96,31 +97,31 @@ class History
     /**
      * Add Event: new link.
      *
-     * @param array $link Link data.
+     * @param Bookmark $link Link data.
      */
     public function addLink($link)
     {
-        $this->addEvent(self::CREATED, $link['id']);
+        $this->addEvent(self::CREATED, $link->getId());
     }
 
     /**
      * Add Event: update existing link.
      *
-     * @param array $link Link data.
+     * @param Bookmark $link Link data.
      */
     public function updateLink($link)
     {
-        $this->addEvent(self::UPDATED, $link['id']);
+        $this->addEvent(self::UPDATED, $link->getId());
     }
 
     /**
      * Add Event: delete existing link.
      *
-     * @param array $link Link data.
+     * @param Bookmark $link Link data.
      */
     public function deleteLink($link)
     {
-        $this->addEvent(self::DELETED, $link['id']);
+        $this->addEvent(self::DELETED, $link->getId());
     }
 
     /**
@@ -134,7 +135,7 @@ class History
     /**
      * Add Event: bulk import.
      *
-     * Note: we don't store links add/update one by one since it can have a huge impact on performances.
+     * Note: we don't store bookmarks add/update one by one since it can have a huge impact on performances.
      */
     public function importLinks()
     {
index 925e1a22c909011a342b3dc3ced3c9f0f5d9a3da..56f5b9a20d8b33b9fa3c06fd6f819931100d4545 100644 (file)
@@ -162,7 +162,7 @@ function generateLocation($referer, $host, $loopTerms = array())
     $finalReferer = '?';
 
     // No referer if it contains any value in $loopCriteria.
-    foreach ($loopTerms as $value) {
+    foreach (array_filter($loopTerms) as $value) {
         if (strpos($referer, $value) !== false) {
             return $finalReferer;
         }
index 2d55bda65eb2e24fd7348abaafac4f2f12567a8e..4745ac94101db8efb3c83bc760dd154a3af0445e 100644 (file)
@@ -3,6 +3,7 @@ namespace Shaarli\Api;
 
 use Shaarli\Api\Exceptions\ApiAuthorizationException;
 use Shaarli\Api\Exceptions\ApiException;
+use Shaarli\Bookmark\BookmarkFileService;
 use Shaarli\Config\ConfigManager;
 use Slim\Container;
 use Slim\Http\Request;
@@ -117,7 +118,7 @@ class ApiMiddleware
     }
 
     /**
-     * Instantiate a new LinkDB including private links,
+     * Instantiate a new LinkDB including private bookmarks,
      * and load in the Slim container.
      *
      * FIXME! LinkDB could use a refactoring to avoid this trick.
@@ -126,10 +127,10 @@ class ApiMiddleware
      */
     protected function setLinkDb($conf)
     {
-        $linkDb = new \Shaarli\Bookmark\LinkDB(
-            $conf->get('resource.datastore'),
-            true,
-            $conf->get('privacy.hide_public_links')
+        $linkDb = new BookmarkFileService(
+            $conf,
+            $this->container->get('history'),
+            true
         );
         $this->container['db'] = $linkDb;
     }
index 5ac07c4d2f0e7331969f714325d00ce6a68a1edb..5156a5f783f0bc6767c0684f1c1a595959f3fbbd 100644 (file)
@@ -2,6 +2,7 @@
 namespace Shaarli\Api;
 
 use Shaarli\Api\Exceptions\ApiAuthorizationException;
+use Shaarli\Bookmark\Bookmark;
 use Shaarli\Http\Base64Url;
 
 /**
@@ -54,28 +55,28 @@ class ApiUtils
     /**
      * Format a Link for the REST API.
      *
-     * @param array  $link     Link data read from the datastore.
-     * @param string $indexUrl Shaarli's index URL (used for relative URL).
+     * @param Bookmark $bookmark Bookmark data read from the datastore.
+     * @param string   $indexUrl Shaarli's index URL (used for relative URL).
      *
      * @return array Link data formatted for the REST API.
      */
-    public static function formatLink($link, $indexUrl)
+    public static function formatLink($bookmark, $indexUrl)
     {
-        $out['id'] = $link['id'];
+        $out['id'] = $bookmark->getId();
         // Not an internal link
-        if (! is_note($link['url'])) {
-            $out['url'] = $link['url'];
+        if (! $bookmark->isNote()) {
+            $out['url'] = $bookmark->getUrl();
         } else {
-            $out['url'] = $indexUrl . $link['url'];
+            $out['url'] = $indexUrl . $bookmark->getUrl();
         }
-        $out['shorturl'] = $link['shorturl'];
-        $out['title'] = $link['title'];
-        $out['description'] = $link['description'];
-        $out['tags'] = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
-        $out['private'] = $link['private'] == true;
-        $out['created'] = $link['created']->format(\DateTime::ATOM);
-        if (! empty($link['updated'])) {
-            $out['updated'] = $link['updated']->format(\DateTime::ATOM);
+        $out['shorturl'] = $bookmark->getShortUrl();
+        $out['title'] = $bookmark->getTitle();
+        $out['description'] = $bookmark->getDescription();
+        $out['tags'] = $bookmark->getTags();
+        $out['private'] = $bookmark->isPrivate();
+        $out['created'] = $bookmark->getCreated()->format(\DateTime::ATOM);
+        if (! empty($bookmark->getUpdated())) {
+            $out['updated'] = $bookmark->getUpdated()->format(\DateTime::ATOM);
         } else {
             $out['updated'] = '';
         }
@@ -83,7 +84,7 @@ class ApiUtils
     }
 
     /**
-     * Convert a link given through a request, to a valid link for LinkDB.
+     * Convert a link given through a request, to a valid Bookmark for the datastore.
      *
      * If no URL is provided, it will generate a local note URL.
      * If no title is provided, it will use the URL as title.
@@ -91,50 +92,42 @@ class ApiUtils
      * @param array  $input          Request Link.
      * @param bool   $defaultPrivate Request Link.
      *
-     * @return array Formatted link.
+     * @return Bookmark instance.
      */
     public static function buildLinkFromRequest($input, $defaultPrivate)
     {
-        $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : '';
+        $bookmark = new Bookmark();
+        $url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
         if (isset($input['private'])) {
             $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
         } else {
             $private = $defaultPrivate;
         }
 
-        $link = [
-            'title'         => ! empty($input['title']) ? $input['title'] : $input['url'],
-            'url'           => $input['url'],
-            'description'   => ! empty($input['description']) ? $input['description'] : '',
-            'tags'          => ! empty($input['tags']) ? implode(' ', $input['tags']) : '',
-            'private'       => $private,
-            'created'       => new \DateTime(),
-        ];
-        return $link;
+        $bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
+        $bookmark->setUrl($url);
+        $bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
+        $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
+        $bookmark->setPrivate($private);
+
+        return $bookmark;
     }
 
     /**
      * Update link fields using an updated link object.
      *
-     * @param array $oldLink data
-     * @param array $newLink data
+     * @param Bookmark $oldLink data
+     * @param Bookmark $newLink data
      *
-     * @return array $oldLink updated with $newLink values
+     * @return Bookmark $oldLink updated with $newLink values
      */
     public static function updateLink($oldLink, $newLink)
     {
-        foreach (['title', 'url', 'description', 'tags', 'private'] as $field) {
-            $oldLink[$field] = $newLink[$field];
-        }
-        $oldLink['updated'] = new \DateTime();
-
-        if (empty($oldLink['url'])) {
-            $oldLink['url'] = '?' . $oldLink['shorturl'];
-        }
-
-        if (empty($oldLink['title'])) {
-            $oldLink['title'] = $oldLink['url'];
-        }
+        $oldLink->setTitle($newLink->getTitle());
+        $oldLink->setUrl($newLink->getUrl());
+        $oldLink->setDescription($newLink->getDescription());
+        $oldLink->setTags($newLink->getTags());
+        $oldLink->setPrivate($newLink->isPrivate());
 
         return $oldLink;
     }
@@ -143,7 +136,7 @@ class ApiUtils
      * Format a Tag for the REST API.
      *
      * @param string $tag         Tag name
-     * @param int    $occurrences Number of links using this tag
+     * @param int    $occurrences Number of bookmarks using this tag
      *
      * @return array Link data formatted for the REST API.
      */
index a6e7cbab23565007a71f85ac2338f54a930bae6f..c4b3d0c3df983484c535d5396530c93843688cdd 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Shaarli\Api\Controllers;
 
-use Shaarli\Bookmark\LinkDB;
+use Shaarli\Bookmark\BookmarkServiceInterface;
 use Shaarli\Config\ConfigManager;
 use Slim\Container;
 
@@ -26,9 +26,9 @@ abstract class ApiController
     protected $conf;
 
     /**
-     * @var LinkDB
+     * @var BookmarkServiceInterface
      */
-    protected $linkDb;
+    protected $bookmarkService;
 
     /**
      * @var HistoryController
@@ -51,7 +51,7 @@ abstract class ApiController
     {
         $this->ci = $ci;
         $this->conf = $ci->get('conf');
-        $this->linkDb = $ci->get('db');
+        $this->bookmarkService = $ci->get('db');
         $this->history = $ci->get('history');
         if ($this->conf->get('dev.debug', false)) {
             $this->jsonStyle = JSON_PRETTY_PRINT;
index 9afcfa264430cd7af5910357bfcd07fbea797301..505647a9568599c3eb97c8bbc519fb9e9a3837f3 100644 (file)
@@ -41,7 +41,7 @@ class HistoryController extends ApiController
             throw new ApiBadParametersException('Invalid offset');
         }
 
-        // limit parameter is either a number of links or 'all' for everything.
+        // limit parameter is either a number of bookmarks or 'all' for everything.
         $limit = $request->getParam('limit');
         if (empty($limit)) {
             $limit = count($history);
index f37dcae5330d77b67d7bcaf277f63178d0895703..12f6b2f012e4964bd279f0c832042ac243314095 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Shaarli\Api\Controllers;
 
+use Shaarli\Bookmark\BookmarkFilter;
 use Slim\Http\Request;
 use Slim\Http\Response;
 
@@ -26,8 +27,8 @@ class Info extends ApiController
     public function getInfo($request, $response)
     {
         $info = [
-            'global_counter' => count($this->linkDb),
-            'private_counter' => count_private($this->linkDb),
+            'global_counter' => $this->bookmarkService->count(),
+            'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
             'settings' => array(
                 'title' => $this->conf->get('general.title', 'Shaarli'),
                 'header_link' => $this->conf->get('general.header_link', '?'),
index ffcfd4c75a4f8748101675247a138c083bd1dd27..2924795012d4c1cd037a008ecfd9f6fdb091ad49 100644 (file)
@@ -11,7 +11,7 @@ use Slim\Http\Response;
 /**
  * Class Links
  *
- * REST API Controller: all services related to links collection.
+ * REST API Controller: all services related to bookmarks collection.
  *
  * @package Api\Controllers
  * @see http://shaarli.github.io/api-documentation/#links-links-collection
@@ -19,12 +19,12 @@ use Slim\Http\Response;
 class Links extends ApiController
 {
     /**
-     * @var int Number of links returned if no limit is provided.
+     * @var int Number of bookmarks returned if no limit is provided.
      */
     public static $DEFAULT_LIMIT = 20;
 
     /**
-     * Retrieve a list of links, allowing different filters.
+     * Retrieve a list of bookmarks, allowing different filters.
      *
      * @param Request  $request  Slim request.
      * @param Response $response Slim response.
@@ -36,33 +36,32 @@ class Links extends ApiController
     public function getLinks($request, $response)
     {
         $private = $request->getParam('visibility');
-        $links = $this->linkDb->filterSearch(
+        $bookmarks = $this->bookmarkService->search(
             [
                 'searchtags' => $request->getParam('searchtags', ''),
                 'searchterm' => $request->getParam('searchterm', ''),
             ],
-            false,
             $private
         );
 
-        // Return links from the {offset}th link, starting from 0.
+        // Return bookmarks from the {offset}th link, starting from 0.
         $offset = $request->getParam('offset');
         if (! empty($offset) && ! ctype_digit($offset)) {
             throw new ApiBadParametersException('Invalid offset');
         }
         $offset = ! empty($offset) ? intval($offset) : 0;
-        if ($offset > count($links)) {
+        if ($offset > count($bookmarks)) {
             return $response->withJson([], 200, $this->jsonStyle);
         }
 
-        // limit parameter is either a number of links or 'all' for everything.
+        // limit parameter is either a number of bookmarks or 'all' for everything.
         $limit = $request->getParam('limit');
         if (empty($limit)) {
             $limit = self::$DEFAULT_LIMIT;
         } elseif (ctype_digit($limit)) {
             $limit = intval($limit);
         } elseif ($limit === 'all') {
-            $limit = count($links);
+            $limit = count($bookmarks);
         } else {
             throw new ApiBadParametersException('Invalid limit');
         }
@@ -72,12 +71,12 @@ class Links extends ApiController
 
         $out = [];
         $index = 0;
-        foreach ($links as $link) {
+        foreach ($bookmarks as $bookmark) {
             if (count($out) >= $limit) {
                 break;
             }
             if ($index++ >= $offset) {
-                $out[] = ApiUtils::formatLink($link, $indexUrl);
+                $out[] = ApiUtils::formatLink($bookmark, $indexUrl);
             }
         }
 
@@ -97,11 +96,11 @@ class Links extends ApiController
      */
     public function getLink($request, $response, $args)
     {
-        if (!isset($this->linkDb[$args['id']])) {
+        if (!$this->bookmarkService->exists($args['id'])) {
             throw new ApiLinkNotFoundException();
         }
         $index = index_url($this->ci['environment']);
-        $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index);
+        $out = ApiUtils::formatLink($this->bookmarkService->get($args['id']), $index);
 
         return $response->withJson($out, 200, $this->jsonStyle);
     }
@@ -117,9 +116,11 @@ class Links extends ApiController
     public function postLink($request, $response)
     {
         $data = $request->getParsedBody();
-        $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
+        $bookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
         // duplicate by URL, return 409 Conflict
-        if (! empty($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) {
+        if (! empty($bookmark->getUrl())
+            && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl()))
+        ) {
             return $response->withJson(
                 ApiUtils::formatLink($dup, index_url($this->ci['environment'])),
                 409,
@@ -127,23 +128,9 @@ class Links extends ApiController
             );
         }
 
-        $link['id'] = $this->linkDb->getNextId();
-        $link['shorturl'] = link_small_hash($link['created'], $link['id']);
-
-        // note: general relative URL
-        if (empty($link['url'])) {
-            $link['url'] = '?' . $link['shorturl'];
-        }
-
-        if (empty($link['title'])) {
-            $link['title'] = $link['url'];
-        }
-
-        $this->linkDb[$link['id']] = $link;
-        $this->linkDb->save($this->conf->get('resource.page_cache'));
-        $this->history->addLink($link);
-        $out = ApiUtils::formatLink($link, index_url($this->ci['environment']));
-        $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $link['id']]);
+        $this->bookmarkService->add($bookmark);
+        $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
+        $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]);
         return $response->withAddedHeader('Location', $redirect)
                         ->withJson($out, 201, $this->jsonStyle);
     }
@@ -161,18 +148,18 @@ class Links extends ApiController
      */
     public function putLink($request, $response, $args)
     {
-        if (! isset($this->linkDb[$args['id']])) {
+        if (! $this->bookmarkService->exists($args['id'])) {
             throw new ApiLinkNotFoundException();
         }
 
         $index = index_url($this->ci['environment']);
         $data = $request->getParsedBody();
 
-        $requestLink = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
+        $requestBookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
         // duplicate URL on a different link, return 409 Conflict
-        if (! empty($requestLink['url'])
-            && ! empty($dup = $this->linkDb->getLinkFromUrl($requestLink['url']))
-            && $dup['id'] != $args['id']
+        if (! empty($requestBookmark->getUrl())
+            && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
+            && $dup->getId() != $args['id']
         ) {
             return $response->withJson(
                 ApiUtils::formatLink($dup, $index),
@@ -181,13 +168,11 @@ class Links extends ApiController
             );
         }
 
-        $responseLink = $this->linkDb[$args['id']];
-        $responseLink = ApiUtils::updateLink($responseLink, $requestLink);
-        $this->linkDb[$responseLink['id']] = $responseLink;
-        $this->linkDb->save($this->conf->get('resource.page_cache'));
-        $this->history->updateLink($responseLink);
+        $responseBookmark = $this->bookmarkService->get($args['id']);
+        $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark);
+        $this->bookmarkService->set($responseBookmark);
 
-        $out = ApiUtils::formatLink($responseLink, $index);
+        $out = ApiUtils::formatLink($responseBookmark, $index);
         return $response->withJson($out, 200, $this->jsonStyle);
     }
 
@@ -204,13 +189,11 @@ class Links extends ApiController
      */
     public function deleteLink($request, $response, $args)
     {
-        if (! isset($this->linkDb[$args['id']])) {
+        if (! $this->bookmarkService->exists($args['id'])) {
             throw new ApiLinkNotFoundException();
         }
-        $link = $this->linkDb[$args['id']];
-        unset($this->linkDb[(int) $args['id']]);
-        $this->linkDb->save($this->conf->get('resource.page_cache'));
-        $this->history->deleteLink($link);
+        $bookmark = $this->bookmarkService->get($args['id']);
+        $this->bookmarkService->remove($bookmark);
 
         return $response->withStatus(204);
     }
index 82f3ef746dde8cd88ead34ac02c02f0b037e696e..e60e00a7058365d8adc8d850601ec41b5c5b4a08 100644 (file)
@@ -5,6 +5,7 @@ namespace Shaarli\Api\Controllers;
 use Shaarli\Api\ApiUtils;
 use Shaarli\Api\Exceptions\ApiBadParametersException;
 use Shaarli\Api\Exceptions\ApiTagNotFoundException;
+use Shaarli\Bookmark\BookmarkFilter;
 use Slim\Http\Request;
 use Slim\Http\Response;
 
@@ -18,7 +19,7 @@ use Slim\Http\Response;
 class Tags extends ApiController
 {
     /**
-     * @var int Number of links returned if no limit is provided.
+     * @var int Number of bookmarks returned if no limit is provided.
      */
     public static $DEFAULT_LIMIT = 'all';
 
@@ -35,7 +36,7 @@ class Tags extends ApiController
     public function getTags($request, $response)
     {
         $visibility = $request->getParam('visibility');
-        $tags = $this->linkDb->linksCountPerTag([], $visibility);
+        $tags = $this->bookmarkService->bookmarksCountPerTag([], $visibility);
 
         // Return tags from the {offset}th tag, starting from 0.
         $offset = $request->getParam('offset');
@@ -47,7 +48,7 @@ class Tags extends ApiController
             return $response->withJson([], 200, $this->jsonStyle);
         }
 
-        // limit parameter is either a number of links or 'all' for everything.
+        // limit parameter is either a number of bookmarks or 'all' for everything.
         $limit = $request->getParam('limit');
         if (empty($limit)) {
             $limit = self::$DEFAULT_LIMIT;
@@ -87,7 +88,7 @@ class Tags extends ApiController
      */
     public function getTag($request, $response, $args)
     {
-        $tags = $this->linkDb->linksCountPerTag();
+        $tags = $this->bookmarkService->bookmarksCountPerTag();
         if (!isset($tags[$args['tagName']])) {
             throw new ApiTagNotFoundException();
         }
@@ -111,7 +112,7 @@ class Tags extends ApiController
      */
     public function putTag($request, $response, $args)
     {
-        $tags = $this->linkDb->linksCountPerTag();
+        $tags = $this->bookmarkService->bookmarksCountPerTag();
         if (! isset($tags[$args['tagName']])) {
             throw new ApiTagNotFoundException();
         }
@@ -121,13 +122,19 @@ class Tags extends ApiController
             throw new ApiBadParametersException('New tag name is required in the request body');
         }
 
-        $updated = $this->linkDb->renameTag($args['tagName'], $data['name']);
-        $this->linkDb->save($this->conf->get('resource.page_cache'));
-        foreach ($updated as $link) {
-            $this->history->updateLink($link);
+        $bookmarks = $this->bookmarkService->search(
+            ['searchtags' => $args['tagName']],
+            BookmarkFilter::$ALL,
+            true
+        );
+        foreach ($bookmarks as $bookmark) {
+            $bookmark->renameTag($args['tagName'], $data['name']);
+            $this->bookmarkService->set($bookmark, false);
+            $this->history->updateLink($bookmark);
         }
+        $this->bookmarkService->save();
 
-        $tags = $this->linkDb->linksCountPerTag();
+        $tags = $this->bookmarkService->bookmarksCountPerTag();
         $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]);
         return $response->withJson($out, 200, $this->jsonStyle);
     }
@@ -145,15 +152,22 @@ class Tags extends ApiController
      */
     public function deleteTag($request, $response, $args)
     {
-        $tags = $this->linkDb->linksCountPerTag();
+        $tags = $this->bookmarkService->bookmarksCountPerTag();
         if (! isset($tags[$args['tagName']])) {
             throw new ApiTagNotFoundException();
         }
-        $updated = $this->linkDb->renameTag($args['tagName'], null);
-        $this->linkDb->save($this->conf->get('resource.page_cache'));
-        foreach ($updated as $link) {
-            $this->history->updateLink($link);
+
+        $bookmarks = $this->bookmarkService->search(
+            ['searchtags' => $args['tagName']],
+            BookmarkFilter::$ALL,
+            true
+        );
+        foreach ($bookmarks as $bookmark) {
+            $bookmark->deleteTag($args['tagName']);
+            $this->bookmarkService->set($bookmark, false);
+            $this->history->updateLink($bookmark);
         }
+        $this->bookmarkService->save();
 
         return $response->withStatus(204);
     }
index b427c91a588277f51b0a59b7ea325ebdb98f8639..d87d43b41ae41a3753aff732aa3989636728c4b4 100644 (file)
@@ -118,7 +118,7 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
         $realOffset = $this->getBookmarkOffset($offset);
         $url = $this->bookmarks[$realOffset]->getUrl();
         unset($this->urls[$url]);
-        unset($this->ids[$realOffset]);
+        unset($this->ids[$offset]);
         unset($this->bookmarks[$realOffset]);
     }
 
index c95e6800b745624ed6759acdcd7eaa0c538368aa..e45bb4c391a21d1c5d14ec00007bf830788ba144 100644 (file)
@@ -389,6 +389,8 @@ class ConfigManager
         $this->setEmpty('translation.extensions', []);
 
         $this->setEmpty('plugins', array());
+
+        $this->setEmpty('formatter', 'markdown');
     }
 
     /**
index 957c827339515af83acddcb1245d258b59dad9b0..40bd4f153393553bfda8803a3b81e739bb9e155b 100644 (file)
@@ -2,6 +2,9 @@
 namespace Shaarli\Feed;
 
 use DateTime;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\BookmarkServiceInterface;
+use Shaarli\Formatter\BookmarkFormatter;
 
 /**
  * FeedBuilder class.
@@ -26,15 +29,20 @@ class FeedBuilder
     public static $DEFAULT_LANGUAGE = 'en-en';
 
     /**
-     * @var int Number of links to display in a feed by default.
+     * @var int Number of bookmarks to display in a feed by default.
      */
     public static $DEFAULT_NB_LINKS = 50;
 
     /**
-     * @var \Shaarli\Bookmark\LinkDB instance.
+     * @var BookmarkServiceInterface instance.
      */
     protected $linkDB;
 
+    /**
+     * @var BookmarkFormatter instance.
+     */
+    protected $formatter;
+
     /**
      * @var string RSS or ATOM feed.
      */
@@ -56,7 +64,7 @@ class FeedBuilder
     protected $isLoggedIn;
 
     /**
-     * @var boolean Use permalinks instead of direct links if true.
+     * @var boolean Use permalinks instead of direct bookmarks if true.
      */
     protected $usePermalinks;
 
@@ -78,16 +86,17 @@ class FeedBuilder
     /**
      * Feed constructor.
      *
-     * @param \Shaarli\Bookmark\LinkDB $linkDB     LinkDB instance.
+     * @param BookmarkServiceInterface $linkDB     LinkDB instance.
+     * @param BookmarkFormatter        $formatter  instance.
      * @param string                   $feedType   Type of feed.
      * @param array                    $serverInfo $_SERVER.
      * @param array                    $userInput  $_GET.
-     * @param boolean                  $isLoggedIn True if the user is currently logged in,
-     *                                             false otherwise.
+     * @param boolean                  $isLoggedIn True if the user is currently logged in, false otherwise.
      */
-    public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLoggedIn)
+    public function __construct($linkDB, $formatter, $feedType, $serverInfo, $userInput, $isLoggedIn)
     {
         $this->linkDB = $linkDB;
+        $this->formatter = $formatter;
         $this->feedType = $feedType;
         $this->serverInfo = $serverInfo;
         $this->userInput = $userInput;
@@ -101,13 +110,13 @@ class FeedBuilder
      */
     public function buildData()
     {
-        // Search for untagged links
+        // Search for untagged bookmarks
         if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) {
             $this->userInput['searchtags'] = false;
         }
 
         // Optionally filter the results:
-        $linksToDisplay = $this->linkDB->filterSearch($this->userInput);
+        $linksToDisplay = $this->linkDB->search($this->userInput);
 
         $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay));
 
@@ -118,6 +127,7 @@ class FeedBuilder
         }
 
         $pageaddr = escape(index_url($this->serverInfo));
+        $this->formatter->addContextData('index_url', $pageaddr);
         $linkDisplayed = array();
         for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
             $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr);
@@ -139,54 +149,44 @@ class FeedBuilder
     /**
      * Build a feed item (one per shaare).
      *
-     * @param array  $link     Single link array extracted from LinkDB.
-     * @param string $pageaddr Index URL.
+     * @param Bookmark $link     Single link array extracted from LinkDB.
+     * @param string   $pageaddr Index URL.
      *
      * @return array Link array with feed attributes.
      */
     protected function buildItem($link, $pageaddr)
     {
-        $link['guid'] = $pageaddr . '?' . $link['shorturl'];
-        // Prepend the root URL for notes
-        if (is_note($link['url'])) {
-            $link['url'] = $pageaddr . $link['url'];
-        }
+        $data = $this->formatter->format($link);
+        $data['guid'] = $pageaddr . '?' . $data['shorturl'];
         if ($this->usePermalinks === true) {
-            $permalink = '<a href="' . $link['url'] . '" title="' . t('Direct link') . '">' . t('Direct link') . '</a>';
+            $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
         } else {
-            $permalink = '<a href="' . $link['guid'] . '" title="' . t('Permalink') . '">' . t('Permalink') . '</a>';
+            $permalink = '<a href="'. $data['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
         }
-        $link['description'] = format_description($link['description'], $pageaddr);
-        $link['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
+        $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
 
-        $pubDate = $link['created'];
-        $link['pub_iso_date'] = $this->getIsoDate($pubDate);
+        $data['pub_iso_date'] = $this->getIsoDate($data['created']);
 
         // atom:entry elements MUST contain exactly one atom:updated element.
-        if (!empty($link['updated'])) {
-            $upDate = $link['updated'];
-            $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
+        if (!empty($link->getUpdated())) {
+            $data['up_iso_date'] = $this->getIsoDate($data['updated'], DateTime::ATOM);
         } else {
-            $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);
+            $data['up_iso_date'] = $this->getIsoDate($data['created'], DateTime::ATOM);
         }
 
         // Save the more recent item.
-        if (empty($this->latestDate) || $this->latestDate < $pubDate) {
-            $this->latestDate = $pubDate;
+        if (empty($this->latestDate) || $this->latestDate < $data['created']) {
+            $this->latestDate = $data['created'];
         }
-        if (!empty($upDate) && $this->latestDate < $upDate) {
-            $this->latestDate = $upDate;
+        if (!empty($data['updated']) && $this->latestDate < $data['updated']) {
+            $this->latestDate = $data['updated'];
         }
 
-        $taglist = array_filter(explode(' ', $link['tags']), 'strlen');
-        uasort($taglist, 'strcasecmp');
-        $link['taglist'] = $taglist;
-
-        return $link;
+        return $data;
     }
 
     /**
-     * Set this to true to use permalinks instead of direct links.
+     * Set this to true to use permalinks instead of direct bookmarks.
      *
      * @param boolean $usePermalinks true to force permalinks.
      */
@@ -273,11 +273,11 @@ class FeedBuilder
      * Returns the number of link to display according to 'nb' user input parameter.
      *
      * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
-     * If 'nb' is set to 'all', display all filtered links (max parameter).
+     * If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
      *
-     * @param int $max maximum number of links to display.
+     * @param int $max maximum number of bookmarks to display.
      *
-     * @return int number of links to display.
+     * @return int number of bookmarks to display.
      */
     public function getNbLinks($max)
     {
index f60c61f46dcdc811815f68406b98929e43d03483..7797bfbf33433c308a6c7a396fb3f00d0083f267 100644 (file)
@@ -57,6 +57,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
         $processedDescription = $bookmark->getDescription();
         $processedDescription = $this->filterProtocols($processedDescription);
         $processedDescription = $this->formatHashTags($processedDescription);
+        $processedDescription = $this->reverseEscapedHtml($processedDescription);
         $processedDescription = $this->parsedown
             ->setMarkupEscaped($this->escape)
             ->setBreaksEnabled(true)
@@ -195,4 +196,9 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
         );
         return $description;
     }
+
+    protected function reverseEscapedHtml($description)
+    {
+        return unescape($description);
+    }
 }
index 28665941507366a364930f4adcdde702cf983a25..d64eef7f802d05a3685a7a9f294287a58a0298b6 100644 (file)
@@ -7,8 +7,10 @@ use DateTimeZone;
 use Exception;
 use Katzgrau\KLogger\Logger;
 use Psr\Log\LogLevel;
-use Shaarli\Bookmark\LinkDB;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\BookmarkServiceInterface;
 use Shaarli\Config\ConfigManager;
+use Shaarli\Formatter\BookmarkFormatter;
 use Shaarli\History;
 use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser;
 
@@ -20,41 +22,39 @@ class NetscapeBookmarkUtils
 {
 
     /**
-     * Filters links and adds Netscape-formatted fields
+     * Filters bookmarks and adds Netscape-formatted fields
      *
      * Added fields:
      * - timestamp  link addition date, using the Unix epoch format
      * - taglist    comma-separated tag list
      *
-     * @param LinkDB $linkDb         Link datastore
-     * @param string $selection      Which links to export: (all|private|public)
-     * @param bool   $prependNoteUrl Prepend note permalinks with the server's URL
-     * @param string $indexUrl       Absolute URL of the Shaarli index page
+     * @param BookmarkServiceInterface $bookmarkService Link datastore
+     * @param BookmarkFormatter        $formatter       instance
+     * @param string                   $selection       Which bookmarks to export: (all|private|public)
+     * @param bool                     $prependNoteUrl  Prepend note permalinks with the server's URL
+     * @param string                   $indexUrl        Absolute URL of the Shaarli index page
      *
-     * @throws Exception Invalid export selection
+     * @return array The bookmarks to be exported, with additional fields
+     *@throws Exception Invalid export selection
      *
-     * @return array The links to be exported, with additional fields
      */
-    public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $indexUrl)
-    {
+    public static function filterAndFormat(
+        $bookmarkService,
+        $formatter,
+        $selection,
+        $prependNoteUrl,
+        $indexUrl
+    ) {
         // see tpl/export.html for possible values
         if (!in_array($selection, array('all', 'public', 'private'))) {
             throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
         }
 
         $bookmarkLinks = array();
-        foreach ($linkDb as $link) {
-            if ($link['private'] != 0 && $selection == 'public') {
-                continue;
-            }
-            if ($link['private'] == 0 && $selection == 'private') {
-                continue;
-            }
-            $date = $link['created'];
-            $link['timestamp'] = $date->getTimestamp();
-            $link['taglist'] = str_replace(' ', ',', $link['tags']);
-
-            if (is_note($link['url']) && $prependNoteUrl) {
+        foreach ($bookmarkService->search([], $selection) as $bookmark) {
+            $link = $formatter->format($bookmark);
+            $link['taglist'] = implode(',', $bookmark->getTags());
+            if ($bookmark->isNote() && $prependNoteUrl) {
                 $link['url'] = $indexUrl . $link['url'];
             }
 
@@ -69,9 +69,9 @@ class NetscapeBookmarkUtils
      *
      * @param string $filename       name of the file to import
      * @param int    $filesize       size of the file to import
-     * @param int    $importCount    how many links were imported
-     * @param int    $overwriteCount how many links were overwritten
-     * @param int    $skipCount      how many links were skipped
+     * @param int    $importCount    how many bookmarks were imported
+     * @param int    $overwriteCount how many bookmarks were overwritten
+     * @param int    $skipCount      how many bookmarks were skipped
      * @param int    $duration       how many seconds did the import take
      *
      * @return string Summary of the bookmark import status
@@ -91,7 +91,7 @@ class NetscapeBookmarkUtils
             $status .= vsprintf(
                 t(
                     'was successfully processed in %d seconds: '
-                    . '%d links imported, %d links overwritten, %d links skipped.'
+                    . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
                 ),
                 [$duration, $importCount, $overwriteCount, $skipCount]
             );
@@ -102,15 +102,15 @@ class NetscapeBookmarkUtils
     /**
      * Imports Web bookmarks from an uploaded Netscape bookmark dump
      *
-     * @param array         $post    Server $_POST parameters
-     * @param array         $files   Server $_FILES parameters
-     * @param LinkDB        $linkDb  Loaded LinkDB instance
-     * @param ConfigManager $conf    instance
-     * @param History       $history History instance
+     * @param array                    $post            Server $_POST parameters
+     * @param array                    $files           Server $_FILES parameters
+     * @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance
+     * @param ConfigManager            $conf            instance
+     * @param History                  $history         History instance
      *
      * @return string Summary of the bookmark import status
      */
-    public static function import($post, $files, $linkDb, $conf, $history)
+    public static function import($post, $files, $bookmarkService, $conf, $history)
     {
         $start = time();
         $filename = $files['filetoupload']['name'];
@@ -121,10 +121,10 @@ class NetscapeBookmarkUtils
             return self::importStatus($filename, $filesize);
         }
 
-        // Overwrite existing links?
+        // Overwrite existing bookmarks?
         $overwrite = !empty($post['overwrite']);
 
-        // Add tags to all imported links?
+        // Add tags to all imported bookmarks?
         if (empty($post['default_tags'])) {
             $defaultTags = array();
         } else {
@@ -134,7 +134,7 @@ class NetscapeBookmarkUtils
             );
         }
 
-        // links are imported as public by default
+        // bookmarks are imported as public by default
         $defaultPrivacy = 0;
 
         $parser = new NetscapeBookmarkParser(
@@ -164,22 +164,18 @@ class NetscapeBookmarkUtils
                 // use value from the imported file
                 $private = $bkm['pub'] == '1' ? 0 : 1;
             } elseif ($post['privacy'] == 'private') {
-                // all imported links are private
+                // all imported bookmarks are private
                 $private = 1;
             } elseif ($post['privacy'] == 'public') {
-                // all imported links are public
+                // all imported bookmarks are public
                 $private = 0;
             }
 
-            $newLink = array(
-                'title' => $bkm['title'],
-                'url' => $bkm['uri'],
-                'description' => $bkm['note'],
-                'private' => $private,
-                'tags' => $bkm['tags']
-            );
-
-            $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
+            $link = $bookmarkService->findByUrl($bkm['uri']);
+            $existingLink = $link !== null;
+            if (! $existingLink) {
+                $link = new Bookmark();
+            }
 
             if ($existingLink !== false) {
                 if ($overwrite === false) {
@@ -188,28 +184,25 @@ class NetscapeBookmarkUtils
                     continue;
                 }
 
-                // Overwrite an existing link, keep its date
-                $newLink['id'] = $existingLink['id'];
-                $newLink['created'] = $existingLink['created'];
-                $newLink['updated'] = new DateTime();
-                $newLink['shorturl'] = $existingLink['shorturl'];
-                $linkDb[$existingLink['id']] = $newLink;
-                $importCount++;
+                $link->setUpdated(new DateTime());
                 $overwriteCount++;
-                continue;
+            } else {
+                $newLinkDate = new DateTime('@' . strval($bkm['time']));
+                $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
+                $link->setCreated($newLinkDate);
             }
 
-            // Add a new link - @ used for UNIX timestamps
-            $newLinkDate = new DateTime('@' . strval($bkm['time']));
-            $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
-            $newLink['created'] = $newLinkDate;
-            $newLink['id'] = $linkDb->getNextId();
-            $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
-            $linkDb[$newLink['id']] = $newLink;
+            $link->setTitle($bkm['title']);
+            $link->setUrl($bkm['uri'], $conf->get('security.allowed_protocols'));
+            $link->setDescription($bkm['note']);
+            $link->setPrivate($private);
+            $link->setTagsString($bkm['tags']);
+
+            $bookmarkService->addOrSet($link, false);
             $importCount++;
         }
 
-        $linkDb->save($conf->get('resource.page_cache'));
+        $bookmarkService->save();
         $history->importLinks();
 
         $duration = time() - $start;
index 3f86fc2681010c99b724ffbcef2cc758b0240418..65e85aaf4918c1f29c85af30f9560dd596184cef 100644 (file)
@@ -5,7 +5,7 @@ namespace Shaarli\Render;
 use Exception;
 use RainTPL;
 use Shaarli\ApplicationUtils;
-use Shaarli\Bookmark\LinkDB;
+use Shaarli\Bookmark\BookmarkServiceInterface;
 use Shaarli\Config\ConfigManager;
 use Shaarli\Thumbnailer;
 
@@ -34,9 +34,9 @@ class PageBuilder
     protected $session;
 
     /**
-     * @var LinkDB $linkDB instance.
+     * @var BookmarkServiceInterface $bookmarkService instance.
      */
-    protected $linkDB;
+    protected $bookmarkService;
 
     /**
      * @var null|string XSRF token
@@ -52,18 +52,18 @@ class PageBuilder
      * PageBuilder constructor.
      * $tpl is initialized at false for lazy loading.
      *
-     * @param ConfigManager $conf       Configuration Manager instance (reference).
-     * @param array         $session    $_SESSION array
-     * @param LinkDB        $linkDB     instance.
-     * @param string        $token      Session token
-     * @param bool          $isLoggedIn
+     * @param ConfigManager            $conf    Configuration Manager instance (reference).
+     * @param array                    $session $_SESSION array
+     * @param BookmarkServiceInterface $linkDB  instance.
+     * @param string                   $token   Session token
+     * @param bool                     $isLoggedIn
      */
     public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
     {
         $this->tpl = false;
         $this->conf = $conf;
         $this->session = $session;
-        $this->linkDB = $linkDB;
+        $this->bookmarkService = $linkDB;
         $this->token = $token;
         $this->isLoggedIn = $isLoggedIn;
     }
@@ -125,8 +125,8 @@ class PageBuilder
 
         $this->tpl->assign('language', $this->conf->get('translation.language'));
 
-        if ($this->linkDB !== null) {
-            $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
+        if ($this->bookmarkService !== null) {
+            $this->tpl->assign('tags', $this->bookmarkService->bookmarksCountPerTag());
         }
 
         $this->tpl->assign(
@@ -141,6 +141,8 @@ class PageBuilder
             unset($_SESSION['warnings']);
         }
 
+        $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
+
         // To be removed with a proper theme configuration.
         $this->tpl->assign('conf', $this->conf);
     }
index beb9ea9b7517613cd9c2ca01c884ba239f250355..95654d81da96c0d23c2d26424c47983b39c25961 100644 (file)
@@ -2,25 +2,14 @@
 
 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\ConfigManager;
-use Shaarli\Config\ConfigPhp;
-use Shaarli\Exceptions\IOException;
-use Shaarli\Thumbnailer;
+use Shaarli\Bookmark\BookmarkServiceInterface;
 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.
+ * Update methods are ran only once, and the stored in a TXT file.
  */
 class Updater
 {
@@ -30,9 +19,9 @@ class Updater
     protected $doneUpdates;
 
     /**
-     * @var LinkDB instance.
+     * @var BookmarkServiceInterface instance.
      */
-    protected $linkDB;
+    protected $linkServices;
 
     /**
      * @var ConfigManager $conf Configuration Manager instance.
@@ -45,36 +34,27 @@ class Updater
     protected $isLoggedIn;
 
     /**
-     * @var array $_SESSION
-     */
-    protected $session;
-
-    /**
-     * @var ReflectionMethod[] List of current class methods.
+     * @var \ReflectionMethod[] List of current class methods.
      */
     protected $methods;
 
     /**
      * Object constructor.
      *
-     * @param array         $doneUpdates Updates which are already done.
-     * @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
+     * @param array                    $doneUpdates Updates which are already done.
+     * @param BookmarkServiceInterface $linkDB      LinksService instance.
+     * @param ConfigManager            $conf        Configuration Manager instance.
+     * @param boolean                  $isLoggedIn  True if the user is logged in.
      */
-    public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
+    public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
     {
         $this->doneUpdates = $doneUpdates;
-        $this->linkDB = $linkDB;
+        $this->linkServices = $linkDB;
         $this->conf = $conf;
         $this->isLoggedIn = $isLoggedIn;
-        $this->session = &$session;
 
         // Retrieve all update methods.
-        $class = new ReflectionClass($this);
+        $class = new \ReflectionClass($this);
         $this->methods = $class->getMethods();
     }
 
@@ -96,12 +76,12 @@ class Updater
         }
 
         if ($this->methods === null) {
-            throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
+            throw new UpdaterException('Couldn\'t retrieve LegacyUpdater 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;
@@ -114,7 +94,7 @@ class Updater
                 if ($res === true) {
                     $updatesRan[] = $method->getName();
                 }
-            } catch (Exception $e) {
+            } catch (\Exception $e) {
                 throw new UpdaterException($method, $e);
             }
         }
@@ -131,432 +111,4 @@ class Updater
     {
         return $this->doneUpdates;
     }
-
-    /**
-     * Move deprecated options.php to config.php.
-     *
-     * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
-     *    options.php is not supported anymore.
-     */
-    public function updateMethodMergeDeprecatedConfigFile()
-    {
-        if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
-            include $this->conf->get('resource.data_dir') . '/options.php';
-
-            // Load GLOBALS into config
-            $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
-            $allowedKeys[] = 'config';
-            foreach ($GLOBALS as $key => $value) {
-                if (in_array($key, $allowedKeys)) {
-                    $this->conf->set($key, $value);
-                }
-            }
-            $this->conf->write($this->isLoggedIn);
-            unlink($this->conf->get('resource.data_dir') . '/options.php');
-        }
-
-        return true;
-    }
-
-    /**
-     * Move old configuration in PHP to the new config system in JSON format.
-     *
-     * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
-     * It will also convert legacy setting keys to the new ones.
-     */
-    public function updateMethodConfigToJson()
-    {
-        // JSON config already exists, nothing to do.
-        if ($this->conf->getConfigIO() instanceof ConfigJson) {
-            return true;
-        }
-
-        $configPhp = new ConfigPhp();
-        $configJson = new ConfigJson();
-        $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
-        rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
-        $this->conf->setConfigIO($configJson);
-        $this->conf->reload();
-
-        $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
-        foreach (ConfigPhp::$ROOT_KEYS as $key) {
-            $this->conf->set($legacyMap[$key], $oldConfig[$key]);
-        }
-
-        // Set sub config keys (config and plugins)
-        $subConfig = array('config', 'plugins');
-        foreach ($subConfig as $sub) {
-            foreach ($oldConfig[$sub] as $key => $value) {
-                if (isset($legacyMap[$sub . '.' . $key])) {
-                    $configKey = $legacyMap[$sub . '.' . $key];
-                } else {
-                    $configKey = $sub . '.' . $key;
-                }
-                $this->conf->set($configKey, $value);
-            }
-        }
-
-        try {
-            $this->conf->write($this->isLoggedIn);
-            return true;
-        } catch (IOException $e) {
-            error_log($e->getMessage());
-            return false;
-        }
-    }
-
-    /**
-     * Escape settings which have been manually escaped in every request in previous versions:
-     *   - general.title
-     *   - general.header_link
-     *   - redirector.url
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodEscapeUnescapedConfig()
-    {
-        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->write($this->isLoggedIn);
-        } catch (Exception $e) {
-            error_log($e->getMessage());
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Update the database to use the new ID system, which replaces linkdate primary keys.
-     * Also, creation and update dates are now DateTime objects (done by LinkDB).
-     *
-     * Since this update is very sensitve (changing the whole database), the datastore will be
-     * automatically backed up into the file datastore.<datetime>.php.
-     *
-     * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
-     * which will be saved by this method.
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodDatastoreIds()
-    {
-        // up to date database
-        if (isset($this->linkDB[0])) {
-            return true;
-        }
-
-        $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
-        copy($this->conf->get('resource.datastore'), $save);
-
-        $links = array();
-        foreach ($this->linkDB as $offset => $value) {
-            $links[] = $value;
-            unset($this->linkDB[$offset]);
-        }
-        $links = array_reverse($links);
-        $cpt = 0;
-        foreach ($links as $l) {
-            unset($l['linkdate']);
-            $l['id'] = $cpt;
-            $this->linkDB[$cpt++] = $l;
-        }
-
-        $this->linkDB->save($this->conf->get('resource.page_cache'));
-        $this->linkDB->reorder();
-
-        return true;
-    }
-
-    /**
-     * Rename tags starting with a '-' to work with tag exclusion search.
-     */
-    public function updateMethodRenameDashTags()
-    {
-        $linklist = $this->linkDB->filterSearch();
-        foreach ($linklist as $key => $link) {
-            $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
-            $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
-            $this->linkDB[$key] = $link;
-        }
-        $this->linkDB->save($this->conf->get('resource.page_cache'));
-        return true;
-    }
-
-    /**
-     * Initialize API settings:
-     *   - api.enabled: true
-     *   - api.secret: generated secret
-     */
-    public function updateMethodApiSettings()
-    {
-        if ($this->conf->exists('api.secret')) {
-            return true;
-        }
-
-        $this->conf->set('api.enabled', true);
-        $this->conf->set(
-            'api.secret',
-            generate_api_secret(
-                $this->conf->get('credentials.login'),
-                $this->conf->get('credentials.salt')
-            )
-        );
-        $this->conf->write($this->isLoggedIn);
-        return true;
-    }
-
-    /**
-     * New setting: theme name. If the default theme is used, nothing to do.
-     *
-     * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
-     * and the current theme is set as default in the theme setting.
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodDefaultTheme()
-    {
-        // raintpl_tpl isn't the root template directory anymore.
-        // 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)) {
-            return true;
-        }
-
-        $parent = dirname($tplDir);
-        $this->conf->set('resource.raintpl_tpl', $parent);
-        $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
-        $this->conf->write($this->isLoggedIn);
-
-        // Dependency injection gore
-        RainTPL::$tpl_dir = $tplDir;
-
-        return true;
-    }
-
-    /**
-     * Move the file to inc/user.css to data/user.css.
-     *
-     * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodMoveUserCss()
-    {
-        if (!is_file('inc/user.css')) {
-            return true;
-        }
-
-        return rename('inc/user.css', 'data/user.css');
-    }
-
-    /**
-     * * `markdown_escape` is a new setting, set to true as default.
-     *
-     * If the markdown plugin was already enabled, escaping is disabled to avoid
-     * breaking existing entries.
-     */
-    public function updateMethodEscapeMarkdown()
-    {
-        if ($this->conf->exists('security.markdown_escape')) {
-            return true;
-        }
-
-        if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
-            $this->conf->set('security.markdown_escape', false);
-        } else {
-            $this->conf->set('security.markdown_escape', true);
-        }
-        $this->conf->write($this->isLoggedIn);
-
-        return true;
-    }
-
-    /**
-     * Add 'http://' to Piwik URL the setting is set.
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodPiwikUrl()
-    {
-        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->write($this->isLoggedIn);
-
-        return true;
-    }
-
-    /**
-     * Use ATOM feed as default.
-     */
-    public function updateMethodAtomDefault()
-    {
-        if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
-            return true;
-        }
-
-        $this->conf->set('feed.show_atom', true);
-        $this->conf->write($this->isLoggedIn);
-
-        return true;
-    }
-
-    /**
-     * Update updates.check_updates_branch setting.
-     *
-     * If the current major version digit matches the latest branch
-     * major version digit, we set the branch to `latest`,
-     * otherwise we'll check updates on the `stable` branch.
-     *
-     * No update required for the dev version.
-     *
-     * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
-     *
-     * FIXME! This needs to be removed when we switch to first digit major version
-     *        instead of the second one since the versionning process will change.
-     */
-    public function updateMethodCheckUpdateRemoteBranch()
-    {
-        if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
-            return true;
-        }
-
-        // Get latest branch major version digit
-        $latestVersion = ApplicationUtils::getLatestGitVersionCode(
-            'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
-            5
-        );
-        if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
-            return false;
-        }
-        $latestMajor = $matches[1];
-
-        // Get current major version digit
-        preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
-        $currentMajor = $matches[1];
-
-        if ($currentMajor === $latestMajor) {
-            $branch = 'latest';
-        } else {
-            $branch = 'stable';
-        }
-        $this->conf->set('updates.check_updates_branch', $branch);
-        $this->conf->write($this->isLoggedIn);
-        return true;
-    }
-
-    /**
-     * Reset history store file due to date format change.
-     */
-    public function updateMethodResetHistoryFile()
-    {
-        if (is_file($this->conf->get('resource.history'))) {
-            unlink($this->conf->get('resource.history'));
-        }
-        return true;
-    }
-
-    /**
-     * Save the datastore -> the link order is now applied when links are saved.
-     */
-    public function updateMethodReorderDatastore()
-    {
-        $this->linkDB->save($this->conf->get('resource.page_cache'));
-        return true;
-    }
-
-    /**
-     * Change privateonly session key to visibility.
-     */
-    public function updateMethodVisibilitySession()
-    {
-        if (isset($_SESSION['privateonly'])) {
-            unset($_SESSION['privateonly']);
-            $_SESSION['visibility'] = 'private';
-        }
-        return true;
-    }
-
-    /**
-     * Add download size and timeout to the configuration file
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodDownloadSizeAndTimeoutConf()
-    {
-        if ($this->conf->exists('general.download_max_size')
-            && $this->conf->exists('general.download_timeout')
-        ) {
-            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_timeout')) {
-            $this->conf->set('general.download_timeout', 30);
-        }
-
-        $this->conf->write($this->isLoggedIn);
-        return true;
-    }
-
-    /**
-     * * Move thumbnails management to WebThumbnailer, coming with new settings.
-     */
-    public function updateMethodWebThumbnailer()
-    {
-        if ($this->conf->exists('thumbnails.mode')) {
-            return true;
-        }
-
-        $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
-        $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
-        $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 or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
-            );
-        }
-
-        return true;
-    }
-
-    /**
-     * Set sticky = false on all links
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodSetSticky()
-    {
-        foreach ($this->linkDB as $key => $link) {
-            if (isset($link['sticky'])) {
-                return true;
-            }
-            $link['sticky'] = false;
-            $this->linkDB[$key] = $link;
-        }
-
-        $this->linkDB->save($this->conf->get('resource.page_cache'));
-
-        return true;
-    }
-
-    /**
-     * Remove redirector settings.
-     */
-    public function updateMethodRemoveRedirector()
-    {
-        $this->conf->remove('redirector');
-        $this->conf->write(true);
-        return true;
-    }
 }
index 34d4f422c26a59cc96e2b3eb982da7a77ae1445e..828a49fc02ae5909e6ab4c5c3502421af581ec81 100644 (file)
@@ -1,39 +1,44 @@
 <?php
 
-/**
- * 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)
+namespace Shaarli\Updater;
+
+class UpdaterUtils
 {
-    if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
-        $content = file_get_contents($updatesFilepath);
-        if (! empty($content)) {
-            return explode(';', $content);
+    /**
+     * Read the updates file, and return already done updates.
+     *
+     * @param string $updatesFilepath Updates file path.
+     *
+     * @return array Already done update methods.
+     */
+    public static function read_updates_file($updatesFilepath)
+    {
+        if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
+            $content = file_get_contents($updatesFilepath);
+            if (! empty($content)) {
+                return explode(';', $content);
+            }
         }
+        return array();
     }
-    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.'));
-    }
+    /**
    * Write updates file.
    *
    * @param string $updatesFilepath Updates file path.
    * @param array  $updates         Updates array to write.
    *
    * @throws \Exception Couldn't write version number.
    */
+    public static function write_updates_file($updatesFilepath, $updates)
+    {
+        if (empty($updatesFilepath)) {
+            throw new \Exception('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 . '.'));
+        $res = file_put_contents($updatesFilepath, implode(';', $updates));
+        if ($res === false) {
+            throw new \Exception('Unable to write updates in '. $updatesFilepath . '.');
+        }
     }
 }
similarity index 98%
rename from plugins/markdown/markdown.css
rename to assets/common/css/markdown.css
index ce19cd2ab9666eb3deddc63809b1bfc6c156cc77..f651e67e93ed6c1b39a636a9637f1926c97896b1 100644 (file)
     -webkit-hyphens: none;
     -moz-hyphens: none;
     -ms-hyphens: none;
-    hyphens: none;  
+    hyphens: none;
 }
 
 .markdown :not(pre) code {
 }
 
 /*
- Remove header links style
+ Remove header bookmarks style
  */
 #pageheader .md_help a {
     color: lightgray;
index b3aa869d2c43fbcca02db61a25414e19ac942406..0cef99dffc0b9881ec71884cd9b748d17026c238 100644 (file)
@@ -17,14 +17,6 @@ Alternatively, you can transform to JSON format (and pretty-print if you have `j
 php -r 'print(json_encode(unserialize(gzinflate(base64_decode(preg_replace("!.*/\* (.+) \*/.*!", "$1", file_get_contents("data/datastore.php")))))));' | jq .
 ```
 
-### Changing the timestamp for a shaare
-
-- Look for `<input type="hidden" name="lf_linkdate" value="{$link.linkdate}">` in `tpl/editlink.tpl` (line 14)
-- Replace `type="hidden"` with `type="text"` from this line
-- A new date/time field becomes available in the edit/new link dialog.
-- You can set the timestamp manually by entering it in the format `YYYMMDD_HHMMS`.
-
-
 ### See also
 
 - [Add a new custom field to shaares (example patch)](https://gist.github.com/nodiscc/8b0194921f059d7b9ad89a581ecd482c)
index 9783539a21b3eca3fbd4c5a72c43d12e33a699b4..ae74bc7efe2476745ebb30e7a4254ff86b7968d0 100644 (file)
--- a/index.php
+++ b/index.php
@@ -35,9 +35,6 @@ ini_set('upload_max_filesize', '16M');
 
 // See all error except warnings
 error_reporting(E_ALL^E_WARNING);
-// See all errors (for debugging only)
-//error_reporting(-1);
-
 
 // 3rd-party libraries
 if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
@@ -65,11 +62,15 @@ require_once 'application/TimeZone.php';
 require_once 'application/Utils.php';
 
 use \Shaarli\ApplicationUtils;
-use \Shaarli\Bookmark\Exception\LinkNotFoundException;
-use \Shaarli\Bookmark\LinkDB;
+use Shaarli\Bookmark\BookmarkServiceInterface;
+use \Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\BookmarkFilter;
+use Shaarli\Bookmark\BookmarkFileService;
 use \Shaarli\Config\ConfigManager;
 use \Shaarli\Feed\CachedPage;
 use \Shaarli\Feed\FeedBuilder;
+use Shaarli\Formatter\FormatterFactory;
 use \Shaarli\History;
 use \Shaarli\Languages;
 use \Shaarli\Netscape\NetscapeBookmarkUtils;
@@ -81,6 +82,7 @@ use \Shaarli\Security\LoginManager;
 use \Shaarli\Security\SessionManager;
 use \Shaarli\Thumbnailer;
 use \Shaarli\Updater\Updater;
+use \Shaarli\Updater\UpdaterUtils;
 
 // Ensure the PHP version is supported
 try {
@@ -122,6 +124,17 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli']))
 }
 
 $conf = new ConfigManager();
+
+// In dev mode, throw exception on any warning
+if ($conf->get('dev.debug', false)) {
+    // See all errors (for debugging only)
+    error_reporting(-1);
+
+    set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) {
+        throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
+    });
+}
+
 $sessionManager = new SessionManager($_SESSION, $conf);
 $loginManager = new LoginManager($conf, $sessionManager);
 $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
@@ -140,7 +153,7 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
 new Languages(setlocale(LC_MESSAGES, 0), $conf);
 
 $conf->setEmpty('general.timezone', date_default_timezone_get());
-$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
+$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER)));
 RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
 RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
 
@@ -283,14 +296,15 @@ if (!isset($_SESSION['tokens'])) {
 }
 
 /**
- * Daily RSS feed: 1 RSS entry per day giving all the links on that day.
- * Gives the last 7 days (which have links).
+ * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
+ * Gives the last 7 days (which have bookmarks).
  * This RSS feed cannot be filtered.
  *
- * @param ConfigManager $conf         Configuration Manager instance
- * @param LoginManager  $loginManager LoginManager instance
+ * @param BookmarkServiceInterface $bookmarkService
+ * @param ConfigManager            $conf            Configuration Manager instance
+ * @param LoginManager             $loginManager    LoginManager instance
  */
-function showDailyRSS($conf, $loginManager)
+function showDailyRSS($bookmarkService, $conf, $loginManager)
 {
     // Cache system
     $query = $_SERVER['QUERY_STRING'];
@@ -305,28 +319,20 @@ function showDailyRSS($conf, $loginManager)
         exit;
     }
 
-    // If cached was not found (or not usable), then read the database and build the response:
-    // Read links from database (and filter private links if used it not logged in).
-    $LINKSDB = new LinkDB(
-        $conf->get('resource.datastore'),
-        $loginManager->isLoggedIn(),
-        $conf->get('privacy.hide_public_links')
-    );
-
-    /* Some Shaarlies may have very few links, so we need to look
+    /* Some Shaarlies may have very few bookmarks, so we need to look
        back in time until we have enough days ($nb_of_days).
     */
     $nb_of_days = 7; // We take 7 days.
     $today = date('Ymd');
     $days = array();
 
-    foreach ($LINKSDB as $link) {
-        $day = $link['created']->format('Ymd'); // Extract day (without time)
+    foreach ($bookmarkService->search() as $bookmark) {
+        $day = $bookmark->getCreated()->format('Ymd'); // Extract day (without time)
         if (strcmp($day, $today) < 0) {
             if (empty($days[$day])) {
                 $days[$day] = array();
             }
-            $days[$day][] = $link;
+            $days[$day][] = $bookmark;
         }
 
         if (count($days) > $nb_of_days) {
@@ -341,30 +347,38 @@ function showDailyRSS($conf, $loginManager)
     echo '<channel>';
     echo '<title>Daily - '. $conf->get('general.title') . '</title>';
     echo '<link>'. $pageaddr .'</link>';
-    echo '<description>Daily shared links</description>';
+    echo '<description>Daily shared bookmarks</description>';
     echo '<language>en-en</language>';
     echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
 
+    $factory = new FormatterFactory($conf);
+    $formatter = $factory->getFormatter();
+    $formatter->addContextData('index_url', index_url($_SERVER));
     // For each day.
-    foreach ($days as $day => $links) {
-        $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
+    /** @var Bookmark[] $bookmarks */
+    foreach ($days as $day => $bookmarks) {
+        $formattedBookmarks = [];
+        $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
         $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day);  // Absolute URL of the corresponding "Daily" page.
 
         // We pre-format some fields for proper output.
-        foreach ($links as &$link) {
-            $link['formatedDescription'] = format_description($link['description']);
-            $link['timestamp'] = $link['created']->getTimestamp();
-            if (is_note($link['url'])) {
-                $link['url'] = index_url($_SERVER) . $link['url'];  // make permalink URL absolute
+        foreach ($bookmarks as $key => $bookmark) {
+            $formattedBookmarks[$key] = $formatter->format($bookmark);
+            // This page is a bit specific, we need raw description to calculate the length
+            $formattedBookmarks[$key]['formatedDescription'] = $formattedBookmarks[$key]['description'];
+            $formattedBookmarks[$key]['description'] = $bookmark->getDescription();
+
+            if ($bookmark->isNote()) {
+                $link['url'] = index_url($_SERVER) . $bookmark->getUrl();  // make permalink URL absolute
             }
         }
 
         // Then build the HTML for this day:
-        $tpl = new RainTPL;
+        $tpl = new RainTPL();
         $tpl->assign('title', $conf->get('general.title'));
         $tpl->assign('daydate', $dayDate->getTimestamp());
         $tpl->assign('absurl', $absurl);
-        $tpl->assign('links', $links);
+        $tpl->assign('links', $formattedBookmarks);
         $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
         $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
         $tpl->assign('index_url', $pageaddr);
@@ -382,13 +396,13 @@ function showDailyRSS($conf, $loginManager)
 /**
  * Show the 'Daily' page.
  *
- * @param PageBuilder   $pageBuilder   Template engine wrapper.
- * @param LinkDB        $LINKSDB       LinkDB instance.
- * @param ConfigManager $conf          Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
- * @param LoginManager  $loginManager  Login Manager instance
+ * @param PageBuilder              $pageBuilder     Template engine wrapper.
+ * @param BookmarkServiceInterface $bookmarkService instance.
+ * @param ConfigManager            $conf            Configuration Manager instance.
+ * @param PluginManager            $pluginManager   Plugin Manager instance.
+ * @param LoginManager             $loginManager    Login Manager instance
  */
-function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
+function showDaily($pageBuilder, $bookmarkService, $conf, $pluginManager, $loginManager)
 {
     if (isset($_GET['day'])) {
         $day = $_GET['day'];
@@ -402,10 +416,10 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
         $pageBuilder->assign('dayDesc', t('Today'));
     }
 
-    $days = $LINKSDB->days();
+    $days = $bookmarkService->days();
     $i = array_search($day, $days);
     if ($i === false && count($days)) {
-        // no links for day, but at least one day with links
+        // no bookmarks for day, but at least one day with bookmarks
         $i = count($days) - 1;
         $day = $days[$i];
     }
@@ -414,29 +428,30 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
 
     if ($i !== false) {
         if ($i >= 1) {
-             $previousday=$days[$i - 1];
+             $previousday = $days[$i - 1];
         }
         if ($i < count($days) - 1) {
             $nextday = $days[$i + 1];
         }
     }
     try {
-        $linksToDisplay = $LINKSDB->filterDay($day);
+        $linksToDisplay = $bookmarkService->filterDay($day);
     } catch (Exception $exc) {
         error_log($exc);
-        $linksToDisplay = array();
+        $linksToDisplay = [];
     }
 
+    $factory = new FormatterFactory($conf);
+    $formatter = $factory->getFormatter();
     // We pre-format some fields for proper output.
-    foreach ($linksToDisplay as $key => $link) {
-        $taglist = explode(' ', $link['tags']);
-        uasort($taglist, 'strcasecmp');
-        $linksToDisplay[$key]['taglist']=$taglist;
-        $linksToDisplay[$key]['formatedDescription'] = format_description($link['description']);
-        $linksToDisplay[$key]['timestamp'] =  $link['created']->getTimestamp();
+    foreach ($linksToDisplay as $key => $bookmark) {
+        $linksToDisplay[$key] = $formatter->format($bookmark);
+        // This page is a bit specific, we need raw description to calculate the length
+        $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
+        $linksToDisplay[$key]['description'] = $bookmark->getDescription();
     }
 
-    $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
+    $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
     $data = array(
         'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
         'linksToDisplay' => $linksToDisplay,
@@ -457,19 +472,19 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
     */
     $columns = array(array(), array(), array()); // Entries to display, for each column.
     $fill = array(0, 0, 0);  // Rough estimate of columns fill.
-    foreach ($data['linksToDisplay'] as $key => $link) {
+    foreach ($data['linksToDisplay'] as $key => $bookmark) {
         // Roughly estimate length of entry (by counting characters)
         // Title: 30 chars = 1 line. 1 line is 30 pixels height.
         // Description: 836 characters gives roughly 342 pixel height.
         // This is not perfect, but it's usually OK.
-        $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836;
-        if (! empty($link['thumbnail'])) {
+        $length = strlen($bookmark['title']) + (342 * strlen($bookmark['description'])) / 836;
+        if (! empty($bookmark['thumbnail'])) {
             $length += 100; // 1 thumbnails roughly takes 100 pixels height.
         }
         // Then put in column which is the less filled:
         $smallest = min($fill); // find smallest value in array.
         $index = array_search($smallest, $fill); // find index of this smallest value.
-        array_push($columns[$index], $link); // Put entry in this column.
+        array_push($columns[$index], $bookmark); // Put entry in this column.
         $fill[$index] += $length;
     }
 
@@ -487,40 +502,39 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
 /**
  * Renders the linklist
  *
- * @param pageBuilder   $PAGE    pageBuilder instance.
- * @param LinkDB        $LINKSDB LinkDB instance.
- * @param ConfigManager $conf    Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
+ * @param pageBuilder              $PAGE          pageBuilder instance.
+ * @param BookmarkServiceInterface $linkDb        instance.
+ * @param ConfigManager            $conf          Configuration Manager instance.
+ * @param PluginManager            $pluginManager Plugin Manager instance.
  */
-function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
+function showLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
 {
-    buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
+    buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager);
     $PAGE->renderPage('linklist');
 }
 
 /**
  * Render HTML page (according to URL parameters and user rights)
  *
- * @param ConfigManager  $conf           Configuration Manager instance.
- * @param PluginManager  $pluginManager  Plugin Manager instance,
- * @param LinkDB         $LINKSDB
- * @param History        $history        instance
- * @param SessionManager $sessionManager SessionManager instance
- * @param LoginManager   $loginManager   LoginManager instance
+ * @param ConfigManager            $conf           Configuration Manager instance.
+ * @param PluginManager            $pluginManager  Plugin Manager instance,
+ * @param BookmarkServiceInterface $bookmarkService
+ * @param History                  $history        instance
+ * @param SessionManager           $sessionManager SessionManager instance
+ * @param LoginManager             $loginManager   LoginManager instance
  */
-function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $loginManager)
+function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionManager, $loginManager)
 {
     $updater = new Updater(
-        read_updates_file($conf->get('resource.updates')),
-        $LINKSDB,
+        UpdaterUtils::read_updates_file($conf->get('resource.updates')),
+        $bookmarkService,
         $conf,
-        $loginManager->isLoggedIn(),
-        $_SESSION
+        $loginManager->isLoggedIn()
     );
     try {
         $newUpdates = $updater->update();
         if (! empty($newUpdates)) {
-            write_updates_file(
+            UpdaterUtils::write_updates_file(
                 $conf->get('resource.updates'),
                 $updater->getDoneUpdates()
             );
@@ -529,9 +543,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         die($e->getMessage());
     }
 
-    $PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
-    $PAGE->assign('linkcount', count($LINKSDB));
-    $PAGE->assign('privateLinkcount', count_private($LINKSDB));
+    $PAGE = new PageBuilder($conf, $_SESSION, $bookmarkService, $sessionManager->generateToken(), $loginManager->isLoggedIn());
+    $PAGE->assign('linkcount', $bookmarkService->count(BookmarkFilter::$ALL));
+    $PAGE->assign('privateLinkcount', $bookmarkService->count(BookmarkFilter::$PRIVATE));
     $PAGE->assign('plugin_errors', $pluginManager->getErrors());
 
     // Determine which page will be rendered.
@@ -611,27 +625,28 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         }
 
         // Optionally filter the results:
-        $links = $LINKSDB->filterSearch($_GET);
-        $linksToDisplay = array();
+        $links = $bookmarkService->search($_GET);
+        $linksToDisplay = [];
 
-        // Get only links which have a thumbnail.
+        // Get only bookmarks which have a thumbnail.
         // Note: we do not retrieve thumbnails here, the request is too heavy.
+        $factory = new FormatterFactory($conf);
+    $formatter = $factory->getFormatter();
         foreach ($links as $key => $link) {
-            if (isset($link['thumbnail']) && $link['thumbnail'] !== false) {
-                $linksToDisplay[] = $link; // Add to array.
+            if ($link->getThumbnail() !== false) {
+                $linksToDisplay[] = $formatter->format($link);
             }
         }
 
-        $data = array(
+        $data = [
             'linksToDisplay' => $linksToDisplay,
-        );
-        $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
+        ];
+        $pluginManager->executeHooks('render_picwall', $data, ['loggedin' => $loginManager->isLoggedIn()]);
 
         foreach ($data as $key => $value) {
             $PAGE->assign($key, $value);
         }
 
-
         $PAGE->renderPage('picwall');
         exit;
     }
@@ -640,7 +655,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
     if ($targetPage == Router::$PAGE_TAGCLOUD) {
         $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
         $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
-        $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
+        $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
 
         // We sort tags alphabetically, then choose a font size according to count.
         // First, find max value.
@@ -687,7 +702,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
     if ($targetPage == Router::$PAGE_TAGLIST) {
         $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
         $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
-        $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
+        $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
         foreach ($filteringTags as $tag) {
             if (array_key_exists($tag, $tags)) {
                 unset($tags[$tag]);
@@ -717,7 +732,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
 
     // Daily page.
     if ($targetPage == Router::$PAGE_DAILY) {
-        showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
+        showDaily($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
     }
 
     // ATOM and RSS feed.
@@ -738,8 +753,16 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             exit;
         }
 
+        $factory = new FormatterFactory($conf);
         // Generate data.
-        $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
+        $feedGenerator = new FeedBuilder(
+            $bookmarkService,
+            $factory->getFormatter('raw'),
+            $feedType,
+            $_SERVER,
+            $_GET,
+            $loginManager->isLoggedIn()
+        );
         $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
         $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
         $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
@@ -845,7 +868,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         exit;
     }
 
-    // -------- User wants to change the number of links per page (linksperpage=...)
+    // -------- User wants to change the number of bookmarks per page (linksperpage=...)
     if (isset($_GET['linksperpage'])) {
         if (is_numeric($_GET['linksperpage'])) {
             $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
@@ -860,19 +883,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         exit;
     }
 
-    // -------- User wants to see only private links (toggle)
+    // -------- User wants to see only private bookmarks (toggle)
     if (isset($_GET['visibility'])) {
         if ($_GET['visibility'] === 'private') {
             // Visibility not set or not already private, set private, otherwise reset it
             if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
-                // See only private links
+                // See only private bookmarks
                 $_SESSION['visibility'] = 'private';
             } else {
                 unset($_SESSION['visibility']);
             }
         } elseif ($_GET['visibility'] === 'public') {
             if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
-                // See only public links
+                // See only public bookmarks
                 $_SESSION['visibility'] = 'public';
             } else {
                 unset($_SESSION['visibility']);
@@ -888,7 +911,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         exit;
     }
 
-    // -------- User wants to see only untagged links (toggle)
+    // -------- User wants to see only untagged bookmarks (toggle)
     if (isset($_GET['untaggedonly'])) {
         $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
 
@@ -916,7 +939,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             exit;
         }
 
-        showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
+        showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
         if (isset($_GET['edit_link'])) {
             header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
             exit;
@@ -1022,7 +1045,11 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
             $conf->set('api.enabled', !empty($_POST['enableApi']));
             $conf->set('api.secret', escape($_POST['apiSecret']));
-            $conf->set('translation.language', escape($_POST['language']));
+            $conf->set('formatter', escape($_POST['formatter']));
+
+            if (! empty($_POST['language'])) {
+                $conf->set('translation.language', escape($_POST['language']));
+            }
 
             $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
             if ($thumbnailsMode !== Thumbnailer::MODE_NONE
@@ -1056,6 +1083,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             $PAGE->assign('title', $conf->get('general.title'));
             $PAGE->assign('theme', $conf->get('resource.theme'));
             $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
+            $PAGE->assign('formatter_available', ['default', 'markdown']);
             list($continents, $cities) = generateTimeZoneData(
                 timezone_identifiers_list(),
                 $conf->get('general.timezone')
@@ -1093,17 +1121,25 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         }
 
         $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
-        $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), $toTag);
-        $LINKSDB->save($conf->get('resource.page_cache'));
-        foreach ($alteredLinks as $link) {
-            $history->updateLink($link);
+        $fromTag = escape($_POST['fromtag']);
+        $count = 0;
+        $bookmarks = $bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
+        foreach ($bookmarks as $bookmark) {
+            if ($toTag) {
+                $bookmark->renameTag($fromTag, $toTag);
+            } else {
+                $bookmark->deleteTag($fromTag);
+            }
+            $bookmarkService->set($bookmark, false);
+            $history->updateLink($bookmark);
+            $count++;
         }
+        $bookmarkService->save();
         $delete = empty($_POST['totag']);
         $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
-        $count = count($alteredLinks);
         $alert = $delete
-            ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
-            : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
+            ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d bookmarks.', $count), $count)
+            : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d bookmarks.', $count), $count);
         echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
         exit;
     }
@@ -1123,69 +1159,37 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         }
 
         // 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])) {
+        $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null;
+        if ($id && $bookmarkService->exists($id)) {
             // Edit
-            $link['updated'] = new DateTime();
-            $link['shorturl'] = $LINKSDB[$id]['shorturl'];
-            $link['sticky'] = isset($LINKSDB[$id]['sticky']) ? $LINKSDB[$id]['sticky'] : false;
-            $new = false;
+            $bookmark = $bookmarkService->get($id);
         } else {
             // New link
-            $link['updated'] = null;
-            $link['shorturl'] = link_small_hash($link['created'], $id);
-            $link['sticky'] = false;
-            $new = true;
-        }
-
-        // Remove multiple spaces.
-        $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
-        // Remove first '-' char in tags.
-        $tags = preg_replace('/(^| )\-/', '$1', $tags);
-        // Remove duplicates.
-        $tags = implode(' ', array_unique(explode(' ', $tags)));
-
-        if (empty(trim($_POST['lf_url']))) {
-            $_POST['lf_url'] = '?' . smallHash($linkdate . $id);
+            $bookmark = new Bookmark();
         }
-        $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols'));
 
-        $link = array_merge($link, [
-            'title' => trim($_POST['lf_title']),
-            'url' => $url,
-            'description' => $_POST['lf_description'],
-            'private' => (isset($_POST['lf_private']) ? 1 : 0),
-            'tags' => str_replace(',', ' ', $tags),
-        ]);
-
-        // If title is empty, use the URL as title.
-        if ($link['title'] == '') {
-            $link['title'] = $link['url'];
-        }
+        $bookmark->setTitle($_POST['lf_title']);
+        $bookmark->setDescription($_POST['lf_description']);
+        $bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols'));
+        $bookmark->setPrivate(isset($_POST['lf_private']));
+        $bookmark->setTagsString($_POST['lf_tags']);
 
         if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
-            && ! is_note($link['url'])
+            && ! $bookmark->isNote()
         ) {
             $thumbnailer = new Thumbnailer($conf);
-            $link['thumbnail'] = $thumbnailer->get($url);
+            $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
         }
+        $bookmarkService->addOrSet($bookmark, false);
 
-        $pluginManager->executeHooks('save_link', $link);
+        // To preserve backward compatibility with 3rd parties, plugins still use arrays
+        $factory = new FormatterFactory($conf);
+        $formatter = $factory->getFormatter('raw');
+        $data = $formatter->format($bookmark);
+        $pluginManager->executeHooks('save_link', $data);
 
-        $LINKSDB[$id] = $link;
-        $LINKSDB->save($conf->get('resource.page_cache'));
-        if ($new) {
-            $history->addLink($link);
-        } else {
-            $history->updateLink($link);
-        }
+        $bookmark->fromArray($data);
+        $bookmarkService->set($bookmark);
 
         // If we are called from the bookmarklet, we must close the popup:
         if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
@@ -1196,32 +1200,12 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
         $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
         // Scroll to the link which has been edited.
-        $location .= '#' . $link['shorturl'];
+        $location .= '#' . $bookmark->getShortUrl();
         // After saving the link, redirect to the page the user was on.
         header('Location: '. $location);
         exit;
     }
 
-    // -------- User clicked the "Cancel" button when editing a link.
-    if (isset($_POST['cancel_edit'])) {
-        $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false;
-        if (! isset($LINKSDB[$id])) {
-            header('Location: ?');
-        }
-        // If we are called from the bookmarklet, we must close the popup:
-        if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
-            echo '<script>self.close();</script>';
-            exit;
-        }
-        $link = $LINKSDB[$id];
-        $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
-        // Scroll to the link which has been edited.
-        $returnurl .= '#'. $link['shorturl'];
-        $returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
-        header('Location: '.$returnurl); // After canceling, redirect to the page the user was on.
-        exit;
-    }
-
     // -------- User clicked the "Delete" button when editing a link: Delete link from database.
     if ($targetPage == Router::$PAGE_DELETELINK) {
         if (! $sessionManager->checkToken($_GET['token'])) {
@@ -1231,23 +1215,31 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         $ids = trim($_GET['lf_linkdate']);
         if (strpos($ids, ' ') !== false) {
             // multiple, space-separated ids provided
-            $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
+            $ids = array_values(array_filter(
+                preg_split('/\s+/', escape($ids)),
+                function ($item) {
+                    return $item !== '';
+                }
+            ));
         } else {
             // only a single id provided
+            $shortUrl = $bookmarkService->get($ids)->getShortUrl();
             $ids = [$ids];
         }
         // assert at least one id is given
         if (!count($ids)) {
             die('no id provided');
         }
+        $factory = new FormatterFactory($conf);
+        $formatter = $factory->getFormatter('raw');
         foreach ($ids as $id) {
             $id = (int) escape($id);
-            $link = $LINKSDB[$id];
-            $pluginManager->executeHooks('delete_link', $link);
-            $history->deleteLink($link);
-            unset($LINKSDB[$id]);
+            $bookmark = $bookmarkService->get($id);
+            $data = $formatter->format($bookmark);
+            $pluginManager->executeHooks('delete_link', $data);
+            $bookmarkService->remove($bookmark, false);
         }
-        $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
+        $bookmarkService->save();
 
         // If we are called from the bookmarklet, we must close the popup:
         if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
@@ -1261,7 +1253,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             $location = generateLocation(
                 $_SERVER['HTTP_REFERER'],
                 $_SERVER['HTTP_HOST'],
-                ['delete_link', 'edit_link', $link['shorturl']]
+                ['delete_link', 'edit_link', ! empty($shortUrl) ? $shortUrl : null]
             );
         }
 
@@ -1294,14 +1286,21 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         } else {
             $private = $_GET['newVisibility'] === 'private';
         }
+        $factory = new FormatterFactory($conf);
+        $formatter = $factory->getFormatter('raw');
         foreach ($ids as $id) {
             $id = (int) escape($id);
-            $link = $LINKSDB[$id];
-            $link['private'] = $private;
-            $pluginManager->executeHooks('save_link', $link);
-            $LINKSDB[$id] = $link;
+            $bookmark = $bookmarkService->get($id);
+            $bookmark->setPrivate($private);
+
+            // To preserve backward compatibility with 3rd parties, plugins still use arrays
+            $data = $formatter->format($bookmark);
+            $pluginManager->executeHooks('save_link', $data);
+            $bookmark->fromArray($data);
+
+            $bookmarkService->set($bookmark);
         }
-        $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
+        $bookmarkService->save();
 
         $location = '?';
         if (isset($_SERVER['HTTP_REFERER'])) {
@@ -1317,17 +1316,22 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
     // -------- User clicked the "EDIT" button on a link: Display link edit form.
     if (isset($_GET['edit_link'])) {
         $id = (int) escape($_GET['edit_link']);
-        $link = $LINKSDB[$id];  // Read database
-        if (!$link) {
+        try {
+            $link = $bookmarkService->get($id);  // Read database
+        } catch (BookmarkNotFoundException $e) {
+            // Link not found in database.
             header('Location: ?');
             exit;
-        } // Link not found in database.
-        $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
+        }
+
+        $factory = new FormatterFactory($conf);
+        $formatter = $factory->getFormatter('raw');
+        $formattedLink = $formatter->format($link);
         $data = array(
-            'link' => $link,
+            'link' => $formattedLink,
             'link_is_new' => false,
             'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
-            'tags' => $LINKSDB->linksCountPerTag(),
+            'tags' => $bookmarkService->bookmarksCountPerTag(),
         );
         $pluginManager->executeHooks('render_editlink', $data);
 
@@ -1346,10 +1350,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
 
         $link_is_new = false;
         // Check if URL is not already in database (in this case, we will edit the existing link)
-        $link = $LINKSDB->getLinkFromUrl($url);
-        if (! $link) {
+        $bookmark = $bookmarkService->findByUrl($url);
+        if (! $bookmark) {
             $link_is_new = true;
-            $linkdate = strval(date(LinkDB::LINK_DATE_FORMAT));
             // Get title if it was provided in URL (by the bookmarklet).
             $title = empty($_GET['title']) ? '' : escape($_GET['title']);
             // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
@@ -1375,32 +1378,32 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             }
 
             if ($url == '') {
-                $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
                 $title = $conf->get('general.default_note_title', t('Note: '));
             }
             $url = escape($url);
             $title = escape($title);
 
-            $link = array(
-                'linkdate' => $linkdate,
+            $link = [
                 'title' => $title,
                 'url' => $url,
                 'description' => $description,
                 'tags' => $tags,
                 'private' => $private,
-            );
+            ];
         } else {
-            $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
+            $factory = new FormatterFactory($conf);
+        $formatter = $factory->getFormatter('raw');
+            $link = $formatter->format($bookmark);
         }
 
-        $data = array(
+        $data = [
             'link' => $link,
             'link_is_new' => $link_is_new,
             'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
             'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
-            'tags' => $LINKSDB->linksCountPerTag(),
+            'tags' => $bookmarkService->bookmarksCountPerTag(),
             'default_private_links' => $conf->get('privacy.default_private_links', false),
-        );
+        ];
         $pluginManager->executeHooks('render_editlink', $data);
 
         foreach ($data as $key => $value) {
@@ -1413,7 +1416,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
     }
 
     if ($targetPage == Router::$PAGE_PINLINK) {
-        if (! isset($_GET['id']) || empty($LINKSDB[$_GET['id']])) {
+        if (! isset($_GET['id']) || !$bookmarkService->exists($_GET['id'])) {
             // FIXME! Use a proper error system.
             $msg = t('Invalid link ID provided');
             echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>';
@@ -1423,16 +1426,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             die('Wrong token.');
         }
 
-        $link = $LINKSDB[$_GET['id']];
-        $link['sticky'] = ! $link['sticky'];
-        $LINKSDB[(int) $_GET['id']] = $link;
-        $LINKSDB->save($conf->get('resource.page_cache'));
+        $link = $bookmarkService->get($_GET['id']);
+        $link->setSticky(! $link->isSticky());
+        $bookmarkService->set($link);
         header('Location: '.index_url($_SERVER));
         exit;
     }
 
     if ($targetPage == Router::$PAGE_EXPORT) {
-        // Export links as a Netscape Bookmarks file
+        // Export bookmarks as a Netscape Bookmarks file
 
         if (empty($_GET['selection'])) {
             $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
@@ -1449,10 +1451,13 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         }
 
         try {
+            $factory = new FormatterFactory($conf);
+        $formatter = $factory->getFormatter('raw');
             $PAGE->assign(
                 'links',
                 NetscapeBookmarkUtils::filterAndFormat(
-                    $LINKSDB,
+                    $bookmarkService,
+                    $formatter,
                     $selection,
                     $prependNoteUrl,
                     index_url($_SERVER)
@@ -1467,7 +1472,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         header('Content-Type: text/html; charset=utf-8');
         header(
             'Content-disposition: attachment; filename=bookmarks_'
-            .$selection.'_'.$now->format(LinkDB::LINK_DATE_FORMAT).'.html'
+            .$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
         );
         $PAGE->assign('date', $now->format(DateTime::RFC822));
         $PAGE->assign('eol', PHP_EOL);
@@ -1521,7 +1526,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         $status = NetscapeBookmarkUtils::import(
             $_POST,
             $_FILES,
-            $LINKSDB,
+            $bookmarkService,
             $conf,
             $history
         );
@@ -1592,19 +1597,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
     // Get a fresh token
     if ($targetPage == Router::$GET_TOKEN) {
         header('Content-Type:text/plain');
-        echo $sessionManager->generateToken($conf);
+        echo $sessionManager->generateToken();
         exit;
     }
 
     // -------- Thumbnails Update
     if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
         $ids = [];
-        foreach ($LINKSDB as $link) {
+        foreach ($bookmarkService->search() as $bookmark) {
             // A note or not HTTP(S)
-            if (is_note($link['url']) || ! startsWith(strtolower($link['url']), 'http')) {
+            if ($bookmark->isNote() || ! startsWith(strtolower($bookmark->getUrl()), 'http')) {
                 continue;
             }
-            $ids[] = $link['id'];
+            $ids[] = $bookmark->getId();
         }
         $PAGE->assign('ids', $ids);
         $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
@@ -1619,37 +1624,40 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             exit;
         }
         $id = (int) $_POST['id'];
-        if (empty($LINKSDB[$id])) {
+        if (! $bookmarkService->exists($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'));
+        $bookmark = $bookmarkService->get($id);
+        $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
+        $bookmarkService->set($bookmark);
 
-        echo json_encode($link);
+        $factory = new FormatterFactory($conf);
+        echo json_encode($factory->getFormatter('raw')->format($bookmark));
         exit;
     }
 
-    // -------- Otherwise, simply display search form and links:
-    showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
+    // -------- Otherwise, simply display search form and bookmarks:
+    showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
     exit;
 }
 
 /**
- * Template for the list of links (<div id="linklist">)
+ * Template for the list of bookmarks (<div id="linklist">)
  * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
  *
- * @param pageBuilder   $PAGE          pageBuilder instance.
- * @param LinkDB        $LINKSDB       LinkDB instance.
- * @param ConfigManager $conf          Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
- * @param LoginManager  $loginManager  LoginManager instance
+ * @param pageBuilder              $PAGE          pageBuilder instance.
+ * @param BookmarkServiceInterface $linkDb        LinkDB instance.
+ * @param ConfigManager            $conf          Configuration Manager instance.
+ * @param PluginManager            $pluginManager Plugin Manager instance.
+ * @param LoginManager             $loginManager  LoginManager instance
  */
-function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
+function buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
 {
+    $factory = new FormatterFactory($conf);
+    $formatter = $factory->getFormatter();
+
     // Used in templates
     if (isset($_GET['searchtags'])) {
         if (! empty($_GET['searchtags'])) {
@@ -1666,19 +1674,19 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
     if (! empty($_SERVER['QUERY_STRING'])
         && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
         try {
-            $linksToDisplay = $LINKSDB->filterHash($_SERVER['QUERY_STRING']);
-        } catch (LinkNotFoundException $e) {
+            $linksToDisplay = $linkDb->findByHash($_SERVER['QUERY_STRING']);
+        } catch (BookmarkNotFoundException $e) {
             $PAGE->render404($e->getMessage());
             exit;
         }
     } else {
-        // Filter links according search parameters.
+        // Filter bookmarks according search parameters.
         $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
         $request = [
             'searchtags' => $searchtags,
             'searchterm' => $searchterm,
         ];
-        $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility, !empty($_SESSION['untaggedonly']));
+        $linksToDisplay = $linkDb->search($request, $visibility, false, !empty($_SESSION['untaggedonly']));
     }
 
     // ---- Handle paging.
@@ -1704,36 +1712,26 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
 
     $linkDisp = array();
     while ($i<$end && $i<count($keys)) {
-        $link = $linksToDisplay[$keys[$i]];
-        $link['description'] = format_description($link['description']);
-        $classLi =  ($i % 2) != 0 ? '' : 'publicLinkHightLight';
-        $link['class'] = $link['private'] == 0 ? $classLi : 'private';
-        $link['timestamp'] = $link['created']->getTimestamp();
-        if (! empty($link['updated'])) {
-            $link['updated_timestamp'] = $link['updated']->getTimestamp();
-        } else {
-            $link['updated_timestamp'] = '';
-        }
-        $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
-        uasort($taglist, 'strcasecmp');
-        $link['taglist'] = $taglist;
+        $link = $formatter->format($linksToDisplay[$keys[$i]]);
 
         // Logged in, thumbnails enabled, not a note,
         // and (never retrieved yet or no valid cache file)
-        if ($loginManager->isLoggedIn() && $thumbnailsEnabled && $link['url'][0] != '?'
-            && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail'])))
+        if ($loginManager->isLoggedIn()
+            && $thumbnailsEnabled
+            && !$linksToDisplay[$keys[$i]]->isNote()
+            && $linksToDisplay[$keys[$i]]->getThumbnail() !== false
+            && ! is_file($linksToDisplay[$keys[$i]]->getThumbnail())
         ) {
-            $elem = $LINKSDB[$keys[$i]];
-            $elem['thumbnail'] = $thumbnailer->get($link['url']);
-            $LINKSDB[$keys[$i]] = $elem;
+            $linksToDisplay[$keys[$i]]->setThumbnail($thumbnailer->get($link['url']));
+            $linkDb->set($linksToDisplay[$keys[$i]], false);
             $updateDB = true;
-            $link['thumbnail'] = $elem['thumbnail'];
+            $link['thumbnail'] = $linksToDisplay[$keys[$i]]->getThumbnail();
         }
 
         // Check for both signs of a note: starting with ? and 7 chars long.
-        if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
-            $link['url'] = index_url($_SERVER) . $link['url'];
-        }
+//        if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
+//            $link['url'] = index_url($_SERVER) . $link['url'];
+//        }
 
         $linkDisp[$keys[$i]] = $link;
         $i++;
@@ -1741,7 +1739,7 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
 
     // If we retrieved new thumbnails, we update the database.
     if (!empty($updateDB)) {
-        $LINKSDB->save($conf->get('resource.page_cache'));
+        $linkDb->save();
     }
 
     // Compute paging navigation
@@ -1771,7 +1769,7 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
 
     // If there is only a single link, we change on-the-fly the title of the page.
     if (count($linksToDisplay) == 1) {
-        $data['pagetitle'] = $linksToDisplay[$keys[0]]['title'] .' - '. $conf->get('general.title');
+        $data['pagetitle'] = $linksToDisplay[$keys[0]]->getTitle() .' - '. $conf->get('general.title');
     } elseif (! empty($searchterm) || ! empty($searchtags)) {
         $data['pagetitle'] = t('Search: ');
         $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
@@ -1856,7 +1854,7 @@ function install($conf, $sessionManager, $loginManager)
         if (!empty($_POST['title'])) {
             $conf->set('general.title', escape($_POST['title']));
         } else {
-            $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
+            $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER)));
         }
         $conf->set('translation.language', escape($_POST['language']));
         $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
@@ -1881,9 +1879,16 @@ function install($conf, $sessionManager, $loginManager)
             echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
             exit;
         }
+
+        $history = new History($conf->get('resource.history'));
+        $bookmarkService = new BookmarkFileService($conf, $history, true);
+        if ($bookmarkService->count() === 0) {
+            $bookmarkService->initialize();
+        }
+
         echo '<script>alert('
             .'"Shaarli is now configured. '
-            .'Please enter your login/password and start shaaring your links!"'
+            .'Please enter your login/password and start shaaring your bookmarks!"'
             .');document.location=\'?do=login\';</script>';
         exit;
     }
@@ -1897,11 +1902,6 @@ function install($conf, $sessionManager, $loginManager)
     exit;
 }
 
-if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
-    showDailyRSS($conf, $loginManager);
-    exit;
-}
-
 if (!isset($_SESSION['LINKS_PER_PAGE'])) {
     $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
 }
@@ -1912,11 +1912,12 @@ try {
     die($e->getMessage());
 }
 
-$linkDb = new LinkDB(
-    $conf->get('resource.datastore'),
-    $loginManager->isLoggedIn(),
-    $conf->get('privacy.hide_public_links')
-);
+$linkDb = new BookmarkFileService($conf, $history, $loginManager->isLoggedIn());
+
+if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
+    showDailyRSS($linkDb, $conf, $loginManager);
+    exit;
+}
 
 $container = new \Slim\Container();
 $container['conf'] = $conf;
@@ -1927,11 +1928,11 @@ $app = new \Slim\App($container);
 // REST API routes
 $app->group('/api/v1', function () {
     $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
-    $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
-    $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
-    $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
-    $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
-    $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
+    $this->get('/bookmarks', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
+    $this->get('/bookmarks/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
+    $this->post('/bookmarks', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
+    $this->put('/bookmarks/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
+    $this->delete('/bookmarks/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
 
     $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags');
     $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag');
diff --git a/plugins/markdown/README.md b/plugins/markdown/README.md
deleted file mode 100644 (file)
index bc9427e..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-## Markdown Shaarli plugin
-
-Convert all your shaares description to HTML formatted Markdown.
-
-[Read more about Markdown syntax](http://daringfireball.net/projects/markdown/syntax).
-
-Markdown processing is done with [Parsedown library](https://github.com/erusev/parsedown).
-
-### Installation
-
-As a default plugin, it should already be in `tpl/plugins/` directory.
-If not, download and unpack it there.
-
-The directory structure should look like:
-
-```
---- plugins
-  |--- markdown
-     |--- help.html
-     |--- markdown.css
-     |--- markdown.meta
-     |--- markdown.php
-     |--- README.md
-```
-
-To enable the plugin, just check it in the plugin administration page.
-
-You can also add `markdown` to your list of enabled plugins in `data/config.json.php`
-(`general.enabled_plugins` list).
-
-This should look like:
-
-```
-"general": {
-  "enabled_plugins": [
-    "markdown",
-    [...]
-  ],
-}
-```
-
-Parsedown parsing library is imported using Composer. If you installed Shaarli using `git`,
-or the `master` branch, run
-
-    composer update --no-dev --prefer-dist
-
-### No Markdown tag
-
-If the tag `nomarkdown` is set for a shaare, it won't be converted to Markdown syntax.
-> Note: this is a special tag, so it won't be displayed in link list.
-
-### HTML escape
-
-By default, HTML tags are escaped. You can enable HTML tags rendering
-by setting `security.markdwon_escape` to `false` in `data/config.json.php`:
-
-```json
-{
-  "security": {
-    "markdown_escape": false
-  }
-}
-```
-
-With this setting, Markdown support HTML tags. For example:
-
-    > <strong>strong</strong><strike>strike</strike>
-   
-Will render as:
-
-> <strong>strong</strong><strike>strike</strike>
-
-
-**Warning:**
-
-  * This setting might present **security risks** (XSS) on shared instances, even though tags 
-  such as script, iframe, etc should be disabled.
-  * If you want to shaare HTML code, it is necessary to use inline code or code blocks.
-  * If your shaared descriptions contained HTML tags before enabling the markdown plugin, 
-enabling it might break your page.
-
-### Known issue
-
-#### Redirector
-
-If you're using a redirector, you *need* to add a space after a link,
-otherwise the rest of the line will be `urlencode`.
-
-```
-[link](http://domain.tld)-->test
-```
-
-Will consider `http://domain.tld)-->test` as URL.
-
-Instead, add an additional space.
-
-```
-[link](http://domain.tld) -->test
-```
-
-> Won't fix because a `)` is a valid part of an URL.
diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html
deleted file mode 100644 (file)
index ded3d34..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<div class="md_help">
-    %s
-    <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
-        %s</a>.
-</div>
diff --git a/plugins/markdown/markdown.meta b/plugins/markdown/markdown.meta
deleted file mode 100644 (file)
index 322856e..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-description="Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
-If your shaared descriptions contained HTML tags before enabling the markdown plugin,
-enabling it might break your page.
-See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering\">README</a>."
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
deleted file mode 100644 (file)
index f6f66cc..0000000
+++ /dev/null
@@ -1,365 +0,0 @@
-<?php
-
-/**
- * Plugin Markdown.
- *
- * Shaare's descriptions are parsed with Markdown.
- */
-
-use Shaarli\Config\ConfigManager;
-use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
-
-/*
- * If this tag is used on a shaare, the description won't be processed by Parsedown.
- */
-define('NO_MD_TAG', 'nomarkdown');
-
-/**
- * Parse linklist descriptions.
- *
- * @param array         $data linklist data.
- * @param ConfigManager $conf instance.
- *
- * @return mixed linklist data parsed in markdown (and converted to HTML).
- */
-function hook_markdown_render_linklist($data, $conf)
-{
-    foreach ($data['links'] as &$value) {
-        if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
-            $value = stripNoMarkdownTag($value);
-            continue;
-        }
-        $value['description_src'] = $value['description'];
-        $value['description'] = process_markdown(
-            $value['description'],
-            $conf->get('security.markdown_escape', true),
-            $conf->get('security.allowed_protocols')
-        );
-    }
-    return $data;
-}
-
-/**
- * Parse feed linklist descriptions.
- *
- * @param array $data linklist data.
- * @param ConfigManager $conf instance.
- *
- * @return mixed linklist data parsed in markdown (and converted to HTML).
- */
-function hook_markdown_render_feed($data, $conf)
-{
-    foreach ($data['links'] as &$value) {
-        if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
-            $value = stripNoMarkdownTag($value);
-            continue;
-        }
-        $value['description'] = reverse_feed_permalink($value['description']);
-        $value['description'] = process_markdown(
-            $value['description'],
-            $conf->get('security.markdown_escape', true),
-            $conf->get('security.allowed_protocols')
-        );
-    }
-
-    return $data;
-}
-
-/**
- * Parse daily descriptions.
- *
- * @param array         $data daily data.
- * @param ConfigManager $conf instance.
- *
- * @return mixed daily data parsed in markdown (and converted to HTML).
- */
-function hook_markdown_render_daily($data, $conf)
-{
-    //var_dump($data);die;
-    // Manipulate columns data
-    foreach ($data['linksToDisplay'] as &$value) {
-        if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
-            $value = stripNoMarkdownTag($value);
-            continue;
-        }
-        $value['formatedDescription'] = process_markdown(
-            $value['formatedDescription'],
-            $conf->get('security.markdown_escape', true),
-            $conf->get('security.allowed_protocols')
-        );
-    }
-
-    return $data;
-}
-
-/**
- * Check if noMarkdown is set in tags.
- *
- * @param string $tags tag list
- *
- * @return bool true if markdown should be disabled on this link.
- */
-function noMarkdownTag($tags)
-{
-    return preg_match('/(^|\s)'. NO_MD_TAG .'(\s|$)/', $tags);
-}
-
-/**
- * Remove the no-markdown meta tag so it won't be displayed.
- *
- * @param array $link Link data.
- *
- * @return array Updated link without no markdown tag.
- */
-function stripNoMarkdownTag($link)
-{
-    if (! empty($link['taglist'])) {
-        $offset = array_search(NO_MD_TAG, $link['taglist']);
-        if ($offset !== false) {
-            unset($link['taglist'][$offset]);
-        }
-    }
-
-    if (!empty($link['tags'])) {
-        str_replace(NO_MD_TAG, '', $link['tags']);
-    }
-
-    return $link;
-}
-
-/**
- * When link list is displayed, include markdown CSS.
- *
- * @param array $data includes data.
- *
- * @return mixed - includes data with markdown CSS file added.
- */
-function hook_markdown_render_includes($data)
-{
-    if ($data['_PAGE_'] == Router::$PAGE_LINKLIST
-        || $data['_PAGE_'] == Router::$PAGE_DAILY
-        || $data['_PAGE_'] == Router::$PAGE_EDITLINK
-    ) {
-        $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/markdown/markdown.css';
-    }
-
-    return $data;
-}
-
-/**
- * Hook render_editlink.
- * Adds an help link to markdown syntax.
- *
- * @param array $data data passed to plugin
- *
- * @return array altered $data.
- */
-function hook_markdown_render_editlink($data)
-{
-    // Load help HTML into a string
-    $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
-    $translations = [
-        t('Description will be rendered with'),
-        t('Markdown syntax documentation'),
-        t('Markdown syntax'),
-    ];
-    $data['edit_link_plugin'][] = vsprintf($txt, $translations);
-    // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
-    if (! in_array(NO_MD_TAG, $data['tags'])) {
-        $data['tags'][NO_MD_TAG] = 0;
-    }
-
-    return $data;
-}
-
-
-/**
- * Remove HTML links auto generated by Shaarli core system.
- * Keeps HREF attributes.
- *
- * @param string $description input description text.
- *
- * @return string $description without HTML links.
- */
-function reverse_text2clickable($description)
-{
-    $descriptionLines = explode(PHP_EOL, $description);
-    $descriptionOut = '';
-    $codeBlockOn = false;
-    $lineCount = 0;
-
-    foreach ($descriptionLines as $descriptionLine) {
-        // Detect line of code: starting with 4 spaces,
-        // except lists which can start with +/*/- or `2.` after spaces.
-        $codeLineOn = preg_match('/^    +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
-        // Detect and toggle block of code
-        if (!$codeBlockOn) {
-            $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
-        } elseif (preg_match('/^```/', $descriptionLine) > 0) {
-            $codeBlockOn = false;
-        }
-
-        $hashtagTitle = ' title="Hashtag [^"]+"';
-        // Reverse `inline code` hashtags.
-        $descriptionLine = preg_replace(
-            '!(`[^`\n]*)<a href="[^ ]*"'. $hashtagTitle .'>([^<]+)</a>([^`\n]*`)!m',
-            '$1$2$3',
-            $descriptionLine
-        );
-
-        // Reverse all links in code blocks, only non hashtag elsewhere.
-        $hashtagFilter = (!$codeBlockOn && !$codeLineOn) ? '(?!'. $hashtagTitle .')': '(?:'. $hashtagTitle .')?';
-        $descriptionLine = preg_replace(
-            '#<a href="[^ ]*"'. $hashtagFilter .'>([^<]+)</a>#m',
-            '$1',
-            $descriptionLine
-        );
-
-        // Make hashtag links markdown ready, otherwise the links will be ignored with escape set to true
-        if (!$codeBlockOn && !$codeLineOn) {
-            $descriptionLine = preg_replace(
-                '#<a href="([^ ]*)"'. $hashtagTitle .'>([^<]+)</a>#m',
-                '[$2]($1)',
-                $descriptionLine
-            );
-        }
-
-        $descriptionOut .= $descriptionLine;
-        if ($lineCount++ < count($descriptionLines) - 1) {
-            $descriptionOut .= PHP_EOL;
-        }
-    }
-    return $descriptionOut;
-}
-
-/**
- * Remove <br> tag to let markdown handle it.
- *
- * @param string $description input description text.
- *
- * @return string $description without <br> tags.
- */
-function reverse_nl2br($description)
-{
-    return preg_replace('!<br */?>!im', '', $description);
-}
-
-/**
- * Remove HTML spaces '&nbsp;' auto generated by Shaarli core system.
- *
- * @param string $description input description text.
- *
- * @return string $description without HTML links.
- */
-function reverse_space2nbsp($description)
-{
-    return preg_replace('/(^| )&nbsp;/m', '$1 ', $description);
-}
-
-function reverse_feed_permalink($description)
-{
-    return preg_replace('@&#8212; <a href="([^"]+)" title="[^"]+">([^<]+)</a>$@im', '&#8212; [$2]($1)', $description);
-}
-
-/**
- * Replace not whitelisted protocols with http:// in given description.
- *
- * @param string $description      input description text.
- * @param array  $allowedProtocols list of allowed protocols.
- *
- * @return string $description without malicious link.
- */
-function filter_protocols($description, $allowedProtocols)
-{
-    return preg_replace_callback(
-        '#]\((.*?)\)#is',
-        function ($match) use ($allowedProtocols) {
-            return ']('. whitelist_protocols($match[1], $allowedProtocols) .')';
-        },
-        $description
-    );
-}
-
-/**
- * Remove dangerous HTML tags (tags, iframe, etc.).
- * Doesn't affect <code> content (already escaped by Parsedown).
- *
- * @param string $description input description text.
- *
- * @return string given string escaped.
- */
-function sanitize_html($description)
-{
-    $escapeTags = array(
-        'script',
-        'style',
-        'link',
-        'iframe',
-        'frameset',
-        'frame',
-    );
-    foreach ($escapeTags as $tag) {
-        $description = preg_replace_callback(
-            '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is',
-            function ($match) {
-                return escape($match[0]);
-            },
-            $description
-        );
-    }
-    $description = preg_replace(
-        '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is',
-        '$1',
-        $description
-    );
-    return $description;
-}
-
-/**
- * Render shaare contents through Markdown parser.
- *   1. Remove HTML generated by Shaarli core.
- *   2. Reverse the escape function.
- *   3. Generate markdown descriptions.
- *   4. Sanitize sensible HTML tags for security.
- *   5. Wrap description in 'markdown' CSS class.
- *
- * @param string $description input description text.
- * @param bool   $escape      escape HTML entities
- *
- * @return string HTML processed $description.
- */
-function process_markdown($description, $escape = true, $allowedProtocols = [])
-{
-    $parsedown = new Parsedown();
-
-    $processedDescription = $description;
-    $processedDescription = reverse_nl2br($processedDescription);
-    $processedDescription = reverse_space2nbsp($processedDescription);
-    $processedDescription = reverse_text2clickable($processedDescription);
-    $processedDescription = filter_protocols($processedDescription, $allowedProtocols);
-    $processedDescription = unescape($processedDescription);
-    $processedDescription = $parsedown
-        ->setMarkupEscaped($escape)
-        ->setBreaksEnabled(true)
-        ->text($processedDescription);
-    $processedDescription = sanitize_html($processedDescription);
-
-    if (!empty($processedDescription)) {
-        $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
-    }
-
-    return $processedDescription;
-}
-
-/**
- * This function is never called, but contains translation calls for GNU gettext extraction.
- */
-function markdown_dummy_translation()
-{
-    // meta
-    t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
-If your shaared descriptions contained HTML tags before enabling the markdown plugin,
-enabling it might break your page.
-See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
-}
index c1a6a6bc953445cfd82460fc822823d55f2a8f08..8b75900de2e73a1c12c6039dbd2be4d56a1423bb 100644 (file)
             </select>
           </div>
         </div>
+        <div class="pure-u-lg-{$ratioLabel} pure-u-1">
+          <div class="form-label">
+            <label for="formatter">
+              <span class="label-name">{'Description formatter'|t}</span>
+            </label>
+          </div>
+        </div>
+        <div class="pure-u-lg-{$ratioInput} pure-u-1">
+          <div class="form-input">
+            <select name="formatter" id="formatter" class="align">
+              {loop="$formatter_available"}
+                <option value="{$value}"
+                  {if="$value===$formatter"}
+                    selected="selected"
+                  {/if}
+                >
+                  {$value|ucfirst}
+                </option>
+              {/loop}
+            </select>
+          </div>
+        </div>
       </div>
       <div class="pure-g">
         <div class="pure-u-lg-{$ratioLabel} pure-u-1">
index df14535d610ce5ce8008b3205d79121332065afe..d16059a39b2dcfa204ddbc0ddff742eb812c7710 100644 (file)
@@ -11,7 +11,6 @@
       <h2 class="window-title">
         {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
       </h2>
-      <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
       {if="isset($link.id)"}
         <input type="hidden" name="lf_id" value="{$link.id}">
       {/if}
@@ -20,7 +19,7 @@
         <label for="lf_url">{'URL'|t}</label>
       </div>
       <div>
-        <input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input autofocus">
+        <input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input">
       </div>
       <div>
       <label for="lf_title">{'Title'|t}</label>
         &nbsp;<label for="lf_private">{'Private'|t}</label>
       </div>
 
+      {if="$formatter==='markdown'"}
+        <div class="md_help">
+          {'Description will be rendered with'|t}
+          <a href="http://daringfireball.net/projects/markdown/syntax" title="{'Markdown syntax documentation'|t}">
+            {'Markdown syntax'|t}
+          </a>.
+        </div>
+      {/if}
+
       <div id="editlink-plugins">
         {loop="$edit_link_plugin"}
           {$value}
index 428b8ee292ef8feddb9f426271b6d9942f500bb7..3820a4f7ea14c6799820ede1c878b1be6b8810b5 100644 (file)
@@ -8,6 +8,9 @@
 <link href="img/favicon.png" rel="shortcut icon" type="image/png" />
 <link href="img/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
 <link type="text/css" rel="stylesheet" href="css/shaarli.min.css?v={$version_hash}" />
+{if="$formatter==='markdown'"}
+  <link type="text/css" rel="stylesheet" href="css/markdown.min.css?v={$version_hash}" />
+{/if}
 {loop="$plugins_includes.css_files"}
   <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
 {/loop}
index 160286a58228ba0ff4aa2dc88d011b17443fcb50..53b0cad20ef6f3b74b424f271cf854a151280a47 100644 (file)
         </td>
       </tr>
 
+      <tr>
+        <td><b>Description formatter:</b></td>
+        <td>
+          <select name="formatter" id="formatter">
+            {loop="$formatter_available"}
+              <option value="{$value}" {if="$value===$formatter"}selected{/if}>
+                {$value|ucfirst}
+              </option>
+            {/loop}
+          </select>
+        </td>
+      </tr>
+
       <tr>
         <td><b>Timezone:</b></td>
         <td>
index 5fa7d19490b64295917691c2073176fb1ba937f4..6f7a330f4f979ab1f044f482f3a56dc26f24adae 100644 (file)
             <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input"
                 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" ><br>
 
-            {loop="$edit_link_plugin"}
+          {if="$formatter==='markdown'"}
+            <div class="md_help">
+              {'Description will be rendered with'|t}
+              <a href="http://daringfireball.net/projects/markdown/syntax" title="{'Markdown syntax documentation'|t}">
+                {'Markdown syntax'|t}
+              </a>.
+            </div>
+          {/if}
+
+          {loop="$edit_link_plugin"}
                 {$value}
             {/loop}
 
@@ -38,7 +47,6 @@
             &nbsp;<label for="lf_private"><i>Private</i></label><br><br>
             {/if}
             <input type="submit" value="Save" name="save_edit" class="bigbutton">
-            <input type="submit" value="Cancel" name="cancel_edit" class="bigbutton">
             {if="!$link_is_new && isset($link.id)"}
               <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}"
                  name="delete_link" class="bigbutton"
index 1c4ff79c74adcf2ee37ec0522c0c8f2820b4f348..8d273c441c95bb7f5123bfeae86114d1a75f25b7 100644 (file)
@@ -7,6 +7,9 @@
 <link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
 <link href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" />
 <link type="text/css" rel="stylesheet" href="css/shaarli.min.css" />
+{if="$formatter==='markdown'"}
+  <link type="text/css" rel="stylesheet" href="css/markdown.min.css?v={$version_hash}" />
+{/if}
 {loop="$plugins_includes.css_files"}
 <link type="text/css" rel="stylesheet" href="{$value}#"/>
 {/loop}
index ed548c735ad08444ba7adf204e93a2c14012e4ab..602147e51d3d200b8dcf45bbb6df7f951e08c6c3 100644 (file)
@@ -30,6 +30,7 @@ module.exports = [
         './assets/default/js/base.js',
         './assets/default/scss/shaarli.scss',
       ].concat(glob.sync('./assets/default/img/*')),
+      markdown: './assets/common/css/markdown.css',
     },
     output: {
       filename: '[name].min.js',
@@ -50,7 +51,7 @@ module.exports = [
           }
         },
         {
-          test: /\.scss/,
+          test: /\.s?css/,
           use: extractCssDefault.extract({
             use: [{
               loader: "css-loader",
@@ -97,6 +98,7 @@ module.exports = [
         './assets/vintage/css/reset.css',
         './assets/vintage/css/shaarli.css',
       ].concat(glob.sync('./assets/vintage/img/*')),
+      markdown: './assets/common/css/markdown.css',
       thumbnails: './assets/common/js/thumbnails.js',
       thumbnails_update: './assets/common/js/thumbnails-update.js',
     },