Store bookmarks as PHP objects and add a service layer to retri… (#1307)

Store bookmarks as PHP objects and add a service layer to retrieve them
This commit is contained in:
ArthurHoaro 2020-01-18 10:01:06 +01:00 committed by GitHub
commit 3fb29fdda0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 8560 additions and 3299 deletions

View file

@ -80,7 +80,8 @@ locale_test_%:
--testsuite language-$(firstword $(subst _, ,$*)) --testsuite language-$(firstword $(subst _, ,$*))
all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR
@$(BIN)/phpcov merge --html coverage coverage @# --The current version is not compatible with PHP 7.2
@#$(BIN)/phpcov merge --html coverage coverage
@# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6) @# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6)
@#$(BIN)/phpcov merge --text coverage/txt coverage @#$(BIN)/phpcov merge --text coverage/txt coverage

View file

@ -3,6 +3,7 @@
use DateTime; use DateTime;
use Exception; use Exception;
use Shaarli\Bookmark\Bookmark;
/** /**
* Class History * Class History
@ -20,7 +21,7 @@
* - UPDATED: link updated * - UPDATED: link updated
* - DELETED: link deleted * - DELETED: link deleted
* - SETTINGS: the settings have been updated through the UI. * - 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. * 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. * Add Event: new link.
* *
* @param array $link Link data. * @param Bookmark $link Link data.
*/ */
public function addLink($link) public function addLink($link)
{ {
$this->addEvent(self::CREATED, $link['id']); $this->addEvent(self::CREATED, $link->getId());
} }
/** /**
* Add Event: update existing link. * Add Event: update existing link.
* *
* @param array $link Link data. * @param Bookmark $link Link data.
*/ */
public function updateLink($link) public function updateLink($link)
{ {
$this->addEvent(self::UPDATED, $link['id']); $this->addEvent(self::UPDATED, $link->getId());
} }
/** /**
* Add Event: delete existing link. * Add Event: delete existing link.
* *
* @param array $link Link data. * @param Bookmark $link Link data.
*/ */
public function deleteLink($link) 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. * 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() public function importLinks()
{ {

View file

@ -162,7 +162,7 @@ function generateLocation($referer, $host, $loopTerms = array())
$finalReferer = '?'; $finalReferer = '?';
// No referer if it contains any value in $loopCriteria. // No referer if it contains any value in $loopCriteria.
foreach ($loopTerms as $value) { foreach (array_filter($loopTerms) as $value) {
if (strpos($referer, $value) !== false) { if (strpos($referer, $value) !== false) {
return $finalReferer; return $finalReferer;
} }

View file

@ -3,6 +3,7 @@
use Shaarli\Api\Exceptions\ApiAuthorizationException; use Shaarli\Api\Exceptions\ApiAuthorizationException;
use Shaarli\Api\Exceptions\ApiException; use Shaarli\Api\Exceptions\ApiException;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Request; 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. * and load in the Slim container.
* *
* FIXME! LinkDB could use a refactoring to avoid this trick. * FIXME! LinkDB could use a refactoring to avoid this trick.
@ -126,10 +127,10 @@ protected function checkToken($request)
*/ */
protected function setLinkDb($conf) protected function setLinkDb($conf)
{ {
$linkDb = new \Shaarli\Bookmark\LinkDB( $linkDb = new BookmarkFileService(
$conf->get('resource.datastore'), $conf,
true, $this->container->get('history'),
$conf->get('privacy.hide_public_links') true
); );
$this->container['db'] = $linkDb; $this->container['db'] = $linkDb;
} }

View file

@ -2,6 +2,7 @@
namespace Shaarli\Api; namespace Shaarli\Api;
use Shaarli\Api\Exceptions\ApiAuthorizationException; use Shaarli\Api\Exceptions\ApiAuthorizationException;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Http\Base64Url; use Shaarli\Http\Base64Url;
/** /**
@ -54,28 +55,28 @@ public static function validateJwtToken($token, $secret)
/** /**
* Format a Link for the REST API. * Format a Link for the REST API.
* *
* @param array $link Link data read from the datastore. * @param Bookmark $bookmark Bookmark data read from the datastore.
* @param string $indexUrl Shaarli's index URL (used for relative URL). * @param string $indexUrl Shaarli's index URL (used for relative URL).
* *
* @return array Link data formatted for the REST API. * @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 // Not an internal link
if (! is_note($link['url'])) { if (! $bookmark->isNote()) {
$out['url'] = $link['url']; $out['url'] = $bookmark->getUrl();
} else { } else {
$out['url'] = $indexUrl . $link['url']; $out['url'] = $indexUrl . $bookmark->getUrl();
} }
$out['shorturl'] = $link['shorturl']; $out['shorturl'] = $bookmark->getShortUrl();
$out['title'] = $link['title']; $out['title'] = $bookmark->getTitle();
$out['description'] = $link['description']; $out['description'] = $bookmark->getDescription();
$out['tags'] = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY); $out['tags'] = $bookmark->getTags();
$out['private'] = $link['private'] == true; $out['private'] = $bookmark->isPrivate();
$out['created'] = $link['created']->format(\DateTime::ATOM); $out['created'] = $bookmark->getCreated()->format(\DateTime::ATOM);
if (! empty($link['updated'])) { if (! empty($bookmark->getUpdated())) {
$out['updated'] = $link['updated']->format(\DateTime::ATOM); $out['updated'] = $bookmark->getUpdated()->format(\DateTime::ATOM);
} else { } else {
$out['updated'] = ''; $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 URL is provided, it will generate a local note URL.
* If no title is provided, it will use the URL as title. * 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 array $input Request Link.
* @param bool $defaultPrivate Request Link. * @param bool $defaultPrivate Request Link.
* *
* @return array Formatted link. * @return Bookmark instance.
*/ */
public static function buildLinkFromRequest($input, $defaultPrivate) 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'])) { if (isset($input['private'])) {
$private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN); $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
} else { } else {
$private = $defaultPrivate; $private = $defaultPrivate;
} }
$link = [ $bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
'title' => ! empty($input['title']) ? $input['title'] : $input['url'], $bookmark->setUrl($url);
'url' => $input['url'], $bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
'description' => ! empty($input['description']) ? $input['description'] : '', $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '', $bookmark->setPrivate($private);
'private' => $private,
'created' => new \DateTime(), return $bookmark;
];
return $link;
} }
/** /**
* Update link fields using an updated link object. * Update link fields using an updated link object.
* *
* @param array $oldLink data * @param Bookmark $oldLink data
* @param array $newLink 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) public static function updateLink($oldLink, $newLink)
{ {
foreach (['title', 'url', 'description', 'tags', 'private'] as $field) { $oldLink->setTitle($newLink->getTitle());
$oldLink[$field] = $newLink[$field]; $oldLink->setUrl($newLink->getUrl());
} $oldLink->setDescription($newLink->getDescription());
$oldLink['updated'] = new \DateTime(); $oldLink->setTags($newLink->getTags());
$oldLink->setPrivate($newLink->isPrivate());
if (empty($oldLink['url'])) {
$oldLink['url'] = '?' . $oldLink['shorturl'];
}
if (empty($oldLink['title'])) {
$oldLink['title'] = $oldLink['url'];
}
return $oldLink; return $oldLink;
} }
@ -143,7 +136,7 @@ public static function updateLink($oldLink, $newLink)
* Format a Tag for the REST API. * Format a Tag for the REST API.
* *
* @param string $tag Tag name * @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. * @return array Link data formatted for the REST API.
*/ */

View file

@ -2,7 +2,7 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Slim\Container; use Slim\Container;
@ -26,9 +26,9 @@ abstract class ApiController
protected $conf; protected $conf;
/** /**
* @var LinkDB * @var BookmarkServiceInterface
*/ */
protected $linkDb; protected $bookmarkService;
/** /**
* @var HistoryController * @var HistoryController
@ -51,7 +51,7 @@ public function __construct(Container $ci)
{ {
$this->ci = $ci; $this->ci = $ci;
$this->conf = $ci->get('conf'); $this->conf = $ci->get('conf');
$this->linkDb = $ci->get('db'); $this->bookmarkService = $ci->get('db');
$this->history = $ci->get('history'); $this->history = $ci->get('history');
if ($this->conf->get('dev.debug', false)) { if ($this->conf->get('dev.debug', false)) {
$this->jsonStyle = JSON_PRETTY_PRINT; $this->jsonStyle = JSON_PRETTY_PRINT;

View file

@ -41,7 +41,7 @@ public function getHistory($request, $response)
throw new ApiBadParametersException('Invalid offset'); 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'); $limit = $request->getParam('limit');
if (empty($limit)) { if (empty($limit)) {
$limit = count($history); $limit = count($history);

View file

@ -2,6 +2,7 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\BookmarkFilter;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
@ -26,8 +27,8 @@ class Info extends ApiController
public function getInfo($request, $response) public function getInfo($request, $response)
{ {
$info = [ $info = [
'global_counter' => count($this->linkDb), 'global_counter' => $this->bookmarkService->count(),
'private_counter' => count_private($this->linkDb), 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
'settings' => array( 'settings' => array(
'title' => $this->conf->get('general.title', 'Shaarli'), 'title' => $this->conf->get('general.title', 'Shaarli'),
'header_link' => $this->conf->get('general.header_link', '?'), 'header_link' => $this->conf->get('general.header_link', '?'),

View file

@ -11,7 +11,7 @@
/** /**
* Class Links * Class Links
* *
* REST API Controller: all services related to links collection. * REST API Controller: all services related to bookmarks collection.
* *
* @package Api\Controllers * @package Api\Controllers
* @see http://shaarli.github.io/api-documentation/#links-links-collection * @see http://shaarli.github.io/api-documentation/#links-links-collection
@ -19,12 +19,12 @@
class Links extends ApiController 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; 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 Request $request Slim request.
* @param Response $response Slim response. * @param Response $response Slim response.
@ -36,33 +36,32 @@ class Links extends ApiController
public function getLinks($request, $response) public function getLinks($request, $response)
{ {
$private = $request->getParam('visibility'); $private = $request->getParam('visibility');
$links = $this->linkDb->filterSearch( $bookmarks = $this->bookmarkService->search(
[ [
'searchtags' => $request->getParam('searchtags', ''), 'searchtags' => $request->getParam('searchtags', ''),
'searchterm' => $request->getParam('searchterm', ''), 'searchterm' => $request->getParam('searchterm', ''),
], ],
false,
$private $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'); $offset = $request->getParam('offset');
if (! empty($offset) && ! ctype_digit($offset)) { if (! empty($offset) && ! ctype_digit($offset)) {
throw new ApiBadParametersException('Invalid offset'); throw new ApiBadParametersException('Invalid offset');
} }
$offset = ! empty($offset) ? intval($offset) : 0; $offset = ! empty($offset) ? intval($offset) : 0;
if ($offset > count($links)) { if ($offset > count($bookmarks)) {
return $response->withJson([], 200, $this->jsonStyle); 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'); $limit = $request->getParam('limit');
if (empty($limit)) { if (empty($limit)) {
$limit = self::$DEFAULT_LIMIT; $limit = self::$DEFAULT_LIMIT;
} elseif (ctype_digit($limit)) { } elseif (ctype_digit($limit)) {
$limit = intval($limit); $limit = intval($limit);
} elseif ($limit === 'all') { } elseif ($limit === 'all') {
$limit = count($links); $limit = count($bookmarks);
} else { } else {
throw new ApiBadParametersException('Invalid limit'); throw new ApiBadParametersException('Invalid limit');
} }
@ -72,12 +71,12 @@ public function getLinks($request, $response)
$out = []; $out = [];
$index = 0; $index = 0;
foreach ($links as $link) { foreach ($bookmarks as $bookmark) {
if (count($out) >= $limit) { if (count($out) >= $limit) {
break; break;
} }
if ($index++ >= $offset) { 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) public function getLink($request, $response, $args)
{ {
if (!isset($this->linkDb[$args['id']])) { if (!$this->bookmarkService->exists($args['id'])) {
throw new ApiLinkNotFoundException(); throw new ApiLinkNotFoundException();
} }
$index = index_url($this->ci['environment']); $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); return $response->withJson($out, 200, $this->jsonStyle);
} }
@ -117,9 +116,11 @@ public function getLink($request, $response, $args)
public function postLink($request, $response) public function postLink($request, $response)
{ {
$data = $request->getParsedBody(); $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 // 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( return $response->withJson(
ApiUtils::formatLink($dup, index_url($this->ci['environment'])), ApiUtils::formatLink($dup, index_url($this->ci['environment'])),
409, 409,
@ -127,23 +128,9 @@ public function postLink($request, $response)
); );
} }
$link['id'] = $this->linkDb->getNextId(); $this->bookmarkService->add($bookmark);
$link['shorturl'] = link_small_hash($link['created'], $link['id']); $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
$redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]);
// 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']]);
return $response->withAddedHeader('Location', $redirect) return $response->withAddedHeader('Location', $redirect)
->withJson($out, 201, $this->jsonStyle); ->withJson($out, 201, $this->jsonStyle);
} }
@ -161,18 +148,18 @@ public function postLink($request, $response)
*/ */
public function putLink($request, $response, $args) public function putLink($request, $response, $args)
{ {
if (! isset($this->linkDb[$args['id']])) { if (! $this->bookmarkService->exists($args['id'])) {
throw new ApiLinkNotFoundException(); throw new ApiLinkNotFoundException();
} }
$index = index_url($this->ci['environment']); $index = index_url($this->ci['environment']);
$data = $request->getParsedBody(); $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 // duplicate URL on a different link, return 409 Conflict
if (! empty($requestLink['url']) if (! empty($requestBookmark->getUrl())
&& ! empty($dup = $this->linkDb->getLinkFromUrl($requestLink['url'])) && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
&& $dup['id'] != $args['id'] && $dup->getId() != $args['id']
) { ) {
return $response->withJson( return $response->withJson(
ApiUtils::formatLink($dup, $index), ApiUtils::formatLink($dup, $index),
@ -181,13 +168,11 @@ public function putLink($request, $response, $args)
); );
} }
$responseLink = $this->linkDb[$args['id']]; $responseBookmark = $this->bookmarkService->get($args['id']);
$responseLink = ApiUtils::updateLink($responseLink, $requestLink); $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark);
$this->linkDb[$responseLink['id']] = $responseLink; $this->bookmarkService->set($responseBookmark);
$this->linkDb->save($this->conf->get('resource.page_cache'));
$this->history->updateLink($responseLink);
$out = ApiUtils::formatLink($responseLink, $index); $out = ApiUtils::formatLink($responseBookmark, $index);
return $response->withJson($out, 200, $this->jsonStyle); return $response->withJson($out, 200, $this->jsonStyle);
} }
@ -204,13 +189,11 @@ public function putLink($request, $response, $args)
*/ */
public function deleteLink($request, $response, $args) public function deleteLink($request, $response, $args)
{ {
if (! isset($this->linkDb[$args['id']])) { if (! $this->bookmarkService->exists($args['id'])) {
throw new ApiLinkNotFoundException(); throw new ApiLinkNotFoundException();
} }
$link = $this->linkDb[$args['id']]; $bookmark = $this->bookmarkService->get($args['id']);
unset($this->linkDb[(int) $args['id']]); $this->bookmarkService->remove($bookmark);
$this->linkDb->save($this->conf->get('resource.page_cache'));
$this->history->deleteLink($link);
return $response->withStatus(204); return $response->withStatus(204);
} }

View file

@ -5,6 +5,7 @@
use Shaarli\Api\ApiUtils; use Shaarli\Api\ApiUtils;
use Shaarli\Api\Exceptions\ApiBadParametersException; use Shaarli\Api\Exceptions\ApiBadParametersException;
use Shaarli\Api\Exceptions\ApiTagNotFoundException; use Shaarli\Api\Exceptions\ApiTagNotFoundException;
use Shaarli\Bookmark\BookmarkFilter;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
@ -18,7 +19,7 @@
class Tags extends ApiController 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'; public static $DEFAULT_LIMIT = 'all';
@ -35,7 +36,7 @@ class Tags extends ApiController
public function getTags($request, $response) public function getTags($request, $response)
{ {
$visibility = $request->getParam('visibility'); $visibility = $request->getParam('visibility');
$tags = $this->linkDb->linksCountPerTag([], $visibility); $tags = $this->bookmarkService->bookmarksCountPerTag([], $visibility);
// Return tags from the {offset}th tag, starting from 0. // Return tags from the {offset}th tag, starting from 0.
$offset = $request->getParam('offset'); $offset = $request->getParam('offset');
@ -47,7 +48,7 @@ public function getTags($request, $response)
return $response->withJson([], 200, $this->jsonStyle); 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'); $limit = $request->getParam('limit');
if (empty($limit)) { if (empty($limit)) {
$limit = self::$DEFAULT_LIMIT; $limit = self::$DEFAULT_LIMIT;
@ -87,7 +88,7 @@ public function getTags($request, $response)
*/ */
public function getTag($request, $response, $args) public function getTag($request, $response, $args)
{ {
$tags = $this->linkDb->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
if (!isset($tags[$args['tagName']])) { if (!isset($tags[$args['tagName']])) {
throw new ApiTagNotFoundException(); throw new ApiTagNotFoundException();
} }
@ -111,7 +112,7 @@ public function getTag($request, $response, $args)
*/ */
public function putTag($request, $response, $args) public function putTag($request, $response, $args)
{ {
$tags = $this->linkDb->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
if (! isset($tags[$args['tagName']])) { if (! isset($tags[$args['tagName']])) {
throw new ApiTagNotFoundException(); 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'); throw new ApiBadParametersException('New tag name is required in the request body');
} }
$updated = $this->linkDb->renameTag($args['tagName'], $data['name']); $bookmarks = $this->bookmarkService->search(
$this->linkDb->save($this->conf->get('resource.page_cache')); ['searchtags' => $args['tagName']],
foreach ($updated as $link) { BookmarkFilter::$ALL,
$this->history->updateLink($link); 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']]); $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]);
return $response->withJson($out, 200, $this->jsonStyle); return $response->withJson($out, 200, $this->jsonStyle);
} }
@ -145,15 +152,22 @@ public function putTag($request, $response, $args)
*/ */
public function deleteTag($request, $response, $args) public function deleteTag($request, $response, $args)
{ {
$tags = $this->linkDb->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
if (! isset($tags[$args['tagName']])) { if (! isset($tags[$args['tagName']])) {
throw new ApiTagNotFoundException(); throw new ApiTagNotFoundException();
} }
$updated = $this->linkDb->renameTag($args['tagName'], null);
$this->linkDb->save($this->conf->get('resource.page_cache')); $bookmarks = $this->bookmarkService->search(
foreach ($updated as $link) { ['searchtags' => $args['tagName']],
$this->history->updateLink($link); 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); return $response->withStatus(204);
} }

View file

@ -0,0 +1,461 @@
<?php
namespace Shaarli\Bookmark;
use DateTime;
use Shaarli\Bookmark\Exception\InvalidBookmarkException;
/**
* Class Bookmark
*
* This class represent a single Bookmark with all its attributes.
* Every bookmark should manipulated using this, before being formatted.
*
* @package Shaarli\Bookmark
*/
class Bookmark
{
/** @var string Date format used in string (former ID format) */
const LINK_DATE_FORMAT = 'Ymd_His';
/** @var int Bookmark ID */
protected $id;
/** @var string Permalink identifier */
protected $shortUrl;
/** @var string Bookmark's URL - $shortUrl prefixed with `?` for notes */
protected $url;
/** @var string Bookmark's title */
protected $title;
/** @var string Raw bookmark's description */
protected $description;
/** @var array List of bookmark's tags */
protected $tags;
/** @var string Thumbnail's URL - false if no thumbnail could be found */
protected $thumbnail;
/** @var bool Set to true if the bookmark is set as sticky */
protected $sticky;
/** @var DateTime Creation datetime */
protected $created;
/** @var DateTime Update datetime */
protected $updated;
/** @var bool True if the bookmark can only be seen while logged in */
protected $private;
/**
* Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
*
* @param array $data
*
* @return $this
*/
public function fromArray($data)
{
$this->id = $data['id'];
$this->shortUrl = $data['shorturl'];
$this->url = $data['url'];
$this->title = $data['title'];
$this->description = $data['description'];
$this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null;
$this->sticky = isset($data['sticky']) ? $data['sticky'] : false;
$this->created = $data['created'];
if (is_array($data['tags'])) {
$this->tags = $data['tags'];
} else {
$this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY);
}
if (! empty($data['updated'])) {
$this->updated = $data['updated'];
}
$this->private = $data['private'] ? true : false;
return $this;
}
/**
* Make sure that the current instance of Bookmark is valid and can be saved into the data store.
* A valid link requires:
* - an integer ID
* - a short URL (for permalinks)
* - a creation date
*
* This function also initialize optional empty fields:
* - the URL with the permalink
* - the title with the URL
*
* @throws InvalidBookmarkException
*/
public function validate()
{
if ($this->id === null
|| ! is_int($this->id)
|| empty($this->shortUrl)
|| empty($this->created)
|| ! $this->created instanceof DateTime
) {
throw new InvalidBookmarkException($this);
}
if (empty($this->url)) {
$this->url = '?'. $this->shortUrl;
}
if (empty($this->title)) {
$this->title = $this->url;
}
}
/**
* Set the Id.
* If they're not already initialized, this function also set:
* - created: with the current datetime
* - shortUrl: with a generated small hash from the date and the given ID
*
* @param int $id
*
* @return Bookmark
*/
public function setId($id)
{
$this->id = $id;
if (empty($this->created)) {
$this->created = new DateTime();
}
if (empty($this->shortUrl)) {
$this->shortUrl = link_small_hash($this->created, $this->id);
}
return $this;
}
/**
* Get the Id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Get the ShortUrl.
*
* @return string
*/
public function getShortUrl()
{
return $this->shortUrl;
}
/**
* Get the Url.
*
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Get the Title.
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Get the Description.
*
* @return string
*/
public function getDescription()
{
return ! empty($this->description) ? $this->description : '';
}
/**
* Get the Created.
*
* @return DateTime
*/
public function getCreated()
{
return $this->created;
}
/**
* Get the Updated.
*
* @return DateTime
*/
public function getUpdated()
{
return $this->updated;
}
/**
* Set the ShortUrl.
*
* @param string $shortUrl
*
* @return Bookmark
*/
public function setShortUrl($shortUrl)
{
$this->shortUrl = $shortUrl;
return $this;
}
/**
* Set the Url.
*
* @param string $url
* @param array $allowedProtocols
*
* @return Bookmark
*/
public function setUrl($url, $allowedProtocols = [])
{
$url = trim($url);
if (! empty($url)) {
$url = whitelist_protocols($url, $allowedProtocols);
}
$this->url = $url;
return $this;
}
/**
* Set the Title.
*
* @param string $title
*
* @return Bookmark
*/
public function setTitle($title)
{
$this->title = trim($title);
return $this;
}
/**
* Set the Description.
*
* @param string $description
*
* @return Bookmark
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Set the Created.
* Note: you shouldn't set this manually except for special cases (like bookmark import)
*
* @param DateTime $created
*
* @return Bookmark
*/
public function setCreated($created)
{
$this->created = $created;
return $this;
}
/**
* Set the Updated.
*
* @param DateTime $updated
*
* @return Bookmark
*/
public function setUpdated($updated)
{
$this->updated = $updated;
return $this;
}
/**
* Get the Private.
*
* @return bool
*/
public function isPrivate()
{
return $this->private ? true : false;
}
/**
* Set the Private.
*
* @param bool $private
*
* @return Bookmark
*/
public function setPrivate($private)
{
$this->private = $private ? true : false;
return $this;
}
/**
* Get the Tags.
*
* @return array
*/
public function getTags()
{
return is_array($this->tags) ? $this->tags : [];
}
/**
* Set the Tags.
*
* @param array $tags
*
* @return Bookmark
*/
public function setTags($tags)
{
$this->setTagsString(implode(' ', $tags));
return $this;
}
/**
* Get the Thumbnail.
*
* @return string|bool
*/
public function getThumbnail()
{
return !$this->isNote() ? $this->thumbnail : false;
}
/**
* Set the Thumbnail.
*
* @param string|bool $thumbnail
*
* @return Bookmark
*/
public function setThumbnail($thumbnail)
{
$this->thumbnail = $thumbnail;
return $this;
}
/**
* Get the Sticky.
*
* @return bool
*/
public function isSticky()
{
return $this->sticky ? true : false;
}
/**
* Set the Sticky.
*
* @param bool $sticky
*
* @return Bookmark
*/
public function setSticky($sticky)
{
$this->sticky = $sticky ? true : false;
return $this;
}
/**
* @return string Bookmark's tags as a string, separated by a space
*/
public function getTagsString()
{
return implode(' ', $this->getTags());
}
/**
* @return bool
*/
public function isNote()
{
// We check empty value to get a valid result if the link has not been saved yet
return empty($this->url) || $this->url[0] === '?';
}
/**
* Set tags from a string.
* Note:
* - tags must be separated whether by a space or a comma
* - multiple spaces will be removed
* - trailing dash in tags will be removed
*
* @param string $tags
*
* @return $this
*/
public function setTagsString($tags)
{
// Remove first '-' char in tags.
$tags = preg_replace('/(^| )\-/', '$1', $tags);
// Explode all tags separted by spaces or commas
$tags = preg_split('/[\s,]+/', $tags);
// Remove eventual empty values
$tags = array_values(array_filter($tags));
$this->tags = $tags;
return $this;
}
/**
* Rename a tag in tags list.
*
* @param string $fromTag
* @param string $toTag
*/
public function renameTag($fromTag, $toTag)
{
if (($pos = array_search($fromTag, $this->tags)) !== false) {
$this->tags[$pos] = trim($toTag);
}
}
/**
* Delete a tag from tags list.
*
* @param string $tag
*/
public function deleteTag($tag)
{
if (($pos = array_search($tag, $this->tags)) !== false) {
unset($this->tags[$pos]);
$this->tags = array_values($this->tags);
}
}
}

View file

@ -0,0 +1,259 @@
<?php
namespace Shaarli\Bookmark;
use Shaarli\Bookmark\Exception\InvalidBookmarkException;
/**
* Class BookmarkArray
*
* Implementing ArrayAccess, this allows us to use the bookmark list
* as an array and iterate over it.
*
* @package Shaarli\Bookmark
*/
class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
{
/**
* @var Bookmark[]
*/
protected $bookmarks;
/**
* @var array List of all bookmarks IDS mapped with their array offset.
* Map: id->offset.
*/
protected $ids;
/**
* @var int Position in the $this->keys array (for the Iterator interface)
*/
protected $position;
/**
* @var array List of offset keys (for the Iterator interface implementation)
*/
protected $keys;
/**
* @var array List of all recorded URLs (key=url, value=bookmark offset)
* for fast reserve search (url-->bookmark offset)
*/
protected $urls;
public function __construct()
{
$this->ids = [];
$this->bookmarks = [];
$this->keys = [];
$this->urls = [];
$this->position = 0;
}
/**
* Countable - Counts elements of an object
*
* @return int Number of bookmarks
*/
public function count()
{
return count($this->bookmarks);
}
/**
* ArrayAccess - Assigns a value to the specified offset
*
* @param int $offset Bookmark ID
* @param Bookmark $value instance
*
* @throws InvalidBookmarkException
*/
public function offsetSet($offset, $value)
{
if (! $value instanceof Bookmark
|| $value->getId() === null || empty($value->getUrl())
|| ($offset !== null && ! is_int($offset)) || ! is_int($value->getId())
|| $offset !== null && $offset !== $value->getId()
) {
throw new InvalidBookmarkException($value);
}
// If the bookmark exists, we reuse the real offset, otherwise new entry
if ($offset !== null) {
$existing = $this->getBookmarkOffset($offset);
} else {
$existing = $this->getBookmarkOffset($value->getId());
}
if ($existing !== null) {
$offset = $existing;
} else {
$offset = count($this->bookmarks);
}
$this->bookmarks[$offset] = $value;
$this->urls[$value->getUrl()] = $offset;
$this->ids[$value->getId()] = $offset;
}
/**
* ArrayAccess - Whether or not an offset exists
*
* @param int $offset Bookmark ID
*
* @return bool true if it exists, false otherwise
*/
public function offsetExists($offset)
{
return array_key_exists($this->getBookmarkOffset($offset), $this->bookmarks);
}
/**
* ArrayAccess - Unsets an offset
*
* @param int $offset Bookmark ID
*/
public function offsetUnset($offset)
{
$realOffset = $this->getBookmarkOffset($offset);
$url = $this->bookmarks[$realOffset]->getUrl();
unset($this->urls[$url]);
unset($this->ids[$offset]);
unset($this->bookmarks[$realOffset]);
}
/**
* ArrayAccess - Returns the value at specified offset
*
* @param int $offset Bookmark ID
*
* @return Bookmark|null The Bookmark if found, null otherwise
*/
public function offsetGet($offset)
{
$realOffset = $this->getBookmarkOffset($offset);
return isset($this->bookmarks[$realOffset]) ? $this->bookmarks[$realOffset] : null;
}
/**
* Iterator - Returns the current element
*
* @return Bookmark corresponding to the current position
*/
public function current()
{
return $this[$this->keys[$this->position]];
}
/**
* Iterator - Returns the key of the current element
*
* @return int Bookmark ID corresponding to the current position
*/
public function key()
{
return $this->keys[$this->position];
}
/**
* Iterator - Moves forward to next element
*/
public function next()
{
++$this->position;
}
/**
* Iterator - Rewinds the Iterator to the first element
*
* Entries are sorted by date (latest first)
*/
public function rewind()
{
$this->keys = array_keys($this->ids);
$this->position = 0;
}
/**
* Iterator - Checks if current position is valid
*
* @return bool true if the current Bookmark ID exists, false otherwise
*/
public function valid()
{
return isset($this->keys[$this->position]);
}
/**
* Returns a bookmark offset in bookmarks array from its unique ID.
*
* @param int $id Persistent ID of a bookmark.
*
* @return int Real offset in local array, or null if doesn't exist.
*/
protected function getBookmarkOffset($id)
{
if (isset($this->ids[$id])) {
return $this->ids[$id];
}
return null;
}
/**
* Return the next key for bookmark creation.
* E.g. If the last ID is 597, the next will be 598.
*
* @return int next ID.
*/
public function getNextId()
{
if (!empty($this->ids)) {
return max(array_keys($this->ids)) + 1;
}
return 0;
}
/**
* @param $url
*
* @return Bookmark|null
*/
public function getByUrl($url)
{
if (! empty($url)
&& isset($this->urls[$url])
&& isset($this->bookmarks[$this->urls[$url]])
) {
return $this->bookmarks[$this->urls[$url]];
}
return null;
}
/**
* Reorder links by creation date (newest first).
*
* Also update the urls and ids mapping arrays.
*
* @param string $order ASC|DESC
*/
public function reorder($order = 'DESC')
{
$order = $order === 'ASC' ? -1 : 1;
// Reorder array by dates.
usort($this->bookmarks, function ($a, $b) use ($order) {
/** @var $a Bookmark */
/** @var $b Bookmark */
if ($a->isSticky() !== $b->isSticky()) {
return $a->isSticky() ? -1 : 1;
}
return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order;
});
$this->urls = [];
$this->ids = [];
foreach ($this->bookmarks as $key => $bookmark) {
$this->urls[$bookmark->getUrl()] = $key;
$this->ids[$bookmark->getId()] = $key;
}
}
}

View file

@ -0,0 +1,373 @@
<?php
namespace Shaarli\Bookmark;
use Exception;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Legacy\LegacyLinkDB;
use Shaarli\Legacy\LegacyUpdater;
use Shaarli\Updater\UpdaterUtils;
/**
* Class BookmarksService
*
* This is the entry point to manipulate the bookmark DB.
* It manipulates loads links from a file data store containing all bookmarks.
*
* It also triggers the legacy format (bookmarks as arrays) migration.
*/
class BookmarkFileService implements BookmarkServiceInterface
{
/** @var Bookmark[] instance */
protected $bookmarks;
/** @var BookmarkIO instance */
protected $bookmarksIO;
/** @var BookmarkFilter */
protected $bookmarkFilter;
/** @var ConfigManager instance */
protected $conf;
/** @var History instance */
protected $history;
/** @var bool true for logged in users. Default value to retrieve private bookmarks. */
protected $isLoggedIn;
/**
* @inheritDoc
*/
public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
{
$this->conf = $conf;
$this->history = $history;
$this->bookmarksIO = new BookmarkIO($this->conf);
$this->isLoggedIn = $isLoggedIn;
if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
$this->bookmarks = [];
} else {
try {
$this->bookmarks = $this->bookmarksIO->read();
} catch (EmptyDataStoreException $e) {
$this->bookmarks = new BookmarkArray();
if ($isLoggedIn) {
$this->save();
}
}
if (! $this->bookmarks instanceof BookmarkArray) {
$this->migrate();
exit(
'Your data store has been migrated, please reload the page.'. PHP_EOL .
'If this message keeps showing up, please delete data/updates.txt file.'
);
}
}
$this->bookmarkFilter = new BookmarkFilter($this->bookmarks);
}
/**
* @inheritDoc
*/
public function findByHash($hash)
{
$bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
// PHP 7.3 introduced array_key_first() to avoid this hack
$first = reset($bookmark);
if (! $this->isLoggedIn && $first->isPrivate()) {
throw new Exception('Not authorized');
}
return $bookmark;
}
/**
* @inheritDoc
*/
public function findByUrl($url)
{
return $this->bookmarks->getByUrl($url);
}
/**
* @inheritDoc
*/
public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false)
{
if ($visibility === null) {
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
}
// Filter bookmark database according to parameters.
$searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
$searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
return $this->bookmarkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
[$searchtags, $searchterm],
$caseSensitive,
$visibility,
$untaggedOnly
);
}
/**
* @inheritDoc
*/
public function get($id, $visibility = null)
{
if (! isset($this->bookmarks[$id])) {
throw new BookmarkNotFoundException();
}
if ($visibility === null) {
$visibility = $this->isLoggedIn ? 'all' : 'public';
}
$bookmark = $this->bookmarks[$id];
if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
|| (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
) {
throw new Exception('Unauthorized');
}
return $bookmark;
}
/**
* @inheritDoc
*/
public function set($bookmark, $save = true)
{
if ($this->isLoggedIn !== true) {
throw new Exception(t('You\'re not authorized to alter the datastore'));
}
if (! $bookmark instanceof Bookmark) {
throw new Exception(t('Provided data is invalid'));
}
if (! isset($this->bookmarks[$bookmark->getId()])) {
throw new BookmarkNotFoundException();
}
$bookmark->validate();
$bookmark->setUpdated(new \DateTime());
$this->bookmarks[$bookmark->getId()] = $bookmark;
if ($save === true) {
$this->save();
$this->history->updateLink($bookmark);
}
return $this->bookmarks[$bookmark->getId()];
}
/**
* @inheritDoc
*/
public function add($bookmark, $save = true)
{
if ($this->isLoggedIn !== true) {
throw new Exception(t('You\'re not authorized to alter the datastore'));
}
if (! $bookmark instanceof Bookmark) {
throw new Exception(t('Provided data is invalid'));
}
if (! empty($bookmark->getId())) {
throw new Exception(t('This bookmarks already exists'));
}
$bookmark->setId($this->bookmarks->getNextId());
$bookmark->validate();
$this->bookmarks[$bookmark->getId()] = $bookmark;
if ($save === true) {
$this->save();
$this->history->addLink($bookmark);
}
return $this->bookmarks[$bookmark->getId()];
}
/**
* @inheritDoc
*/
public function addOrSet($bookmark, $save = true)
{
if ($this->isLoggedIn !== true) {
throw new Exception(t('You\'re not authorized to alter the datastore'));
}
if (! $bookmark instanceof Bookmark) {
throw new Exception('Provided data is invalid');
}
if ($bookmark->getId() === null) {
return $this->add($bookmark, $save);
}
return $this->set($bookmark, $save);
}
/**
* @inheritDoc
*/
public function remove($bookmark, $save = true)
{
if ($this->isLoggedIn !== true) {
throw new Exception(t('You\'re not authorized to alter the datastore'));
}
if (! $bookmark instanceof Bookmark) {
throw new Exception(t('Provided data is invalid'));
}
if (! isset($this->bookmarks[$bookmark->getId()])) {
throw new BookmarkNotFoundException();
}
unset($this->bookmarks[$bookmark->getId()]);
if ($save === true) {
$this->save();
$this->history->deleteLink($bookmark);
}
}
/**
* @inheritDoc
*/
public function exists($id, $visibility = null)
{
if (! isset($this->bookmarks[$id])) {
return false;
}
if ($visibility === null) {
$visibility = $this->isLoggedIn ? 'all' : 'public';
}
$bookmark = $this->bookmarks[$id];
if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
|| (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
) {
return false;
}
return true;
}
/**
* @inheritDoc
*/
public function count($visibility = null)
{
return count($this->search([], $visibility));
}
/**
* @inheritDoc
*/
public function save()
{
if (!$this->isLoggedIn) {
// TODO: raise an Exception instead
die('You are not authorized to change the database.');
}
$this->bookmarks->reorder();
$this->bookmarksIO->write($this->bookmarks);
invalidateCaches($this->conf->get('resource.page_cache'));
}
/**
* @inheritDoc
*/
public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
{
$bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
$tags = [];
$caseMapping = [];
foreach ($bookmarks as $bookmark) {
foreach ($bookmark->getTags() as $tag) {
if (empty($tag) || (! $this->isLoggedIn && startsWith($tag, '.'))) {
continue;
}
// The first case found will be displayed.
if (!isset($caseMapping[strtolower($tag)])) {
$caseMapping[strtolower($tag)] = $tag;
$tags[$caseMapping[strtolower($tag)]] = 0;
}
$tags[$caseMapping[strtolower($tag)]]++;
}
}
/*
* Formerly used arsort(), which doesn't define the sort behaviour for equal values.
* Also, this function doesn't produce the same result between PHP 5.6 and 7.
*
* So we now use array_multisort() to sort tags by DESC occurrences,
* then ASC alphabetically for equal values.
*
* @see https://github.com/shaarli/Shaarli/issues/1142
*/
$keys = array_keys($tags);
$tmpTags = array_combine($keys, $keys);
array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
return $tags;
}
/**
* @inheritDoc
*/
public function days()
{
$bookmarkDays = [];
foreach ($this->search() as $bookmark) {
$bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
}
$bookmarkDays = array_keys($bookmarkDays);
sort($bookmarkDays);
return $bookmarkDays;
}
/**
* @inheritDoc
*/
public function filterDay($request)
{
return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request);
}
/**
* @inheritDoc
*/
public function initialize()
{
$initializer = new BookmarkInitializer($this);
$initializer->initialize();
}
/**
* Handles migration to the new database format (BookmarksArray).
*/
protected function migrate()
{
$bookmarkDb = new LegacyLinkDB(
$this->conf->get('resource.datastore'),
true,
false
);
$updater = new LegacyUpdater(
UpdaterUtils::read_updates_file($this->conf->get('resource.updates')),
$bookmarkDb,
$this->conf,
true
);
$newUpdates = $updater->update();
if (! empty($newUpdates)) {
UpdaterUtils::write_updates_file(
$this->conf->get('resource.updates'),
$updater->getDoneUpdates()
);
}
}
}

View file

@ -0,0 +1,468 @@
<?php
namespace Shaarli\Bookmark;
use Exception;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
/**
* Class LinkFilter.
*
* Perform search and filter operation on link data list.
*/
class BookmarkFilter
{
/**
* @var string permalinks.
*/
public static $FILTER_HASH = 'permalink';
/**
* @var string text search.
*/
public static $FILTER_TEXT = 'fulltext';
/**
* @var string tag filter.
*/
public static $FILTER_TAG = 'tags';
/**
* @var string filter by day.
*/
public static $FILTER_DAY = 'FILTER_DAY';
/**
* @var string filter by day.
*/
public static $DEFAULT = 'NO_FILTER';
/** @var string Visibility: all */
public static $ALL = 'all';
/** @var string Visibility: public */
public static $PUBLIC = 'public';
/** @var string Visibility: private */
public static $PRIVATE = 'private';
/**
* @var string Allowed characters for hashtags (regex syntax).
*/
public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
/**
* @var Bookmark[] all available bookmarks.
*/
private $bookmarks;
/**
* @param Bookmark[] $bookmarks initialization.
*/
public function __construct($bookmarks)
{
$this->bookmarks = $bookmarks;
}
/**
* Filter bookmarks according to parameters.
*
* @param string $type Type of filter (eg. tags, permalink, etc.).
* @param mixed $request Filter content.
* @param bool $casesensitive Optional: Perform case sensitive filter if true.
* @param string $visibility Optional: return only all/private/public bookmarks
* @param bool $untaggedonly Optional: return only untagged bookmarks. Applies only if $type includes FILTER_TAG
*
* @return Bookmark[] filtered bookmark list.
*
* @throws BookmarkNotFoundException
*/
public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false)
{
if (!in_array($visibility, ['all', 'public', 'private'])) {
$visibility = 'all';
}
switch ($type) {
case self::$FILTER_HASH:
return $this->filterSmallHash($request);
case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext"
$noRequest = empty($request) || (empty($request[0]) && empty($request[1]));
if ($noRequest) {
if ($untaggedonly) {
return $this->filterUntagged($visibility);
}
return $this->noFilter($visibility);
}
if ($untaggedonly) {
$filtered = $this->filterUntagged($visibility);
} else {
$filtered = $this->bookmarks;
}
if (!empty($request[0])) {
$filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
}
if (!empty($request[1])) {
$filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility);
}
return $filtered;
case self::$FILTER_TEXT:
return $this->filterFulltext($request, $visibility);
case self::$FILTER_TAG:
if ($untaggedonly) {
return $this->filterUntagged($visibility);
} else {
return $this->filterTags($request, $casesensitive, $visibility);
}
case self::$FILTER_DAY:
return $this->filterDay($request);
default:
return $this->noFilter($visibility);
}
}
/**
* Unknown filter, but handle private only.
*
* @param string $visibility Optional: return only all/private/public bookmarks
*
* @return Bookmark[] filtered bookmarks.
*/
private function noFilter($visibility = 'all')
{
if ($visibility === 'all') {
return $this->bookmarks;
}
$out = array();
foreach ($this->bookmarks as $key => $value) {
if ($value->isPrivate() && $visibility === 'private') {
$out[$key] = $value;
} elseif (!$value->isPrivate() && $visibility === 'public') {
$out[$key] = $value;
}
}
return $out;
}
/**
* Returns the shaare corresponding to a smallHash.
*
* @param string $smallHash permalink hash.
*
* @return array $filtered array containing permalink data.
*
* @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link.
*/
private function filterSmallHash($smallHash)
{
foreach ($this->bookmarks as $key => $l) {
if ($smallHash == $l->getShortUrl()) {
// Yes, this is ugly and slow
return [$key => $l];
}
}
throw new BookmarkNotFoundException();
}
/**
* Returns the list of bookmarks corresponding to a full-text search
*
* Searches:
* - in the URLs, title and description;
* - are case-insensitive;
* - terms surrounded by quotes " are exact terms search.
* - terms starting with a dash - are excluded (except exact terms).
*
* Example:
* print_r($mydb->filterFulltext('hollandais'));
*
* mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
* - allows to perform searches on Unicode text
* - see https://github.com/shaarli/Shaarli/issues/75 for examples
*
* @param string $searchterms search query.
* @param string $visibility Optional: return only all/private/public bookmarks.
*
* @return array search results.
*/
private function filterFulltext($searchterms, $visibility = 'all')
{
if (empty($searchterms)) {
return $this->noFilter($visibility);
}
$filtered = array();
$search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
$exactRegex = '/"([^"]+)"/';
// Retrieve exact search terms.
preg_match_all($exactRegex, $search, $exactSearch);
$exactSearch = array_values(array_filter($exactSearch[1]));
// Remove exact search terms to get AND terms search.
$explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search)));
$explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
// Filter excluding terms and update andSearch.
$excludeSearch = array();
$andSearch = array();
foreach ($explodedSearchAnd as $needle) {
if ($needle[0] == '-' && strlen($needle) > 1) {
$excludeSearch[] = substr($needle, 1);
} else {
$andSearch[] = $needle;
}
}
// Iterate over every stored link.
foreach ($this->bookmarks as $id => $link) {
// ignore non private bookmarks when 'privatonly' is on.
if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') {
continue;
} elseif ($link->isPrivate() && $visibility === 'public') {
continue;
}
}
// Concatenate link fields to search across fields.
// Adds a '\' separator for exact search terms.
$content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
$content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
$content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
$content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\';
// Be optimistic
$found = true;
// First, we look for exact term search
for ($i = 0; $i < count($exactSearch) && $found; $i++) {
$found = strpos($content, $exactSearch[$i]) !== false;
}
// Iterate over keywords, if keyword is not found,
// no need to check for the others. We want all or nothing.
for ($i = 0; $i < count($andSearch) && $found; $i++) {
$found = strpos($content, $andSearch[$i]) !== false;
}
// Exclude terms.
for ($i = 0; $i < count($excludeSearch) && $found; $i++) {
$found = strpos($content, $excludeSearch[$i]) === false;
}
if ($found) {
$filtered[$id] = $link;
}
}
return $filtered;
}
/**
* generate a regex fragment out of a tag
*
* @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
*
* @return string generated regex fragment
*/
private static function tag2regex($tag)
{
$len = strlen($tag);
if (!$len || $tag === "-" || $tag === "*") {
// nothing to search, return empty regex
return '';
}
if ($tag[0] === "-") {
// query is negated
$i = 1; // use offset to start after '-' character
$regex = '(?!'; // create negative lookahead
} else {
$i = 0; // start at first character
$regex = '(?='; // use positive lookahead
}
$regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
// iterate over string, separating it into placeholder and content
for (; $i < $len; $i++) {
if ($tag[$i] === '*') {
// placeholder found
$regex .= '[^ ]*?';
} else {
// regular characters
$offset = strpos($tag, '*', $i);
if ($offset === false) {
// no placeholder found, set offset to end of string
$offset = $len;
}
// subtract one, as we want to get before the placeholder or end of string
$offset -= 1;
// we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
$regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
// move $i on
$i = $offset;
}
}
$regex .= '(?:$| ))'; // after the tag may only be a space or the end
return $regex;
}
/**
* Returns the list of bookmarks associated with a given list of tags
*
* You can specify one or more tags, separated by space or a comma, e.g.
* print_r($mydb->filterTags('linux programming'));
*
* @param string $tags list of tags separated by commas or blank spaces.
* @param bool $casesensitive ignore case if false.
* @param string $visibility Optional: return only all/private/public bookmarks.
*
* @return array filtered bookmarks.
*/
public function filterTags($tags, $casesensitive = false, $visibility = 'all')
{
// get single tags (we may get passed an array, even though the docs say different)
$inputTags = $tags;
if (!is_array($tags)) {
// we got an input string, split tags
$inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
}
if (!count($inputTags)) {
// no input tags
return $this->noFilter($visibility);
}
// If we only have public visibility, we can't look for hidden tags
if ($visibility === self::$PUBLIC) {
$inputTags = array_values(array_filter($inputTags, function ($tag) {
return ! startsWith($tag, '.');
}));
if (empty($inputTags)) {
return [];
}
}
// build regex from all tags
$re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
if (!$casesensitive) {
// make regex case insensitive
$re .= 'i';
}
// create resulting array
$filtered = [];
// iterate over each link
foreach ($this->bookmarks as $key => $link) {
// check level of visibility
// ignore non private bookmarks when 'privateonly' is on.
if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') {
continue;
} elseif ($link->isPrivate() && $visibility === 'public') {
continue;
}
}
$search = $link->getTagsString(); // build search string, start with tags of current link
if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
// description given and at least one possible tag found
$descTags = array();
// find all tags in the form of #tag in the description
preg_match_all(
'/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
$link->getDescription(),
$descTags
);
if (count($descTags[1])) {
// there were some tags in the description, add them to the search string
$search .= ' ' . implode(' ', $descTags[1]);
}
};
// match regular expression with search string
if (!preg_match($re, $search)) {
// this entry does _not_ match our regex
continue;
}
$filtered[$key] = $link;
}
return $filtered;
}
/**
* Return only bookmarks without any tag.
*
* @param string $visibility return only all/private/public bookmarks.
*
* @return array filtered bookmarks.
*/
public function filterUntagged($visibility)
{
$filtered = [];
foreach ($this->bookmarks as $key => $link) {
if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') {
continue;
} elseif ($link->isPrivate() && $visibility === 'public') {
continue;
}
}
if (empty(trim($link->getTagsString()))) {
$filtered[$key] = $link;
}
}
return $filtered;
}
/**
* Returns the list of articles for a given day, chronologically sorted
*
* Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
* print_r($mydb->filterDay('20120125'));
*
* @param string $day day to filter.
*
* @return array all link matching given day.
*
* @throws Exception if date format is invalid.
*/
public function filterDay($day)
{
if (!checkDateFormat('Ymd', $day)) {
throw new Exception('Invalid date format');
}
$filtered = array();
foreach ($this->bookmarks as $key => $l) {
if ($l->getCreated()->format('Ymd') == $day) {
$filtered[$key] = $l;
}
}
// sort by date ASC
return array_reverse($filtered, true);
}
/**
* Convert a list of tags (str) to an array. Also
* - handle case sensitivity.
* - accepts spaces commas as separator.
*
* @param string $tags string containing a list of tags.
* @param bool $casesensitive will convert everything to lowercase if false.
*
* @return array filtered tags string.
*/
public static function tagsStrToArray($tags, $casesensitive)
{
// We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
$tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
$tagsOut = str_replace(',', ' ', $tagsOut);
return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
}
}

View file

@ -0,0 +1,108 @@
<?php
namespace Shaarli\Bookmark;
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
use Shaarli\Config\ConfigManager;
/**
* Class BookmarkIO
*
* This class performs read/write operation to the file data store.
* Used by BookmarkFileService.
*
* @package Shaarli\Bookmark
*/
class BookmarkIO
{
/**
* @var string Datastore file path
*/
protected $datastore;
/**
* @var ConfigManager instance
*/
protected $conf;
/**
* string Datastore PHP prefix
*/
protected static $phpPrefix = '<?php /* ';
/**
* string Datastore PHP suffix
*/
protected static $phpSuffix = ' */ ?>';
/**
* LinksIO constructor.
*
* @param ConfigManager $conf instance
*/
public function __construct($conf)
{
$this->conf = $conf;
$this->datastore = $conf->get('resource.datastore');
}
/**
* Reads database from disk to memory
*
* @return BookmarkArray instance
*
* @throws NotWritableDataStoreException Data couldn't be loaded
* @throws EmptyDataStoreException Datastore doesn't exist
*/
public function read()
{
if (! file_exists($this->datastore)) {
throw new EmptyDataStoreException();
}
if (!is_writable($this->datastore)) {
throw new NotWritableDataStoreException($this->datastore);
}
// Note that gzinflate is faster than gzuncompress.
// See: http://www.php.net/manual/en/function.gzdeflate.php#96439
$links = unserialize(gzinflate(base64_decode(
substr(file_get_contents($this->datastore),
strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
if (empty($links)) {
if (filesize($this->datastore) > 100) {
throw new NotWritableDataStoreException($this->datastore);
}
throw new EmptyDataStoreException();
}
return $links;
}
/**
* Saves the database from memory to disk
*
* @param BookmarkArray $links instance.
*
* @throws NotWritableDataStoreException the datastore is not writable
*/
public function write($links)
{
if (is_file($this->datastore) && !is_writeable($this->datastore)) {
// The datastore exists but is not writeable
throw new NotWritableDataStoreException($this->datastore);
} else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
// The datastore does not exist and its parent directory is not writeable
throw new NotWritableDataStoreException(dirname($this->datastore));
}
file_put_contents(
$this->datastore,
self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
);
invalidateCaches($this->conf->get('resource.page_cache'));
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Shaarli\Bookmark;
/**
* Class BookmarkInitializer
*
* This class is used to initialized default bookmarks after a fresh install of Shaarli.
* It is no longer call when the data store is empty,
* because user might want to delete default bookmarks after the install.
*
* To prevent data corruption, it does not overwrite existing bookmarks,
* even though there should not be any.
*
* @package Shaarli\Bookmark
*/
class BookmarkInitializer
{
/** @var BookmarkServiceInterface */
protected $bookmarkService;
/**
* BookmarkInitializer constructor.
*
* @param BookmarkServiceInterface $bookmarkService
*/
public function __construct($bookmarkService)
{
$this->bookmarkService = $bookmarkService;
}
/**
* Initialize the data store with default bookmarks
*/
public function initialize()
{
$bookmark = new Bookmark();
$bookmark->setTitle(t('My secret stuff... - Pastebin.com'));
$bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []);
$bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'));
$bookmark->setTagsString('secretstuff');
$bookmark->setPrivate(true);
$this->bookmarkService->add($bookmark);
$bookmark = new Bookmark();
$bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service'));
$bookmark->setUrl('https://shaarli.readthedocs.io', []);
$bookmark->setDescription(t(
'Welcome to Shaarli! This is your first public bookmark. '
. 'To edit or delete me, you must first login.
To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
));
$bookmark->setTagsString('opensource software');
$this->bookmarkService->add($bookmark);
}
}

View file

@ -0,0 +1,180 @@
<?php
namespace Shaarli\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
use Shaarli\Config\ConfigManager;
use Shaarli\Exceptions\IOException;
use Shaarli\History;
/**
* Class BookmarksService
*
* This is the entry point to manipulate the bookmark DB.
*/
interface BookmarkServiceInterface
{
/**
* BookmarksService constructor.
*
* @param ConfigManager $conf instance
* @param History $history instance
* @param bool $isLoggedIn true if the current user is logged in
*/
public function __construct(ConfigManager $conf, History $history, $isLoggedIn);
/**
* Find a bookmark by hash
*
* @param string $hash
*
* @return mixed
*
* @throws \Exception
*/
public function findByHash($hash);
/**
* @param $url
*
* @return Bookmark|null
*/
public function findByUrl($url);
/**
* Search bookmarks
*
* @param mixed $request
* @param string $visibility
* @param bool $caseSensitive
* @param bool $untaggedOnly
*
* @return Bookmark[]
*/
public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false);
/**
* Get a single bookmark by its ID.
*
* @param int $id Bookmark ID
* @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
* exception
*
* @return Bookmark
*
* @throws BookmarkNotFoundException
* @throws \Exception
*/
public function get($id, $visibility = null);
/**
* Updates an existing bookmark (depending on its ID).
*
* @param Bookmark $bookmark
* @param bool $save Writes to the datastore if set to true
*
* @return Bookmark Updated bookmark
*
* @throws BookmarkNotFoundException
* @throws \Exception
*/
public function set($bookmark, $save = true);
/**
* Adds a new bookmark (the ID must be empty).
*
* @param Bookmark $bookmark
* @param bool $save Writes to the datastore if set to true
*
* @return Bookmark new bookmark
*
* @throws \Exception
*/
public function add($bookmark, $save = true);
/**
* Adds or updates a bookmark depending on its ID:
* - a Bookmark without ID will be added
* - a Bookmark with an existing ID will be updated
*
* @param Bookmark $bookmark
* @param bool $save
*
* @return Bookmark
*
* @throws \Exception
*/
public function addOrSet($bookmark, $save = true);
/**
* Deletes a bookmark.
*
* @param Bookmark $bookmark
* @param bool $save
*
* @throws \Exception
*/
public function remove($bookmark, $save = true);
/**
* Get a single bookmark by its ID.
*
* @param int $id Bookmark ID
* @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
* exception
*
* @return bool
*/
public function exists($id, $visibility = null);
/**
* Return the number of available bookmarks for given visibility.
*
* @param string $visibility public|private|all
*
* @return int Number of bookmarks
*/
public function count($visibility = null);
/**
* Write the datastore.
*
* @throws NotWritableDataStoreException
*/
public function save();
/**
* Returns the list tags appearing in the bookmarks with the given tags
*
* @param array $filteringTags tags selecting the bookmarks to consider
* @param string $visibility process only all/private/public bookmarks
*
* @return array tag => bookmarksCount
*/
public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all');
/**
* Returns the list of days containing articles (oldest first)
*
* @return array containing days (in format YYYYMMDD).
*/
public function days();
/**
* Returns the list of articles for a given day.
*
* @param string $request day to filter. Format: YYYYMMDD.
*
* @return Bookmark[] list of shaare found.
*
* @throws BookmarkNotFoundException
*/
public function filterDay($request);
/**
* Creates the default database after a fresh install.
*/
public function initialize();
}

View file

@ -1,6 +1,6 @@
<?php <?php
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\Bookmark;
/** /**
* Get cURL callback function for CURLOPT_WRITEFUNCTION * Get cURL callback function for CURLOPT_WRITEFUNCTION
@ -188,30 +188,11 @@ function html_extract_tag($tag, $html)
} }
/** /**
* Count private links in given linklist. * In a string, converts URLs to clickable bookmarks.
*
* @param array|Countable $links Linklist.
*
* @return int Number of private links.
*/
function count_private($links)
{
$cpt = 0;
foreach ($links as $link) {
if ($link['private']) {
$cpt += 1;
}
}
return $cpt;
}
/**
* In a string, converts URLs to clickable links.
* *
* @param string $text input string. * @param string $text input string.
* *
* @return string returns $text with all links converted to HTML links. * @return string returns $text with all bookmarks converted to HTML bookmarks.
* *
* @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
*/ */
@ -279,7 +260,7 @@ function format_description($description, $indexUrl = '')
*/ */
function link_small_hash($date, $id) function link_small_hash($date, $id)
{ {
return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id); return smallHash($date->format(Bookmark::LINK_DATE_FORMAT) . $id);
} }
/** /**

View file

@ -3,7 +3,7 @@
use Exception; use Exception;
class LinkNotFoundException extends Exception class BookmarkNotFoundException extends Exception
{ {
/** /**
* LinkNotFoundException constructor. * LinkNotFoundException constructor.

View file

@ -0,0 +1,7 @@
<?php
namespace Shaarli\Bookmark\Exception;
class EmptyDataStoreException extends \Exception {}

View file

@ -0,0 +1,30 @@
<?php
namespace Shaarli\Bookmark\Exception;
use Shaarli\Bookmark\Bookmark;
class InvalidBookmarkException extends \Exception
{
public function __construct($bookmark)
{
if ($bookmark instanceof Bookmark) {
if ($bookmark->getCreated() instanceof \DateTime) {
$created = $bookmark->getCreated()->format(\DateTime::ATOM);
} elseif (empty($bookmark->getCreated())) {
$created = '';
} else {
$created = 'Not a DateTime object';
}
$this->message = 'This bookmark is not valid'. PHP_EOL;
$this->message .= ' - ID: '. $bookmark->getId() . PHP_EOL;
$this->message .= ' - Title: '. $bookmark->getTitle() . PHP_EOL;
$this->message .= ' - Url: '. $bookmark->getUrl() . PHP_EOL;
$this->message .= ' - ShortUrl: '. $bookmark->getShortUrl() . PHP_EOL;
$this->message .= ' - Created: '. $created . PHP_EOL;
} else {
$this->message = 'The provided data is not a bookmark'. PHP_EOL;
$this->message .= var_export($bookmark, true);
}
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Shaarli\Bookmark\Exception;
class NotWritableDataStoreException extends \Exception
{
/**
* NotReadableDataStore constructor.
*
* @param string $dataStore file path
*/
public function __construct($dataStore)
{
$this->message = 'Couldn\'t load data from the data store file "'. $dataStore .'". '.
'Your data might be corrupted, or your file isn\'t readable.';
}
}

View file

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

View file

@ -2,6 +2,9 @@
namespace Shaarli\Feed; namespace Shaarli\Feed;
use DateTime; use DateTime;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Formatter\BookmarkFormatter;
/** /**
* FeedBuilder class. * FeedBuilder class.
@ -26,15 +29,20 @@ class FeedBuilder
public static $DEFAULT_LANGUAGE = 'en-en'; 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; public static $DEFAULT_NB_LINKS = 50;
/** /**
* @var \Shaarli\Bookmark\LinkDB instance. * @var BookmarkServiceInterface instance.
*/ */
protected $linkDB; protected $linkDB;
/**
* @var BookmarkFormatter instance.
*/
protected $formatter;
/** /**
* @var string RSS or ATOM feed. * @var string RSS or ATOM feed.
*/ */
@ -56,7 +64,7 @@ class FeedBuilder
protected $isLoggedIn; protected $isLoggedIn;
/** /**
* @var boolean Use permalinks instead of direct links if true. * @var boolean Use permalinks instead of direct bookmarks if true.
*/ */
protected $usePermalinks; protected $usePermalinks;
@ -78,16 +86,17 @@ class FeedBuilder
/** /**
* Feed constructor. * 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 string $feedType Type of feed.
* @param array $serverInfo $_SERVER. * @param array $serverInfo $_SERVER.
* @param array $userInput $_GET. * @param array $userInput $_GET.
* @param boolean $isLoggedIn True if the user is currently logged in, * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
* false otherwise.
*/ */
public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLoggedIn) public function __construct($linkDB, $formatter, $feedType, $serverInfo, $userInput, $isLoggedIn)
{ {
$this->linkDB = $linkDB; $this->linkDB = $linkDB;
$this->formatter = $formatter;
$this->feedType = $feedType; $this->feedType = $feedType;
$this->serverInfo = $serverInfo; $this->serverInfo = $serverInfo;
$this->userInput = $userInput; $this->userInput = $userInput;
@ -101,13 +110,13 @@ public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLogg
*/ */
public function buildData() public function buildData()
{ {
// Search for untagged links // Search for untagged bookmarks
if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) {
$this->userInput['searchtags'] = false; $this->userInput['searchtags'] = false;
} }
// Optionally filter the results: // Optionally filter the results:
$linksToDisplay = $this->linkDB->filterSearch($this->userInput); $linksToDisplay = $this->linkDB->search($this->userInput);
$nblinksToDisplay = $this->getNbLinks(count($linksToDisplay)); $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay));
@ -118,6 +127,7 @@ public function buildData()
} }
$pageaddr = escape(index_url($this->serverInfo)); $pageaddr = escape(index_url($this->serverInfo));
$this->formatter->addContextData('index_url', $pageaddr);
$linkDisplayed = array(); $linkDisplayed = array();
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
$linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr); $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr);
@ -139,54 +149,44 @@ public function buildData()
/** /**
* Build a feed item (one per shaare). * Build a feed item (one per shaare).
* *
* @param array $link Single link array extracted from LinkDB. * @param Bookmark $link Single link array extracted from LinkDB.
* @param string $pageaddr Index URL. * @param string $pageaddr Index URL.
* *
* @return array Link array with feed attributes. * @return array Link array with feed attributes.
*/ */
protected function buildItem($link, $pageaddr) protected function buildItem($link, $pageaddr)
{ {
$link['guid'] = $pageaddr . '?' . $link['shorturl']; $data = $this->formatter->format($link);
// Prepend the root URL for notes $data['guid'] = $pageaddr . '?' . $data['shorturl'];
if (is_note($link['url'])) {
$link['url'] = $pageaddr . $link['url'];
}
if ($this->usePermalinks === true) { 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 { } 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); $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
$link['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
$pubDate = $link['created']; $data['pub_iso_date'] = $this->getIsoDate($data['created']);
$link['pub_iso_date'] = $this->getIsoDate($pubDate);
// atom:entry elements MUST contain exactly one atom:updated element. // atom:entry elements MUST contain exactly one atom:updated element.
if (!empty($link['updated'])) { if (!empty($link->getUpdated())) {
$upDate = $link['updated']; $data['up_iso_date'] = $this->getIsoDate($data['updated'], DateTime::ATOM);
$link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
} else { } 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. // Save the more recent item.
if (empty($this->latestDate) || $this->latestDate < $pubDate) { if (empty($this->latestDate) || $this->latestDate < $data['created']) {
$this->latestDate = $pubDate; $this->latestDate = $data['created'];
} }
if (!empty($upDate) && $this->latestDate < $upDate) { if (!empty($data['updated']) && $this->latestDate < $data['updated']) {
$this->latestDate = $upDate; $this->latestDate = $data['updated'];
} }
$taglist = array_filter(explode(' ', $link['tags']), 'strlen'); return $data;
uasort($taglist, 'strcasecmp');
$link['taglist'] = $taglist;
return $link;
} }
/** /**
* 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. * @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. * 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' 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) public function getNbLinks($max)
{ {

View file

@ -0,0 +1,81 @@
<?php
namespace Shaarli\Formatter;
/**
* Class BookmarkDefaultFormatter
*
* Default bookmark formatter.
* Escape values for HTML display and automatically add link to URL and hashtags.
*
* @package Shaarli\Formatter
*/
class BookmarkDefaultFormatter extends BookmarkFormatter
{
/**
* @inheritdoc
*/
public function formatTitle($bookmark)
{
return escape($bookmark->getTitle());
}
/**
* @inheritdoc
*/
public function formatDescription($bookmark)
{
$indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
return format_description(escape($bookmark->getDescription()), $indexUrl);
}
/**
* @inheritdoc
*/
protected function formatTagList($bookmark)
{
return escape($bookmark->getTags());
}
/**
* @inheritdoc
*/
public function formatTagString($bookmark)
{
return implode(' ', $this->formatTagList($bookmark));
}
/**
* @inheritdoc
*/
public function formatUrl($bookmark)
{
if (! empty($this->contextData['index_url']) && (
startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
)) {
return $this->contextData['index_url'] . escape($bookmark->getUrl());
}
return escape($bookmark->getUrl());
}
/**
* @inheritdoc
*/
protected function formatRealUrl($bookmark)
{
if (! empty($this->contextData['index_url']) && (
startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
)) {
return $this->contextData['index_url'] . escape($bookmark->getUrl());
}
return escape($bookmark->getUrl());
}
/**
* @inheritdoc
*/
protected function formatThumbnail($bookmark)
{
return escape($bookmark->getThumbnail());
}
}

View file

@ -0,0 +1,256 @@
<?php
namespace Shaarli\Formatter;
use DateTime;
use Shaarli\Config\ConfigManager;
use Shaarli\Bookmark\Bookmark;
/**
* Class BookmarkFormatter
*
* Abstract class processing all bookmark attributes through methods designed to be overridden.
*
* @package Shaarli\Formatter
*/
abstract class BookmarkFormatter
{
/**
* @var ConfigManager
*/
protected $conf;
/**
* @var array Additional parameters than can be used for specific formatting
* e.g. index_url for Feed formatting
*/
protected $contextData = [];
/**
* LinkDefaultFormatter constructor.
* @param ConfigManager $conf
*/
public function __construct(ConfigManager $conf)
{
$this->conf = $conf;
}
/**
* Convert a Bookmark into an array usable by templates and plugins.
*
* All Bookmark attributes are formatted through a format method
* that can be overridden in a formatter extending this class.
*
* @param Bookmark $bookmark instance
*
* @return array formatted representation of a Bookmark
*/
public function format($bookmark)
{
$out['id'] = $this->formatId($bookmark);
$out['shorturl'] = $this->formatShortUrl($bookmark);
$out['url'] = $this->formatUrl($bookmark);
$out['real_url'] = $this->formatRealUrl($bookmark);
$out['title'] = $this->formatTitle($bookmark);
$out['description'] = $this->formatDescription($bookmark);
$out['thumbnail'] = $this->formatThumbnail($bookmark);
$out['taglist'] = $this->formatTagList($bookmark);
$out['tags'] = $this->formatTagString($bookmark);
$out['sticky'] = $bookmark->isSticky();
$out['private'] = $bookmark->isPrivate();
$out['class'] = $this->formatClass($bookmark);
$out['created'] = $this->formatCreated($bookmark);
$out['updated'] = $this->formatUpdated($bookmark);
$out['timestamp'] = $this->formatCreatedTimestamp($bookmark);
$out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark);
return $out;
}
/**
* Add additional data available to formatters.
* This is used for example to add `index_url` in description's links.
*
* @param string $key Context data key
* @param string $value Context data value
*/
public function addContextData($key, $value)
{
$this->contextData[$key] = $value;
}
/**
* Format ID
*
* @param Bookmark $bookmark instance
*
* @return int formatted ID
*/
protected function formatId($bookmark)
{
return $bookmark->getId();
}
/**
* Format ShortUrl
*
* @param Bookmark $bookmark instance
*
* @return string formatted ShortUrl
*/
protected function formatShortUrl($bookmark)
{
return $bookmark->getShortUrl();
}
/**
* Format Url
*
* @param Bookmark $bookmark instance
*
* @return string formatted Url
*/
protected function formatUrl($bookmark)
{
return $bookmark->getUrl();
}
/**
* Format RealUrl
* Legacy: identical to Url
*
* @param Bookmark $bookmark instance
*
* @return string formatted RealUrl
*/
protected function formatRealUrl($bookmark)
{
return $bookmark->getUrl();
}
/**
* Format Title
*
* @param Bookmark $bookmark instance
*
* @return string formatted Title
*/
protected function formatTitle($bookmark)
{
return $bookmark->getTitle();
}
/**
* Format Description
*
* @param Bookmark $bookmark instance
*
* @return string formatted Description
*/
protected function formatDescription($bookmark)
{
return $bookmark->getDescription();
}
/**
* Format Thumbnail
*
* @param Bookmark $bookmark instance
*
* @return string formatted Thumbnail
*/
protected function formatThumbnail($bookmark)
{
return $bookmark->getThumbnail();
}
/**
* Format Tags
*
* @param Bookmark $bookmark instance
*
* @return array formatted Tags
*/
protected function formatTagList($bookmark)
{
return $bookmark->getTags();
}
/**
* Format TagString
*
* @param Bookmark $bookmark instance
*
* @return string formatted TagString
*/
protected function formatTagString($bookmark)
{
return implode(' ', $bookmark->getTags());
}
/**
* Format Class
* Used to add specific CSS class for a link
*
* @param Bookmark $bookmark instance
*
* @return string formatted Class
*/
protected function formatClass($bookmark)
{
return $bookmark->isPrivate() ? 'private' : '';
}
/**
* Format Created
*
* @param Bookmark $bookmark instance
*
* @return DateTime instance
*/
protected function formatCreated(Bookmark $bookmark)
{
return $bookmark->getCreated();
}
/**
* Format Updated
*
* @param Bookmark $bookmark instance
*
* @return DateTime instance
*/
protected function formatUpdated(Bookmark $bookmark)
{
return $bookmark->getUpdated();
}
/**
* Format CreatedTimestamp
*
* @param Bookmark $bookmark instance
*
* @return int formatted CreatedTimestamp
*/
protected function formatCreatedTimestamp(Bookmark $bookmark)
{
if (! empty($bookmark->getCreated())) {
return $bookmark->getCreated()->getTimestamp();
}
return 0;
}
/**
* Format UpdatedTimestamp
*
* @param Bookmark $bookmark instance
*
* @return int formatted UpdatedTimestamp
*/
protected function formatUpdatedTimestamp(Bookmark $bookmark)
{
if (! empty($bookmark->getUpdated())) {
return $bookmark->getUpdated()->getTimestamp();
}
return 0;
}
}

View file

@ -0,0 +1,204 @@
<?php
namespace Shaarli\Formatter;
use Shaarli\Config\ConfigManager;
/**
* Class BookmarkMarkdownFormatter
*
* Format bookmark description into Markdown format.
*
* @package Shaarli\Formatter
*/
class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
{
/**
* When this tag is present in a bookmark, its description should not be processed with Markdown
*/
const NO_MD_TAG = 'nomarkdown';
/** @var \Parsedown instance */
protected $parsedown;
/** @var bool used to escape HTML in Markdown or not.
* It MUST be set to true for shared instance as HTML content can
* introduce XSS vulnerabilities.
*/
protected $escape;
/**
* @var array List of allowed protocols for links inside bookmark's description.
*/
protected $allowedProtocols;
/**
* LinkMarkdownFormatter constructor.
*
* @param ConfigManager $conf instance
*/
public function __construct(ConfigManager $conf)
{
parent::__construct($conf);
$this->parsedown = new \Parsedown();
$this->escape = $conf->get('security.markdown_escape', true);
$this->allowedProtocols = $conf->get('security.allowed_protocols', []);
}
/**
* @inheritdoc
*/
public function formatDescription($bookmark)
{
if (in_array(self::NO_MD_TAG, $bookmark->getTags())) {
return parent::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)
->text($processedDescription);
$processedDescription = $this->sanitizeHtml($processedDescription);
if (!empty($processedDescription)) {
$processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
}
return $processedDescription;
}
/**
* Remove the NO markdown tag if it is present
*
* @inheritdoc
*/
protected function formatTagList($bookmark)
{
$out = parent::formatTagList($bookmark);
if (($pos = array_search(self::NO_MD_TAG, $out)) !== false) {
unset($out[$pos]);
return array_values($out);
}
return $out;
}
/**
* Replace not whitelisted protocols with http:// in given description.
* Also adds `index_url` to relative links if it's specified
*
* @param string $description input description text.
*
* @return string $description without malicious link.
*/
protected function filterProtocols($description)
{
$allowedProtocols = $this->allowedProtocols;
$indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
return preg_replace_callback(
'#]\((.*?)\)#is',
function ($match) use ($allowedProtocols, $indexUrl) {
$link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
$link .= whitelist_protocols($match[1], $allowedProtocols);
return ']('. $link.')';
},
$description
);
}
/**
* Replace hashtag in Markdown links format
* E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)`
* It includes the index URL if specified.
*
* @param string $description
*
* @return string
*/
protected function formatHashTags($description)
{
$indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
/*
* To support unicode: http://stackoverflow.com/a/35498078/1484919
* \p{Pc} - to match underscore
* \p{N} - numeric character in any script
* \p{L} - letter from any language
* \p{Mn} - any non marking space (accents, umlauts, etc)
*/
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
$replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)';
$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;
}
if (!$codeBlockOn && !$codeLineOn) {
$descriptionLine = preg_replace($regex, $replacement, $descriptionLine);
}
$descriptionOut .= $descriptionLine;
if ($lineCount++ < count($descriptionLines) - 1) {
$descriptionOut .= PHP_EOL;
}
}
return $descriptionOut;
}
/**
* 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.
*/
protected function sanitizeHtml($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;
}
protected function reverseEscapedHtml($description)
{
return unescape($description);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Shaarli\Formatter;
/**
* Class BookmarkRawFormatter
*
* Used to retrieve bookmarks as array with raw values.
* Warning: Do NOT use this for HTML content as it can introduce XSS vulnerabilities.
*
* @package Shaarli\Formatter
*/
class BookmarkRawFormatter extends BookmarkFormatter {}

View file

@ -0,0 +1,46 @@
<?php
namespace Shaarli\Formatter;
use Shaarli\Config\ConfigManager;
/**
* Class FormatterFactory
*
* Helper class used to instantiate the proper BookmarkFormatter.
*
* @package Shaarli\Formatter
*/
class FormatterFactory
{
/** @var ConfigManager instance */
protected $conf;
/**
* FormatterFactory constructor.
*
* @param ConfigManager $conf
*/
public function __construct(ConfigManager $conf)
{
$this->conf = $conf;
}
/**
* Instanciate a BookmarkFormatter depending on the configuration or provided formatter type.
*
* @param string|null $type force a specific type regardless of the configuration
*
* @return BookmarkFormatter instance.
*/
public function getFormatter($type = null)
{
$type = $type ? $type : $this->conf->get('formatter', 'default');
$className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';
if (!class_exists($className)) {
$className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
}
return new $className($this->conf);
}
}

View file

@ -1,17 +1,17 @@
<?php <?php
namespace Shaarli\Bookmark; namespace Shaarli\Legacy;
use ArrayAccess; use ArrayAccess;
use Countable; use Countable;
use DateTime; use DateTime;
use Iterator; use Iterator;
use Shaarli\Bookmark\Exception\LinkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Exceptions\IOException; use Shaarli\Exceptions\IOException;
use Shaarli\FileUtils; use Shaarli\FileUtils;
/** /**
* Data storage for links. * Data storage for bookmarks.
* *
* This object behaves like an associative array. * This object behaves like an associative array.
* *
@ -29,8 +29,8 @@
* - private: Is this link private? 0=no, other value=yes * - private: Is this link private? 0=no, other value=yes
* - tags: tags attached to this entry (separated by spaces) * - tags: tags attached to this entry (separated by spaces)
* - title Title of the link * - title Title of the link
* - url URL of the link. Used for displayable links. * - url URL of the link. Used for displayable bookmarks.
* Can be absolute or relative in the database but the relative links * Can be absolute or relative in the database but the relative bookmarks
* will be converted to absolute ones in templates. * will be converted to absolute ones in templates.
* - real_url Raw URL in stored in the DB (absolute or relative). * - real_url Raw URL in stored in the DB (absolute or relative).
* - shorturl Permalink smallhash * - shorturl Permalink smallhash
@ -49,11 +49,13 @@
* Example: * Example:
* - DB: link #1 (2010-01-01) link #2 (2016-01-01) * - DB: link #1 (2010-01-01) link #2 (2016-01-01)
* - Order: #2 #1 * - Order: #2 #1
* - Import links containing: link #3 (2013-01-01) * - Import bookmarks containing: link #3 (2013-01-01)
* - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01) * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01)
* - Real order: #2 #3 #1 * - Real order: #2 #3 #1
*
* @deprecated
*/ */
class LinkDB implements Iterator, Countable, ArrayAccess class LegacyLinkDB implements Iterator, Countable, ArrayAccess
{ {
// Links are stored as a PHP serialized string // Links are stored as a PHP serialized string
private $datastore; private $datastore;
@ -61,7 +63,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
// Link date storage format // Link date storage format
const LINK_DATE_FORMAT = 'Ymd_His'; const LINK_DATE_FORMAT = 'Ymd_His';
// List of links (associative array) // List of bookmarks (associative array)
// - key: link date (e.g. "20110823_124546"), // - key: link date (e.g. "20110823_124546"),
// - value: associative array (keys: title, description...) // - value: associative array (keys: title, description...)
private $links; private $links;
@ -71,7 +73,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
private $urls; private $urls;
/** /**
* @var array List of all links IDS mapped with their array offset. * @var array List of all bookmarks IDS mapped with their array offset.
* Map: id->offset. * Map: id->offset.
*/ */
protected $ids; protected $ids;
@ -82,10 +84,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess
// Position in the $this->keys array (for the Iterator interface) // Position in the $this->keys array (for the Iterator interface)
private $position; private $position;
// Is the user logged in? (used to filter private links) // Is the user logged in? (used to filter private bookmarks)
private $loggedIn; private $loggedIn;
// Hide public links // Hide public bookmarks
private $hidePublicLinks; private $hidePublicLinks;
/** /**
@ -95,7 +97,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
* *
* @param string $datastore datastore file path. * @param string $datastore datastore file path.
* @param boolean $isLoggedIn is the user logged in? * @param boolean $isLoggedIn is the user logged in?
* @param boolean $hidePublicLinks if true all links are private. * @param boolean $hidePublicLinks if true all bookmarks are private.
*/ */
public function __construct( public function __construct(
$datastore, $datastore,
@ -280,7 +282,7 @@ private function check()
*/ */
private function read() private function read()
{ {
// Public links are hidden and user not logged in => nothing to show // Public bookmarks are hidden and user not logged in => nothing to show
if ($this->hidePublicLinks && !$this->loggedIn) { if ($this->hidePublicLinks && !$this->loggedIn) {
$this->links = array(); $this->links = array();
return; return;
@ -310,7 +312,7 @@ private function read()
$link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false; $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
// To be able to load links before running the update, and prepare the update // To be able to load bookmarks before running the update, and prepare the update
if (!isset($link['created'])) { if (!isset($link['created'])) {
$link['id'] = $link['linkdate']; $link['id'] = $link['linkdate'];
$link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
@ -375,13 +377,13 @@ public function getLinkFromUrl($url)
* *
* @return array $filtered array containing permalink data. * @return array $filtered array containing permalink data.
* *
* @throws LinkNotFoundException if the smallhash is malformed or doesn't match any link. * @throws BookmarkNotFoundException if the smallhash is malformed or doesn't match any link.
*/ */
public function filterHash($request) public function filterHash($request)
{ {
$request = substr($request, 0, 6); $request = substr($request, 0, 6);
$linkFilter = new LinkFilter($this->links); $linkFilter = new LegacyLinkFilter($this->links);
return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request); return $linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, $request);
} }
/** /**
@ -393,21 +395,21 @@ public function filterHash($request)
*/ */
public function filterDay($request) public function filterDay($request)
{ {
$linkFilter = new LinkFilter($this->links); $linkFilter = new LegacyLinkFilter($this->links);
return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request); return $linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, $request);
} }
/** /**
* Filter links according to search parameters. * Filter bookmarks according to search parameters.
* *
* @param array $filterRequest Search request content. Supported keys: * @param array $filterRequest Search request content. Supported keys:
* - searchtags: list of tags * - searchtags: list of tags
* - searchterm: term search * - searchterm: term search
* @param bool $casesensitive Optional: Perform case sensitive filter * @param bool $casesensitive Optional: Perform case sensitive filter
* @param string $visibility return only all/private/public links * @param string $visibility return only all/private/public bookmarks
* @param bool $untaggedonly return only untagged links * @param bool $untaggedonly return only untagged bookmarks
* *
* @return array filtered links, all links if no suitable filter was provided. * @return array filtered bookmarks, all bookmarks if no suitable filter was provided.
*/ */
public function filterSearch( public function filterSearch(
$filterRequest = array(), $filterRequest = array(),
@ -420,19 +422,19 @@ public function filterSearch(
$searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
$searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
// Search tags + fullsearch - blank string parameter will return all links. // Search tags + fullsearch - blank string parameter will return all bookmarks.
$type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; // == "vuotext" $type = LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT; // == "vuotext"
$request = [$searchtags, $searchterm]; $request = [$searchtags, $searchterm];
$linkFilter = new LinkFilter($this); $linkFilter = new LegacyLinkFilter($this);
return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly); return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly);
} }
/** /**
* Returns the list tags appearing in the links with the given tags * Returns the list tags appearing in the bookmarks with the given tags
* *
* @param array $filteringTags tags selecting the links to consider * @param array $filteringTags tags selecting the bookmarks to consider
* @param string $visibility process only all/private/public links * @param string $visibility process only all/private/public bookmarks
* *
* @return array tag => linksCount * @return array tag => linksCount
*/ */
@ -471,12 +473,12 @@ public function linksCountPerTag($filteringTags = [], $visibility = 'all')
} }
/** /**
* Rename or delete a tag across all links. * Rename or delete a tag across all bookmarks.
* *
* @param string $from Tag to rename * @param string $from Tag to rename
* @param string $to New tag. If none is provided, the from tag will be deleted * @param string $to New tag. If none is provided, the from tag will be deleted
* *
* @return array|bool List of altered links or false on error * @return array|bool List of altered bookmarks or false on error
*/ */
public function renameTag($from, $to) public function renameTag($from, $to)
{ {
@ -519,7 +521,7 @@ public function days()
} }
/** /**
* Reorder links by creation date (newest first). * Reorder bookmarks by creation date (newest first).
* *
* Also update the urls and ids mapping arrays. * Also update the urls and ids mapping arrays.
* *
@ -562,7 +564,7 @@ public function getNextId()
} }
/** /**
* Returns a link offset in links array from its unique ID. * Returns a link offset in bookmarks array from its unique ID.
* *
* @param int $id Persistent ID of a link. * @param int $id Persistent ID of a link.
* *

View file

@ -1,16 +1,18 @@
<?php <?php
namespace Shaarli\Bookmark; namespace Shaarli\Legacy;
use Exception; use Exception;
use Shaarli\Bookmark\Exception\LinkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
/** /**
* Class LinkFilter. * Class LinkFilter.
* *
* Perform search and filter operation on link data list. * Perform search and filter operation on link data list.
*
* @deprecated
*/ */
class LinkFilter class LegacyLinkFilter
{ {
/** /**
* @var string permalinks. * @var string permalinks.
@ -38,12 +40,12 @@ class LinkFilter
public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
/** /**
* @var LinkDB all available links. * @var LegacyLinkDB all available links.
*/ */
private $links; private $links;
/** /**
* @param LinkDB $links initialization. * @param LegacyLinkDB $links initialization.
*/ */
public function __construct($links) public function __construct($links)
{ {
@ -84,10 +86,10 @@ public function filter($type, $request, $casesensitive = false, $visibility = 'a
$filtered = $this->links; $filtered = $this->links;
} }
if (!empty($request[0])) { if (!empty($request[0])) {
$filtered = (new LinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); $filtered = (new LegacyLinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
} }
if (!empty($request[1])) { if (!empty($request[1])) {
$filtered = (new LinkFilter($filtered))->filterFulltext($request[1], $visibility); $filtered = (new LegacyLinkFilter($filtered))->filterFulltext($request[1], $visibility);
} }
return $filtered; return $filtered;
case self::$FILTER_TEXT: case self::$FILTER_TEXT:
@ -137,7 +139,7 @@ private function noFilter($visibility = 'all')
* *
* @return array $filtered array containing permalink data. * @return array $filtered array containing permalink data.
* *
* @throws \Shaarli\Bookmark\Exception\LinkNotFoundException if the smallhash doesn't match any link. * @throws BookmarkNotFoundException if the smallhash doesn't match any link.
*/ */
private function filterSmallHash($smallHash) private function filterSmallHash($smallHash)
{ {
@ -151,7 +153,7 @@ private function filterSmallHash($smallHash)
} }
if (empty($filtered)) { if (empty($filtered)) {
throw new LinkNotFoundException(); throw new BookmarkNotFoundException();
} }
return $filtered; return $filtered;

View file

@ -0,0 +1,617 @@
<?php
namespace Shaarli\Legacy;
use Exception;
use RainTPL;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkArray;
use Shaarli\Bookmark\LinkDB;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\BookmarkIO;
use Shaarli\Config\ConfigJson;
use Shaarli\Config\ConfigManager;
use Shaarli\Config\ConfigPhp;
use Shaarli\Exceptions\IOException;
use Shaarli\Thumbnailer;
use Shaarli\Updater\Exception\UpdaterException;
/**
* 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.
*
* @deprecated
*/
class LegacyUpdater
{
/**
* @var array Updates which are already done.
*/
protected $doneUpdates;
/**
* @var LegacyLinkDB instance.
*/
protected $linkDB;
/**
* @var ConfigManager $conf Configuration Manager instance.
*/
protected $conf;
/**
* @var bool True if the user is logged in, false otherwise.
*/
protected $isLoggedIn;
/**
* @var array $_SESSION
*/
protected $session;
/**
* @var ReflectionMethod[] List of current class methods.
*/
protected $methods;
/**
* Object constructor.
*
* @param array $doneUpdates Updates which are already done.
* @param LegacyLinkDB $linkDB LinkDB instance.
* @param ConfigManager $conf Configuration Manager instance.
* @param boolean $isLoggedIn True if the user is logged in.
* @param array $session $_SESSION (by reference)
*
* @throws ReflectionException
*/
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
{
$this->doneUpdates = $doneUpdates;
$this->linkDB = $linkDB;
$this->conf = $conf;
$this->isLoggedIn = $isLoggedIn;
$this->session = &$session;
// Retrieve all update methods.
$class = new ReflectionClass($this);
$this->methods = $class->getMethods();
}
/**
* Run all new updates.
* Update methods have to start with 'updateMethod' and return true (on success).
*
* @return array An array containing ran updates.
*
* @throws UpdaterException If something went wrong.
*/
public function update()
{
$updatesRan = array();
// If the user isn't logged in, exit without updating.
if ($this->isLoggedIn !== true) {
return $updatesRan;
}
if ($this->methods === null) {
throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
}
foreach ($this->methods as $method) {
// Not an update method or already done, pass.
if (!startsWith($method->getName(), 'updateMethod')
|| in_array($method->getName(), $this->doneUpdates)
) {
continue;
}
try {
$method->setAccessible(true);
$res = $method->invoke($this);
// Update method must return true to be considered processed.
if ($res === true) {
$updatesRan[] = $method->getName();
}
} catch (Exception $e) {
throw new UpdaterException($method, $e);
}
}
$this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
return $updatesRan;
}
/**
* @return array Updates methods already processed.
*/
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()
{
$first = 'update';
foreach ($this->linkDB as $key => $link) {
$first = $key;
break;
}
// up to date database
if (is_int($first)) {
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(BookmarkFilter::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 bookmarks 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 bookmarks
*
* @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;
}
/**
* Migrate the legacy arrays to Bookmark objects.
* Also make a backup of the datastore.
*/
public function updateMethodMigrateDatabase()
{
$save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '_1.php';
if (! copy($this->conf->get('resource.datastore'), $save)) {
die('Could not backup the datastore.');
}
$linksArray = new BookmarkArray();
foreach ($this->linkDB as $key => $link) {
$linksArray[$key] = (new Bookmark())->fromArray($link);
}
$linksIo = new BookmarkIO($this->conf);
$linksIo->write($linksArray);
return true;
}
/**
* Write the `formatter` setting in config file.
* Use markdown if the markdown plugin is enabled, the default one otherwise.
* Also remove markdown plugin setting as it is now integrated to the core.
*/
public function updateMethodFormatterSetting()
{
if (!$this->conf->exists('formatter') || $this->conf->get('formatter') === 'default') {
$enabledPlugins = $this->conf->get('general.enabled_plugins');
if (($pos = array_search('markdown', $enabledPlugins)) !== false) {
$formatter = 'markdown';
unset($enabledPlugins[$pos]);
$this->conf->set('general.enabled_plugins', array_values($enabledPlugins));
} else {
$formatter = 'default';
}
$this->conf->set('formatter', $formatter);
$this->conf->write(true);
}
return true;
}
}

View file

@ -7,8 +7,10 @@
use Exception; use Exception;
use Katzgrau\KLogger\Logger; use Katzgrau\KLogger\Logger;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\BookmarkFormatter;
use Shaarli\History; use Shaarli\History;
use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; 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: * Added fields:
* - timestamp link addition date, using the Unix epoch format * - timestamp link addition date, using the Unix epoch format
* - taglist comma-separated tag list * - taglist comma-separated tag list
* *
* @param LinkDB $linkDb Link datastore * @param BookmarkServiceInterface $bookmarkService Link datastore
* @param string $selection Which links to export: (all|private|public) * @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 bool $prependNoteUrl Prepend note permalinks with the server's URL
* @param string $indexUrl Absolute URL of the Shaarli index page * @param string $indexUrl Absolute URL of the Shaarli index page
* *
* @return array The bookmarks to be exported, with additional fields
*@throws Exception Invalid export selection *@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 // see tpl/export.html for possible values
if (!in_array($selection, array('all', 'public', 'private'))) { if (!in_array($selection, array('all', 'public', 'private'))) {
throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"'); throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
} }
$bookmarkLinks = array(); $bookmarkLinks = array();
foreach ($linkDb as $link) { foreach ($bookmarkService->search([], $selection) as $bookmark) {
if ($link['private'] != 0 && $selection == 'public') { $link = $formatter->format($bookmark);
continue; $link['taglist'] = implode(',', $bookmark->getTags());
} if ($bookmark->isNote() && $prependNoteUrl) {
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) {
$link['url'] = $indexUrl . $link['url']; $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 string $filename name of the file to import
* @param int $filesize size of the file to import * @param int $filesize size of the file to import
* @param int $importCount how many links were imported * @param int $importCount how many bookmarks were imported
* @param int $overwriteCount how many links were overwritten * @param int $overwriteCount how many bookmarks were overwritten
* @param int $skipCount how many links were skipped * @param int $skipCount how many bookmarks were skipped
* @param int $duration how many seconds did the import take * @param int $duration how many seconds did the import take
* *
* @return string Summary of the bookmark import status * @return string Summary of the bookmark import status
@ -91,7 +91,7 @@ private static function importStatus(
$status .= vsprintf( $status .= vsprintf(
t( t(
'was successfully processed in %d seconds: ' '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] [$duration, $importCount, $overwriteCount, $skipCount]
); );
@ -104,13 +104,13 @@ private static function importStatus(
* *
* @param array $post Server $_POST parameters * @param array $post Server $_POST parameters
* @param array $files Server $_FILES parameters * @param array $files Server $_FILES parameters
* @param LinkDB $linkDb Loaded LinkDB instance * @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance
* @param ConfigManager $conf instance * @param ConfigManager $conf instance
* @param History $history History instance * @param History $history History instance
* *
* @return string Summary of the bookmark import status * @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(); $start = time();
$filename = $files['filetoupload']['name']; $filename = $files['filetoupload']['name'];
@ -121,10 +121,10 @@ public static function import($post, $files, $linkDb, $conf, $history)
return self::importStatus($filename, $filesize); return self::importStatus($filename, $filesize);
} }
// Overwrite existing links? // Overwrite existing bookmarks?
$overwrite = !empty($post['overwrite']); $overwrite = !empty($post['overwrite']);
// Add tags to all imported links? // Add tags to all imported bookmarks?
if (empty($post['default_tags'])) { if (empty($post['default_tags'])) {
$defaultTags = array(); $defaultTags = array();
} else { } 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; $defaultPrivacy = 0;
$parser = new NetscapeBookmarkParser( $parser = new NetscapeBookmarkParser(
@ -164,22 +164,18 @@ public static function import($post, $files, $linkDb, $conf, $history)
// use value from the imported file // use value from the imported file
$private = $bkm['pub'] == '1' ? 0 : 1; $private = $bkm['pub'] == '1' ? 0 : 1;
} elseif ($post['privacy'] == 'private') { } elseif ($post['privacy'] == 'private') {
// all imported links are private // all imported bookmarks are private
$private = 1; $private = 1;
} elseif ($post['privacy'] == 'public') { } elseif ($post['privacy'] == 'public') {
// all imported links are public // all imported bookmarks are public
$private = 0; $private = 0;
} }
$newLink = array( $link = $bookmarkService->findByUrl($bkm['uri']);
'title' => $bkm['title'], $existingLink = $link !== null;
'url' => $bkm['uri'], if (! $existingLink) {
'description' => $bkm['note'], $link = new Bookmark();
'private' => $private, }
'tags' => $bkm['tags']
);
$existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
if ($existingLink !== false) { if ($existingLink !== false) {
if ($overwrite === false) { if ($overwrite === false) {
@ -188,28 +184,25 @@ public static function import($post, $files, $linkDb, $conf, $history)
continue; continue;
} }
// Overwrite an existing link, keep its date $link->setUpdated(new DateTime());
$newLink['id'] = $existingLink['id'];
$newLink['created'] = $existingLink['created'];
$newLink['updated'] = new DateTime();
$newLink['shorturl'] = $existingLink['shorturl'];
$linkDb[$existingLink['id']] = $newLink;
$importCount++;
$overwriteCount++; $overwriteCount++;
continue; } else {
}
// Add a new link - @ used for UNIX timestamps
$newLinkDate = new DateTime('@' . strval($bkm['time'])); $newLinkDate = new DateTime('@' . strval($bkm['time']));
$newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get())); $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
$newLink['created'] = $newLinkDate; $link->setCreated($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++; $importCount++;
} }
$linkDb->save($conf->get('resource.page_cache')); $bookmarkService->save();
$history->importLinks(); $history->importLinks();
$duration = time() - $start; $duration = time() - $start;

View file

@ -5,7 +5,7 @@
use Exception; use Exception;
use RainTPL; use RainTPL;
use Shaarli\ApplicationUtils; use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Thumbnailer; use Shaarli\Thumbnailer;
@ -34,9 +34,9 @@ class PageBuilder
protected $session; protected $session;
/** /**
* @var LinkDB $linkDB instance. * @var BookmarkServiceInterface $bookmarkService instance.
*/ */
protected $linkDB; protected $bookmarkService;
/** /**
* @var null|string XSRF token * @var null|string XSRF token
@ -54,7 +54,7 @@ class PageBuilder
* *
* @param ConfigManager $conf Configuration Manager instance (reference). * @param ConfigManager $conf Configuration Manager instance (reference).
* @param array $session $_SESSION array * @param array $session $_SESSION array
* @param LinkDB $linkDB instance. * @param BookmarkServiceInterface $linkDB instance.
* @param string $token Session token * @param string $token Session token
* @param bool $isLoggedIn * @param bool $isLoggedIn
*/ */
@ -63,7 +63,7 @@ public function __construct(&$conf, $session, $linkDB = null, $token = null, $is
$this->tpl = false; $this->tpl = false;
$this->conf = $conf; $this->conf = $conf;
$this->session = $session; $this->session = $session;
$this->linkDB = $linkDB; $this->bookmarkService = $linkDB;
$this->token = $token; $this->token = $token;
$this->isLoggedIn = $isLoggedIn; $this->isLoggedIn = $isLoggedIn;
} }
@ -125,8 +125,8 @@ private function initialize()
$this->tpl->assign('language', $this->conf->get('translation.language')); $this->tpl->assign('language', $this->conf->get('translation.language'));
if ($this->linkDB !== null) { if ($this->bookmarkService !== null) {
$this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); $this->tpl->assign('tags', $this->bookmarkService->bookmarksCountPerTag());
} }
$this->tpl->assign( $this->tpl->assign(
@ -141,6 +141,8 @@ private function initialize()
unset($_SESSION['warnings']); unset($_SESSION['warnings']);
} }
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
// To be removed with a proper theme configuration. // To be removed with a proper theme configuration.
$this->tpl->assign('conf', $this->conf); $this->tpl->assign('conf', $this->conf);
} }

View file

@ -2,25 +2,14 @@
namespace Shaarli\Updater; 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\ConfigManager;
use Shaarli\Config\ConfigPhp; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Exceptions\IOException;
use Shaarli\Thumbnailer;
use Shaarli\Updater\Exception\UpdaterException; use Shaarli\Updater\Exception\UpdaterException;
/** /**
* Class updater. * Class Updater.
* Used to update stuff when a new Shaarli's version is reached. * 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 class Updater
{ {
@ -30,9 +19,9 @@ class Updater
protected $doneUpdates; protected $doneUpdates;
/** /**
* @var LinkDB instance. * @var BookmarkServiceInterface instance.
*/ */
protected $linkDB; protected $linkServices;
/** /**
* @var ConfigManager $conf Configuration Manager instance. * @var ConfigManager $conf Configuration Manager instance.
@ -45,12 +34,7 @@ class Updater
protected $isLoggedIn; protected $isLoggedIn;
/** /**
* @var array $_SESSION * @var \ReflectionMethod[] List of current class methods.
*/
protected $session;
/**
* @var ReflectionMethod[] List of current class methods.
*/ */
protected $methods; protected $methods;
@ -58,23 +42,19 @@ class Updater
* Object constructor. * Object constructor.
* *
* @param array $doneUpdates Updates which are already done. * @param array $doneUpdates Updates which are already done.
* @param LinkDB $linkDB LinkDB instance. * @param BookmarkServiceInterface $linkDB LinksService instance.
* @param ConfigManager $conf Configuration Manager instance. * @param ConfigManager $conf Configuration Manager instance.
* @param boolean $isLoggedIn True if the user is logged in. * @param boolean $isLoggedIn True if the user is logged in.
* @param array $session $_SESSION (by reference)
*
* @throws ReflectionException
*/ */
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = []) public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
{ {
$this->doneUpdates = $doneUpdates; $this->doneUpdates = $doneUpdates;
$this->linkDB = $linkDB; $this->linkServices = $linkDB;
$this->conf = $conf; $this->conf = $conf;
$this->isLoggedIn = $isLoggedIn; $this->isLoggedIn = $isLoggedIn;
$this->session = &$session;
// Retrieve all update methods. // Retrieve all update methods.
$class = new ReflectionClass($this); $class = new \ReflectionClass($this);
$this->methods = $class->getMethods(); $this->methods = $class->getMethods();
} }
@ -96,7 +76,7 @@ public function update()
} }
if ($this->methods === null) { 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) { foreach ($this->methods as $method) {
@ -114,7 +94,7 @@ public function update()
if ($res === true) { if ($res === true) {
$updatesRan[] = $method->getName(); $updatesRan[] = $method->getName();
} }
} catch (Exception $e) { } catch (\Exception $e) {
throw new UpdaterException($method, $e); throw new UpdaterException($method, $e);
} }
} }
@ -131,432 +111,4 @@ public function getDoneUpdates()
{ {
return $this->doneUpdates; 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,5 +1,9 @@
<?php <?php
namespace Shaarli\Updater;
class UpdaterUtils
{
/** /**
* Read the updates file, and return already done updates. * Read the updates file, and return already done updates.
* *
@ -7,7 +11,7 @@
* *
* @return array Already done update methods. * @return array Already done update methods.
*/ */
function read_updates_file($updatesFilepath) public static function read_updates_file($updatesFilepath)
{ {
if (! empty($updatesFilepath) && is_file($updatesFilepath)) { if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
$content = file_get_contents($updatesFilepath); $content = file_get_contents($updatesFilepath);
@ -24,16 +28,17 @@ function read_updates_file($updatesFilepath)
* @param string $updatesFilepath Updates file path. * @param string $updatesFilepath Updates file path.
* @param array $updates Updates array to write. * @param array $updates Updates array to write.
* *
* @throws Exception Couldn't write version number. * @throws \Exception Couldn't write version number.
*/ */
function write_updates_file($updatesFilepath, $updates) public static function write_updates_file($updatesFilepath, $updates)
{ {
if (empty($updatesFilepath)) { if (empty($updatesFilepath)) {
throw new Exception(t('Updates file path is not set, can\'t write updates.')); throw new \Exception('Updates file path is not set, can\'t write updates.');
} }
$res = file_put_contents($updatesFilepath, implode(';', $updates)); $res = file_put_contents($updatesFilepath, implode(';', $updates));
if ($res === false) { if ($res === false) {
throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.')); throw new \Exception('Unable to write updates in '. $updatesFilepath . '.');
}
} }
} }

View file

@ -155,7 +155,7 @@
} }
/* /*
Remove header links style Remove header bookmarks style
*/ */
#pageheader .md_help a { #pageheader .md_help a {
color: lightgray; color: lightgray;

View file

@ -50,7 +50,9 @@
"Shaarli\\Config\\Exception\\": "application/config/exception", "Shaarli\\Config\\Exception\\": "application/config/exception",
"Shaarli\\Exceptions\\": "application/exceptions", "Shaarli\\Exceptions\\": "application/exceptions",
"Shaarli\\Feed\\": "application/feed", "Shaarli\\Feed\\": "application/feed",
"Shaarli\\Formatter\\": "application/formatter",
"Shaarli\\Http\\": "application/http", "Shaarli\\Http\\": "application/http",
"Shaarli\\Legacy\\": "application/legacy",
"Shaarli\\Netscape\\": "application/netscape", "Shaarli\\Netscape\\": "application/netscape",
"Shaarli\\Plugin\\": "application/plugin", "Shaarli\\Plugin\\": "application/plugin",
"Shaarli\\Plugin\\Exception\\": "application/plugin/exception", "Shaarli\\Plugin\\Exception\\": "application/plugin/exception",

20
composer.lock generated
View file

@ -8,16 +8,16 @@
"packages": [ "packages": [
{ {
"name": "arthurhoaro/web-thumbnailer", "name": "arthurhoaro/web-thumbnailer",
"version": "v2.0.0", "version": "v2.0.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/ArthurHoaro/web-thumbnailer.git", "url": "https://github.com/ArthurHoaro/web-thumbnailer.git",
"reference": "609a495277ad3e478738d4b8dd522f9cc50c9faa" "reference": "4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/609a495277ad3e478738d4b8dd522f9cc50c9faa", "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a",
"reference": "609a495277ad3e478738d4b8dd522f9cc50c9faa", "reference": "4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -49,7 +49,7 @@
} }
], ],
"description": "PHP library which will retrieve a thumbnail for any given URL", "description": "PHP library which will retrieve a thumbnail for any given URL",
"time": "2019-08-10T11:33:13+00:00" "time": "2020-01-17T19:42:49+00:00"
}, },
{ {
"name": "erusev/parsedown", "name": "erusev/parsedown",
@ -786,16 +786,16 @@
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
"version": "1.9.4", "version": "1.9.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/myclabs/DeepCopy.git", "url": "https://github.com/myclabs/DeepCopy.git",
"reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7" "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/579bb7356d91f9456ccd505f24ca8b667966a0a7", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef",
"reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7", "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -830,7 +830,7 @@
"object", "object",
"object graph" "object graph"
], ],
"time": "2019-12-15T19:12:40+00:00" "time": "2020-01-17T21:11:47+00:00"
}, },
{ {
"name": "phar-io/manifest", "name": "phar-io/manifest",

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 . 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 ### See also
- [Add a new custom field to shaares (example patch)](https://gist.github.com/nodiscc/8b0194921f059d7b9ad89a581ecd482c) - [Add a new custom field to shaares (example patch)](https://gist.github.com/nodiscc/8b0194921f059d7b9ad89a581ecd482c)

515
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

@ -4,6 +4,7 @@
use DateTime; use DateTime;
use Exception; use Exception;
use Shaarli\Bookmark\Bookmark;
class HistoryTest extends \PHPUnit\Framework\TestCase class HistoryTest extends \PHPUnit\Framework\TestCase
{ {
@ -15,9 +16,11 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
/** /**
* Delete history file. * Delete history file.
*/ */
public function tearDown() public function setUp()
{ {
@unlink(self::$historyFilePath); if (file_exists(self::$historyFilePath)) {
unlink(self::$historyFilePath);
}
} }
/** /**
@ -73,137 +76,140 @@ public function testConstructNotParsable()
public function testAddLink() public function testAddLink()
{ {
$history = new History(self::$historyFilePath); $history = new History(self::$historyFilePath);
$history->addLink(['id' => 0]); $bookmark = (new Bookmark())->setId(0);
$history->addLink($bookmark);
$actual = $history->getHistory()[0]; $actual = $history->getHistory()[0];
$this->assertEquals(History::CREATED, $actual['event']); $this->assertEquals(History::CREATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(0, $actual['id']); $this->assertEquals(0, $actual['id']);
$history = new History(self::$historyFilePath); $history = new History(self::$historyFilePath);
$history->addLink(['id' => 1]); $bookmark = (new Bookmark())->setId(1);
$history->addLink($bookmark);
$actual = $history->getHistory()[0]; $actual = $history->getHistory()[0];
$this->assertEquals(History::CREATED, $actual['event']); $this->assertEquals(History::CREATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(1, $actual['id']); $this->assertEquals(1, $actual['id']);
$history = new History(self::$historyFilePath); $history = new History(self::$historyFilePath);
$history->addLink(['id' => 'str']); $bookmark = (new Bookmark())->setId('str');
$history->addLink($bookmark);
$actual = $history->getHistory()[0]; $actual = $history->getHistory()[0];
$this->assertEquals(History::CREATED, $actual['event']); $this->assertEquals(History::CREATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals('str', $actual['id']); $this->assertEquals('str', $actual['id']);
} }
/** // /**
* Test updated link event // * Test updated link event
*/ // */
public function testUpdateLink() // public function testUpdateLink()
{ // {
$history = new History(self::$historyFilePath); // $history = new History(self::$historyFilePath);
$history->updateLink(['id' => 1]); // $history->updateLink(['id' => 1]);
$actual = $history->getHistory()[0]; // $actual = $history->getHistory()[0];
$this->assertEquals(History::UPDATED, $actual['event']); // $this->assertEquals(History::UPDATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); // $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(1, $actual['id']); // $this->assertEquals(1, $actual['id']);
} // }
//
/** // /**
* Test delete link event // * Test delete link event
*/ // */
public function testDeleteLink() // public function testDeleteLink()
{ // {
$history = new History(self::$historyFilePath); // $history = new History(self::$historyFilePath);
$history->deleteLink(['id' => 1]); // $history->deleteLink(['id' => 1]);
$actual = $history->getHistory()[0]; // $actual = $history->getHistory()[0];
$this->assertEquals(History::DELETED, $actual['event']); // $this->assertEquals(History::DELETED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); // $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(1, $actual['id']); // $this->assertEquals(1, $actual['id']);
} // }
//
/** // /**
* Test updated settings event // * Test updated settings event
*/ // */
public function testUpdateSettings() // public function testUpdateSettings()
{ // {
$history = new History(self::$historyFilePath); // $history = new History(self::$historyFilePath);
$history->updateSettings(); // $history->updateSettings();
$actual = $history->getHistory()[0]; // $actual = $history->getHistory()[0];
$this->assertEquals(History::SETTINGS, $actual['event']); // $this->assertEquals(History::SETTINGS, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); // $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEmpty($actual['id']); // $this->assertEmpty($actual['id']);
} // }
//
/** // /**
* Make sure that new items are stored at the beginning // * Make sure that new items are stored at the beginning
*/ // */
public function testHistoryOrder() // public function testHistoryOrder()
{ // {
$history = new History(self::$historyFilePath); // $history = new History(self::$historyFilePath);
$history->updateLink(['id' => 1]); // $history->updateLink(['id' => 1]);
$actual = $history->getHistory()[0]; // $actual = $history->getHistory()[0];
$this->assertEquals(History::UPDATED, $actual['event']); // $this->assertEquals(History::UPDATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); // $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(1, $actual['id']); // $this->assertEquals(1, $actual['id']);
//
$history->addLink(['id' => 1]); // $history->addLink(['id' => 1]);
$actual = $history->getHistory()[0]; // $actual = $history->getHistory()[0];
$this->assertEquals(History::CREATED, $actual['event']); // $this->assertEquals(History::CREATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); // $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(1, $actual['id']); // $this->assertEquals(1, $actual['id']);
} // }
//
/** // /**
* Re-read history from file after writing an event // * Re-read history from file after writing an event
*/ // */
public function testHistoryRead() // public function testHistoryRead()
{ // {
$history = new History(self::$historyFilePath); // $history = new History(self::$historyFilePath);
$history->updateLink(['id' => 1]); // $history->updateLink(['id' => 1]);
$history = new History(self::$historyFilePath); // $history = new History(self::$historyFilePath);
$actual = $history->getHistory()[0]; // $actual = $history->getHistory()[0];
$this->assertEquals(History::UPDATED, $actual['event']); // $this->assertEquals(History::UPDATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); // $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(1, $actual['id']); // $this->assertEquals(1, $actual['id']);
} // }
//
/** // /**
* Re-read history from file after writing an event and make sure that the order is correct // * Re-read history from file after writing an event and make sure that the order is correct
*/ // */
public function testHistoryOrderRead() // public function testHistoryOrderRead()
{ // {
$history = new History(self::$historyFilePath); // $history = new History(self::$historyFilePath);
$history->updateLink(['id' => 1]); // $history->updateLink(['id' => 1]);
$history->addLink(['id' => 1]); // $history->addLink(['id' => 1]);
//
$history = new History(self::$historyFilePath); // $history = new History(self::$historyFilePath);
$actual = $history->getHistory()[0]; // $actual = $history->getHistory()[0];
$this->assertEquals(History::CREATED, $actual['event']); // $this->assertEquals(History::CREATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); // $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(1, $actual['id']); // $this->assertEquals(1, $actual['id']);
//
$actual = $history->getHistory()[1]; // $actual = $history->getHistory()[1];
$this->assertEquals(History::UPDATED, $actual['event']); // $this->assertEquals(History::UPDATED, $actual['event']);
$this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); // $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
$this->assertEquals(1, $actual['id']); // $this->assertEquals(1, $actual['id']);
} // }
//
/** // /**
* Test retention time: delete old entries. // * Test retention time: delete old entries.
*/ // */
public function testHistoryRententionTime() // public function testHistoryRententionTime()
{ // {
$history = new History(self::$historyFilePath, 5); // $history = new History(self::$historyFilePath, 5);
$history->updateLink(['id' => 1]); // $history->updateLink(['id' => 1]);
$this->assertEquals(1, count($history->getHistory())); // $this->assertEquals(1, count($history->getHistory()));
$arr = $history->getHistory(); // $arr = $history->getHistory();
$arr[0]['datetime'] = new DateTime('-1 hour'); // $arr[0]['datetime'] = new DateTime('-1 hour');
FileUtils::writeFlatDB(self::$historyFilePath, $arr); // FileUtils::writeFlatDB(self::$historyFilePath, $arr);
//
$history = new History(self::$historyFilePath, 60); // $history = new History(self::$historyFilePath, 60);
$this->assertEquals(1, count($history->getHistory())); // $this->assertEquals(1, count($history->getHistory()));
$this->assertEquals(1, $history->getHistory()[0]['id']); // $this->assertEquals(1, $history->getHistory()[0]['id']);
$history->updateLink(['id' => 2]); // $history->updateLink(['id' => 2]);
$this->assertEquals(1, count($history->getHistory())); // $this->assertEquals(1, count($history->getHistory()));
$this->assertEquals(2, $history->getHistory()[0]['id']); // $this->assertEquals(2, $history->getHistory()[0]['id']);
} // }
} }

View file

@ -2,6 +2,7 @@
namespace Shaarli\Api; namespace Shaarli\Api;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -40,18 +41,21 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
protected $container; protected $container;
/** /**
* Before every test, instantiate a new Api with its config, plugins and links. * Before every test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('api.secret', 'NapoleonWasALizard'); $this->conf->set('api.secret', 'NapoleonWasALizard');
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['history'] = $history;
} }
/** /**

View file

@ -2,6 +2,7 @@
namespace Shaarli\Api; namespace Shaarli\Api;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Http\Base64Url; use Shaarli\Http\Base64Url;
/** /**
@ -212,7 +213,7 @@ public function testValidateJwtTokenInvalidTimeFuture()
public function testFormatLinkComplete() public function testFormatLinkComplete()
{ {
$indexUrl = 'https://domain.tld/sub/'; $indexUrl = 'https://domain.tld/sub/';
$link = [ $data = [
'id' => 12, 'id' => 12,
'url' => 'http://lol.lol', 'url' => 'http://lol.lol',
'shorturl' => 'abc', 'shorturl' => 'abc',
@ -223,6 +224,8 @@ public function testFormatLinkComplete()
'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'), 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'),
'updated' => \DateTime::createFromFormat('Ymd_His', '20170107_160612'), 'updated' => \DateTime::createFromFormat('Ymd_His', '20170107_160612'),
]; ];
$bookmark = new Bookmark();
$bookmark->fromArray($data);
$expected = [ $expected = [
'id' => 12, 'id' => 12,
@ -236,7 +239,7 @@ public function testFormatLinkComplete()
'updated' => '2017-01-07T16:06:12+00:00', 'updated' => '2017-01-07T16:06:12+00:00',
]; ];
$this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
} }
/** /**
@ -245,7 +248,7 @@ public function testFormatLinkComplete()
public function testFormatLinkMinimalNote() public function testFormatLinkMinimalNote()
{ {
$indexUrl = 'https://domain.tld/sub/'; $indexUrl = 'https://domain.tld/sub/';
$link = [ $data = [
'id' => 12, 'id' => 12,
'url' => '?abc', 'url' => '?abc',
'shorturl' => 'abc', 'shorturl' => 'abc',
@ -255,6 +258,8 @@ public function testFormatLinkMinimalNote()
'private' => '', 'private' => '',
'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'), 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'),
]; ];
$bookmark = new Bookmark();
$bookmark->fromArray($data);
$expected = [ $expected = [
'id' => 12, 'id' => 12,
@ -268,7 +273,7 @@ public function testFormatLinkMinimalNote()
'updated' => '', 'updated' => '',
]; ];
$this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
} }
/** /**
@ -277,7 +282,7 @@ public function testFormatLinkMinimalNote()
public function testUpdateLink() public function testUpdateLink()
{ {
$created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
$old = [ $data = [
'id' => 12, 'id' => 12,
'url' => '?abc', 'url' => '?abc',
'shorturl' => 'abc', 'shorturl' => 'abc',
@ -287,8 +292,10 @@ public function testUpdateLink()
'private' => '', 'private' => '',
'created' => $created, 'created' => $created,
]; ];
$old = new Bookmark();
$old->fromArray($data);
$new = [ $data = [
'id' => 13, 'id' => 13,
'shorturl' => 'nope', 'shorturl' => 'nope',
'url' => 'http://somewhere.else', 'url' => 'http://somewhere.else',
@ -299,17 +306,18 @@ public function testUpdateLink()
'created' => 'creation', 'created' => 'creation',
'updated' => 'updation', 'updated' => 'updation',
]; ];
$new = new Bookmark();
$new->fromArray($data);
$result = ApiUtils::updateLink($old, $new); $result = ApiUtils::updateLink($old, $new);
$this->assertEquals(12, $result['id']); $this->assertEquals(12, $result->getId());
$this->assertEquals('http://somewhere.else', $result['url']); $this->assertEquals('http://somewhere.else', $result->getUrl());
$this->assertEquals('abc', $result['shorturl']); $this->assertEquals('abc', $result->getShortUrl());
$this->assertEquals('Le Cid', $result['title']); $this->assertEquals('Le Cid', $result->getTitle());
$this->assertEquals('Percé jusques au fond du cœur [...]', $result['description']); $this->assertEquals('Percé jusques au fond du cœur [...]', $result->getDescription());
$this->assertEquals('corneille rodrigue', $result['tags']); $this->assertEquals('corneille rodrigue', $result->getTagsString());
$this->assertEquals(true, $result['private']); $this->assertEquals(true, $result->isPrivate());
$this->assertEquals($created, $result['created']); $this->assertEquals($created, $result->getCreated());
$this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']);
} }
/** /**
@ -318,7 +326,7 @@ public function testUpdateLink()
public function testUpdateLinkMinimal() public function testUpdateLinkMinimal()
{ {
$created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
$old = [ $data = [
'id' => 12, 'id' => 12,
'url' => '?abc', 'url' => '?abc',
'shorturl' => 'abc', 'shorturl' => 'abc',
@ -328,24 +336,19 @@ public function testUpdateLinkMinimal()
'private' => true, 'private' => true,
'created' => $created, 'created' => $created,
]; ];
$old = new Bookmark();
$old->fromArray($data);
$new = [ $new = new Bookmark();
'url' => '',
'title' => '',
'description' => '',
'tags' => '',
'private' => false,
];
$result = ApiUtils::updateLink($old, $new); $result = ApiUtils::updateLink($old, $new);
$this->assertEquals(12, $result['id']); $this->assertEquals(12, $result->getId());
$this->assertEquals('?abc', $result['url']); $this->assertEquals('', $result->getUrl());
$this->assertEquals('abc', $result['shorturl']); $this->assertEquals('abc', $result->getShortUrl());
$this->assertEquals('?abc', $result['title']); $this->assertEquals('', $result->getTitle());
$this->assertEquals('', $result['description']); $this->assertEquals('', $result->getDescription());
$this->assertEquals('', $result['tags']); $this->assertEquals('', $result->getTagsString());
$this->assertEquals(false, $result['private']); $this->assertEquals(false, $result->isPrivate());
$this->assertEquals($created, $result['created']); $this->assertEquals($created, $result->getCreated());
$this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']);
} }
} }

View file

@ -39,11 +39,11 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
protected $controller; protected $controller;
/** /**
* Before every test, instantiate a new Api with its config, plugins and links. * Before every test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->refHistory = new \ReferenceHistory(); $this->refHistory = new \ReferenceHistory();
$this->refHistory->write(self::$testHistory); $this->refHistory->write(self::$testHistory);
$this->container = new Container(); $this->container = new Container();

View file

@ -1,7 +1,10 @@
<?php <?php
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -14,7 +17,7 @@
* *
* @package Api\Controllers * @package Api\Controllers
*/ */
class InfoTest extends \PHPUnit\Framework\TestCase class InfoTest extends TestCase
{ {
/** /**
* @var string datastore to test write operations * @var string datastore to test write operations
@ -42,17 +45,20 @@ class InfoTest extends \PHPUnit\Framework\TestCase
protected $controller; protected $controller;
/** /**
* Before every test, instantiate a new Api with its config, plugins and links. * Before every test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Info($this->container); $this->controller = new Info($this->container);
@ -84,11 +90,11 @@ public function testGetInfo()
$this->assertEquals(2, $data['private_counter']); $this->assertEquals(2, $data['private_counter']);
$this->assertEquals('Shaarli', $data['settings']['title']); $this->assertEquals('Shaarli', $data['settings']['title']);
$this->assertEquals('?', $data['settings']['header_link']); $this->assertEquals('?', $data['settings']['header_link']);
$this->assertEquals('UTC', $data['settings']['timezone']); $this->assertEquals('Europe/Paris', $data['settings']['timezone']);
$this->assertEquals(ConfigManager::$DEFAULT_PLUGINS, $data['settings']['enabled_plugins']); $this->assertEquals(ConfigManager::$DEFAULT_PLUGINS, $data['settings']['enabled_plugins']);
$this->assertEquals(false, $data['settings']['default_private_links']); $this->assertEquals(true, $data['settings']['default_private_links']);
$title = 'My links'; $title = 'My bookmarks';
$headerLink = 'http://shaarli.tld'; $headerLink = 'http://shaarli.tld';
$timezone = 'Europe/Paris'; $timezone = 'Europe/Paris';
$enabledPlugins = array('foo', 'bar'); $enabledPlugins = array('foo', 'bar');

View file

@ -3,7 +3,7 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Slim\Container; use Slim\Container;
@ -34,9 +34,9 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
protected $refDB = null; protected $refDB = null;
/** /**
* @var LinkDB instance. * @var BookmarkFileService instance.
*/ */
protected $linkDB; protected $bookmarkService;
/** /**
* @var HistoryController instance. * @var HistoryController instance.
@ -54,20 +54,22 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
protected $controller; protected $controller;
/** /**
* Before each test, instantiate a new Api with its config, plugins and links. * Before each test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$this->linkDB = new LinkDB(self::$testDatastore, true, false);
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = $this->linkDB; $this->container['db'] = $this->bookmarkService;
$this->container['history'] = $this->history; $this->container['history'] = $this->history;
$this->controller = new Links($this->container); $this->controller = new Links($this->container);
@ -88,7 +90,7 @@ public function tearDown()
public function testDeleteLinkValid() public function testDeleteLinkValid()
{ {
$id = '41'; $id = '41';
$this->assertTrue(isset($this->linkDB[$id])); $this->assertTrue($this->bookmarkService->exists($id));
$env = Environment::mock([ $env = Environment::mock([
'REQUEST_METHOD' => 'DELETE', 'REQUEST_METHOD' => 'DELETE',
]); ]);
@ -98,8 +100,8 @@ public function testDeleteLinkValid()
$this->assertEquals(204, $response->getStatusCode()); $this->assertEquals(204, $response->getStatusCode());
$this->assertEmpty((string) $response->getBody()); $this->assertEmpty((string) $response->getBody());
$this->linkDB = new LinkDB(self::$testDatastore, true, false); $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->assertFalse(isset($this->linkDB[$id])); $this->assertFalse($this->bookmarkService->exists($id));
$historyEntry = $this->history->getHistory()[0]; $historyEntry = $this->history->getHistory()[0];
$this->assertEquals(History::DELETED, $historyEntry['event']); $this->assertEquals(History::DELETED, $historyEntry['event']);
@ -117,7 +119,7 @@ public function testDeleteLinkValid()
public function testDeleteLink404() public function testDeleteLink404()
{ {
$id = -1; $id = -1;
$this->assertFalse(isset($this->linkDB[$id])); $this->assertFalse($this->bookmarkService->exists($id));
$env = Environment::mock([ $env = Environment::mock([
'REQUEST_METHOD' => 'DELETE', 'REQUEST_METHOD' => 'DELETE',
]); ]);

View file

@ -2,7 +2,10 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -50,17 +53,19 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
const NB_FIELDS_LINK = 9; const NB_FIELDS_LINK = 9;
/** /**
* Before each test, instantiate a new Api with its config, plugins and links. * Before each test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Links($this->container); $this->controller = new Links($this->container);
@ -107,7 +112,7 @@ public function testGetLinkId()
$this->assertEquals('sTuff', $data['tags'][0]); $this->assertEquals('sTuff', $data['tags'][0]);
$this->assertEquals(false, $data['private']); $this->assertEquals(false, $data['private']);
$this->assertEquals( $this->assertEquals(
\DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM), \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM),
$data['created'] $data['created']
); );
$this->assertEmpty($data['updated']); $this->assertEmpty($data['updated']);

View file

@ -1,8 +1,11 @@
<?php <?php
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -50,17 +53,19 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
const NB_FIELDS_LINK = 9; const NB_FIELDS_LINK = 9;
/** /**
* Before every test, instantiate a new Api with its config, plugins and links. * Before every test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new LinkDB(self::$testDatastore, true, false); $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Links($this->container); $this->controller = new Links($this->container);
@ -75,7 +80,7 @@ public function tearDown()
} }
/** /**
* Test basic getLinks service: returns all links. * Test basic getLinks service: returns all bookmarks.
*/ */
public function testGetLinks() public function testGetLinks()
{ {
@ -114,7 +119,7 @@ public function testGetLinks()
$this->assertEquals('sTuff', $first['tags'][0]); $this->assertEquals('sTuff', $first['tags'][0]);
$this->assertEquals(false, $first['private']); $this->assertEquals(false, $first['private']);
$this->assertEquals( $this->assertEquals(
\DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM), \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM),
$first['created'] $first['created']
); );
$this->assertEmpty($first['updated']); $this->assertEmpty($first['updated']);
@ -125,7 +130,7 @@ public function testGetLinks()
// Update date // Update date
$this->assertEquals( $this->assertEquals(
\DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033')->format(\DateTime::ATOM), \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160803_093033')->format(\DateTime::ATOM),
$link['updated'] $link['updated']
); );
} }

View file

@ -3,6 +3,8 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Slim\Container; use Slim\Container;
@ -40,6 +42,11 @@ class PostLinkTest extends TestCase
*/ */
protected $refDB = null; protected $refDB = null;
/**
* @var BookmarkFileService instance.
*/
protected $bookmarkService;
/** /**
* @var HistoryController instance. * @var HistoryController instance.
*/ */
@ -61,29 +68,30 @@ class PostLinkTest extends TestCase
const NB_FIELDS_LINK = 9; const NB_FIELDS_LINK = 9;
/** /**
* Before every test, instantiate a new Api with its config, plugins and links. * Before every test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); $this->container['db'] = $this->bookmarkService;
$this->container['history'] = new History(self::$testHistory); $this->container['history'] = $this->history;
$this->controller = new Links($this->container); $this->controller = new Links($this->container);
$mock = $this->createMock(Router::class); $mock = $this->createMock(Router::class);
$mock->expects($this->any()) $mock->expects($this->any())
->method('relativePathFor') ->method('relativePathFor')
->willReturn('api/v1/links/1'); ->willReturn('api/v1/bookmarks/1');
// affect @property-read... seems to work // affect @property-read... seems to work
$this->controller->getCi()->router = $mock; $this->controller->getCi()->router = $mock;
@ -118,7 +126,7 @@ public function testPostLinkMinimal()
$response = $this->controller->postLink($request, new Response()); $response = $this->controller->postLink($request, new Response());
$this->assertEquals(201, $response->getStatusCode()); $this->assertEquals(201, $response->getStatusCode());
$this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]); $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]);
$data = json_decode((string) $response->getBody(), true); $data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data)); $this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(43, $data['id']); $this->assertEquals(43, $data['id']);
@ -127,7 +135,7 @@ public function testPostLinkMinimal()
$this->assertEquals('?' . $data['shorturl'], $data['title']); $this->assertEquals('?' . $data['shorturl'], $data['title']);
$this->assertEquals('', $data['description']); $this->assertEquals('', $data['description']);
$this->assertEquals([], $data['tags']); $this->assertEquals([], $data['tags']);
$this->assertEquals(false, $data['private']); $this->assertEquals(true, $data['private']);
$this->assertTrue( $this->assertTrue(
new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
); );
@ -163,7 +171,7 @@ public function testPostLinkFull()
$response = $this->controller->postLink($request, new Response()); $response = $this->controller->postLink($request, new Response());
$this->assertEquals(201, $response->getStatusCode()); $this->assertEquals(201, $response->getStatusCode());
$this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]); $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]);
$data = json_decode((string) $response->getBody(), true); $data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data)); $this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(43, $data['id']); $this->assertEquals(43, $data['id']);
@ -211,11 +219,11 @@ public function testPostLinkDuplicate()
$this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']); $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
$this->assertEquals(false, $data['private']); $this->assertEquals(false, $data['private']);
$this->assertEquals( $this->assertEquals(
\DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130614_184135'), \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
\DateTime::createFromFormat(\DateTime::ATOM, $data['created']) \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
); );
$this->assertEquals( $this->assertEquals(
\DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130615_184230'), \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
\DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
); );
} }

View file

@ -3,6 +3,8 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Slim\Container; use Slim\Container;
@ -32,6 +34,11 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
*/ */
protected $refDB = null; protected $refDB = null;
/**
* @var BookmarkFileService instance.
*/
protected $bookmarkService;
/** /**
* @var HistoryController instance. * @var HistoryController instance.
*/ */
@ -53,22 +60,23 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
const NB_FIELDS_LINK = 9; const NB_FIELDS_LINK = 9;
/** /**
* Before every test, instantiate a new Api with its config, plugins and links. * Before every test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); $this->container['db'] = $this->bookmarkService;
$this->container['history'] = new History(self::$testHistory); $this->container['history'] = $this->history;
$this->controller = new Links($this->container); $this->controller = new Links($this->container);
@ -110,7 +118,7 @@ public function testPutLinkMinimal()
$this->assertEquals('?WDWyig', $data['title']); $this->assertEquals('?WDWyig', $data['title']);
$this->assertEquals('', $data['description']); $this->assertEquals('', $data['description']);
$this->assertEquals([], $data['tags']); $this->assertEquals([], $data['tags']);
$this->assertEquals(false, $data['private']); $this->assertEquals(true, $data['private']);
$this->assertEquals( $this->assertEquals(
\DateTime::createFromFormat('Ymd_His', '20150310_114651'), \DateTime::createFromFormat('Ymd_His', '20150310_114651'),
\DateTime::createFromFormat(\DateTime::ATOM, $data['created']) \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
@ -199,11 +207,11 @@ public function testPutLinkDuplicate()
$this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']); $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
$this->assertEquals(false, $data['private']); $this->assertEquals(false, $data['private']);
$this->assertEquals( $this->assertEquals(
\DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130614_184135'), \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
\DateTime::createFromFormat(\DateTime::ATOM, $data['created']) \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
); );
$this->assertEquals( $this->assertEquals(
\DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130615_184230'), \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
\DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
); );
} }

View file

@ -3,6 +3,7 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
@ -34,9 +35,9 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
protected $refDB = null; protected $refDB = null;
/** /**
* @var LinkDB instance. * @var BookmarkFileService instance.
*/ */
protected $linkDB; protected $bookmarkService;
/** /**
* @var HistoryController instance. * @var HistoryController instance.
@ -54,20 +55,22 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
protected $controller; protected $controller;
/** /**
* Before each test, instantiate a new Api with its config, plugins and links. * Before each test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$this->linkDB = new LinkDB(self::$testDatastore, true, false);
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = $this->linkDB; $this->container['db'] = $this->bookmarkService;
$this->container['history'] = $this->history; $this->container['history'] = $this->history;
$this->controller = new Tags($this->container); $this->controller = new Tags($this->container);
@ -88,7 +91,7 @@ public function tearDown()
public function testDeleteTagValid() public function testDeleteTagValid()
{ {
$tagName = 'gnu'; $tagName = 'gnu';
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertTrue($tags[$tagName] > 0); $this->assertTrue($tags[$tagName] > 0);
$env = Environment::mock([ $env = Environment::mock([
'REQUEST_METHOD' => 'DELETE', 'REQUEST_METHOD' => 'DELETE',
@ -99,11 +102,11 @@ public function testDeleteTagValid()
$this->assertEquals(204, $response->getStatusCode()); $this->assertEquals(204, $response->getStatusCode());
$this->assertEmpty((string) $response->getBody()); $this->assertEmpty((string) $response->getBody());
$this->linkDB = new LinkDB(self::$testDatastore, true, false); $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertFalse(isset($tags[$tagName])); $this->assertFalse(isset($tags[$tagName]));
// 2 links affected // 2 bookmarks affected
$historyEntry = $this->history->getHistory()[0]; $historyEntry = $this->history->getHistory()[0];
$this->assertEquals(History::UPDATED, $historyEntry['event']); $this->assertEquals(History::UPDATED, $historyEntry['event']);
$this->assertTrue( $this->assertTrue(
@ -122,7 +125,7 @@ public function testDeleteTagValid()
public function testDeleteTagCaseSensitivity() public function testDeleteTagCaseSensitivity()
{ {
$tagName = 'sTuff'; $tagName = 'sTuff';
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertTrue($tags[$tagName] > 0); $this->assertTrue($tags[$tagName] > 0);
$env = Environment::mock([ $env = Environment::mock([
'REQUEST_METHOD' => 'DELETE', 'REQUEST_METHOD' => 'DELETE',
@ -133,8 +136,8 @@ public function testDeleteTagCaseSensitivity()
$this->assertEquals(204, $response->getStatusCode()); $this->assertEquals(204, $response->getStatusCode());
$this->assertEmpty((string) $response->getBody()); $this->assertEmpty((string) $response->getBody());
$this->linkDB = new LinkDB(self::$testDatastore, true, false); $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertFalse(isset($tags[$tagName])); $this->assertFalse(isset($tags[$tagName]));
$this->assertTrue($tags[strtolower($tagName)] > 0); $this->assertTrue($tags[strtolower($tagName)] > 0);
@ -154,7 +157,7 @@ public function testDeleteTagCaseSensitivity()
public function testDeleteLink404() public function testDeleteLink404()
{ {
$tagName = 'nopenope'; $tagName = 'nopenope';
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertFalse(isset($tags[$tagName])); $this->assertFalse(isset($tags[$tagName]));
$env = Environment::mock([ $env = Environment::mock([
'REQUEST_METHOD' => 'DELETE', 'REQUEST_METHOD' => 'DELETE',

View file

@ -2,8 +2,10 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -49,17 +51,19 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
const NB_FIELDS_TAG = 2; const NB_FIELDS_TAG = 2;
/** /**
* Before each test, instantiate a new Api with its config, plugins and links. * Before each test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new LinkDB(self::$testDatastore, true, false); $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Tags($this->container); $this->controller = new Tags($this->container);

View file

@ -1,8 +1,10 @@
<?php <?php
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -38,9 +40,9 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
protected $container; protected $container;
/** /**
* @var LinkDB instance. * @var BookmarkFileService instance.
*/ */
protected $linkDB; protected $bookmarkService;
/** /**
* @var Tags controller instance. * @var Tags controller instance.
@ -53,18 +55,21 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
const NB_FIELDS_TAG = 2; const NB_FIELDS_TAG = 2;
/** /**
* Before every test, instantiate a new Api with its config, plugins and links. * Before every test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
$this->bookmarkService = new BookmarkFileService($this->conf, $history, true);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->linkDB = new LinkDB(self::$testDatastore, true, false); $this->container['db'] = $this->bookmarkService;
$this->container['db'] = $this->linkDB;
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Tags($this->container); $this->controller = new Tags($this->container);
@ -83,7 +88,7 @@ public function tearDown()
*/ */
public function testGetTagsAll() public function testGetTagsAll()
{ {
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$env = Environment::mock([ $env = Environment::mock([
'REQUEST_METHOD' => 'GET', 'REQUEST_METHOD' => 'GET',
]); ]);
@ -136,7 +141,7 @@ public function testGetTagsOffsetLimit()
*/ */
public function testGetTagsLimitAll() public function testGetTagsLimitAll()
{ {
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$env = Environment::mock([ $env = Environment::mock([
'REQUEST_METHOD' => 'GET', 'REQUEST_METHOD' => 'GET',
'QUERY_STRING' => 'limit=all' 'QUERY_STRING' => 'limit=all'
@ -170,7 +175,7 @@ public function testGetTagsOffsetTooHigh()
*/ */
public function testGetTagsVisibilityPrivate() public function testGetTagsVisibilityPrivate()
{ {
$tags = $this->linkDB->linksCountPerTag([], 'private'); $tags = $this->bookmarkService->bookmarksCountPerTag([], 'private');
$env = Environment::mock([ $env = Environment::mock([
'REQUEST_METHOD' => 'GET', 'REQUEST_METHOD' => 'GET',
'QUERY_STRING' => 'visibility=private' 'QUERY_STRING' => 'visibility=private'
@ -190,7 +195,7 @@ public function testGetTagsVisibilityPrivate()
*/ */
public function testGetTagsVisibilityPublic() public function testGetTagsVisibilityPublic()
{ {
$tags = $this->linkDB->linksCountPerTag([], 'public'); $tags = $this->bookmarkService->bookmarksCountPerTag([], 'public');
$env = Environment::mock( $env = Environment::mock(
[ [
'REQUEST_METHOD' => 'GET', 'REQUEST_METHOD' => 'GET',

View file

@ -3,6 +3,7 @@
namespace Shaarli\Api\Controllers; namespace Shaarli\Api\Controllers;
use Shaarli\Api\Exceptions\ApiBadParametersException; use Shaarli\Api\Exceptions\ApiBadParametersException;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
@ -44,9 +45,9 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
protected $container; protected $container;
/** /**
* @var LinkDB instance. * @var BookmarkFileService instance.
*/ */
protected $linkDB; protected $bookmarkService;
/** /**
* @var Tags controller instance. * @var Tags controller instance.
@ -59,22 +60,22 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
const NB_FIELDS_TAG = 2; const NB_FIELDS_TAG = 2;
/** /**
* Before every test, instantiate a new Api with its config, plugins and links. * Before every test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->linkDB = new LinkDB(self::$testDatastore, true, false); $this->container['db'] = $this->bookmarkService;
$this->container['db'] = $this->linkDB;
$this->container['history'] = $this->history; $this->container['history'] = $this->history;
$this->controller = new Tags($this->container); $this->controller = new Tags($this->container);
@ -109,7 +110,7 @@ public function testPutLinkValid()
$this->assertEquals($newName, $data['name']); $this->assertEquals($newName, $data['name']);
$this->assertEquals(2, $data['occurrences']); $this->assertEquals(2, $data['occurrences']);
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertNotTrue(isset($tags[$tagName])); $this->assertNotTrue(isset($tags[$tagName]));
$this->assertEquals(2, $tags[$newName]); $this->assertEquals(2, $tags[$newName]);
@ -133,7 +134,7 @@ public function testPutTagMerge()
$tagName = 'gnu'; $tagName = 'gnu';
$newName = 'w3c'; $newName = 'w3c';
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertEquals(1, $tags[$newName]); $this->assertEquals(1, $tags[$newName]);
$this->assertEquals(2, $tags[$tagName]); $this->assertEquals(2, $tags[$tagName]);
@ -151,7 +152,7 @@ public function testPutTagMerge()
$this->assertEquals($newName, $data['name']); $this->assertEquals($newName, $data['name']);
$this->assertEquals(3, $data['occurrences']); $this->assertEquals(3, $data['occurrences']);
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertNotTrue(isset($tags[$tagName])); $this->assertNotTrue(isset($tags[$tagName]));
$this->assertEquals(3, $tags[$newName]); $this->assertEquals(3, $tags[$newName]);
} }
@ -167,7 +168,7 @@ public function testPutTagEmpty()
$tagName = 'gnu'; $tagName = 'gnu';
$newName = ''; $newName = '';
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertEquals(2, $tags[$tagName]); $this->assertEquals(2, $tags[$tagName]);
$env = Environment::mock([ $env = Environment::mock([
@ -185,7 +186,7 @@ public function testPutTagEmpty()
try { try {
$this->controller->putTag($request, new Response(), ['tagName' => $tagName]); $this->controller->putTag($request, new Response(), ['tagName' => $tagName]);
} catch (ApiBadParametersException $e) { } catch (ApiBadParametersException $e) {
$tags = $this->linkDB->linksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertEquals(2, $tags[$tagName]); $this->assertEquals(2, $tags[$tagName]);
throw $e; throw $e;
} }

View file

@ -0,0 +1,239 @@
<?php
namespace Shaarli\Bookmark;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\Exception\InvalidBookmarkException;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
/**
* Class BookmarkArrayTest
*/
class BookmarkArrayTest extends TestCase
{
/**
* Test the constructor and make sure that the instance is properly initialized
*/
public function testArrayConstructorEmpty()
{
$array = new BookmarkArray();
$this->assertTrue(is_iterable($array));
$this->assertEmpty($array);
}
/**
* Test adding entries to the array, specifying the key offset or not.
*/
public function testArrayAccessAddEntries()
{
$array = new BookmarkArray();
$bookmark = new Bookmark();
$bookmark->setId(11)->validate();
$array[] = $bookmark;
$this->assertCount(1, $array);
$this->assertTrue(isset($array[11]));
$this->assertNull($array[0]);
$this->assertEquals($bookmark, $array[11]);
$bookmark = new Bookmark();
$bookmark->setId(14)->validate();
$array[14] = $bookmark;
$this->assertCount(2, $array);
$this->assertTrue(isset($array[14]));
$this->assertNull($array[0]);
$this->assertEquals($bookmark, $array[14]);
}
/**
* Test adding a bad entry: wrong type
*
* @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
*/
public function testArrayAccessAddBadEntryInstance()
{
$array = new BookmarkArray();
$array[] = 'nope';
}
/**
* Test adding a bad entry: no id
*
* @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
*/
public function testArrayAccessAddBadEntryNoId()
{
$array = new BookmarkArray();
$bookmark = new Bookmark();
$array[] = $bookmark;
}
/**
* Test adding a bad entry: no url
*
* @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
*/
public function testArrayAccessAddBadEntryNoUrl()
{
$array = new BookmarkArray();
$bookmark = (new Bookmark())->setId(11);
$array[] = $bookmark;
}
/**
* Test adding a bad entry: invalid offset
*
* @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
*/
public function testArrayAccessAddBadEntryOffset()
{
$array = new BookmarkArray();
$bookmark = (new Bookmark())->setId(11);
$bookmark->validate();
$array['nope'] = $bookmark;
}
/**
* Test adding a bad entry: invalid ID type
*
* @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
*/
public function testArrayAccessAddBadEntryIdType()
{
$array = new BookmarkArray();
$bookmark = (new Bookmark())->setId('nope');
$bookmark->validate();
$array[] = $bookmark;
}
/**
* Test adding a bad entry: ID/offset not consistent
*
* @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
*/
public function testArrayAccessAddBadEntryIdOffset()
{
$array = new BookmarkArray();
$bookmark = (new Bookmark())->setId(11);
$bookmark->validate();
$array[14] = $bookmark;
}
/**
* Test update entries through array access.
*/
public function testArrayAccessUpdateEntries()
{
$array = new BookmarkArray();
$bookmark = new Bookmark();
$bookmark->setId(11)->validate();
$bookmark->setTitle('old');
$array[] = $bookmark;
$bookmark = new Bookmark();
$bookmark->setId(11)->validate();
$bookmark->setTitle('test');
$array[] = $bookmark;
$this->assertCount(1, $array);
$this->assertEquals('test', $array[11]->getTitle());
$bookmark = new Bookmark();
$bookmark->setId(11)->validate();
$bookmark->setTitle('test2');
$array[11] = $bookmark;
$this->assertCount(1, $array);
$this->assertEquals('test2', $array[11]->getTitle());
}
/**
* Test delete entries through array access.
*/
public function testArrayAccessDeleteEntries()
{
$array = new BookmarkArray();
$bookmark11 = new Bookmark();
$bookmark11->setId(11)->validate();
$array[] = $bookmark11;
$bookmark14 = new Bookmark();
$bookmark14->setId(14)->validate();
$array[] = $bookmark14;
$bookmark23 = new Bookmark();
$bookmark23->setId(23)->validate();
$array[] = $bookmark23;
$bookmark0 = new Bookmark();
$bookmark0->setId(0)->validate();
$array[] = $bookmark0;
$this->assertCount(4, $array);
unset($array[14]);
$this->assertCount(3, $array);
$this->assertEquals($bookmark11, $array[11]);
$this->assertEquals($bookmark23, $array[23]);
$this->assertEquals($bookmark0, $array[0]);
unset($array[23]);
$this->assertCount(2, $array);
$this->assertEquals($bookmark11, $array[11]);
$this->assertEquals($bookmark0, $array[0]);
unset($array[11]);
$this->assertCount(1, $array);
$this->assertEquals($bookmark0, $array[0]);
unset($array[0]);
$this->assertCount(0, $array);
}
/**
* Test iterating through array access.
*/
public function testArrayAccessIterate()
{
$array = new BookmarkArray();
$bookmark11 = new Bookmark();
$bookmark11->setId(11)->validate();
$array[] = $bookmark11;
$bookmark14 = new Bookmark();
$bookmark14->setId(14)->validate();
$array[] = $bookmark14;
$bookmark23 = new Bookmark();
$bookmark23->setId(23)->validate();
$array[] = $bookmark23;
$this->assertCount(3, $array);
foreach ($array as $id => $bookmark) {
$this->assertEquals(${'bookmark'. $id}, $bookmark);
}
}
/**
* Test reordering the array.
*/
public function testReorder()
{
$refDB = new \ReferenceLinkDB();
$refDB->write('sandbox/datastore.php');
$bookmarks = $refDB->getLinks();
$bookmarks->reorder('ASC');
$this->assertInstanceOf(BookmarkArray::class, $bookmarks);
$stickyIds = [11, 10];
$standardIds = [42, 4, 9, 1, 0, 7, 6, 8, 41];
$linkIds = array_merge($stickyIds, $standardIds);
$cpt = 0;
foreach ($bookmarks as $key => $value) {
$this->assertEquals($linkIds[$cpt++], $key);
}
$bookmarks = $refDB->getLinks();
$bookmarks->reorder('DESC');
$this->assertInstanceOf(BookmarkArray::class, $bookmarks);
$linkIds = array_merge(array_reverse($stickyIds), array_reverse($standardIds));
$cpt = 0;
foreach ($bookmarks as $key => $value) {
$this->assertEquals($linkIds[$cpt++], $key);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,514 @@
<?php
namespace Shaarli\Bookmark;
use Exception;
use PHPUnit\Framework\TestCase;
use ReferenceLinkDB;
use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\History;
/**
* Class BookmarkFilterTest.
*/
class BookmarkFilterTest extends TestCase
{
/**
* @var string Test datastore path.
*/
protected static $testDatastore = 'sandbox/datastore.php';
/**
* @var BookmarkFilter instance.
*/
protected static $linkFilter;
/**
* @var ReferenceLinkDB instance
*/
protected static $refDB;
/**
* @var BookmarkFileService instance
*/
protected static $bookmarkService;
/**
* Instantiate linkFilter with ReferenceLinkDB data.
*/
public static function setUpBeforeClass()
{
$conf = new ConfigManager('tests/utils/config/configJson');
$conf->set('resource.datastore', self::$testDatastore);
self::$refDB = new \ReferenceLinkDB();
self::$refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
self::$bookmarkService = new \FakeBookmarkService($conf, $history, true);
self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks());
}
/**
* Blank filter.
*/
public function testFilter()
{
$this->assertEquals(
self::$refDB->countLinks(),
count(self::$linkFilter->filter('', ''))
);
$this->assertEquals(
self::$refDB->countLinks(),
count(self::$linkFilter->filter('', '', 'all'))
);
$this->assertEquals(
self::$refDB->countLinks(),
count(self::$linkFilter->filter('', '', 'randomstr'))
);
// Private only.
$this->assertEquals(
self::$refDB->countPrivateLinks(),
count(self::$linkFilter->filter('', '', false, 'private'))
);
// Public only.
$this->assertEquals(
self::$refDB->countPublicLinks(),
count(self::$linkFilter->filter('', '', false, 'public'))
);
$this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, ''))
);
$this->assertEquals(
self::$refDB->countUntaggedLinks(),
count(
self::$linkFilter->filter(
BookmarkFilter::$FILTER_TAG,
/*$request=*/
'',
/*$casesensitive=*/
false,
/*$visibility=*/
'all',
/*$untaggedonly=*/
true
)
)
);
$this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, ''))
);
}
/**
* Filter bookmarks using a tag
*/
public function testFilterOneTag()
{
$this->assertEquals(
4,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false))
);
$this->assertEquals(
4,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'all'))
);
$this->assertEquals(
4,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'default-blabla'))
);
// Private only.
$this->assertEquals(
1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'private'))
);
// Public only.
$this->assertEquals(
3,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'public'))
);
}
/**
* Filter bookmarks using a tag - case-sensitive
*/
public function testFilterCaseSensitiveTag()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'mercurial', true))
);
$this->assertEquals(
1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'Mercurial', true))
);
}
/**
* Filter bookmarks using a tag combination
*/
public function testFilterMultipleTags()
{
$this->assertEquals(
2,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'dev cartoon', false))
);
}
/**
* Filter bookmarks using a non-existent tag
*/
public function testFilterUnknownTag()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'null', false))
);
}
/**
* Return bookmarks for a given day
*/
public function testFilterDay()
{
$this->assertEquals(
4,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20121206'))
);
}
/**
* 404 - day not found
*/
public function testFilterUnknownDay()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '19700101'))
);
}
/**
* Use an invalid date format
* @expectedException Exception
* @expectedExceptionMessageRegExp /Invalid date format/
*/
public function testFilterInvalidDayWithChars()
{
self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, 'Rainy day, dream away');
}
/**
* Use an invalid date format
* @expectedException Exception
* @expectedExceptionMessageRegExp /Invalid date format/
*/
public function testFilterInvalidDayDigits()
{
self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20');
}
/**
* Retrieve a link entry with its hash
*/
public function testFilterSmallHash()
{
$links = self::$linkFilter->filter(BookmarkFilter::$FILTER_HASH, 'IuWvgA');
$this->assertEquals(
1,
count($links)
);
$this->assertEquals(
'MediaGoblin',
$links[7]->getTitle()
);
}
/**
* No link for this hash
*
* @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
*/
public function testFilterUnknownSmallHash()
{
self::$linkFilter->filter(BookmarkFilter::$FILTER_HASH, 'Iblaah');
}
/**
* Full-text search - no result found.
*/
public function testFilterFullTextNoResult()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'azertyuiop'))
);
}
/**
* Full-text search - result from a link's URL
*/
public function testFilterFullTextURL()
{
$this->assertEquals(
2,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
);
$this->assertEquals(
2,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'ars org'))
);
}
/**
* Full-text search - result from a link's title only
*/
public function testFilterFullTextTitle()
{
// use miscellaneous cases
$this->assertEquals(
2,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'userfriendly -'))
);
$this->assertEquals(
2,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'UserFriendly -'))
);
$this->assertEquals(
2,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
);
// use miscellaneous case and offset
$this->assertEquals(
2,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'RFrIendL'))
);
}
/**
* Full-text search - result from the link's description only
*/
public function testFilterFullTextDescription()
{
$this->assertEquals(
1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'publishing media'))
);
$this->assertEquals(
1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'mercurial w3c'))
);
$this->assertEquals(
3,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, '"free software"'))
);
}
/**
* Full-text search - result from the link's tags only
*/
public function testFilterFullTextTags()
{
$this->assertEquals(
6,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web'))
);
$this->assertEquals(
6,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', 'all'))
);
$this->assertEquals(
6,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', 'bla'))
);
// Private only.
$this->assertEquals(
1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', false, 'private'))
);
// Public only.
$this->assertEquals(
5,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', false, 'public'))
);
}
/**
* Full-text search - result set from mixed sources
*/
public function testFilterFullTextMixed()
{
$this->assertEquals(
3,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'free software'))
);
}
/**
* Full-text search - test exclusion with '-'.
*/
public function testExcludeSearch()
{
$this->assertEquals(
1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'free -gnu'))
);
$this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, '-revolution'))
);
}
/**
* Full-text search - test AND, exact terms and exclusion combined, across fields.
*/
public function testMultiSearch()
{
$this->assertEquals(
2,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TEXT,
'"Free Software " stallman "read this" @website stuff'
))
);
$this->assertEquals(
1,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TEXT,
'"free software " stallman "read this" -beard @website stuff'
))
);
}
/**
* Full-text search - make sure that exact search won't work across fields.
*/
public function testSearchExactTermMultiFieldsKo()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TEXT,
'"designer naming"'
))
);
$this->assertEquals(
0,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TEXT,
'"designernaming"'
))
);
}
/**
* Tag search with exclusion.
*/
public function testTagFilterWithExclusion()
{
$this->assertEquals(
1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'gnu -free'))
);
$this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, '-free'))
);
}
/**
* Test crossed search (terms + tags).
*/
public function testFilterCrossedSearch()
{
$terms = '"Free Software " stallman "read this" @website stuff';
$tags = 'free';
$this->assertEquals(
1,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
array($tags, $terms)
))
);
$this->assertEquals(
2,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
array('', $terms)
))
);
$this->assertEquals(
1,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
array(false, 'PSR-2')
))
);
$this->assertEquals(
1,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
array($tags, '')
))
);
$this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
''
))
);
}
/**
* Filter bookmarks by #hashtag.
*/
public function testFilterByHashtag()
{
$hashtag = 'hashtag';
$this->assertEquals(
3,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TAG,
$hashtag
))
);
$hashtag = 'private';
$this->assertEquals(
1,
count(self::$linkFilter->filter(
BookmarkFilter::$FILTER_TAG,
$hashtag,
false,
'private'
))
);
}
}

View file

@ -0,0 +1,120 @@
<?php
namespace Shaarli\Bookmark;
use PHPUnit\Framework\TestCase;
use ReferenceLinkDB;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
/**
* Class BookmarkInitializerTest
* @package Shaarli\Bookmark
*/
class BookmarkInitializerTest extends TestCase
{
/** @var string Path of test data store */
protected static $testDatastore = 'sandbox/datastore.php';
/** @var string Path of test config file */
protected static $testConf = 'sandbox/config';
/**
* @var ConfigManager instance.
*/
protected $conf;
/**
* @var History instance.
*/
protected $history;
/** @var BookmarkServiceInterface instance */
protected $bookmarkService;
/** @var BookmarkInitializer instance */
protected $initializer;
/**
* Initialize an empty BookmarkFileService
*/
public function setUp()
{
if (file_exists(self::$testDatastore)) {
unlink(self::$testDatastore);
}
copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
$this->conf = new ConfigManager(self::$testConf);
$this->conf->set('resource.datastore', self::$testDatastore);
$this->history = new History('sandbox/history.php');
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->initializer = new BookmarkInitializer($this->bookmarkService);
}
/**
* Test initialize() with an empty data store.
*/
public function testInitializeEmptyDataStore()
{
$refDB = new \ReferenceLinkDB();
$refDB->write(self::$testDatastore);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->initializer = new BookmarkInitializer($this->bookmarkService);
$this->initializer->initialize();
$this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count());
$bookmark = $this->bookmarkService->get(43);
$this->assertEquals(43, $bookmark->getId());
$this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle());
$this->assertTrue($bookmark->isPrivate());
$bookmark = $this->bookmarkService->get(44);
$this->assertEquals(44, $bookmark->getId());
$this->assertEquals(
'The personal, minimalist, super-fast, database free, bookmarking service',
$bookmark->getTitle()
);
$this->assertFalse($bookmark->isPrivate());
// Reload from file
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count());
$bookmark = $this->bookmarkService->get(43);
$this->assertEquals(43, $bookmark->getId());
$this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle());
$this->assertTrue($bookmark->isPrivate());
$bookmark = $this->bookmarkService->get(44);
$this->assertEquals(44, $bookmark->getId());
$this->assertEquals(
'The personal, minimalist, super-fast, database free, bookmarking service',
$bookmark->getTitle()
);
$this->assertFalse($bookmark->isPrivate());
}
/**
* Test initialize() with a data store containing bookmarks.
*/
public function testInitializeNotEmptyDataStore()
{
$this->initializer->initialize();
$this->assertEquals(2, $this->bookmarkService->count());
$bookmark = $this->bookmarkService->get(0);
$this->assertEquals(0, $bookmark->getId());
$this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle());
$this->assertTrue($bookmark->isPrivate());
$bookmark = $this->bookmarkService->get(1);
$this->assertEquals(1, $bookmark->getId());
$this->assertEquals(
'The personal, minimalist, super-fast, database free, bookmarking service',
$bookmark->getTitle()
);
$this->assertFalse($bookmark->isPrivate());
}
}

View file

@ -0,0 +1,388 @@
<?php
namespace Shaarli\Bookmark;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\Exception\InvalidBookmarkException;
/**
* Class BookmarkTest
*/
class BookmarkTest extends TestCase
{
/**
* Test fromArray() with a link with full data
*/
public function testFromArrayFull()
{
$data = [
'id' => 1,
'shorturl' => 'abc',
'url' => 'https://domain.tld/oof.html?param=value#anchor',
'title' => 'This is an array link',
'description' => 'HTML desc<br><p>hi!</p>',
'thumbnail' => 'https://domain.tld/pic.png',
'sticky' => true,
'created' => new \DateTime('-1 minute'),
'tags' => ['tag1', 'tag2', 'chair'],
'updated' => new \DateTime(),
'private' => true,
];
$bookmark = (new Bookmark())->fromArray($data);
$this->assertEquals($data['id'], $bookmark->getId());
$this->assertEquals($data['shorturl'], $bookmark->getShortUrl());
$this->assertEquals($data['url'], $bookmark->getUrl());
$this->assertEquals($data['title'], $bookmark->getTitle());
$this->assertEquals($data['description'], $bookmark->getDescription());
$this->assertEquals($data['thumbnail'], $bookmark->getThumbnail());
$this->assertEquals($data['sticky'], $bookmark->isSticky());
$this->assertEquals($data['created'], $bookmark->getCreated());
$this->assertEquals($data['tags'], $bookmark->getTags());
$this->assertEquals('tag1 tag2 chair', $bookmark->getTagsString());
$this->assertEquals($data['updated'], $bookmark->getUpdated());
$this->assertEquals($data['private'], $bookmark->isPrivate());
$this->assertFalse($bookmark->isNote());
}
/**
* Test fromArray() with a link with minimal data.
* Note that I use null values everywhere but this should not happen in the real world.
*/
public function testFromArrayMinimal()
{
$data = [
'id' => null,
'shorturl' => null,
'url' => null,
'title' => null,
'description' => null,
'created' => null,
'tags' => null,
'private' => null,
];
$bookmark = (new Bookmark())->fromArray($data);
$this->assertNull($bookmark->getId());
$this->assertNull($bookmark->getShortUrl());
$this->assertNull($bookmark->getUrl());
$this->assertNull($bookmark->getTitle());
$this->assertEquals('', $bookmark->getDescription());
$this->assertNull($bookmark->getCreated());
$this->assertEquals([], $bookmark->getTags());
$this->assertEquals('', $bookmark->getTagsString());
$this->assertNull($bookmark->getUpdated());
$this->assertFalse($bookmark->getThumbnail());
$this->assertFalse($bookmark->isSticky());
$this->assertFalse($bookmark->isPrivate());
$this->assertTrue($bookmark->isNote());
}
/**
* Test validate() with a valid minimal bookmark
*/
public function testValidateValidFullBookmark()
{
$bookmark = new Bookmark();
$bookmark->setId(2);
$bookmark->setShortUrl('abc');
$bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
$bookmark->setUpdated($dateUp = \DateTime::createFromFormat('Ymd_His', '20190514_210203'));
$bookmark->setUrl($url = 'https://domain.tld/oof.html?param=value#anchor');
$bookmark->setTitle($title = 'This is an array link');
$bookmark->setDescription($desc = 'HTML desc<br><p>hi!</p>');
$bookmark->setTags($tags = ['tag1', 'tag2', 'chair']);
$bookmark->setThumbnail($thumb = 'https://domain.tld/pic.png');
$bookmark->setPrivate(true);
$bookmark->validate();
$this->assertEquals(2, $bookmark->getId());
$this->assertEquals('abc', $bookmark->getShortUrl());
$this->assertEquals($date, $bookmark->getCreated());
$this->assertEquals($dateUp, $bookmark->getUpdated());
$this->assertEquals($url, $bookmark->getUrl());
$this->assertEquals($title, $bookmark->getTitle());
$this->assertEquals($desc, $bookmark->getDescription());
$this->assertEquals($tags, $bookmark->getTags());
$this->assertEquals(implode(' ', $tags), $bookmark->getTagsString());
$this->assertEquals($thumb, $bookmark->getThumbnail());
$this->assertTrue($bookmark->isPrivate());
$this->assertFalse($bookmark->isNote());
}
/**
* Test validate() with a valid minimal bookmark
*/
public function testValidateValidMinimalBookmark()
{
$bookmark = new Bookmark();
$bookmark->setId(1);
$bookmark->setShortUrl('abc');
$bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
$bookmark->validate();
$this->assertEquals(1, $bookmark->getId());
$this->assertEquals('abc', $bookmark->getShortUrl());
$this->assertEquals($date, $bookmark->getCreated());
$this->assertEquals('?abc', $bookmark->getUrl());
$this->assertEquals('?abc', $bookmark->getTitle());
$this->assertEquals('', $bookmark->getDescription());
$this->assertEquals([], $bookmark->getTags());
$this->assertEquals('', $bookmark->getTagsString());
$this->assertFalse($bookmark->getThumbnail());
$this->assertFalse($bookmark->isPrivate());
$this->assertTrue($bookmark->isNote());
$this->assertNull($bookmark->getUpdated());
}
/**
* Test validate() with a a bookmark without ID.
*/
public function testValidateNotValidNoId()
{
$bookmark = new Bookmark();
$bookmark->setShortUrl('abc');
$bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
$exception = null;
try {
$bookmark->validate();
} catch (InvalidBookmarkException $e) {
$exception = $e;
}
$this->assertNotNull($exception);
$this->assertContains('- ID: '. PHP_EOL, $exception->getMessage());
}
/**
* Test validate() with a a bookmark with a non integer ID.
*/
public function testValidateNotValidStringId()
{
$bookmark = new Bookmark();
$bookmark->setId('str');
$bookmark->setShortUrl('abc');
$bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
$exception = null;
try {
$bookmark->validate();
} catch (InvalidBookmarkException $e) {
$exception = $e;
}
$this->assertNotNull($exception);
$this->assertContains('- ID: str'. PHP_EOL, $exception->getMessage());
}
/**
* Test validate() with a a bookmark without short url.
*/
public function testValidateNotValidNoShortUrl()
{
$bookmark = new Bookmark();
$bookmark->setId(1);
$bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
$bookmark->setShortUrl(null);
$exception = null;
try {
$bookmark->validate();
} catch (InvalidBookmarkException $e) {
$exception = $e;
}
$this->assertNotNull($exception);
$this->assertContains('- ShortUrl: '. PHP_EOL, $exception->getMessage());
}
/**
* Test validate() with a a bookmark without created datetime.
*/
public function testValidateNotValidNoCreated()
{
$bookmark = new Bookmark();
$bookmark->setId(1);
$bookmark->setShortUrl('abc');
$bookmark->setCreated(null);
$exception = null;
try {
$bookmark->validate();
} catch (InvalidBookmarkException $e) {
$exception = $e;
}
$this->assertNotNull($exception);
$this->assertContains('- Created: '. PHP_EOL, $exception->getMessage());
}
/**
* Test validate() with a a bookmark with a bad created datetime.
*/
public function testValidateNotValidBadCreated()
{
$bookmark = new Bookmark();
$bookmark->setId(1);
$bookmark->setShortUrl('abc');
$bookmark->setCreated('hi!');
$exception = null;
try {
$bookmark->validate();
} catch (InvalidBookmarkException $e) {
$exception = $e;
}
$this->assertNotNull($exception);
$this->assertContains('- Created: Not a DateTime object'. PHP_EOL, $exception->getMessage());
}
/**
* Test setId() and make sure that default fields are generated.
*/
public function testSetIdEmptyGeneratedFields()
{
$bookmark = new Bookmark();
$bookmark->setId(2);
$this->assertEquals(2, $bookmark->getId());
$this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
$this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getCreated());
}
/**
* Test setId() and with generated fields already set.
*/
public function testSetIdSetGeneratedFields()
{
$bookmark = new Bookmark();
$bookmark->setShortUrl('abc');
$bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
$bookmark->setId(2);
$this->assertEquals(2, $bookmark->getId());
$this->assertEquals('abc', $bookmark->getShortUrl());
$this->assertEquals($date, $bookmark->getCreated());
}
/**
* Test setUrl() and make sure it accepts custom protocols
*/
public function testGetUrlWithValidProtocols()
{
$bookmark = new Bookmark();
$bookmark->setUrl($url = 'myprotocol://helloworld', ['myprotocol']);
$this->assertEquals($url, $bookmark->getUrl());
$bookmark->setUrl($url = 'https://helloworld.tld', ['myprotocol']);
$this->assertEquals($url, $bookmark->getUrl());
}
/**
* Test setUrl() and make sure it accepts custom protocols
*/
public function testGetUrlWithNotValidProtocols()
{
$bookmark = new Bookmark();
$bookmark->setUrl('myprotocol://helloworld', []);
$this->assertEquals('http://helloworld', $bookmark->getUrl());
$bookmark->setUrl($url = 'https://helloworld.tld', []);
$this->assertEquals($url, $bookmark->getUrl());
}
/**
* Test setTagsString() with exotic data
*/
public function testSetTagsString()
{
$bookmark = new Bookmark();
$str = 'tag1 tag2 tag3.tag3-2, tag4 , -tag5 ';
$bookmark->setTagsString($str);
$this->assertEquals(
[
'tag1',
'tag2',
'tag3.tag3-2',
'tag4',
'tag5',
],
$bookmark->getTags()
);
}
/**
* Test setTags() with exotic data
*/
public function testSetTags()
{
$bookmark = new Bookmark();
$array = [
'tag1 ',
' tag2',
'tag3.tag3-2,',
', tag4',
', ',
'-tag5 ',
];
$bookmark->setTags($array);
$this->assertEquals(
[
'tag1',
'tag2',
'tag3.tag3-2',
'tag4',
'tag5',
],
$bookmark->getTags()
);
}
/**
* Test renameTag()
*/
public function testRenameTag()
{
$bookmark = new Bookmark();
$bookmark->setTags(['tag1', 'tag2', 'chair']);
$bookmark->renameTag('chair', 'table');
$this->assertEquals(['tag1', 'tag2', 'table'], $bookmark->getTags());
$bookmark->renameTag('tag1', 'tag42');
$this->assertEquals(['tag42', 'tag2', 'table'], $bookmark->getTags());
$bookmark->renameTag('tag42', 'tag43');
$this->assertEquals(['tag43', 'tag2', 'table'], $bookmark->getTags());
$bookmark->renameTag('table', 'desk');
$this->assertEquals(['tag43', 'tag2', 'desk'], $bookmark->getTags());
}
/**
* Test renameTag() with a tag that is not present in the bookmark
*/
public function testRenameTagNotExists()
{
$bookmark = new Bookmark();
$bookmark->setTags(['tag1', 'tag2', 'chair']);
$bookmark->renameTag('nope', 'table');
$this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
}
/**
* Test deleteTag()
*/
public function testDeleteTag()
{
$bookmark = new Bookmark();
$bookmark->setTags(['tag1', 'tag2', 'chair']);
$bookmark->deleteTag('chair');
$this->assertEquals(['tag1', 'tag2'], $bookmark->getTags());
$bookmark->deleteTag('tag1');
$this->assertEquals(['tag2'], $bookmark->getTags());
$bookmark->deleteTag('tag2');
$this->assertEquals([], $bookmark->getTags());
}
/**
* Test deleteTag() with a tag that is not present in the bookmark
*/
public function testDeleteTagNotExists()
{
$bookmark = new Bookmark();
$bookmark->setTags(['tag1', 'tag2', 'chair']);
$bookmark->deleteTag('nope');
$this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
}
}

View file

@ -388,15 +388,6 @@ public function testCurlDownloadCallbackOkWithDescNotFound()
$this->assertEmpty($keywords); $this->assertEmpty($keywords);
} }
/**
* Test count_private.
*/
public function testCountPrivateLinks()
{
$refDB = new ReferenceLinkDB();
$this->assertEquals($refDB->countPrivateLinks(), count_private($refDB->getLinks()));
}
/** /**
* Test text2clickable. * Test text2clickable.
*/ */

View file

@ -4,3 +4,21 @@
$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson'); $conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
new \Shaarli\Languages('en', $conf); new \Shaarli\Languages('en', $conf);
// is_iterable is only compatible with PHP 7.1+
if (!function_exists('is_iterable')) {
function is_iterable($var)
{
return is_array($var) || $var instanceof \Traversable;
}
}
// TODO: remove this after fixing UT
require_once 'application/bookmark/LinkUtils.php';
require_once 'application/Utils.php';
require_once 'application/http/UrlUtils.php';
require_once 'application/http/HttpUtils.php';
require_once 'application/feed/Cache.php';
require_once 'tests/utils/ReferenceLinkDB.php';
require_once 'tests/utils/ReferenceHistory.php';
require_once 'tests/utils/FakeBookmarkService.php';

View file

@ -24,7 +24,7 @@ public function testRead()
$conf = $this->configIO->read('tests/utils/config/configJson.json.php'); $conf = $this->configIO->read('tests/utils/config/configJson.json.php');
$this->assertEquals('root', $conf['credentials']['login']); $this->assertEquals('root', $conf['credentials']['login']);
$this->assertEquals('lala', $conf['redirector']['url']); $this->assertEquals('lala', $conf['redirector']['url']);
$this->assertEquals('tests/utils/config/datastore.php', $conf['resource']['datastore']); $this->assertEquals('sandbox/datastore.php', $conf['resource']['datastore']);
$this->assertEquals('1', $conf['plugins']['WALLABAG_VERSION']); $this->assertEquals('1', $conf['plugins']['WALLABAG_VERSION']);
} }

View file

@ -4,7 +4,12 @@
use DateTime; use DateTime;
use ReferenceLinkDB; use ReferenceLinkDB;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\History;
/** /**
* FeedBuilderTest class. * FeedBuilderTest class.
@ -30,7 +35,9 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
protected static $testDatastore = 'sandbox/datastore.php'; protected static $testDatastore = 'sandbox/datastore.php';
public static $linkDB; public static $bookmarkService;
public static $formatter;
public static $serverInfo; public static $serverInfo;
@ -39,9 +46,15 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
*/ */
public static function setUpBeforeClass() public static function setUpBeforeClass()
{ {
$refLinkDB = new ReferenceLinkDB(); $conf = new ConfigManager('tests/utils/config/configJson');
$conf->set('resource.datastore', self::$testDatastore);
$refLinkDB = new \ReferenceLinkDB();
$refLinkDB->write(self::$testDatastore); $refLinkDB->write(self::$testDatastore);
self::$linkDB = new LinkDB(self::$testDatastore, true, false); $history = new History('sandbox/history.php');
$factory = new FormatterFactory($conf);
self::$formatter = $factory->getFormatter();
self::$bookmarkService = new BookmarkFileService($conf, $history, true);
self::$serverInfo = array( self::$serverInfo = array(
'HTTPS' => 'Off', 'HTTPS' => 'Off',
'SERVER_NAME' => 'host.tld', 'SERVER_NAME' => 'host.tld',
@ -56,15 +69,15 @@ public static function setUpBeforeClass()
*/ */
public function testGetTypeLanguage() public function testGetTypeLanguage()
{ {
$feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_ATOM, null, null, false); $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage()); $this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage());
$feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_RSS, null, null, false); $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage()); $this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage());
$feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_ATOM, null, null, false); $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
$this->assertEquals('en', $feedBuilder->getTypeLanguage()); $this->assertEquals('en', $feedBuilder->getTypeLanguage());
$feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_RSS, null, null, false); $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
$this->assertEquals('en-en', $feedBuilder->getTypeLanguage()); $this->assertEquals('en-en', $feedBuilder->getTypeLanguage());
} }
@ -73,7 +86,14 @@ public function testGetTypeLanguage()
*/ */
public function testRSSBuildData() public function testRSSBuildData()
{ {
$feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_RSS, self::$serverInfo, null, false); $feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
FeedBuilder::$FEED_RSS,
self::$serverInfo,
null,
false
);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$data = $feedBuilder->buildData(); $data = $feedBuilder->buildData();
// Test headers (RSS) // Test headers (RSS)
@ -88,7 +108,7 @@ public function testRSSBuildData()
// Test first not pinned link (note link) // Test first not pinned link (note link)
$link = $data['links'][array_keys($data['links'])[2]]; $link = $data['links'][array_keys($data['links'])[2]];
$this->assertEquals(41, $link['id']); $this->assertEquals(41, $link['id']);
$this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
$this->assertEquals('http://host.tld/?WDWyig', $link['guid']); $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
$this->assertEquals('http://host.tld/?WDWyig', $link['url']); $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
$this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']); $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
@ -117,7 +137,14 @@ public function testRSSBuildData()
*/ */
public function testAtomBuildData() public function testAtomBuildData()
{ {
$feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); $feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
FeedBuilder::$FEED_ATOM,
self::$serverInfo,
null,
false
);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$data = $feedBuilder->buildData(); $data = $feedBuilder->buildData();
$this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
@ -136,13 +163,20 @@ public function testBuildDataFiltered()
'searchtags' => 'stuff', 'searchtags' => 'stuff',
'searchterm' => 'beard', 'searchterm' => 'beard',
); );
$feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, $criteria, false); $feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
FeedBuilder::$FEED_ATOM,
self::$serverInfo,
$criteria,
false
);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$data = $feedBuilder->buildData(); $data = $feedBuilder->buildData();
$this->assertEquals(1, count($data['links'])); $this->assertEquals(1, count($data['links']));
$link = array_shift($data['links']); $link = array_shift($data['links']);
$this->assertEquals(41, $link['id']); $this->assertEquals(41, $link['id']);
$this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
} }
/** /**
@ -153,13 +187,20 @@ public function testBuildDataCount()
$criteria = array( $criteria = array(
'nb' => '3', 'nb' => '3',
); );
$feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, $criteria, false); $feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
FeedBuilder::$FEED_ATOM,
self::$serverInfo,
$criteria,
false
);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$data = $feedBuilder->buildData(); $data = $feedBuilder->buildData();
$this->assertEquals(3, count($data['links'])); $this->assertEquals(3, count($data['links']));
$link = $data['links'][array_keys($data['links'])[2]]; $link = $data['links'][array_keys($data['links'])[2]];
$this->assertEquals(41, $link['id']); $this->assertEquals(41, $link['id']);
$this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
} }
/** /**
@ -167,7 +208,14 @@ public function testBuildDataCount()
*/ */
public function testBuildDataPermalinks() public function testBuildDataPermalinks()
{ {
$feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); $feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
FeedBuilder::$FEED_ATOM,
self::$serverInfo,
null,
false
);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$feedBuilder->setUsePermalinks(true); $feedBuilder->setUsePermalinks(true);
$data = $feedBuilder->buildData(); $data = $feedBuilder->buildData();
@ -176,7 +224,7 @@ public function testBuildDataPermalinks()
// First link is a permalink // First link is a permalink
$link = $data['links'][array_keys($data['links'])[2]]; $link = $data['links'][array_keys($data['links'])[2]];
$this->assertEquals(41, $link['id']); $this->assertEquals(41, $link['id']);
$this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
$this->assertEquals('http://host.tld/?WDWyig', $link['guid']); $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
$this->assertEquals('http://host.tld/?WDWyig', $link['url']); $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
$this->assertContains('Direct link', $link['description']); $this->assertContains('Direct link', $link['description']);
@ -184,7 +232,7 @@ public function testBuildDataPermalinks()
// Second link is a direct link // Second link is a direct link
$link = $data['links'][array_keys($data['links'])[3]]; $link = $data['links'][array_keys($data['links'])[3]];
$this->assertEquals(8, $link['id']); $this->assertEquals(8, $link['id']);
$this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), $link['created']); $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'), $link['created']);
$this->assertEquals('http://host.tld/?RttfEw', $link['guid']); $this->assertEquals('http://host.tld/?RttfEw', $link['guid']);
$this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']); $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
$this->assertContains('Direct link', $link['description']); $this->assertContains('Direct link', $link['description']);
@ -196,7 +244,14 @@ public function testBuildDataPermalinks()
*/ */
public function testBuildDataHideDates() public function testBuildDataHideDates()
{ {
$feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); $feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
FeedBuilder::$FEED_ATOM,
self::$serverInfo,
null,
false
);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$feedBuilder->setHideDates(true); $feedBuilder->setHideDates(true);
$data = $feedBuilder->buildData(); $data = $feedBuilder->buildData();
@ -204,7 +259,14 @@ public function testBuildDataHideDates()
$this->assertFalse($data['show_dates']); $this->assertFalse($data['show_dates']);
// Show dates while logged in // Show dates while logged in
$feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, true); $feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
FeedBuilder::$FEED_ATOM,
self::$serverInfo,
null,
true
);
$feedBuilder->setLocale(self::$LOCALE); $feedBuilder->setLocale(self::$LOCALE);
$feedBuilder->setHideDates(true); $feedBuilder->setHideDates(true);
$data = $feedBuilder->buildData(); $data = $feedBuilder->buildData();
@ -225,7 +287,8 @@ public function testBuildDataServerSubdir()
'REQUEST_URI' => '/~user/shaarli/index.php?do=feed', 'REQUEST_URI' => '/~user/shaarli/index.php?do=feed',
); );
$feedBuilder = new FeedBuilder( $feedBuilder = new FeedBuilder(
self::$linkDB, self::$bookmarkService,
self::$formatter,
FeedBuilder::$FEED_ATOM, FeedBuilder::$FEED_ATOM,
$serverInfo, $serverInfo,
null, null,

View file

@ -0,0 +1,156 @@
<?php
namespace Shaarli\Formatter;
use DateTime;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
/**
* Class BookmarkDefaultFormatterTest
* @package Shaarli\Formatter
*/
class BookmarkDefaultFormatterTest extends TestCase
{
/** @var string Path of test config file */
protected static $testConf = 'sandbox/config';
/** @var BookmarkFormatter */
protected $formatter;
/** @var ConfigManager instance */
protected $conf;
/**
* Initialize formatter instance.
*/
public function setUp()
{
copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
$this->conf = new ConfigManager(self::$testConf);
$this->formatter = new BookmarkDefaultFormatter($this->conf);
}
/**
* Test formatting a bookmark with all its attribute filled.
*/
public function testFormatFull()
{
$bookmark = new Bookmark();
$bookmark->setId($id = 11);
$bookmark->setShortUrl($short = 'abcdef');
$bookmark->setUrl('https://sub.domain.tld?query=here&for=real#hash');
$bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
$bookmark->setDescription($desc = '<h2>Content</h2><p>`Here is some content</p>');
$bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
$bookmark->setThumbnail('http://domain2.tdl2/?type=img&name=file.png');
$bookmark->setSticky(true);
$bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412'));
$bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213'));
$bookmark->setPrivate(true);
$link = $this->formatter->format($bookmark);
$this->assertEquals($id, $link['id']);
$this->assertEquals($short, $link['shorturl']);
$this->assertEquals('https://sub.domain.tld?query=here&amp;for=real#hash', $link['url']);
$this->assertEquals(
'https://sub.domain.tld?query=here&amp;for=real#hash',
$link['real_url']
);
$this->assertEquals('This is a &lt;strong&gt;bookmark&lt;/strong&gt;', $link['title']);
$this->assertEquals(
'&lt;h2&gt;Content&lt;/h2&gt;&lt;p&gt;`Here is some content&lt;/p&gt;',
$link['description']
);
$tags[3] = '&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;';
$this->assertEquals($tags, $link['taglist']);
$this->assertEquals(implode(' ', $tags), $link['tags']);
$this->assertEquals(
'http://domain2.tdl2/?type=img&amp;name=file.png',
$link['thumbnail']
);
$this->assertEquals($created, $link['created']);
$this->assertEquals($created->getTimestamp(), $link['timestamp']);
$this->assertEquals($updated, $link['updated']);
$this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
$this->assertTrue($link['private']);
$this->assertTrue($link['sticky']);
$this->assertEquals('private', $link['class']);
}
/**
* Test formatting a bookmark with all its attribute filled.
*/
public function testFormatMinimal()
{
$bookmark = new Bookmark();
$link = $this->formatter->format($bookmark);
$this->assertEmpty($link['id']);
$this->assertEmpty($link['shorturl']);
$this->assertEmpty($link['url']);
$this->assertEmpty($link['real_url']);
$this->assertEmpty($link['title']);
$this->assertEmpty($link['description']);
$this->assertEmpty($link['taglist']);
$this->assertEmpty($link['tags']);
$this->assertEmpty($link['thumbnail']);
$this->assertEmpty($link['created']);
$this->assertEmpty($link['timestamp']);
$this->assertEmpty($link['updated']);
$this->assertEmpty($link['updated_timestamp']);
$this->assertFalse($link['private']);
$this->assertFalse($link['sticky']);
$this->assertEmpty($link['class']);
}
/**
* Make sure that the description is properly formatted by the default formatter.
*/
public function testFormatDescription()
{
$description = [];
$description[] = 'This a <strong>description</strong>' . PHP_EOL;
$description[] = 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
$description[] = 'Also, there is an #hashtag added'. PHP_EOL;
$description[] = ' A N D KEEP SPACES ! '. PHP_EOL;
$bookmark = new Bookmark();
$bookmark->setDescription(implode('', $description));
$link = $this->formatter->format($bookmark);
$description[0] = 'This a &lt;strong&gt;description&lt;/strong&gt;<br />';
$url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
$description[1] = 'text <a href="'. $url .'">'. $url .'</a> more text<br />';
$description[2] = 'Also, there is an <a href="?addtag=hashtag" '.
'title="Hashtag hashtag">#hashtag</a> added<br />';
$description[3] = '&nbsp; &nbsp; A &nbsp;N &nbsp;D KEEP &nbsp; &nbsp; '.
'SPACES &nbsp; &nbsp;! &nbsp; <br />';
$this->assertEquals(implode(PHP_EOL, $description) . PHP_EOL, $link['description']);
}
/**
* Test formatting URL with an index_url set
* It should prepend relative links.
*/
public function testFormatNoteWithIndexUrl()
{
$bookmark = new Bookmark();
$bookmark->setUrl($short = '?abcdef');
$description = 'Text #hashtag more text';
$bookmark->setDescription($description);
$this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
$link = $this->formatter->format($bookmark);
$this->assertEquals($root . $short, $link['url']);
$this->assertEquals($root . $short, $link['real_url']);
$this->assertEquals(
'Text <a href="'. $root .'?addtag=hashtag" title="Hashtag hashtag">'.
'#hashtag</a> more text',
$link['description']
);
}
}

View file

@ -0,0 +1,160 @@
<?php
namespace Shaarli\Formatter;
use DateTime;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
/**
* Class BookmarkMarkdownFormatterTest
* @package Shaarli\Formatter
*/
class BookmarkMarkdownFormatterTest extends TestCase
{
/** @var string Path of test config file */
protected static $testConf = 'sandbox/config';
/** @var BookmarkFormatter */
protected $formatter;
/** @var ConfigManager instance */
protected $conf;
/**
* Initialize formatter instance.
*/
public function setUp()
{
copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
$this->conf = new ConfigManager(self::$testConf);
$this->formatter = new BookmarkMarkdownFormatter($this->conf);
}
/**
* Test formatting a bookmark with all its attribute filled.
*/
public function testFormatFull()
{
$bookmark = new Bookmark();
$bookmark->setId($id = 11);
$bookmark->setShortUrl($short = 'abcdef');
$bookmark->setUrl('https://sub.domain.tld?query=here&for=real#hash');
$bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
$bookmark->setDescription('<h2>Content</h2><p>`Here is some content</p>');
$bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
$bookmark->setThumbnail('http://domain2.tdl2/?type=img&name=file.png');
$bookmark->setSticky(true);
$bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412'));
$bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213'));
$bookmark->setPrivate(true);
$link = $this->formatter->format($bookmark);
$this->assertEquals($id, $link['id']);
$this->assertEquals($short, $link['shorturl']);
$this->assertEquals('https://sub.domain.tld?query=here&amp;for=real#hash', $link['url']);
$this->assertEquals(
'https://sub.domain.tld?query=here&amp;for=real#hash',
$link['real_url']
);
$this->assertEquals('This is a &lt;strong&gt;bookmark&lt;/strong&gt;', $link['title']);
$this->assertEquals(
'<div class="markdown"><p>'.
'&lt;h2&gt;Content&lt;/h2&gt;&lt;p&gt;`Here is some content&lt;/p&gt;'.
'</p></div>',
$link['description']
);
$tags[3] = '&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;';
$this->assertEquals($tags, $link['taglist']);
$this->assertEquals(implode(' ', $tags), $link['tags']);
$this->assertEquals(
'http://domain2.tdl2/?type=img&amp;name=file.png',
$link['thumbnail']
);
$this->assertEquals($created, $link['created']);
$this->assertEquals($created->getTimestamp(), $link['timestamp']);
$this->assertEquals($updated, $link['updated']);
$this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
$this->assertTrue($link['private']);
$this->assertTrue($link['sticky']);
$this->assertEquals('private', $link['class']);
}
/**
* Test formatting a bookmark with all its attribute filled.
*/
public function testFormatMinimal()
{
$bookmark = new Bookmark();
$link = $this->formatter->format($bookmark);
$this->assertEmpty($link['id']);
$this->assertEmpty($link['shorturl']);
$this->assertEmpty($link['url']);
$this->assertEmpty($link['real_url']);
$this->assertEmpty($link['title']);
$this->assertEmpty($link['description']);
$this->assertEmpty($link['taglist']);
$this->assertEmpty($link['tags']);
$this->assertEmpty($link['thumbnail']);
$this->assertEmpty($link['created']);
$this->assertEmpty($link['timestamp']);
$this->assertEmpty($link['updated']);
$this->assertEmpty($link['updated_timestamp']);
$this->assertFalse($link['private']);
$this->assertFalse($link['sticky']);
$this->assertEmpty($link['class']);
}
/**
* Make sure that the description is properly formatted by the default formatter.
*/
public function testFormatDescription()
{
$description = 'This a <strong>description</strong>'. PHP_EOL;
$description .= 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
$description .= 'Also, there is an #hashtag added'. PHP_EOL;
$description .= ' A N D KEEP SPACES ! '. PHP_EOL;
$bookmark = new Bookmark();
$bookmark->setDescription($description);
$link = $this->formatter->format($bookmark);
$description = '<div class="markdown"><p>';
$description .= 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'. PHP_EOL;
$url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
$description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL;
$description .= 'Also, there is an <a href="?addtag=hashtag">#hashtag</a> added<br />'. PHP_EOL;
$description .= 'A N D KEEP SPACES ! ';
$description .= '</p></div>';
$this->assertEquals($description, $link['description']);
}
/**
* Test formatting URL with an index_url set
* It should prepend relative links.
*/
public function testFormatNoteWithIndexUrl()
{
$bookmark = new Bookmark();
$bookmark->setUrl($short = '?abcdef');
$description = 'Text #hashtag more text';
$bookmark->setDescription($description);
$this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
$description = '<div class="markdown"><p>';
$description .= 'Text <a href="'. $root .'?addtag=hashtag">#hashtag</a> more text';
$description .= '</p></div>';
$link = $this->formatter->format($bookmark);
$this->assertEquals($root . $short, $link['url']);
$this->assertEquals($root . $short, $link['real_url']);
$this->assertEquals(
$description,
$link['description']
);
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Shaarli\Formatter;
use DateTime;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
/**
* Class BookmarkRawFormatterTest
* @package Shaarli\Formatter
*/
class BookmarkRawFormatterTest extends TestCase
{
/** @var string Path of test config file */
protected static $testConf = 'sandbox/config';
/** @var BookmarkFormatter */
protected $formatter;
/** @var ConfigManager instance */
protected $conf;
/**
* Initialize formatter instance.
*/
public function setUp()
{
copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
$this->conf = new ConfigManager(self::$testConf);
$this->formatter = new BookmarkRawFormatter($this->conf);
}
/**
* Test formatting a bookmark with all its attribute filled.
*/
public function testFormatFull()
{
$bookmark = new Bookmark();
$bookmark->setId($id = 11);
$bookmark->setShortUrl($short = 'abcdef');
$bookmark->setUrl($url = 'https://sub.domain.tld?query=here&for=real#hash');
$bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
$bookmark->setDescription($desc = '<h2>Content</h2><p>`Here is some content</p>');
$bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
$bookmark->setThumbnail($thumb = 'http://domain2.tdl2/file.png');
$bookmark->setSticky(true);
$bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412'));
$bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213'));
$bookmark->setPrivate(true);
$link = $this->formatter->format($bookmark);
$this->assertEquals($id, $link['id']);
$this->assertEquals($short, $link['shorturl']);
$this->assertEquals($url, $link['url']);
$this->assertEquals($url, $link['real_url']);
$this->assertEquals($title, $link['title']);
$this->assertEquals($desc, $link['description']);
$this->assertEquals($tags, $link['taglist']);
$this->assertEquals(implode(' ', $tags), $link['tags']);
$this->assertEquals($thumb, $link['thumbnail']);
$this->assertEquals($created, $link['created']);
$this->assertEquals($created->getTimestamp(), $link['timestamp']);
$this->assertEquals($updated, $link['updated']);
$this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
$this->assertTrue($link['private']);
$this->assertTrue($link['sticky']);
$this->assertEquals('private', $link['class']);
}
/**
* Test formatting a bookmark with all its attribute filled.
*/
public function testFormatMinimal()
{
$bookmark = new Bookmark();
$link = $this->formatter->format($bookmark);
$this->assertEmpty($link['id']);
$this->assertEmpty($link['shorturl']);
$this->assertEmpty($link['url']);
$this->assertEmpty($link['real_url']);
$this->assertEmpty($link['title']);
$this->assertEmpty($link['description']);
$this->assertEmpty($link['taglist']);
$this->assertEmpty($link['tags']);
$this->assertEmpty($link['thumbnail']);
$this->assertEmpty($link['created']);
$this->assertEmpty($link['timestamp']);
$this->assertEmpty($link['updated']);
$this->assertEmpty($link['updated_timestamp']);
$this->assertFalse($link['private']);
$this->assertFalse($link['sticky']);
$this->assertEmpty($link['class']);
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Shaarli\Formatter;
use PHPUnit\Framework\TestCase;
use Shaarli\Config\ConfigManager;
/**
* Class FormatterFactoryTest
*
* @package Shaarli\Formatter
*/
class FormatterFactoryTest extends TestCase
{
/** @var string Path of test config file */
protected static $testConf = 'sandbox/config';
/** @var FormatterFactory instance */
protected $factory;
/** @var ConfigManager instance */
protected $conf;
/**
* Initialize FormatterFactory instance
*/
public function setUp()
{
copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
$this->conf = new ConfigManager(self::$testConf);
$this->factory = new FormatterFactory($this->conf);
}
/**
* Test creating an instance of BookmarkFormatter without any setting -> default formatter
*/
public function testCreateInstanceDefault()
{
$this->assertInstanceOf(BookmarkDefaultFormatter::class, $this->factory->getFormatter());
}
/**
* Test creating an instance of BookmarkDefaultFormatter from settings
*/
public function testCreateInstanceDefaultSetting()
{
$this->conf->set('formatter', 'default');
$this->assertInstanceOf(BookmarkDefaultFormatter::class, $this->factory->getFormatter());
}
/**
* Test creating an instance of BookmarkDefaultFormatter from parameter
*/
public function testCreateInstanceDefaultParameter()
{
$this->assertInstanceOf(
BookmarkDefaultFormatter::class,
$this->factory->getFormatter('default')
);
}
/**
* Test creating an instance of BookmarkRawFormatter from settings
*/
public function testCreateInstanceRawSetting()
{
$this->conf->set('formatter', 'raw');
$this->assertInstanceOf(BookmarkRawFormatter::class, $this->factory->getFormatter());
}
/**
* Test creating an instance of BookmarkRawFormatter from parameter
*/
public function testCreateInstanceRawParameter()
{
$this->assertInstanceOf(
BookmarkRawFormatter::class,
$this->factory->getFormatter('raw')
);
}
/**
* Test creating an instance of BookmarkMarkdownFormatter from settings
*/
public function testCreateInstanceMarkdownSetting()
{
$this->conf->set('formatter', 'markdown');
$this->assertInstanceOf(BookmarkMarkdownFormatter::class, $this->factory->getFormatter());
}
/**
* Test creating an instance of BookmarkMarkdownFormatter from parameter
*/
public function testCreateInstanceMarkdownParameter()
{
$this->assertInstanceOf(
BookmarkMarkdownFormatter::class,
$this->factory->getFormatter('markdown')
);
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Shaarli\Updater;
use Exception;
use ReflectionClass;
use ReflectionMethod;
use Shaarli\Config\ConfigManager;
use Shaarli\Legacy\LegacyLinkDB;
use Shaarli\Legacy\LegacyUpdater;
/**
* Class LegacyDummyUpdater.
* Extends updater to add update method designed for unit tests.
*/
class LegacyDummyUpdater extends LegacyUpdater
{
/**
* Object constructor.
*
* @param array $doneUpdates Updates which are already done.
* @param LegacyLinkDB $linkDB LinkDB instance.
* @param ConfigManager $conf Configuration Manager instance.
* @param boolean $isLoggedIn True if the user is logged in.
*/
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
{
parent::__construct($doneUpdates, $linkDB, $conf, $isLoggedIn);
// Retrieve all update methods.
// For unit test, only retrieve final methods,
$class = new ReflectionClass($this);
$this->methods = $class->getMethods(ReflectionMethod::IS_FINAL);
}
/**
* Update method 1.
*
* @return bool true.
*/
final private function updateMethodDummy1()
{
return true;
}
/**
* Update method 2.
*
* @return bool true.
*/
final private function updateMethodDummy2()
{
return true;
}
/**
* Update method 3.
*
* @return bool true.
*/
final private function updateMethodDummy3()
{
return true;
}
/**
* Update method 4, raise an exception.
*
* @throws Exception error.
*/
final private function updateMethodException()
{
throw new Exception('whatever');
}
}

View file

@ -3,12 +3,13 @@
* Link datastore tests * Link datastore tests
*/ */
namespace Shaarli\Bookmark; namespace Shaarli\Legacy;
use DateTime; use DateTime;
use ReferenceLinkDB; use ReferenceLinkDB;
use ReflectionClass; use ReflectionClass;
use Shaarli; use Shaarli;
use Shaarli\Bookmark\Bookmark;
require_once 'application/feed/Cache.php'; require_once 'application/feed/Cache.php';
require_once 'application/Utils.php'; require_once 'application/Utils.php';
@ -16,9 +17,9 @@
/** /**
* Unitary tests for LinkDB * Unitary tests for LegacyLinkDBTest
*/ */
class LinkDBTest extends \PHPUnit\Framework\TestCase class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
{ {
// datastore to test write operations // datastore to test write operations
protected static $testDatastore = 'sandbox/datastore.php'; protected static $testDatastore = 'sandbox/datastore.php';
@ -29,19 +30,19 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
protected static $refDB = null; protected static $refDB = null;
/** /**
* @var LinkDB public LinkDB instance. * @var LegacyLinkDB public LinkDB instance.
*/ */
protected static $publicLinkDB = null; protected static $publicLinkDB = null;
/** /**
* @var LinkDB private LinkDB instance. * @var LegacyLinkDB private LinkDB instance.
*/ */
protected static $privateLinkDB = null; protected static $privateLinkDB = null;
/** /**
* Instantiates public and private LinkDBs with test data * Instantiates public and private LinkDBs with test data
* *
* The reference datastore contains public and private links that * The reference datastore contains public and private bookmarks that
* will be used to test LinkDB's methods: * will be used to test LinkDB's methods:
* - access filtering (public/private), * - access filtering (public/private),
* - link searches: * - link searches:
@ -58,11 +59,10 @@ protected function setUp()
unlink(self::$testDatastore); unlink(self::$testDatastore);
} }
self::$refDB = new ReferenceLinkDB(); self::$refDB = new ReferenceLinkDB(true);
self::$refDB->write(self::$testDatastore); self::$refDB->write(self::$testDatastore);
self::$publicLinkDB = new LegacyLinkDB(self::$testDatastore, false, false);
self::$publicLinkDB = new LinkDB(self::$testDatastore, false, false); self::$privateLinkDB = new LegacyLinkDB(self::$testDatastore, true, false);
self::$privateLinkDB = new LinkDB(self::$testDatastore, true, false);
} }
/** /**
@ -74,7 +74,7 @@ protected function setUp()
*/ */
protected static function getMethod($name) protected static function getMethod($name)
{ {
$class = new ReflectionClass('Shaarli\Bookmark\LinkDB'); $class = new ReflectionClass('Shaarli\Legacy\LegacyLinkDB');
$method = $class->getMethod($name); $method = $class->getMethod($name);
$method->setAccessible(true); $method->setAccessible(true);
return $method; return $method;
@ -85,7 +85,7 @@ protected static function getMethod($name)
*/ */
public function testConstructLoggedIn() public function testConstructLoggedIn()
{ {
new LinkDB(self::$testDatastore, true, false); new LegacyLinkDB(self::$testDatastore, true, false);
$this->assertFileExists(self::$testDatastore); $this->assertFileExists(self::$testDatastore);
} }
@ -94,7 +94,7 @@ public function testConstructLoggedIn()
*/ */
public function testConstructLoggedOut() public function testConstructLoggedOut()
{ {
new LinkDB(self::$testDatastore, false, false); new LegacyLinkDB(self::$testDatastore, false, false);
$this->assertFileExists(self::$testDatastore); $this->assertFileExists(self::$testDatastore);
} }
@ -106,7 +106,7 @@ public function testConstructLoggedOut()
*/ */
public function testConstructDatastoreNotWriteable() public function testConstructDatastoreNotWriteable()
{ {
new LinkDB('null/store.db', false, false); new LegacyLinkDB('null/store.db', false, false);
} }
/** /**
@ -114,7 +114,7 @@ public function testConstructDatastoreNotWriteable()
*/ */
public function testCheckDBNew() public function testCheckDBNew()
{ {
$linkDB = new LinkDB(self::$testDatastore, false, false); $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
unlink(self::$testDatastore); unlink(self::$testDatastore);
$this->assertFileNotExists(self::$testDatastore); $this->assertFileNotExists(self::$testDatastore);
@ -131,7 +131,7 @@ public function testCheckDBNew()
*/ */
public function testCheckDBLoad() public function testCheckDBLoad()
{ {
$linkDB = new LinkDB(self::$testDatastore, false, false); $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
$datastoreSize = filesize(self::$testDatastore); $datastoreSize = filesize(self::$testDatastore);
$this->assertGreaterThan(0, $datastoreSize); $this->assertGreaterThan(0, $datastoreSize);
@ -151,13 +151,13 @@ public function testCheckDBLoad()
public function testReadEmptyDB() public function testReadEmptyDB()
{ {
file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>'); file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
$emptyDB = new LinkDB(self::$testDatastore, false, false); $emptyDB = new LegacyLinkDB(self::$testDatastore, false, false);
$this->assertEquals(0, sizeof($emptyDB)); $this->assertEquals(0, sizeof($emptyDB));
$this->assertEquals(0, count($emptyDB)); $this->assertEquals(0, count($emptyDB));
} }
/** /**
* Load public links from the DB * Load public bookmarks from the DB
*/ */
public function testReadPublicDB() public function testReadPublicDB()
{ {
@ -168,7 +168,7 @@ public function testReadPublicDB()
} }
/** /**
* Load public and private links from the DB * Load public and private bookmarks from the DB
*/ */
public function testReadPrivateDB() public function testReadPrivateDB()
{ {
@ -179,11 +179,11 @@ public function testReadPrivateDB()
} }
/** /**
* Save the links to the DB * Save the bookmarks to the DB
*/ */
public function testSave() public function testSave()
{ {
$testDB = new LinkDB(self::$testDatastore, true, false); $testDB = new LegacyLinkDB(self::$testDatastore, true, false);
$dbSize = sizeof($testDB); $dbSize = sizeof($testDB);
$link = array( $link = array(
@ -192,18 +192,18 @@ public function testSave()
'url' => 'http://dum.my', 'url' => 'http://dum.my',
'description' => 'One more', 'description' => 'One more',
'private' => 0, 'private' => 0,
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150518_190000'), 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150518_190000'),
'tags' => 'unit test' 'tags' => 'unit test'
); );
$testDB[$link['id']] = $link; $testDB[$link['id']] = $link;
$testDB->save('tests'); $testDB->save('tests');
$testDB = new LinkDB(self::$testDatastore, true, false); $testDB = new LegacyLinkDB(self::$testDatastore, true, false);
$this->assertEquals($dbSize + 1, sizeof($testDB)); $this->assertEquals($dbSize + 1, sizeof($testDB));
} }
/** /**
* Count existing links * Count existing bookmarks
*/ */
public function testCount() public function testCount()
{ {
@ -218,11 +218,11 @@ public function testCount()
} }
/** /**
* Count existing links - public links hidden * Count existing bookmarks - public bookmarks hidden
*/ */
public function testCountHiddenPublic() public function testCountHiddenPublic()
{ {
$linkDB = new LinkDB(self::$testDatastore, false, true); $linkDB = new LegacyLinkDB(self::$testDatastore, false, true);
$this->assertEquals( $this->assertEquals(
0, 0,
@ -235,7 +235,7 @@ public function testCountHiddenPublic()
} }
/** /**
* List the days for which links have been posted * List the days for which bookmarks have been posted
*/ */
public function testDays() public function testDays()
{ {
@ -422,7 +422,7 @@ public function testFilterHashValid()
/** /**
* Test filterHash() with an invalid smallhash. * Test filterHash() with an invalid smallhash.
* *
* @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
*/ */
public function testFilterHashInValid1() public function testFilterHashInValid1()
{ {
@ -433,7 +433,7 @@ public function testFilterHashInValid1()
/** /**
* Test filterHash() with an empty smallhash. * Test filterHash() with an empty smallhash.
* *
* @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
*/ */
public function testFilterHashInValid() public function testFilterHashInValid()
{ {
@ -462,12 +462,12 @@ public function testReorderLinksDesc()
} }
/** /**
* Test rename tag with a valid value present in multiple links * Test rename tag with a valid value present in multiple bookmarks
*/ */
public function testRenameTagMultiple() public function testRenameTagMultiple()
{ {
self::$refDB->write(self::$testDatastore); self::$refDB->write(self::$testDatastore);
$linkDB = new LinkDB(self::$testDatastore, true, false); $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$res = $linkDB->renameTag('cartoon', 'Taz'); $res = $linkDB->renameTag('cartoon', 'Taz');
$this->assertEquals(3, count($res)); $this->assertEquals(3, count($res));
@ -482,7 +482,7 @@ public function testRenameTagMultiple()
public function testRenameTagCaseSensitive() public function testRenameTagCaseSensitive()
{ {
self::$refDB->write(self::$testDatastore); self::$refDB->write(self::$testDatastore);
$linkDB = new LinkDB(self::$testDatastore, true, false); $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$res = $linkDB->renameTag('sTuff', 'Taz'); $res = $linkDB->renameTag('sTuff', 'Taz');
$this->assertEquals(1, count($res)); $this->assertEquals(1, count($res));
@ -494,7 +494,7 @@ public function testRenameTagCaseSensitive()
*/ */
public function testRenameTagInvalid() public function testRenameTagInvalid()
{ {
$linkDB = new LinkDB(self::$testDatastore, false, false); $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
$this->assertFalse($linkDB->renameTag('', 'test')); $this->assertFalse($linkDB->renameTag('', 'test'));
$this->assertFalse($linkDB->renameTag('', '')); $this->assertFalse($linkDB->renameTag('', ''));
@ -509,7 +509,7 @@ public function testRenameTagInvalid()
public function testDeleteTag() public function testDeleteTag()
{ {
self::$refDB->write(self::$testDatastore); self::$refDB->write(self::$testDatastore);
$linkDB = new LinkDB(self::$testDatastore, true, false); $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$res = $linkDB->renameTag('cartoon', null); $res = $linkDB->renameTag('cartoon', null);
$this->assertEquals(3, count($res)); $this->assertEquals(3, count($res));
@ -624,7 +624,7 @@ public function testConsistentOrder()
{ {
$nextId = 43; $nextId = 43;
$creation = DateTime::createFromFormat('Ymd_His', '20190807_130444'); $creation = DateTime::createFromFormat('Ymd_His', '20190807_130444');
$linkDB = new LinkDB(self::$testDatastore, true, false); $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
for ($i = 0; $i < 4; ++$i) { for ($i = 0; $i < 4; ++$i) {
$linkDB[$nextId + $i] = [ $linkDB[$nextId + $i] = [
'id' => $nextId + $i, 'id' => $nextId + $i,
@ -639,7 +639,7 @@ public function testConsistentOrder()
// Check 4 new links 4 times // Check 4 new links 4 times
for ($i = 0; $i < 4; ++$i) { for ($i = 0; $i < 4; ++$i) {
$linkDB->save('tests'); $linkDB->save('tests');
$linkDB = new LinkDB(self::$testDatastore, true, false); $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$count = 3; $count = 3;
foreach ($linkDB as $link) { foreach ($linkDB as $link) {
if ($link['sticky'] === true) { if ($link['sticky'] === true) {

View file

@ -4,18 +4,20 @@
use Exception; use Exception;
use ReferenceLinkDB; use ReferenceLinkDB;
use Shaarli\Legacy\LegacyLinkDB;
use Shaarli\Legacy\LegacyLinkFilter;
/** /**
* Class LinkFilterTest. * Class LegacyLinkFilterTest.
*/ */
class LinkFilterTest extends \PHPUnit\Framework\TestCase class LegacyLinkFilterTest extends \PHPUnit\Framework\TestCase
{ {
/** /**
* @var string Test datastore path. * @var string Test datastore path.
*/ */
protected static $testDatastore = 'sandbox/datastore.php'; protected static $testDatastore = 'sandbox/datastore.php';
/** /**
* @var LinkFilter instance. * @var BookmarkFilter instance.
*/ */
protected static $linkFilter; protected static $linkFilter;
@ -25,7 +27,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
protected static $refDB; protected static $refDB;
/** /**
* @var LinkDB instance * @var LegacyLinkDB instance
*/ */
protected static $linkDB; protected static $linkDB;
@ -34,10 +36,10 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
*/ */
public static function setUpBeforeClass() public static function setUpBeforeClass()
{ {
self::$refDB = new ReferenceLinkDB(); self::$refDB = new ReferenceLinkDB(true);
self::$refDB->write(self::$testDatastore); self::$refDB->write(self::$testDatastore);
self::$linkDB = new LinkDB(self::$testDatastore, true, false); self::$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
self::$linkFilter = new LinkFilter(self::$linkDB); self::$linkFilter = new LegacyLinkFilter(self::$linkDB);
} }
/** /**
@ -74,14 +76,14 @@ public function testFilter()
$this->assertEquals( $this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL, ReferenceLinkDB::$NB_LINKS_TOTAL,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, ''))
); );
$this->assertEquals( $this->assertEquals(
self::$refDB->countUntaggedLinks(), self::$refDB->countUntaggedLinks(),
count( count(
self::$linkFilter->filter( self::$linkFilter->filter(
LinkFilter::$FILTER_TAG, LegacyLinkFilter::$FILTER_TAG,
/*$request=*/ /*$request=*/
'', '',
/*$casesensitive=*/ /*$casesensitive=*/
@ -96,89 +98,89 @@ public function testFilter()
$this->assertEquals( $this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL, ReferenceLinkDB::$NB_LINKS_TOTAL,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, ''))
); );
} }
/** /**
* Filter links using a tag * Filter bookmarks using a tag
*/ */
public function testFilterOneTag() public function testFilterOneTag()
{ {
$this->assertEquals( $this->assertEquals(
4, 4,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false)) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false))
); );
$this->assertEquals( $this->assertEquals(
4, 4,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'all')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'all'))
); );
$this->assertEquals( $this->assertEquals(
4, 4,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'default-blabla')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'default-blabla'))
); );
// Private only. // Private only.
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'private')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'private'))
); );
// Public only. // Public only.
$this->assertEquals( $this->assertEquals(
3, 3,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'public')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'public'))
); );
} }
/** /**
* Filter links using a tag - case-sensitive * Filter bookmarks using a tag - case-sensitive
*/ */
public function testFilterCaseSensitiveTag() public function testFilterCaseSensitiveTag()
{ {
$this->assertEquals( $this->assertEquals(
0, 0,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'mercurial', true)) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'mercurial', true))
); );
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'Mercurial', true)) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'Mercurial', true))
); );
} }
/** /**
* Filter links using a tag combination * Filter bookmarks using a tag combination
*/ */
public function testFilterMultipleTags() public function testFilterMultipleTags()
{ {
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'dev cartoon', false)) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'dev cartoon', false))
); );
} }
/** /**
* Filter links using a non-existent tag * Filter bookmarks using a non-existent tag
*/ */
public function testFilterUnknownTag() public function testFilterUnknownTag()
{ {
$this->assertEquals( $this->assertEquals(
0, 0,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'null', false)) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'null', false))
); );
} }
/** /**
* Return links for a given day * Return bookmarks for a given day
*/ */
public function testFilterDay() public function testFilterDay()
{ {
$this->assertEquals( $this->assertEquals(
4, 4,
count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '20121206'))
); );
} }
@ -189,7 +191,7 @@ public function testFilterUnknownDay()
{ {
$this->assertEquals( $this->assertEquals(
0, 0,
count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '19700101')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '19700101'))
); );
} }
@ -200,7 +202,7 @@ public function testFilterUnknownDay()
*/ */
public function testFilterInvalidDayWithChars() public function testFilterInvalidDayWithChars()
{ {
self::$linkFilter->filter(LinkFilter::$FILTER_DAY, 'Rainy day, dream away'); self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, 'Rainy day, dream away');
} }
/** /**
@ -210,7 +212,7 @@ public function testFilterInvalidDayWithChars()
*/ */
public function testFilterInvalidDayDigits() public function testFilterInvalidDayDigits()
{ {
self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20'); self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '20');
} }
/** /**
@ -218,7 +220,7 @@ public function testFilterInvalidDayDigits()
*/ */
public function testFilterSmallHash() public function testFilterSmallHash()
{ {
$links = self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'IuWvgA'); $links = self::$linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, 'IuWvgA');
$this->assertEquals( $this->assertEquals(
1, 1,
@ -234,11 +236,11 @@ public function testFilterSmallHash()
/** /**
* No link for this hash * No link for this hash
* *
* @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
*/ */
public function testFilterUnknownSmallHash() public function testFilterUnknownSmallHash()
{ {
self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah'); self::$linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, 'Iblaah');
} }
/** /**
@ -248,7 +250,7 @@ public function testFilterFullTextNoResult()
{ {
$this->assertEquals( $this->assertEquals(
0, 0,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'azertyuiop')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'azertyuiop'))
); );
} }
@ -259,12 +261,12 @@ public function testFilterFullTextURL()
{ {
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars.userfriendly.org')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
); );
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars org')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'ars org'))
); );
} }
@ -276,21 +278,21 @@ public function testFilterFullTextTitle()
// use miscellaneous cases // use miscellaneous cases
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'userfriendly -')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'userfriendly -'))
); );
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'UserFriendly -')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'UserFriendly -'))
); );
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
); );
// use miscellaneous case and offset // use miscellaneous case and offset
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'RFrIendL')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'RFrIendL'))
); );
} }
@ -301,17 +303,17 @@ public function testFilterFullTextDescription()
{ {
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'publishing media')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'publishing media'))
); );
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'mercurial w3c')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'mercurial w3c'))
); );
$this->assertEquals( $this->assertEquals(
3, 3,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '"free software"')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, '"free software"'))
); );
} }
@ -322,29 +324,29 @@ public function testFilterFullTextTags()
{ {
$this->assertEquals( $this->assertEquals(
6, 6,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web'))
); );
$this->assertEquals( $this->assertEquals(
6, 6,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', 'all')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', 'all'))
); );
$this->assertEquals( $this->assertEquals(
6, 6,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', 'bla')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', 'bla'))
); );
// Private only. // Private only.
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, 'private')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', false, 'private'))
); );
// Public only. // Public only.
$this->assertEquals( $this->assertEquals(
5, 5,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, 'public')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', false, 'public'))
); );
} }
@ -355,7 +357,7 @@ public function testFilterFullTextMixed()
{ {
$this->assertEquals( $this->assertEquals(
3, 3,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'free software'))
); );
} }
@ -366,12 +368,12 @@ public function testExcludeSearch()
{ {
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free -gnu')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'free -gnu'))
); );
$this->assertEquals( $this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL - 1, ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, '-revolution'))
); );
} }
@ -383,7 +385,7 @@ public function testMultiSearch()
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TEXT,
'"Free Software " stallman "read this" @website stuff' '"Free Software " stallman "read this" @website stuff'
)) ))
); );
@ -391,7 +393,7 @@ public function testMultiSearch()
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TEXT,
'"free software " stallman "read this" -beard @website stuff' '"free software " stallman "read this" -beard @website stuff'
)) ))
); );
@ -405,7 +407,7 @@ public function testSearchExactTermMultiFieldsKo()
$this->assertEquals( $this->assertEquals(
0, 0,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TEXT,
'"designer naming"' '"designer naming"'
)) ))
); );
@ -413,7 +415,7 @@ public function testSearchExactTermMultiFieldsKo()
$this->assertEquals( $this->assertEquals(
0, 0,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TEXT,
'"designernaming"' '"designernaming"'
)) ))
); );
@ -426,12 +428,12 @@ public function testTagFilterWithExclusion()
{ {
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'gnu -free')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'gnu -free'))
); );
$this->assertEquals( $this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL - 1, ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free')) count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, '-free'))
); );
} }
@ -445,42 +447,42 @@ public function testFilterCrossedSearch()
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
array($tags, $terms) array($tags, $terms)
)) ))
); );
$this->assertEquals( $this->assertEquals(
2, 2,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
array('', $terms) array('', $terms)
)) ))
); );
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
array(false, 'PSR-2') array(false, 'PSR-2')
)) ))
); );
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
array($tags, '') array($tags, '')
)) ))
); );
$this->assertEquals( $this->assertEquals(
ReferenceLinkDB::$NB_LINKS_TOTAL, ReferenceLinkDB::$NB_LINKS_TOTAL,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
'' ''
)) ))
); );
} }
/** /**
* Filter links by #hashtag. * Filter bookmarks by #hashtag.
*/ */
public function testFilterByHashtag() public function testFilterByHashtag()
{ {
@ -488,7 +490,7 @@ public function testFilterByHashtag()
$this->assertEquals( $this->assertEquals(
3, 3,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TAG, LegacyLinkFilter::$FILTER_TAG,
$hashtag $hashtag
)) ))
); );
@ -497,7 +499,7 @@ public function testFilterByHashtag()
$this->assertEquals( $this->assertEquals(
1, 1,
count(self::$linkFilter->filter( count(self::$linkFilter->filter(
LinkFilter::$FILTER_TAG, LegacyLinkFilter::$FILTER_TAG,
$hashtag, $hashtag,
false, false,
'private' 'private'

View file

@ -0,0 +1,886 @@
<?php
namespace Shaarli\Updater;
use DateTime;
use Exception;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigJson;
use Shaarli\Config\ConfigManager;
use Shaarli\Config\ConfigPhp;
use Shaarli\Legacy\LegacyLinkDB;
use Shaarli\Legacy\LegacyUpdater;
use Shaarli\Thumbnailer;
require_once 'application/updater/UpdaterUtils.php';
require_once 'tests/updater/DummyUpdater.php';
require_once 'tests/utils/ReferenceLinkDB.php';
require_once 'inc/rain.tpl.class.php';
/**
* Class UpdaterTest.
* Runs unit tests against the updater class.
*/
class LegacyUpdaterTest extends \PHPUnit\Framework\TestCase
{
/**
* @var string Path to test datastore.
*/
protected static $testDatastore = 'sandbox/datastore.php';
/**
* @var string Config file path (without extension).
*/
protected static $configFile = 'sandbox/config';
/**
* @var ConfigManager
*/
protected $conf;
/**
* Executed before each test.
*/
public function setUp()
{
copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
$this->conf = new ConfigManager(self::$configFile);
}
/**
* Test UpdaterUtils::read_updates_file with an empty/missing file.
*/
public function testReadEmptyUpdatesFile()
{
$this->assertEquals(array(), UpdaterUtils::read_updates_file(''));
$updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
touch($updatesFile);
$this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile));
unlink($updatesFile);
}
/**
* Test read/write updates file.
*/
public function testReadWriteUpdatesFile()
{
$updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
$updatesMethods = array('m1', 'm2', 'm3');
UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
$readMethods = UpdaterUtils::read_updates_file($updatesFile);
$this->assertEquals($readMethods, $updatesMethods);
// Update
$updatesMethods[] = 'm4';
UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
$readMethods = UpdaterUtils::read_updates_file($updatesFile);
$this->assertEquals($readMethods, $updatesMethods);
unlink($updatesFile);
}
/**
* Test errors in UpdaterUtils::write_updates_file(): empty updates file.
*
* @expectedException Exception
* @expectedExceptionMessageRegExp /Updates file path is not set(.*)/
*/
public function testWriteEmptyUpdatesFile()
{
UpdaterUtils::write_updates_file('', array('test'));
}
/**
* Test errors in UpdaterUtils::write_updates_file(): not writable updates file.
*
* @expectedException Exception
* @expectedExceptionMessageRegExp /Unable to write(.*)/
*/
public function testWriteUpdatesFileNotWritable()
{
$updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
touch($updatesFile);
chmod($updatesFile, 0444);
try {
@UpdaterUtils::write_updates_file($updatesFile, array('test'));
} catch (Exception $e) {
unlink($updatesFile);
throw $e;
}
}
/**
* Test the update() method, with no update to run.
* 1. Everything already run.
* 2. User is logged out.
*/
public function testNoUpdates()
{
$updates = array(
'updateMethodDummy1',
'updateMethodDummy2',
'updateMethodDummy3',
'updateMethodException',
);
$updater = new DummyUpdater($updates, array(), $this->conf, true);
$this->assertEquals(array(), $updater->update());
$updater = new DummyUpdater(array(), array(), $this->conf, false);
$this->assertEquals(array(), $updater->update());
}
/**
* Test the update() method, with all updates to run (except the failing one).
*/
public function testUpdatesFirstTime()
{
$updates = array('updateMethodException',);
$expectedUpdates = array(
'updateMethodDummy1',
'updateMethodDummy2',
'updateMethodDummy3',
);
$updater = new DummyUpdater($updates, array(), $this->conf, true);
$this->assertEquals($expectedUpdates, $updater->update());
}
/**
* Test the update() method, only one update to run.
*/
public function testOneUpdate()
{
$updates = array(
'updateMethodDummy1',
'updateMethodDummy3',
'updateMethodException',
);
$expectedUpdate = array('updateMethodDummy2');
$updater = new DummyUpdater($updates, array(), $this->conf, true);
$this->assertEquals($expectedUpdate, $updater->update());
}
/**
* Test Update failed.
*
* @expectedException \Exception
*/
public function testUpdateFailed()
{
$updates = array(
'updateMethodDummy1',
'updateMethodDummy2',
'updateMethodDummy3',
);
$updater = new DummyUpdater($updates, array(), $this->conf, true);
$updater->update();
}
/**
* Test update mergeDeprecatedConfig:
* 1. init a config file.
* 2. init a options.php file with update value.
* 3. merge.
* 4. check updated value in config file.
*/
public function testUpdateMergeDeprecatedConfig()
{
$this->conf->setConfigFile('tests/utils/config/configPhp');
$this->conf->reset();
$optionsFile = 'tests/updater/options.php';
$options = '<?php
$GLOBALS[\'privateLinkByDefault\'] = true;';
file_put_contents($optionsFile, $options);
// tmp config file.
$this->conf->setConfigFile('tests/updater/config');
// merge configs
$updater = new LegacyUpdater(array(), array(), $this->conf, true);
// This writes a new config file in tests/updater/config.php
$updater->updateMethodMergeDeprecatedConfigFile();
// make sure updated field is changed
$this->conf->reload();
$this->assertTrue($this->conf->get('privacy.default_private_links'));
$this->assertFalse(is_file($optionsFile));
// Delete the generated file.
unlink($this->conf->getConfigFileExt());
}
/**
* Test mergeDeprecatedConfig in without options file.
*/
public function testMergeDeprecatedConfigNoFile()
{
$updater = new LegacyUpdater(array(), array(), $this->conf, true);
$updater->updateMethodMergeDeprecatedConfigFile();
$this->assertEquals('root', $this->conf->get('credentials.login'));
}
/**
* Test renameDashTags update method.
*/
public function testRenameDashTags()
{
$refDB = new \ReferenceLinkDB(true);
$refDB->write(self::$testDatastore);
$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
$updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
$updater->updateMethodRenameDashTags();
$this->assertNotEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
}
/**
* Convert old PHP config file to JSON config.
*/
public function testConfigToJson()
{
$configFile = 'tests/utils/config/configPhp';
$this->conf->setConfigFile($configFile);
$this->conf->reset();
// The ConfigIO is initialized with ConfigPhp.
$this->assertTrue($this->conf->getConfigIO() instanceof ConfigPhp);
$updater = new LegacyUpdater(array(), array(), $this->conf, false);
$done = $updater->updateMethodConfigToJson();
$this->assertTrue($done);
// The ConfigIO has been updated to ConfigJson.
$this->assertTrue($this->conf->getConfigIO() instanceof ConfigJson);
$this->assertTrue(file_exists($this->conf->getConfigFileExt()));
// Check JSON config data.
$this->conf->reload();
$this->assertEquals('root', $this->conf->get('credentials.login'));
$this->assertEquals('lala', $this->conf->get('redirector.url'));
$this->assertEquals('data/datastore.php', $this->conf->get('resource.datastore'));
$this->assertEquals('1', $this->conf->get('plugins.WALLABAG_VERSION'));
rename($configFile . '.save.php', $configFile . '.php');
unlink($this->conf->getConfigFileExt());
}
/**
* Launch config conversion update with an existing JSON file => nothing to do.
*/
public function testConfigToJsonNothingToDo()
{
$filetime = filemtime($this->conf->getConfigFileExt());
$updater = new LegacyUpdater(array(), array(), $this->conf, false);
$done = $updater->updateMethodConfigToJson();
$this->assertTrue($done);
$expected = filemtime($this->conf->getConfigFileExt());
$this->assertEquals($expected, $filetime);
}
/**
* Test escapeUnescapedConfig with valid data.
*/
public function testEscapeConfig()
{
$sandbox = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandbox . '.json.php');
$this->conf = new ConfigManager($sandbox);
$title = '<script>alert("title");</script>';
$headerLink = '<script>alert("header_link");</script>';
$this->conf->set('general.title', $title);
$this->conf->set('general.header_link', $headerLink);
$updater = new LegacyUpdater(array(), array(), $this->conf, true);
$done = $updater->updateMethodEscapeUnescapedConfig();
$this->assertTrue($done);
$this->conf->reload();
$this->assertEquals(escape($title), $this->conf->get('general.title'));
$this->assertEquals(escape($headerLink), $this->conf->get('general.header_link'));
unlink($sandbox . '.json.php');
}
/**
* Test updateMethodApiSettings(): create default settings for the API (enabled + secret).
*/
public function testUpdateApiSettings()
{
$confFile = 'sandbox/config';
copy(self::$configFile .'.json.php', $confFile .'.json.php');
$conf = new ConfigManager($confFile);
$updater = new LegacyUpdater(array(), array(), $conf, true);
$this->assertFalse($conf->exists('api.enabled'));
$this->assertFalse($conf->exists('api.secret'));
$updater->updateMethodApiSettings();
$conf->reload();
$this->assertTrue($conf->get('api.enabled'));
$this->assertTrue($conf->exists('api.secret'));
unlink($confFile .'.json.php');
}
/**
* Test updateMethodApiSettings(): already set, do nothing.
*/
public function testUpdateApiSettingsNothingToDo()
{
$confFile = 'sandbox/config';
copy(self::$configFile .'.json.php', $confFile .'.json.php');
$conf = new ConfigManager($confFile);
$conf->set('api.enabled', false);
$conf->set('api.secret', '');
$updater = new LegacyUpdater(array(), array(), $conf, true);
$updater->updateMethodApiSettings();
$this->assertFalse($conf->get('api.enabled'));
$this->assertEmpty($conf->get('api.secret'));
unlink($confFile .'.json.php');
}
/**
* Test updateMethodDatastoreIds().
*/
public function testDatastoreIds()
{
$links = array(
'20121206_182539' => array(
'linkdate' => '20121206_182539',
'title' => 'Geek and Poke',
'url' => 'http://geek-and-poke.com/',
'description' => 'desc',
'tags' => 'dev cartoon tag1 tag2 tag3 tag4 ',
'updated' => '20121206_190301',
'private' => false,
),
'20121206_172539' => array(
'linkdate' => '20121206_172539',
'title' => 'UserFriendly - Samba',
'url' => 'http://ars.userfriendly.org/cartoons/?id=20010306',
'description' => '',
'tags' => 'samba cartoon web',
'private' => false,
),
'20121206_142300' => array(
'linkdate' => '20121206_142300',
'title' => 'UserFriendly - Web Designer',
'url' => 'http://ars.userfriendly.org/cartoons/?id=20121206',
'description' => 'Naming conventions... #private',
'tags' => 'samba cartoon web',
'private' => true,
),
);
$refDB = new \ReferenceLinkDB(true);
$refDB->setLinks($links);
$refDB->write(self::$testDatastore);
$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$checksum = hash_file('sha1', self::$testDatastore);
$this->conf->set('resource.data_dir', 'sandbox');
$this->conf->set('resource.datastore', self::$testDatastore);
$updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
$this->assertTrue($updater->updateMethodDatastoreIds());
$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$backupFiles = glob($this->conf->get('resource.data_dir') . '/datastore.'. date('YmdH') .'*.php');
$backup = null;
foreach ($backupFiles as $backupFile) {
if (strpos($backupFile, '_1') === false) {
$backup = $backupFile;
}
}
$this->assertNotNull($backup);
$this->assertFileExists($backup);
$this->assertEquals($checksum, hash_file('sha1', $backup));
unlink($backup);
$this->assertEquals(3, count($linkDB));
$this->assertTrue(isset($linkDB[0]));
$this->assertFalse(isset($linkDB[0]['linkdate']));
$this->assertEquals(0, $linkDB[0]['id']);
$this->assertEquals('UserFriendly - Web Designer', $linkDB[0]['title']);
$this->assertEquals('http://ars.userfriendly.org/cartoons/?id=20121206', $linkDB[0]['url']);
$this->assertEquals('Naming conventions... #private', $linkDB[0]['description']);
$this->assertEquals('samba cartoon web', $linkDB[0]['tags']);
$this->assertTrue($linkDB[0]['private']);
$this->assertEquals(
DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_142300'),
$linkDB[0]['created']
);
$this->assertTrue(isset($linkDB[1]));
$this->assertFalse(isset($linkDB[1]['linkdate']));
$this->assertEquals(1, $linkDB[1]['id']);
$this->assertEquals('UserFriendly - Samba', $linkDB[1]['title']);
$this->assertEquals(
DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_172539'),
$linkDB[1]['created']
);
$this->assertTrue(isset($linkDB[2]));
$this->assertFalse(isset($linkDB[2]['linkdate']));
$this->assertEquals(2, $linkDB[2]['id']);
$this->assertEquals('Geek and Poke', $linkDB[2]['title']);
$this->assertEquals(
DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_182539'),
$linkDB[2]['created']
);
$this->assertEquals(
DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_190301'),
$linkDB[2]['updated']
);
}
/**
* Test updateMethodDatastoreIds() with the update already applied: nothing to do.
*/
public function testDatastoreIdsNothingToDo()
{
$refDB = new \ReferenceLinkDB(true);
$refDB->write(self::$testDatastore);
$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$this->conf->set('resource.data_dir', 'sandbox');
$this->conf->set('resource.datastore', self::$testDatastore);
$checksum = hash_file('sha1', self::$testDatastore);
$updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
$this->assertTrue($updater->updateMethodDatastoreIds());
$this->assertEquals($checksum, hash_file('sha1', self::$testDatastore));
}
/**
* Test defaultTheme update with default settings: nothing to do.
*/
public function testDefaultThemeWithDefaultSettings()
{
$sandbox = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandbox . '.json.php');
$this->conf = new ConfigManager($sandbox);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDefaultTheme());
$this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
$this->assertEquals('default', $this->conf->get('resource.theme'));
$this->conf = new ConfigManager($sandbox);
$this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
$this->assertEquals('default', $this->conf->get('resource.theme'));
unlink($sandbox . '.json.php');
}
/**
* Test defaultTheme update with a custom theme in a subfolder
*/
public function testDefaultThemeWithCustomTheme()
{
$theme = 'iamanartist';
$sandbox = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandbox . '.json.php');
$this->conf = new ConfigManager($sandbox);
mkdir('sandbox/'. $theme);
touch('sandbox/'. $theme .'/linklist.html');
$this->conf->set('resource.raintpl_tpl', 'sandbox/'. $theme .'/');
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDefaultTheme());
$this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
$this->assertEquals($theme, $this->conf->get('resource.theme'));
$this->conf = new ConfigManager($sandbox);
$this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
$this->assertEquals($theme, $this->conf->get('resource.theme'));
unlink($sandbox . '.json.php');
unlink('sandbox/'. $theme .'/linklist.html');
rmdir('sandbox/'. $theme);
}
/**
* Test updateMethodEscapeMarkdown with markdown plugin enabled
* => setting markdown_escape set to false.
*/
public function testEscapeMarkdownSettingToFalse()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.enabled_plugins', ['markdown']);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodEscapeMarkdown());
$this->assertFalse($this->conf->get('security.markdown_escape'));
// reload from file
$this->conf = new ConfigManager($sandboxConf);
$this->assertFalse($this->conf->get('security.markdown_escape'));
}
/**
* Test updateMethodEscapeMarkdown with markdown plugin disabled
* => setting markdown_escape set to true.
*/
public function testEscapeMarkdownSettingToTrue()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.enabled_plugins', []);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodEscapeMarkdown());
$this->assertTrue($this->conf->get('security.markdown_escape'));
// reload from file
$this->conf = new ConfigManager($sandboxConf);
$this->assertTrue($this->conf->get('security.markdown_escape'));
}
/**
* Test updateMethodEscapeMarkdown with nothing to do (setting already enabled)
*/
public function testEscapeMarkdownSettingNothingToDoEnabled()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('security.markdown_escape', true);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodEscapeMarkdown());
$this->assertTrue($this->conf->get('security.markdown_escape'));
}
/**
* Test updateMethodEscapeMarkdown with nothing to do (setting already disabled)
*/
public function testEscapeMarkdownSettingNothingToDoDisabled()
{
$this->conf->set('security.markdown_escape', false);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodEscapeMarkdown());
$this->assertFalse($this->conf->get('security.markdown_escape'));
}
/**
* Test updateMethodPiwikUrl with valid data
*/
public function testUpdatePiwikUrlValid()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$url = 'mypiwik.tld';
$this->conf->set('plugins.PIWIK_URL', $url);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodPiwikUrl());
$this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
// reload from file
$this->conf = new ConfigManager($sandboxConf);
$this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
}
/**
* Test updateMethodPiwikUrl without setting
*/
public function testUpdatePiwikUrlEmpty()
{
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodPiwikUrl());
$this->assertEmpty($this->conf->get('plugins.PIWIK_URL'));
}
/**
* Test updateMethodPiwikUrl: valid URL, nothing to do
*/
public function testUpdatePiwikUrlNothingToDo()
{
$url = 'https://mypiwik.tld';
$this->conf->set('plugins.PIWIK_URL', $url);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodPiwikUrl());
$this->assertEquals($url, $this->conf->get('plugins.PIWIK_URL'));
}
/**
* Test updateMethodAtomDefault with show_atom set to false
* => update to true.
*/
public function testUpdateMethodAtomDefault()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('feed.show_atom', false);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodAtomDefault());
$this->assertTrue($this->conf->get('feed.show_atom'));
// reload from file
$this->conf = new ConfigManager($sandboxConf);
$this->assertTrue($this->conf->get('feed.show_atom'));
}
/**
* Test updateMethodAtomDefault with show_atom not set.
* => nothing to do
*/
public function testUpdateMethodAtomDefaultNoExist()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodAtomDefault());
$this->assertTrue($this->conf->get('feed.show_atom'));
}
/**
* Test updateMethodAtomDefault with show_atom set to true.
* => nothing to do
*/
public function testUpdateMethodAtomDefaultAlreadyTrue()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('feed.show_atom', true);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodAtomDefault());
$this->assertTrue($this->conf->get('feed.show_atom'));
}
/**
* Test updateMethodDownloadSizeAndTimeoutConf, it should be set if none is already defined.
*/
public function testUpdateMethodDownloadSizeAndTimeoutConf()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
$this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
$this->assertEquals(30, $this->conf->get('general.download_timeout'));
$this->conf = new ConfigManager($sandboxConf);
$this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
$this->assertEquals(30, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodDownloadSizeAndTimeoutConf, it shouldn't be set if it is already defined.
*/
public function testUpdateMethodDownloadSizeAndTimeoutConfIgnore()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.download_max_size', 38);
$this->conf->set('general.download_timeout', 70);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
$this->assertEquals(38, $this->conf->get('general.download_max_size'));
$this->assertEquals(70, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodDownloadSizeAndTimeoutConf, only the maz size should be set here.
*/
public function testUpdateMethodDownloadSizeAndTimeoutConfOnlySize()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.download_max_size', 38);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
$this->assertEquals(38, $this->conf->get('general.download_max_size'));
$this->assertEquals(30, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodDownloadSizeAndTimeoutConf, only the time out should be set here.
*/
public function testUpdateMethodDownloadSizeAndTimeoutConfOnlyTimeout()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.download_timeout', 3);
$updater = new LegacyUpdater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
$this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
$this->assertEquals(3, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodWebThumbnailer with thumbnails enabled.
*/
public function testUpdateMethodWebThumbnailerEnabled()
{
$this->conf->remove('thumbnails');
$this->conf->set('thumbnail.enable_thumbnails', true);
$updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
$this->assertEquals(125, $this->conf->get('thumbnails.width'));
$this->assertEquals(90, $this->conf->get('thumbnails.height'));
$this->assertContains('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
}
/**
* Test updateMethodWebThumbnailer with thumbnails disabled.
*/
public function testUpdateMethodWebThumbnailerDisabled()
{
if (isset($_SESSION['warnings'])) {
unset($_SESSION['warnings']);
}
$this->conf->remove('thumbnails');
$this->conf->set('thumbnail.enable_thumbnails', false);
$updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(Thumbnailer::MODE_NONE, $this->conf->get('thumbnails.mode'));
$this->assertEquals(125, $this->conf->get('thumbnails.width'));
$this->assertEquals(90, $this->conf->get('thumbnails.height'));
$this->assertTrue(empty($_SESSION['warnings']));
}
/**
* Test updateMethodWebThumbnailer with thumbnails disabled.
*/
public function testUpdateMethodWebThumbnailerNothingToDo()
{
if (isset($_SESSION['warnings'])) {
unset($_SESSION['warnings']);
}
$updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(Thumbnailer::MODE_COMMON, $this->conf->get('thumbnails.mode'));
$this->assertEquals(90, $this->conf->get('thumbnails.width'));
$this->assertEquals(53, $this->conf->get('thumbnails.height'));
$this->assertTrue(empty($_SESSION['warnings']));
}
/**
* Test updateMethodSetSticky().
*/
public function testUpdateStickyValid()
{
$blank = [
'id' => 1,
'url' => 'z',
'title' => '',
'description' => '',
'tags' => '',
'created' => new DateTime(),
];
$links = [
1 => ['id' => 1] + $blank,
2 => ['id' => 2] + $blank,
];
$refDB = new \ReferenceLinkDB(true);
$refDB->setLinks($links);
$refDB->write(self::$testDatastore);
$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
$this->assertTrue($updater->updateMethodSetSticky());
$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
foreach ($linkDB as $link) {
$this->assertFalse($link['sticky']);
}
}
/**
* Test updateMethodSetSticky().
*/
public function testUpdateStickyNothingToDo()
{
$blank = [
'id' => 1,
'url' => 'z',
'title' => '',
'description' => '',
'tags' => '',
'created' => new DateTime(),
];
$links = [
1 => ['id' => 1, 'sticky' => true] + $blank,
2 => ['id' => 2] + $blank,
];
$refDB = new \ReferenceLinkDB(true);
$refDB->setLinks($links);
$refDB->write(self::$testDatastore);
$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
$this->assertTrue($updater->updateMethodSetSticky());
$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
$this->assertTrue($linkDB[1]['sticky']);
}
/**
* Test updateMethodRemoveRedirector().
*/
public function testUpdateRemoveRedirector()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$updater = new LegacyUpdater([], null, $this->conf, true);
$this->assertTrue($updater->updateMethodRemoveRedirector());
$this->assertFalse($this->conf->exists('redirector'));
$this->conf = new ConfigManager($sandboxConf);
$this->assertFalse($this->conf->exists('redirector'));
}
/**
* Test updateMethodFormatterSetting()
*/
public function testUpdateMethodFormatterSettingDefault()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('formatter', 'default');
$updater = new LegacyUpdater([], null, $this->conf, true);
$enabledPlugins = $this->conf->get('general.enabled_plugins');
$this->assertFalse(in_array('markdown', $enabledPlugins));
$this->assertTrue($updater->updateMethodFormatterSetting());
$this->assertEquals('default', $this->conf->get('formatter'));
$this->assertEquals($enabledPlugins, $this->conf->get('general.enabled_plugins'));
$this->conf = new ConfigManager($sandboxConf);
$this->assertEquals('default', $this->conf->get('formatter'));
$this->assertEquals($enabledPlugins, $this->conf->get('general.enabled_plugins'));
}
/**
* Test updateMethodFormatterSetting()
*/
public function testUpdateMethodFormatterSettingMarkdown()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('formatter', 'default');
$updater = new LegacyUpdater([], null, $this->conf, true);
$enabledPlugins = $this->conf->get('general.enabled_plugins');
$enabledPlugins[] = 'markdown';
$this->conf->set('general.enabled_plugins', $enabledPlugins);
$this->assertTrue(in_array('markdown', $this->conf->get('general.enabled_plugins')));
$this->assertTrue($updater->updateMethodFormatterSetting());
$this->assertEquals('markdown', $this->conf->get('formatter'));
$this->assertFalse(in_array('markdown', $this->conf->get('general.enabled_plugins')));
$this->conf = new ConfigManager($sandboxConf);
$this->assertEquals('markdown', $this->conf->get('formatter'));
$this->assertFalse(in_array('markdown', $this->conf->get('general.enabled_plugins')));
}
}

View file

@ -1,7 +1,12 @@
<?php <?php
namespace Shaarli\Netscape; namespace Shaarli\Netscape;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\Formatter\BookmarkFormatter;
use Shaarli\History;
require_once 'tests/utils/ReferenceLinkDB.php'; require_once 'tests/utils/ReferenceLinkDB.php';
@ -21,18 +26,28 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
protected static $refDb = null; protected static $refDb = null;
/** /**
* @var LinkDB private LinkDB instance. * @var BookmarkFileService private instance.
*/ */
protected static $linkDb = null; protected static $bookmarkService = null;
/**
* @var BookmarkFormatter instance
*/
protected static $formatter;
/** /**
* Instantiate reference data * Instantiate reference data
*/ */
public static function setUpBeforeClass() public static function setUpBeforeClass()
{ {
$conf = new ConfigManager('tests/utils/config/configJson');
$conf->set('resource.datastore', self::$testDatastore);
self::$refDb = new \ReferenceLinkDB(); self::$refDb = new \ReferenceLinkDB();
self::$refDb->write(self::$testDatastore); self::$refDb->write(self::$testDatastore);
self::$linkDb = new LinkDB(self::$testDatastore, true, false); $history = new History('sandbox/history.php');
self::$bookmarkService = new BookmarkFileService($conf, $history, true);
$factory = new FormatterFactory($conf);
self::$formatter = $factory->getFormatter('raw');
} }
/** /**
@ -42,15 +57,27 @@ public static function setUpBeforeClass()
*/ */
public function testFilterAndFormatInvalid() public function testFilterAndFormatInvalid()
{ {
NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'derp', false, ''); NetscapeBookmarkUtils::filterAndFormat(
self::$bookmarkService,
self::$formatter,
'derp',
false,
''
);
} }
/** /**
* Prepare all links for export * Prepare all bookmarks for export
*/ */
public function testFilterAndFormatAll() public function testFilterAndFormatAll()
{ {
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all', false, ''); $links = NetscapeBookmarkUtils::filterAndFormat(
self::$bookmarkService,
self::$formatter,
'all',
false,
''
);
$this->assertEquals(self::$refDb->countLinks(), sizeof($links)); $this->assertEquals(self::$refDb->countLinks(), sizeof($links));
foreach ($links as $link) { foreach ($links as $link) {
$date = $link['created']; $date = $link['created'];
@ -66,11 +93,17 @@ public function testFilterAndFormatAll()
} }
/** /**
* Prepare private links for export * Prepare private bookmarks for export
*/ */
public function testFilterAndFormatPrivate() public function testFilterAndFormatPrivate()
{ {
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private', false, ''); $links = NetscapeBookmarkUtils::filterAndFormat(
self::$bookmarkService,
self::$formatter,
'private',
false,
''
);
$this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links)); $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links));
foreach ($links as $link) { foreach ($links as $link) {
$date = $link['created']; $date = $link['created'];
@ -86,11 +119,17 @@ public function testFilterAndFormatPrivate()
} }
/** /**
* Prepare public links for export * Prepare public bookmarks for export
*/ */
public function testFilterAndFormatPublic() public function testFilterAndFormatPublic()
{ {
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, ''); $links = NetscapeBookmarkUtils::filterAndFormat(
self::$bookmarkService,
self::$formatter,
'public',
false,
''
);
$this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links)); $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links));
foreach ($links as $link) { foreach ($links as $link) {
$date = $link['created']; $date = $link['created'];
@ -110,7 +149,13 @@ public function testFilterAndFormatPublic()
*/ */
public function testFilterAndFormatDoNotPrependNoteUrl() public function testFilterAndFormatDoNotPrependNoteUrl()
{ {
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, ''); $links = NetscapeBookmarkUtils::filterAndFormat(
self::$bookmarkService,
self::$formatter,
'public',
false,
''
);
$this->assertEquals( $this->assertEquals(
'?WDWyig', '?WDWyig',
$links[2]['url'] $links[2]['url']
@ -124,7 +169,8 @@ public function testFilterAndFormatPrependNoteUrl()
{ {
$indexUrl = 'http://localhost:7469/shaarli/'; $indexUrl = 'http://localhost:7469/shaarli/';
$links = NetscapeBookmarkUtils::filterAndFormat( $links = NetscapeBookmarkUtils::filterAndFormat(
self::$linkDb, self::$bookmarkService,
self::$formatter,
'public', 'public',
true, true,
$indexUrl $indexUrl

View file

@ -2,6 +2,9 @@
namespace Shaarli\Netscape; namespace Shaarli\Netscape;
use DateTime; use DateTime;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
@ -41,9 +44,9 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
protected static $historyFilePath = 'sandbox/history.php'; protected static $historyFilePath = 'sandbox/history.php';
/** /**
* @var LinkDB private LinkDB instance * @var BookmarkFileService private LinkDB instance
*/ */
protected $linkDb = null; protected $bookmarkService = null;
/** /**
* @var string Dummy page cache * @var string Dummy page cache
@ -82,10 +85,12 @@ protected function setUp()
} }
// start with an empty datastore // start with an empty datastore
file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>'); file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
$this->linkDb = new LinkDB(self::$testDatastore, true, false);
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.page_cache', $this->pagecache); $this->conf->set('resource.page_cache', $this->pagecache);
$this->conf->set('resource.datastore', self::$testDatastore);
$this->history = new History(self::$historyFilePath); $this->history = new History(self::$historyFilePath);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
} }
/** /**
@ -112,7 +117,7 @@ public function testImportEmptyData()
.' Nothing was imported.', .' Nothing was imported.',
NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
); );
$this->assertEquals(0, count($this->linkDb)); $this->assertEquals(0, $this->bookmarkService->count());
} }
/** /**
@ -125,7 +130,7 @@ public function testImportNoDoctype()
'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.', 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
); );
$this->assertEquals(0, count($this->linkDb)); $this->assertEquals(0, $this->bookmarkService->count());
} }
/** /**
@ -136,10 +141,10 @@ public function testImportLowecaseDoctype()
$files = file2array('lowercase_doctype.htm'); $files = file2array('lowercase_doctype.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:' 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import(null, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import(null, $files, $this->bookmarkService, $this->conf, $this->history)
); );
$this->assertEquals(2, count($this->linkDb)); $this->assertEquals(2, $this->bookmarkService->count());
} }
@ -151,25 +156,24 @@ public function testImportInternetExplorerEncoding()
$files = file2array('internet_explorer_encoding.htm'); $files = file2array('internet_explorer_encoding.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:' 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
.' 1 links imported, 0 links overwritten, 0 links skipped.', .' 1 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
); );
$this->assertEquals(1, count($this->linkDb)); $this->assertEquals(1, $this->bookmarkService->count());
$this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$bookmark = $this->bookmarkService->findByUrl('http://hginit.com/');
$this->assertEquals(0, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160618_203944'),
'id' => 0, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160618_203944'),
'title' => 'Hg Init a Mercurial tutorial by Joel Spolsky',
'url' => 'http://hginit.com/',
'description' => '',
'private' => 0,
'tags' => '',
'shorturl' => 'La37cg',
),
$this->linkDb->getLinkFromUrl('http://hginit.com/')
); );
$this->assertEquals('Hg Init a Mercurial tutorial by Joel Spolsky', $bookmark->getTitle());
$this->assertEquals('http://hginit.com/', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('', $bookmark->getTagsString());
$this->assertEquals('La37cg', $bookmark->getShortUrl());
} }
/** /**
@ -180,116 +184,115 @@ public function testImportNested()
$files = file2array('netscape_nested.htm'); $files = file2array('netscape_nested.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:' 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
.' 8 links imported, 0 links overwritten, 0 links skipped.', .' 8 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
); );
$this->assertEquals(8, count($this->linkDb)); $this->assertEquals(8, $this->bookmarkService->count());
$this->assertEquals(2, count_private($this->linkDb)); $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1');
$this->assertEquals(0, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235541'),
'id' => 0, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235541'),
'title' => 'Nested 1',
'url' => 'http://nest.ed/1',
'description' => '',
'private' => 0,
'tags' => 'tag1 tag2',
'shorturl' => 'KyDNKA',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/1')
); );
$this->assertEquals('Nested 1', $bookmark->getTitle());
$this->assertEquals('http://nest.ed/1', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('tag1 tag2', $bookmark->getTagsString());
$this->assertEquals('KyDNKA', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1-1');
$this->assertEquals(1, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235542'),
'id' => 1, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235542'),
'title' => 'Nested 1-1',
'url' => 'http://nest.ed/1-1',
'description' => '',
'private' => 0,
'tags' => 'folder1 tag1 tag2',
'shorturl' => 'T2LnXg',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/1-1')
); );
$this->assertEquals('Nested 1-1', $bookmark->getTitle());
$this->assertEquals('http://nest.ed/1-1', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('folder1 tag1 tag2', $bookmark->getTagsString());
$this->assertEquals('T2LnXg', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1-2');
$this->assertEquals(2, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235547'),
'id' => 2, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235547'),
'title' => 'Nested 1-2',
'url' => 'http://nest.ed/1-2',
'description' => '',
'private' => 0,
'tags' => 'folder1 tag3 tag4',
'shorturl' => '46SZxA',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/1-2')
); );
$this->assertEquals('Nested 1-2', $bookmark->getTitle());
$this->assertEquals('http://nest.ed/1-2', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('folder1 tag3 tag4', $bookmark->getTagsString());
$this->assertEquals('46SZxA', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2-1');
$this->assertEquals(3, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160202_202222'),
'id' => 3, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'),
'title' => 'Nested 2-1',
'url' => 'http://nest.ed/2-1',
'description' => 'First link of the second section',
'private' => 1,
'tags' => 'folder2',
'shorturl' => '4UHOSw',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/2-1')
); );
$this->assertEquals('Nested 2-1', $bookmark->getTitle());
$this->assertEquals('http://nest.ed/2-1', $bookmark->getUrl());
$this->assertEquals('First link of the second section', $bookmark->getDescription());
$this->assertTrue($bookmark->isPrivate());
$this->assertEquals('folder2', $bookmark->getTagsString());
$this->assertEquals('4UHOSw', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2-2');
$this->assertEquals(4, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160119_230227'),
'id' => 4, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'),
'title' => 'Nested 2-2',
'url' => 'http://nest.ed/2-2',
'description' => 'Second link of the second section',
'private' => 1,
'tags' => 'folder2',
'shorturl' => 'yfzwbw',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/2-2')
); );
$this->assertEquals('Nested 2-2', $bookmark->getTitle());
$this->assertEquals('http://nest.ed/2-2', $bookmark->getUrl());
$this->assertEquals('Second link of the second section', $bookmark->getDescription());
$this->assertTrue($bookmark->isPrivate());
$this->assertEquals('folder2', $bookmark->getTagsString());
$this->assertEquals('yfzwbw', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://nest.ed/3-1');
$this->assertEquals(5, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160202_202222'),
'id' => 5, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'),
'title' => 'Nested 3-1',
'url' => 'http://nest.ed/3-1',
'description' => '',
'private' => 0,
'tags' => 'folder3 folder3-1 tag3',
'shorturl' => 'UwxIUQ',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/3-1')
); );
$this->assertEquals('Nested 3-1', $bookmark->getTitle());
$this->assertEquals('http://nest.ed/3-1', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('folder3 folder3-1 tag3', $bookmark->getTagsString());
$this->assertEquals('UwxIUQ', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://nest.ed/3-2');
$this->assertEquals(6, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160119_230227'),
'id' => 6, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'),
'title' => 'Nested 3-2',
'url' => 'http://nest.ed/3-2',
'description' => '',
'private' => 0,
'tags' => 'folder3 folder3-1',
'shorturl' => 'p8dyZg',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/3-2')
); );
$this->assertEquals('Nested 3-2', $bookmark->getTitle());
$this->assertEquals('http://nest.ed/3-2', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('folder3 folder3-1', $bookmark->getTagsString());
$this->assertEquals('p8dyZg', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2');
$this->assertEquals(7, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160229_111541'),
'id' => 7, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160229_111541'),
'title' => 'Nested 2',
'url' => 'http://nest.ed/2',
'description' => '',
'private' => 0,
'tags' => 'tag4',
'shorturl' => 'Gt3Uug',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/2')
); );
$this->assertEquals('Nested 2', $bookmark->getTitle());
$this->assertEquals('http://nest.ed/2', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('tag4', $bookmark->getTagsString());
$this->assertEquals('Gt3Uug', $bookmark->getShortUrl());
} }
/** /**
@ -302,40 +305,38 @@ public function testImportDefaultPrivacyNoPost()
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
); );
$this->assertEquals(2, count($this->linkDb)); $this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(1, count_private($this->linkDb)); $this->assertEquals(1, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$bookmark = $this->bookmarkService->findByUrl('https://private.tld');
$this->assertEquals(0, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20001010_135536'),
'id' => 0, $bookmark->getCreated()
// Old link - UTC+4 (note that TZ in the import file is ignored).
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20001010_135536'),
'title' => 'Secret stuff',
'url' => 'https://private.tld',
'description' => "Super-secret stuff you're not supposed to know about",
'private' => 1,
'tags' => 'private secret',
'shorturl' => 'EokDtA',
),
$this->linkDb->getLinkFromUrl('https://private.tld')
); );
$this->assertEquals('Secret stuff', $bookmark->getTitle());
$this->assertEquals('https://private.tld', $bookmark->getUrl());
$this->assertEquals('Super-secret stuff you\'re not supposed to know about', $bookmark->getDescription());
$this->assertTrue($bookmark->isPrivate());
$this->assertEquals('private secret', $bookmark->getTagsString());
$this->assertEquals('EokDtA', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://public.tld');
$this->assertEquals(1, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235548'),
'id' => 1, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
'title' => 'Public stuff',
'url' => 'http://public.tld',
'description' => '',
'private' => 0,
'tags' => 'public hello world',
'shorturl' => 'Er9ddA',
),
$this->linkDb->getLinkFromUrl('http://public.tld')
); );
$this->assertEquals('Public stuff', $bookmark->getTitle());
$this->assertEquals('http://public.tld', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('public hello world', $bookmark->getTagsString());
$this->assertEquals('Er9ddA', $bookmark->getShortUrl());
} }
/** /**
@ -347,43 +348,42 @@ public function testImportKeepPrivacy()
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
); );
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(1, count_private($this->linkDb));
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(1, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$bookmark = $this->bookmarkService->findByUrl('https://private.tld');
$this->assertEquals(0, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20001010_135536'),
'id' => 0, $bookmark->getCreated()
// Note that TZ in the import file is ignored.
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20001010_135536'),
'title' => 'Secret stuff',
'url' => 'https://private.tld',
'description' => "Super-secret stuff you're not supposed to know about",
'private' => 1,
'tags' => 'private secret',
'shorturl' => 'EokDtA',
),
$this->linkDb->getLinkFromUrl('https://private.tld')
); );
$this->assertEquals('Secret stuff', $bookmark->getTitle());
$this->assertEquals('https://private.tld', $bookmark->getUrl());
$this->assertEquals('Super-secret stuff you\'re not supposed to know about', $bookmark->getDescription());
$this->assertTrue($bookmark->isPrivate());
$this->assertEquals('private secret', $bookmark->getTagsString());
$this->assertEquals('EokDtA', $bookmark->getShortUrl());
$bookmark = $this->bookmarkService->findByUrl('http://public.tld');
$this->assertEquals(1, $bookmark->getId());
$this->assertEquals( $this->assertEquals(
array( DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235548'),
'id' => 1, $bookmark->getCreated()
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
'title' => 'Public stuff',
'url' => 'http://public.tld',
'description' => '',
'private' => 0,
'tags' => 'public hello world',
'shorturl' => 'Er9ddA',
),
$this->linkDb->getLinkFromUrl('http://public.tld')
); );
$this->assertEquals('Public stuff', $bookmark->getTitle());
$this->assertEquals('http://public.tld', $bookmark->getUrl());
$this->assertEquals('', $bookmark->getDescription());
$this->assertFalse($bookmark->isPrivate());
$this->assertEquals('public hello world', $bookmark->getTagsString());
$this->assertEquals('Er9ddA', $bookmark->getShortUrl());
} }
/** /**
* Import links as public * Import bookmarks as public
*/ */
public function testImportAsPublic() public function testImportAsPublic()
{ {
@ -391,23 +391,17 @@ public function testImportAsPublic()
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
);
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
0,
$this->linkDb[0]['private']
);
$this->assertEquals(
0,
$this->linkDb[1]['private']
); );
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertFalse($this->bookmarkService->get(0)->isPrivate());
$this->assertFalse($this->bookmarkService->get(1)->isPrivate());
} }
/** /**
* Import links as private * Import bookmarks as private
*/ */
public function testImportAsPrivate() public function testImportAsPrivate()
{ {
@ -415,45 +409,34 @@ public function testImportAsPrivate()
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
);
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(2, count_private($this->linkDb));
$this->assertEquals(
1,
$this->linkDb['0']['private']
);
$this->assertEquals(
1,
$this->linkDb['1']['private']
); );
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertTrue($this->bookmarkService->get(0)->isPrivate());
$this->assertTrue($this->bookmarkService->get(1)->isPrivate());
} }
/** /**
* Overwrite private links so they become public * Overwrite private bookmarks so they become public
*/ */
public function testOverwriteAsPublic() public function testOverwriteAsPublic()
{ {
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
// import links as private // import bookmarks as private
$post = array('privacy' => 'private'); $post = array('privacy' => 'private');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
);
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(2, count_private($this->linkDb));
$this->assertEquals(
1,
$this->linkDb[0]['private']
);
$this->assertEquals(
1,
$this->linkDb[1]['private']
); );
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertTrue($this->bookmarkService->get(0)->isPrivate());
$this->assertTrue($this->bookmarkService->get(1)->isPrivate());
// re-import as public, enable overwriting // re-import as public, enable overwriting
$post = array( $post = array(
'privacy' => 'public', 'privacy' => 'public',
@ -461,45 +444,33 @@ public function testOverwriteAsPublic()
); );
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 2 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
);
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
0,
$this->linkDb[0]['private']
);
$this->assertEquals(
0,
$this->linkDb[1]['private']
); );
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertFalse($this->bookmarkService->get(0)->isPrivate());
$this->assertFalse($this->bookmarkService->get(1)->isPrivate());
} }
/** /**
* Overwrite public links so they become private * Overwrite public bookmarks so they become private
*/ */
public function testOverwriteAsPrivate() public function testOverwriteAsPrivate()
{ {
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
// import links as public // import bookmarks as public
$post = array('privacy' => 'public'); $post = array('privacy' => 'public');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
);
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
0,
$this->linkDb['0']['private']
);
$this->assertEquals(
0,
$this->linkDb['1']['private']
); );
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertFalse($this->bookmarkService->get(0)->isPrivate());
$this->assertFalse($this->bookmarkService->get(1)->isPrivate());
// re-import as private, enable overwriting // re-import as private, enable overwriting
$post = array( $post = array(
@ -508,23 +479,17 @@ public function testOverwriteAsPrivate()
); );
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 2 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
);
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(2, count_private($this->linkDb));
$this->assertEquals(
1,
$this->linkDb['0']['private']
);
$this->assertEquals(
1,
$this->linkDb['1']['private']
); );
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertTrue($this->bookmarkService->get(0)->isPrivate());
$this->assertTrue($this->bookmarkService->get(1)->isPrivate());
} }
/** /**
* Attept to import the same links twice without enabling overwriting * Attept to import the same bookmarks twice without enabling overwriting
*/ */
public function testSkipOverwrite() public function testSkipOverwrite()
{ {
@ -532,21 +497,21 @@ public function testSkipOverwrite()
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
); );
$this->assertEquals(2, count($this->linkDb)); $this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
// re-import as private, DO NOT enable overwriting // re-import as private, DO NOT enable overwriting
$post = array('privacy' => 'private'); $post = array('privacy' => 'private');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 0 links imported, 0 links overwritten, 2 links skipped.', .' 0 bookmarks imported, 0 bookmarks overwritten, 2 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
); );
$this->assertEquals(2, count($this->linkDb)); $this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
} }
/** /**
@ -561,19 +526,13 @@ public function testSetDefaultTags()
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
);
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
'tag1 tag2 tag3 private secret',
$this->linkDb['0']['tags']
);
$this->assertEquals(
'tag1 tag2 tag3 public hello world',
$this->linkDb['1']['tags']
); );
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertEquals('tag1 tag2 tag3 private secret', $this->bookmarkService->get(0)->getTagsString());
$this->assertEquals('tag1 tag2 tag3 public hello world', $this->bookmarkService->get(1)->getTagsString());
} }
/** /**
@ -588,18 +547,18 @@ public function testSanitizeDefaultTags()
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.', .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
); );
$this->assertEquals(2, count($this->linkDb)); $this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertEquals( $this->assertEquals(
'tag1&amp; tag2 &quot;tag3&quot; private secret', 'tag1&amp; tag2 &quot;tag3&quot; private secret',
$this->linkDb['0']['tags'] $this->bookmarkService->get(0)->getTagsString()
); );
$this->assertEquals( $this->assertEquals(
'tag1&amp; tag2 &quot;tag3&quot; public hello world', 'tag1&amp; tag2 &quot;tag3&quot; public hello world',
$this->linkDb['1']['tags'] $this->bookmarkService->get(1)->getTagsString()
); );
} }
@ -613,23 +572,14 @@ public function testImportSameDate()
$files = file2array('same_date.htm'); $files = file2array('same_date.htm');
$this->assertStringMatchesFormat( $this->assertStringMatchesFormat(
'File same_date.htm (453 bytes) was successfully processed in %d seconds:' 'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
.' 3 links imported, 0 links overwritten, 0 links skipped.', .' 3 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history) NetscapeBookmarkUtils::import(array(), $files, $this->bookmarkService, $this->conf, $this->history)
);
$this->assertEquals(3, count($this->linkDb));
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
0,
$this->linkDb[0]['id']
);
$this->assertEquals(
1,
$this->linkDb[1]['id']
);
$this->assertEquals(
2,
$this->linkDb[2]['id']
); );
$this->assertEquals(3, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertEquals(0, $this->bookmarkService->get(0)->getId());
$this->assertEquals(1, $this->bookmarkService->get(1)->getId());
$this->assertEquals(2, $this->bookmarkService->get(2)->getId());
} }
public function testImportCreateUpdateHistory() public function testImportCreateUpdateHistory()
@ -639,14 +589,14 @@ public function testImportCreateUpdateHistory()
'overwrite' => 'true', 'overwrite' => 'true',
]; ];
$files = file2array('netscape_basic.htm'); $files = file2array('netscape_basic.htm');
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history);
$history = $this->history->getHistory(); $history = $this->history->getHistory();
$this->assertEquals(1, count($history)); $this->assertEquals(1, count($history));
$this->assertEquals(History::IMPORT, $history[0]['event']); $this->assertEquals(History::IMPORT, $history[0]['event']);
$this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']); $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
// re-import as private, enable overwriting // re-import as private, enable overwriting
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history);
$history = $this->history->getHistory(); $history = $this->history->getHistory();
$this->assertEquals(2, count($history)); $this->assertEquals(2, count($history));
$this->assertEquals(History::IMPORT, $history[0]['event']); $this->assertEquals(History::IMPORT, $history[0]['event']);

View file

@ -24,7 +24,7 @@ public function setUp()
} }
/** /**
* Test render_linklist hook on external links. * Test render_linklist hook on external bookmarks.
*/ */
public function testArchiveorgLinklistOnExternalLinks() public function testArchiveorgLinklistOnExternalLinks()
{ {
@ -54,7 +54,7 @@ public function testArchiveorgLinklistOnExternalLinks()
} }
/** /**
* Test render_linklist hook on internal links. * Test render_linklist hook on internal bookmarks.
*/ */
public function testArchiveorgLinklistOnInternalLinks() public function testArchiveorgLinklistOnInternalLinks()
{ {

View file

@ -2,7 +2,7 @@
namespace Shaarli\Plugin\Isso; namespace Shaarli\Plugin\Isso;
use DateTime; use DateTime;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
@ -60,7 +60,7 @@ public function testIssoDisplayed()
array( array(
'id' => 12, 'id' => 12,
'url' => $str, 'url' => $str,
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date), 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date),
) )
) )
); );
@ -85,7 +85,7 @@ public function testIssoDisplayed()
} }
/** /**
* Test isso plugin when multiple links are displayed (shouldn't be displayed). * Test isso plugin when multiple bookmarks are displayed (shouldn't be displayed).
*/ */
public function testIssoMultipleLinks() public function testIssoMultipleLinks()
{ {
@ -102,13 +102,13 @@ public function testIssoMultipleLinks()
'id' => 12, 'id' => 12,
'url' => $str, 'url' => $str,
'shorturl' => $short1 = 'abcd', 'shorturl' => $short1 = 'abcd',
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date1), 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date1),
), ),
array( array(
'id' => 13, 'id' => 13,
'url' => $str . '2', 'url' => $str . '2',
'shorturl' => $short2 = 'efgh', 'shorturl' => $short2 = 'efgh',
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date2), 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date2),
), ),
) )
); );
@ -136,7 +136,7 @@ public function testIssoNotDisplayedWhenSearch()
'id' => 12, 'id' => 12,
'url' => $str, 'url' => $str,
'shorturl' => $short1 = 'abcd', 'shorturl' => $short1 = 'abcd',
'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date), 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date),
) )
), ),
'search_term' => $str 'search_term' => $str

View file

@ -1,316 +0,0 @@
<?php
namespace Shaarli\Plugin\Markdown;
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager;
/**
* PluginMarkdownTest.php
*/
require_once 'application/bookmark/LinkUtils.php';
require_once 'application/Utils.php';
require_once 'plugins/markdown/markdown.php';
/**
* Class PluginMarkdownTest
* Unit test for the Markdown plugin
*/
class PluginMarkdownTest extends \PHPUnit\Framework\TestCase
{
/**
* @var ConfigManager instance.
*/
protected $conf;
/**
* Reset plugin path
*/
public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
$this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('security.allowed_protocols', ['ftp', 'magnet']);
}
/**
* Test render_linklist hook.
* Only check that there is basic markdown rendering.
*/
public function testMarkdownLinklist()
{
$markdown = '# My title' . PHP_EOL . 'Very interesting content.';
$data = array(
'links' => array(
0 => array(
'description' => $markdown,
),
),
);
$data = hook_markdown_render_linklist($data, $this->conf);
$this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
$this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
$this->assertEquals($markdown, $data['links'][0]['description_src']);
}
/**
* Test render_feed hook.
*/
public function testMarkdownFeed()
{
$markdown = '# My title' . PHP_EOL . 'Very interesting content.';
$markdown .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
$data = array(
'links' => array(
0 => array(
'description' => $markdown,
),
),
);
$data = hook_markdown_render_feed($data, $this->conf);
$this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
$this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
$this->assertStringEndsWith(
'&#8212; <a href="http://domain.tld/?0oc_VQ">Permalien</a></p></div>',
$data['links'][0]['description']
);
}
/**
* Test render_daily hook.
* Only check that there is basic markdown rendering.
*/
public function testMarkdownDaily()
{
$markdown = '# My title' . PHP_EOL . 'Very interesting content.';
$data = array(
// Columns data
'linksToDisplay' => array(
// nth link
0 => array(
'formatedDescription' => $markdown,
),
),
);
$data = hook_markdown_render_daily($data, $this->conf);
$this->assertNotFalse(strpos($data['linksToDisplay'][0]['formatedDescription'], '<h1>'));
$this->assertNotFalse(strpos($data['linksToDisplay'][0]['formatedDescription'], '<p>'));
}
/**
* Test reverse_text2clickable().
*/
public function testReverseText2clickable()
{
$text = 'stuff http://hello.there/is=someone#here otherstuff';
$clickableText = text2clickable($text);
$reversedText = reverse_text2clickable($clickableText);
$this->assertEquals($text, $reversedText);
}
/**
* Test reverse_text2clickable().
*/
public function testReverseText2clickableHashtags()
{
$text = file_get_contents('tests/plugins/resources/hashtags.raw');
$md = file_get_contents('tests/plugins/resources/hashtags.md');
$clickableText = hashtag_autolink($text);
$reversedText = reverse_text2clickable($clickableText);
$this->assertEquals($md, $reversedText);
}
/**
* Test reverse_nl2br().
*/
public function testReverseNl2br()
{
$text = 'stuff' . PHP_EOL . 'otherstuff';
$processedText = nl2br($text);
$reversedText = reverse_nl2br($processedText);
$this->assertEquals($text, $reversedText);
}
/**
* Test reverse_space2nbsp().
*/
public function testReverseSpace2nbsp()
{
$text = ' stuff' . PHP_EOL . ' otherstuff and another';
$processedText = space2nbsp($text);
$reversedText = reverse_space2nbsp($processedText);
$this->assertEquals($text, $reversedText);
}
public function testReverseFeedPermalink()
{
$text = 'Description... ';
$text .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
$expected = 'Description... &#8212; [Permalien](http://domain.tld/?0oc_VQ)';
$processedText = reverse_feed_permalink($text);
$this->assertEquals($expected, $processedText);
}
public function testReverseFeedDirectLink()
{
$text = 'Description... ';
$text .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Direct link">Direct link</a>';
$expected = 'Description... &#8212; [Direct link](http://domain.tld/?0oc_VQ)';
$processedText = reverse_feed_permalink($text);
$this->assertEquals($expected, $processedText);
}
public function testReverseLastFeedPermalink()
{
$text = 'Description... ';
$text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
$expected = $text;
$text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
$expected .= '<br>&#8212; [Permalien](http://domain.tld/?0oc_VQ)';
$processedText = reverse_feed_permalink($text);
$this->assertEquals($expected, $processedText);
}
public function testReverseNoFeedPermalink()
{
$text = 'Hello! Where are you from?';
$expected = $text;
$processedText = reverse_feed_permalink($text);
$this->assertEquals($expected, $processedText);
}
/**
* Test sanitize_html().
*/
public function testSanitizeHtml()
{
$input = '< script src="js.js"/>';
$input .= '< script attr>alert(\'xss\');</script>';
$input .= '<style> * { display: none }</style>';
$output = escape($input);
$input .= '<a href="#" onmouseHover="alert(\'xss\');" attr="tt">link</a>';
$output .= '<a href="#" attr="tt">link</a>';
$input .= '<a href="#" onmouseHover=alert(\'xss\'); attr="tt">link</a>';
$output .= '<a href="#" attr="tt">link</a>';
$this->assertEquals($output, sanitize_html($input));
// Do not touch escaped HTML.
$input = escape($input);
$this->assertEquals($input, sanitize_html($input));
}
/**
* Test the no markdown tag.
*/
public function testNoMarkdownTag()
{
$str = 'All _work_ and `no play` makes Jack a *dull* boy.';
$data = array(
'links' => array(array(
'description' => $str,
'tags' => NO_MD_TAG,
'taglist' => array(NO_MD_TAG),
))
);
$processed = hook_markdown_render_linklist($data, $this->conf);
$this->assertEquals($str, $processed['links'][0]['description']);
$processed = hook_markdown_render_feed($data, $this->conf);
$this->assertEquals($str, $processed['links'][0]['description']);
$data = array(
// Columns data
'linksToDisplay' => array(
// nth link
0 => array(
'formatedDescription' => $str,
'tags' => NO_MD_TAG,
'taglist' => array(),
),
),
);
$data = hook_markdown_render_daily($data, $this->conf);
$this->assertEquals($str, $data['linksToDisplay'][0]['formatedDescription']);
}
/**
* Test that a close value to nomarkdown is not understand as nomarkdown (previous value `.nomarkdown`).
*/
public function testNoMarkdownNotExcactlyMatching()
{
$str = 'All _work_ and `no play` makes Jack a *dull* boy.';
$data = array(
'links' => array(array(
'description' => $str,
'tags' => '.' . NO_MD_TAG,
'taglist' => array('.'. NO_MD_TAG),
))
);
$data = hook_markdown_render_feed($data, $this->conf);
$this->assertContains('<em>', $data['links'][0]['description']);
}
/**
* Make sure that the generated HTML match the reference HTML file.
*/
public function testMarkdownGlobalProcessDescription()
{
$md = file_get_contents('tests/plugins/resources/markdown.md');
$md = format_description($md);
$html = file_get_contents('tests/plugins/resources/markdown.html');
$data = process_markdown(
$md,
$this->conf->get('security.markdown_escape', true),
$this->conf->get('security.allowed_protocols')
);
$this->assertEquals($html, $data . PHP_EOL);
}
/**
* Make sure that the HTML tags are escaped.
*/
public function testMarkdownWithHtmlEscape()
{
$md = '**strong** <strong>strong</strong>';
$html = '<div class="markdown"><p><strong>strong</strong> &lt;strong&gt;strong&lt;/strong&gt;</p></div>';
$data = array(
'links' => array(
0 => array(
'description' => $md,
),
),
);
$data = hook_markdown_render_linklist($data, $this->conf);
$this->assertEquals($html, $data['links'][0]['description']);
}
/**
* Make sure that the HTML tags aren't escaped with the setting set to false.
*/
public function testMarkdownWithHtmlNoEscape()
{
$this->conf->set('security.markdown_escape', false);
$md = '**strong** <strong>strong</strong>';
$html = '<div class="markdown"><p><strong>strong</strong> <strong>strong</strong></p></div>';
$data = array(
'links' => array(
0 => array(
'description' => $md,
),
),
);
$data = hook_markdown_render_linklist($data, $this->conf);
$this->assertEquals($html, $data['links'][0]['description']);
}
}

View file

@ -4,6 +4,7 @@
use Exception; use Exception;
use ReflectionClass; use ReflectionClass;
use ReflectionMethod; use ReflectionMethod;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
@ -17,13 +18,13 @@ class DummyUpdater extends Updater
* Object constructor. * Object constructor.
* *
* @param array $doneUpdates Updates which are already done. * @param array $doneUpdates Updates which are already done.
* @param LinkDB $linkDB LinkDB instance. * @param BookmarkFileService $bookmarkService LinkDB instance.
* @param ConfigManager $conf Configuration Manager instance. * @param ConfigManager $conf Configuration Manager instance.
* @param boolean $isLoggedIn True if the user is logged in. * @param boolean $isLoggedIn True if the user is logged in.
*/ */
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) public function __construct($doneUpdates, $bookmarkService, $conf, $isLoggedIn)
{ {
parent::__construct($doneUpdates, $linkDB, $conf, $isLoggedIn); parent::__construct($doneUpdates, $bookmarkService, $conf, $isLoggedIn);
// Retrieve all update methods. // Retrieve all update methods.
// For unit test, only retrieve final methods, // For unit test, only retrieve final methods,

View file

@ -1,15 +1,9 @@
<?php <?php
namespace Shaarli\Updater; namespace Shaarli\Updater;
use DateTime;
use Exception; use Exception;
use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigJson;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Config\ConfigPhp;
use Shaarli\Thumbnailer;
require_once 'application/updater/UpdaterUtils.php';
require_once 'tests/updater/DummyUpdater.php'; require_once 'tests/updater/DummyUpdater.php';
require_once 'tests/utils/ReferenceLinkDB.php'; require_once 'tests/utils/ReferenceLinkDB.php';
require_once 'inc/rain.tpl.class.php'; require_once 'inc/rain.tpl.class.php';
@ -45,14 +39,14 @@ public function setUp()
} }
/** /**
* Test read_updates_file with an empty/missing file. * Test UpdaterUtils::read_updates_file with an empty/missing file.
*/ */
public function testReadEmptyUpdatesFile() public function testReadEmptyUpdatesFile()
{ {
$this->assertEquals(array(), read_updates_file('')); $this->assertEquals(array(), UpdaterUtils::read_updates_file(''));
$updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
touch($updatesFile); touch($updatesFile);
$this->assertEquals(array(), read_updates_file($updatesFile)); $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile));
unlink($updatesFile); unlink($updatesFile);
} }
@ -64,31 +58,31 @@ public function testReadWriteUpdatesFile()
$updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
$updatesMethods = array('m1', 'm2', 'm3'); $updatesMethods = array('m1', 'm2', 'm3');
write_updates_file($updatesFile, $updatesMethods); UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
$readMethods = read_updates_file($updatesFile); $readMethods = UpdaterUtils::read_updates_file($updatesFile);
$this->assertEquals($readMethods, $updatesMethods); $this->assertEquals($readMethods, $updatesMethods);
// Update // Update
$updatesMethods[] = 'm4'; $updatesMethods[] = 'm4';
write_updates_file($updatesFile, $updatesMethods); UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
$readMethods = read_updates_file($updatesFile); $readMethods = UpdaterUtils::read_updates_file($updatesFile);
$this->assertEquals($readMethods, $updatesMethods); $this->assertEquals($readMethods, $updatesMethods);
unlink($updatesFile); unlink($updatesFile);
} }
/** /**
* Test errors in write_updates_file(): empty updates file. * Test errors in UpdaterUtils::write_updates_file(): empty updates file.
* *
* @expectedException Exception * @expectedException Exception
* @expectedExceptionMessageRegExp /Updates file path is not set(.*)/ * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/
*/ */
public function testWriteEmptyUpdatesFile() public function testWriteEmptyUpdatesFile()
{ {
write_updates_file('', array('test')); UpdaterUtils::write_updates_file('', array('test'));
} }
/** /**
* Test errors in write_updates_file(): not writable updates file. * Test errors in UpdaterUtils::write_updates_file(): not writable updates file.
* *
* @expectedException Exception * @expectedException Exception
* @expectedExceptionMessageRegExp /Unable to write(.*)/ * @expectedExceptionMessageRegExp /Unable to write(.*)/
@ -99,7 +93,7 @@ public function testWriteUpdatesFileNotWritable()
touch($updatesFile); touch($updatesFile);
chmod($updatesFile, 0444); chmod($updatesFile, 0444);
try { try {
@write_updates_file($updatesFile, array('test')); @UpdaterUtils::write_updates_file($updatesFile, array('test'));
} catch (Exception $e) { } catch (Exception $e) {
unlink($updatesFile); unlink($updatesFile);
throw $e; throw $e;
@ -173,660 +167,4 @@ public function testUpdateFailed()
$updater = new DummyUpdater($updates, array(), $this->conf, true); $updater = new DummyUpdater($updates, array(), $this->conf, true);
$updater->update(); $updater->update();
} }
/**
* Test update mergeDeprecatedConfig:
* 1. init a config file.
* 2. init a options.php file with update value.
* 3. merge.
* 4. check updated value in config file.
*/
public function testUpdateMergeDeprecatedConfig()
{
$this->conf->setConfigFile('tests/utils/config/configPhp');
$this->conf->reset();
$optionsFile = 'tests/updater/options.php';
$options = '<?php
$GLOBALS[\'privateLinkByDefault\'] = true;';
file_put_contents($optionsFile, $options);
// tmp config file.
$this->conf->setConfigFile('tests/updater/config');
// merge configs
$updater = new Updater(array(), array(), $this->conf, true);
// This writes a new config file in tests/updater/config.php
$updater->updateMethodMergeDeprecatedConfigFile();
// make sure updated field is changed
$this->conf->reload();
$this->assertTrue($this->conf->get('privacy.default_private_links'));
$this->assertFalse(is_file($optionsFile));
// Delete the generated file.
unlink($this->conf->getConfigFileExt());
}
/**
* Test mergeDeprecatedConfig in without options file.
*/
public function testMergeDeprecatedConfigNoFile()
{
$updater = new Updater(array(), array(), $this->conf, true);
$updater->updateMethodMergeDeprecatedConfigFile();
$this->assertEquals('root', $this->conf->get('credentials.login'));
}
/**
* Test renameDashTags update method.
*/
public function testRenameDashTags()
{
$refDB = new \ReferenceLinkDB();
$refDB->write(self::$testDatastore);
$linkDB = new LinkDB(self::$testDatastore, true, false);
$this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
$updater = new Updater(array(), $linkDB, $this->conf, true);
$updater->updateMethodRenameDashTags();
$this->assertNotEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
}
/**
* Convert old PHP config file to JSON config.
*/
public function testConfigToJson()
{
$configFile = 'tests/utils/config/configPhp';
$this->conf->setConfigFile($configFile);
$this->conf->reset();
// The ConfigIO is initialized with ConfigPhp.
$this->assertTrue($this->conf->getConfigIO() instanceof ConfigPhp);
$updater = new Updater(array(), array(), $this->conf, false);
$done = $updater->updateMethodConfigToJson();
$this->assertTrue($done);
// The ConfigIO has been updated to ConfigJson.
$this->assertTrue($this->conf->getConfigIO() instanceof ConfigJson);
$this->assertTrue(file_exists($this->conf->getConfigFileExt()));
// Check JSON config data.
$this->conf->reload();
$this->assertEquals('root', $this->conf->get('credentials.login'));
$this->assertEquals('lala', $this->conf->get('redirector.url'));
$this->assertEquals('data/datastore.php', $this->conf->get('resource.datastore'));
$this->assertEquals('1', $this->conf->get('plugins.WALLABAG_VERSION'));
rename($configFile . '.save.php', $configFile . '.php');
unlink($this->conf->getConfigFileExt());
}
/**
* Launch config conversion update with an existing JSON file => nothing to do.
*/
public function testConfigToJsonNothingToDo()
{
$filetime = filemtime($this->conf->getConfigFileExt());
$updater = new Updater(array(), array(), $this->conf, false);
$done = $updater->updateMethodConfigToJson();
$this->assertTrue($done);
$expected = filemtime($this->conf->getConfigFileExt());
$this->assertEquals($expected, $filetime);
}
/**
* Test escapeUnescapedConfig with valid data.
*/
public function testEscapeConfig()
{
$sandbox = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandbox . '.json.php');
$this->conf = new ConfigManager($sandbox);
$title = '<script>alert("title");</script>';
$headerLink = '<script>alert("header_link");</script>';
$this->conf->set('general.title', $title);
$this->conf->set('general.header_link', $headerLink);
$updater = new Updater(array(), array(), $this->conf, true);
$done = $updater->updateMethodEscapeUnescapedConfig();
$this->assertTrue($done);
$this->conf->reload();
$this->assertEquals(escape($title), $this->conf->get('general.title'));
$this->assertEquals(escape($headerLink), $this->conf->get('general.header_link'));
unlink($sandbox . '.json.php');
}
/**
* Test updateMethodApiSettings(): create default settings for the API (enabled + secret).
*/
public function testUpdateApiSettings()
{
$confFile = 'sandbox/config';
copy(self::$configFile .'.json.php', $confFile .'.json.php');
$conf = new ConfigManager($confFile);
$updater = new Updater(array(), array(), $conf, true);
$this->assertFalse($conf->exists('api.enabled'));
$this->assertFalse($conf->exists('api.secret'));
$updater->updateMethodApiSettings();
$conf->reload();
$this->assertTrue($conf->get('api.enabled'));
$this->assertTrue($conf->exists('api.secret'));
unlink($confFile .'.json.php');
}
/**
* Test updateMethodApiSettings(): already set, do nothing.
*/
public function testUpdateApiSettingsNothingToDo()
{
$confFile = 'sandbox/config';
copy(self::$configFile .'.json.php', $confFile .'.json.php');
$conf = new ConfigManager($confFile);
$conf->set('api.enabled', false);
$conf->set('api.secret', '');
$updater = new Updater(array(), array(), $conf, true);
$updater->updateMethodApiSettings();
$this->assertFalse($conf->get('api.enabled'));
$this->assertEmpty($conf->get('api.secret'));
unlink($confFile .'.json.php');
}
/**
* Test updateMethodDatastoreIds().
*/
public function testDatastoreIds()
{
$links = array(
'20121206_182539' => array(
'linkdate' => '20121206_182539',
'title' => 'Geek and Poke',
'url' => 'http://geek-and-poke.com/',
'description' => 'desc',
'tags' => 'dev cartoon tag1 tag2 tag3 tag4 ',
'updated' => '20121206_190301',
'private' => false,
),
'20121206_172539' => array(
'linkdate' => '20121206_172539',
'title' => 'UserFriendly - Samba',
'url' => 'http://ars.userfriendly.org/cartoons/?id=20010306',
'description' => '',
'tags' => 'samba cartoon web',
'private' => false,
),
'20121206_142300' => array(
'linkdate' => '20121206_142300',
'title' => 'UserFriendly - Web Designer',
'url' => 'http://ars.userfriendly.org/cartoons/?id=20121206',
'description' => 'Naming conventions... #private',
'tags' => 'samba cartoon web',
'private' => true,
),
);
$refDB = new \ReferenceLinkDB();
$refDB->setLinks($links);
$refDB->write(self::$testDatastore);
$linkDB = new LinkDB(self::$testDatastore, true, false);
$checksum = hash_file('sha1', self::$testDatastore);
$this->conf->set('resource.data_dir', 'sandbox');
$this->conf->set('resource.datastore', self::$testDatastore);
$updater = new Updater(array(), $linkDB, $this->conf, true);
$this->assertTrue($updater->updateMethodDatastoreIds());
$linkDB = new LinkDB(self::$testDatastore, true, false);
$backup = glob($this->conf->get('resource.data_dir') . '/datastore.'. date('YmdH') .'*.php');
$backup = $backup[0];
$this->assertFileExists($backup);
$this->assertEquals($checksum, hash_file('sha1', $backup));
unlink($backup);
$this->assertEquals(3, count($linkDB));
$this->assertTrue(isset($linkDB[0]));
$this->assertFalse(isset($linkDB[0]['linkdate']));
$this->assertEquals(0, $linkDB[0]['id']);
$this->assertEquals('UserFriendly - Web Designer', $linkDB[0]['title']);
$this->assertEquals('http://ars.userfriendly.org/cartoons/?id=20121206', $linkDB[0]['url']);
$this->assertEquals('Naming conventions... #private', $linkDB[0]['description']);
$this->assertEquals('samba cartoon web', $linkDB[0]['tags']);
$this->assertTrue($linkDB[0]['private']);
$this->assertEquals(
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'),
$linkDB[0]['created']
);
$this->assertTrue(isset($linkDB[1]));
$this->assertFalse(isset($linkDB[1]['linkdate']));
$this->assertEquals(1, $linkDB[1]['id']);
$this->assertEquals('UserFriendly - Samba', $linkDB[1]['title']);
$this->assertEquals(
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'),
$linkDB[1]['created']
);
$this->assertTrue(isset($linkDB[2]));
$this->assertFalse(isset($linkDB[2]['linkdate']));
$this->assertEquals(2, $linkDB[2]['id']);
$this->assertEquals('Geek and Poke', $linkDB[2]['title']);
$this->assertEquals(
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'),
$linkDB[2]['created']
);
$this->assertEquals(
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_190301'),
$linkDB[2]['updated']
);
}
/**
* Test updateMethodDatastoreIds() with the update already applied: nothing to do.
*/
public function testDatastoreIdsNothingToDo()
{
$refDB = new \ReferenceLinkDB();
$refDB->write(self::$testDatastore);
$linkDB = new LinkDB(self::$testDatastore, true, false);
$this->conf->set('resource.data_dir', 'sandbox');
$this->conf->set('resource.datastore', self::$testDatastore);
$checksum = hash_file('sha1', self::$testDatastore);
$updater = new Updater(array(), $linkDB, $this->conf, true);
$this->assertTrue($updater->updateMethodDatastoreIds());
$this->assertEquals($checksum, hash_file('sha1', self::$testDatastore));
}
/**
* Test defaultTheme update with default settings: nothing to do.
*/
public function testDefaultThemeWithDefaultSettings()
{
$sandbox = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandbox . '.json.php');
$this->conf = new ConfigManager($sandbox);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDefaultTheme());
$this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
$this->assertEquals('default', $this->conf->get('resource.theme'));
$this->conf = new ConfigManager($sandbox);
$this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
$this->assertEquals('default', $this->conf->get('resource.theme'));
unlink($sandbox . '.json.php');
}
/**
* Test defaultTheme update with a custom theme in a subfolder
*/
public function testDefaultThemeWithCustomTheme()
{
$theme = 'iamanartist';
$sandbox = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandbox . '.json.php');
$this->conf = new ConfigManager($sandbox);
mkdir('sandbox/'. $theme);
touch('sandbox/'. $theme .'/linklist.html');
$this->conf->set('resource.raintpl_tpl', 'sandbox/'. $theme .'/');
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDefaultTheme());
$this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
$this->assertEquals($theme, $this->conf->get('resource.theme'));
$this->conf = new ConfigManager($sandbox);
$this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
$this->assertEquals($theme, $this->conf->get('resource.theme'));
unlink($sandbox . '.json.php');
unlink('sandbox/'. $theme .'/linklist.html');
rmdir('sandbox/'. $theme);
}
/**
* Test updateMethodEscapeMarkdown with markdown plugin enabled
* => setting markdown_escape set to false.
*/
public function testEscapeMarkdownSettingToFalse()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.enabled_plugins', ['markdown']);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodEscapeMarkdown());
$this->assertFalse($this->conf->get('security.markdown_escape'));
// reload from file
$this->conf = new ConfigManager($sandboxConf);
$this->assertFalse($this->conf->get('security.markdown_escape'));
}
/**
* Test updateMethodEscapeMarkdown with markdown plugin disabled
* => setting markdown_escape set to true.
*/
public function testEscapeMarkdownSettingToTrue()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.enabled_plugins', []);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodEscapeMarkdown());
$this->assertTrue($this->conf->get('security.markdown_escape'));
// reload from file
$this->conf = new ConfigManager($sandboxConf);
$this->assertTrue($this->conf->get('security.markdown_escape'));
}
/**
* Test updateMethodEscapeMarkdown with nothing to do (setting already enabled)
*/
public function testEscapeMarkdownSettingNothingToDoEnabled()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('security.markdown_escape', true);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodEscapeMarkdown());
$this->assertTrue($this->conf->get('security.markdown_escape'));
}
/**
* Test updateMethodEscapeMarkdown with nothing to do (setting already disabled)
*/
public function testEscapeMarkdownSettingNothingToDoDisabled()
{
$this->conf->set('security.markdown_escape', false);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodEscapeMarkdown());
$this->assertFalse($this->conf->get('security.markdown_escape'));
}
/**
* Test updateMethodPiwikUrl with valid data
*/
public function testUpdatePiwikUrlValid()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$url = 'mypiwik.tld';
$this->conf->set('plugins.PIWIK_URL', $url);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodPiwikUrl());
$this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
// reload from file
$this->conf = new ConfigManager($sandboxConf);
$this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
}
/**
* Test updateMethodPiwikUrl without setting
*/
public function testUpdatePiwikUrlEmpty()
{
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodPiwikUrl());
$this->assertEmpty($this->conf->get('plugins.PIWIK_URL'));
}
/**
* Test updateMethodPiwikUrl: valid URL, nothing to do
*/
public function testUpdatePiwikUrlNothingToDo()
{
$url = 'https://mypiwik.tld';
$this->conf->set('plugins.PIWIK_URL', $url);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodPiwikUrl());
$this->assertEquals($url, $this->conf->get('plugins.PIWIK_URL'));
}
/**
* Test updateMethodAtomDefault with show_atom set to false
* => update to true.
*/
public function testUpdateMethodAtomDefault()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('feed.show_atom', false);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodAtomDefault());
$this->assertTrue($this->conf->get('feed.show_atom'));
// reload from file
$this->conf = new ConfigManager($sandboxConf);
$this->assertTrue($this->conf->get('feed.show_atom'));
}
/**
* Test updateMethodAtomDefault with show_atom not set.
* => nothing to do
*/
public function testUpdateMethodAtomDefaultNoExist()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodAtomDefault());
$this->assertTrue($this->conf->get('feed.show_atom'));
}
/**
* Test updateMethodAtomDefault with show_atom set to true.
* => nothing to do
*/
public function testUpdateMethodAtomDefaultAlreadyTrue()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('feed.show_atom', true);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodAtomDefault());
$this->assertTrue($this->conf->get('feed.show_atom'));
}
/**
* Test updateMethodDownloadSizeAndTimeoutConf, it should be set if none is already defined.
*/
public function testUpdateMethodDownloadSizeAndTimeoutConf()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
$this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
$this->assertEquals(30, $this->conf->get('general.download_timeout'));
$this->conf = new ConfigManager($sandboxConf);
$this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
$this->assertEquals(30, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodDownloadSizeAndTimeoutConf, it shouldn't be set if it is already defined.
*/
public function testUpdateMethodDownloadSizeAndTimeoutConfIgnore()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.download_max_size', 38);
$this->conf->set('general.download_timeout', 70);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
$this->assertEquals(38, $this->conf->get('general.download_max_size'));
$this->assertEquals(70, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodDownloadSizeAndTimeoutConf, only the maz size should be set here.
*/
public function testUpdateMethodDownloadSizeAndTimeoutConfOnlySize()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.download_max_size', 38);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
$this->assertEquals(38, $this->conf->get('general.download_max_size'));
$this->assertEquals(30, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodDownloadSizeAndTimeoutConf, only the time out should be set here.
*/
public function testUpdateMethodDownloadSizeAndTimeoutConfOnlyTimeout()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$this->conf->set('general.download_timeout', 3);
$updater = new Updater([], [], $this->conf, true);
$this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
$this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
$this->assertEquals(3, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodWebThumbnailer with thumbnails enabled.
*/
public function testUpdateMethodWebThumbnailerEnabled()
{
$this->conf->remove('thumbnails');
$this->conf->set('thumbnail.enable_thumbnails', true);
$updater = new Updater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
$this->assertEquals(125, $this->conf->get('thumbnails.width'));
$this->assertEquals(90, $this->conf->get('thumbnails.height'));
$this->assertContains('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
}
/**
* Test updateMethodWebThumbnailer with thumbnails disabled.
*/
public function testUpdateMethodWebThumbnailerDisabled()
{
if (isset($_SESSION['warnings'])) {
unset($_SESSION['warnings']);
}
$this->conf->remove('thumbnails');
$this->conf->set('thumbnail.enable_thumbnails', false);
$updater = new Updater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(Thumbnailer::MODE_NONE, $this->conf->get('thumbnails.mode'));
$this->assertEquals(125, $this->conf->get('thumbnails.width'));
$this->assertEquals(90, $this->conf->get('thumbnails.height'));
$this->assertTrue(empty($_SESSION['warnings']));
}
/**
* Test updateMethodWebThumbnailer with thumbnails disabled.
*/
public function testUpdateMethodWebThumbnailerNothingToDo()
{
if (isset($_SESSION['warnings'])) {
unset($_SESSION['warnings']);
}
$updater = new Updater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(Thumbnailer::MODE_COMMON, $this->conf->get('thumbnails.mode'));
$this->assertEquals(90, $this->conf->get('thumbnails.width'));
$this->assertEquals(53, $this->conf->get('thumbnails.height'));
$this->assertTrue(empty($_SESSION['warnings']));
}
/**
* Test updateMethodSetSticky().
*/
public function testUpdateStickyValid()
{
$blank = [
'id' => 1,
'url' => 'z',
'title' => '',
'description' => '',
'tags' => '',
'created' => new DateTime(),
];
$links = [
1 => ['id' => 1] + $blank,
2 => ['id' => 2] + $blank,
];
$refDB = new \ReferenceLinkDB();
$refDB->setLinks($links);
$refDB->write(self::$testDatastore);
$linkDB = new LinkDB(self::$testDatastore, true, false);
$updater = new Updater(array(), $linkDB, $this->conf, true);
$this->assertTrue($updater->updateMethodSetSticky());
$linkDB = new LinkDB(self::$testDatastore, true, false);
foreach ($linkDB as $link) {
$this->assertFalse($link['sticky']);
}
}
/**
* Test updateMethodSetSticky().
*/
public function testUpdateStickyNothingToDo()
{
$blank = [
'id' => 1,
'url' => 'z',
'title' => '',
'description' => '',
'tags' => '',
'created' => new DateTime(),
];
$links = [
1 => ['id' => 1, 'sticky' => true] + $blank,
2 => ['id' => 2] + $blank,
];
$refDB = new \ReferenceLinkDB();
$refDB->setLinks($links);
$refDB->write(self::$testDatastore);
$linkDB = new LinkDB(self::$testDatastore, true, false);
$updater = new Updater(array(), $linkDB, $this->conf, true);
$this->assertTrue($updater->updateMethodSetSticky());
$linkDB = new LinkDB(self::$testDatastore, true, false);
$this->assertTrue($linkDB[1]['sticky']);
}
/**
* Test updateMethodRemoveRedirector().
*/
public function testUpdateRemoveRedirector()
{
$sandboxConf = 'sandbox/config';
copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
$this->conf = new ConfigManager($sandboxConf);
$updater = new Updater([], null, $this->conf, true);
$this->assertTrue($updater->updateMethodRemoveRedirector());
$this->assertFalse($this->conf->exists('redirector'));
$this->conf = new ConfigManager($sandboxConf);
$this->assertFalse($this->conf->exists('redirector'));
}
} }

View file

@ -0,0 +1,18 @@
<?php
use Shaarli\Bookmark\BookmarkArray;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\BookmarkIO;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
class FakeBookmarkService extends BookmarkFileService
{
public function getBookmarks()
{
return $this->bookmarks;
}
}

View file

@ -76,7 +76,7 @@ public function write($filename)
} }
/** /**
* Returns the number of links in the reference data * Returns the number of bookmarks in the reference data
*/ */
public function count() public function count()
{ {

View file

@ -1,30 +1,39 @@
<?php <?php
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkArray;
/** /**
* Populates a reference datastore to test LinkDB * Populates a reference datastore to test Bookmark
*/ */
class ReferenceLinkDB class ReferenceLinkDB
{ {
public static $NB_LINKS_TOTAL = 11; public static $NB_LINKS_TOTAL = 11;
private $_links = array(); private $bookmarks = array();
private $_publicCount = 0; private $_publicCount = 0;
private $_privateCount = 0; private $_privateCount = 0;
private $isLegacy;
/** /**
* Populates the test DB with reference data * Populates the test DB with reference data
*
* @param bool $isLegacy Use links as array instead of Bookmark object
*/ */
public function __construct() public function __construct($isLegacy = false)
{ {
$this->isLegacy = $isLegacy;
if (! $this->isLegacy) {
$this->bookmarks = new BookmarkArray();
}
$this->addLink( $this->addLink(
11, 11,
'Pined older', 'Pined older',
'?PCRizQ', '?PCRizQ',
'This is an older pinned link', 'This is an older pinned link',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20100309_101010'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100309_101010'),
'', '',
null, null,
'PCRizQ', 'PCRizQ',
@ -37,7 +46,7 @@ public function __construct()
'?0gCTjQ', '?0gCTjQ',
'This is a pinned link', 'This is a pinned link',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121207_152312'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121207_152312'),
'', '',
null, null,
'0gCTjQ', '0gCTjQ',
@ -50,7 +59,7 @@ public function __construct()
'?WDWyig', '?WDWyig',
'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag', 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'),
'sTuff', 'sTuff',
null, null,
'WDWyig' 'WDWyig'
@ -60,9 +69,9 @@ public function __construct()
42, 42,
'Note: I have a big ID but an old date', 'Note: I have a big ID but an old date',
'?WDWyig', '?WDWyig',
'Used to test links reordering.', 'Used to test bookmarks reordering.',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20100310_101010'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
'ut' 'ut'
); );
@ -72,7 +81,7 @@ public function __construct()
'http://www.php-fig.org/psr/psr-2/', 'http://www.php-fig.org/psr/psr-2/',
'This guide extends and expands on PSR-1, the basic coding standard.', 'This guide extends and expands on PSR-1, the basic coding standard.',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_152312'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'),
'' ''
); );
@ -82,9 +91,9 @@ public function __construct()
'https://static.fsf.org/nosvn/faif-2.0.pdf', 'https://static.fsf.org/nosvn/faif-2.0.pdf',
'Richard Stallman and the Free Software Revolution. Read this. #hashtag', 'Richard Stallman and the Free Software Revolution. Read this. #hashtag',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'),
'free gnu software stallman -exclude stuff hashtag', 'free gnu software stallman -exclude stuff hashtag',
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033') DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160803_093033')
); );
$this->addLink( $this->addLink(
@ -93,9 +102,9 @@ public function __construct()
'http://mediagoblin.org/', 'http://mediagoblin.org/',
'A free software media publishing platform #hashtagOther', 'A free software media publishing platform #hashtagOther',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
'gnu media web .hidden hashtag', 'gnu media web .hidden hashtag',
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130615_184230'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
'IuWvgA' 'IuWvgA'
); );
@ -105,7 +114,7 @@ public function __construct()
'https://dvcs.w3.org/hg/markup-validator/summary', 'https://dvcs.w3.org/hg/markup-validator/summary',
'Mercurial repository for the W3C Validator #private', 'Mercurial repository for the W3C Validator #private',
1, 1,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20141125_084734'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20141125_084734'),
'css html w3c web Mercurial' 'css html w3c web Mercurial'
); );
@ -115,7 +124,7 @@ public function __construct()
'http://ars.userfriendly.org/cartoons/?id=20121206', 'http://ars.userfriendly.org/cartoons/?id=20121206',
'Naming conventions... #private', 'Naming conventions... #private',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_142300'),
'dev cartoon web' 'dev cartoon web'
); );
@ -125,7 +134,7 @@ public function __construct()
'http://ars.userfriendly.org/cartoons/?id=20010306', 'http://ars.userfriendly.org/cartoons/?id=20010306',
'Tropical printing', 'Tropical printing',
0, 0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_172539'),
'samba cartoon web' 'samba cartoon web'
); );
@ -135,7 +144,7 @@ public function __construct()
'http://geek-and-poke.com/', 'http://geek-and-poke.com/',
'', '',
1, 1,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'), DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_182539'),
'dev cartoon tag1 tag2 tag3 tag4 ' 'dev cartoon tag1 tag2 tag3 tag4 '
); );
} }
@ -164,10 +173,15 @@ protected function addLink(
'tags' => $tags, 'tags' => $tags,
'created' => $date, 'created' => $date,
'updated' => $updated, 'updated' => $updated,
'shorturl' => $shorturl ? $shorturl : smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id), 'shorturl' => $shorturl ? $shorturl : smallHash($date->format(Bookmark::LINK_DATE_FORMAT) . $id),
'sticky' => $pinned 'sticky' => $pinned
); );
$this->_links[$id] = $link; if (! $this->isLegacy) {
$bookmark = new Bookmark();
$this->bookmarks[$id] = $bookmark->fromArray($link);
} else {
$this->bookmarks[$id] = $link;
}
if ($private) { if ($private) {
$this->_privateCount++; $this->_privateCount++;
@ -184,27 +198,27 @@ public function write($filename)
$this->reorder(); $this->reorder();
file_put_contents( file_put_contents(
$filename, $filename,
'<?php /* '.base64_encode(gzdeflate(serialize($this->_links))).' */ ?>' '<?php /* '.base64_encode(gzdeflate(serialize($this->bookmarks))).' */ ?>'
); );
} }
/** /**
* Reorder links by creation date (newest first). * Reorder links by creation date (newest first).
* *
* Also update the urls and ids mapping arrays.
*
* @param string $order ASC|DESC * @param string $order ASC|DESC
*/ */
public function reorder($order = 'DESC') public function reorder($order = 'DESC')
{ {
if (! $this->isLegacy) {
$this->bookmarks->reorder($order);
} else {
$order = $order === 'ASC' ? -1 : 1;
// backward compatibility: ignore reorder if the the `created` field doesn't exist // backward compatibility: ignore reorder if the the `created` field doesn't exist
if (! isset(array_values($this->_links)[0]['created'])) { if (! isset(array_values($this->bookmarks)[0]['created'])) {
return; return;
} }
$order = $order === 'ASC' ? -1 : 1; usort($this->bookmarks, function ($a, $b) use ($order) {
// Reorder array by dates.
usort($this->_links, function ($a, $b) use ($order) {
if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) { if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
return $a['sticky'] ? -1 : 1; return $a['sticky'] ? -1 : 1;
} }
@ -212,9 +226,10 @@ public function reorder($order = 'DESC')
return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
}); });
} }
}
/** /**
* Returns the number of links in the reference data * Returns the number of bookmarks in the reference data
*/ */
public function countLinks() public function countLinks()
{ {
@ -222,7 +237,7 @@ public function countLinks()
} }
/** /**
* Returns the number of public links in the reference data * Returns the number of public bookmarks in the reference data
*/ */
public function countPublicLinks() public function countPublicLinks()
{ {
@ -230,7 +245,7 @@ public function countPublicLinks()
} }
/** /**
* Returns the number of private links in the reference data * Returns the number of private bookmarks in the reference data
*/ */
public function countPrivateLinks() public function countPrivateLinks()
{ {
@ -238,32 +253,38 @@ public function countPrivateLinks()
} }
/** /**
* Returns the number of links without tag * Returns the number of bookmarks without tag
*/ */
public function countUntaggedLinks() public function countUntaggedLinks()
{ {
$cpt = 0; $cpt = 0;
foreach ($this->_links as $link) { foreach ($this->bookmarks as $link) {
if (! $this->isLegacy) {
if (empty($link->getTags())) {
++$cpt;
}
} else {
if (empty($link['tags'])) { if (empty($link['tags'])) {
++$cpt; ++$cpt;
} }
} }
}
return $cpt; return $cpt;
} }
public function getLinks() public function getLinks()
{ {
$this->reorder(); $this->reorder();
return $this->_links; return $this->bookmarks;
} }
/** /**
* Setter to override link creation. * Setter to override link creation.
* *
* @param array $links List of links. * @param array $links List of bookmarks.
*/ */
public function setLinks($links) public function setLinks($links)
{ {
$this->_links = $links; $this->bookmarks = $links;
} }
} }

View file

@ -41,12 +41,12 @@
"foo": "bar" "foo": "bar"
}, },
"resource": { "resource": {
"datastore": "tests\/utils\/config\/datastore.php", "datastore": "sandbox/datastore.php",
"data_dir": "sandbox\/", "data_dir": "sandbox\/",
"raintpl_tpl": "tpl\/", "raintpl_tpl": "tpl\/",
"config": "data\/config.php", "config": "data\/config.php",
"ban_file": "data\/ipbans.php", "ban_file": "data\/ipbans.php",
"updates": "data\/updates.txt", "updates": "sandbox/updates.txt",
"log": "data\/log.txt", "log": "data\/log.txt",
"update_check": "data\/lastupdatecheck.txt", "update_check": "data\/lastupdatecheck.txt",
"history": "data\/history.php", "history": "data\/history.php",
@ -59,7 +59,7 @@
"WALLABAG_VERSION": 1 "WALLABAG_VERSION": 1
}, },
"dev": { "dev": {
"debug": true "debug": false
}, },
"updates": { "updates": {
"check_updates": false, "check_updates": false,

View file

@ -68,6 +68,28 @@ <h2 class="window-title">{'Configure'|t}</h2>
</select> </select>
</div> </div>
</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>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-1"> <div class="pure-u-lg-{$ratioLabel} pure-u-1">

View file

@ -11,7 +11,6 @@
<h2 class="window-title"> <h2 class="window-title">
{if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
</h2> </h2>
<input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
{if="isset($link.id)"} {if="isset($link.id)"}
<input type="hidden" name="lf_id" value="{$link.id}"> <input type="hidden" name="lf_id" value="{$link.id}">
{/if} {/if}
@ -20,7 +19,7 @@ <h2 class="window-title">
<label for="lf_url">{'URL'|t}</label> <label for="lf_url">{'URL'|t}</label>
</div> </div>
<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>
<div> <div>
<label for="lf_title">{'Title'|t}</label> <label for="lf_title">{'Title'|t}</label>
@ -50,6 +49,15 @@ <h2 class="window-title">
&nbsp;<label for="lf_private">{'Private'|t}</label> &nbsp;<label for="lf_private">{'Private'|t}</label>
</div> </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"> <div id="editlink-plugins">
{loop="$edit_link_plugin"} {loop="$edit_link_plugin"}
{$value} {$value}

View file

@ -8,6 +8,9 @@
<link href="img/favicon.png" rel="shortcut icon" type="image/png" /> <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 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}" /> <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"} {loop="$plugins_includes.css_files"}
<link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/> <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
{/loop} {/loop}

View file

@ -32,6 +32,19 @@
</td> </td>
</tr> </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> <tr>
<td><b>Timezone:</b></td> <td><b>Timezone:</b></td>
<td> <td>

View file

@ -26,6 +26,15 @@
<input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input" <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> data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" ><br>
{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"} {loop="$edit_link_plugin"}
{$value} {$value}
{/loop} {/loop}
@ -38,7 +47,6 @@
&nbsp;<label for="lf_private"><i>Private</i></label><br><br> &nbsp;<label for="lf_private"><i>Private</i></label><br><br>
{/if} {/if}
<input type="submit" value="Save" name="save_edit" class="bigbutton"> <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)"} {if="!$link_is_new && isset($link.id)"}
<a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}" <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}"
name="delete_link" class="bigbutton" 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 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 href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link type="text/css" rel="stylesheet" href="css/shaarli.min.css" /> <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"} {loop="$plugins_includes.css_files"}
<link type="text/css" rel="stylesheet" href="{$value}#"/> <link type="text/css" rel="stylesheet" href="{$value}#"/>
{/loop} {/loop}

View file

@ -30,6 +30,7 @@ module.exports = [
'./assets/default/js/base.js', './assets/default/js/base.js',
'./assets/default/scss/shaarli.scss', './assets/default/scss/shaarli.scss',
].concat(glob.sync('./assets/default/img/*')), ].concat(glob.sync('./assets/default/img/*')),
markdown: './assets/common/css/markdown.css',
}, },
output: { output: {
filename: '[name].min.js', filename: '[name].min.js',
@ -50,7 +51,7 @@ module.exports = [
} }
}, },
{ {
test: /\.scss/, test: /\.s?css/,
use: extractCssDefault.extract({ use: extractCssDefault.extract({
use: [{ use: [{
loader: "css-loader", loader: "css-loader",
@ -97,6 +98,7 @@ module.exports = [
'./assets/vintage/css/reset.css', './assets/vintage/css/reset.css',
'./assets/vintage/css/shaarli.css', './assets/vintage/css/shaarli.css',
].concat(glob.sync('./assets/vintage/img/*')), ].concat(glob.sync('./assets/vintage/img/*')),
markdown: './assets/common/css/markdown.css',
thumbnails: './assets/common/js/thumbnails.js', thumbnails: './assets/common/js/thumbnails.js',
thumbnails_update: './assets/common/js/thumbnails-update.js', thumbnails_update: './assets/common/js/thumbnails-update.js',
}, },