Apply the new system (Bookmark + Service) to the whole code base
See https://github.com/shaarli/Shaarli/issues/1307
This commit is contained in:
parent
336a28fa4a
commit
cf92b4dd15
31 changed files with 634 additions and 1505 deletions
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', '?'),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -389,6 +389,8 @@ protected function setDefaultValues()
|
|||
$this->setEmpty('translation.extensions', []);
|
||||
|
||||
$this->setEmpty('plugins', array());
|
||||
|
||||
$this->setEmpty('formatter', 'markdown');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>— ' . $permalink;
|
||||
$data['description'] .= PHP_EOL . PHP_EOL . '<br>— ' . $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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 . '.'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
|
@ -1,5 +0,0 @@
|
|||
<div class="md_help">
|
||||
%s
|
||||
<a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
|
||||
%s</a>.
|
||||
</div>
|
|
@ -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>."
|
|
@ -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 ' ' 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('/(^| ) /m', '$1 ', $description);
|
||||
}
|
||||
|
||||
function reverse_feed_permalink($description)
|
||||
{
|
||||
return preg_replace('@— <a href="([^"]+)" title="[^"]+">([^<]+)</a>$@im', '— [$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>.');
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
|||
<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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 @@
|
|||
<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&lf_linkdate={$link.id}&token={$token}"
|
||||
name="delete_link" class="bigbutton"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue