Apply the new system (Bookmark + Service) to the whole code base

See https://github.com/shaarli/Shaarli/issues/1307
This commit is contained in:
ArthurHoaro 2019-05-25 15:52:27 +02:00
parent 336a28fa4a
commit cf92b4dd15
31 changed files with 634 additions and 1505 deletions

View file

@ -3,6 +3,7 @@
use DateTime;
use Exception;
use Shaarli\Bookmark\Bookmark;
/**
* Class History
@ -20,7 +21,7 @@
* - 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 @@ protected function initialize()
/**
* 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 @@ public function updateSettings()
/**
* 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()
{

View 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;
}

View file

@ -3,6 +3,7 @@
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 @@ protected function checkToken($request)
}
/**
* 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 @@ protected function checkToken($request)
*/
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;
}

View 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 @@ public static function validateJwtToken($token, $secret)
/**
* 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 @@ public static function formatLink($link, $indexUrl)
}
/**
* 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 @@ public static function formatLink($link, $indexUrl)
* @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 @@ public static function updateLink($oldLink, $newLink)
* 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.
*/

View 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 @@ public function __construct(Container $ci)
{
$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;

View file

@ -41,7 +41,7 @@ public function getHistory($request, $response)
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);

View 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', '?'),

View file

@ -11,7 +11,7 @@
/**
* 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 @@
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 @@ public function getLinks($request, $response)
$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 @@ public function getLinks($request, $response)
*/
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 @@ public function getLink($request, $response, $args)
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 @@ public function postLink($request, $response)
);
}
$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 @@ public function postLink($request, $response)
*/
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 @@ public function putLink($request, $response, $args)
);
}
$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 @@ public function putLink($request, $response, $args)
*/
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);
}

View file

@ -5,6 +5,7 @@
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 @@
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 @@ public function getTags($request, $response)
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 @@ public function getTags($request, $response)
*/
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 @@ public function getTag($request, $response, $args)
*/
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 @@ public function putTag($request, $response, $args)
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 @@ public function putTag($request, $response, $args)
*/
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);
}

View file

@ -118,7 +118,7 @@ public function offsetUnset($offset)
$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]);
}

View file

@ -389,6 +389,8 @@ protected function setDefaultValues()
$this->setEmpty('translation.extensions', []);
$this->setEmpty('plugins', array());
$this->setEmpty('formatter', 'markdown');
}
/**

View 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 @@ public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLogg
*/
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 @@ public function buildData()
}
$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 @@ public function buildData()
/**
* 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 @@ protected function getIsoDate(DateTime $date, $format = false)
* 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)
{

View file

@ -57,6 +57,7 @@ public function formatDescription($bookmark)
$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 @@ function ($match) {
);
return $description;
}
protected function reverseEscapedHtml($description)
{
return unescape($description);
}
}

View file

@ -7,8 +7,10 @@
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 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in
*
* @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 @@ private static function importStatus(
$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 @@ private static function importStatus(
/**
* 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 @@ public static function import($post, $files, $linkDb, $conf, $history)
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 @@ public static function import($post, $files, $linkDb, $conf, $history)
);
}
// links are imported as public by default
// bookmarks are imported as public by default
$defaultPrivacy = 0;
$parser = new NetscapeBookmarkParser(
@ -164,22 +164,18 @@ public static function import($post, $files, $linkDb, $conf, $history)
// 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 @@ public static function import($post, $files, $linkDb, $conf, $history)
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;

View file

@ -5,7 +5,7 @@
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 @@ private function initialize()
$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 @@ private function initialize()
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);
}

View 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 @@ public function update()
}
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 @@ public function update()
if ($res === true) {
$updatesRan[] = $method->getName();
}
} catch (Exception $e) {
} catch (\Exception $e) {
throw new UpdaterException($method, $e);
}
}
@ -131,432 +111,4 @@ public function getDoneUpdates()
{
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;
}
}

View 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();
}
/**
* 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('Unable to write updates in '. $updatesFilepath . '.');
}
}
return array();
}
/**
* Write updates file.
*
* @param string $updatesFilepath Updates file path.
* @param array $updates Updates array to write.
*
* @throws Exception Couldn't write version number.
*/
function write_updates_file($updatesFilepath, $updates)
{
if (empty($updatesFilepath)) {
throw new Exception(t('Updates file path is not set, can\'t write updates.'));
}
$res = file_put_contents($updatesFilepath, implode(';', $updates));
if ($res === false) {
throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
}
}

View file

@ -140,7 +140,7 @@
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
hyphens: none;
}
.markdown :not(pre) code {
@ -155,7 +155,7 @@
}
/*
Remove header links style
Remove header bookmarks style
*/
#pageheader .md_help a {
color: lightgray;

View 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)

555
index.php

File diff suppressed because it is too large Load diff

View file

@ -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.

View file

@ -1,5 +0,0 @@
<div class="md_help">
%s
<a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
%s</a>.
</div>

View file

@ -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>."

View file

@ -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>.');
}

View file

@ -68,6 +68,28 @@ <h2 class="window-title">{'Configure'|t}</h2>
</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">

View 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 @@ <h2 class="window-title">
<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>
@ -50,6 +49,15 @@ <h2 class="window-title">
&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}

View 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}

View file

@ -32,6 +32,19 @@
</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>

View file

@ -26,7 +26,16 @@
<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"

View 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}

View 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',
},